@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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Who signs. Every write command takes a Signer, so the commands never see a key:
3
+ *
4
+ * browser (default) a local page asks MetaMask, or any injected wallet, to approve each signature (browser.ts)
5
+ * key a 0x-prefixed private key in an environment variable (AGT_OWNER_KEY, or the one --key-env names)
6
+ * keyfile a passphrase-encrypted key file (keyfile.ts), for automation that should not hold a raw key in env
7
+ *
8
+ * A key is never read from a command-line argument, where it would land in shell history.
9
+ */
10
+ import { type Address, type Hex } from "viem";
11
+ import type { Network } from "./config.js";
12
+ export interface TxRequest {
13
+ to: Address;
14
+ data: Hex;
15
+ value?: bigint;
16
+ /** One line shown in the wallet page and the terminal: what this transaction does. */
17
+ description: string;
18
+ }
19
+ export interface Signer {
20
+ kind: "browser" | "key" | "keyfile";
21
+ address: Address;
22
+ /** EIP-191 personal_sign over the UTF-8 message. */
23
+ signMessage(message: string, description: string): Promise<Hex>;
24
+ sendTransaction(tx: TxRequest): Promise<Hex>;
25
+ close(): Promise<void>;
26
+ }
27
+ /**
28
+ * Check a private key's shape before use and say what is wrong in plain words. The traps from the first terminal run:
29
+ * a missing 0x, a passphrase pasted where the key belongs, and an address pasted where the key belongs.
30
+ */
31
+ export declare function parsePrivateKey(raw: string | undefined, source: string): Hex;
32
+ export declare function keySigner(pk: Hex, network: Network, kind?: "key" | "keyfile"): Signer;
package/dist/signer.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Who signs. Every write command takes a Signer, so the commands never see a key:
3
+ *
4
+ * browser (default) a local page asks MetaMask, or any injected wallet, to approve each signature (browser.ts)
5
+ * key a 0x-prefixed private key in an environment variable (AGT_OWNER_KEY, or the one --key-env names)
6
+ * keyfile a passphrase-encrypted key file (keyfile.ts), for automation that should not hold a raw key in env
7
+ *
8
+ * A key is never read from a command-line argument, where it would land in shell history.
9
+ */
10
+ import { createWalletClient, http } from "viem";
11
+ import { privateKeyToAccount } from "viem/accounts";
12
+ /**
13
+ * Check a private key's shape before use and say what is wrong in plain words. The traps from the first terminal run:
14
+ * a missing 0x, a passphrase pasted where the key belongs, and an address pasted where the key belongs.
15
+ */
16
+ export function parsePrivateKey(raw, source) {
17
+ const v = (raw ?? "").trim();
18
+ if (!v)
19
+ throw new Error(`${source} is empty. Set it to the name owner's private key (0x followed by 64 hex characters).`);
20
+ if (/^0x[0-9a-fA-F]{64}$/.test(v))
21
+ return v;
22
+ if (/^[0-9a-fA-F]{64}$/.test(v))
23
+ throw new Error(`${source} is missing the 0x prefix. Use 0x${v.slice(0, 4)}… (0x followed by the 64 hex characters).`);
24
+ if (/^0x[0-9a-fA-F]{40}$/.test(v))
25
+ throw new Error(`${source} holds an address, not a private key. The key is 64 hex characters; the address is 40.`);
26
+ if (/^0x[0-9a-fA-F]*$/.test(v))
27
+ throw new Error(`${source} is ${v.length - 2} hex characters after 0x; a private key is exactly 64.`);
28
+ throw new Error(`${source} is not a private key. If this is your keyfile passphrase, pass the file with --keyfile and the passphrase in AGT_KEYFILE_PASSPHRASE instead.`);
29
+ }
30
+ export function keySigner(pk, network, kind = "key") {
31
+ const account = privateKeyToAccount(pk);
32
+ const wallet = createWalletClient({ account, chain: network.chain, transport: http(network.rpcUrls[0]) });
33
+ return {
34
+ kind,
35
+ address: account.address,
36
+ signMessage: (message) => account.signMessage({ message }),
37
+ sendTransaction: (tx) => wallet.sendTransaction({ to: tx.to, data: tx.data, value: tx.value ?? 0n }),
38
+ close: async () => { },
39
+ };
40
+ }
package/dist/tui.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ export interface Key {
2
+ name?: string;
3
+ ctrl?: boolean;
4
+ meta?: boolean;
5
+ shift?: boolean;
6
+ sequence?: string;
7
+ }
8
+ export interface Frame {
9
+ lines: string[];
10
+ cursor: {
11
+ row: number;
12
+ col: number;
13
+ };
14
+ }
15
+ export interface Widget<T> {
16
+ render(): Frame;
17
+ /** Return { done: value } to finish; anything else redraws. */
18
+ onKey(str: string | undefined, key: Key): {
19
+ done: T;
20
+ } | void | Promise<{
21
+ done: T;
22
+ } | void>;
23
+ /** The one line left on screen after the widget finishes. */
24
+ summary(value: T): string;
25
+ /** Shown when the step is skipped. */
26
+ label?: string;
27
+ /** Called with a redraw function, for widgets that change on their own (timers, background data). */
28
+ attach?(redraw: () => void): void;
29
+ detach?(): void;
30
+ }
31
+ export declare class CancelledError extends Error {
32
+ constructor();
33
+ }
34
+ /** Step navigation a widget can return instead of a value: `j` goes back a step, `;` skips it. */
35
+ export declare const BACK: unique symbol;
36
+ export declare const SKIP: unique symbol;
37
+ export type Nav = typeof BACK | typeof SKIP;
38
+ export declare const isNav: (v: unknown) => v is Nav;
39
+ /** Erase the last `n` printed lines (a finished step's summary) so a step re-opens in place. */
40
+ export declare function eraseLines(n: number): void;
41
+ export declare const style: {
42
+ bold: (s: string) => string;
43
+ dim: (s: string) => string;
44
+ italic: (s: string) => string;
45
+ cyan: (s: string) => string;
46
+ green: (s: string) => string;
47
+ red: (s: string) => string;
48
+ yellow: (s: string) => string;
49
+ inverse: (s: string) => string;
50
+ /** A grey from the 256-colour ramp, 232 (black) … 255 (white); used for the examples fade. */
51
+ grey: (level: number) => (s: string) => string;
52
+ };
53
+ export declare const visibleLength: (s: string) => number;
54
+ /** Cut a styled line to `width` visible characters, keeping escape codes intact. */
55
+ export declare function truncate(s: string, width: number): string;
56
+ export declare function isInteractive(): boolean;
57
+ export declare function run<T>(widget: Widget<T | Nav>): Promise<T | Nav>;
58
+ /**
59
+ * Copy text: OSC 52 (understood by Windows Terminal, iTerm2, kitty, most modern terminals, and over SSH) plus the
60
+ * platform's clipboard tool. Either one is enough; failures are silent.
61
+ */
62
+ export declare function copyToClipboard(text: string): void;
package/dist/tui.js ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The terminal layer for interactive prompts (RATIONALE R-1). A widget renders a block of lines plus a cursor
3
+ * position; the runner redraws the whole block on every change and parks the cursor exactly there, so faint text can
4
+ * sit after the cursor (RATIONALE R-5). Lines are cut to the terminal width so nothing wraps and breaks the redraw.
5
+ */
6
+ import { spawn } from "node:child_process";
7
+ import { emitKeypressEvents } from "node:readline";
8
+ export class CancelledError extends Error {
9
+ constructor() { super("cancelled"); this.name = "CancelledError"; }
10
+ }
11
+ /** Step navigation a widget can return instead of a value: `j` goes back a step, `;` skips it. */
12
+ export const BACK = Symbol("back");
13
+ export const SKIP = Symbol("skip");
14
+ export const isNav = (v) => v === BACK || v === SKIP;
15
+ /** Erase the last `n` printed lines (a finished step's summary) so a step re-opens in place. */
16
+ export function eraseLines(n) {
17
+ if (n > 0 && process.stdout.isTTY)
18
+ process.stdout.write(`\x1b[${n}A\r\x1b[J`);
19
+ }
20
+ // ── styles ────────────────────────────────────────────────────────────────────────────────────────────────────────
21
+ const color = process.env.NO_COLOR === undefined && process.stdout.isTTY;
22
+ const wrap = (open, close) => (s) => (color ? `\x1b[${open}m${s}\x1b[${close}m` : s);
23
+ export const style = {
24
+ bold: wrap("1", "22"),
25
+ dim: wrap("2", "22"),
26
+ italic: wrap("3", "23"),
27
+ cyan: wrap("36", "39"),
28
+ green: wrap("32", "39"),
29
+ red: wrap("31", "39"),
30
+ yellow: wrap("33", "39"),
31
+ inverse: wrap("7", "27"),
32
+ /** A grey from the 256-colour ramp, 232 (black) … 255 (white); used for the examples fade. */
33
+ grey: (level) => (s) => (color ? `\x1b[38;5;${level}m${s}\x1b[39m` : s),
34
+ };
35
+ const ANSI = /\x1b\[[0-9;]*m|\x1b\]52;[^\x07]*\x07/g;
36
+ export const visibleLength = (s) => s.replace(ANSI, "").length;
37
+ /** Cut a styled line to `width` visible characters, keeping escape codes intact. */
38
+ export function truncate(s, width) {
39
+ if (visibleLength(s) <= width)
40
+ return s;
41
+ let out = "";
42
+ let seen = 0;
43
+ let i = 0;
44
+ while (i < s.length && seen < width - 1) {
45
+ const m = s[i] === "\x1b" ? /^\x1b\[[0-9;]*m/.exec(s.slice(i)) : null;
46
+ if (m) {
47
+ out += m[0];
48
+ i += m[0].length;
49
+ continue;
50
+ }
51
+ out += s[i];
52
+ seen++;
53
+ i++;
54
+ }
55
+ return out + "…" + (color ? "\x1b[0m" : "");
56
+ }
57
+ // ── runner ────────────────────────────────────────────────────────────────────────────────────────────────────────
58
+ let keypressReady = false;
59
+ export function isInteractive() {
60
+ return !!(process.stdin.isTTY && process.stdout.isTTY);
61
+ }
62
+ export function run(widget) {
63
+ const out = process.stdout;
64
+ const stdin = process.stdin;
65
+ if (!keypressReady) {
66
+ emitKeypressEvents(stdin);
67
+ keypressReady = true;
68
+ }
69
+ let cursorRow = 0; // row the cursor sits on, within the block
70
+ let finished = false;
71
+ const draw = () => {
72
+ if (finished)
73
+ return;
74
+ const width = Math.max(20, (out.columns ?? 80) - 1);
75
+ const f = widget.render();
76
+ const lines = f.lines.map((l) => truncate(l, width));
77
+ let s = "\x1b[?25l"; // hide cursor while drawing
78
+ if (cursorRow > 0)
79
+ s += `\x1b[${cursorRow}A`;
80
+ s += "\r\x1b[J" + lines.join("\n");
81
+ const up = lines.length - 1 - f.cursor.row;
82
+ if (up > 0)
83
+ s += `\x1b[${up}A`;
84
+ s += "\r" + (f.cursor.col > 0 ? `\x1b[${Math.min(f.cursor.col, width)}C` : "") + "\x1b[?25h";
85
+ out.write(s);
86
+ cursorRow = f.cursor.row;
87
+ };
88
+ return new Promise((resolve, reject) => {
89
+ const finish = (err, value) => {
90
+ if (finished)
91
+ return;
92
+ widget.detach?.();
93
+ stdin.removeListener("keypress", onKey);
94
+ out.removeListener("resize", draw);
95
+ // Replace the block with the one-line summary.
96
+ let s = cursorRow > 0 ? `\x1b[${cursorRow}A` : "";
97
+ const width = Math.max(20, (out.columns ?? 80) - 1);
98
+ // Back: leave nothing, the previous step re-opens where this one was. Skip: one dim line. Done: the summary.
99
+ if (value === BACK && !err)
100
+ s += "\r\x1b[J";
101
+ else
102
+ s += "\r\x1b[J" + truncate(err ? style.dim("✗ cancelled") : value === SKIP ? style.dim(`↷ ${widget.label ?? "step"} skipped`) : widget.summary(value), width) + "\n";
103
+ out.write(s);
104
+ finished = true;
105
+ if (stdin.isTTY)
106
+ stdin.setRawMode(false);
107
+ stdin.pause();
108
+ if (err)
109
+ reject(err);
110
+ else
111
+ resolve(value);
112
+ };
113
+ const onKey = async (str, key = {}) => {
114
+ if (key.ctrl && key.name === "c")
115
+ return finish(new CancelledError());
116
+ try {
117
+ const r = await widget.onKey(str, key);
118
+ if (r && "done" in r)
119
+ return finish(null, r.done);
120
+ draw();
121
+ }
122
+ catch (e) {
123
+ finish(e);
124
+ }
125
+ };
126
+ if (stdin.isTTY)
127
+ stdin.setRawMode(true);
128
+ stdin.resume();
129
+ stdin.on("keypress", onKey);
130
+ out.on("resize", draw);
131
+ widget.attach?.(draw);
132
+ draw();
133
+ });
134
+ }
135
+ // ── clipboard ─────────────────────────────────────────────────────────────────────────────────────────────────────
136
+ /**
137
+ * Copy text: OSC 52 (understood by Windows Terminal, iTerm2, kitty, most modern terminals, and over SSH) plus the
138
+ * platform's clipboard tool. Either one is enough; failures are silent.
139
+ */
140
+ export function copyToClipboard(text) {
141
+ if (process.stdout.isTTY)
142
+ process.stdout.write(`\x1b]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`);
143
+ const [cmd, args] = process.platform === "win32" ? ["clip", []]
144
+ : process.platform === "darwin" ? ["pbcopy", []]
145
+ : process.env.WAYLAND_DISPLAY ? ["wl-copy", []] : ["xclip", ["-selection", "clipboard"]];
146
+ try {
147
+ const p = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
148
+ p.on("error", () => { });
149
+ p.stdin.on("error", () => { });
150
+ p.stdin.end(text);
151
+ }
152
+ catch { /* OSC 52 already sent */ }
153
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * verify: the three-way check (manifest signer == manifest owner == on-chain owner) from @agtnames/resolver, plus a
3
+ * liveness probe of every on-chain endpoint. An endpoint is live when `<origin>/health` answers 2xx, or, failing
4
+ * that, when the endpoint URL itself answers below 500 (an MCP endpoint answers GET with 405 or 406, which is alive).
5
+ */
6
+ import { type AgentResolution } from "@agtnames/resolver";
7
+ import type { Network } from "./config.js";
8
+ export interface EndpointCheck {
9
+ protocol: string;
10
+ url: string;
11
+ live: boolean;
12
+ via: "health" | "endpoint" | null;
13
+ status: number | null;
14
+ error?: string;
15
+ }
16
+ export interface VerifyReport {
17
+ name: string;
18
+ ok: boolean;
19
+ verified: boolean;
20
+ manifestStatus: AgentResolution["manifestStatus"];
21
+ owner: string | null;
22
+ signer: string | null;
23
+ manifestUri: string;
24
+ reasons: string[];
25
+ endpoints: EndpointCheck[];
26
+ problems: string[];
27
+ }
28
+ export declare function checkEndpoint(protocol: string, url: string, fetchImpl?: typeof fetch, timeoutMs?: number): Promise<EndpointCheck>;
29
+ export declare function verifyName(n: Network, label: string, opts?: {
30
+ fetchImpl?: typeof fetch;
31
+ skipEndpoints?: boolean;
32
+ }): Promise<VerifyReport>;
package/dist/verify.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * verify: the three-way check (manifest signer == manifest owner == on-chain owner) from @agtnames/resolver, plus a
3
+ * liveness probe of every on-chain endpoint. An endpoint is live when `<origin>/health` answers 2xx, or, failing
4
+ * that, when the endpoint URL itself answers below 500 (an MCP endpoint answers GET with 405 or 406, which is alive).
5
+ */
6
+ import { AgtResolver } from "@agtnames/resolver";
7
+ async function probe(url, fetchImpl, timeoutMs) {
8
+ try {
9
+ const r = await fetchImpl(url, { method: "GET", redirect: "follow", signal: AbortSignal.timeout(timeoutMs), headers: { accept: "application/json, text/event-stream, */*" } });
10
+ await r.body?.cancel().catch(() => { });
11
+ return r.status;
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export async function checkEndpoint(protocol, url, fetchImpl = fetch, timeoutMs = 10_000) {
18
+ let origin;
19
+ try {
20
+ origin = new URL(url).origin;
21
+ }
22
+ catch {
23
+ return { protocol, url, live: false, via: null, status: null, error: "not a URL" };
24
+ }
25
+ const h = await probe(`${origin}/health`, fetchImpl, timeoutMs);
26
+ if (h !== null && h >= 200 && h < 300)
27
+ return { protocol, url, live: true, via: "health", status: h };
28
+ const e = await probe(url, fetchImpl, timeoutMs);
29
+ if (e !== null && e < 500)
30
+ return { protocol, url, live: true, via: "endpoint", status: e };
31
+ return { protocol, url, live: false, via: null, status: e ?? h, error: e === null && h === null ? "no response" : `HTTP ${e ?? h}` };
32
+ }
33
+ export async function verifyName(n, label, opts = {}) {
34
+ const resolver = new AgtResolver({ chain: n.name, rpcUrls: n.rpcUrls, registry: n.registry });
35
+ const r = await resolver.resolveAgent(`${label}.agt`);
36
+ const onchain = Object.entries(r.records.endpoints);
37
+ const endpoints = opts.skipEndpoints ? [] : await Promise.all(onchain.map(([p, u]) => checkEndpoint(p, u, opts.fetchImpl)));
38
+ const problems = [];
39
+ if (!r.registered)
40
+ problems.push("not registered");
41
+ else if (!r.active)
42
+ problems.push("registered but not active (expired?)");
43
+ if (r.registered && r.manifestStatus === "none")
44
+ problems.push("no manifest published (run `agt manifest publish`)");
45
+ if (r.manifestStatus === "unavailable")
46
+ problems.push("the manifest pointer is set but no gateway returned the document (transport; retry later)");
47
+ if (r.manifestStatus === "unverified")
48
+ problems.push(`manifest does not verify: ${r.reasons.join("; ")}`);
49
+ const declared = new Map((r.manifest?.endpoints ?? []).map((e) => [e.protocol, e.url]));
50
+ for (const [p, u] of declared)
51
+ if (r.records.endpoints[p] !== undefined && r.records.endpoints[p] !== u)
52
+ problems.push(`endpoint ${p}: manifest says ${u}, on-chain record says ${r.records.endpoints[p]}`);
53
+ for (const [p, u] of declared)
54
+ if (r.records.endpoints[p] === undefined && ["mcp", "a2a", "http", "ws"].includes(p))
55
+ problems.push(`endpoint ${p} (${u}) is in the manifest but not on-chain (run \`agt records set\`)`);
56
+ for (const e of endpoints)
57
+ if (!e.live)
58
+ problems.push(`endpoint ${e.protocol} ${e.url} is not reachable (${e.error})`);
59
+ return {
60
+ name: `${label}.agt`,
61
+ ok: problems.length === 0 && r.verified,
62
+ verified: r.verified,
63
+ manifestStatus: r.manifestStatus,
64
+ owner: r.owner,
65
+ signer: r.signer,
66
+ manifestUri: r.records.manifestUri,
67
+ reasons: r.reasons,
68
+ endpoints,
69
+ problems,
70
+ };
71
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Prompt widgets. The placeholder rules (RATIONALE R-5, R-6):
3
+ * prefill a prediction from real data, dim after the cursor; Tab fills it, Enter submits. Typing over it never
4
+ * loses it: it stays on the hint line, ↑ puts it back, ^Y copies it.
5
+ * instruction says what to enter; in the field when there is no prefill, otherwise on the label line. Always primary.
6
+ * examples only on an empty field after ~3 s idle, fading in italic below it; never fillable.
7
+ *
8
+ * Step navigation: `j` goes back a step and `;` skips it. In lists they are plain keys; in a typing field they act
9
+ * only while the field is empty, so a value containing j or ; types normally.
10
+ */
11
+ import { type Frame, type Key, type Nav, type Widget } from "./tui.js";
12
+ export interface TextOptions {
13
+ label: string;
14
+ instruction: string;
15
+ prefill?: string;
16
+ /** Where the prefill came from, shown beside it ("from your MCP server"). */
17
+ prefillSource?: string;
18
+ examples?: string[];
19
+ initial?: string;
20
+ optional?: boolean;
21
+ /** Soft length guide: a counter, red past the limit. */
22
+ maxLength?: number;
23
+ /** Error message, or null when fine. */
24
+ validate?: (v: string) => string | null;
25
+ /** Live note under the field (e.g. "✓ live"), recomputed on every redraw. */
26
+ note?: () => string | null;
27
+ /** j / ; on an empty field go back / skip (default true). Off for fields embedded in another widget. */
28
+ nav?: boolean;
29
+ }
30
+ export declare const NAV_HINT = "j back \u00B7 ; skip";
31
+ export declare class TextInput implements Widget<string | Nav> {
32
+ private o;
33
+ value: string;
34
+ pos: number;
35
+ private error;
36
+ private flash;
37
+ private lastKey;
38
+ private timer?;
39
+ get label(): string;
40
+ constructor(o: TextOptions);
41
+ private get prefill();
42
+ attach(redraw: () => void): void;
43
+ detach(): void;
44
+ private say;
45
+ render(): Frame;
46
+ onKey(str: string | undefined, key: Key): {
47
+ done: string | Nav;
48
+ } | void;
49
+ summary(v: string | Nav): string;
50
+ }
51
+ export interface Choice<T> {
52
+ value: T;
53
+ label: string;
54
+ description?: string;
55
+ }
56
+ export declare class Select<T> implements Widget<T | Nav> {
57
+ private o;
58
+ private i;
59
+ get label(): string;
60
+ constructor(o: {
61
+ label: string;
62
+ instruction: string;
63
+ choices: Choice<T>[];
64
+ initial?: T;
65
+ prefillSource?: string;
66
+ });
67
+ render(): Frame;
68
+ onKey(str: string | undefined, key: Key): {
69
+ done: T | Nav;
70
+ } | void;
71
+ summary(v: T | Nav): string;
72
+ }
73
+ export declare class Checkbox<T> implements Widget<T[] | Nav> {
74
+ private o;
75
+ private i;
76
+ get label(): string;
77
+ private on;
78
+ private error;
79
+ constructor(o: {
80
+ label: string;
81
+ instruction: string;
82
+ choices: Choice<T>[];
83
+ initial?: T[];
84
+ min?: number;
85
+ });
86
+ render(): Frame;
87
+ onKey(str: string | undefined, key: Key): {
88
+ done: T[] | Nav;
89
+ } | void;
90
+ summary(v: T[] | Nav): string;
91
+ }