Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .claude/scheduled_tasks.lock

This file was deleted.

124 changes: 124 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Development Guide

## Prerequisites

- **Node.js** >= 22 (v24+ recommended)
- **pnpm** >= 10 (`corepack enable` to use the version pinned in templates)

## Getting Started

```bash
git clone https://github.com/BuilderIO/agent-native.git
cd agent-native/framework
pnpm install
```

The `postinstall` script automatically builds `@agent-native/core` and `@agent-native/pinpoint`, which other packages depend on.

## Development

### Run all template apps

```bash
pnpm run dev:all
```

This builds core first, then starts every template app in parallel on sequential ports.

### Run a single package or template

```bash
pnpm --filter mail dev # run the mail template
pnpm --filter calendar dev # run the calendar template
pnpm --filter @agent-native/core dev # watch-build core
pnpm --filter @agent-native/docs dev # run the docs site
```

### Electron desktop app

```bash
pnpm run dev:electron # run the desktop app
pnpm run dev:electron:apps # run with template apps
```

## Workspace Structure

This is a pnpm monorepo. Workspaces are defined in `pnpm-workspace.yaml`.

### Packages (`packages/`)

| Package | Description |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| `core` | Core framework library (`@agent-native/core`) -- CLI, server plugins, agent tools, Vite plugin |
| `desktop-app` | Electron desktop app |
| `mobile-app` | Mobile app |
| `docs` | Documentation site |
| `pinpoint` | Pinpoint package |
| `shared-app-config` | Shared app configuration |

### Templates (`templates/`)

Production-ready template apps that demonstrate the framework. Each template is a standalone app with its own `package.json`, Drizzle schema, actions, and UI.

Templates: `analytics`, `calendar`, `calorie-tracker`, `content`, `forms`, `issues`, `mail`, `recruiting`, `slides`, `starter`, `videos`

Each template uses the same scripts:

```bash
pnpm dev # start dev server (via agent-native dev)
pnpm build # production build
pnpm action <name> # run an agent action
pnpm typecheck # type-check
```

## Environment Variables

Templates read from `.env` in their own directory. Key variables:

| Variable | Purpose |
| ---------------------- | ------------------------------------------------------------- |
| `DATABASE_URL` | Database connection string (see below) |
| `ANTHROPIC_API_KEY` | API key for Claude (required for agent chat) |
| `ACCESS_TOKEN` | Enables auth in production mode; without it, auth is bypassed |
| `GOOGLE_CLIENT_ID` | Google OAuth client ID (for Gmail, Calendar integrations) |
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret |

### Database options

Set `DATABASE_URL` to connect to your database. When unset, defaults to a local SQLite file at `data/app.db`.

| Provider | Example `DATABASE_URL` |
| ---------------- | ---------------------------------------------------------- |
| SQLite (default) | _(unset, or `file:./data/app.db`)_ |
| Neon Postgres | `postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/db` |
| Supabase | `postgresql://user:pass@db.xxx.supabase.co:5432/postgres` |
| Turso (libSQL) | `libsql://your-db.turso.io?authToken=...` |
| Plain Postgres | `postgresql://user:pass@localhost:5432/mydb` |

All SQL must be dialect-agnostic -- never assume SQLite.

## Key Commands

Run these from the repo root:

| Command | Description |
| -------------------- | ------------------------------------------------------- |
| `pnpm run prep` | Format + typecheck + test in parallel (run before push) |
| `pnpm run fmt` | Format all files with Prettier |
| `pnpm run fmt:check` | Check formatting without writing |
| `pnpm run typecheck` | Type-check all packages and templates |
| `pnpm test` | Run tests (core + docs) |
| `pnpm run lint` | Format check + typecheck |

## Building

```bash
pnpm run build # build all packages and templates
```

Individual packages:

```bash
pnpm --filter @agent-native/core build
pnpm --filter mail build
```
2 changes: 2 additions & 0 deletions packages/core/src/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface MentionProvider {
icon?: string;
search: (
query: string,
/** The H3 event for the current request — use to make internal API calls */
event?: any,
) => MentionProviderItem[] | Promise<MentionProviderItem[]>;
}

Expand Down
218 changes: 218 additions & 0 deletions packages/core/src/client/AgentPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import {
IconClockHour3,
IconDotsVertical,
IconHistory,
IconTrash,
IconPlugConnected,
} from "@tabler/icons-react";
import {
MultiTabAssistantChat,
Expand Down Expand Up @@ -343,6 +345,9 @@ function AgentSettingsPopover({
<IntegrationsPanel />
</Suspense>
</div>
<div className="border-t border-border pt-3 mt-3">
<AgentsSection />
</div>
</div>
</div>,
document.body,
Expand All @@ -351,6 +356,219 @@ function AgentSettingsPopover({
);
}

// ─── Agents Management Section ───────────────────────────────────────────────

function AgentsSection() {
const [showAdd, setShowAdd] = useState(false);
const [name, setName] = useState("");
const [url, setUrl] = useState("");
const [description, setDescription] = useState("");
const nameRef = useRef<HTMLInputElement>(null);

// Fetch agents from resources
const [agents, setAgents] = useState<
{
id: string;
path: string;
name: string;
url: string;
description?: string;
}[]
>([]);
const [loading, setLoading] = useState(true);

const fetchAgents = useCallback(async () => {
try {
const res = await fetch("/_agent-native/resources?scope=all");
if (!res.ok) return;
Comment thread
builder-io-integration[bot] marked this conversation as resolved.
const data = await res.json();
const agentResources = (data.resources ?? []).filter(
(r: { path: string }) =>
r.path.startsWith("agents/") && r.path.endsWith(".json"),
);
const parsed = await Promise.all(
agentResources.map(async (r: { id: string; path: string }) => {
try {
const detail = await fetch(`/_agent-native/resources/${r.id}`);
if (!detail.ok) return null;
const d = await detail.json();
const config = JSON.parse(d.content);
return {
id: r.id,
path: r.path,
name: config.name,
url: config.url,
description: config.description,
};
} catch {
return null;
}
}),
);
setAgents(parsed.filter(Boolean));
} finally {
setLoading(false);
}
}, []);

useEffect(() => {
fetchAgents();
}, [fetchAgents]);

useEffect(() => {
if (showAdd) {
setName("");
setUrl("");
setDescription("");
const t = setTimeout(() => nameRef.current?.focus(), 50);
return () => clearTimeout(t);
}
}, [showAdd]);

const handleAdd = async () => {
const trimmedName = name.trim();
const trimmedUrl = url.trim();
if (!trimmedName || !trimmedUrl) return;

const id = trimmedName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
const agentJson = JSON.stringify(
{
id,
name: trimmedName,
description: description.trim() || undefined,
url: trimmedUrl,
color: "#6B7280",
},
null,
2,
);

try {
const res = await fetch("/_agent-native/resources", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: `agents/${id}.json`,
content: agentJson,
shared: true,
}),
});
if (res.ok) {
setShowAdd(false);
fetchAgents();
}
} catch {}
};

const handleDelete = async (agentId: string) => {
try {
const res = await fetch(`/_agent-native/resources/${agentId}`, {
method: "DELETE",
});
if (res.ok) fetchAgents();
} catch {}
};

return (
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-[11px] font-medium text-muted-foreground">
Agents
</label>
<button
onClick={() => setShowAdd(!showAdd)}
className="flex h-5 w-5 items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-accent/50"
title="Add agent"
>
{showAdd ? <IconX size={12} /> : <IconPlus size={12} />}
</button>
</div>

{showAdd && (
<div className="mb-2 flex flex-col gap-1.5 rounded-md border border-border bg-background p-2">
<input
ref={nameRef}
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleAdd();
if (e.key === "Escape") setShowAdd(false);
}}
className="w-full rounded border border-border bg-background px-2 py-1 text-[12px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
placeholder="Name"
/>
<input
value={url}
onChange={(e) => setUrl(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleAdd();
if (e.key === "Escape") setShowAdd(false);
}}
className="w-full rounded border border-border bg-background px-2 py-1 text-[12px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
placeholder="URL"
/>
<input
value={description}
onChange={(e) => setDescription(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleAdd();
if (e.key === "Escape") setShowAdd(false);
}}
className="w-full rounded border border-border bg-background px-2 py-1 text-[12px] text-foreground outline-none placeholder:text-muted-foreground/50 focus:ring-1 focus:ring-accent"
placeholder="Description (optional)"
/>
<div className="flex justify-end">
<button
onClick={handleAdd}
disabled={!name.trim() || !url.trim()}
className="rounded bg-accent px-2.5 py-1 text-[11px] font-medium text-foreground hover:bg-accent/80 disabled:opacity-40 disabled:pointer-events-none"
>
Add
</button>
</div>
</div>
)}

{loading ? (
<p className="text-[11px] text-muted-foreground/50">Loading...</p>
) : agents.length === 0 && !showAdd ? (
<p className="text-[11px] text-muted-foreground/50">
No agents configured. Add one to @-mention and communicate via A2A.
</p>
) : (
<div className="flex flex-col gap-1">
{agents.map((agent) => (
<div
key={agent.id}
className="group flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-accent/30"
>
<IconPlugConnected
size={13}
className="shrink-0 text-muted-foreground"
/>
<div className="flex-1 min-w-0">
<div className="text-[12px] font-medium text-foreground truncate">
{agent.name}
</div>
<div className="text-[10px] text-muted-foreground/60 truncate">
{agent.url}
</div>
</div>
<button
onClick={() => handleDelete(agent.id)}
className="flex h-5 w-5 items-center justify-center rounded text-muted-foreground/40 hover:text-red-400 opacity-0 group-hover:opacity-100"
title="Remove agent"
>
<IconTrash size={12} />
</button>
</div>
))}
</div>
)}
</div>
);
}

// ─── AgentPanel ─────────────────────────────────────────────────────────────

export interface AgentPanelProps extends Omit<
Expand Down
Loading
Loading