@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,55 @@
1
+ /**
2
+ * The capability picker. Ranked results (best match first and highlighted, @agtnames/resolver rankCapabilities), a
3
+ * fixed viewport with "N more", the highlighted item's description on its own line, suggestions read from the
4
+ * agent's own tools pre-ticked and labelled with where they came from, a category drill-down, and custom ids with
5
+ * "did you mean" (spec §4.2: custom ids allowed, lowercase hyphenated).
6
+ */
7
+ import { type CapabilityVocabulary } from "@agtnames/resolver";
8
+ import { type Frame, type Key, type Nav, type Widget } from "./tui.js";
9
+ export interface CapabilitySuggestion {
10
+ id: string;
11
+ from: string[];
12
+ }
13
+ export interface CapabilitySource {
14
+ vocabulary(): CapabilityVocabulary;
15
+ usage(): Record<string, number>;
16
+ suggestions(): CapabilitySuggestion[];
17
+ /** Register a callback for when background data (vocabulary refresh, endpoint probes) arrives. */
18
+ onChange(cb: () => void): (() => void) | void;
19
+ }
20
+ export declare class CapabilityPicker implements Widget<string[] | Nav> {
21
+ private src;
22
+ private o;
23
+ get label(): string;
24
+ private query;
25
+ private selected;
26
+ private i;
27
+ private top;
28
+ private mode;
29
+ private category;
30
+ private catIndex;
31
+ private flash;
32
+ private seenSuggestions;
33
+ constructor(src: CapabilitySource, o: {
34
+ label: string;
35
+ initial?: string[];
36
+ });
37
+ /** New suggestions are pre-ticked once; unticking one sticks. */
38
+ private absorbSuggestions;
39
+ private off?;
40
+ attach(redraw: () => void): void;
41
+ detach(): void;
42
+ private capOf;
43
+ private uses;
44
+ private rows;
45
+ private selectable;
46
+ private clampCursor;
47
+ private move;
48
+ private toggle;
49
+ private topMatch;
50
+ render(): Frame;
51
+ onKey(str: string | undefined, key: Key): {
52
+ done: string[] | Nav;
53
+ } | void;
54
+ summary(v: string[] | Nav): string;
55
+ }
package/dist/picker.js ADDED
@@ -0,0 +1,330 @@
1
+ /**
2
+ * The capability picker. Ranked results (best match first and highlighted, @agtnames/resolver rankCapabilities), a
3
+ * fixed viewport with "N more", the highlighted item's description on its own line, suggestions read from the
4
+ * agent's own tools pre-ticked and labelled with where they came from, a category drill-down, and custom ids with
5
+ * "did you mean" (spec §4.2: custom ids allowed, lowercase hyphenated).
6
+ */
7
+ import { nearestCapability, rankCapabilities, toCapabilityId } from "@agtnames/resolver";
8
+ import { BACK, copyToClipboard, SKIP, style } from "./tui.js";
9
+ const VIEW = 8;
10
+ export class CapabilityPicker {
11
+ src;
12
+ o;
13
+ get label() { return this.o.label; }
14
+ query = "";
15
+ selected;
16
+ i = 0;
17
+ top = 0;
18
+ mode = "search";
19
+ category = null;
20
+ catIndex = 0;
21
+ flash = null;
22
+ seenSuggestions = new Set();
23
+ constructor(src, o) {
24
+ this.src = src;
25
+ this.o = o;
26
+ this.selected = [...(o.initial ?? [])];
27
+ this.absorbSuggestions();
28
+ }
29
+ /** New suggestions are pre-ticked once; unticking one sticks. */
30
+ absorbSuggestions() {
31
+ for (const s of this.src.suggestions()) {
32
+ if (this.seenSuggestions.has(s.id))
33
+ continue;
34
+ this.seenSuggestions.add(s.id);
35
+ if (!this.selected.includes(s.id))
36
+ this.selected.push(s.id);
37
+ }
38
+ }
39
+ off;
40
+ attach(redraw) { this.off = this.src.onChange(() => { this.absorbSuggestions(); redraw(); }); }
41
+ detach() { if (this.off)
42
+ this.off(); }
43
+ capOf(id) { return this.src.vocabulary().capabilities.find((c) => c.id === id); }
44
+ uses(id) { return this.src.usage()[id] ?? 0; }
45
+ rows() {
46
+ const vocab = this.src.vocabulary();
47
+ const usage = this.src.usage();
48
+ if (this.mode === "categories") {
49
+ return vocab.categories.map((c) => ({ kind: "category", id: c.id, label: c.label, count: vocab.capabilities.filter((x) => x.category === c.id && !x.deprecated).length }));
50
+ }
51
+ if (this.mode === "category") {
52
+ return vocab.capabilities.filter((c) => c.category === this.category && !c.deprecated).sort((a, b) => (usage[b.id] ?? 0) - (usage[a.id] ?? 0)).map((cap) => ({ kind: "cap", cap }));
53
+ }
54
+ const q = this.query.trim();
55
+ if (!q) {
56
+ const sugg = this.src.suggestions();
57
+ const rows = [];
58
+ if (sugg.length) {
59
+ rows.push({ kind: "heading", text: "Suggested from your agent's own description" });
60
+ for (const s of sugg) {
61
+ const cap = this.capOf(s.id);
62
+ if (cap)
63
+ rows.push({ kind: "cap", cap, from: s.from });
64
+ }
65
+ }
66
+ const others = this.selected.filter((id) => !sugg.some((s) => s.id === id));
67
+ if (others.length) {
68
+ rows.push({ kind: "heading", text: "Selected" });
69
+ for (const id of others) {
70
+ const cap = this.capOf(id);
71
+ rows.push(cap ? { kind: "cap", cap } : { kind: "custom", id });
72
+ }
73
+ }
74
+ rows.push({ kind: "heading", text: "Most used" });
75
+ const shown = new Set(rows.flatMap((r) => (r.kind === "cap" ? [r.cap.id] : [])));
76
+ for (const r of rankCapabilities("", { vocabulary: vocab, usage }))
77
+ if (!shown.has(r.capability.id))
78
+ rows.push({ kind: "cap", cap: r.capability });
79
+ return rows;
80
+ }
81
+ const ranked = rankCapabilities(q, { vocabulary: vocab, usage });
82
+ const rows = ranked.map((r) => ({ kind: "cap", cap: r.capability }));
83
+ const id = toCapabilityId(q);
84
+ const exact = ranked.some((r) => r.capability.id === id);
85
+ if (id && !exact) {
86
+ const near = nearestCapability(id, vocab);
87
+ if (near && !ranked.slice(0, 3).some((r) => r.capability.id === near.id))
88
+ rows.unshift({ kind: "cap", cap: near, from: [`did you mean ${near.id}?`] });
89
+ rows.push({ kind: "custom", id, known: near ?? undefined });
90
+ }
91
+ return rows;
92
+ }
93
+ selectable = (r) => r.kind !== "heading";
94
+ clampCursor(rows) {
95
+ if (!rows.length) {
96
+ this.i = 0;
97
+ return;
98
+ }
99
+ this.i = Math.max(0, Math.min(this.i, rows.length - 1));
100
+ if (!this.selectable(rows[this.i])) {
101
+ const next = rows.findIndex((r, k) => k > this.i && this.selectable(r));
102
+ this.i = next >= 0 ? next : rows.findIndex(this.selectable);
103
+ }
104
+ if (this.i < this.top)
105
+ this.top = this.i;
106
+ if (this.i >= this.top + VIEW)
107
+ this.top = this.i - VIEW + 1;
108
+ this.top = Math.max(0, Math.min(this.top, Math.max(0, rows.length - VIEW)));
109
+ if (this.top > 0 && rows[this.top - 1]?.kind === "heading" && this.i - (this.top - 1) < VIEW)
110
+ this.top--;
111
+ }
112
+ move(rows, d) {
113
+ let k = this.i;
114
+ for (let n = 0; n < rows.length; n++) {
115
+ k = (k + d + rows.length) % rows.length;
116
+ if (this.selectable(rows[k])) {
117
+ this.i = k;
118
+ return;
119
+ }
120
+ }
121
+ }
122
+ toggle(id) {
123
+ const at = this.selected.indexOf(id);
124
+ if (at >= 0)
125
+ this.selected.splice(at, 1);
126
+ else
127
+ this.selected.push(id);
128
+ }
129
+ topMatch() {
130
+ const q = this.query.trim();
131
+ return q ? rankCapabilities(q, { vocabulary: this.src.vocabulary(), usage: this.src.usage(), limit: 1 })[0]?.capability : undefined;
132
+ }
133
+ render() {
134
+ const rows = this.rows();
135
+ this.clampCursor(rows);
136
+ const vocab = this.src.vocabulary();
137
+ const lines = [];
138
+ const where = this.mode === "categories" ? style.dim(" · categories") : this.mode === "category" ? style.dim(` · ${vocab.categories.find((c) => c.id === this.category)?.label ?? ""}`) : "";
139
+ lines.push(`${style.cyan("?")} ${style.bold(this.o.label)} Choose what your agent can do${where}`);
140
+ // Input line: the instruction sits in the field until you type; then the top match completes dimly after the cursor.
141
+ const tm = this.topMatch();
142
+ let field;
143
+ if (this.mode !== "search")
144
+ field = style.dim("← back to search");
145
+ else if (!this.query)
146
+ field = style.dim("Search capabilities");
147
+ else
148
+ field = this.query + (tm && tm.id.startsWith(this.query.toLowerCase()) && tm.id.length > this.query.length ? style.dim(tm.id.slice(this.query.length)) : "");
149
+ lines.push("› " + field);
150
+ const cursorCol = 2 + (this.mode === "search" ? this.query.length : 0);
151
+ const view = rows.slice(this.top, this.top + VIEW);
152
+ if (this.top > 0)
153
+ lines.push(style.dim(` ↑ ${this.top} more`));
154
+ view.forEach((r, k) => {
155
+ const idx = this.top + k;
156
+ const on = idx === this.i;
157
+ const ptr = on ? style.cyan("❯") : " ";
158
+ if (r.kind === "heading") {
159
+ lines.push(" " + style.dim(r.text));
160
+ return;
161
+ }
162
+ if (r.kind === "category") {
163
+ lines.push(`${ptr} ${on ? style.bold(r.label) : r.label} ${style.dim(`(${r.count})`)} ${style.dim("→")}`);
164
+ return;
165
+ }
166
+ if (r.kind === "custom") {
167
+ const sel = this.selected.includes(r.id);
168
+ lines.push(`${ptr} ${sel ? style.green("[x]") : "[ ]"} ${on ? style.bold(r.id) : r.id} ${style.yellow("custom")}${sel ? "" : style.dim(" Enter adds it as your own capability")}`);
169
+ return;
170
+ }
171
+ const sel = this.selected.includes(r.cap.id);
172
+ const n = this.uses(r.cap.id);
173
+ const from = r.from?.length ? style.dim(` ← ${r.from.slice(0, 2).join(", ")}${r.from.length > 2 ? "…" : ""}`) : "";
174
+ lines.push(`${ptr} ${sel ? style.green("[x]") : "[ ]"} ${on ? style.bold(r.cap.id.padEnd(22)) : r.cap.id.padEnd(22)} ${style.dim(n ? `${n} agent${n === 1 ? "" : "s"}` : "")}${from}`);
175
+ });
176
+ const below = rows.length - (this.top + VIEW);
177
+ if (below > 0)
178
+ lines.push(style.dim(` ↓ ${below} more`));
179
+ if (!rows.length)
180
+ lines.push(style.dim(" no matches"));
181
+ // Detail of the highlighted row, on its own line so the list never has to cram it.
182
+ const cur = rows[this.i];
183
+ if (cur?.kind === "cap")
184
+ lines.push(` ${style.bold(cur.cap.label)}: ${cur.cap.description} ${style.dim(`· ${vocab.categories.find((c) => c.id === cur.cap.category)?.label ?? cur.cap.category}`)}`);
185
+ else if (cur?.kind === "custom")
186
+ lines.push(` ${style.yellow("custom")}: not in the shared list. Other agents searching for it won't find it unless they use the same id.${cur.known ? ` Closest shared term: ${cur.known.id}.` : ""}`);
187
+ else if (cur?.kind === "category")
188
+ lines.push(` ${vocab.categories.find((c) => c.id === cur.id)?.description ?? ""}`);
189
+ else
190
+ lines.push("");
191
+ lines.push(` ${style.dim("Selected:")} ${this.selected.length ? style.cyan(this.selected.join(", ")) : style.dim("none yet")}${this.flash ? " " + style.green(this.flash) : ""}`);
192
+ const hint = this.mode !== "search"
193
+ ? "↑/↓ move · Space or Enter toggles · ← categories · Esc search · j back · ; skip"
194
+ : this.query
195
+ ? "↑/↓ move · Enter toggles the highlighted row · Tab completes · Esc clears · ^Y copies the id"
196
+ : "type to search · ↑/↓ move · Space toggles · → categories · Enter when done · ^Y copy id · j back · ; skip";
197
+ lines.push(style.dim(" " + hint));
198
+ return { lines, cursor: { row: 1, col: cursorCol } };
199
+ }
200
+ onKey(str, key) {
201
+ this.flash = null;
202
+ if (!this.query && !key.ctrl && !key.meta) {
203
+ if (str === "j")
204
+ return { done: BACK };
205
+ if (str === ";")
206
+ return { done: SKIP };
207
+ }
208
+ const rows = this.rows();
209
+ this.clampCursor(rows);
210
+ const cur = rows[this.i];
211
+ const idOf = (r) => (r?.kind === "cap" ? r.cap.id : r?.kind === "custom" ? r.id : null);
212
+ if (key.ctrl && key.name === "y") {
213
+ const id = idOf(cur);
214
+ if (id) {
215
+ copyToClipboard(id);
216
+ this.flash = `copied ${id}`;
217
+ }
218
+ return;
219
+ }
220
+ if (key.name === "up")
221
+ return this.move(rows, -1);
222
+ if (key.name === "down")
223
+ return this.move(rows, 1);
224
+ if (key.name === "pageup") {
225
+ this.i = Math.max(0, this.i - VIEW);
226
+ return;
227
+ }
228
+ if (key.name === "pagedown") {
229
+ this.i = Math.min(rows.length - 1, this.i + VIEW);
230
+ return;
231
+ }
232
+ if (this.mode === "categories") {
233
+ if (key.name === "escape" || key.name === "left") {
234
+ this.mode = "search";
235
+ this.i = 0;
236
+ this.top = 0;
237
+ return;
238
+ }
239
+ if ((key.name === "return" || key.name === "right") && cur?.kind === "category") {
240
+ this.catIndex = this.i;
241
+ this.category = cur.id;
242
+ this.mode = "category";
243
+ this.i = 0;
244
+ this.top = 0;
245
+ }
246
+ return;
247
+ }
248
+ if (this.mode === "category") {
249
+ if (key.name === "left") {
250
+ this.mode = "categories";
251
+ this.i = this.catIndex;
252
+ this.top = 0;
253
+ return;
254
+ }
255
+ if (key.name === "escape") {
256
+ this.mode = "search";
257
+ this.i = 0;
258
+ this.top = 0;
259
+ return;
260
+ }
261
+ if (key.name === "space" || str === " " || key.name === "return") {
262
+ const id = idOf(cur);
263
+ if (id)
264
+ this.toggle(id);
265
+ }
266
+ return;
267
+ }
268
+ // search mode
269
+ if (key.name === "escape") {
270
+ this.query = "";
271
+ this.i = 0;
272
+ this.top = 0;
273
+ return;
274
+ }
275
+ if (key.name === "right" && !this.query) {
276
+ this.mode = "categories";
277
+ this.i = 0;
278
+ this.top = 0;
279
+ return;
280
+ }
281
+ if (key.name === "tab") {
282
+ const tm = this.topMatch();
283
+ if (tm)
284
+ this.query = tm.id;
285
+ this.i = 0;
286
+ return;
287
+ }
288
+ if (key.name === "return" || key.name === "enter") {
289
+ if (!this.query.trim())
290
+ return { done: [...this.selected] };
291
+ const id = idOf(cur);
292
+ if (id) {
293
+ this.toggle(id);
294
+ this.flash = this.selected.includes(id) ? `added ${id}` : `removed ${id}`;
295
+ }
296
+ this.query = "";
297
+ this.i = 0;
298
+ this.top = 0;
299
+ return;
300
+ }
301
+ if ((key.name === "space" || str === " ") && !this.query) {
302
+ const id = idOf(cur);
303
+ if (id)
304
+ this.toggle(id);
305
+ return;
306
+ }
307
+ if (key.name === "backspace") {
308
+ this.query = this.query.slice(0, -1);
309
+ this.i = 0;
310
+ this.top = 0;
311
+ return;
312
+ }
313
+ if (key.ctrl && key.name === "u") {
314
+ this.query = "";
315
+ this.i = 0;
316
+ this.top = 0;
317
+ return;
318
+ }
319
+ if (str && !key.ctrl && !key.meta && !/[\x00-\x1f\x7f]/.test(str)) {
320
+ this.query += str;
321
+ this.i = 0;
322
+ this.top = 0;
323
+ }
324
+ }
325
+ summary(v) {
326
+ if (!Array.isArray(v))
327
+ return "";
328
+ return `${style.green("✔")} ${style.bold(this.o.label)} ${v.length ? style.cyan(v.join(", ")) : style.dim("(none)")}`;
329
+ }
330
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Read what an agent says about itself (RATIONALE R-3): an MCP server's name, instructions and tools over Streamable
3
+ * HTTP, or an A2A agent card's description and skills. Read-only, short timeouts, and only for endpoints the user
4
+ * just entered. The result feeds the description prefill and the capability suggestions.
5
+ */
6
+ export interface SelfDescription {
7
+ protocol: string;
8
+ url: string;
9
+ live: boolean;
10
+ name?: string;
11
+ description?: string;
12
+ items: {
13
+ kind: "tool" | "skill";
14
+ name: string;
15
+ description?: string;
16
+ tags?: string[];
17
+ inputSchema?: unknown;
18
+ }[];
19
+ error?: string;
20
+ }
21
+ export declare function probeMcp(url: string, fetchImpl?: typeof fetch): Promise<SelfDescription>;
22
+ export declare function probeA2a(url: string, fetchImpl?: typeof fetch): Promise<SelfDescription>;
23
+ export declare function probeHttp(url: string, fetchImpl?: typeof fetch): Promise<SelfDescription>;
24
+ export declare function probe(protocol: string, url: string, fetchImpl?: typeof fetch): Promise<SelfDescription>;
package/dist/probe.js ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Read what an agent says about itself (RATIONALE R-3): an MCP server's name, instructions and tools over Streamable
3
+ * HTTP, or an A2A agent card's description and skills. Read-only, short timeouts, and only for endpoints the user
4
+ * just entered. The result feeds the description prefill and the capability suggestions.
5
+ */
6
+ const TIMEOUT = 8000;
7
+ async function rpc(url, body, session, fetchImpl) {
8
+ const headers = { "content-type": "application/json", accept: "application/json, text/event-stream", "mcp-protocol-version": "2025-06-18" };
9
+ if (session)
10
+ headers["mcp-session-id"] = session;
11
+ const r = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal: AbortSignal.timeout(TIMEOUT) });
12
+ const sid = r.headers.get("mcp-session-id") ?? session;
13
+ if (!r.ok)
14
+ throw new Error(`HTTP ${r.status}`);
15
+ const type = r.headers.get("content-type") ?? "";
16
+ const text = await r.text();
17
+ let msg;
18
+ if (type.includes("text/event-stream")) {
19
+ const data = text.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).filter(Boolean);
20
+ msg = data.map((d) => { try {
21
+ return JSON.parse(d);
22
+ }
23
+ catch {
24
+ return null;
25
+ } }).find((m) => m && "id" in m && m.id === body.id);
26
+ }
27
+ else
28
+ msg = text ? JSON.parse(text) : null;
29
+ if (!msg)
30
+ throw new Error("no JSON-RPC response");
31
+ if (msg.error)
32
+ throw new Error(msg.error.message ?? "JSON-RPC error");
33
+ return { result: msg.result, session: sid };
34
+ }
35
+ export async function probeMcp(url, fetchImpl = fetch) {
36
+ try {
37
+ const init = await rpc(url, { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "agt-cli", version: "0" } } }, null, fetchImpl);
38
+ const headers = { "content-type": "application/json", accept: "application/json, text/event-stream" };
39
+ if (init.session)
40
+ headers["mcp-session-id"] = init.session;
41
+ await fetchImpl(url, { method: "POST", headers, body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }), signal: AbortSignal.timeout(TIMEOUT) }).then((r) => r.body?.cancel()).catch(() => { });
42
+ const tools = await rpc(url, { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }, init.session, fetchImpl).catch(() => ({ result: { tools: [] } }));
43
+ const info = init.result?.serverInfo ?? {};
44
+ return {
45
+ protocol: "mcp", url, live: true,
46
+ name: info.title ?? info.name,
47
+ description: typeof init.result?.instructions === "string" ? firstSentence(init.result.instructions) : info.description,
48
+ items: (tools.result?.tools ?? []).map((t) => ({ kind: "tool", name: String(t.name), description: t.description, inputSchema: t.inputSchema })),
49
+ };
50
+ }
51
+ catch (e) {
52
+ return { protocol: "mcp", url, live: false, items: [], error: e.message };
53
+ }
54
+ }
55
+ export async function probeA2a(url, fetchImpl = fetch) {
56
+ let origin;
57
+ try {
58
+ origin = new URL(url).origin;
59
+ }
60
+ catch {
61
+ return { protocol: "a2a", url, live: false, items: [], error: "not a URL" };
62
+ }
63
+ const candidates = url.endsWith(".json") ? [url] : [`${origin}/.well-known/agent-card.json`, `${origin}/.well-known/agent.json`];
64
+ let lastError = "no agent card";
65
+ for (const c of candidates) {
66
+ try {
67
+ const r = await fetchImpl(c, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(TIMEOUT) });
68
+ if (!r.ok) {
69
+ lastError = `HTTP ${r.status}`;
70
+ continue;
71
+ }
72
+ const card = await r.json();
73
+ return {
74
+ protocol: "a2a", url, live: true, name: card.name, description: card.description,
75
+ items: (card.skills ?? []).map((s) => ({ kind: "skill", name: String(s.name ?? s.id), description: s.description, tags: Array.isArray(s.tags) ? s.tags : undefined })),
76
+ };
77
+ }
78
+ catch (e) {
79
+ lastError = e.message;
80
+ }
81
+ }
82
+ return { protocol: "a2a", url, live: false, items: [], error: lastError };
83
+ }
84
+ export async function probeHttp(url, fetchImpl = fetch) {
85
+ try {
86
+ const r = await fetchImpl(url, { signal: AbortSignal.timeout(TIMEOUT) });
87
+ await r.body?.cancel().catch(() => { });
88
+ return { protocol: "http", url, live: r.status < 500, items: [], error: r.status < 500 ? undefined : `HTTP ${r.status}` };
89
+ }
90
+ catch (e) {
91
+ return { protocol: "http", url, live: false, items: [], error: e.message };
92
+ }
93
+ }
94
+ export function probe(protocol, url, fetchImpl = fetch) {
95
+ if (protocol === "mcp")
96
+ return probeMcp(url, fetchImpl);
97
+ if (protocol === "a2a")
98
+ return probeA2a(url, fetchImpl);
99
+ return probeHttp(url, fetchImpl);
100
+ }
101
+ function firstSentence(s) {
102
+ const t = s.trim().split(/\r?\n/)[0];
103
+ const m = /^(.{20,200}?[.!?])(\s|$)/.exec(t);
104
+ return (m ? m[1] : t).slice(0, 200);
105
+ }
@@ -0,0 +1,2 @@
1
+ export declare function promptHidden(question: string): Promise<string>;
2
+ export declare function confirm(question: string): Promise<boolean>;
package/dist/prompt.js ADDED
@@ -0,0 +1,45 @@
1
+ /** Terminal prompts. Hidden input needs a TTY; without one the caller must pass the value through the environment. */
2
+ import { createInterface } from "node:readline";
3
+ export function promptHidden(question) {
4
+ const stdin = process.stdin;
5
+ if (!stdin.isTTY)
6
+ return Promise.reject(new Error(`${question.trim()} needs an interactive terminal; set the value in the environment instead`));
7
+ return new Promise((resolve, reject) => {
8
+ process.stderr.write(question);
9
+ let value = "";
10
+ stdin.setRawMode(true);
11
+ stdin.resume();
12
+ stdin.setEncoding("utf8");
13
+ const done = (err) => {
14
+ stdin.setRawMode(false);
15
+ stdin.pause();
16
+ stdin.removeListener("data", onData);
17
+ process.stderr.write("\n");
18
+ if (err)
19
+ reject(err);
20
+ else
21
+ resolve(value);
22
+ };
23
+ const onData = (chunk) => {
24
+ for (const ch of chunk) {
25
+ if (ch === "\r" || ch === "\n")
26
+ return done();
27
+ if (ch === "\u0003")
28
+ return done(new Error("cancelled"));
29
+ if (ch === "\u007f" || ch === "\b") {
30
+ value = value.slice(0, -1);
31
+ continue;
32
+ }
33
+ if (ch >= " ")
34
+ value += ch;
35
+ }
36
+ };
37
+ stdin.on("data", onData);
38
+ });
39
+ }
40
+ export function confirm(question) {
41
+ if (!process.stdin.isTTY)
42
+ return Promise.resolve(false);
43
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
44
+ return new Promise((resolve) => rl.question(`${question} [y/N] `, (a) => { rl.close(); resolve(/^y(es)?$/i.test(a.trim())); }));
45
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Register: the site signs an EIP-712 quote (the controller only accepts quotes from its quote signer), the buyer
3
+ * approves the USDC amount and calls AGTController.register(quote, sig) from their own wallet. Same flow as the
4
+ * browser checkout (site: src/lib/migration-client.ts registerWithQuote). The quote binds label, recipient, token,
5
+ * price and term, and expires (about 15 minutes), so approve and register go out back to back.
6
+ */
7
+ import { type Address, type Hex, type PublicClient } from "viem";
8
+ import type { Network } from "./config.js";
9
+ import type { TxRequest } from "./signer.js";
10
+ export interface SignedQuote {
11
+ chainId: number;
12
+ controller: Address;
13
+ currency: "USDC" | "native";
14
+ usd?: number;
15
+ years?: number;
16
+ quote: {
17
+ label: string;
18
+ to: Address;
19
+ payToken: Address;
20
+ price: string;
21
+ duration: string;
22
+ validUntil: string;
23
+ nonce: Hex;
24
+ };
25
+ signature: Hex;
26
+ }
27
+ export interface Quote {
28
+ label: string;
29
+ to: Address;
30
+ payToken: Address;
31
+ price: bigint;
32
+ duration: bigint;
33
+ validUntil: bigint;
34
+ nonce: Hex;
35
+ }
36
+ export declare function fetchQuote(site: string, body: {
37
+ label: string;
38
+ to: Address;
39
+ years: number;
40
+ payToken: "usdc";
41
+ }, fetchImpl?: typeof fetch): Promise<SignedQuote>;
42
+ /** Everything the quote binds must match what was asked for and the network we are on; otherwise refuse to pay. */
43
+ export declare function checkQuote(q: SignedQuote, want: {
44
+ label: string;
45
+ to: Address;
46
+ }, n: Network, nowSec?: number): Quote;
47
+ export declare const usdc: (units: bigint) => string;
48
+ export interface RegisterPlan {
49
+ quote: Quote;
50
+ signature: Hex;
51
+ balance: bigint;
52
+ allowance: bigint;
53
+ txs: TxRequest[];
54
+ }
55
+ export declare function planRegister(pc: PublicClient, n: Network, q: SignedQuote, quote: Quote, payer: Address): Promise<RegisterPlan>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Register: the site signs an EIP-712 quote (the controller only accepts quotes from its quote signer), the buyer
3
+ * approves the USDC amount and calls AGTController.register(quote, sig) from their own wallet. Same flow as the
4
+ * browser checkout (site: src/lib/migration-client.ts registerWithQuote). The quote binds label, recipient, token,
5
+ * price and term, and expires (about 15 minutes), so approve and register go out back to back.
6
+ */
7
+ import { encodeFunctionData, formatUnits, getAddress, zeroAddress } from "viem";
8
+ import { controllerAbi, erc20Abi } from "./abi.js";
9
+ export async function fetchQuote(site, body, fetchImpl = fetch) {
10
+ const r = await fetchImpl(`${site}/api/v2/quote`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
11
+ const j = (await r.json().catch(() => ({})));
12
+ if (!r.ok)
13
+ throw new Error(`quote refused (${r.status}): ${j.error ?? "no reason given"}`);
14
+ return j;
15
+ }
16
+ /** Everything the quote binds must match what was asked for and the network we are on; otherwise refuse to pay. */
17
+ export function checkQuote(q, want, n, nowSec = Math.floor(Date.now() / 1000)) {
18
+ if (q.chainId !== n.chainId)
19
+ throw new Error(`the quote is for chain ${q.chainId}, not ${n.name} (${n.chainId})`);
20
+ if (getAddress(q.controller) !== getAddress(n.controller))
21
+ throw new Error(`the quote names controller ${q.controller}, expected ${n.controller}`);
22
+ if (q.quote.label !== want.label)
23
+ throw new Error(`the quote is for ${q.quote.label}.agt, not ${want.label}.agt`);
24
+ if (getAddress(q.quote.to) !== getAddress(want.to))
25
+ throw new Error(`the quote mints to ${q.quote.to}, not ${want.to}`);
26
+ if (q.currency !== "USDC" || getAddress(q.quote.payToken) !== getAddress(n.usdc))
27
+ throw new Error(`the quote asks for ${q.currency} at ${q.quote.payToken}; the CLI pays in USDC (${n.usdc})`);
28
+ const quote = { ...q.quote, price: BigInt(q.quote.price), duration: BigInt(q.quote.duration), validUntil: BigInt(q.quote.validUntil) };
29
+ if (quote.validUntil <= BigInt(nowSec + 60))
30
+ throw new Error("the quote expires in under a minute; run the command again");
31
+ return quote;
32
+ }
33
+ export const usdc = (units) => `${formatUnits(units, 6)} USDC`;
34
+ export async function planRegister(pc, n, q, quote, payer) {
35
+ const [balance, allowance] = await Promise.all([
36
+ pc.readContract({ address: n.usdc, abi: erc20Abi, functionName: "balanceOf", args: [payer] }),
37
+ pc.readContract({ address: n.usdc, abi: erc20Abi, functionName: "allowance", args: [payer, n.controller] }),
38
+ ]);
39
+ const years = Number(quote.duration / (365n * 86400n));
40
+ const txs = [];
41
+ if (allowance < quote.price)
42
+ txs.push({
43
+ to: n.usdc,
44
+ data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [n.controller, quote.price] }),
45
+ description: `Approve ${usdc(quote.price)} for the .agt controller`,
46
+ });
47
+ txs.push({
48
+ to: n.controller,
49
+ data: encodeFunctionData({ abi: controllerAbi, functionName: "register", args: [quote, q.signature] }),
50
+ value: quote.payToken === zeroAddress ? quote.price : 0n,
51
+ description: `Register ${quote.label}.agt for ${years} year${years === 1 ? "" : "s"} (${usdc(quote.price)}) to ${quote.to}`,
52
+ });
53
+ return { quote, signature: q.signature, balance, allowance, txs };
54
+ }