Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
880ef74
feat: codegen cli
olliethedev Mar 24, 2026
3da427b
feat: enhance framework and adapter detection in init command to supp…
olliethedev Mar 24, 2026
21f8315
chore: remove nightly initialization workflow from GitHub Actions
olliethedev Mar 24, 2026
0fa2cb4
chore: update GitHub Actions workflow to use specific versions of act…
olliethedev Mar 24, 2026
f541898
feat: implement hash-based idempotency check in test initialization s…
olliethedev Mar 24, 2026
e34b273
chore: remove ui-builder from backend imports in scaffold plan
olliethedev Mar 24, 2026
bfc05aa
feat: improve CSS import handling to append imports correctly when fi…
olliethedev Mar 24, 2026
e598d91
refactor: simplify ProjectContext interface and update InitCliOptions…
olliethedev Mar 24, 2026
b68c40f
feat: update stack-client templates to use a static baseURL when plug…
olliethedev Mar 24, 2026
dcd9ccb
feat: add early return in patchCssImports for empty imports and updat…
olliethedev Mar 24, 2026
ccdf6ce
feat: add --plugins option to CLI documentation for enhanced plugin s…
olliethedev Mar 24, 2026
4804376
feat: enhance CLI initialization with plugin support and compile safe…
olliethedev Mar 24, 2026
204a525
feat: update release workflow to include README copying and typecheck…
olliethedev Mar 24, 2026
adb0b7f
feat: enhance CSS import handling to preserve blank lines and improve…
olliethedev Mar 24, 2026
48b8e87
refactor: extract file existence check into a separate utility functi…
olliethedev Mar 24, 2026
bf5b98e
fix: update stack templates to use triple braces for backendEntries r…
olliethedev Mar 24, 2026
4cf5e66
fix: enhance error handling in detectAlias function and add tests for…
olliethedev Mar 24, 2026
794a345
feat: add pagesLayoutPath to ScaffoldPlan and update initialization l…
olliethedev Mar 24, 2026
be68327
chore: update package.json for CLI and stack; change CLI binary path …
olliethedev Mar 24, 2026
be9c080
feat: implement project hash generation in test-init script for idemp…
olliethedev Mar 24, 2026
ad782b3
feat: update CLI documentation and scaffold plan to use camelCase for…
olliethedev Mar 24, 2026
18f2096
feat: update getGenerateHintForAdapter to accept configPath and adjus…
olliethedev Mar 24, 2026
09ebf0e
feat: implement caching for stack clients and update base URL retriev…
olliethedev Mar 25, 2026
09ebab7
feat: refine stack file path resolution logic in createInitCommand to…
olliethedev Mar 25, 2026
3dcf30a
refactor: streamline base URL retrieval logic in stack-client templat…
olliethedev Mar 25, 2026
3f97ce8
feat: utilize shared query client utility in react-router pages route…
olliethedev Mar 25, 2026
2ed8f4f
refactor: migrate stack template to shared library and remove framewo…
olliethedev Mar 25, 2026
db056d0
refactor: consolidate stack-client template into shared library, remo…
olliethedev Mar 25, 2026
08f027b
fix: handle missing CSS file gracefully in patchCssImports function a…
olliethedev Mar 25, 2026
0eb27ca
chore: bump version to 0.1.1 in package.json
olliethedev Mar 25, 2026
eb05aaf
fix: update patchCssImports to correctly insert imports after the las…
olliethedev Mar 25, 2026
e00ca91
chore: use diff library for diffs of file-writer.ts
olliethedev Mar 25, 2026
383c71b
feat: add BTST integration skill for AI agents and enhance documentation
olliethedev Mar 25, 2026
149f2d9
fix: update return statement retrieval logic in patchLayoutWithQueryC…
olliethedev Mar 25, 2026
d17cf80
docs: update AI agent skills section in README and installation docum…
olliethedev Mar 25, 2026
3be6707
refactor: agents.md to individial skills.md
olliethedev Mar 25, 2026
57006dd
feat: implement global singleton pattern for stack in Next.js to shar…
olliethedev Mar 25, 2026
c79a099
refactor: update route type imports and scaffold paths to use a unifi…
olliethedev Mar 25, 2026
4d03e56
test: update scaffold plan test to check for new route file naming co…
olliethedev Mar 25, 2026
3b26213
fix: handle optional cssImport in plugin metadata and filter valid CS…
olliethedev Mar 25, 2026
105a593
feat: enhance scaffold generation with support for layout overrides a…
olliethedev Mar 25, 2026
ea18ee1
feat: initialize shadcn Next.js baseline in test setup
olliethedev Mar 25, 2026
8c133d8
feat: add getBaseURL function to support dynamic base URL retrieval i…
olliethedev Mar 26, 2026
728e452
feat: specify shadcn version in test setup and validate Tailwind toke…
olliethedev Mar 26, 2026
36fe4f0
fix: update navigate function type in scaffold plan to ensure type sa…
olliethedev Mar 26, 2026
71dcf4e
feat: integrate shared query client utility in tanstack pages route t…
olliethedev Mar 26, 2026
a9c6ef4
feat: enhance plugin template context to conditionally include CMS im…
olliethedev Mar 26, 2026
91c8da7
feat: update scaffold plan to conditionally include CMS plugin and ad…
olliethedev Mar 26, 2026
85db0d4
feat: add better-auth-ui plugin
olliethedev Mar 26, 2026
7b073ca
refactor: remove direct packing of better-auth-ui and update installa…
olliethedev Mar 26, 2026
4f776d7
feat: enhance plugin installation process by adding support for extra…
olliethedev Mar 26, 2026
4ed7ee7
fix: update test-init script to conditionally install better-auth-ui …
olliethedev Mar 26, 2026
7a84f75
feat: add new plugins 'route-docs' and 'open-api' to CLI, update plug…
olliethedev Mar 26, 2026
ef9e56f
refactor: update API route handling to use createFileRoute and improv…
olliethedev Mar 26, 2026
d482a19
feat: ensure CMS is included when ui-builder is selected, update rela…
olliethedev Mar 26, 2026
373dae6
fix: filter out falsy values in plugin template context to ensure pro…
olliethedev Mar 26, 2026
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
92 changes: 92 additions & 0 deletions .agents/skills/btst-ai-context/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
name: btst-ai-context
description: Patterns for registering page-level AI context in BTST plugin pages so the AI chat widget understands the current page and can act on it. Use when adding useRegisterPageAIContext to a plugin page, implementing clientTools for AI-driven form filling or editor updates, registering server-side tool schemas in BUILT_IN_PAGE_TOOL_SCHEMAS, or wiring PageAIContextProvider in layouts.
---

# BTST AI Chat Page Context

Plugin pages can register AI context so the chat widget understands the current page and can act on it (fill forms, update editors, summarize content).

## useRegisterPageAIContext

Call inside `.internal.tsx` page components.

### Read-only (content pages)

```tsx
import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context"

// Pass null while data is loading — context is not registered until non-null
useRegisterPageAIContext(item ? {
routeName: "my-plugin-detail",
pageDescription: `Viewing: "${item.title}"\n\n${item.content?.slice(0, 16000)}`,
suggestions: ["Summarize this", "What are the key points?"],
} : null)
```

### With client-side tools (form/editor pages)

```tsx
import { useRef } from "react"
import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context"
import type { UseFormReturn } from "react-hook-form"

const formRef = useRef<UseFormReturn<any> | null>(null)

useRegisterPageAIContext({
routeName: "my-plugin-edit",
pageDescription: "User is editing an item. Help them fill out the form.",
suggestions: ["Fill in the form for me", "Suggest a title"],
clientTools: {
fillMyForm: async ({ title, description }) => {
if (!formRef.current) return { success: false, message: "Form not ready" }
formRef.current.setValue("title", title, { shouldValidate: true })
formRef.current.setValue("description", description, { shouldValidate: true })
return { success: true }
},
},
})
```

`clientTools` execute client-side only. Return `{ success: boolean, message?: string }`.

## Server-side tool schemas (first-party tools)

For first-party tools, add the server-side schema to `BUILT_IN_PAGE_TOOL_SCHEMAS` in `src/plugins/ai-chat/api/page-tools.ts`. No `execute` — that's handled client-side:

```typescript
// src/plugins/ai-chat/api/page-tools.ts
export const BUILT_IN_PAGE_TOOL_SCHEMAS = {
fillBlogForm: { /* existing */ },
updatePageLayers: { /* existing */ },
fillMyForm: {
description: "Fill the my-plugin edit form with the provided values",
parameters: z.object({
title: z.string().describe("Item title"),
description: z.string().optional().describe("Item description"),
}),
},
}
```

## PageAIContextProvider placement

`PageAIContextProvider` must wrap the **root layout**, above all `StackProvider` instances:

```tsx
import { PageAIContextProvider } from "@btst/stack/plugins/ai-chat/client/context"

export default function RootLayout({ children }) {
return <PageAIContextProvider>{children}</PageAIContextProvider>
}
```

In the monorepo example apps this is already wired — don't add it again there. In a consumer app, add it once to the root layout when integrating the ai-chat plugin.

## Reference examples in the codebase

| File | Pattern |
|---|---|
| `src/plugins/blog/client/components/pages/new-post-page.internal.tsx` | `fillBlogForm` (clientTools) |
| `src/plugins/blog/client/components/pages/post-page.internal.tsx` | Read-only context |
| `src/plugins/ui-builder/client/components/pages/page-builder-page.internal.tsx` | `updatePageLayers` (clientTools) |
205 changes: 205 additions & 0 deletions .agents/skills/btst-backend-plugin-dev/REFERENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# btst-backend-plugin-dev — Reference

## defineBackendPlugin shape (api/plugin.ts)

```typescript
import type { DBAdapter as Adapter } from "@btst/db"
import { defineBackendPlugin, createEndpoint } from "@btst/stack/plugins/api"
import { z } from "zod"
import { dbSchema } from "./db-schema"
import { listItems, getItemById } from "./getters"
import { createItem, updateItem, deleteItem } from "./mutations"

const ItemQuerySchema = z.object({ limit: z.coerce.number().optional() })
const CreateItemSchema = z.object({ name: z.string() })

export const myBackendPlugin = defineBackendPlugin({
name: "my-plugin",
dbPlugin: dbSchema,

// api factory — bound to shared adapter, no HTTP context
api: (adapter: Adapter) => ({
listItems: () => listItems(adapter),
getItemById: (id: string) => getItemById(adapter, id),
createItem: (data: CreateItemInput) => createItem(adapter, data),
}),

// routes factory — HTTP endpoints built with createEndpoint
routes: (adapter: Adapter) => {
const listItemsEndpoint = createEndpoint(
"/items",
{ method: "GET", query: ItemQuerySchema },
async (ctx) => {
return await listItems(adapter)
},
)

const createItemEndpoint = createEndpoint(
"/items",
{ method: "POST", body: CreateItemSchema },
async (ctx) => {
return await createItem(adapter, ctx.body)
},
)

const getItemEndpoint = createEndpoint(
"/items/:id",
{ method: "GET" },
async (ctx) => {
const item = await getItemById(adapter, ctx.params.id)
if (!item) throw ctx.error(404, { message: "Item not found" })
return item
},
)

return { listItems: listItemsEndpoint, createItem: createItemEndpoint, getItem: getItemEndpoint } as const
},
})

// Router type for client consumption
export type MyApiRouter = ReturnType<ReturnType<typeof myBackendPlugin>["routes"]>
```

### ctx object inside createEndpoint handlers

| Property | Description |
|---|---|
| `ctx.query` | Validated query params (when `query:` schema provided) |
| `ctx.body` | Validated request body (when `body:` schema provided) |
| `ctx.params` | URL path params (e.g. `:id` → `ctx.params.id`) |
| `ctx.headers` | Request `Headers` object |
| `ctx.request` | Raw `Request` object |
| `ctx.error(status, { message })` | Create an HTTP error — always `throw` the result |

---

## getters.ts

Pure DB functions — no HTTP context, no lifecycle hooks, always accept `adapter` as first arg:

```typescript
import type { DBAdapter as Adapter } from "@btst/db"
import type { Item } from "./types"

// Authorization hooks are NOT called — callers are responsible for access control
export async function listItems(adapter: Adapter): Promise<Item[]> {
return adapter.findMany({ model: "item" })
}

export async function getItemById(adapter: Adapter, id: string): Promise<Item | null> {
return adapter.findOne({ model: "item", where: { id } }) ?? null
}
```

---

## mutations.ts

Write operations — no auth hooks, no HTTP context. JSDoc disclaimer required:

```typescript
import type { DBAdapter as Adapter } from "@btst/db"
import type { CreateItemInput, Item } from "./types"

/**
* Create a new item directly in the database.
* Authorization hooks are NOT called — caller is responsible for access control.
*/
export async function createItem(adapter: Adapter, data: CreateItemInput): Promise<Item> {
return adapter.create({
model: "item",
data: { id: crypto.randomUUID(), ...data, createdAt: new Date() },
})
}

/**
* Update an existing item.
* Authorization hooks are NOT called — caller is responsible for access control.
*/
export async function updateItem(
adapter: Adapter,
id: string,
data: Partial<CreateItemInput>,
): Promise<Item | null> {
return adapter.update({ model: "item", where: { id }, data }) ?? null
}

/**
* Delete an item.
* Authorization hooks are NOT called — caller is responsible for access control.
*/
export async function deleteItem(adapter: Adapter, id: string): Promise<void> {
await adapter.delete({ model: "item", where: { id } })
}
```

---

## api/index.ts

Re-export getters and mutations for direct server-side import (SSG, scripts, AI tools):

```typescript
// Getters — read-only, no auth hooks
export { listItems, getItemById } from "./getters"

// Mutations — write ops, no auth hooks
export { createItem, updateItem, deleteItem } from "./mutations"

// Types for consumers
export type { MyApiRouter } from "./plugin"
export { MY_PLUGIN_QUERY_KEYS } from "./query-key-defs"
export { serializeItem } from "./serializers"
```

---

## Lifecycle hook implementation in routes

```typescript
routes: (adapter: Adapter) => {
const createItemEndpoint = createEndpoint(
"/items",
{ method: "POST", body: CreateItemSchema },
async (ctx) => {
const context = { body: ctx.body, headers: ctx.headers }

// before hook — throw to deny
await ctx.hooks?.onBeforeItemCreated?.(ctx.body, context)

const item = await createItem(adapter, ctx.body)

// after hook — fire and forget or await
await ctx.hooks?.onAfterItemCreated?.(item, context)

return item
},
)

return { createItem: createItemEndpoint } as const
},
```

Hook naming always follows: `onBefore{Entity}{Action}`, `onAfter{Entity}{Action}`, `on{Entity}{Action}Error`.

---

## Plugin stack() wiring (in stack.ts)

```typescript
import { stack } from "@btst/stack"
import { myBackendPlugin } from "./src/plugins/my-plugin/api/plugin"

export const myStack = stack({
basePath: "/api/data",
plugins: {
myPlugin: myBackendPlugin,
},
adapter: (db) => createDrizzleAdapter(schema, db, {}),
})

export const { handler, dbSchema } = myStack

// Direct server-side access (bypasses auth hooks):
const items = await myStack.api.myPlugin.listItems()
```
82 changes: 82 additions & 0 deletions .agents/skills/btst-backend-plugin-dev/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
name: btst-backend-plugin-dev
description: Patterns for writing BTST backend plugins inside the monorepo, including defineBackendPlugin structure, getters.ts/mutations.ts separation, the api factory, lifecycle hook naming conventions, and accessing the adapter in AI tool execute functions. Use when creating or modifying a backend plugin, adding DB getters or mutations, wiring the api factory, or implementing lifecycle hooks in src/plugins/{name}/api/.
---

# BTST Backend Plugin Development

## File structure

```
src/plugins/{name}/
api/
plugin.ts ← defineBackendPlugin entry
getters.ts ← read-only DB functions (no HTTP context)
mutations.ts ← write DB functions (no auth hooks)
index.ts ← re-exports getters + mutations + types
query-keys.ts ← React Query key factory
```

## Rules

- **`getters.ts`** — pure async DB functions only. No HTTP context, no lifecycle hooks. Always takes `adapter` as first arg.
- **`mutations.ts`** — write operations (create/update/delete). No auth hooks, no HTTP context. Add JSDoc: "Authorization hooks are NOT called."
- **`api/index.ts`** — re-export everything from getters + mutations for direct server-side import.
- The `api` factory and `routes` factory share the same adapter instance — bind getters inside the factory, don't pass adapter at call site.
- If the plugin has a one-time init step (e.g. `syncContentTypes`), call it inside each getter/mutation wrapper — not only inside `routes`.
- **Never** use `myStack.api.*` as a substitute for authenticated HTTP endpoints — auth hooks are not called.

## Key patterns

- Import `defineBackendPlugin` and `createEndpoint` from `"@btst/stack/plugins/api"` (not `@btst/stack/plugins`).
- Import the adapter type as `import type { DBAdapter as Adapter } from "@btst/db"`.
- Routes are defined with `createEndpoint(path, { method, query?, body? }, handler)` — not string-keyed `"GET /path"` objects.
- Route handlers return data directly (`return item`) — no `ctx.json()`.
- Throw errors with `throw ctx.error(statusCode, { message })`.
- The `routes` factory returns a named object: `return { listItems, createItem } as const`.
- Export the router type as `ReturnType<ReturnType<typeof myBackendPlugin>["routes"]>`.

## Lifecycle hook naming

Pattern: `onBefore{Entity}{Action}`, `onAfter{Entity}{Action}`, `on{Entity}{Action}Error`

```typescript
// Examples from existing plugins:
onBeforeListPosts, onPostsRead, onListPostsError
onBeforeCreatePost, onPostCreated, onCreatePostError
onBeforeUpdatePost, onPostUpdated, onUpdatePostError
onBeforeDeletePost, onPostDeleted, onDeletePostError
onBeforePost, onAfterPost // comments plugin (create comment)
onBeforeEdit, onAfterEdit // comments plugin (edit comment)
onBeforeDelete, onAfterDelete // comments plugin (delete comment)
onBeforeStatusChange, onAfterApprove
```

## Adapter in AI tool execute functions

`myStack` is a module-level const. The `execute` closure runs lazily (only on HTTP request), so `myStack` is always initialised by then:

```typescript
export const myStack = stack({ ... })

const myTool = tool({
execute: async (params) => {
await createKanbanTask(myStack.adapter, { title: params.title, columnId: "col-id" })
return { success: true }
}
})
```

## Gotchas

- **Wrong import path** — always import from `"@btst/stack/plugins/api"`, not `"@btst/stack/plugins"`.
- **Wrong adapter type** — use `import type { DBAdapter as Adapter } from "@btst/db"` in getters/mutations/plugin files.
- **`"GET /path"` string keys** — routes use `createEndpoint()`, not string-keyed method/path objects.
- **`ctx.json()`** — does not exist; return data directly from route handlers.
- **`stack().api` bypasses auth hooks** — never use for authenticated data access; enforce auth at the call site.
- **Plugin init not called via `api`** — if `routes` factory runs a setup (e.g. `syncContentTypes`), also await it inside each `api` getter wrapper.
- **Write ops in `getters.ts`** — write functions belong in `mutations.ts`, not `getters.ts`.

## Full code patterns

See [REFERENCE.md](REFERENCE.md) for complete `defineBackendPlugin`, getters, mutations, and `api/index.ts` code shapes.
Loading
Loading