# Next.js API Routes, Zustand Stores & UI Architecture

> The complete surface of Next.js API routes under src/app/api/ (agents, jobs, kb, git, search, terminal, onboarding, registry, and more), the Zustand client state stores (app-store, tree-store, ai-panel-store, editor-store, search-store), and the major React component groups (editor with Tiptap, sidebar, agents panel, composer, skills browser, settings). Closes the reference with extension points: adding a new provider adapter, registering a skill source, and what to inspect next.

- Repository: hilash/cabinet
- GitHub: https://github.com/hilash/cabinet
- Human wiki: https://grok-wiki.com/public/wiki/hilash-cabinet-73c70f449a59
- Complete Markdown: https://grok-wiki.com/public/wiki/hilash-cabinet-73c70f449a59/llms-full.txt

## Source Files

- `src/app/api/agents`
- `src/app/api/kb`
- `src/app/api/jobs`
- `src/stores/app-store.ts`
- `src/stores/tree-store.ts`
- `src/stores/ai-panel-store.ts`
- `src/components/editor`
- `src/components/agents`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [src/app/api/agents/route.ts](src/app/api/agents/route.ts)
- [src/app/api/agents/providers/route.ts](src/app/api/agents/providers/route.ts)
- [src/app/api/agents/conversations/route.ts](src/app/api/agents/conversations/route.ts)
- [src/app/api/agents/tasks/route.ts](src/app/api/agents/tasks/route.ts)
- [src/app/api/agents/skills/route.ts](src/app/api/agents/skills/route.ts)
- [src/app/api/agents/skills/catalog/route.ts](src/app/api/agents/skills/catalog/route.ts)
- [src/app/api/jobs/library/route.ts](src/app/api/jobs/library/route.ts)
- [src/app/api/registry/route.ts](src/app/api/registry/route.ts)
- [src/app/api/search/route.ts](src/app/api/search/route.ts)
- [src/stores/app-store.ts](src/stores/app-store.ts)
- [src/stores/tree-store.ts](src/stores/tree-store.ts)
- [src/stores/ai-panel-store.ts](src/stores/ai-panel-store.ts)
- [src/stores/editor-store.ts](src/stores/editor-store.ts)
- [src/stores/search-store.ts](src/stores/search-store.ts)
- [src/lib/agents/provider-interface.ts](src/lib/agents/provider-interface.ts)
- [src/lib/agents/provider-registry.ts](src/lib/agents/provider-registry.ts)
- [src/components/editor/editor.tsx](src/components/editor/editor.tsx)
- [src/components/editor/extensions.ts](src/components/editor/extensions.ts)
- [src/components/agents/agent-live-panel.tsx](src/components/agents/agent-live-panel.tsx)
- [src/components/sidebar/sidebar.tsx](src/components/sidebar/sidebar.tsx)
- [src/components/composer/composer-input.tsx](src/components/composer/composer-input.tsx)
- [src/components/skills/skill-catalog-browser.tsx](src/components/skills/skill-catalog-browser.tsx)
</details>

# Next.js API Routes, Zustand Stores & UI Architecture

Cabinet is a local-first knowledge-base and AI-agent workbench built on Next.js. The server is split between Next.js API routes (under `src/app/api/`) that handle all data mutation, agent orchestration, and external integrations, and a set of Zustand stores that hold every piece of client-visible state. The React component tree is organised by domain — editor, sidebar, agents, composer, skills, and settings — and the two layers communicate exclusively through store reads and fetch calls to the API routes. Understanding these three layers (API routes, stores, components) gives a maintainer complete mental ownership of the application.

---

## API Routes (`src/app/api/`)

The following table lists every subdirectory and its primary responsibility:

| Route namespace | Methods | Purpose |
|---|---|---|
| `/api/agents` | GET / POST | List active/recent sessions + stats; launch a new agent run |
| `/api/agents/[id]` | GET / DELETE | Individual session detail and cancel |
| `/api/agents/conversations` | GET / POST | List, filter, and create AI conversation records |
| `/api/agents/tasks` | GET / POST / PATCH | Task inbox CRUD with cabinet-scoped visibility |
| `/api/agents/providers` | GET / PUT | List all registered providers with health-check results; update default + disabled set |
| `/api/agents/providers/[id]/models` | GET | Dynamic model list for one provider (lazy, cached 60 s) |
| `/api/agents/skills` | GET / POST | List installed skills; create a new skill from frontmatter + body |
| `/api/agents/skills/catalog` | GET | Search mode (`?q=`) → proxies skills.sh; detail mode (`?owner=&repo=`) → GitHub meta + audit |
| `/api/agents/skills/[key]` | GET / PUT / DELETE | Read, update, or remove a single skill |
| `/api/agents/skills/scan` | POST | Re-scan disk for installed skills |
| `/api/agents/library` | GET | List saved agent persona templates |
| `/api/agents/events` | GET (SSE) | Server-sent events stream for live agent output |
| `/api/kb` | — | Namespace root |
| `/api/kb/pages/meta` | GET | Page metadata list (title, frontmatter) |
| `/api/jobs/library` | GET | Static job-template catalog from `JOB_LIBRARY_TEMPLATES` |
| `/api/registry` | GET | Agent registry templates from `getRegistryTemplates()` |
| `/api/search` | GET | Proxy to the local daemon's search service |
| `/api/git/commit` / `diff` / `log` / `pull` / `restore` | POST / GET | Git operations on the knowledge-base directory |
| `/api/terminal/open` | POST | Open / join a PTY session |
| `/api/onboarding` | GET / POST | First-run state and completion flags |
| `/api/upload/[path]` | POST | File-upload endpoint used by the editor's drag-and-drop handler |
| `/api/telemetry` | POST | Fire-and-forget usage event beacon |
| `/api/health` | GET | Liveness probe |

### Agents root — `/api/agents`

`GET` returns the union of `getActiveSessions()`, `getRecentSessions()`, and `getAgentStats()` from `agent-manager`. `POST` accepts `{ prompt, taskTitle, taskId, workdir, providerId }` and calls `runAgent(...)`, returning the new `sessionId`.

Sources: [src/app/api/agents/route.ts:1-50]()

### Providers — `/api/agents/providers`

The response is built by iterating `providerRegistry.listAll()`, running `p.healthCheck()` concurrently for every provider, and merging in `readProviderSettings()` and `getProviderUsage()`. The response is cached in module scope for 15 s to avoid spawning CLI probes on every page mount. The `dynamicModels: true` flag on a provider record signals that a second request to `GET /api/agents/providers/:id/models` is needed for the real model list.

Sources: [src/app/api/agents/providers/route.ts:20-100]()

### Search — `/api/search`

The route is a thin proxy: it authenticates via `getOrCreateDaemonToken()`, builds the daemon URL, and forwards the query with a 5 s `AbortSignal` timeout. All full-text indexing lives inside the daemon process; this route only adapts the HTTP interface.

Sources: [src/app/api/search/route.ts:1-58]()

### Skills catalog — `/api/agents/skills/catalog`

Operates in two modes selected by query parameters. Search mode (`?q=`) calls `https://skills.sh/api/search`, enriches results with per-skill audit summaries from `fetchAuditsBatch`, and caches the enriched list for 1 h. Detail mode (`?owner=&repo=`) fetches GitHub repo metadata plus a skill's `SKILL.md` frontmatter via the raw CDN. `GITHUB_TOKEN` is read from the environment to raise the GitHub API rate-limit ceiling.

Sources: [src/app/api/agents/skills/catalog/route.ts:1-80]()

---

## System Architecture

```text
Browser (React + Zustand)
┌──────────────────────────────────────────────────────────┐
│  app-store   tree-store   editor-store   ai-panel-store  │
│  search-store                                            │
│  ┌────────┐  ┌──────────┐  ┌──────────┐  ┌───────────┐ │
│  │Sidebar │  │  Editor  │  │  Agents  │  │ Composer  │ │
│  │(tree)  │  │ (Tiptap) │  │  Panel   │  │ (tasks)   │ │
│  └────┬───┘  └────┬─────┘  └────┬─────┘  └─────┬─────┘ │
└───────┼───────────┼─────────────┼───────────────┼────────┘
        │ fetch     │ fetch        │ fetch          │ fetch
┌───────▼───────────▼─────────────▼───────────────▼────────┐
│            Next.js API Routes  (src/app/api/)             │
│  /agents  /providers  /conversations  /tasks  /skills     │
│  /kb      /search     /git            /terminal           │
└────────────────────────────┬──────────────────────────────┘
                             │
              ┌──────────────▼──────────────┐
              │  lib/agents/  (server-side)  │
              │  agent-manager, provider-    │
              │  registry, conversation-     │
              │  store, skills/loader        │
              └──────────────┬──────────────┘
                             │ CLI spawn / API call
              ┌──────────────▼──────────────┐
              │  Provider CLIs / Daemon      │
              │  (claude-code, gemini-cli,   │
              │   codex-cli, opencode, …)    │
              └─────────────────────────────┘
```

---

## Zustand Client Stores (`src/stores/`)

All stores are created with `create<State>()` from Zustand with no middleware. Persistence is implemented by hand using `localStorage` or `sessionStorage` where noted.

### `useAppStore` — global UI layout

The largest store. Holds every cross-cutting layout concern.

| State field | Type | Persisted | Description |
|---|---|---|---|
| `section` | `SelectedSection` | No | Current primary view (home, cabinet, page, agents, task, settings, registry, help) |
| `returnTo` | `SelectedSection \| null` | No | Breadcrumb for `pushSection` / `popReturnTo` |
| `navHistory` / `navIndex` | `string[]` / `number` | No | Hash-based back/forward stack (cap 50) |
| `terminalOpen` | `boolean` | No | Terminal panel visibility |
| `terminalTabs` | `TerminalTab[]` | No | Open PTY tab list |
| `terminalPosition` | `"bottom" \| "right"` | `localStorage` | Terminal dock position |
| `sidebarCollapsed` | `boolean` | `localStorage` | Sidebar collapse state |
| `sidebarDrawer` | `"data" \| "agents" \| "tasks"` | `localStorage` | Active sidebar drawer tab |
| `aiPanelCollapsed` | `boolean` | No | Right-side AI panel visibility |
| `cabinetVisibilityModes` | `Record<string, CabinetVisibilityMode>` | `localStorage` | Per-cabinet page visibility depth (own, children-1, children-2, all) |
| `taskPanelOpen/Mode/ComposeContext` | — | No | Task side-panel state machine (compose ↔ conversation) |
| `taskRailOpen` | `boolean` | No | Slim recent-tasks rail on right edge |
| `providers` / `defaultProviderId` / `defaultModel` | — | No | Hydrated from `GET /api/agents/providers` |

Key actions:
- `loadProviders()` — one-shot deduped fetch; guards with `providersLoaded`.
- `ensureProviderModels(providerId)` — lazy per-provider model hydration with in-flight deduplication via `inflightModelFetches`.
- `goBack()` / `goForward()` — manipulate `window.location.hash` from the `navHistory` ring buffer.
- `setTerminalPosition("right")` — also collapses `aiPanelCollapsed` to avoid overlap.

Sources: [src/stores/app-store.ts:1-250]()

### `useTreeStore` — knowledge-base file tree

Manages the sidebar tree state including expand/collapse, drag-and-drop, and all CRUD operations.

| State | Notes |
|---|---|
| `nodes: TreeNode[]` | Loaded from `GET /api/tree`; stale-while-revalidate from `localStorage` (`kb-tree-cache`) |
| `selectedPath: string \| null` | Currently focused file path |
| `expandedPaths: Set<string>` | Persisted to `localStorage` (`kb-expanded-paths`) |
| `movingPaths: Set<string>` | Optimistic in-flight moves; used to dim the dragged row |
| `focusTick: number` | Bumped to signal sidebar scroll-to-selected |
| `showHiddenFiles: boolean` | Persisted to `localStorage` |

`renamePage` is the most complex action: it calls `renamePageApi`, reloads the tree, follows the `selectedPath` to the new path, and then checks the `editor-store`'s `currentPath` — if the editor has the renamed page open, it reloads it; if the editor has an open referrer that was rewritten and is not dirty, it reloads that too. A toast with an undo token is dispatched via `cabinet:toast` custom event.

Sources: [src/stores/tree-store.ts:1-290]()

### `useEditorStore` — single-page editor

Owns the lifecycle of the currently open Markdown page.

| State | Notes |
|---|---|
| `currentPath` | Path loaded or loading |
| `content` | Raw Markdown string |
| `frontmatter` | Parsed frontmatter object |
| `saveStatus` | `"idle" \| "saving" \| "saved" \| "error"` |
| `loadStatus` | `"idle" \| "loading" \| "ok" \| "missing" \| "error"` |
| `isDirty` | True when content has unsaved edits |
| `lastSavedAt` | Epoch ms for "Saved · Xs ago" status bar |

`loadPage` implements stale-while-revalidate: it immediately paints from a single-entry `localStorage` page cache before the fetch resolves, then replaces with the server response. Auto-save triggers 500 ms after `updateContent` via a debounced `setTimeout`. `loadStatus: "missing"` lets the editor distinguish a 404 (offer to create the page) from a 5xx error (show error).

Sources: [src/stores/editor-store.ts:1-200]()

### `useAIPanelStore` — AI side panel sessions

Tracks two session lists that survive same-tab page navigation:

- **`editorSessions: EditorSession[]`** — sessions tied to a specific `pagePath`, persisted to `sessionStorage` so the live stream can reconnect after a browser refresh.
- **`agentSessions: AgentLiveSession[]`** — free-running agent sessions not tied to a page, similarly persisted.

`addEditorSession` / `markSessionCompleted` / `removeSession` write through to `sessionStorage`. On mount, `restoreSessionsFromStorage()` and `restoreAgentSessionsFromStorage()` re-hydrate the lists and set `reconnect: true` on restored entries so the streaming consumer knows to rejoin an existing SSE stream rather than start a new one.

Sources: [src/stores/ai-panel-store.ts:1-185]()

### `useSearchStore` — command palette

Drives the global search palette (`Cmd+K`). Fields:

| Field | Notes |
|---|---|
| `open` | Palette visibility |
| `query` | Current input string |
| `scope: SearchScope` | `"all" \| "pages" \| "agents" \| "tasks"` |
| `results: SearchResponse \| null` | Last daemon response |
| `recentQueries / recentPageIds` | Persisted (up to 8 each) in `localStorage` |
| `aiPending / aiResult` | AI-assisted query state |

`commitRecentQuery` and `commitRecentPage` deduplicate and cap their lists before writing to `localStorage`. The `setResults` action auto-selects the first result across all hit types.

Sources: [src/stores/search-store.ts:1-130]()

---

## React Component Groups

### Editor (`src/components/editor/`)

The editor is a Tiptap ProseMirror instance mounted in `editor.tsx`. The extension bundle registered in `extensions.ts` is curated for minimal bundle size — 13 highlight.js languages (bash, css, go, JavaScript, JSON, Markdown, Python, Rust, shell, SQL, TypeScript, XML, YAML) instead of the full 35+ `common` set.

**Custom extensions registered:**

| Extension | File | Purpose |
|---|---|---|
| `WikiLink` | `wiki-link-extension.ts` | `[[Page Name]]` cross-links with in-editor navigation |
| `CalloutExtension` | `callout-extension.ts` | Styled callout blocks |
| `ResizableImage` | `extensions/resizable-image` | Drag-to-resize embedded images |
| `EmbedExtension` | `extensions/embed-extension` | URL embed cards (video, tweet, etc.) |
| `CabinetMath` | `extensions/math-extension` | LaTeX math rendering |
| `DragHandle` | `extensions/drag-handle` | Block-level drag-and-drop |
| `IconExtension` | `extensions/icon-extension` | Inline icon picker |
| `HeadingAnchors` | `extensions/heading-anchors` | Auto-generated heading IDs |
| `AutoDirection` | `extensions/auto-direction` | RTL/LTR auto-detection per-block |
| `FindExtension` | `extensions/find` | In-editor find-and-highlight |
| `EditorMentionExtension` | `mention-extension.ts` | `@mention` chips for agent/page refs |
| `colorAndStyleExtensions` | `extensions/color-highlight` | Text color and highlight marks |

File upload (drag-and-drop or paste) is handled inline in `editor.tsx`; files are POST-ed to `/api/upload/:pagePath` and the returned URL is inserted as a Markdown image node.

Sources: [src/components/editor/extensions.ts:1-130](), [src/components/editor/editor.tsx:1-80]()

### Sidebar (`src/components/sidebar/`)

`sidebar.tsx` is a resizable panel (220–420 px, default 280 px) with pointer-drag resize logic. It reads `useAppStore.sidebarCollapsed`, `useAppStore.sidebarDrawer`, and `useTreeStore`. The drawer has three tabs — `data` (the tree view), `agents`, and `tasks` — controlled by `setSidebarDrawer`. The `TreeView` + `TreeNode` sub-components handle expand/collapse, context menus, drag-and-drop zones, and inline rename.

Sources: [src/components/sidebar/sidebar.tsx:1-60](), [src/stores/app-store.ts:54-60]()

### Agents Panel (`src/components/agents/`)

| Component | Purpose |
|---|---|
| `agent-list.tsx` | Gallery of all configured agents with filter tabs |
| `agent-detail-v2.tsx` | Full agent detail with composer, conversation list, and live session indicator |
| `agent-live-panel.tsx` | Inline terminal + heartbeat history for a running agent session |
| `conversation-live-view.tsx` | SSE-backed live stream view |
| `conversation-result-view.tsx` | Completed conversation viewer |
| `pending-actions-panel.tsx` | Human-approval queue for agent-proposed actions |
| `heartbeat-dialog.tsx` | Schedule a recurring heartbeat run |
| `new-routine-dialog.tsx` | Create a new scheduled routine |
| `provider-glyph.tsx` | Per-provider icon badge |

`agent-live-panel.tsx` reads `agentSessions` from `useAIPanelStore` and renders a `WebTerminal` component for each live session, including reconnect support when `session.reconnect === true`.

Sources: [src/components/agents/agent-live-panel.tsx:1-60]()

### Composer (`src/components/composer/`)

The composer is a reusable prompt-input surface used across the agents panel, task creation dialogs, and start-work flows. `ComposerInput` (`composer-input.tsx`) accepts a `UseComposerReturn` hook, `MentionableItem[]` for the `@mention` dropdown, optional `attachments`, and a `focusTint` for agent brand coloring. Structural props include `topRightOverlay` (for scheduling chips), `header`, `footer`, `actionsStart`, and a `secondaryAction` slot. `AttachmentChips` and `MentionChips` render selected context items; `MentionDropdown` is the suggestion list.

Sources: [src/components/composer/composer-input.tsx:1-60]()

### Skills Browser (`src/components/skills/`)

| Component | Purpose |
|---|---|
| `skill-catalog-browser.tsx` | Search-first browse of skills.sh; debounced 300 ms input, minimum 2 chars; each row shows install count + audit-pass ratio |
| `skill-library.tsx` | List of installed skills with usage stats |
| `skill-detail.tsx` | Individual skill view: description, allowed tools, source provenance |
| `skill-add-dialog.tsx` | Install-from-source dialog; wraps the catalog browser |
| `skill-picker.tsx` | Inline multi-select used in agent-compose forms |

`skill-catalog-browser` calls `GET /api/agents/skills/catalog?q=<query>` and passes the selected skill's `source` field to the parent via `onPick`, which then triggers preview and install.

Sources: [src/components/skills/skill-catalog-browser.tsx:1-60]()

### Settings (`src/components/settings/`)

`settings-page.tsx` composes: `api-keys-section` (per-provider key management), `built-in-tools-section` (toggle MCP tools), `cli-mcp-section` (CLI MCP server config), `integrations-hub-section` (Slack, GitHub, etc.), `data-locations-section` (cabinet root and output paths), `storage-backend-section`, and `uninstall-section`. Provider settings are written back via `PUT /api/agents/providers`.

---

## Provider Adapter System

The provider layer is the primary extension surface for adding new AI backends.

### `AgentProvider` interface

Defined in `src/lib/agents/provider-interface.ts`, the interface declares:

```ts
interface AgentProvider {
  id: string;                   // unique key
  name: string;
  type: "cli" | "api";
  isAvailable(): Promise<boolean>;
  healthCheck(): Promise<ProviderStatus>;

  // CLI providers
  buildOneShotInvocation?(prompt, workdir, opts?): CliProviderInvocation;
  buildSessionInvocation?(prompt, workdir, opts?): CliProviderInvocation;
  supportsTerminalResume?: boolean;

  // Dynamic model discovery
  listModels?(): Promise<ProviderModel[]>;

  // API providers
  runPrompt?(prompt, context): Promise<string>;
}
```

Sources: [src/lib/agents/provider-interface.ts:56-120]()

### Registered built-in providers

| Provider id | File | Type |
|---|---|---|
| `claude-code` | `providers/claude-code.ts` | CLI |
| `codex-cli` | `providers/codex-cli.ts` | CLI |
| `gemini-cli` | `providers/gemini-cli.ts` | CLI |
| `cursor-cli` | `providers/cursor-cli.ts` | CLI |
| `opencode` | `providers/opencode.ts` | CLI |
| `grok-cli` | `providers/grok-cli.ts` | CLI |
| `copilot-cli` | `providers/copilot-cli.ts` | CLI |
| `pi` | `providers/pi.ts` | CLI |

The singleton `providerRegistry` (module-level `ProviderRegistryImpl`) is the only write surface. Registering a new provider requires a single `providerRegistry.register(myProvider)` call; the registry's `listAll()` is the source for `GET /api/agents/providers`.

Sources: [src/lib/agents/provider-registry.ts:1-76]()

### Adding a new provider adapter

1. Create `src/lib/agents/providers/my-provider.ts` that implements `AgentProvider`.
2. Implement at minimum `isAvailable()`, `healthCheck()`, and `buildOneShotInvocation()`.
3. Add `providerRegistry.register(myProvider)` at the bottom of `src/lib/agents/provider-registry.ts`.
4. The provider will appear in `GET /api/agents/providers` responses and in the Settings → Providers UI automatically — no other wiring is required.
5. Set `dynamicModels: true` (i.e. implement `listModels()`) if the backend can report its available models at runtime; `useAppStore.ensureProviderModels()` will call the models endpoint lazily when the provider's tab opens.

---

## Skill Sources and Extension Points

Skills are file-system-based `SKILL.md` bundles. The skill loader (`src/lib/agents/skills/loader.ts`) resolves three origin classes:

| Origin | Description |
|---|---|
| `system` | Built-in skills shipped with Cabinet |
| `linked` | Symlinked from external repositories or packs |
| `legacy` | Skills present in the old flat `~/.cabinet/skills/` directory |

`GET /api/agents/skills` accepts an `?origins=system,linked,legacy` filter and returns all three by default. To register a new skill source:

1. Write a `SKILL.md` file in a directory named after the skill key (kebab-case).
2. POST to `POST /api/agents/skills` with `{ key, name, description, body, scope }` — scope can be `"root"` (global) or `"cabinet:<path>"` (cabinet-scoped). The route creates the directory and file, then calls `readSkill` to return the hydrated record.
3. For package-distributed skills, create a symlink in the linked-skills root so the loader picks them up as origin `"linked"`.

Sources: [src/app/api/agents/skills/route.ts:1-80]()

---

## Data Flow Summary

```text
User action (e.g. open page)
  │
  ▼
Zustand store action (tree-store.selectPage / editor-store.loadPage)
  │
  ├─► localStorage cache → immediate paint (stale content)
  │
  └─► fetch("/api/kb/...") or fetch("/api/tree")
          │
          ▼
      Next.js route handler (Node.js / server)
          │
          ├─► lib/agents/* (agent-manager, persona-manager, etc.)
          │       └─► provider CLI spawn / API call
          │
          └─► File system (markdown pages, SKILL.md, settings JSON)
```

The stores are the single source of truth for UI state; the API routes are the single write path to disk and external services. Components never access the file system or provider CLIs directly — they read from a store or call an API route.

---

## What to Inspect Next

- **`src/lib/agents/conversation-runner.ts`** — how a `POST /api/agents/conversations` turns a user prompt into a streamed PTY session, including system-prompt construction and the editor/manual prompt builders.
- **`src/lib/agents/adapters/`** — the adapter layer between the generic `AgentProvider` interface and the actual PTY launch for each CLI; `agentAdapterRegistry` parallels `providerRegistry`.
- **`server/search/`** — the daemon process that owns the full-text index; the Next.js search route is only a proxy into this process.
- **`src/app/api/agents/events/route.ts`** — the SSE endpoint that drives live agent output in `conversation-live-view.tsx` and `agent-live-panel.tsx`.
- **`src/lib/agents/skills/`** — `loader.ts`, `stats.ts`, `lock.ts`, and `upstream.ts` show the complete skill lifecycle from disk scan through install-count decoration.

Cabinet's architecture cleanly separates BYOC/BYOK concerns: the `AgentProvider` interface is provider-neutral, skill sources are portable file-system paths with no vendor lock-in, and the search daemon is a local process with no cloud dependency. Adding a model or skill source requires implementing one interface or creating one file directory — no framework internals need to change.
