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 +302 -0
- package/app/index.ts +5 -0
- package/app/plugins/a2a.ts +34 -6
- package/app/plugins/index-page/html.tsx +624 -0
- package/app/plugins/index-page/index.ts +184 -0
- package/package.json +7 -4
- package/app/plugins/index-page.ts +0 -46
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);
|
package/app/plugins/a2a.ts
CHANGED
|
@@ -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),
|
|
203
|
+
// If result is an AsyncGenerator (streaming), return as SSE
|
|
204
204
|
if (Symbol.asyncIterator in Object(result)) {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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='✎ 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
|
+
{"✎ 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.
|
|
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.
|
|
40
|
-
"@aixyz/config": "0.
|
|
41
|
-
"@aixyz/erc-8004": "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
|
-
}
|