@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/LICENSE +21 -0
- package/README.md +68 -2
- package/dist/abi.d.ts +239 -0
- package/dist/abi.js +32 -0
- package/dist/args.d.ts +15 -0
- package/dist/args.js +66 -0
- package/dist/browser.d.ts +18 -0
- package/dist/browser.js +309 -0
- package/dist/chain.d.ts +36 -0
- package/dist/chain.js +63 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +48 -0
- package/dist/endpoints.d.ts +42 -0
- package/dist/endpoints.js +142 -0
- package/dist/init.d.ts +47 -0
- package/dist/init.js +430 -0
- package/dist/keyfile.d.ts +20 -0
- package/dist/keyfile.js +60 -0
- package/dist/manifest.d.ts +49 -0
- package/dist/manifest.js +83 -0
- package/dist/picker.d.ts +55 -0
- package/dist/picker.js +330 -0
- package/dist/probe.d.ts +24 -0
- package/dist/probe.js +105 -0
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +45 -0
- package/dist/register.d.ts +55 -0
- package/dist/register.js +54 -0
- package/dist/signer.d.ts +32 -0
- package/dist/signer.js +40 -0
- package/dist/tui.d.ts +62 -0
- package/dist/tui.js +153 -0
- package/dist/verify.d.ts +32 -0
- package/dist/verify.js +71 -0
- package/dist/widgets.d.ts +91 -0
- package/dist/widgets.js +294 -0
- package/package.json +64 -4
package/dist/init.js
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive `agt manifest init`. Each step prefills from data the CLI actually has (RATIONALE R-5): the name's
|
|
3
|
+
* current manifest, the agent's own MCP tools or A2A card (R-3), the top-ranked capability. Background work starts
|
|
4
|
+
* as early as possible: the current manifest and the capability vocabulary load while the first questions are
|
|
5
|
+
* answered, and each endpoint is read the moment its URL is entered. Answers are saved as a draft after every step,
|
|
6
|
+
* so Ctrl+C loses nothing and the next run resumes (R-6).
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join, resolve as resolvePath } from "node:path";
|
|
12
|
+
import { AgtResolver, CAPABILITY_VOCABULARY, suggestCapabilities } from "@agtnames/resolver";
|
|
13
|
+
import { normalizeLabel } from "./chain.js";
|
|
14
|
+
import { CapabilityPicker } from "./picker.js";
|
|
15
|
+
import { probe } from "./probe.js";
|
|
16
|
+
import { BACK, CancelledError, copyToClipboard, eraseLines, run, SKIP, style } from "./tui.js";
|
|
17
|
+
import { EndpointsInput } from "./endpoints.js";
|
|
18
|
+
import { Select, TextInput } from "./widgets.js";
|
|
19
|
+
export function detectShell(env = process.env, platform = process.platform) {
|
|
20
|
+
if (env.SHELL && /bash|zsh|sh$/.test(env.SHELL))
|
|
21
|
+
return "bash";
|
|
22
|
+
return platform === "win32" ? "powershell" : "bash";
|
|
23
|
+
}
|
|
24
|
+
/** Quote one argument for the shell, only when it needs it. */
|
|
25
|
+
export function shellQuote(arg, shell) {
|
|
26
|
+
if (/^[A-Za-z0-9_./:\\=@-]+$/.test(arg))
|
|
27
|
+
return arg;
|
|
28
|
+
return shell === "powershell" ? `'${arg.replace(/'/g, "''")}'` : `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
29
|
+
}
|
|
30
|
+
const PROTOCOLS = [
|
|
31
|
+
{ value: "mcp", label: "MCP", description: "Model Context Protocol server (tools for AI clients)" },
|
|
32
|
+
{ value: "a2a", label: "A2A", description: "Agent-to-Agent endpoint with an agent card" },
|
|
33
|
+
{ value: "http", label: "HTTP", description: "Plain HTTP API" },
|
|
34
|
+
{ value: "ws", label: "WebSocket", description: "Streaming endpoint (wss://)" },
|
|
35
|
+
];
|
|
36
|
+
const httpsUrl = (protocol) => (v) => {
|
|
37
|
+
let u;
|
|
38
|
+
try {
|
|
39
|
+
u = new URL(v);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return "Enter a full URL starting with https://";
|
|
43
|
+
}
|
|
44
|
+
if (u.protocol !== "https:" && !(protocol === "ws" && u.protocol === "wss:"))
|
|
45
|
+
return protocol === "ws" ? "Use wss:// or https://" : "Use https://";
|
|
46
|
+
return null;
|
|
47
|
+
};
|
|
48
|
+
// ── drafts ────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
const draftDir = () => process.env.AGT_DRAFT_DIR ?? join(homedir(), ".agt", "drafts");
|
|
50
|
+
const draftPath = (label) => join(draftDir(), `${label}.json`);
|
|
51
|
+
export function loadDraft(label) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(draftPath(label), "utf8"));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function saveDraft(label, answers) {
|
|
60
|
+
mkdirSync(draftDir(), { recursive: true });
|
|
61
|
+
writeFileSync(draftPath(label), JSON.stringify({ label, savedAt: new Date().toISOString(), answers }, null, 2) + "\n", { mode: 0o600 });
|
|
62
|
+
}
|
|
63
|
+
export function clearDraft(label) { rmSync(draftPath(label), { force: true }); }
|
|
64
|
+
const ago = (iso) => {
|
|
65
|
+
const m = Math.round((Date.now() - Date.parse(iso)) / 60000);
|
|
66
|
+
return m < 1 ? "just now" : m < 60 ? `${m} min ago` : m < 1440 ? `${Math.round(m / 60)} h ago` : `${Math.round(m / 1440)} days ago`;
|
|
67
|
+
};
|
|
68
|
+
// ── manifest file from answers ────────────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
export function answersToInput(a) {
|
|
70
|
+
const pricing = a.pricing ? { model: a.pricing, ...(a.paid && (a.pricing === "paid" || a.pricing === "freemium") ? { paid: a.paid } : {}), ...(a.freeTier && a.pricing === "freemium" ? { free_tier: a.freeTier } : {}) } : undefined;
|
|
71
|
+
return {
|
|
72
|
+
description: a.description,
|
|
73
|
+
website: a.website || undefined,
|
|
74
|
+
endpoints: (a.protocols ?? []).filter((p) => a.endpoints?.[p]).map((p) => ({ protocol: p, url: a.endpoints[p] })),
|
|
75
|
+
capabilities: a.capabilities ?? [],
|
|
76
|
+
pricing,
|
|
77
|
+
payments: a.payment?.address ? [{ rail: a.payment.rail, chain: "polygon", address: a.payment.address, token: "USDC" }] : [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function answersFromManifest(m) {
|
|
81
|
+
const endpoints = {};
|
|
82
|
+
for (const e of m.endpoints ?? [])
|
|
83
|
+
endpoints[e.protocol] = e.url;
|
|
84
|
+
const pay = (m.payments ?? []).find((p) => p.address);
|
|
85
|
+
const paid = m.pricing?.paid;
|
|
86
|
+
return {
|
|
87
|
+
protocols: Object.keys(endpoints),
|
|
88
|
+
endpoints,
|
|
89
|
+
description: m.description,
|
|
90
|
+
website: m.website,
|
|
91
|
+
capabilities: (m.capabilities ?? []).map((c) => c.id),
|
|
92
|
+
pricing: typeof m.pricing?.model === "string" ? m.pricing.model : undefined,
|
|
93
|
+
paid: paid?.currency && paid.amount && paid.unit ? { currency: paid.currency, amount: paid.amount, unit: paid.unit } : undefined,
|
|
94
|
+
freeTier: typeof m.pricing?.free_tier === "string" ? m.pricing.free_tier : undefined,
|
|
95
|
+
payment: pay?.address ? { rail: pay.rail, address: pay.address } : null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// ── background data ───────────────────────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
class Background {
|
|
100
|
+
site;
|
|
101
|
+
probing;
|
|
102
|
+
vocab = CAPABILITY_VOCABULARY;
|
|
103
|
+
counts = {};
|
|
104
|
+
probes = new Map();
|
|
105
|
+
results = new Map();
|
|
106
|
+
listeners = [];
|
|
107
|
+
constructor(site, probing) {
|
|
108
|
+
this.site = site;
|
|
109
|
+
this.probing = probing;
|
|
110
|
+
}
|
|
111
|
+
vocabulary() { return this.vocab; }
|
|
112
|
+
usage() { return this.counts; }
|
|
113
|
+
onChange(cb) { this.listeners.push(cb); return () => { this.listeners = this.listeners.filter((l) => l !== cb); }; }
|
|
114
|
+
changed() { for (const l of this.listeners)
|
|
115
|
+
l(); }
|
|
116
|
+
/** Vocabulary and usage: /api/v2/capabilities when the site has it, else usage counted from /api/agents. */
|
|
117
|
+
refreshVocabulary() {
|
|
118
|
+
if (!this.site)
|
|
119
|
+
return;
|
|
120
|
+
const site = this.site;
|
|
121
|
+
(async () => {
|
|
122
|
+
try {
|
|
123
|
+
const r = await fetch(`${site}/api/v2/capabilities`, { signal: AbortSignal.timeout(6000) });
|
|
124
|
+
if (r.ok) {
|
|
125
|
+
const j = (await r.json());
|
|
126
|
+
if (Array.isArray(j.capabilities) && Array.isArray(j.categories) && j.capabilities.length)
|
|
127
|
+
this.vocab = { categories: j.categories, capabilities: j.capabilities };
|
|
128
|
+
if (j.usage) {
|
|
129
|
+
this.counts = j.usage;
|
|
130
|
+
this.changed();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch { /* fall through */ }
|
|
136
|
+
try {
|
|
137
|
+
const r = await fetch(`${site}/api/agents`, { signal: AbortSignal.timeout(6000) });
|
|
138
|
+
const j = (await r.json());
|
|
139
|
+
const agents = Array.isArray(j) ? j : j.agents ?? [];
|
|
140
|
+
const c = {};
|
|
141
|
+
for (const a of agents)
|
|
142
|
+
for (const id of a.capabilities ?? [])
|
|
143
|
+
c[id] = (c[id] ?? 0) + 1;
|
|
144
|
+
this.counts = c;
|
|
145
|
+
this.changed();
|
|
146
|
+
}
|
|
147
|
+
catch { /* the bundled vocabulary still works */ }
|
|
148
|
+
})();
|
|
149
|
+
}
|
|
150
|
+
startProbe(protocol, url) {
|
|
151
|
+
const key = `${protocol} ${url}`;
|
|
152
|
+
if (!this.probing || this.probes.has(key))
|
|
153
|
+
return;
|
|
154
|
+
const p = probe(protocol, url).then((d) => { this.results.set(key, d); this.changed(); return d; });
|
|
155
|
+
this.probes.set(key, p);
|
|
156
|
+
}
|
|
157
|
+
probeFor(protocol, url) { return this.results.get(`${protocol} ${url}`); }
|
|
158
|
+
/** The first self-description a live endpoint gave. */
|
|
159
|
+
description() {
|
|
160
|
+
for (const d of this.results.values())
|
|
161
|
+
if (d.live && d.description)
|
|
162
|
+
return { text: d.description.slice(0, 280), source: `from your ${d.protocol.toUpperCase()} endpoint` };
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
suggestions() {
|
|
166
|
+
const sources = [...this.results.values()].filter((d) => d.live).flatMap((d) => [
|
|
167
|
+
...d.items.map((i) => ({ from: `${i.kind} ${i.name}`, text: [i.name, i.description, ...(i.tags ?? [])].filter(Boolean).join(" ") })),
|
|
168
|
+
...(d.description ? [{ from: `${d.protocol} description`, text: d.description }] : []),
|
|
169
|
+
]);
|
|
170
|
+
return suggestCapabilities(sources, { vocabulary: this.vocab }).map((s) => ({ id: s.id, from: s.from }));
|
|
171
|
+
}
|
|
172
|
+
async settle(ms) { await Promise.race([Promise.allSettled([...this.probes.values()]), new Promise((r) => setTimeout(r, ms))]); }
|
|
173
|
+
}
|
|
174
|
+
const FORMAT = {
|
|
175
|
+
mcp: "https://… (your MCP endpoint URL)",
|
|
176
|
+
a2a: "https://… (your A2A endpoint URL)",
|
|
177
|
+
http: "https://… (your HTTP API URL)",
|
|
178
|
+
ws: "wss://… (your WebSocket URL)",
|
|
179
|
+
};
|
|
180
|
+
/** Turn a widget's answer into a step move: BACK goes back, SKIP clears the field and moves on. */
|
|
181
|
+
async function ask(w, set) {
|
|
182
|
+
const v = await run(w);
|
|
183
|
+
if (v === BACK)
|
|
184
|
+
return "back";
|
|
185
|
+
set(v === SKIP ? undefined : v);
|
|
186
|
+
return "next";
|
|
187
|
+
}
|
|
188
|
+
export async function interactiveInit(o) {
|
|
189
|
+
const log = o.log;
|
|
190
|
+
let label;
|
|
191
|
+
if (o.label)
|
|
192
|
+
label = normalizeLabel(o.label);
|
|
193
|
+
else {
|
|
194
|
+
let v;
|
|
195
|
+
do
|
|
196
|
+
v = await run(new TextInput({ label: "Name", instruction: "Enter name of agt", nav: false, validate: (s) => { try {
|
|
197
|
+
normalizeLabel(s);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
return e.message;
|
|
202
|
+
} } }));
|
|
203
|
+
while (typeof v !== "string");
|
|
204
|
+
label = normalizeLabel(v);
|
|
205
|
+
}
|
|
206
|
+
const bg = new Background(o.network.site, o.probe);
|
|
207
|
+
bg.refreshVocabulary();
|
|
208
|
+
const resolver = new AgtResolver({ chain: o.network.name, rpcUrls: o.network.rpcUrls, registry: o.network.registry, timeoutMs: 8000 });
|
|
209
|
+
const currentP = resolver.resolveAgent(`${label}.agt`).then((r) => (r.manifest && r.verified ? r.manifest : null)).catch(() => null);
|
|
210
|
+
// Resume a draft, or start from the published manifest's values as prefills.
|
|
211
|
+
let a = {};
|
|
212
|
+
const draft = loadDraft(label);
|
|
213
|
+
if (draft) {
|
|
214
|
+
const resume = await run(new Select({ label: "Draft", instruction: `You have an unfinished draft for ${label}.agt from ${ago(draft.savedAt)}`, choices: [{ value: true, label: "Resume it" }, { value: false, label: "Start over" }], initial: true }));
|
|
215
|
+
if (resume === false)
|
|
216
|
+
clearDraft(label);
|
|
217
|
+
else
|
|
218
|
+
a = draft.answers; // only an explicit "Start over" discards it
|
|
219
|
+
}
|
|
220
|
+
const current = await Promise.race([currentP, new Promise((r) => setTimeout(() => r(null), 4000))]);
|
|
221
|
+
const pre = current ? answersFromManifest(current) : {};
|
|
222
|
+
if (current && !draft)
|
|
223
|
+
log(style.dim(` ${label}.agt has a published manifest; its values are offered as suggestions (Tab to keep each).`));
|
|
224
|
+
const fromCurrent = (has) => (has !== undefined && has !== null ? "from your current manifest" : undefined);
|
|
225
|
+
const paidModel = () => a.pricing === "paid" || a.pricing === "freemium";
|
|
226
|
+
// Each step pre-fills with your earlier answer (going back, or a resumed draft), else the current manifest, else a
|
|
227
|
+
// prediction. j goes back one step, ; skips it.
|
|
228
|
+
const steps = [
|
|
229
|
+
{
|
|
230
|
+
id: "endpoints",
|
|
231
|
+
run: async () => {
|
|
232
|
+
const urls = a.endpoints ?? {};
|
|
233
|
+
const rows = PROTOCOLS.map((p) => ({
|
|
234
|
+
protocol: p.value, label: p.label, format: FORMAT[p.value],
|
|
235
|
+
url: urls[p.value] ?? "", on: !!urls[p.value],
|
|
236
|
+
prefill: pre.endpoints?.[p.value], prefillSource: fromCurrent(pre.endpoints?.[p.value]),
|
|
237
|
+
}));
|
|
238
|
+
for (const r of rows)
|
|
239
|
+
if (r.on)
|
|
240
|
+
bg.startProbe(r.protocol, r.url);
|
|
241
|
+
return ask(new EndpointsInput({
|
|
242
|
+
label: "Endpoints",
|
|
243
|
+
rows,
|
|
244
|
+
validate: (p, v) => httpsUrl(p)(v),
|
|
245
|
+
onSave: (p, v) => bg.startProbe(p, v),
|
|
246
|
+
status: (p, v) => {
|
|
247
|
+
const d = bg.probeFor(p, v);
|
|
248
|
+
if (!o.probe)
|
|
249
|
+
return null;
|
|
250
|
+
if (!d)
|
|
251
|
+
return style.dim("checking…");
|
|
252
|
+
if (!d.live)
|
|
253
|
+
return style.yellow(`! no answer (${d.error})`);
|
|
254
|
+
return style.green("✓ live") + (d.items.length ? style.dim(` · ${d.items.length} ${d.items[0].kind}${d.items.length === 1 ? "" : "s"}`) : "");
|
|
255
|
+
},
|
|
256
|
+
onChange: (cb) => bg.onChange(cb),
|
|
257
|
+
}), (v) => { a.endpoints = v ?? {}; a.protocols = Object.keys(a.endpoints); });
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "description",
|
|
262
|
+
run: async () => {
|
|
263
|
+
await bg.settle(2000);
|
|
264
|
+
const fromProbe = bg.description();
|
|
265
|
+
return ask(new TextInput({
|
|
266
|
+
label: "Description",
|
|
267
|
+
instruction: "Enter a one-line description of what your agent does",
|
|
268
|
+
initial: a.description,
|
|
269
|
+
prefill: pre.description ?? fromProbe?.text,
|
|
270
|
+
prefillSource: pre.description ? "from your current manifest" : fromProbe?.source,
|
|
271
|
+
examples: ["Hourly and 10-day weather forecasts for any city", "Summarizes long PDFs into cited key points"],
|
|
272
|
+
maxLength: 280,
|
|
273
|
+
validate: (v) => (v.length > 280 ? "Keep it to 280 characters" : null),
|
|
274
|
+
}), (v) => { a.description = v; });
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: "website",
|
|
279
|
+
run: async () => {
|
|
280
|
+
const first = Object.values(a.endpoints ?? {})[0];
|
|
281
|
+
const origin = first ? new URL(first).origin.replace(/^wss:/, "https:") : undefined;
|
|
282
|
+
return ask(new TextInput({
|
|
283
|
+
label: "Website", instruction: "Enter your agent's homepage URL",
|
|
284
|
+
initial: a.website,
|
|
285
|
+
prefill: pre.website ?? origin,
|
|
286
|
+
prefillSource: pre.website ? "from your current manifest" : origin ? "your endpoint's site" : undefined,
|
|
287
|
+
optional: true, validate: httpsUrl("http"),
|
|
288
|
+
}), (v) => { a.website = v || undefined; });
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: "capabilities",
|
|
293
|
+
run: async () => {
|
|
294
|
+
await bg.settle(1500);
|
|
295
|
+
return ask(new CapabilityPicker(bg, { label: "Capabilities", initial: a.capabilities ?? pre.capabilities }), (v) => { a.capabilities = v; });
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: "pricing",
|
|
300
|
+
run: () => ask(new Select({
|
|
301
|
+
label: "Pricing", instruction: "Choose how your agent charges",
|
|
302
|
+
choices: [
|
|
303
|
+
{ value: "free", label: "Free", description: "No charge" },
|
|
304
|
+
{ value: "freemium", label: "Freemium", description: "A free tier, then paid" },
|
|
305
|
+
{ value: "paid", label: "Paid", description: "Every use is charged" },
|
|
306
|
+
{ value: "contact", label: "Contact", description: "Pricing on request" },
|
|
307
|
+
],
|
|
308
|
+
initial: a.pricing ?? pre.pricing ?? "free",
|
|
309
|
+
prefillSource: !a.pricing && pre.pricing ? "from your current manifest" : undefined,
|
|
310
|
+
}), (v) => { a.pricing = v; }),
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: "free-tier", when: () => a.pricing === "freemium",
|
|
314
|
+
run: () => ask(new TextInput({ label: "Free tier", instruction: "Enter what the free tier includes", initial: a.freeTier, prefill: pre.freeTier, prefillSource: fromCurrent(pre.freeTier), optional: true }), (v) => { a.freeTier = v || undefined; }),
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
id: "currency", when: paidModel,
|
|
318
|
+
run: () => ask(new Select({ label: "Currency", instruction: "Choose what you charge in", choices: [{ value: "USDC", label: "USDC" }, { value: "USD", label: "USD" }, { value: "EUR", label: "EUR" }], initial: a.paid?.currency ?? pre.paid?.currency ?? "USDC" }), (v) => { a.paid = { currency: v ?? "USDC", amount: a.paid?.amount ?? "", unit: a.paid?.unit ?? "per_request" }; }),
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: "price", when: paidModel,
|
|
322
|
+
run: () => ask(new TextInput({
|
|
323
|
+
label: "Price", instruction: `Enter the price in ${a.paid?.currency ?? "USDC"} as a decimal number`,
|
|
324
|
+
initial: a.paid?.amount || undefined, prefill: pre.paid?.amount, prefillSource: fromCurrent(pre.paid?.amount),
|
|
325
|
+
validate: (v) => (/^\d+(\.\d+)?$/.test(v) ? null : "Use a decimal number like 0.01"),
|
|
326
|
+
}), (v) => { a.paid = { currency: a.paid?.currency ?? "USDC", amount: v ?? "", unit: a.paid?.unit ?? "per_request" }; }),
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: "unit", when: paidModel,
|
|
330
|
+
run: () => ask(new Select({
|
|
331
|
+
label: "Per", instruction: "Choose what one price unit is",
|
|
332
|
+
choices: ["per_request", "per_session", "per_minute", "per_month", "per_token_in", "per_token_out"].map((u) => ({ value: u, label: u.replace(/_/g, " ") })),
|
|
333
|
+
initial: a.paid?.unit ?? pre.paid?.unit ?? "per_request",
|
|
334
|
+
}), (v) => { a.paid = { currency: a.paid?.currency ?? "USDC", amount: a.paid?.amount ?? "", unit: v ?? "per_request" }; }),
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
id: "payment-address", when: paidModel,
|
|
338
|
+
run: () => ask(new TextInput({
|
|
339
|
+
label: "Payment address", instruction: "Enter the 0x address that receives payments",
|
|
340
|
+
initial: a.payment?.address, prefill: pre.payment?.address, prefillSource: fromCurrent(pre.payment?.address),
|
|
341
|
+
optional: true, validate: (v) => (/^0x[0-9a-fA-F]{40}$/.test(v) ? null : "Use a 0x address (40 hex characters)"),
|
|
342
|
+
}), (v) => { a.payment = v ? { rail: a.payment?.rail ?? pre.payment?.rail ?? "x402", address: v } : null; }),
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: "payment-rail", when: () => paidModel() && !!a.payment?.address,
|
|
346
|
+
run: () => ask(new Select({ label: "Payment rail", instruction: "Choose how clients pay that address", choices: [{ value: "x402", label: "x402", description: "HTTP 402 pay-per-request" }, { value: "evm", label: "EVM transfer", description: "Plain on-chain transfer" }], initial: a.payment?.rail ?? "x402" }), (v) => { if (a.payment)
|
|
347
|
+
a.payment.rail = v ?? "x402"; }),
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
// Walk the steps. Each finished step leaves one summary line; going back erases it and re-opens that step there.
|
|
351
|
+
let i = 0;
|
|
352
|
+
const done = [];
|
|
353
|
+
while (i < steps.length) {
|
|
354
|
+
const s = steps[i];
|
|
355
|
+
if (s.when && !s.when()) {
|
|
356
|
+
i++;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const r = await s.run();
|
|
360
|
+
if (r === "back") {
|
|
361
|
+
const prev = done.pop();
|
|
362
|
+
if (prev === undefined)
|
|
363
|
+
continue; // first step: nothing to go back to
|
|
364
|
+
eraseLines(1);
|
|
365
|
+
i = prev;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
saveDraft(label, a);
|
|
369
|
+
done.push(i);
|
|
370
|
+
i++;
|
|
371
|
+
}
|
|
372
|
+
if (!paidModel()) {
|
|
373
|
+
a.paid = undefined;
|
|
374
|
+
a.freeTier = undefined;
|
|
375
|
+
a.payment = null;
|
|
376
|
+
}
|
|
377
|
+
// Review and write.
|
|
378
|
+
const input = answersToInput(a);
|
|
379
|
+
const file = o.out ?? `${label}.manifest.json`;
|
|
380
|
+
log("");
|
|
381
|
+
log(style.bold(`${label}.agt manifest`));
|
|
382
|
+
log(` ${style.dim("description")} ${input.description ?? style.dim("(skipped)")}`);
|
|
383
|
+
if (input.website)
|
|
384
|
+
log(` ${style.dim("website")} ${input.website}`);
|
|
385
|
+
if (!input.endpoints?.length)
|
|
386
|
+
log(` ${style.dim("endpoints")} ${style.dim("(skipped)")}`);
|
|
387
|
+
for (const e of input.endpoints ?? [])
|
|
388
|
+
log(` ${style.dim(e.protocol.padEnd(12))} ${e.url}`);
|
|
389
|
+
log(` ${style.dim("capabilities")} ${(a.capabilities ?? []).join(", ") || style.dim("none")}`);
|
|
390
|
+
log(` ${style.dim("pricing")} ${a.pricing ?? style.dim("(skipped)")}${a.paid?.amount ? ` · ${a.paid.amount} ${a.paid.currency} ${a.paid.unit.replace(/_/g, " ")}` : ""}${a.payment ? ` · ${a.payment.rail} to ${a.payment.address}` : ""}`);
|
|
391
|
+
const exists = existsSync(file);
|
|
392
|
+
const next = await run(new Select({
|
|
393
|
+
label: "Next", instruction: `Write ${file}${exists ? " (it exists and will be replaced)" : ""}`,
|
|
394
|
+
choices: [
|
|
395
|
+
{ value: "write", label: "Write the file", description: "then publish when ready" },
|
|
396
|
+
{ value: "publish", label: "Write and publish now", description: "signs in your wallet, hosts it, sets the records" },
|
|
397
|
+
{ value: "quit", label: "Not yet", description: "keep the draft for later" },
|
|
398
|
+
],
|
|
399
|
+
initial: "write",
|
|
400
|
+
}));
|
|
401
|
+
if (next === BACK) {
|
|
402
|
+
log(style.dim(`Draft kept. Run agt manifest init ${label} to change an answer.`));
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
if (next === "quit" || next === SKIP) {
|
|
406
|
+
log(style.dim(`Draft kept. Run agt manifest init ${label} to pick it up.`));
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
writeFileSync(file, JSON.stringify(input, null, 2) + "\n");
|
|
410
|
+
clearDraft(label);
|
|
411
|
+
log(`${style.green("✔")} Wrote ${file}`);
|
|
412
|
+
// The equivalent command, as a plain line to run or copy (R-6).
|
|
413
|
+
const net = o.network.name === "polygon" ? [] : ["--network", o.network.name];
|
|
414
|
+
const cmd = ["agt", "manifest", "publish", label, file, ...net].map((x) => shellQuote(x, o.shell)).join(" ");
|
|
415
|
+
if (next === "publish") {
|
|
416
|
+
log(style.dim(`Running: ${cmd}`));
|
|
417
|
+
const code = await new Promise((res) => spawn(process.execPath, [process.argv[1], "manifest", "publish", label, resolvePath(file), ...net], { stdio: "inherit" }).on("exit", (c) => res(c ?? 1)));
|
|
418
|
+
process.exitCode = code;
|
|
419
|
+
return { file, label };
|
|
420
|
+
}
|
|
421
|
+
log("");
|
|
422
|
+
log("Publish it with:");
|
|
423
|
+
log("");
|
|
424
|
+
log(cmd);
|
|
425
|
+
log("");
|
|
426
|
+
copyToClipboard(cmd);
|
|
427
|
+
log(style.dim(`(copied to the clipboard · ${o.shell} quoting · --shell bash|powershell to change)`));
|
|
428
|
+
return { file, label };
|
|
429
|
+
}
|
|
430
|
+
export { CancelledError };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Address, Hex } from "viem";
|
|
2
|
+
export interface KeyEnvelope {
|
|
3
|
+
v: 1;
|
|
4
|
+
address: Address;
|
|
5
|
+
kdf: {
|
|
6
|
+
N: number;
|
|
7
|
+
r: number;
|
|
8
|
+
p: number;
|
|
9
|
+
salt: string;
|
|
10
|
+
};
|
|
11
|
+
iv: string;
|
|
12
|
+
ct: string;
|
|
13
|
+
tag: string;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function checkPassphrase(p: string): string;
|
|
17
|
+
export declare function encryptKey(pk: Hex, passphrase: string): KeyEnvelope;
|
|
18
|
+
export declare function decryptKey(e: KeyEnvelope, passphrase: string): Hex;
|
|
19
|
+
export declare function readKeyfile(path: string): KeyEnvelope;
|
|
20
|
+
export declare function writeKeyfile(path: string, e: KeyEnvelope): void;
|
package/dist/keyfile.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encrypted key files: the same envelope as @agtnames/mcp's session key (~/.agt/session/session.json), so one format
|
|
3
|
+
* covers both. scrypt (N=2^15, r=8, p=1) → AES-256-GCM over the raw 32-byte key; the address is stored in the clear
|
|
4
|
+
* so `agt keyfile address` and the ownership pre-check work without the passphrase.
|
|
5
|
+
*
|
|
6
|
+
* { v: 1, address, kdf: { N, r, p, salt }, iv, ct, tag, createdAt }
|
|
7
|
+
*/
|
|
8
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
11
|
+
const KDF = { N: 2 ** 15, r: 8, p: 1 };
|
|
12
|
+
const MAXMEM = 128 * 1024 * 1024;
|
|
13
|
+
export function checkPassphrase(p) {
|
|
14
|
+
if (p.length < 12)
|
|
15
|
+
throw new Error("the keyfile passphrase must be at least 12 characters");
|
|
16
|
+
return p.normalize("NFKC");
|
|
17
|
+
}
|
|
18
|
+
export function encryptKey(pk, passphrase) {
|
|
19
|
+
const salt = randomBytes(16);
|
|
20
|
+
const key = scryptSync(checkPassphrase(passphrase), salt, 32, { ...KDF, maxmem: MAXMEM });
|
|
21
|
+
const iv = randomBytes(12);
|
|
22
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
23
|
+
const ct = Buffer.concat([cipher.update(Buffer.from(pk.slice(2), "hex")), cipher.final()]);
|
|
24
|
+
key.fill(0);
|
|
25
|
+
return { v: 1, address: privateKeyToAccount(pk).address, kdf: { ...KDF, salt: salt.toString("base64") }, iv: iv.toString("base64"), ct: ct.toString("base64"), tag: cipher.getAuthTag().toString("base64"), createdAt: new Date().toISOString() };
|
|
26
|
+
}
|
|
27
|
+
export function decryptKey(e, passphrase) {
|
|
28
|
+
const key = scryptSync(checkPassphrase(passphrase), Buffer.from(e.kdf.salt, "base64"), 32, { N: e.kdf.N, r: e.kdf.r, p: e.kdf.p, maxmem: MAXMEM });
|
|
29
|
+
try {
|
|
30
|
+
const d = createDecipheriv("aes-256-gcm", key, Buffer.from(e.iv, "base64"));
|
|
31
|
+
d.setAuthTag(Buffer.from(e.tag, "base64"));
|
|
32
|
+
const pk = Buffer.concat([d.update(Buffer.from(e.ct, "base64")), d.final()]);
|
|
33
|
+
return ("0x" + pk.toString("hex"));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error("could not decrypt the keyfile: wrong passphrase, or the file was modified");
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
key.fill(0);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function readKeyfile(path) {
|
|
43
|
+
if (!existsSync(path))
|
|
44
|
+
throw new Error(`keyfile not found: ${path}`);
|
|
45
|
+
let e;
|
|
46
|
+
try {
|
|
47
|
+
e = JSON.parse(readFileSync(path, "utf8"));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new Error(`${path} is not a keyfile (not JSON)`);
|
|
51
|
+
}
|
|
52
|
+
if (e?.v !== 1 || !e.address || !e.kdf?.salt || !e.iv || !e.ct || !e.tag)
|
|
53
|
+
throw new Error(`${path} is not an agt keyfile (expected the v1 envelope that \`agt keyfile create\` writes)`);
|
|
54
|
+
return e;
|
|
55
|
+
}
|
|
56
|
+
export function writeKeyfile(path, e) {
|
|
57
|
+
if (existsSync(path))
|
|
58
|
+
throw new Error(`${path} already exists; remove it first if you mean to replace it`);
|
|
59
|
+
writeFileSync(path, JSON.stringify(e, null, 2) + "\n", { mode: 0o600 });
|
|
60
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest v3: build from the owner's input file, sign (EIP-191 over the canonical form), host, point the name at it.
|
|
3
|
+
* The build mirrors the site's scripts/publish-manifest.mjs and src/lib/manifest-v3.ts, so a document published from
|
|
4
|
+
* here and one published from the browser editor canonicalize identically.
|
|
5
|
+
*/
|
|
6
|
+
import { type AgtManifest } from "@agtnames/resolver";
|
|
7
|
+
import type { Address } from "viem";
|
|
8
|
+
import type { Signer } from "./signer.js";
|
|
9
|
+
/** What the owner writes; name / owner / updated / agt are filled in. */
|
|
10
|
+
export interface ManifestInput {
|
|
11
|
+
description?: string;
|
|
12
|
+
website?: string;
|
|
13
|
+
icon?: string;
|
|
14
|
+
endpoints?: {
|
|
15
|
+
protocol: string;
|
|
16
|
+
url: string;
|
|
17
|
+
version?: string;
|
|
18
|
+
}[];
|
|
19
|
+
capabilities?: (string | {
|
|
20
|
+
id: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
})[];
|
|
23
|
+
pricing?: {
|
|
24
|
+
model?: string;
|
|
25
|
+
[k: string]: unknown;
|
|
26
|
+
};
|
|
27
|
+
payments?: {
|
|
28
|
+
rail: string;
|
|
29
|
+
chain?: string;
|
|
30
|
+
address?: string;
|
|
31
|
+
token?: string;
|
|
32
|
+
}[];
|
|
33
|
+
keys?: AgtManifest["keys"];
|
|
34
|
+
registrations?: AgtManifest["registrations"];
|
|
35
|
+
}
|
|
36
|
+
export declare const MANIFEST_TEMPLATE: ManifestInput;
|
|
37
|
+
export declare function stripEmpty<T>(v: T): T;
|
|
38
|
+
export declare function buildManifest(label: string, owner: Address, input: ManifestInput, now?: Date): AgtManifest;
|
|
39
|
+
/** Problems to fix before signing, in plain words. Empty = fine. */
|
|
40
|
+
export declare function lintInput(input: ManifestInput): string[];
|
|
41
|
+
export declare function signManifestWith(signer: Signer, unsigned: AgtManifest): Promise<AgtManifest>;
|
|
42
|
+
export interface PinResult {
|
|
43
|
+
uri: string;
|
|
44
|
+
provider: string;
|
|
45
|
+
digest?: string;
|
|
46
|
+
cid?: string;
|
|
47
|
+
}
|
|
48
|
+
/** POST to the site's pin API, which re-verifies signer == owner == on-chain owner before storing. */
|
|
49
|
+
export declare function hostManifest(site: string, manifest: AgtManifest, host: "agts.dev" | "ipfs", fetchImpl?: typeof fetch): Promise<PinResult>;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest v3: build from the owner's input file, sign (EIP-191 over the canonical form), host, point the name at it.
|
|
3
|
+
* The build mirrors the site's scripts/publish-manifest.mjs and src/lib/manifest-v3.ts, so a document published from
|
|
4
|
+
* here and one published from the browser editor canonicalize identically.
|
|
5
|
+
*/
|
|
6
|
+
import { canonicalize, verifyManifest } from "@agtnames/resolver";
|
|
7
|
+
export const MANIFEST_TEMPLATE = {
|
|
8
|
+
description: "What this agent does, in one sentence.",
|
|
9
|
+
website: "https://example.com",
|
|
10
|
+
endpoints: [
|
|
11
|
+
{ protocol: "mcp", url: "https://example.com/mcp" },
|
|
12
|
+
{ protocol: "http", url: "https://example.com/api" },
|
|
13
|
+
],
|
|
14
|
+
capabilities: ["example-capability"],
|
|
15
|
+
pricing: { model: "free" },
|
|
16
|
+
payments: [],
|
|
17
|
+
};
|
|
18
|
+
export function stripEmpty(v) {
|
|
19
|
+
if (Array.isArray(v))
|
|
20
|
+
return v.map(stripEmpty).filter((x) => x !== undefined && x !== "");
|
|
21
|
+
if (v && typeof v === "object") {
|
|
22
|
+
const out = {};
|
|
23
|
+
for (const [k, x] of Object.entries(v)) {
|
|
24
|
+
const y = stripEmpty(x);
|
|
25
|
+
if (y === undefined || y === "" || (Array.isArray(y) && y.length === 0))
|
|
26
|
+
continue;
|
|
27
|
+
out[k] = y;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
return v;
|
|
32
|
+
}
|
|
33
|
+
export function buildManifest(label, owner, input, now = new Date()) {
|
|
34
|
+
return stripEmpty({
|
|
35
|
+
agt: "3.0",
|
|
36
|
+
name: `${label}.agt`,
|
|
37
|
+
owner,
|
|
38
|
+
updated: now.toISOString(),
|
|
39
|
+
description: input.description,
|
|
40
|
+
icon: input.icon,
|
|
41
|
+
website: input.website,
|
|
42
|
+
keys: input.keys,
|
|
43
|
+
endpoints: (input.endpoints ?? []).filter((e) => e && e.url),
|
|
44
|
+
capabilities: (input.capabilities ?? []).map((c) => (typeof c === "string" ? { id: c.trim().toLowerCase() } : c)),
|
|
45
|
+
pricing: input.pricing && input.pricing.model ? input.pricing : undefined,
|
|
46
|
+
payments: (input.payments ?? []).filter((p) => p && p.address),
|
|
47
|
+
registrations: input.registrations ?? [],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** Problems to fix before signing, in plain words. Empty = fine. */
|
|
51
|
+
export function lintInput(input) {
|
|
52
|
+
const out = [];
|
|
53
|
+
if (input.description === MANIFEST_TEMPLATE.description)
|
|
54
|
+
out.push("description is still the template text");
|
|
55
|
+
for (const e of input.endpoints ?? []) {
|
|
56
|
+
if (!e.protocol)
|
|
57
|
+
out.push("an endpoint has no protocol");
|
|
58
|
+
if (!/^https:\/\//.test(e.url ?? ""))
|
|
59
|
+
out.push(`endpoint ${e.protocol ?? "?"} must be an https:// URL (got ${e.url || "nothing"})`);
|
|
60
|
+
if (/\bexample\.com\b/.test(e.url ?? ""))
|
|
61
|
+
out.push(`endpoint ${e.protocol} is still the template URL`);
|
|
62
|
+
}
|
|
63
|
+
for (const p of input.payments ?? [])
|
|
64
|
+
if (p.address && !/^0x[0-9a-fA-F]{40}$/.test(p.address))
|
|
65
|
+
out.push(`payment address ${p.address} is not a 0x address`);
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
export async function signManifestWith(signer, unsigned) {
|
|
69
|
+
const signature = await signer.signMessage(canonicalize(unsigned), `Sign the manifest for ${unsigned.name}`);
|
|
70
|
+
const signed = { ...unsigned, signature };
|
|
71
|
+
const v = verifyManifest(signed, signer.address);
|
|
72
|
+
if (!v.verified)
|
|
73
|
+
throw new Error(`the signed manifest does not verify: ${v.reasons.join("; ")}`);
|
|
74
|
+
return signed;
|
|
75
|
+
}
|
|
76
|
+
/** POST to the site's pin API, which re-verifies signer == owner == on-chain owner before storing. */
|
|
77
|
+
export async function hostManifest(site, manifest, host, fetchImpl = fetch) {
|
|
78
|
+
const r = await fetchImpl(`${site}/api/v2/manifest/pin`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ manifest, host }) });
|
|
79
|
+
const body = (await r.json().catch(() => ({})));
|
|
80
|
+
if (!r.ok || !body.uri)
|
|
81
|
+
throw new Error(`hosting failed (${r.status}): ${body.error ?? "no uri returned"}${body.reasons?.length ? " — " + body.reasons.join("; ") : ""}`);
|
|
82
|
+
return body;
|
|
83
|
+
}
|