diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..9bac67e --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "host39-web", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev", "--", "--port", "3107"], + "cwd": "web", + "port": 3107 + } + ] +} diff --git a/web/next-env.d.ts b/web/next-env.d.ts new file mode 100644 index 0000000..9edff1c --- /dev/null +++ b/web/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/web/src/app/about/page.tsx b/web/src/app/about/page.tsx new file mode 100644 index 0000000..d4f6a17 --- /dev/null +++ b/web/src/app/about/page.tsx @@ -0,0 +1,146 @@ +import Link from "next/link"; + +const SAMPLE_CARD = { + name: "Moon Bakery Orders Agent", + description: "Place and track orders at Moon Bakery.", + url: "https://moonbakery-orders.aws.example.com", + version: "1.0", + capabilities: { streaming: false, pushNotifications: false }, + authentication: { schemes: ["Bearer"] }, + skills: [ + { name: "placeOrder", description: "Place a new bakery order" }, + { name: "trackOrder", description: "Track an existing order status" }, + ], + provider: { organization: "Moon Bakery", url: "https://moonbakery.com" }, + _meta: { + identifier: "urn:ai:domain:moonbakery.com:agent:orders", + publicUrl: "https://agentcards.host39.org/moonbakery.com/orders.json", + hostedBy: "host39.org", + }, +}; + +export default function AboutPage() { + return ( + <> + {/* Hero */} +
+
+
+ Agent Card Hosting +
+

+ host your agents +
+ at predictable URLs +

+

+ host39 is a third-party A2A agent card host. Register your identity, create agent cards, + and publish them at stable public URLs - no server required. +

+
+ + Get started free + + + Go to dashboard + +
+
+
+ + {/* How it works */} +
+

+ How it works +

+

+ Three steps to publish your agent +

+ +
+ {[ + { + step: "01", + title: "Register", + desc: "Create an account with your email or a domain identity (for businesses). Domain users get URLs like /moonbakery.com/orders.json.", + }, + { + step: "02", + title: "Create a card", + desc: "Fill in your agent name, runtime URL, capabilities, authentication, and skills using our guided form.", + }, + { + step: "03", + title: "Share the URL", + desc: "Your card is live at a stable public URL. Register it with the NANDA Index so resolvers can find your agent by identity.", + }, + ].map((item) => ( +
+ + {item.step} + +

{item.title}

+

{item.desc}

+
+ ))} +
+
+ + {/* URL patterns */} +
+

+ URL patterns +

+

+ Predictable, stable, public +

+ +
+
+

Business / Domain

+ + /moonbakery.com/orders.json + +

+ Register with a domain identity to get clean, branded URLs for your business agents. +

+
+
+

Personal / Email

+ + /personal/john@hotmail.com/card.json + +

+ Register with your email for personal agent cards - great for developers and individuals. +

+
+
+
+ + {/* Sample agent card */} +
+

+ A2A Agent Card format +

+

+ Standard JSON, served instantly +

+ +
+
+            {JSON.stringify(SAMPLE_CARD, null, 2)}
+          
+
+
+ + ); +} diff --git a/web/src/app/dashboard/cards/[id]/page.tsx b/web/src/app/dashboard/cards/[id]/page.tsx index 716c8b8..892b9ce 100644 --- a/web/src/app/dashboard/cards/[id]/page.tsx +++ b/web/src/app/dashboard/cards/[id]/page.tsx @@ -8,9 +8,26 @@ import type { AgentCard, Me } from "@/lib/api"; import { clearAuthToken } from "@/lib/auth"; import { useRequireAuth } from "@/hooks/useRequireAuth"; +// ── Shared primitives (outshift utility classes) ────────────────────────────── + +const infoCardClass = + "bg-surface-light rounded-card border border-line p-6 shadow-card"; + +const inputClass = + "w-full h-10 rounded-control border-2 border-line bg-surface-light px-3 text-sm text-ink placeholder:text-ink-weak focus:outline-none focus:border-brand-500 transition-colors"; + +const textareaClass = + "w-full rounded-control border-2 border-line bg-surface-light px-3 py-2 text-sm text-ink placeholder:text-ink-weak focus:outline-none focus:border-brand-500 transition-colors resize-none"; + +const primaryBtnClass = + "inline-flex items-center justify-center h-9 rounded-control bg-brand-500 px-4 text-sm font-medium text-white hover:bg-brand-600 transition disabled:opacity-60"; + +const secondaryBtnClass = + "inline-flex items-center justify-center h-9 rounded-control border-2 border-line bg-surface-light px-3 text-sm font-medium text-ink hover:border-line-strong transition"; + function FieldLabel({ children }: { children: React.ReactNode }) { return ( -

+

{children}

); @@ -167,7 +184,7 @@ export default function EditCardPage() { if (loading) { return ( -
+
Loading…
@@ -177,7 +194,7 @@ export default function EditCardPage() { if (!card || !me) { return ( -
+
{error ?? "Card not found."}
@@ -191,9 +208,6 @@ export default function EditCardPage() { me.handle ); - const inputClass = - "w-full rounded-xl border border-black/10 px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-slate-300"; - return ( {/* Public URL banner */} {isPublic ? ( -
-

+

+

Public URL

- + {publicUrl} @@ -220,25 +234,25 @@ export default function EditCardPage() { href={publicUrl} target="_blank" rel="noopener noreferrer" - className="shrink-0 rounded-lg border border-black/10 px-3 py-1.5 text-xs text-slate-600 hover:bg-slate-50" + className="shrink-0 inline-flex items-center justify-center h-9 rounded-control border-2 border-line bg-surface-light px-3 text-xs font-medium text-ink hover:border-line-strong transition" > Open
) : ( -
-

Private

-

This card is not publicly accessible. Enable “Public” below to publish it.

+
+

Private

+

This card is not publicly accessible. Enable “Public” below to publish it.

)} {/* Edit form */} -
+ {/* Basic */}
-

Basic info

+

Basic info

Slug * @@ -267,17 +281,17 @@ export default function EditCardPage() { value={description} onChange={(e) => setDescription(e.target.value)} rows={3} - className={`${inputClass} resize-none`} + className={textareaClass} />
-
+
{/* Runtime */}
-

Runtime

+

Runtime

Runtime URL @@ -319,23 +333,23 @@ export default function EditCardPage() {
-
+
{/* Capabilities */}
-

Capabilities & Auth

+

Capabilities & Auth

-
+
@@ -363,7 +377,7 @@ export default function EditCardPage() { setStatus(e.target.value as "active" | "inactive")} - className={`${inputClass} bg-white`} + className={inputClass} > @@ -398,12 +412,12 @@ export default function EditCardPage() {
{error && ( -

+

{error}

)} {success && ( -

+

{success}

)} @@ -412,14 +426,14 @@ export default function EditCardPage() { @@ -427,32 +441,32 @@ export default function EditCardPage() { {/* Delete zone */} -
-

Danger zone

-

+

+

Danger zone

+

Permanently delete this agent card. This action cannot be undone.

{!confirmDelete ? ( ) : ( -
-

Are you sure?

+
+

Are you sure?

diff --git a/web/src/app/dashboard/cards/new/page.tsx b/web/src/app/dashboard/cards/new/page.tsx index b283c82..f2e0393 100644 --- a/web/src/app/dashboard/cards/new/page.tsx +++ b/web/src/app/dashboard/cards/new/page.tsx @@ -26,6 +26,26 @@ interface FormState { is_public: boolean; } +// ── Shared primitives (outshift utility classes) ────────────────────────────── + +const infoCardClass = + "bg-surface-light rounded-card border border-line p-6 shadow-card"; + +const inputClass = + "w-full h-10 rounded-control border-2 border-line bg-surface-light px-3 text-sm text-ink placeholder:text-ink-weak focus:outline-none focus:border-brand-500 transition-colors"; + +const textareaClass = + "w-full rounded-control border-2 border-line bg-surface-light px-3 py-2 text-sm text-ink placeholder:text-ink-weak focus:outline-none focus:border-brand-500 transition-colors resize-none"; + +const primaryBtnClass = + "inline-flex items-center justify-center h-9 rounded-control bg-brand-500 px-4 text-sm font-medium text-white hover:bg-brand-600 transition disabled:opacity-60"; + +const secondaryBtnClass = + "inline-flex items-center justify-center h-9 rounded-control border-2 border-line bg-surface-light px-3 text-sm font-medium text-ink hover:border-line-strong transition"; + +const microLabelClass = + "mb-1 block text-xs font-bold uppercase tracking-wide text-ink-weak"; + // ── Defaults per identity type ──────────────────────────────────────────────── function defaultsForMe(me: Me): Partial { @@ -50,15 +70,15 @@ function ctx(me: Me | null) { isSmb, slugPlaceholder: isSmb ? "orders" : "agent", slugHint: isSmb - ? 'Short name for this specific agent — e.g. "orders", "support", "tracking".' - : 'A simple identifier for your card — e.g. "agent", "assistant".', + ? 'Short name for this specific agent - e.g. "orders", "support", "tracking".' + : 'A simple identifier for your card - e.g. "agent", "assistant".', runtimePlaceholder: isSmb ? "https://my-agent.aws.example.com" : "https://my-agent.railway.app", runtimeHint: isSmb ? "The A2A endpoint where your business agent runs (AWS, Azure, GCP, etc.)." : "Where your personal agent is hosted (Railway, Vercel, Render, etc.). Optional.", providerHint: isSmb - ? "Your company name and website — pre-filled from your registered domain." - : "Optional — leave blank if you are the provider.", + ? "Your company name and website - pre-filled from your registered domain." + : "Optional - leave blank if you are the provider.", authHint: isSmb ? "How callers must authenticate to use this agent." : "Personal agents usually require user consent or no auth for public actions.", @@ -79,27 +99,27 @@ function Field({ mono?: boolean; textarea?: boolean; optional?: boolean; }) { const base = cn( - "w-full rounded-2xl border px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-slate-300 bg-white transition", + textarea ? textareaClass : inputClass, mono && "font-mono", - error ? "border-rose-300 bg-rose-50/40" : "border-black/10", + error && "border-[color:var(--color-danger)] focus:border-[color:var(--color-danger)]", ); return (