aixyz 0.23.0 → 0.25.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/README.md ADDED
@@ -0,0 +1,302 @@
1
+ # aixyz
2
+
3
+ Framework for bundling AI agents into deployable services with A2A, MCP, x402 payments, and ERC-8004 identity.
4
+
5
+ Write your agent logic. aixyz wires up the protocols, payments, and deployment.
6
+
7
+ ## Prerequisites
8
+
9
+ Install [Bun](https://bun.sh) if you don't have it:
10
+
11
+ ```bash
12
+ curl -fsSL https://bun.sh/install | bash
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ bunx create-aixyz-app my-agent
19
+ cd my-agent
20
+ bun install
21
+ bun run dev
22
+ ```
23
+
24
+ Your agent is running. It exposes:
25
+
26
+ | Endpoint | Protocol | What it does |
27
+ | ------------------------------ | -------- | ------------------------------------ |
28
+ | `/.well-known/agent-card.json` | A2A | Agent discovery card |
29
+ | `/agent` | A2A | JSON-RPC endpoint, x402 payment gate |
30
+ | `/mcp` | MCP | Tool sharing with MCP clients |
31
+
32
+ ## How It Works
33
+
34
+ An aixyz agent has three parts: a config, an agent, and tools.
35
+
36
+ ### 1. Config
37
+
38
+ `aixyz.config.ts` declares your agent's identity, payment address, and skills:
39
+
40
+ ```ts
41
+ import type { AixyzConfig } from "aixyz/config";
42
+
43
+ const config: AixyzConfig = {
44
+ name: "Weather Agent",
45
+ description: "Get current weather for any location worldwide.",
46
+ version: "0.1.0",
47
+ x402: {
48
+ payTo: "0x...",
49
+ network: "eip155:8453", // Base mainnet
50
+ },
51
+ skills: [
52
+ {
53
+ id: "get-weather",
54
+ name: "Get Weather",
55
+ description: "Get current weather conditions for any city or location",
56
+ tags: ["weather"],
57
+ examples: ["What's the weather in Tokyo?"],
58
+ },
59
+ ],
60
+ };
61
+
62
+ export default config;
63
+ ```
64
+
65
+ ### 2. Agent
66
+
67
+ `app/agent.ts` defines your agent, its payment price, and A2A capabilities:
68
+
69
+ ```ts
70
+ import { openai } from "@ai-sdk/openai";
71
+ import { stepCountIs, ToolLoopAgent } from "ai";
72
+ import type { Accepts } from "aixyz/accepts";
73
+ import type { Capabilities } from "aixyz/app/plugins/a2a";
74
+ import weather from "./tools/weather";
75
+
76
+ export const accepts: Accepts = {
77
+ scheme: "exact",
78
+ price: "$0.005",
79
+ };
80
+
81
+ export const capabilities: Capabilities = {
82
+ streaming: true, // default: true — set to false to use generate() instead of stream()
83
+ pushNotifications: false, // default: false
84
+ };
85
+
86
+ export default new ToolLoopAgent({
87
+ model: openai("gpt-4o-mini"),
88
+ instructions: "You are a helpful weather assistant.",
89
+ tools: { weather },
90
+ stopWhen: stepCountIs(10),
91
+ });
92
+ ```
93
+
94
+ ### 3. Tools
95
+
96
+ Each file in `app/tools/` exports a Vercel AI SDK `tool` and an optional `accepts` for MCP payment gating:
97
+
98
+ ```ts
99
+ import { tool } from "ai";
100
+ import { z } from "zod";
101
+ import type { Accepts } from "aixyz/accepts";
102
+
103
+ export const accepts: Accepts = {
104
+ scheme: "exact",
105
+ price: "$0.0001",
106
+ };
107
+
108
+ export default tool({
109
+ description: "Get current weather conditions for a city.",
110
+ inputSchema: z.object({
111
+ location: z.string().describe("City name"),
112
+ }),
113
+ execute: async ({ location }) => {
114
+ // your logic here
115
+ },
116
+ });
117
+ ```
118
+
119
+ That's it. Run `bun run dev` and aixyz auto-generates the server, wires up A2A + MCP + x402, and starts serving.
120
+
121
+ ## Custom Server
122
+
123
+ For full control, create `app/server.ts` instead. This takes precedence over auto-generation:
124
+
125
+ ```ts
126
+ import { AixyzApp } from "aixyz/app";
127
+ import { IndexPagePlugin } from "aixyz/app/plugins/index-page";
128
+ import { A2APlugin } from "aixyz/app/plugins/a2a";
129
+ import { MCPPlugin } from "aixyz/app/plugins/mcp";
130
+
131
+ import * as agent from "./agent";
132
+ import lookup from "./tools/lookup";
133
+
134
+ const server = new AixyzApp();
135
+
136
+ // Index page: human-readable agent info
137
+ await server.withPlugin(new IndexPagePlugin());
138
+
139
+ // A2A: agent discovery + JSON-RPC endpoint
140
+ await server.withPlugin(new A2APlugin(agent));
141
+
142
+ // MCP: expose tools to MCP clients
143
+ await server.withPlugin(
144
+ new MCPPlugin([
145
+ {
146
+ name: "lookup",
147
+ exports: {
148
+ default: lookup,
149
+ accepts: { scheme: "exact", price: "$0.001" },
150
+ },
151
+ },
152
+ ]),
153
+ );
154
+
155
+ await server.initialize();
156
+
157
+ export default server;
158
+ ```
159
+
160
+ ## Configuration
161
+
162
+ | Field | Type | Required | Description |
163
+ | -------------- | -------------- | -------- | -------------------------------------------------- |
164
+ | `name` | `string` | Yes | Agent display name |
165
+ | `description` | `string` | Yes | What the agent does |
166
+ | `version` | `string` | Yes | Semver version |
167
+ | `url` | `string` | No | Agent base URL. Auto-detected on Vercel |
168
+ | `x402.payTo` | `string` | Yes | EVM address to receive payments |
169
+ | `x402.network` | `string` | Yes | Payment network (`eip155:8453` for Base) |
170
+ | `skills` | `AgentSkill[]` | No | Skills your agent exposes (used in A2A agent card) |
171
+
172
+ Environment variables are loaded in the same order as Next.js: `.env`, `.env.local`, `.env.$(NODE_ENV)`,
173
+ `.env.$(NODE_ENV).local`.
174
+
175
+ ### Payment (Accepts)
176
+
177
+ Each agent and tool declares an `accepts` export to control payment:
178
+
179
+ ```ts
180
+ // Require x402 payment
181
+ export const accepts: Accepts = {
182
+ scheme: "exact",
183
+ price: "$0.005", // USD-denominated
184
+ network: "eip155:8453", // optional, defaults to config.x402.network
185
+ payTo: "0x...", // optional, defaults to config.x402.payTo
186
+ };
187
+ ```
188
+
189
+ Agents and tools without an `accepts` export are not registered.
190
+
191
+ ## CLI
192
+
193
+ ### `aixyz dev`
194
+
195
+ Starts a local dev server with hot reload. Watches `app/` and `aixyz.config.ts` for changes.
196
+
197
+ ```bash
198
+ aixyz dev # default port 3000
199
+ aixyz dev -p 4000 # custom port
200
+ ```
201
+
202
+ ### `aixyz build`
203
+
204
+ Bundles your agent for deployment. Default output goes to `.aixyz/output/server.js`.
205
+
206
+ ```bash
207
+ aixyz build # standalone (default), outputs to .aixyz/output/server.js
208
+ bun .aixyz/output/server.js # run the standalone build
209
+
210
+ aixyz build --output vercel # Vercel Build Output API v3, outputs to .vercel/output/
211
+ vercel deploy # deploy the Vercel build
212
+
213
+ aixyz build --output executable # self-contained binary, no Bun runtime required
214
+ ./.aixyz/output/server # run directly
215
+ ```
216
+
217
+ ### `aixyz erc-8004 register`
218
+
219
+ Register your agent's on-chain identity (ERC-8004). Creates
220
+ `app/erc-8004.ts` if it doesn't exist, asks for your deployment URL, and writes the registration back to the file after a successful on-chain transaction.
221
+
222
+ ```bash
223
+ aixyz erc-8004 register --url "https://my-agent.vercel.app" --chain base-sepolia --broadcast
224
+ ```
225
+
226
+ ### `aixyz erc-8004 update`
227
+
228
+ Update the metadata URI of a registered agent. Reads registrations from
229
+ `app/erc-8004.ts` and lets you select which one to update.
230
+
231
+ ```bash
232
+ aixyz erc-8004 update --url "https://new-domain.example.com" --broadcast
233
+ ```
234
+
235
+ ## Protocols
236
+
237
+ **A2A (Agent-to-Agent)** — Generates an agent card at `/.well-known/agent-card.json` and a JSON-RPC endpoint at
238
+ `/agent`. Protocol version 0.3.0. Other agents discover yours and send tasks via JSON-RPC.
239
+
240
+ **MCP (Model Context Protocol)** — Exposes your tools at `/mcp` using
241
+ `WebStandardStreamableHTTPServerTransport`. Any MCP client (Claude Desktop, VS Code, Cursor) can connect and call your tools.
242
+
243
+ **x402** — HTTP 402 micropayments. Clients pay per-request with an
244
+ `X-Payment` header containing cryptographic payment proof. No custodial wallets, no subscriptions. Payments are verified on-chain via a facilitator.
245
+
246
+ **ERC-8004** — On-chain agent identity. Register your agent on Ethereum, Base, Polygon, Scroll, Monad, BSC, or Gnosis so other agents and contracts can reference it.
247
+
248
+ ## Agent File Structure
249
+
250
+ ```
251
+ my-agent/
252
+ aixyz.config.ts # Agent config (required)
253
+ app/
254
+ agent.ts # Agent definition (required if no server.ts)
255
+ server.ts # Custom server (optional, overrides auto-generation)
256
+ erc-8004.ts # ERC-8004 identity registration (optional)
257
+ tools/
258
+ weather.ts # Tool exports (files starting with _ are ignored)
259
+ icon.png # Agent icon (served as static asset)
260
+ public/ # Static assets
261
+ vercel.json # Vercel deployment config
262
+ .env.local # Local environment variables
263
+ ```
264
+
265
+ ## Environment Variables
266
+
267
+ | Variable | Description |
268
+ | ---------------------- | ------------------------------------------------------------------------ |
269
+ | `X402_PAY_TO` | Default payment recipient address |
270
+ | `X402_NETWORK` | Default payment network (e.g. `eip155:8453`) |
271
+ | `X402_FACILITATOR_URL` | Custom facilitator (default: `https://x402.use-agently.com/facilitator`) |
272
+ | `CDP_API_KEY_ID` | Coinbase CDP API key ID (uses Coinbase facilitator) |
273
+ | `CDP_API_KEY_SECRET` | Coinbase CDP API key secret |
274
+ | `STRIPE_SECRET_KEY` | Enable experimental Stripe payment adapter |
275
+ | `OPENAI_API_KEY` | OpenAI API key (for agents using OpenAI models) |
276
+
277
+ ## Examples
278
+
279
+ | Example | Description |
280
+ | ------------------------- | ----------------------------------------------- |
281
+ | `boilerplate` | Minimal starter (auto-generated server) |
282
+ | `chainlink` | Chainlink data feeds with custom server |
283
+ | `flight-search` | Flight search with Stripe payments |
284
+ | `local-llm` | Local LLM via Docker (no external API) |
285
+ | `with-custom-facilitator` | Bring-your-own x402 facilitator |
286
+ | `with-custom-server` | Custom server setup |
287
+ | `with-express` | Express middleware integration |
288
+ | `sub-agents` | Multiple A2A endpoints from one deployment |
289
+ | `with-tests` | Agent with test examples |
290
+ | `fake-llm` | Fully deterministic testing with `fake()` model |
291
+
292
+ ## Contributing
293
+
294
+ ```bash
295
+ bun install
296
+ bun run build
297
+ bun run format
298
+ ```
299
+
300
+ ## License
301
+
302
+ MIT
package/app/index.ts CHANGED
@@ -86,6 +86,11 @@ export class AixyzApp {
86
86
  return this.middlewares;
87
87
  }
88
88
 
89
+ /** Find a registered plugin by name. Returns a read-only reference. */
90
+ getPlugin<T extends BasePlugin>(name: string): Readonly<T> | undefined {
91
+ return this.plugins.find((p) => p.name === name) as Readonly<T> | undefined;
92
+ }
93
+
89
94
  /** Dispatch a web-standard Request through payment verification, middleware, and route handler. */
90
95
  fetch = async (request: Request): Promise<Response> => {
91
96
  const response = await this.dispatch(request);
@@ -200,13 +200,41 @@ export class A2APlugin<TOOLS extends ToolSet = ToolSet> extends BasePlugin {
200
200
  const body = await request.json();
201
201
  const result = await jsonRpcTransport.handle(body);
202
202
 
203
- // If result is an AsyncGenerator (streaming), collect all chunks
203
+ // If result is an AsyncGenerator (streaming), return as SSE
204
204
  if (Symbol.asyncIterator in Object(result)) {
205
- const chunks: unknown[] = [];
206
- for await (const chunk of result as AsyncGenerator) {
207
- chunks.push(chunk);
208
- }
209
- return Response.json(chunks[chunks.length - 1]);
205
+ const stream = result as AsyncGenerator;
206
+ const readable = new ReadableStream({
207
+ async start(controller) {
208
+ const encoder = new TextEncoder();
209
+ try {
210
+ for await (const event of stream) {
211
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
212
+ }
213
+ } catch (error) {
214
+ const errorResponse = {
215
+ jsonrpc: "2.0",
216
+ id: body?.id ?? null,
217
+ error: {
218
+ code: -32603,
219
+ message: error instanceof Error ? error.message : "Streaming error.",
220
+ },
221
+ };
222
+ controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify(errorResponse)}\n\n`));
223
+ } finally {
224
+ controller.close();
225
+ }
226
+ },
227
+ cancel() {
228
+ stream.return?.(undefined);
229
+ },
230
+ });
231
+ return new Response(readable, {
232
+ headers: {
233
+ "Content-Type": "text/event-stream",
234
+ "Cache-Control": "no-cache",
235
+ "X-Accel-Buffering": "no",
236
+ },
237
+ });
210
238
  }
211
239
 
212
240
  return Response.json(result);
@@ -0,0 +1,624 @@
1
+ /** @jsxImportSource @kitajs/html */
2
+ import Html from "@kitajs/html";
3
+ import type { AixyzConfigRuntime, Entrypoint, ProtocolInfo } from "./index";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Icons (Lucide-style, matching shadcn conventions)
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const COPY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
10
+ const SPARKLES_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/></svg>`;
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Chip class constants (shared between JSX and inline script)
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const CHIP_ACTIVE = "border-primary/50 bg-primary/10 text-primary";
17
+ const CHIP_INACTIVE = "border-border bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Safe JSON serialization for embedding in <script> tags.
21
+ // JSON.stringify alone doesn't prevent the HTML parser from seeing </script>
22
+ // or <!-- sequences inside string literals, which can break out of the tag.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ function safeJsonStringify(value: unknown): string {
26
+ return JSON.stringify(value).replace(/</g, "\\u003c");
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // shadcn-style Primitives
31
+ // ---------------------------------------------------------------------------
32
+
33
+ function Badge({
34
+ children,
35
+ variant = "secondary",
36
+ }: {
37
+ children: string;
38
+ variant?: "secondary" | "outline" | "destructive" | "success";
39
+ }) {
40
+ const styles: Record<string, string> = {
41
+ secondary: "bg-secondary text-secondary-foreground border-transparent",
42
+ outline: "bg-transparent text-muted-foreground border-border",
43
+ destructive: "bg-chart-4/10 text-chart-4 border-chart-4/20",
44
+ success: "bg-success/10 text-success border-success/20",
45
+ };
46
+ return (
47
+ <span
48
+ class={`inline-flex items-center rounded-md border px-2 py-0.5 text-[10px] font-semibold tracking-wider uppercase font-mono transition-colors ${styles[variant]}`}
49
+ >
50
+ {children}
51
+ </span>
52
+ );
53
+ }
54
+
55
+ function Button({
56
+ children,
57
+ variant = "default",
58
+ size = "sm",
59
+ onclick,
60
+ }: {
61
+ children: string;
62
+ variant?: "default" | "secondary" | "ghost" | "outline";
63
+ size?: "sm" | "xs" | "icon";
64
+ onclick?: string;
65
+ }) {
66
+ const base =
67
+ "inline-flex items-center justify-center gap-1.5 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 cursor-pointer border-none";
68
+ const variants: Record<string, string> = {
69
+ default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
70
+ secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
71
+ ghost: "bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground",
72
+ outline:
73
+ "border border-border bg-background text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground",
74
+ };
75
+ const sizes: Record<string, string> = {
76
+ sm: "h-8 rounded-md px-3 text-[12px]",
77
+ xs: "h-7 rounded-md px-2.5 text-[11px]",
78
+ icon: "h-8 w-8 rounded-md",
79
+ };
80
+ return (
81
+ <button class={`${base} ${variants[variant]} ${sizes[size]}`} onclick={onclick}>
82
+ {children}
83
+ </button>
84
+ );
85
+ }
86
+
87
+ function Card({ children, className = "" }: { children: JSX.Element; className?: string }) {
88
+ return (
89
+ <div class={`rounded-xl border border-border bg-card text-card-foreground shadow-sm ${className}`}>{children}</div>
90
+ );
91
+ }
92
+
93
+ function Separator() {
94
+ return <div class="shrink-0 bg-border h-px w-full" />;
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Sections
99
+ // ---------------------------------------------------------------------------
100
+
101
+ function TopNav({ config, protocolBadges }: { config: AixyzConfigRuntime; protocolBadges: string[] }) {
102
+ return (
103
+ <nav class="flex items-center justify-between h-14 border-b border-border">
104
+ <div class="flex items-center gap-3">
105
+ <img
106
+ src="/icon.png"
107
+ alt=""
108
+ class="w-7 h-7 rounded-lg object-cover"
109
+ // On error, hide the image and show the fallback svg
110
+ onerror="this.style.display='none';this.nextElementSibling.style.display='flex'"
111
+ />
112
+ <div class="w-7 h-7 rounded-lg items-center justify-center overflow-hidden" style="display:none">
113
+ <svg width="256" height="256" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
114
+ <rect width="24" height="24" rx="3" fill="black" />
115
+ <path
116
+ fill-rule="evenodd"
117
+ clip-rule="evenodd"
118
+ d="M6.96388 5.15701C7.13522 5.05191 7.33432 4.99702 7.53297 5.00012C7.73163 5.00323 7.91978 5.06416 8.07077 5.1743L17.1705 11.8148C17.3129 11.9186 17.4156 12.0614 17.4657 12.2251C17.5158 12.3888 17.5109 12.5659 17.4518 12.7342C17.3927 12.9024 17.2819 13.0541 17.1335 13.1702C16.9851 13.2862 16.8058 13.3613 16.6182 13.386L13.9133 13.7438L16.5299 17.6214C16.6588 17.8126 16.6964 18.0487 16.6343 18.2779C16.5722 18.507 16.4155 18.7103 16.1988 18.8431C15.982 18.9759 15.723 19.0274 15.4785 18.9861C15.2341 18.9448 15.0244 18.8142 14.8954 18.623L12.2804 14.7461L10.817 16.9431C10.7157 17.0957 10.5696 17.2201 10.3973 17.3008C10.2249 17.3814 10.0341 17.4146 9.84892 17.3961C9.66375 17.3776 9.49259 17.3083 9.35712 17.1969C9.22166 17.0855 9.12798 16.9371 9.08796 16.7704L6.52209 6.12374C6.47934 5.94713 6.49896 5.75862 6.57819 5.58491C6.65742 5.41121 6.79223 5.26112 6.96353 5.15591L6.96388 5.15701Z"
119
+ fill="#ffffff"
120
+ />
121
+ </svg>
122
+ </div>
123
+ <div class="flex items-baseline gap-2">
124
+ <span class="text-[14px] font-semibold text-foreground">{Html.escapeHtml(config.name)}</span>
125
+ <span class="text-[11px] font-mono text-muted-foreground hidden xs:inline">
126
+ {"v" + Html.escapeHtml(config.version)}
127
+ </span>
128
+ </div>
129
+ </div>
130
+ {protocolBadges.length > 0 && <div class="flex items-center gap-1.5 flex-wrap">{protocolBadges.join("")}</div>}
131
+ </nav>
132
+ );
133
+ }
134
+
135
+ function HeroBanner({ config }: { config: AixyzConfigRuntime }) {
136
+ return (
137
+ <div class="py-8 sm:py-14 text-center">
138
+ <h1 class="text-[1.75rem] sm:text-[2rem] font-bold tracking-[-0.025em] text-foreground leading-[1.2]">
139
+ {Html.escapeHtml(config.name)}
140
+ </h1>
141
+ <p class="text-[15px] text-muted-foreground leading-relaxed mt-3 max-w-[420px] mx-auto">
142
+ {Html.escapeHtml(config.description)}
143
+ </p>
144
+ </div>
145
+ );
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Prompt script builder
150
+ // ---------------------------------------------------------------------------
151
+
152
+ type ContinuationWithMessages = {
153
+ label: string;
154
+ protocol: string;
155
+ text: string;
156
+ hasMessage: boolean;
157
+ messages: string[];
158
+ };
159
+
160
+ function buildPromptScript(
161
+ basePrompt: string,
162
+ continuations: ContinuationWithMessages[],
163
+ id: string,
164
+ defaultMsg: string,
165
+ hasExamples: boolean,
166
+ ): string {
167
+ return `
168
+ (function(){
169
+ var base=${safeJsonStringify(basePrompt)};
170
+ var conts=${safeJsonStringify(continuations.map((c) => c.text))};
171
+ var hasMsg=${safeJsonStringify(continuations.map((c) => c.hasMessage))};
172
+ var msgSets=${safeJsonStringify(continuations.map((c) => c.messages))};
173
+ var activeChipIdx=0;
174
+ var activeMsg=${safeJsonStringify(defaultMsg)};
175
+ var isCustom=${hasExamples ? "false" : "true"};
176
+ window.__promptBase=base;
177
+ var ACT=${safeJsonStringify(CHIP_ACTIVE)};
178
+ var INACT=${safeJsonStringify(CHIP_INACTIVE)};
179
+ function animateHeight(el){
180
+ var oldH=el.scrollHeight;
181
+ el.style.maxHeight=oldH+'px';
182
+ return function(){
183
+ requestAnimationFrame(function(){
184
+ var newH=el.scrollHeight;
185
+ if(newH!==oldH){el.style.maxHeight=newH+'px';}
186
+ setTimeout(function(){el.style.maxHeight='none';},250);
187
+ });
188
+ };
189
+ }
190
+ function render(animate){
191
+ var el=document.getElementById(${safeJsonStringify(id)});
192
+ var finish=animate&&el?animateHeight(el):null;
193
+ var t=conts[activeChipIdx]||'';
194
+ if(hasMsg[activeChipIdx]){t=t.replaceAll('%MSG%',activeMsg);}
195
+ window.__promptCont=t;
196
+ if(el)el.textContent=base+t;
197
+ if(finish)finish();
198
+ }
199
+ render(false);
200
+ function swapClass(el,from,to){el.className=el.className.replace(from,to);}
201
+ function rebuildMsgChips(idx){
202
+ var msgs=msgSets[idx]||[];
203
+ var container=document.getElementById('msg-chips');
204
+ if(!container)return;
205
+ var finishChips=animateHeight(container);
206
+ container.innerHTML='';
207
+ for(var i=0;i<msgs.length;i++){
208
+ var btn=document.createElement('button');
209
+ btn.className='msg-chip inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 sm:py-1 text-[11px] font-medium font-mono transition-colors cursor-pointer border w-full text-left '+(i===0?ACT:INACT);
210
+ btn.dataset.msgIndex=String(i);
211
+ btn.onclick=function(){window.__selectMessage(this);};
212
+ btn.textContent=msgs[i];
213
+ container.appendChild(btn);
214
+ }
215
+ var custom=document.createElement('button');
216
+ custom.className='msg-chip inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 sm:py-1 text-[11px] font-medium font-mono transition-colors cursor-pointer border w-full text-left '+(msgs.length===0?ACT:INACT);
217
+ custom.dataset.msgIndex='custom';
218
+ custom.onclick=function(){window.__selectMessage(this);};
219
+ custom.innerHTML='&#9998; Custom';
220
+ container.appendChild(custom);
221
+ finishChips();
222
+ var customRow=document.getElementById('custom-input-row');
223
+ if(msgs.length===0){
224
+ isCustom=true;
225
+ if(customRow){customRow.classList.remove('collapsed');customRow.style.maxHeight='60px';}
226
+ var inp=document.getElementById('custom-msg-input');
227
+ activeMsg=inp?inp.value||'<your message>':'<your message>';
228
+ } else {
229
+ isCustom=false;
230
+ if(customRow){customRow.style.maxHeight='0';customRow.classList.add('collapsed');}
231
+ activeMsg=msgs[0];
232
+ }
233
+ }
234
+ window.__selectChip=function(el){
235
+ var idx=parseInt(el.dataset.index,10);
236
+ activeChipIdx=idx;
237
+ var chips=document.querySelectorAll('.prompt-chip');
238
+ for(var i=0;i<chips.length;i++){
239
+ if(i===idx){swapClass(chips[i],INACT,ACT);}
240
+ else{swapClass(chips[i],ACT,INACT);}
241
+ }
242
+ rebuildMsgChips(idx);
243
+ render(true);
244
+ };
245
+ window.__selectMessage=function(el){
246
+ var midx=el.dataset.msgIndex;
247
+ var chips=document.querySelectorAll('.msg-chip');
248
+ for(var i=0;i<chips.length;i++){swapClass(chips[i],ACT,INACT);}
249
+ swapClass(el,INACT,ACT);
250
+ var customRow=document.getElementById('custom-input-row');
251
+ var msgs=msgSets[activeChipIdx]||[];
252
+ if(midx==='custom'){
253
+ isCustom=true;
254
+ if(customRow){customRow.classList.remove('collapsed');customRow.style.maxHeight='60px';}
255
+ var inp=document.getElementById('custom-msg-input');
256
+ activeMsg=inp?inp.value||'<your message>':'<your message>';
257
+ } else {
258
+ isCustom=false;
259
+ if(customRow){customRow.style.maxHeight='0';customRow.classList.add('collapsed');}
260
+ activeMsg=msgs[parseInt(midx,10)]||'<your message>';
261
+ }
262
+ render(true);
263
+ };
264
+ window.__onCustomInput=function(el){
265
+ activeMsg=el.value||'<your message>';
266
+ render(true);
267
+ };
268
+ })();
269
+ `;
270
+ }
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Prompt Section
274
+ // ---------------------------------------------------------------------------
275
+
276
+ function PromptSection({
277
+ basePrompt,
278
+ continuations,
279
+ id,
280
+ }: {
281
+ basePrompt: string;
282
+ continuations: ContinuationWithMessages[];
283
+ id: string;
284
+ }) {
285
+ const hasContinuations = continuations.length > 0;
286
+ const firstCont = hasContinuations ? continuations[0] : null;
287
+ const firstMessages = firstCont?.messages ?? [];
288
+ const defaultMsg = firstMessages.length > 0 ? firstMessages[0] : "<your message>";
289
+ const defaultContinuation = firstCont
290
+ ? firstCont.hasMessage
291
+ ? firstCont.text.replaceAll("%MSG%", defaultMsg)
292
+ : firstCont.text
293
+ : "";
294
+ const fullPrompt = basePrompt + defaultContinuation;
295
+
296
+ return (
297
+ <Card>
298
+ <>
299
+ <div class="flex items-center justify-between px-3.5 sm:px-5 py-3 border-b border-border">
300
+ <div class="flex items-center gap-2">
301
+ <span class="text-primary">{SPARKLES_ICON}</span>
302
+ <span class="text-[13px] font-semibold text-foreground">System Prompt</span>
303
+ <span class="text-[11px] text-muted-foreground hidden sm:inline">— paste into any LLM</span>
304
+ </div>
305
+ <Button
306
+ variant="default"
307
+ size="xs"
308
+ onclick={`navigator.clipboard.writeText(window.__promptBase+(window.__promptCont||'')).then(()=>{this.querySelector('.cp-l').textContent='Copied!';setTimeout(()=>{this.querySelector('.cp-l').textContent='Copy'},1500)})`}
309
+ >
310
+ {COPY_ICON + '<span class="cp-l">Copy</span>'}
311
+ </Button>
312
+ </div>
313
+ <div class={`p-3.5 sm:p-5 bg-muted/30${hasContinuations ? "" : " rounded-b-xl"}`}>
314
+ <code
315
+ class="block text-[11.5px] sm:text-[12.5px] leading-[1.7] sm:leading-[1.8] font-mono text-muted-foreground whitespace-pre-wrap break-words"
316
+ id={id}
317
+ >
318
+ {Html.escapeHtml(fullPrompt)}
319
+ </code>
320
+ </div>
321
+ {hasContinuations && (
322
+ <div class="px-3.5 sm:px-5 py-3 border-t border-border">
323
+ <span class="block text-[11px] text-muted-foreground font-medium mb-1.5">Use via:</span>
324
+ <div class="flex flex-wrap gap-1.5">
325
+ {continuations.map((c, i) => (
326
+ <button
327
+ class={`prompt-chip inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 sm:py-1 text-[11px] font-medium font-mono transition-colors cursor-pointer border ${i === 0 ? CHIP_ACTIVE : CHIP_INACTIVE}`}
328
+ data-index={String(i)}
329
+ onclick="window.__selectChip(this)"
330
+ >
331
+ {Html.escapeHtml(c.label) +
332
+ " " +
333
+ `<span class="text-[9px] opacity-60 uppercase">${Html.escapeHtml(c.protocol)}</span>`}
334
+ </button>
335
+ ))}
336
+ </div>
337
+ </div>
338
+ )}
339
+ {hasContinuations && (
340
+ <div id="msg-row" class="px-3.5 sm:px-5 pb-3 border-t-0">
341
+ <span class="block text-[11px] text-muted-foreground font-medium mb-1.5">Message:</span>
342
+ <div id="msg-chips" class="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
343
+ {firstMessages.map((m, i) => (
344
+ <button
345
+ class={`msg-chip inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 sm:py-1 text-[11px] font-medium font-mono transition-colors cursor-pointer border w-full text-left ${i === 0 ? CHIP_ACTIVE : CHIP_INACTIVE}`}
346
+ data-msg-index={String(i)}
347
+ onclick="window.__selectMessage(this)"
348
+ >
349
+ {Html.escapeHtml(m)}
350
+ </button>
351
+ ))}
352
+ <button
353
+ class={`msg-chip inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 sm:py-1 text-[11px] font-medium font-mono transition-colors cursor-pointer border w-full text-left ${firstMessages.length === 0 ? CHIP_ACTIVE : CHIP_INACTIVE}`}
354
+ data-msg-index="custom"
355
+ onclick="window.__selectMessage(this)"
356
+ >
357
+ {"&#9998; Custom"}
358
+ </button>
359
+ </div>
360
+ <div
361
+ id="custom-input-row"
362
+ class={`mt-2${firstMessages.length === 0 ? "" : " collapsed"}`}
363
+ style={firstMessages.length === 0 ? "max-height:60px" : "max-height:0"}
364
+ >
365
+ <input
366
+ type="text"
367
+ id="custom-msg-input"
368
+ placeholder="Type your message..."
369
+ class="w-full rounded-md border border-border bg-background px-2.5 py-2 sm:py-1.5 text-[12px] sm:text-[11px] font-mono text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-ring"
370
+ oninput="window.__onCustomInput(this)"
371
+ />
372
+ </div>
373
+ </div>
374
+ )}
375
+ {hasContinuations && (
376
+ <script>{buildPromptScript(basePrompt, continuations, id, defaultMsg, firstMessages.length > 0)}</script>
377
+ )}
378
+ </>
379
+ </Card>
380
+ );
381
+ }
382
+
383
+ // ---------------------------------------------------------------------------
384
+ // Skill-to-tool matching
385
+ // ---------------------------------------------------------------------------
386
+
387
+ type Skill = NonNullable<AixyzConfigRuntime["skills"]>[number];
388
+
389
+ export function getExamplesForTool(toolName: string, skills: Skill[]): string[] {
390
+ const name = toolName.toLowerCase();
391
+ const matched = skills.filter((s) => {
392
+ const sId = s.id.toLowerCase();
393
+ const sName = s.name.toLowerCase();
394
+ const sTags = (s.tags ?? []).map((t) => t.toLowerCase());
395
+ return (
396
+ sId.includes(name) ||
397
+ sName.includes(name) ||
398
+ name.includes(sId.replace(/-/g, "")) ||
399
+ sTags.some((t) => name.includes(t) || t.includes(name))
400
+ );
401
+ });
402
+ const examples =
403
+ matched.length > 0 ? matched.flatMap((s) => s.examples ?? []) : skills.flatMap((s) => s.examples ?? []);
404
+ return examples;
405
+ }
406
+
407
+ // ---------------------------------------------------------------------------
408
+ // Prompt builder
409
+ // ---------------------------------------------------------------------------
410
+
411
+ function buildAgentPrompt(config: AixyzConfigRuntime): string {
412
+ const agentUrl = config.url ?? "AGENT_URL";
413
+ const skillsList =
414
+ config.skills && config.skills.length > 0
415
+ ? config.skills.map((s) => `- ${s.name}: ${s.description}`).join("\n")
416
+ : "";
417
+
418
+ let prompt = `You have access to an AI agent called "${config.name}" at ${agentUrl} — ${config.description.replace(/\.$/, "").toLowerCase()}. Use the \`use-agently\` CLI (npx use-agently) to interact with it.`;
419
+ if (skillsList) prompt += `\n\nSkills:\n${skillsList}`;
420
+ return prompt;
421
+ }
422
+
423
+ const CONTINUATION_SUFFIX = `\n\nWhen I ask for something this agent can help with, send it a clear message using the command above. Share the agent's response with me. You can send multiple messages to follow up or refine.`;
424
+
425
+ function buildContinuation(
426
+ entry: Entrypoint,
427
+ agentUrl: string,
428
+ configName: string,
429
+ ): { label: string; protocol: string; text: string; hasMessage: boolean } {
430
+ if (entry.protocol === "a2a") {
431
+ const prefix = entry.path.slice(1, -"/agent".length);
432
+ const uri = prefix ? new URL(`${prefix}/`, agentUrl).toString() : agentUrl;
433
+ const label = prefix || configName;
434
+ return {
435
+ label,
436
+ protocol: "A2A",
437
+ text:
438
+ `\n\nTo call this agent, run:\nnpx use-agently a2a send --uri ${uri} -m "<your message>"\n\nMy request: "%MSG%"` +
439
+ CONTINUATION_SUFFIX,
440
+ hasMessage: true,
441
+ };
442
+ }
443
+ const uri = new URL(entry.path, agentUrl).toString();
444
+ const args = (entry.inputSchema ? JSON.stringify(entry.inputSchema) : "{}").replaceAll("'", "'\\''");
445
+ return {
446
+ label: entry.name,
447
+ protocol: "MCP",
448
+ text:
449
+ `\n\nTo call the "${entry.name}" tool, run:\nnpx use-agently mcp call --uri ${uri} --tool ${entry.name} --args '${args}'` +
450
+ `\n\nMy request: "%MSG%"` +
451
+ `\n\nWhen I ask for something this tool can help with, determine the appropriate --args from my request and run the command above. Share the tool's response with me. You can run the command multiple times to refine.`,
452
+ hasMessage: true,
453
+ };
454
+ }
455
+
456
+ // ---------------------------------------------------------------------------
457
+ // Page
458
+ // ---------------------------------------------------------------------------
459
+
460
+ export function renderHtml(config: AixyzConfigRuntime, protocols: ProtocolInfo): string {
461
+ const agentUrl = config.url ?? "AGENT_URL";
462
+ const basePrompt = buildAgentPrompt(config);
463
+
464
+ const rawContinuations = protocols.entrypoints.map((e) => buildContinuation(e, agentUrl, config.name));
465
+
466
+ const allSkills = config.skills ?? [];
467
+ const allExamples = allSkills.flatMap((s) => s.examples ?? []);
468
+ const continuations: ContinuationWithMessages[] = rawContinuations.map((c, i) => ({
469
+ ...c,
470
+ messages:
471
+ protocols.entrypoints[i].protocol === "a2a"
472
+ ? allExamples
473
+ : getExamplesForTool(protocols.entrypoints[i].name, allSkills),
474
+ }));
475
+
476
+ const protocolBadges: string[] = [];
477
+ if (protocols.a2a) protocolBadges.push((<Badge variant="secondary">A2A</Badge>) as string);
478
+ if (protocols.mcp) protocolBadges.push((<Badge variant="secondary">MCP</Badge>) as string);
479
+ if (protocols.entrypoints.some((e) => e.paid))
480
+ protocolBadges.push((<Badge variant="destructive">x402</Badge>) as string);
481
+
482
+ return (
483
+ "<!doctype html>" +
484
+ (
485
+ <html lang="en" class="dark">
486
+ <head>
487
+ <meta charset="utf-8" />
488
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
489
+ <title>{Html.escapeHtml(config.name)}</title>
490
+ <link rel="icon" href="/favicon.ico" />
491
+ <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
492
+ <style type="text/tailwindcss">{`
493
+ @theme {
494
+ --font-sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
495
+ --font-mono: ui-monospace, SFMono-Regular, "Cascadia Code", "Fira Code", monospace;
496
+ --radius: 0.625rem;
497
+
498
+ --color-background: hsl(240 6% 6%);
499
+ --color-foreground: hsl(240 5% 90%);
500
+ --color-card: hsl(240 5% 8%);
501
+ --color-card-foreground: hsl(240 5% 90%);
502
+ --color-popover: hsl(240 5% 8%);
503
+ --color-popover-foreground: hsl(240 5% 90%);
504
+ --color-primary: hsl(239 84% 67%);
505
+ --color-primary-foreground: hsl(0 0% 100%);
506
+ --color-secondary: hsl(240 5% 13%);
507
+ --color-secondary-foreground: hsl(240 4% 65%);
508
+ --color-muted: hsl(240 5% 13%);
509
+ --color-muted-foreground: hsl(240 4% 55%);
510
+ --color-accent: hsl(240 5% 15%);
511
+ --color-accent-foreground: hsl(240 5% 90%);
512
+ --color-destructive: hsl(0 63% 31%);
513
+ --color-destructive-foreground: hsl(0 0% 98%);
514
+ --color-border: hsl(240 4% 14%);
515
+ --color-input: hsl(240 4% 14%);
516
+ --color-ring: hsl(239 84% 67%);
517
+ --color-chart-1: hsl(239 84% 67%);
518
+ --color-chart-4: hsl(38 92% 50%);
519
+ --color-success: hsl(142 71% 45%);
520
+ }
521
+
522
+ body {
523
+ background: var(--color-background);
524
+ color: var(--color-foreground);
525
+ font-feature-settings: "cv11", "ss01";
526
+ -webkit-font-smoothing: antialiased;
527
+ -moz-osx-font-smoothing: grayscale;
528
+ }
529
+
530
+ /* Top accent bar */
531
+ body::before {
532
+ content: "";
533
+ position: fixed;
534
+ top: 0;
535
+ left: 0;
536
+ right: 0;
537
+ height: 1px;
538
+ background: linear-gradient(
539
+ 90deg,
540
+ transparent 5%,
541
+ color-mix(in srgb, var(--color-primary) 40%, transparent) 20%,
542
+ var(--color-primary) 50%,
543
+ color-mix(in srgb, var(--color-primary) 40%, transparent) 80%,
544
+ transparent 95%
545
+ );
546
+ z-index: 9999;
547
+ }
548
+
549
+ /* Glow */
550
+ .glow {
551
+ background: radial-gradient(
552
+ ellipse 450px 160px at 50% 0%,
553
+ color-mix(in srgb, var(--color-primary) 4%, transparent),
554
+ transparent
555
+ );
556
+ }
557
+
558
+ /* Smooth layout transitions */
559
+ #agent-prompt, #msg-chips {
560
+ transition: max-height 0.2s ease-out;
561
+ overflow: hidden;
562
+ }
563
+ #custom-input-row {
564
+ transition: max-height 0.2s ease-out, opacity 0.15s ease-out, margin 0.2s ease-out;
565
+ overflow: hidden;
566
+ }
567
+ #custom-input-row.collapsed {
568
+ max-height: 0 !important;
569
+ opacity: 0;
570
+ margin-top: 0;
571
+ }
572
+
573
+ /* Entrance */
574
+ @keyframes enter {
575
+ from { opacity: 0; transform: translateY(4px); }
576
+ to { opacity: 1; transform: translateY(0); }
577
+ }
578
+ .anim { animation: enter 0.25s ease-out both; }
579
+
580
+ /* Scrollbar */
581
+ ::-webkit-scrollbar { width: 5px; }
582
+ ::-webkit-scrollbar-track { background: transparent; }
583
+ ::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 9999px; }
584
+
585
+ /* Selection */
586
+ ::selection {
587
+ background: color-mix(in srgb, var(--color-primary) 20%, transparent);
588
+ color: var(--color-foreground);
589
+ }
590
+ `}</style>
591
+ </head>
592
+ <body class="font-sans min-h-screen">
593
+ <div class="glow fixed inset-x-0 top-0 h-[300px] pointer-events-none" />
594
+
595
+ <div class="relative max-w-[620px] mx-auto px-4 sm:px-5 py-5 sm:py-8">
596
+ {/* ── Nav ─────────────────────────────────── */}
597
+ <div class="anim" style="animation-delay: 0ms">
598
+ <TopNav config={config} protocolBadges={protocolBadges} />
599
+ </div>
600
+
601
+ {/* ── Hero ────────────────────────────────── */}
602
+ <div class="anim" style="animation-delay: 30ms">
603
+ <HeroBanner config={config} />
604
+ </div>
605
+
606
+ {/* ── Prompt ──────────────────────────────── */}
607
+ <div class="anim" style="animation-delay: 60ms">
608
+ <PromptSection basePrompt={basePrompt} continuations={continuations} id="agent-prompt" />
609
+ </div>
610
+
611
+ {/* ── Footer ──────────────────────────────── */}
612
+ <footer class="mt-14 pb-6 anim" style="animation-delay: 120ms">
613
+ <Separator />
614
+ <div class="flex items-center justify-center gap-1.5 mt-5">
615
+ <span class="text-[11px] text-muted-foreground/60">powered by</span>
616
+ <span class="text-[11px] text-primary font-semibold">aixyz</span>
617
+ </div>
618
+ </footer>
619
+ </div>
620
+ </body>
621
+ </html>
622
+ )
623
+ );
624
+ }
@@ -0,0 +1,184 @@
1
+ import { getAixyzConfigRuntime } from "@aixyz/config";
2
+ import { z } from "zod";
3
+ import { BasePlugin } from "../../plugin";
4
+ import type { AixyzApp } from "../../index";
5
+ import type { MCPPlugin } from "../mcp";
6
+ import { renderHtml } from "./html";
7
+
8
+ export type AixyzConfigRuntime = ReturnType<typeof getAixyzConfigRuntime>;
9
+
10
+ export interface Entrypoint {
11
+ protocol: "a2a" | "mcp";
12
+ name: string;
13
+ path: string;
14
+ description?: string;
15
+ paid: boolean;
16
+ inputSchema?: Record<string, unknown>;
17
+ }
18
+
19
+ export interface ProtocolInfo {
20
+ a2a: boolean;
21
+ mcp: boolean;
22
+ entrypoints: Entrypoint[];
23
+ }
24
+
25
+ function prefersHtml(request: Request): boolean {
26
+ const accept = request.headers.get("accept") ?? "";
27
+ if (!/text\/html/i.test(accept)) return false;
28
+ return !/text\/html\s*;\s*q\s*=\s*0(\.0*)?\s*(,|$)/i.test(accept);
29
+ }
30
+
31
+ function renderMarkdown(config: AixyzConfigRuntime, protocols: ProtocolInfo): string {
32
+ const url = config.url ?? "AGENT_URL";
33
+
34
+ let md = `# ${config.name}\n\n`;
35
+ md += `${config.description} Use the \`use-agently\` CLI (npx use-agently) to interact with this agent.\n\n`;
36
+ md += `> v${config.version}`;
37
+ if (config.url) md += ` · ${config.url}`;
38
+ md += `\n\n`;
39
+
40
+ const badges: string[] = [];
41
+ if (protocols.a2a) badges.push("A2A");
42
+ if (protocols.mcp) badges.push("MCP");
43
+ if (protocols.entrypoints.some((e) => e.paid)) badges.push("x402");
44
+ if (badges.length > 0) md += `${badges.join(" · ")}\n\n`;
45
+
46
+ if (config.skills && config.skills.length > 0) {
47
+ md += `## Skills\n\n`;
48
+ for (const skill of config.skills) {
49
+ md += `### ${skill.name}\n\n`;
50
+ md += `${skill.description}\n\n`;
51
+ if (skill.tags && skill.tags.length > 0) {
52
+ md += `Tags: ${skill.tags.map((t) => `\`${t}\``).join(", ")}\n\n`;
53
+ }
54
+ if (skill.examples && skill.examples.length > 0 && protocols.a2a) {
55
+ md += `**Examples:**\n\n`;
56
+ md += `\`\`\`sh\n`;
57
+ for (const example of skill.examples) {
58
+ const safeExample = example.replace(/'/g, "'\\''");
59
+ md += `npx use-agently a2a send --uri ${url} -m '${safeExample}'\n`;
60
+ }
61
+ md += `\`\`\`\n\n`;
62
+ }
63
+ }
64
+ }
65
+
66
+ if (protocols.entrypoints.length > 0) {
67
+ md += `## Entrypoints\n\n`;
68
+ const a2aEntries = protocols.entrypoints.filter((e) => e.protocol === "a2a");
69
+ const mcpEntries = protocols.entrypoints.filter((e) => e.protocol === "mcp");
70
+ if (a2aEntries.length > 0) {
71
+ md += `### A2A Agents\n\n`;
72
+ for (const e of a2aEntries) {
73
+ md += `- **${e.name}** \`${e.path}\`${e.description ? ` — ${e.description}` : ""} (${e.paid ? "paid" : "free"})\n`;
74
+ }
75
+ md += `\n`;
76
+ }
77
+ if (mcpEntries.length > 0) {
78
+ md += `### MCP Tools\n\n`;
79
+ for (const e of mcpEntries) {
80
+ md += `- **${e.name}** \`${e.path}\`${e.description ? ` — ${e.description}` : ""} (${e.paid ? "paid" : "free"})\n`;
81
+ }
82
+ md += `\n`;
83
+ }
84
+ }
85
+
86
+ md += `---\n\n`;
87
+ md += `Use this agent:\n\n`;
88
+ md += `\`\`\`sh\n`;
89
+ if (protocols.a2a) {
90
+ md += `# Send a message via A2A\n`;
91
+ md += `npx use-agently a2a send --uri ${url} -m "your prompt here"\n\n`;
92
+ md += `# View agent card\n`;
93
+ md += `npx use-agently a2a card --uri ${url}\n`;
94
+ }
95
+ if (protocols.mcp) {
96
+ if (protocols.a2a) md += `\n`;
97
+ md += `# List tools via MCP\n`;
98
+ md += `npx use-agently mcp tools --uri ${url}\n`;
99
+ }
100
+ md += `\`\`\`\n`;
101
+
102
+ return md;
103
+ }
104
+
105
+ /** Plugin that serves agent info with content negotiation: markdown for agents, HTML for humans. */
106
+ export class IndexPagePlugin extends BasePlugin {
107
+ readonly name = "index-page";
108
+ private protocols: ProtocolInfo = { a2a: false, mcp: false, entrypoints: [] };
109
+
110
+ constructor(private path = "/") {
111
+ super();
112
+ }
113
+
114
+ register(app: AixyzApp): void {
115
+ const config = getAixyzConfigRuntime();
116
+ if (!this.path.startsWith("/")) {
117
+ throw new Error(`Invalid path: ${this.path}. Path must start with "/"`);
118
+ }
119
+
120
+ // Default to serve markdown, else explicitly asked for HTML (which browsers do by default)
121
+ app.route("GET", this.path, (request: Request) => {
122
+ if (prefersHtml(request)) {
123
+ return new Response(renderHtml(config, this.protocols), {
124
+ headers: { "Content-Type": "text/html; charset=utf-8", Vary: "Accept" },
125
+ });
126
+ }
127
+ return new Response(renderMarkdown(config, this.protocols), {
128
+ headers: { "Content-Type": "text/markdown; charset=utf-8", Vary: "Accept" },
129
+ });
130
+ });
131
+ }
132
+
133
+ initialize(app: AixyzApp): void {
134
+ const entrypoints: Entrypoint[] = [];
135
+
136
+ // Detect A2A agents from routes (POST */agent pattern)
137
+ for (const [key, entry] of app.routes) {
138
+ if (key.startsWith("POST ") && entry.path.endsWith("/agent")) {
139
+ const prefix = entry.path.slice(1, -"/agent".length); // e.g. "" or "foo"
140
+ const name = prefix || "agent";
141
+ entrypoints.push({
142
+ protocol: "a2a",
143
+ name,
144
+ path: entry.path,
145
+ paid: entry.payment?.scheme === "exact",
146
+ });
147
+ }
148
+ }
149
+
150
+ // Detect MCP tools from MCPPlugin
151
+ const mcpPlugin = app.getPlugin<MCPPlugin>("mcp");
152
+ if (mcpPlugin?.registeredTools) {
153
+ for (const tool of mcpPlugin.registeredTools) {
154
+ let inputSchema: Record<string, unknown> | undefined;
155
+ try {
156
+ const jsonSchema = z.toJSONSchema(tool.tool.inputSchema as z.ZodType);
157
+ const props = (jsonSchema as any).properties as
158
+ | Record<string, { type?: string; description?: string }>
159
+ | undefined;
160
+ if (props && Object.keys(props).length > 0) {
161
+ const example: Record<string, unknown> = {};
162
+ for (const [key, val] of Object.entries(props)) {
163
+ example[key] = `<${val.description || key}>`;
164
+ }
165
+ inputSchema = example;
166
+ }
167
+ } catch {}
168
+
169
+ entrypoints.push({
170
+ protocol: "mcp",
171
+ name: tool.name,
172
+ path: "/mcp",
173
+ description: tool.tool.description,
174
+ paid: tool.accepts?.scheme === "exact",
175
+ inputSchema,
176
+ });
177
+ }
178
+ }
179
+
180
+ this.protocols.a2a = entrypoints.some((e) => e.protocol === "a2a");
181
+ this.protocols.mcp = entrypoints.some((e) => e.protocol === "mcp");
182
+ this.protocols.entrypoints = entrypoints;
183
+ }
184
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aixyz",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "Payment-native SDK for AI Agent",
5
5
  "keywords": [
6
6
  "ai",
@@ -23,11 +23,13 @@
23
23
  "./app": "./app/index.ts",
24
24
  "./app/*": "./app/*.ts",
25
25
  "./app/adapters/*": "./app/adapters/*.ts",
26
+ "./app/plugins/index-page": "./app/plugins/index-page/index.ts",
26
27
  "./app/plugins/*": "./app/plugins/*.ts"
27
28
  },
28
29
  "bin": "bin.js",
29
30
  "files": [
30
31
  "**/*.ts",
32
+ "**/*.tsx",
31
33
  "!**/*.test.ts",
32
34
  "!test/**"
33
35
  ],
@@ -36,9 +38,10 @@
36
38
  },
37
39
  "dependencies": {
38
40
  "@a2a-js/sdk": "^0.3.10",
39
- "@aixyz/cli": "0.23.0",
40
- "@aixyz/config": "0.23.0",
41
- "@aixyz/erc-8004": "0.23.0",
41
+ "@aixyz/cli": "0.25.0",
42
+ "@aixyz/config": "0.25.0",
43
+ "@aixyz/erc-8004": "0.25.0",
44
+ "@kitajs/html": "^4.2.13",
42
45
  "@modelcontextprotocol/sdk": "^1.27.1",
43
46
  "@next/env": "^16.1.6",
44
47
  "@x402/core": "^2.3.1",
@@ -1,46 +0,0 @@
1
- import { getAixyzConfigRuntime } from "@aixyz/config";
2
- import { BasePlugin } from "../plugin";
3
- import type { AixyzApp } from "../index";
4
-
5
- /** Plugin that registers a plain-text index page displaying the agent's name, description, version, and skills. */
6
- export class IndexPagePlugin extends BasePlugin {
7
- readonly name = "index-page";
8
-
9
- constructor(private path = "/") {
10
- super();
11
- }
12
-
13
- register(app: AixyzApp): void {
14
- const config = getAixyzConfigRuntime();
15
- if (!this.path.startsWith("/")) {
16
- throw new Error(`Invalid path: ${this.path}. Path must start with "/"`);
17
- }
18
-
19
- app.route("GET", this.path, () => {
20
- let text = `${config.name}\n`;
21
- text += `${"=".repeat(config.name.length)}\n\n`;
22
- text += `Description: ${config.description}\n`;
23
- text += `Version: ${config.version}\n\n`;
24
-
25
- if (config.skills && config.skills.length > 0) {
26
- text += `Skills:\n`;
27
- config.skills.forEach((skill, index) => {
28
- text += `\n${index + 1}. ${skill.name}\n`;
29
- text += ` ID: ${skill.id}\n`;
30
- text += ` Description: ${skill.description}\n`;
31
- if (skill.tags && skill.tags.length > 0) {
32
- text += ` Tags: ${skill.tags.join(", ")}\n`;
33
- }
34
- if (skill.examples && skill.examples.length > 0) {
35
- text += ` Examples:\n`;
36
- skill.examples.forEach((example) => {
37
- text += ` - ${example}\n`;
38
- });
39
- }
40
- });
41
- }
42
-
43
- return new Response(text, { headers: { "Content-Type": "text/plain" } });
44
- });
45
- }
46
- }