chore: starter template

This commit is contained in:
e2e
2026-07-10 20:07:50 +02:00
commit 44d0c8239b
157 changed files with 6462 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
export { UserCard } from './src/UserCard'
export type { User, UserCardProps } from './src/UserCard'
export { stubQuery } from './src/fixtures/stubQuery'
export { stubMutation } from './src/fixtures/stubMutation'
export { EmptyState } from './src/EmptyState'
export { NotFoundState } from './src/NotFoundState'
export { PageLoader } from './src/PageLoader'
export { ServerErrorState } from './src/ServerErrorState'
// Layout primitives — compose these instead of hand-rolling page chrome.
export { PageHeader } from './src/PageHeader'
export type { PageHeaderProps } from './src/PageHeader'
export { Panel } from './src/Panel'
export type { PanelProps } from './src/Panel'
export { StatCard } from './src/StatCard'
export type { StatCardProps } from './src/StatCard'
export { StatGrid } from './src/StatGrid'
export type { StatGridProps } from './src/StatGrid'
// Data presentation — feed these the output of a list/stats RPC you implement.
export { DataTable } from './src/DataTable'
export type { DataTableProps, DataTableColumn } from './src/DataTable'
export { BarChart } from './src/BarChart'
export type { BarChartProps, BarChartDatum } from './src/BarChart'

View File

@@ -0,0 +1,31 @@
{
"name": "@project/components",
"version": "0.0.1",
"private": true,
"type": "module",
"exports": {
".": "./index.ts"
},
"scripts": {
"tsc": "tsc --noEmit"
},
"devDependencies": {
"@mantine/core": "^9.2.1",
"@mantine/hooks": "^9.2.1",
"@tanstack/react-query": "^5.66.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"lucide-react": "^0.456.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"typescript": "^5.9"
},
"peerDependencies": {
"@mantine/core": "^9.2.1",
"@mantine/hooks": "^9.2.1",
"@tanstack/react-query": "^5.66.0",
"lucide-react": "^0.456.0",
"react": "^19.2.5",
"react-dom": "^19.2.5"
}
}

View File

@@ -0,0 +1,57 @@
import React from 'react'
import { Box, Group, Stack, Text } from '@mantine/core'
export interface BarChartDatum {
label: React.ReactNode
value: number
/** Mantine color key for the bar (e.g. 'teal', 'red'). Defaults to the theme primary. */
color?: string
}
export interface BarChartProps {
data: BarChartDatum[]
/** Format the numeric value shown at the end of each bar. */
valueFormatter?: (value: number) => string
}
// A dependency-free horizontal bar chart (CSS only, theme colours). Feed it
// aggregated counts from a stats RPC — no charting library to install or break.
export function BarChart({ data, valueFormatter }: BarChartProps) {
const max = Math.max(1, ...data.map((d) => d.value))
const fmt = valueFormatter ?? ((n: number) => String(n))
return (
<Stack gap="sm">
{data.map((datum, index) => (
<Stack key={index} gap={4}>
<Group justify="space-between" gap="xs">
<Text size="sm">{datum.label}</Text>
<Text size="sm" fw={600}>
{fmt(datum.value)}
</Text>
</Group>
<Box
style={{
height: 8,
borderRadius: 'var(--mantine-radius-xl)',
background: 'var(--mantine-color-default-hover)',
overflow: 'hidden',
}}
>
<Box
style={{
height: '100%',
width: `${(datum.value / max) * 100}%`,
borderRadius: 'var(--mantine-radius-xl)',
background: datum.color
? `var(--mantine-color-${datum.color}-6)`
: 'var(--mantine-primary-color-6)',
transition: 'width 200ms ease',
}}
/>
</Box>
</Stack>
))}
</Stack>
)
}

View File

@@ -0,0 +1,85 @@
import React from 'react'
import { Center, Loader, Table, Text } from '@mantine/core'
export interface DataTableColumn<Row> {
/** Stable column id; also the default cell value (row[key]) when no render is given. */
key: string
header: React.ReactNode
/** Custom cell renderer. Defaults to String(row[key]). */
render?: (row: Row) => React.ReactNode
align?: 'left' | 'center' | 'right'
width?: number | string
}
export interface DataTableProps<Row> {
columns: DataTableColumn<Row>[]
rows: Row[]
/** Stable key per row (e.g. r => r.taskId). */
rowKey: (row: Row) => string
loading?: boolean
/** Shown when there are no rows (pass an <EmptyState/> for a rich version). */
empty?: React.ReactNode
onRowClick?: (row: Row) => void
}
// A generic, typed table. Wire it to any list RPC: implement listX, pass its
// rows here, and describe the columns. Pure presentation — no data fetching.
export function DataTable<Row>({
columns,
rows,
rowKey,
loading,
empty,
onRowClick,
}: DataTableProps<Row>) {
if (loading) {
return (
<Center py="xl">
<Loader size="sm" />
</Center>
)
}
if (rows.length === 0) {
return (
<Center py="xl">
{empty ?? (
<Text c="dimmed" size="sm">
Nothing here yet.
</Text>
)}
</Center>
)
}
return (
<Table highlightOnHover={!!onRowClick} verticalSpacing="sm" horizontalSpacing="md">
<Table.Thead>
<Table.Tr>
{columns.map((col) => (
<Table.Th key={col.key} style={{ textAlign: col.align ?? 'left', width: col.width }}>
{col.header}
</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows.map((row) => (
<Table.Tr
key={rowKey(row)}
onClick={onRowClick ? () => onRowClick(row) : undefined}
style={onRowClick ? { cursor: 'pointer' } : undefined}
>
{columns.map((col) => (
<Table.Td key={col.key} style={{ textAlign: col.align ?? 'left' }}>
{col.render
? col.render(row)
: String((row as Record<string, unknown>)[col.key] ?? '')}
</Table.Td>
))}
</Table.Tr>
))}
</Table.Tbody>
</Table>
)
}

View File

@@ -0,0 +1,58 @@
import { AlertCircle, Boxes, FolderOpen } from 'lucide-react'
import type { Story, StoryMeta } from './csf.types'
import { EmptyState } from './EmptyState'
const meta: StoryMeta = {
title: 'EmptyState',
component: EmptyState,
tags: ['feedback'],
argTypes: {
icon: { description: 'Lucide icon component displayed above the title', control: false },
title: { description: 'Primary heading text', control: 'text' },
description: { description: 'Supporting text below the title', control: 'text' },
action: { description: 'Label for the primary action button', control: 'text' },
onAction: { description: 'Callback fired when the action button is clicked', control: false },
compact: {
description: 'Reduces padding and icon size for inline contexts',
control: 'boolean',
},
docsHref: { description: 'Optional link shown as a Docs button', control: 'text' },
},
}
export default meta
function DefaultStory() {
return (
<EmptyState
icon={Boxes}
title="No items yet"
description="Create your first item to get started."
/>
)
}
export const Default: Story = { render: DefaultStory }
function WithActionStory() {
return (
<EmptyState
icon={FolderOpen}
title="No projects"
description="You don't have any projects yet."
action="New project"
onAction={() => {}}
/>
)
}
export const WithAction: Story = { render: WithActionStory }
function CompactStory() {
return (
<EmptyState
icon={AlertCircle}
title="Nothing here"
description="This section is currently empty."
compact
/>
)
}
export const Compact: Story = { render: CompactStory }

View File

@@ -0,0 +1,77 @@
import React from 'react'
import { Button, Center, Group, Stack, Text } from '@mantine/core'
import { ExternalLink } from 'lucide-react'
interface EmptyStateProps {
icon: React.ComponentType<{ size?: number; strokeWidth?: number }>
title: string
description: React.ReactNode
action?: string
onAction?: () => void
actionIcon?: React.ReactNode
actionLoading?: boolean
secondaryText?: React.ReactNode
docsHref?: string
compact?: boolean
}
export function EmptyState({
icon,
title,
description,
action,
onAction,
actionIcon,
actionLoading,
secondaryText,
docsHref,
compact = false,
}: EmptyStateProps) {
const Icon = icon
return (
<Center flex={1}>
<Stack
align="center"
justify="center"
gap={compact ? 'sm' : 'md'}
py={compact ? 'lg' : 'xl'}
style={{ minHeight: compact ? undefined : '60vh', width: '100%' }}
>
<Icon size={compact ? 36 : 48} strokeWidth={1} />
<Text size={compact ? 'lg' : 'xl'} fw={600}>
{title}
</Text>
<Text c="dimmed" ta="center" maw={500}>
{description}
</Text>
{docsHref || (action && onAction) ? (
<Group justify="center">
{docsHref ? (
<Button
component="a"
href={docsHref}
target="_blank"
rel="noopener noreferrer"
variant="default"
leftSection={<ExternalLink size={16} />}
>
Docs
</Button>
) : null}
{action && onAction ? (
<Button onClick={onAction} loading={actionLoading} leftSection={actionIcon}>
{action}
</Button>
) : null}
</Group>
) : null}
{secondaryText ? (
<Text c="dimmed" size="sm" ta="center" maw={500}>
{secondaryText}
</Text>
) : null}
</Stack>
</Center>
)
}

View File

@@ -0,0 +1,25 @@
import type { Story, StoryMeta } from './csf.types'
import { NotFoundState } from './NotFoundState'
const meta: StoryMeta = {
title: 'NotFoundState',
component: NotFoundState,
tags: ['feedback'],
argTypes: {
title: { description: 'Override the "Page not found" heading', control: 'text' },
description: { description: 'Override the default description text', control: 'text' },
},
}
export default meta
export const Default: Story = {}
function CustomStory() {
return (
<NotFoundState
title="Board not found"
description="The board you were looking for has been removed or never existed."
/>
)
}
export const Custom: Story = { render: CustomStory }

View File

@@ -0,0 +1,40 @@
import { Center, Stack, Text } from '@mantine/core'
interface NotFoundStateProps {
title?: string
description?: string
}
export function NotFoundState({
title = 'Page not found',
description = "The resource you're looking for doesn't exist or has been removed.",
}: NotFoundStateProps) {
return (
<Center flex={1}>
<Stack
align="center"
justify="center"
gap="xs"
py="xl"
style={{ minHeight: '60vh', width: '100%' }}
>
<Text
style={{
fontSize: 72,
lineHeight: 1,
letterSpacing: '-0.06em',
fontWeight: 600,
}}
>
404
</Text>
<Text size="xl" fw={600}>
{title}
</Text>
<Text ta="center" maw={520} c="dimmed">
{description}
</Text>
</Stack>
</Center>
)
}

View File

@@ -0,0 +1,32 @@
import { Button } from '@mantine/core'
import { Plus } from 'lucide-react'
import type { Story, StoryMeta } from './csf.types'
import { PageHeader } from './PageHeader'
const meta: StoryMeta = {
title: 'PageHeader',
component: PageHeader,
tags: ['layout'],
argTypes: {
title: { description: 'Page title', control: 'text' },
description: { description: 'Optional sub-text', control: 'text' },
actions: { description: 'Right-aligned actions slot', control: false },
},
}
export default meta
function DefaultStory() {
return <PageHeader title="Tasks" description="Everything on your plate, in one place." />
}
export const Default: Story = { render: DefaultStory }
function WithActionStory() {
return (
<PageHeader
title="Tasks"
description="Everything on your plate, in one place."
actions={<Button leftSection={<Plus size={16} />}>New task</Button>}
/>
)
}
export const WithAction: Story = { render: WithActionStory }

View File

@@ -0,0 +1,32 @@
import React from 'react'
import { Box, Group, Stack, Text, Title } from '@mantine/core'
export interface PageHeaderProps {
/** Page title. Pass an i18n string from the app (m.foo()). */
title: React.ReactNode
/** Optional one-line description under the title. */
description?: React.ReactNode
/** Right-aligned actions (buttons, menus). */
actions?: React.ReactNode
}
// The single shared page header every screen uses: a title, optional
// description, and a right-aligned action slot. Keeps headers consistent across
// pages — compose it, don't hand-roll a per-page header.
export function PageHeader({ title, description, actions }: PageHeaderProps) {
return (
<Group justify="space-between" align="flex-end" wrap="nowrap" mb="xl">
<Stack gap={4} style={{ minWidth: 0 }}>
<Title order={1} fz={26} fw={680} style={{ letterSpacing: '-0.03em', lineHeight: 1.1 }}>
{title}
</Title>
{description ? (
<Text c="dimmed" size="sm" style={{ lineHeight: 1.5 }}>
{description}
</Text>
) : null}
</Stack>
{actions ? <Box style={{ flexShrink: 0 }}>{actions}</Box> : null}
</Group>
)
}

View File

@@ -0,0 +1,11 @@
import type { Story, StoryMeta } from './csf.types'
import { PageLoader } from './PageLoader'
const meta: StoryMeta = {
title: 'PageLoader',
component: PageLoader,
tags: ['loading'],
}
export default meta
export const Default: Story = {}

View File

@@ -0,0 +1,16 @@
import { Center, Loader } from '@mantine/core'
export function PageLoader() {
return (
<Center
style={{
flex: 1,
width: '100%',
minWidth: 0,
minHeight: 0,
}}
>
<Loader size="md" />
</Center>
)
}

View File

@@ -0,0 +1,48 @@
import React from 'react'
import { Card, Group, Stack, Text, Title } from '@mantine/core'
export interface PanelProps {
/** Optional panel heading. */
title?: React.ReactNode
/** Optional sub-text under the heading. */
description?: React.ReactNode
/** Right-aligned header actions. */
actions?: React.ReactNode
/** Panel body. */
children: React.ReactNode
/** Remove inner padding (e.g. for a flush table). */
noPadding?: boolean
}
// A bordered content section with an optional titled header. The workhorse
// container — group related content into Panels instead of loose Cards.
export function Panel({ title, description, actions, children, noPadding }: PanelProps) {
return (
<Card withBorder radius="lg" padding={noPadding ? 0 : 'lg'}>
{title || actions ? (
<Group
justify="space-between"
align="flex-end"
wrap="nowrap"
p={noPadding ? 'lg' : 0}
pb={noPadding ? 'sm' : 'md'}
>
<Stack gap={2} style={{ minWidth: 0 }}>
{title ? (
<Title order={2} fz={16} fw={620} style={{ letterSpacing: '-0.02em' }}>
{title}
</Title>
) : null}
{description ? (
<Text c="dimmed" size="xs">
{description}
</Text>
) : null}
</Stack>
{actions ? <div style={{ flexShrink: 0 }}>{actions}</div> : null}
</Group>
) : null}
{children}
</Card>
)
}

View File

@@ -0,0 +1,47 @@
import { useState } from 'react'
import type { Story, StoryMeta } from './csf.types'
import { ServerErrorState } from './ServerErrorState'
const meta: StoryMeta = {
title: 'ServerErrorState',
component: ServerErrorState,
tags: ['feedback'],
argTypes: {
title: { description: 'Override the "Something went wrong" heading', control: 'text' },
description: { description: 'Override the default error description', control: 'text' },
onRetry: { description: 'Callback fired when the retry button is clicked', control: false },
retrying: {
description: 'Shows a loading spinner on the retry button',
control: 'boolean',
},
retryLabel: { description: 'Label for the retry button', control: 'text' },
glyph: {
description: 'Custom element displayed above the title instead of "500"',
control: false,
},
},
}
export default meta
export const Default: Story = {}
function WithRetryStory() {
const [retrying, setRetrying] = useState(false)
const handleRetry = () => {
setRetrying(true)
setTimeout(() => setRetrying(false), 1500)
}
return <ServerErrorState onRetry={handleRetry} retrying={retrying} />
}
export const WithRetry: Story = { render: WithRetryStory }
function CustomGlyphStory() {
return (
<ServerErrorState
glyph={<span style={{ fontSize: 64, lineHeight: 1 }}></span>}
title="Unexpected error"
description="Please try again or contact support if the problem persists."
/>
)
}
export const CustomGlyph: Story = { render: CustomGlyphStory }

View File

@@ -0,0 +1,70 @@
import type { ReactNode } from 'react'
import { Button, Center, Stack, Text } from '@mantine/core'
import { RefreshCw } from 'lucide-react'
interface ServerErrorStateProps {
title?: string
description?: string
onRetry?: () => void
retrying?: boolean
retryLabel?: string
glyph?: ReactNode
}
export function ServerErrorState({
title = 'Something went wrong',
description = 'The server returned an error while loading this page.',
onRetry,
retrying = false,
retryLabel = 'Retry',
glyph,
}: ServerErrorStateProps) {
const normalizedDescription = typeof description === 'string' ? description.trim() : ''
const showDescription =
normalizedDescription.length > 0 &&
normalizedDescription !== 'HTTP 500' &&
normalizedDescription !== 'HTTP 500.'
return (
<Center flex={1}>
<Stack
align="center"
justify="center"
gap="xs"
py="xl"
style={{ minHeight: '60vh', width: '100%' }}
>
{glyph ?? (
<Text
style={{
fontSize: 72,
lineHeight: 1,
letterSpacing: '-0.06em',
fontWeight: 600,
}}
>
500
</Text>
)}
<Text size="xl" fw={600}>
{title}
</Text>
{showDescription ? (
<Text ta="center" maw={520} c="dimmed">
{normalizedDescription}
</Text>
) : null}
{onRetry ? (
<Button
onClick={onRetry}
loading={retrying}
leftSection={<RefreshCw size={16} />}
variant="default"
>
{retryLabel}
</Button>
) : null}
</Stack>
</Center>
)
}

View File

@@ -0,0 +1,37 @@
import React from 'react'
import { Card, Group, Stack, Text, ThemeIcon } from '@mantine/core'
export interface StatCardProps {
/** Metric label, e.g. "Open tasks". */
label: React.ReactNode
/** The big number / value. */
value: React.ReactNode
/** Optional Lucide icon component. */
icon?: React.ComponentType<{ size?: number; strokeWidth?: number }>
/** Theme color for the icon tile (Mantine color key). Defaults to primary. */
color?: string
}
// A single metric tile: label, large value, optional icon. Compose several in a
// StatGrid for a dashboard summary row.
export function StatCard({ label, value, icon: Icon, color }: StatCardProps) {
return (
<Card withBorder radius="lg" padding="lg">
<Group justify="space-between" align="flex-start" wrap="nowrap">
<Stack gap={4}>
<Text size="xs" c="dimmed" fw={550} tt="uppercase" style={{ letterSpacing: '0.04em' }}>
{label}
</Text>
<Text fz={30} fw={700} lh={1} style={{ letterSpacing: '-0.03em' }}>
{value}
</Text>
</Stack>
{Icon ? (
<ThemeIcon variant="light" color={color} radius="md" size={38}>
<Icon size={20} strokeWidth={1.8} />
</ThemeIcon>
) : null}
</Group>
</Card>
)
}

View File

@@ -0,0 +1,21 @@
import { SimpleGrid } from '@mantine/core'
import { StatCard, type StatCardProps } from './StatCard'
export interface StatGridProps {
stats: StatCardProps[]
/** Columns on wide screens (responsive down to 1). Defaults to stats.length capped at 4. */
columns?: number
}
// A responsive row of StatCards. Feed it the metrics from a stats RPC
// (e.g. getTaskStats) mapped to {label, value, icon}.
export function StatGrid({ stats, columns }: StatGridProps) {
const cols = columns ?? Math.min(stats.length || 1, 4)
return (
<SimpleGrid cols={{ base: 1, sm: 2, lg: cols }} spacing="md">
{stats.map((stat, index) => (
<StatCard key={index} {...stat} />
))}
</SimpleGrid>
)
}

View File

@@ -0,0 +1,83 @@
import { Avatar, Badge, Card, Group, Loader, Stack, Text } from '@mantine/core'
import type { UseQueryResult } from '@tanstack/react-query'
export type User = {
id: string
name: string
email: string
role: string
status: 'active' | 'away' | 'offline'
}
const STATUS_COLOR: Record<User['status'], string> = {
active: 'green',
away: 'yellow',
offline: 'gray',
}
export type UserCardProps = {
query: UseQueryResult<User | null>
}
export const UserCard: React.FC<UserCardProps> = ({ query }) => {
if (query.isLoading) {
return (
<Card withBorder radius="md" padding="lg">
<Group>
<Loader size="sm" />
<Text size="sm" c="dimmed">
Loading user
</Text>
</Group>
</Card>
)
}
if (query.isError) {
return (
<Card withBorder radius="md" padding="lg">
<Stack gap={4}>
<Text fw={600} c="red">
Couldnt load user
</Text>
<Text size="sm" c="dimmed">
{query.error instanceof Error ? query.error.message : 'Unknown error'}
</Text>
</Stack>
</Card>
)
}
const user = query.data
if (!user) {
return (
<Card withBorder radius="md" padding="lg">
<Text size="sm" c="dimmed">
No user to show.
</Text>
</Card>
)
}
return (
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" wrap="nowrap">
<Group wrap="nowrap">
<Avatar color="initials" name={user.name} radius="xl" />
<Stack gap={0}>
<Text fw={600}>{user.name}</Text>
<Text size="sm" c="dimmed">
{user.email}
</Text>
</Stack>
</Group>
<Badge color={STATUS_COLOR[user.status]} variant="light">
{user.status}
</Badge>
</Group>
<Text size="sm" mt="md">
{user.role}
</Text>
</Card>
)
}

View File

@@ -0,0 +1,21 @@
import type { ComponentType } from 'react'
export interface ArgType {
description?: string
control?: string | false
defaultValue?: unknown
}
export interface StoryMeta {
title: string
component: ComponentType<any>
description?: string
tags?: string[]
argTypes?: Record<string, ArgType>
}
export interface Story {
args?: Record<string, unknown>
render?: ComponentType<any>
name?: string
}

View File

@@ -0,0 +1,69 @@
import type { UseMutationResult } from '@tanstack/react-query'
function build<TData, TVariables>(
partial: Record<string, unknown>,
): UseMutationResult<TData, Error, TVariables> {
return {
failureCount: 0,
failureReason: null,
isPaused: false,
submittedAt: 0,
context: undefined,
variables: undefined,
mutate: () => undefined,
mutateAsync: () => Promise.resolve(undefined as never),
reset: () => undefined,
...partial,
} as unknown as UseMutationResult<TData, Error, TVariables>
}
export const stubMutation = {
idle<TData = unknown, TVariables = unknown>(): UseMutationResult<TData, Error, TVariables> {
return build<TData, TVariables>({
status: 'idle',
isIdle: true,
isPending: false,
isError: false,
isSuccess: false,
data: undefined,
error: null,
})
},
pending<TData = unknown, TVariables = unknown>(): UseMutationResult<TData, Error, TVariables> {
return build<TData, TVariables>({
status: 'pending',
isIdle: false,
isPending: true,
isError: false,
isSuccess: false,
data: undefined,
error: null,
})
},
error<TData = unknown, TVariables = unknown>(
error: Error,
): UseMutationResult<TData, Error, TVariables> {
return build<TData, TVariables>({
status: 'error',
isIdle: false,
isPending: false,
isError: true,
isSuccess: false,
data: undefined,
error,
})
},
success<TData = unknown, TVariables = unknown>(
data: TData,
): UseMutationResult<TData, Error, TVariables> {
return build<TData, TVariables>({
status: 'success',
isIdle: false,
isPending: false,
isError: false,
isSuccess: true,
data,
error: null,
})
},
}

View File

@@ -0,0 +1,66 @@
import type { UseQueryResult } from '@tanstack/react-query'
function build<T>(partial: Record<string, unknown>): UseQueryResult<T> {
return {
failureCount: 0,
failureReason: null,
errorUpdateCount: 0,
isFetched: true,
isFetchedAfterMount: true,
isFetching: partial.fetchStatus === 'fetching',
isPaused: false,
isPlaceholderData: false,
isRefetching: false,
isStale: false,
isInitialLoading: partial.isLoading === true,
isLoadingError: false,
isRefetchError: false,
dataUpdatedAt: 0,
errorUpdatedAt: 0,
refetch: () => Promise.resolve(undefined as never),
promise: Promise.resolve(undefined as never),
...partial,
} as unknown as UseQueryResult<T>
}
export const stubQuery = {
loading<T>(): UseQueryResult<T> {
return build<T>({
status: 'pending',
fetchStatus: 'fetching',
isPending: true,
isLoading: true,
isError: false,
isSuccess: false,
data: undefined,
error: null,
})
},
error<T>(error: Error): UseQueryResult<T> {
return build<T>({
status: 'error',
fetchStatus: 'idle',
isPending: false,
isLoading: false,
isError: true,
isSuccess: false,
data: undefined,
error,
})
},
success<T>(data: T): UseQueryResult<T> {
return build<T>({
status: 'success',
fetchStatus: 'idle',
isPending: false,
isLoading: false,
isError: false,
isSuccess: true,
data,
error: null,
})
},
empty<T>(): UseQueryResult<T> {
return stubQuery.success<T>(null as T)
},
}

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["index.ts", "src"]
}

View File

@@ -0,0 +1,19 @@
{
"name": "@project/functions-sdk",
"version": "0.0.1",
"private": true,
"type": "module",
"exports": {
"./pikku/api.gen": "./src/pikku/api.gen.ts",
"./pikku/pikku-fetch.gen": "./src/pikku/pikku-fetch.gen.ts",
"./pikku/pikku-rpc.gen": "./src/pikku/pikku-rpc.gen.ts",
"./pikku/rpc-map.gen": "./src/pikku/rpc-map.gen.d.ts",
"./pikku/http-map.gen": "./src/pikku/http-map.gen.d.ts",
"./pikku/*": "./src/pikku/*"
},
"dependencies": {
"@pikku/fetch": "^0.12.6",
"@pikku/react": "^0.12.5",
"@tanstack/react-query": "^5.66.0"
}
}

View File

@@ -0,0 +1,4 @@
/**
* This file was generated by @pikku/cli@0.12.76
*/
export type AgentMap = {}

View File

@@ -0,0 +1,26 @@
/**
* This file was generated by @pikku/cli@0.12.76
*/
// AUTO-GENERATED by pikku CLI — do not edit
import { pikkuBetterAuth as _pikkuBetterAuth } from '@pikku/better-auth'
import type { BetterAuthInstance, PikkuBetterAuthFactory } from '@pikku/better-auth'
import type { SingletonServices } from '../function/pikku-function-types.gen.js'
import { TypedSecretService } from '../secrets/pikku-secrets.gen.js'
import { TypedVariablesService } from '../variables/pikku-variables.gen.js'
type AuthSingletonServices = Omit<SingletonServices, 'secrets' | 'variables'> & {
secrets: TypedSecretService
variables: TypedVariablesService
}
export const pikkuBetterAuth = <I extends BetterAuthInstance>(
factory: (services: AuthSingletonServices) => I | Promise<I>
): PikkuBetterAuthFactory<I> =>
_pikkuBetterAuth((services) =>
factory({
...services,
secrets: new TypedSecretService(services.secrets),
variables: new TypedVariablesService(services.variables),
} as AuthSingletonServices)
)

View File

@@ -0,0 +1,89 @@
/**
* This file was generated by @pikku/cli@0.12.76
*/
/**
* This provides the structure needed for TypeScript to be aware of channels
*/
import type { StubCall } from '.bun/@pikku+core@0.12.57/node_modules/@pikku/core/dist/services/stub-tracker'
import type { StubCall } from '/Users/yasser/git/pikku/fabric/templates/starter-template/node_modules/.bun/@pikku+core@0.12.57/node_modules/@pikku/core/dist/services/stub-tracker.d.ts'
export type AgentApproveCallerInput = { agentName: string; runId: string; approvals: { toolCallId: string; approved: boolean; }[]; }
export type AgentCallerInput = { agentName: string; message: string; threadId: string; resourceId: string; }
export type AgentResumeCallerInput = { agentName: string; runId: string; toolCallId: string; approved: boolean; }
export type AgentStreamCallerInput = { agentName: string; message: string; threadId: string; resourceId: string; context?: string | undefined; }
export type AuthHandlerOutput = Promise<void> | Promise<any>
export type DeleteAgentThreadInput = { threadId: string; resourceId?: string | undefined; }
export type DeleteAgentThreadOutput = { deleted: boolean; }
export type GetAgentThreadMessagesInput = { threadId: string; resourceId?: string | undefined; }
export type GetAgentThreadMessagesOutput = any[]
export type GetAgentThreadRunsInput = { threadId: string; resourceId?: string | undefined; }
export type GetAgentThreadRunsOutput = any[]
export type GetAgentThreadsInput = { agentName?: string | undefined; resourceId?: string | undefined; limit?: number | undefined; offset?: number | undefined; }
export type GetAgentThreadsOutput = any[]
export type GetSessionInput = {}
export type GetSessionOutput = {
userId: string;
email: string;
name: string | null;
}
export type GraphStarterInput = { workflowName: string; nodeId: string; data?: unknown; }
export type GraphStarterOutput = { runId: string; }
export type PikkuConsoleGetSecretInput = { secretId: string; }
export type PikkuConsoleGetSecretOutput = { exists: boolean; value: unknown; }
export type PikkuConsoleGetVariableInput = { variableId: string; }
export type PikkuConsoleGetVariableOutput = { exists: boolean; value: unknown; }
export type PikkuConsoleHasSecretInput = { secretId: string; }
export type PikkuConsoleHasSecretOutput = { exists: boolean; }
export type PikkuConsoleSetSecretInput = { secretId: string; value: unknown; }
export type PikkuConsoleSetSecretOutput = { success: boolean; }
export type PikkuConsoleSetVariableInput = { variableId: string; value: unknown; }
export type PikkuConsoleSetVariableOutput = { success: boolean; }
export type PikkuScenarioGetStubCallsInput = { service?: string | undefined; }
export type PikkuScenarioGetStubCallsOutput = StubCall[]
export type PikkuScenarioResetLiveCoverageOutput = { enabled: boolean; }
export type PikkuScenarioResetStubsOutput = { enabled: boolean; }
export type RealtimeEventStreamInput = { topic: string; }
export type RealtimeSubscribeInput = { topic: string; }
export type RealtimeUnsubscribeInput = { topic: string; }
export type RpcCallerInput = { rpcName: string; data?: unknown; }
export type SecretSchema_betterAuthSecret = string
export type SessionHealthScenarioOutput = { email: string; userId: string; }
export type WorkflowRunnerInput = { workflowName: string; data?: unknown; }
export type WorkflowStarterInput = { workflowName: string; data?: unknown; }
export type WorkflowStarterOutput = { runId: string; }
export type WorkflowStatusCheckerInput = { workflowName: string; runId: string; }
export type WorkflowStatusStreamFullInput = { workflowName: string; runId: string; }
export type WorkflowStatusStreamInput = { workflowName: string; runId: string; }
interface ChannelHandler<I, O> {
input: I;
output: O;
}
export type ChannelsMap = {
readonly 'events': {
readonly routes: {
readonly action: {
readonly subscribe: ChannelHandler<RealtimeSubscribeInput, never>,
readonly unsubscribe: ChannelHandler<RealtimeUnsubscribeInput, never>,
},
},
readonly defaultMessage: never,
},
};
export type ChannelDefaultHandlerOf<Channel extends keyof ChannelsMap> =
ChannelsMap[Channel]['defaultMessage'] extends { input: infer I; output: infer O }
? ChannelHandler<I, O>
: never;
export type ChannelWiringHandlerOf<
Channel extends keyof ChannelsMap,
Route extends keyof ChannelsMap[Channel]['routes'],
Method extends keyof ChannelsMap[Channel]['routes'][Route],
> =
ChannelsMap[Channel]['routes'][Route][Method] extends { input: infer I; output: infer O }
? ChannelHandler<I, O>
: never;

View File

@@ -0,0 +1,141 @@
// Generated by @pikku/cli — do not edit by hand.
// Run `pikku db migrate` to refresh.
// Use this type in db/classifications.ts:
// import type { DbClassificationMap } from './.pikku/db/classification-map.gen.d.ts'
// export const classifications = { ... } satisfies DbClassificationMap
export type ColumnEntry = {
/** Privacy level. Defaults to 'private' when omitted. */
security?: 'public' | 'private' | 'pii' | 'secret' | 'encrypted'
/** Anonymize strategy used by `pikku db anonymize`. */
classification?: 'fake:email' | 'fake:name' | 'hash' | 'keep'
/** Column kind override for codegen coercion + typing. */
kind?: 'date' | 'bool' | 'json' | 'uuid'
/** TypeScript type override, e.g. `string[]` or `MyJson`. Wins over `kind`. */
tsType?: string
/** Zod string-format validator (keeps the TS type as `string`). */
format?: 'email' | 'url' | 'emoji' | 'e164' | 'jwt' | 'cuid' | 'cuid2' | 'ulid' | 'nanoid' | 'base64' | 'base64url' | 'ipv4' | 'ipv6' | 'cidrv4' | 'cidrv6' | 'isoDate' | 'isoTime' | 'isoDatetime' | 'isoDuration'
description?: string
}
export type DbClassificationMap = {
"account": {
"id": ColumnEntry
"account_id": ColumnEntry
"provider_id": ColumnEntry
"user_id": ColumnEntry
"access_token": ColumnEntry
"refresh_token": ColumnEntry
"id_token": ColumnEntry
"access_token_expires_at": ColumnEntry
"refresh_token_expires_at": ColumnEntry
"scope": ColumnEntry
"password": ColumnEntry
"created_at": ColumnEntry
"updated_at": ColumnEntry
}
"ai_message": {
"id": ColumnEntry
"thread_id": ColumnEntry
"role": ColumnEntry
"content": ColumnEntry
"created_at": ColumnEntry
}
"ai_run": {
"run_id": ColumnEntry
"agent_name": ColumnEntry
"thread_id": ColumnEntry
"resource_id": ColumnEntry
"status": ColumnEntry
"error_message": ColumnEntry
"suspend_reason": ColumnEntry
"missing_rpcs": ColumnEntry
"usage_input_tokens": ColumnEntry
"usage_output_tokens": ColumnEntry
"usage_model": ColumnEntry
"created_at": ColumnEntry
"updated_at": ColumnEntry
}
"ai_threads": {
"id": ColumnEntry
"resource_id": ColumnEntry
"title": ColumnEntry
"metadata": ColumnEntry
"created_at": ColumnEntry
"updated_at": ColumnEntry
}
"ai_tool_call": {
"id": ColumnEntry
"thread_id": ColumnEntry
"message_id": ColumnEntry
"run_id": ColumnEntry
"tool_name": ColumnEntry
"args": ColumnEntry
"result": ColumnEntry
"approval_status": ColumnEntry
"approval_type": ColumnEntry
"agent_run_id": ColumnEntry
"display_tool_name": ColumnEntry
"display_args": ColumnEntry
"created_at": ColumnEntry
}
"ai_working_memory": {
"id": ColumnEntry
"scope": ColumnEntry
"data": ColumnEntry
"updated_at": ColumnEntry
}
"audit": {
"audit_id": ColumnEntry
"occurred_at": ColumnEntry
"type": ColumnEntry
"source": ColumnEntry
"outcome": ColumnEntry
"function_id": ColumnEntry
"wire_type": ColumnEntry
"trace_id": ColumnEntry
"transaction_id": ColumnEntry
"query_id": ColumnEntry
"actor_user_id": ColumnEntry
"actor_org_id": ColumnEntry
"tables": ColumnEntry
"changed_cols": ColumnEntry
"event": ColumnEntry
"old": ColumnEntry
"data": ColumnEntry
}
"session": {
"id": ColumnEntry
"expires_at": ColumnEntry
"token": ColumnEntry
"created_at": ColumnEntry
"updated_at": ColumnEntry
"ip_address": ColumnEntry
"user_agent": ColumnEntry
"user_id": ColumnEntry
"impersonated_by": ColumnEntry
}
"user": {
"id": ColumnEntry
"name": ColumnEntry
"email": ColumnEntry
"email_verified": ColumnEntry
"image": ColumnEntry
"created_at": ColumnEntry
"updated_at": ColumnEntry
"actor": ColumnEntry
"role": ColumnEntry
"banned": ColumnEntry
"ban_reason": ColumnEntry
"ban_expires": ColumnEntry
"fabric": ColumnEntry
}
"verification": {
"id": ColumnEntry
"identifier": ColumnEntry
"value": ColumnEntry
"expires_at": ColumnEntry
"created_at": ColumnEntry
"updated_at": ColumnEntry
}
}

View File

@@ -0,0 +1,148 @@
/**
* This file was generated by @pikku/cli@0.12.76
*/
/**
* This provides the structure needed for typescript to be aware of routes and their return types
*/
import type { StubCall } from '.bun/@pikku+core@0.12.57/node_modules/@pikku/core/dist/services/stub-tracker'
import type { StreamWorkflowRunInput } from '.bun/@pikku+addon-console@0.12.26+8a6fcc78e6b267a5/node_modules/@pikku/addon-console/dist/.pikku/rpc/pikku-rpc-wirings-map.internal.gen'
import type { WorkflowRunStatus } from '.bun/@pikku+core@0.12.57/node_modules/@pikku/core/dist/wirings/workflow/workflow.types'
export type AgentApproveCallerInput = { agentName: string; runId: string; approvals: { toolCallId: string; approved: boolean; }[]; }
export type AgentCallerInput = { agentName: string; message: string; threadId: string; resourceId: string; }
export type AgentResumeCallerInput = { agentName: string; runId: string; toolCallId: string; approved: boolean; }
export type AgentStreamCallerInput = { agentName: string; message: string; threadId: string; resourceId: string; context?: string | undefined; }
export type AuthHandlerOutput = Promise<void> | Promise<any>
export type DeleteAgentThreadInput = { threadId: string; resourceId?: string | undefined; }
export type DeleteAgentThreadOutput = { deleted: boolean; }
export type GetAgentThreadMessagesInput = { threadId: string; resourceId?: string | undefined; }
export type GetAgentThreadMessagesOutput = any[]
export type GetAgentThreadRunsInput = { threadId: string; resourceId?: string | undefined; }
export type GetAgentThreadRunsOutput = any[]
export type GetAgentThreadsInput = { agentName?: string | undefined; resourceId?: string | undefined; limit?: number | undefined; offset?: number | undefined; }
export type GetAgentThreadsOutput = any[]
export type GetSessionInput = {}
export type GetSessionOutput = {
userId: string;
email: string;
name: string | null;
}
export type GraphStarterInput = { workflowName: string; nodeId: string; data?: unknown; }
export type GraphStarterOutput = { runId: string; }
export type PikkuConsoleGetSecretInput = { secretId: string; }
export type PikkuConsoleGetSecretOutput = { exists: boolean; value: unknown; }
export type PikkuConsoleGetVariableInput = { variableId: string; }
export type PikkuConsoleGetVariableOutput = { exists: boolean; value: unknown; }
export type PikkuConsoleHasSecretInput = { secretId: string; }
export type PikkuConsoleHasSecretOutput = { exists: boolean; }
export type PikkuConsoleSetSecretInput = { secretId: string; value: unknown; }
export type PikkuConsoleSetSecretOutput = { success: boolean; }
export type PikkuConsoleSetVariableInput = { variableId: string; value: unknown; }
export type PikkuConsoleSetVariableOutput = { success: boolean; }
export type PikkuScenarioGetStubCallsInput = { service?: string | undefined; }
export type PikkuScenarioGetStubCallsOutput = StubCall[]
export type PikkuScenarioResetLiveCoverageOutput = { enabled: boolean; }
export type PikkuScenarioResetStubsOutput = { enabled: boolean; }
export type RealtimeEventStreamInput = { topic: string; }
export type RealtimeSubscribeInput = { topic: string; }
export type RealtimeUnsubscribeInput = { topic: string; }
export type RpcCallerInput = { rpcName: string; data?: unknown; }
export type SecretSchema_betterAuthSecret = string
export type SessionHealthScenarioOutput = { email: string; userId: string; }
export type WorkflowRunnerInput = { workflowName: string; data?: unknown; }
export type WorkflowStarterInput = { workflowName: string; data?: unknown; }
export type WorkflowStarterOutput = { runId: string; }
export type WorkflowStatusCheckerInput = { workflowName: string; runId: string; }
export type WorkflowStatusStreamFullInput = { workflowName: string; runId: string; }
export type WorkflowStatusStreamInput = { workflowName: string; runId: string; }
// The '& {}' is a workaround for not directly refering to a type since it confuses typescript
export type AgentCallerInputParams = Pick<AgentCallerInput, 'agentName'> & {}
export type AgentCallerInputBody = Omit<AgentCallerInput, 'agentName'> & {}
export type AgentStreamCallerInputParams = Pick<AgentStreamCallerInput, 'agentName'> & {}
export type AgentStreamCallerInputBody = Omit<AgentStreamCallerInput, 'agentName'> & {}
export type AgentApproveCallerInputParams = Pick<AgentApproveCallerInput, 'agentName'> & {}
export type AgentApproveCallerInputBody = Omit<AgentApproveCallerInput, 'agentName'> & {}
export type AgentResumeCallerInputParams = Pick<AgentResumeCallerInput, 'agentName'> & {}
export type AgentResumeCallerInputBody = Omit<AgentResumeCallerInput, 'agentName'> & {}
export type StreamWorkflowRunInputParams = Pick<StreamWorkflowRunInput, 'runId'> & {}
export type StreamWorkflowRunInputBody = Omit<StreamWorkflowRunInput, 'runId'> & {}
export type RealtimeEventStreamInputParams = Pick<RealtimeEventStreamInput, 'topic'> & {}
export type RealtimeEventStreamInputBody = Omit<RealtimeEventStreamInput, 'topic'> & {}
export type RpcCallerInputParams = Pick<RpcCallerInput, 'rpcName'> & {}
export type RpcCallerInputBody = Omit<RpcCallerInput, 'rpcName'> & {}
export type WorkflowStarterInputParams = Pick<WorkflowStarterInput, 'workflowName'> & {}
export type WorkflowStarterInputBody = Omit<WorkflowStarterInput, 'workflowName'> & {}
export type WorkflowRunnerInputParams = Pick<WorkflowRunnerInput, 'workflowName'> & {}
export type WorkflowRunnerInputBody = Omit<WorkflowRunnerInput, 'workflowName'> & {}
export type WorkflowStatusCheckerInputParams = Pick<WorkflowStatusCheckerInput, 'workflowName' | 'runId'> & {}
export type WorkflowStatusCheckerInputBody = Omit<WorkflowStatusCheckerInput, 'workflowName' | 'runId'> & {}
export type WorkflowStatusStreamInputParams = Pick<WorkflowStatusStreamInput, 'workflowName' | 'runId'> & {}
export type WorkflowStatusStreamInputBody = Omit<WorkflowStatusStreamInput, 'workflowName' | 'runId'> & {}
export type WorkflowStatusStreamFullInputParams = Pick<WorkflowStatusStreamFullInput, 'workflowName' | 'runId'> & {}
export type WorkflowStatusStreamFullInputBody = Omit<WorkflowStatusStreamFullInput, 'workflowName' | 'runId'> & {}
export type GraphStarterInputParams = Pick<GraphStarterInput, 'workflowName' | 'nodeId'> & {}
export type GraphStarterInputBody = Omit<GraphStarterInput, 'workflowName' | 'nodeId'> & {}
interface HTTPWiringHandler<I, O> {
input: I;
output: O;
}
export type HTTPWiringsMap = {
readonly '/api/auth{/*splat}': {
readonly GET: HTTPWiringHandler<null, AuthHandlerOutput>,
readonly POST: HTTPWiringHandler<null, AuthHandlerOutput>,
},
readonly '/workflow-run/:runId/stream': {
readonly GET: HTTPWiringHandler<StreamWorkflowRunInput, null>,
},
readonly '/events/:topic': {
readonly GET: HTTPWiringHandler<RealtimeEventStreamInput, null>,
},
readonly '/workflow/:workflowName/status/:runId': {
readonly GET: HTTPWiringHandler<WorkflowStatusCheckerInput, WorkflowRunStatus>,
},
readonly '/workflow/:workflowName/status/:runId/stream': {
readonly GET: HTTPWiringHandler<WorkflowStatusStreamInput, null>,
},
readonly '/workflow/:workflowName/status/:runId/stream/full': {
readonly GET: HTTPWiringHandler<WorkflowStatusStreamFullInput, null>,
},
readonly '/rpc/agent/:agentName': {
readonly POST: HTTPWiringHandler<AgentCallerInput, null>,
},
readonly '/rpc/agent/:agentName/stream': {
readonly POST: HTTPWiringHandler<AgentStreamCallerInput, null>,
},
readonly '/rpc/agent/:agentName/approve': {
readonly POST: HTTPWiringHandler<AgentApproveCallerInput, null>,
},
readonly '/rpc/agent/:agentName/resume': {
readonly POST: HTTPWiringHandler<AgentResumeCallerInput, null>,
},
readonly '/rpc/:rpcName': {
readonly POST: HTTPWiringHandler<RpcCallerInput, null>,
},
readonly '/workflow/:workflowName/start': {
readonly POST: HTTPWiringHandler<WorkflowStarterInput, WorkflowStarterOutput>,
},
readonly '/workflow/:workflowName/run': {
readonly POST: HTTPWiringHandler<WorkflowRunnerInput, null>,
},
readonly '/workflow/:workflowName/graph/:nodeId': {
readonly POST: HTTPWiringHandler<GraphStarterInput, GraphStarterOutput>,
},
};
export type HTTPWiringHandlerOf<HTTPWiring extends keyof HTTPWiringsMap, Method extends keyof HTTPWiringsMap[HTTPWiring]> =
HTTPWiringsMap[HTTPWiring][Method] extends { input: infer I; output: infer O }
? HTTPWiringHandler<I, O>
: never;
export type HTTPWiringsWithMethod<Method extends string> = {
[HTTPWiring in keyof HTTPWiringsMap]: Method extends keyof HTTPWiringsMap[HTTPWiring] ? HTTPWiring : never;
}[keyof HTTPWiringsMap];

View File

@@ -0,0 +1,168 @@
/**
* This file was generated by @pikku/cli@0.12.76
*/
/**
* This provides the structure needed for typescript to be aware of RPCs and their return types
*/
import type { StubCall } from '.bun/@pikku+core@0.12.57/node_modules/@pikku/core/dist/services/stub-tracker'
import type { FunctionCoverageReport } from '.bun/@pikku+core@0.12.57/node_modules/@pikku/core/dist/services/v8-coverage-service'
export type AgentApproveCallerInput = { agentName: string; runId: string; approvals: { toolCallId: string; approved: boolean; }[]; }
export type AgentCallerInput = { agentName: string; message: string; threadId: string; resourceId: string; }
export type AgentResumeCallerInput = { agentName: string; runId: string; toolCallId: string; approved: boolean; }
export type AgentStreamCallerInput = { agentName: string; message: string; threadId: string; resourceId: string; context?: string | undefined; }
export type AuthHandlerOutput = Promise<void> | Promise<any>
export type DeleteAgentThreadInput = { threadId: string; resourceId?: string | undefined; }
export type DeleteAgentThreadOutput = { deleted: boolean; }
export type GetAgentThreadMessagesInput = { threadId: string; resourceId?: string | undefined; }
export type GetAgentThreadMessagesOutput = any[]
export type GetAgentThreadRunsInput = { threadId: string; resourceId?: string | undefined; }
export type GetAgentThreadRunsOutput = any[]
export type GetAgentThreadsInput = { agentName?: string | undefined; resourceId?: string | undefined; limit?: number | undefined; offset?: number | undefined; }
export type GetAgentThreadsOutput = any[]
export type GetSessionInput = {}
export type GetSessionOutput = {
userId: string;
email: string;
name: string | null;
}
export type GraphStarterInput = { workflowName: string; nodeId: string; data?: unknown; }
export type GraphStarterOutput = { runId: string; }
export type PikkuConsoleGetSecretInput = { secretId: string; }
export type PikkuConsoleGetSecretOutput = { exists: boolean; value: unknown; }
export type PikkuConsoleGetVariableInput = { variableId: string; }
export type PikkuConsoleGetVariableOutput = { exists: boolean; value: unknown; }
export type PikkuConsoleHasSecretInput = { secretId: string; }
export type PikkuConsoleHasSecretOutput = { exists: boolean; }
export type PikkuConsoleSetSecretInput = { secretId: string; value: unknown; }
export type PikkuConsoleSetSecretOutput = { success: boolean; }
export type PikkuConsoleSetVariableInput = { variableId: string; value: unknown; }
export type PikkuConsoleSetVariableOutput = { success: boolean; }
export type PikkuScenarioGetStubCallsInput = { service?: string | undefined; }
export type PikkuScenarioGetStubCallsOutput = StubCall[]
export type PikkuScenarioResetLiveCoverageOutput = { enabled: boolean; }
export type PikkuScenarioResetStubsOutput = { enabled: boolean; }
export type RealtimeEventStreamInput = { topic: string; }
export type RealtimeSubscribeInput = { topic: string; }
export type RealtimeUnsubscribeInput = { topic: string; }
export type RpcCallerInput = { rpcName: string; data?: unknown; }
export type SecretSchema_betterAuthSecret = string
export type SessionHealthScenarioOutput = { email: string; userId: string; }
export type WorkflowRunnerInput = { workflowName: string; data?: unknown; }
export type WorkflowStarterInput = { workflowName: string; data?: unknown; }
export type WorkflowStarterOutput = { runId: string; }
export type WorkflowStatusCheckerInput = { workflowName: string; runId: string; }
export type WorkflowStatusStreamFullInput = { workflowName: string; runId: string; }
export type WorkflowStatusStreamInput = { workflowName: string; runId: string; }
interface RPCHandler<I, O> {
input: I;
output: O;
}
export type RPCMap = {
readonly 'getSession': RPCHandler<GetSessionInput, GetSessionOutput>,
readonly 'getAgentThreads': RPCHandler<GetAgentThreadsInput, GetAgentThreadsOutput>,
readonly 'getAgentThreadMessages': RPCHandler<GetAgentThreadMessagesInput, GetAgentThreadMessagesOutput>,
readonly 'getAgentThreadRuns': RPCHandler<GetAgentThreadRunsInput, GetAgentThreadRunsOutput>,
readonly 'deleteAgentThread': RPCHandler<DeleteAgentThreadInput, DeleteAgentThreadOutput>,
readonly 'pikkuConsoleSetSecret': RPCHandler<PikkuConsoleSetSecretInput, PikkuConsoleSetSecretOutput>,
readonly 'pikkuConsoleGetVariable': RPCHandler<PikkuConsoleGetVariableInput, PikkuConsoleGetVariableOutput>,
readonly 'pikkuConsoleSetVariable': RPCHandler<PikkuConsoleSetVariableInput, PikkuConsoleSetVariableOutput>,
readonly 'pikkuConsoleHasSecret': RPCHandler<PikkuConsoleHasSecretInput, PikkuConsoleHasSecretOutput>,
readonly 'pikkuConsoleGetSecret': RPCHandler<PikkuConsoleGetSecretInput, PikkuConsoleGetSecretOutput>,
readonly 'pikkuScenarioTakeLiveCoverage': RPCHandler<null, FunctionCoverageReport>,
readonly 'pikkuScenarioResetLiveCoverage': RPCHandler<null, PikkuScenarioResetLiveCoverageOutput>,
readonly 'pikkuScenarioResetStubs': RPCHandler<null, PikkuScenarioResetStubsOutput>,
readonly 'pikkuScenarioGetStubCalls': RPCHandler<PikkuScenarioGetStubCallsInput, PikkuScenarioGetStubCallsOutput>,
};
// Addon package RPC maps
import type { RPCMap as ConsoleRPCMap } from '@pikku/addon-console/.pikku/rpc/pikku-rpc-wirings-map.internal.gen.js'
// Utility type to prefix keys with namespace (skips 'any' to prevent type poisoning)
type PrefixKeys<T, Prefix extends string> = unknown extends T ? {} : {
[K in keyof T as `${Prefix}:${string & K}`]: T[K]
}
// Merge all RPC maps with namespace prefixes
export type FlattenedRPCMap =
RPCMap & PrefixKeys<ConsoleRPCMap, 'console'>
type IsAny<T> = 0 extends (1 & T) ? true : false
type IsVoidishInput<T> = IsAny<T> extends true
? false
: [T] extends [void | null | undefined]
? true
: false
export type RPCInvoke = <Name extends keyof FlattenedRPCMap>(
...args: IsVoidishInput<FlattenedRPCMap[Name]['input']> extends true
? [name: Name]
: [name: Name, data: FlattenedRPCMap[Name]['input']]
) => Promise<FlattenedRPCMap[Name]['output']>
export type RPCRemote = <Name extends keyof FlattenedRPCMap>(
...args: IsVoidishInput<FlattenedRPCMap[Name]['input']> extends true
? [name: Name]
: [name: Name, data: FlattenedRPCMap[Name]['input']]
) => Promise<FlattenedRPCMap[Name]['output']>
import type { FlattenedWorkflowMap } from '../workflow/pikku-workflow-map.gen.d.js'
import type { AgentMap } from '../agent/pikku-agent-map.gen.d.js'
// Addon package Agent maps
import type { AgentMap as ConsoleAgentMap } from '@pikku/addon-console/.pikku/agent/pikku-agent-map.gen.d.js'
type FlattenedAgentMap =
AgentMap & PrefixKeys<ConsoleAgentMap, 'console'>
import type { PikkuRPC } from '@pikku/core/rpc'
interface AIAgentInput {
message: string
threadId: string
resourceId: string
}
export type TypedStartWorkflow = <Name extends keyof FlattenedWorkflowMap>(
name: Name,
input: FlattenedWorkflowMap[Name]['input'],
options?: { startNode?: string }
) => Promise<{ runId: string }>
export type TypedRunWorkflow = <Name extends keyof FlattenedWorkflowMap>(
name: Name,
input: FlattenedWorkflowMap[Name]['input']
) => Promise<FlattenedWorkflowMap[Name]['output']>
export type TypedWorkflowStatus = (
workflowName: string,
runId: string
) => Promise<{ id: string; status: 'running' | 'suspended' | 'completed' | 'failed' | 'cancelled'; output?: unknown; error?: { message?: string } }>
type TypedAgentRun = [keyof FlattenedAgentMap] extends [never]
? (name: string, input: AIAgentInput) => Promise<any>
: <Name extends keyof FlattenedAgentMap>(
name: Name,
input: AIAgentInput
) => Promise<{ runId: string; result: FlattenedAgentMap[Name]['output']; usage: { inputTokens: number; outputTokens: number } }>
type TypedAgentStream = [keyof FlattenedAgentMap] extends [never]
? (name: string, input: AIAgentInput, options?: { requiresToolApproval?: 'all' | 'explicit' | false }) => Promise<void>
: <Name extends keyof FlattenedAgentMap>(
name: Name,
input: AIAgentInput,
options?: { requiresToolApproval?: 'all' | 'explicit' | false }
) => Promise<void>
export type TypedPikkuRPC = PikkuRPC<RPCInvoke, RPCRemote, TypedStartWorkflow, TypedAgentRun, TypedAgentStream>

View File

@@ -0,0 +1,185 @@
/**
* This file was generated by @pikku/cli@0.12.76
*/
/**
* This provides the structure needed for typescript to be aware of RPCs and their return types
*/
import type { StubCall } from '.bun/@pikku+core@0.12.57/node_modules/@pikku/core/dist/services/stub-tracker'
import type { FunctionCoverageReport } from '.bun/@pikku+core@0.12.57/node_modules/@pikku/core/dist/services/v8-coverage-service'
import type { WorkflowRunStatus } from '.bun/@pikku+core@0.12.57/node_modules/@pikku/core/dist/wirings/workflow/workflow.types'
export type AgentApproveCallerInput = { agentName: string; runId: string; approvals: { toolCallId: string; approved: boolean; }[]; }
export type AgentCallerInput = { agentName: string; message: string; threadId: string; resourceId: string; }
export type AgentResumeCallerInput = { agentName: string; runId: string; toolCallId: string; approved: boolean; }
export type AgentStreamCallerInput = { agentName: string; message: string; threadId: string; resourceId: string; context?: string | undefined; }
export type AuthHandlerOutput = Promise<void> | Promise<any>
export type DeleteAgentThreadInput = { threadId: string; resourceId?: string | undefined; }
export type DeleteAgentThreadOutput = { deleted: boolean; }
export type GetAgentThreadMessagesInput = { threadId: string; resourceId?: string | undefined; }
export type GetAgentThreadMessagesOutput = any[]
export type GetAgentThreadRunsInput = { threadId: string; resourceId?: string | undefined; }
export type GetAgentThreadRunsOutput = any[]
export type GetAgentThreadsInput = { agentName?: string | undefined; resourceId?: string | undefined; limit?: number | undefined; offset?: number | undefined; }
export type GetAgentThreadsOutput = any[]
export type GetSessionInput = {}
export type GetSessionOutput = {
userId: string;
email: string;
name: string | null;
}
export type GraphStarterInput = { workflowName: string; nodeId: string; data?: unknown; }
export type GraphStarterOutput = { runId: string; }
export type PikkuConsoleGetSecretInput = { secretId: string; }
export type PikkuConsoleGetSecretOutput = { exists: boolean; value: unknown; }
export type PikkuConsoleGetVariableInput = { variableId: string; }
export type PikkuConsoleGetVariableOutput = { exists: boolean; value: unknown; }
export type PikkuConsoleHasSecretInput = { secretId: string; }
export type PikkuConsoleHasSecretOutput = { exists: boolean; }
export type PikkuConsoleSetSecretInput = { secretId: string; value: unknown; }
export type PikkuConsoleSetSecretOutput = { success: boolean; }
export type PikkuConsoleSetVariableInput = { variableId: string; value: unknown; }
export type PikkuConsoleSetVariableOutput = { success: boolean; }
export type PikkuScenarioGetStubCallsInput = { service?: string | undefined; }
export type PikkuScenarioGetStubCallsOutput = StubCall[]
export type PikkuScenarioResetLiveCoverageOutput = { enabled: boolean; }
export type PikkuScenarioResetStubsOutput = { enabled: boolean; }
export type RealtimeEventStreamInput = { topic: string; }
export type RealtimeSubscribeInput = { topic: string; }
export type RealtimeUnsubscribeInput = { topic: string; }
export type RpcCallerInput = { rpcName: string; data?: unknown; }
export type SecretSchema_betterAuthSecret = string
export type SessionHealthScenarioOutput = { email: string; userId: string; }
export type WorkflowRunnerInput = { workflowName: string; data?: unknown; }
export type WorkflowStarterInput = { workflowName: string; data?: unknown; }
export type WorkflowStarterOutput = { runId: string; }
export type WorkflowStatusCheckerInput = { workflowName: string; runId: string; }
export type WorkflowStatusStreamFullInput = { workflowName: string; runId: string; }
export type WorkflowStatusStreamInput = { workflowName: string; runId: string; }
interface RPCHandler<I, O> {
input: I;
output: O;
}
export type RPCMap = {
readonly 'getSession': RPCHandler<GetSessionInput, GetSessionOutput>,
readonly 'sessionHealthScenario': RPCHandler<null, SessionHealthScenarioOutput>,
readonly 'agentCaller': RPCHandler<AgentCallerInput, null>,
readonly 'agentStreamCaller': RPCHandler<AgentStreamCallerInput, null>,
readonly 'agentApproveCaller': RPCHandler<AgentApproveCallerInput, null>,
readonly 'agentResumeCaller': RPCHandler<AgentResumeCallerInput, null>,
readonly 'getAgentThreads': RPCHandler<GetAgentThreadsInput, GetAgentThreadsOutput>,
readonly 'getAgentThreadMessages': RPCHandler<GetAgentThreadMessagesInput, GetAgentThreadMessagesOutput>,
readonly 'getAgentThreadRuns': RPCHandler<GetAgentThreadRunsInput, GetAgentThreadRunsOutput>,
readonly 'deleteAgentThread': RPCHandler<DeleteAgentThreadInput, DeleteAgentThreadOutput>,
readonly 'authHandler': RPCHandler<null, AuthHandlerOutput>,
readonly 'pikkuConsoleSetSecret': RPCHandler<PikkuConsoleSetSecretInput, PikkuConsoleSetSecretOutput>,
readonly 'pikkuConsoleGetVariable': RPCHandler<PikkuConsoleGetVariableInput, PikkuConsoleGetVariableOutput>,
readonly 'pikkuConsoleSetVariable': RPCHandler<PikkuConsoleSetVariableInput, PikkuConsoleSetVariableOutput>,
readonly 'pikkuConsoleHasSecret': RPCHandler<PikkuConsoleHasSecretInput, PikkuConsoleHasSecretOutput>,
readonly 'pikkuConsoleGetSecret': RPCHandler<PikkuConsoleGetSecretInput, PikkuConsoleGetSecretOutput>,
readonly 'realtimeSubscribe': RPCHandler<RealtimeSubscribeInput, null>,
readonly 'realtimeUnsubscribe': RPCHandler<RealtimeUnsubscribeInput, null>,
readonly 'realtimeEventStream': RPCHandler<RealtimeEventStreamInput, null>,
readonly 'rpcCaller': RPCHandler<RpcCallerInput, null>,
readonly 'pikkuScenarioTakeLiveCoverage': RPCHandler<null, FunctionCoverageReport>,
readonly 'pikkuScenarioResetLiveCoverage': RPCHandler<null, PikkuScenarioResetLiveCoverageOutput>,
readonly 'pikkuScenarioResetStubs': RPCHandler<null, PikkuScenarioResetStubsOutput>,
readonly 'pikkuScenarioGetStubCalls': RPCHandler<PikkuScenarioGetStubCallsInput, PikkuScenarioGetStubCallsOutput>,
readonly 'workflowStarter': RPCHandler<WorkflowStarterInput, WorkflowStarterOutput>,
readonly 'workflowRunner': RPCHandler<WorkflowRunnerInput, null>,
readonly 'workflowStatusChecker': RPCHandler<WorkflowStatusCheckerInput, WorkflowRunStatus>,
readonly 'workflowStatusStream': RPCHandler<WorkflowStatusStreamInput, null>,
readonly 'workflowStatusStreamFull': RPCHandler<WorkflowStatusStreamFullInput, null>,
readonly 'graphStarter': RPCHandler<GraphStarterInput, GraphStarterOutput>,
};
// Addon package RPC maps
import type { RPCMap as ConsoleRPCMap } from '@pikku/addon-console/.pikku/rpc/pikku-rpc-wirings-map.internal.gen.js'
// Utility type to prefix keys with namespace (skips 'any' to prevent type poisoning)
type PrefixKeys<T, Prefix extends string> = unknown extends T ? {} : {
[K in keyof T as `${Prefix}:${string & K}`]: T[K]
}
// Merge all RPC maps with namespace prefixes
export type FlattenedRPCMap =
RPCMap & PrefixKeys<ConsoleRPCMap, 'console'>
type IsAny<T> = 0 extends (1 & T) ? true : false
type IsVoidishInput<T> = IsAny<T> extends true
? false
: [T] extends [void | null | undefined]
? true
: false
export type RPCInvoke = <Name extends keyof FlattenedRPCMap>(
...args: IsVoidishInput<FlattenedRPCMap[Name]['input']> extends true
? [name: Name]
: [name: Name, data: FlattenedRPCMap[Name]['input']]
) => Promise<FlattenedRPCMap[Name]['output']>
export type RPCRemote = <Name extends keyof FlattenedRPCMap>(
...args: IsVoidishInput<FlattenedRPCMap[Name]['input']> extends true
? [name: Name]
: [name: Name, data: FlattenedRPCMap[Name]['input']]
) => Promise<FlattenedRPCMap[Name]['output']>
import type { FlattenedWorkflowMap } from '../workflow/pikku-workflow-map.gen.d.js'
import type { AgentMap } from '../agent/pikku-agent-map.gen.d.js'
// Addon package Agent maps
import type { AgentMap as ConsoleAgentMap } from '@pikku/addon-console/.pikku/agent/pikku-agent-map.gen.d.js'
type FlattenedAgentMap =
AgentMap & PrefixKeys<ConsoleAgentMap, 'console'>
import type { PikkuRPC } from '@pikku/core/rpc'
interface AIAgentInput {
message: string
threadId: string
resourceId: string
}
export type TypedStartWorkflow = <Name extends keyof FlattenedWorkflowMap>(
name: Name,
input: FlattenedWorkflowMap[Name]['input'],
options?: { startNode?: string }
) => Promise<{ runId: string }>
export type TypedRunWorkflow = <Name extends keyof FlattenedWorkflowMap>(
name: Name,
input: FlattenedWorkflowMap[Name]['input']
) => Promise<FlattenedWorkflowMap[Name]['output']>
export type TypedWorkflowStatus = (
workflowName: string,
runId: string
) => Promise<{ id: string; status: 'running' | 'suspended' | 'completed' | 'failed' | 'cancelled'; output?: unknown; error?: { message?: string } }>
type TypedAgentRun = [keyof FlattenedAgentMap] extends [never]
? (name: string, input: AIAgentInput) => Promise<any>
: <Name extends keyof FlattenedAgentMap>(
name: Name,
input: AIAgentInput
) => Promise<{ runId: string; result: FlattenedAgentMap[Name]['output']; usage: { inputTokens: number; outputTokens: number } }>
type TypedAgentStream = [keyof FlattenedAgentMap] extends [never]
? (name: string, input: AIAgentInput, options?: { requiresToolApproval?: 'all' | 'explicit' | false }) => Promise<void>
: <Name extends keyof FlattenedAgentMap>(
name: Name,
input: AIAgentInput,
options?: { requiresToolApproval?: 'all' | 'explicit' | false }
) => Promise<void>
export type TypedPikkuRPC = PikkuRPC<RPCInvoke, RPCRemote, TypedStartWorkflow, TypedAgentRun, TypedAgentStream>

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"agentName":{"type":"string"},"runId":{"type":"string"},"approvals":{"type":"array","items":{"type":"object","properties":{"toolCallId":{"type":"string"},"approved":{"type":"boolean"}},"required":["toolCallId","approved"],"additionalProperties":false}}},"required":["agentName","runId","approvals"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"agentName":{"type":"string"},"message":{"type":"string"},"threadId":{"type":"string"},"resourceId":{"type":"string"}},"required":["agentName","message","threadId","resourceId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"agentName":{"type":"string"},"runId":{"type":"string"},"toolCallId":{"type":"string"},"approved":{"type":"boolean"}},"required":["agentName","runId","toolCallId","approved"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"agentName":{"type":"string"},"message":{"type":"string"},"threadId":{"type":"string"},"resourceId":{"type":"string"},"context":{"type":"string"}},"required":["agentName","message","threadId","resourceId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"type":"null"},{}],"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"threadId":{"type":"string"},"resourceId":{"type":"string"}},"required":["threadId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"generatedAt":{"type":"string"},"summary":{"type":"object","properties":{"total":{"type":"number"},"covered":{"type":"number"},"partial":{"type":"number"},"uncovered":{"type":"number"},"unknown":{"type":"number"},"overallRatio":{"type":"number"}},"required":["total","covered","partial","uncovered","unknown","overallRatio"],"additionalProperties":false},"functions":{"type":"array","items":{"$ref":"#/definitions/FunctionCoverageEntry"}}},"required":["generatedAt","summary","functions"],"additionalProperties":false,"definitions":{"FunctionCoverageEntry":{"type":"object","properties":{"name":{"type":"string"},"sourceFile":{"type":"string"},"exposed":{"type":"boolean"},"description":{"type":["string","null"]},"coveredLines":{"type":"number"},"totalLines":{"type":"number"},"missedLines":{"type":"array","items":{"type":"number"}},"ratio":{"type":"number"},"status":{"$ref":"#/definitions/CoverageStatus"}},"required":["name","sourceFile","exposed","description","coveredLines","totalLines","missedLines","ratio","status"],"additionalProperties":false},"CoverageStatus":{"type":"string","enum":["covered","partial","uncovered","unknown"]}}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"threadId":{"type":"string"},"resourceId":{"type":"string"}},"required":["threadId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"array","items":{},"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"threadId":{"type":"string"},"resourceId":{"type":"string"}},"required":["threadId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"array","items":{},"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"agentName":{"type":"string"},"resourceId":{"type":"string"},"limit":{"type":"number"},"offset":{"type":"number"}},"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"array","items":{},"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{},"additionalProperties":false}

View File

@@ -0,0 +1 @@
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"userId":{"type":"string"},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["userId","email","name"],"additionalProperties":false}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"workflowName":{"type":"string"},"nodeId":{"type":"string"},"data":{}},"required":["workflowName","nodeId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"runId":{"type":"string"}},"required":["runId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"secretId":{"type":"string"}},"required":["secretId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"exists":{"type":"boolean"},"value":{}},"required":["exists","value"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"variableId":{"type":"string"}},"required":["variableId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"exists":{"type":"boolean"},"value":{}},"required":["exists","value"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"secretId":{"type":"string"}},"required":["secretId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"exists":{"type":"boolean"}},"required":["exists"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"secretId":{"type":"string"},"value":{}},"required":["secretId","value"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"success":{"type":"boolean"}},"required":["success"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"variableId":{"type":"string"},"value":{}},"required":["variableId","value"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"success":{"type":"boolean"}},"required":["success"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"service":{"type":"string"}},"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"array","items":{"$ref":"#/definitions/StubCall"},"definitions":{"StubCall":{"type":"object","properties":{"service":{"type":"string"},"method":{"type":"string"},"args":{"type":"array","items":{}}},"required":["service","method","args"],"additionalProperties":false}}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"topic":{"type":"string"}},"required":["topic"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"topic":{"type":"string"}},"required":["topic"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"topic":{"type":"string"}},"required":["topic"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"rpcName":{"type":"string"},"data":{}},"required":["rpcName"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"string"}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"email":{"type":"string"},"userId":{"type":"string"}},"required":["email","userId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"runId":{"type":"string"}},"required":["runId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"id":{"type":"string"},"status":{"$ref":"#/definitions/WorkflowStatus"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"deterministic":{"type":"boolean"},"plannedSteps":{"type":"array","items":{"$ref":"#/definitions/WorkflowPlannedStep"}},"steps":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"status":{"$ref":"#/definitions/StepStatus"},"duration":{"type":"number"},"attempts":{"type":"number","description":"Number of attempts for this step (1 = first try; > 1 means it retried)."}},"required":["name","status"],"additionalProperties":false}},"output":{},"error":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}},"required":["id","status","startedAt","steps"],"additionalProperties":false,"definitions":{"WorkflowStatus":{"type":"string","enum":["running","suspended","completed","failed","cancelled"],"description":"Workflow run status"},"WorkflowPlannedStep":{"type":"object","properties":{"stepName":{"type":"string","description":"Durable step key — matches the runtime step name stored in the DB"},"displayName":{"type":"string","description":"Optional human-readable label for the UI timeline (falls back to stepName)"}},"required":["stepName"],"additionalProperties":false},"StepStatus":{"type":"string","enum":["pending","running","scheduled","succeeded","failed","suspended"],"description":"Workflow step status"}}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"workflowName":{"type":"string"},"data":{}},"required":["workflowName"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"workflowName":{"type":"string"},"data":{}},"required":["workflowName"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"runId":{"type":"string"}},"required":["runId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"workflowName":{"type":"string"},"runId":{"type":"string"}},"required":["workflowName","runId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"workflowName":{"type":"string"},"runId":{"type":"string"}},"required":["workflowName","runId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1 @@
{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"workflowName":{"type":"string"},"runId":{"type":"string"}},"required":["workflowName","runId"],"additionalProperties":false,"definitions":{}}

View File

@@ -0,0 +1,97 @@
/**
* This file was generated by @pikku/cli@0.12.76
*/
/**
* Workflow type map with input/output types for each workflow
*/
import type { StubCall } from '.bun/@pikku+core@0.12.57/node_modules/@pikku/core/dist/services/stub-tracker'
export type AgentApproveCallerInput = { agentName: string; runId: string; approvals: { toolCallId: string; approved: boolean; }[]; }
export type AgentCallerInput = { agentName: string; message: string; threadId: string; resourceId: string; }
export type AgentResumeCallerInput = { agentName: string; runId: string; toolCallId: string; approved: boolean; }
export type AgentStreamCallerInput = { agentName: string; message: string; threadId: string; resourceId: string; context?: string | undefined; }
export type AuthHandlerOutput = Promise<void> | Promise<any>
export type DeleteAgentThreadInput = { threadId: string; resourceId?: string | undefined; }
export type DeleteAgentThreadOutput = { deleted: boolean; }
export type GetAgentThreadMessagesInput = { threadId: string; resourceId?: string | undefined; }
export type GetAgentThreadMessagesOutput = any[]
export type GetAgentThreadRunsInput = { threadId: string; resourceId?: string | undefined; }
export type GetAgentThreadRunsOutput = any[]
export type GetAgentThreadsInput = { agentName?: string | undefined; resourceId?: string | undefined; limit?: number | undefined; offset?: number | undefined; }
export type GetAgentThreadsOutput = any[]
export type GetSessionInput = {}
export type GetSessionOutput = {
userId: string;
email: string;
name: string | null;
}
export type GraphStarterInput = { workflowName: string; nodeId: string; data?: unknown; }
export type GraphStarterOutput = { runId: string; }
export type PikkuConsoleGetSecretInput = { secretId: string; }
export type PikkuConsoleGetSecretOutput = { exists: boolean; value: unknown; }
export type PikkuConsoleGetVariableInput = { variableId: string; }
export type PikkuConsoleGetVariableOutput = { exists: boolean; value: unknown; }
export type PikkuConsoleHasSecretInput = { secretId: string; }
export type PikkuConsoleHasSecretOutput = { exists: boolean; }
export type PikkuConsoleSetSecretInput = { secretId: string; value: unknown; }
export type PikkuConsoleSetSecretOutput = { success: boolean; }
export type PikkuConsoleSetVariableInput = { variableId: string; value: unknown; }
export type PikkuConsoleSetVariableOutput = { success: boolean; }
export type PikkuScenarioGetStubCallsInput = { service?: string | undefined; }
export type PikkuScenarioGetStubCallsOutput = StubCall[]
export type PikkuScenarioResetLiveCoverageOutput = { enabled: boolean; }
export type PikkuScenarioResetStubsOutput = { enabled: boolean; }
export type RealtimeEventStreamInput = { topic: string; }
export type RealtimeSubscribeInput = { topic: string; }
export type RealtimeUnsubscribeInput = { topic: string; }
export type RpcCallerInput = { rpcName: string; data?: unknown; }
export type SecretSchema_betterAuthSecret = string
export type SessionHealthScenarioOutput = { email: string; userId: string; }
export type WorkflowRunnerInput = { workflowName: string; data?: unknown; }
export type WorkflowStarterInput = { workflowName: string; data?: unknown; }
export type WorkflowStarterOutput = { runId: string; }
export type WorkflowStatusCheckerInput = { workflowName: string; runId: string; }
export type WorkflowStatusStreamFullInput = { workflowName: string; runId: string; }
export type WorkflowStatusStreamInput = { workflowName: string; runId: string; }
// Addon package Workflow maps
import type { WorkflowMap as ConsoleWorkflowMap } from '@pikku/addon-console/.pikku/workflow/pikku-workflow-map.gen.d.js'
interface WorkflowHandler<I, O> {
input: I;
output: O;
}
interface GraphNodeHandler<I> {
input: I;
}
export type WorkflowMap = {
readonly 'sessionHealthScenario': WorkflowHandler<void, SessionHealthScenarioOutput>,
};
export type GraphsMap = {
readonly 'sessionHealthScenario': {
readonly 'visitor signs in and reads their session': GraphNodeHandler<GetSessionInput>,
},
};
type PrefixWorkflowKeys<T, Prefix extends string> = unknown extends T ? {} : {
[K in keyof T as `${Prefix}:${string & K}`]: T[K]
}
export type FlattenedWorkflowMap =
WorkflowMap & PrefixWorkflowKeys<ConsoleWorkflowMap, 'console'>
export type WorkflowClient<Name extends keyof FlattenedWorkflowMap> = {
start: (input: FlattenedWorkflowMap[Name]['input']) => Promise<{ runId: string }>;
getRun: <output extends keyof FlattenedWorkflowMap[Name]>(runId: string) => Promise<FlattenedWorkflowMap[Name][output]>;
cancelRun: (runId: string) => Promise<boolean>;
}
export type TypedWorkflowClients = {
[Name in keyof FlattenedWorkflowMap]: WorkflowClient<Name>;
}

View File

@@ -0,0 +1,34 @@
{
"name": "@project/functions",
"version": "0.0.1",
"private": true,
"type": "module",
"imports": {
"#pikku": "./.pikku/pikku-types.gen.ts",
"#pikku/*.gen.js": "./.pikku/*.gen.ts",
"#pikku/*": "./.pikku/*"
},
"scripts": {
"db:types": "pikku db types"
},
"dependencies": {
"@ai-sdk/openai-compatible": "^2.0.48",
"@pikku/addon-console": "^0.12.22",
"@pikku/ai-vercel": "^0.12.7",
"@pikku/better-auth": "^0.12.16",
"@pikku/core": "^0.12.57",
"@pikku/kysely": "^0.13.0",
"@pikku/schema-cfworker": "^0.12.4",
"@standard-schema/spec": "^1.1.0",
"ai": "^6.0.0",
"better-auth": "^1.6.18",
"kysely": "^0.29.0",
"tsx": "^4.21.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@pikku/deploy-standalone": "^0.12.7",
"@types/node": "^22",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,39 @@
import type { CoreServices, CoreSingletonServices, CoreConfig, CoreUserSession } from '@pikku/core'
import type { AuditLog, EmailService } from '@pikku/core/services'
import type { Kysely } from 'kysely'
import type { DB } from '#pikku/db/schema.gen.js'
import type { TypedSecretService } from '../.pikku/secrets/pikku-secrets.gen.js'
import type { TypedVariablesService } from '../.pikku/variables/pikku-variables.gen.js'
import type { auth } from './auth.js'
export interface UserSession extends CoreUserSession {
userId: string
}
export interface Config extends CoreConfig {
port: number
hostname: string
}
export interface SingletonServices extends CoreSingletonServices<Config> {
variables: TypedVariablesService
secrets: TypedSecretService
kysely: Kysely<DB>
// Resolved Better Auth instance, injected once by the generated pikkuServices
// wrapper from the `auth` factory. Typed as the factory's return so functions
// get better-auth's full server `api`/`handler` surface via DI.
auth: Awaited<ReturnType<typeof auth>>
// Always constructed in services.ts, so declare it REQUIRED here — it is
// optional in CoreSingletonServices, which otherwise makes every emailService
// use read as possibly-undefined and forces needless `!`/guards in functions.
emailService: EmailService
// Per-invocation audit log, ALWAYS returned from createWireServices (see
// services.ts) so general activity logging is available in every function —
// `await auditLog.write({ type, source: 'explicit', metadata })`. Declared
// REQUIRED (like emailService above) so a plain `auditLog.write(...)` doesn't
// read as possibly-undefined and force needless `?.`/guards. A function with
// `audit: true` ADDITIONALLY gets a kysely wrapped to capture every table write.
auditLog: AuditLog
}
export interface Services extends CoreServices<SingletonServices> {}

View File

@@ -0,0 +1,72 @@
import { betterAuth } from 'better-auth'
import { admin } from 'better-auth/plugins'
import { actor, fabric } from '@pikku/better-auth'
import { pikkuBetterAuth } from '#pikku'
/**
* Better Auth configuration — email + password sign-in.
*
* `pikkuBetterAuth` has no side effects: the pikku CLI statically inspects this single
* exported `auth` const and generates the catch-all `/api/auth/**` HTTP wiring,
* the session-bridge middleware, and a `wireSecret` for `BETTER_AUTH_SECRET` (and
* one per social provider, if you add any) — so the auth routes and secret
* requirements flow through normal inspection into the deploy manifest.
*
* The factory runs once when singleton services are built, pulling the secret
* (and the database) off the injected `services`; the resolved instance is then
* available to every function as `services.auth`. Better Auth is given the app's
* own kysely: the CamelCasePlugin maps Better Auth's camelCase field names onto
* the snake_case columns created in db/sqlite/0001-init.sql, keeping the whole DB
* on one naming convention. To offer Google / GitHub / ... add a `socialProviders`
* entry (and a button on the login page) — the CLI will wire its secret too.
*/
// The factory receives the FULL singleton services (emailService, logger, …) —
// destructure whatever you need, e.g. `{ kysely, secrets, emailService }` to wire
// sendResetPassword/verification emails. It runs lazily after all services exist,
// so never re-construct a service here or reach for a dynamic import.
export const auth = pikkuBetterAuth(async ({ kysely, secrets, variables }) => {
const BETTER_AUTH_SECRET = await secrets.getSecret('BETTER_AUTH_SECRET')
// Genuinely optional: unset simply disables /api/auth/sign-in/actor (scenarios
// off for this deployment) — the actor plugin refuses all sign-ins
// without it.
const SCENARIO_ACTOR_SECRET = await secrets
.getSecret('SCENARIO_ACTOR_SECRET')
.catch(() => undefined)
// Fabric operator admin: the RSA public key the control plane's token is
// verified against. The Fabric deployer pushes FABRIC_AUTH_PUBLIC_KEY onto
// every stage; locally it's simply absent, which disables /sign-in/fabric.
// Asymmetric — the app verifies, it can never forge an operator login.
const FABRIC_AUTH_PUBLIC_KEY = await variables.get('FABRIC_AUTH_PUBLIC_KEY')
return betterAuth({
secret: BETTER_AUTH_SECRET,
database: { db: kysely, type: 'sqlite' },
emailAndPassword: { enabled: true },
// Stateless session: CLI splits out betterAuthStatelessSession so non-auth
// units verify the signed cookie instead of bundling better-auth. pikku #737.
session: { cookieCache: { enabled: true } },
advanced: { database: { generateId: 'uuid' } },
// Scenario actors: synthetic users (user.actor = true, see
// db/sqlite/0003-user-actor.sql) signed in by pikkuScenario via
// POST /api/auth/sign-in/actor { email, secret }. Never signs in real users.
//
// admin(): exposes /api/auth/admin/* (listUsers, setRole, impersonateUser,
// …) so an app admin can list and "view as" their end-users — this is what
// the Fabric console's Users tab drives. Adds role/banned/impersonatedBy
// columns (see db/sqlite/0004-admin.sql). No user is an admin by default:
// grant it by setting a user's `role` to 'admin' (or pass
// `adminUserIds: [...]` here) — the admin API refuses non-admins.
//
// fabric(): exposes /api/auth/sign-in/fabric — the Fabric control plane
// mints a short-lived RS256 token and signs in as a synthetic `fabric: true`
// admin operator (db/sqlite/0005-fabric.sql), so the console Users tab can
// list/impersonate real users without the operator being one of them. It
// pairs with admin() and verifies against FABRIC_AUTH_PUBLIC_KEY; missing
// key disables the endpoint.
plugins: [
actor({ secret: SCENARIO_ACTOR_SECRET }),
admin(),
fabric({ publicKey: FABRIC_AUTH_PUBLIC_KEY }),
],
})
})

View File

@@ -0,0 +1,6 @@
import { pikkuConfig } from '../.pikku/pikku-types.gen.js'
export const createConfig = pikkuConfig(async () => ({
port: parseInt(process.env.API_PORT || '4003', 10),
hostname: process.env.HOST || '0.0.0.0',
}))

View File

@@ -0,0 +1,32 @@
import { z } from 'zod'
import { pikkuFunc } from '#pikku'
export const GetSessionInput = z.object({})
export const GetSessionOutput = z.object({
userId: z.string(),
email: z.string().email(),
name: z.string().nullable(),
})
export const getSession = pikkuFunc({
expose: true,
readonly: true,
auth: true,
description: 'Returns the current signed-in user.',
input: GetSessionInput,
output: GetSessionOutput,
func: async ({ kysely }, _input, { session }) => {
const user = await kysely
.selectFrom('user')
.select(['id', 'email', 'name'])
.where('id', '=', session!.userId)
.executeTakeFirstOrThrow()
return {
userId: user.id,
email: user.email,
name: user.name ?? null,
}
},
})

View File

@@ -0,0 +1,33 @@
import { pikkuScenario } from '#pikku/workflow/pikku-workflow-types.gen.js'
/**
* A scenario is a story of RPC calls told through a synthetic persona (an
* "actor" — see scenarios.actors in pikku.config.json). Every step runs over
* the REAL transport with the actor's session cookie, so a passing scenario
* proves the deployed API works exactly as a signed-in user would experience it.
*
* Run it with `pikku scenario run local` (needs SCENARIO_ACTOR_SECRET in the
* environment — the actor plugin in src/auth.ts refuses sign-ins without it).
* This one signs the visitor in and reads their own session: actor sign-in,
* session cookie, authed RPC, and session mapping in one pass — a ready-made
* health check for staging or production.
*/
export const sessionHealthScenario = pikkuScenario<void, { email: string; userId: string }>({
title: 'Session health (scenario)',
tags: ['scenario'],
func: async ({ logger }, _input, { workflow, actors }) => {
if (!actors?.visitor) {
throw new Error(
'sessionHealthScenario needs run actors (visitor) — run via `pikku scenario run <environment>`',
)
}
logger.debug('session-health scenario starting')
const session = await workflow.do(
'visitor signs in and reads their session',
'getSession',
{},
{ actor: actors.visitor },
)
return { email: session.email, userId: session.userId }
},
})

View File

@@ -0,0 +1,52 @@
import {
LocalEmailService,
type EmailService,
type SendEmailInput,
type SendEmailResult,
} from '@pikku/core/services'
import { renderEmailTemplate, type EmailTemplateName } from '../../.pikku/email/pikku-emails.gen.js'
type GeneratedTemplateEmailServiceOptions = {
delegate?: EmailService
defaultLocale?: string
}
export class GeneratedTemplateEmailService implements EmailService {
private readonly delegate: EmailService
private readonly defaultLocale: string
constructor(options: GeneratedTemplateEmailServiceOptions = {}) {
this.delegate = options.delegate ?? new LocalEmailService()
this.defaultLocale = options.defaultLocale ?? 'en'
}
async send(input: SendEmailInput): Promise<SendEmailResult> {
if (!('template' in input) || !input.template) {
return this.delegate.send(input)
}
const rendered = renderEmailTemplate({
name: input.template.name as EmailTemplateName,
locale: (input.template.locale ?? this.defaultLocale) as Parameters<
typeof renderEmailTemplate
>[0]['locale'],
data: input.template.data ?? {},
})
return this.delegate.send({
to: input.to,
from: input.from,
cc: input.cc,
bcc: input.bcc,
replyTo: input.replyTo,
headers: {
...(input.headers ?? {}),
'x-pikku-email-template': String(input.template.name),
'x-pikku-email-hash': rendered.hash,
},
subject: rendered.subject,
html: rendered.html,
...(rendered.text ? { text: rendered.text } : {}),
})
}
}

View File

@@ -0,0 +1,18 @@
import { addHTTPMiddleware } from '@pikku/core/http'
import { cors } from '@pikku/core/middleware'
const corsOrigins = process.env.CORS_ORIGINS
? process.env.CORS_ORIGINS.split(',')
.map((origin) => origin.trim())
.filter(Boolean)
: [process.env.FRONTEND_URL, 'http://localhost:7104', 'http://127.0.0.1:7104'].filter(
(origin): origin is string => Boolean(origin),
)
addHTTPMiddleware('*', [
cors({
origin: corsOrigins,
credentials: true,
headers: ['Content-Type', 'Authorization', 'X-Auth-Return-Redirect'],
}),
])

View File

@@ -0,0 +1,98 @@
import {
JsonConsoleLogger,
LocalEmailService,
LocalSecretService,
LocalVariablesService,
NoopAuditService,
createInvocationAudit,
} from '@pikku/core/services'
import { createAuditedKysely } from '@pikku/kysely'
import { pikkuServices, pikkuWireServices } from '../.pikku/pikku-types.gen.js'
import { TypedSecretService } from '../.pikku/secrets/pikku-secrets.gen.js'
import { TypedVariablesService } from '../.pikku/variables/pikku-variables.gen.js'
import { CFWorkerSchemaService } from '@pikku/schema-cfworker'
import type { Kysely } from 'kysely'
import type { VercelAIAgentRunner } from '@pikku/ai-vercel'
import { GeneratedTemplateEmailService } from './lib/email-service.js'
import type { DB } from '#pikku/db/schema.gen.js'
export const createSingletonServices = pikkuServices(async (config, existingServices) => {
const variables =
existingServices?.variables ?? new TypedVariablesService(new LocalVariablesService())
const secrets =
existingServices?.secrets ?? new TypedSecretService(new LocalSecretService(variables))
const logger = existingServices?.logger ?? new JsonConsoleLogger()
const schema = existingServices?.schema ?? new CFWorkerSchemaService(logger)
const emailService =
existingServices?.emailService ??
new GeneratedTemplateEmailService({
delegate: new LocalEmailService(),
})
// The durable audit sink. In a deployed stage fabric injects the platform's
// audit service; locally it falls back to a no-op so nothing is persisted.
const audit = existingServices?.audit ?? new NoopAuditService()
// kysely is injected by pikku dev (node:sqlite) or the CF Worker workflow (libsql).
// The template never constructs its own dialect — dialects are fabric/runtime
// concerns — so it must always be provided by the runtime.
if (!existingServices?.kysely) {
throw new Error('kysely service was not injected by the runtime (pikku dev / fabric)')
}
const kysely: Kysely<DB> = existingServices.kysely
const litellmProxyUrl = process.env.LITELLM_PROXY_URL ?? null
const litellmApiKey = process.env.LITELLM_API_KEY ?? null
let aiAgentRunner: VercelAIAgentRunner | undefined
if (litellmProxyUrl && litellmApiKey) {
// The AI SDKs (~3MB) are stubbed out of non-agent units at bundle time —
// only units with the `ai-model` capability keep them. So import them
// dynamically and guard on the module resolving to a real export; in a
// stubbed unit the import yields `{}` and the runner is simply not built.
const aiVercel = await import('@pikku/ai-vercel')
const aiSdk = await import('@ai-sdk/openai-compatible')
if (aiVercel.VercelAIAgentRunner && aiSdk.createOpenAICompatible) {
const litellmProvider = aiSdk.createOpenAICompatible({
name: 'litellm',
baseURL: litellmProxyUrl,
apiKey: litellmApiKey,
})
aiAgentRunner = new aiVercel.VercelAIAgentRunner({
openai: (modelId: string) => litellmProvider.chatModel(modelId),
anthropic: (modelId: string) => litellmProvider.chatModel(modelId),
google: (modelId: string) => litellmProvider.chatModel(modelId),
deepseek: (modelId: string) => litellmProvider.chatModel(modelId),
})
}
}
return {
...(existingServices ?? {}),
config,
variables,
secrets,
schema,
logger,
emailService,
audit,
kysely,
...(aiAgentRunner ? { aiAgentRunner } : {}),
}
})
export const createWireServices = pikkuWireServices(async (singletonServices, wire) => {
if (!singletonServices.audit) {
return {}
}
const auditLog = createInvocationAudit(singletonServices.audit, wire)
// auditLog is ALWAYS returned so `auditLog.write(...)` works in every function
// for general activity logging. `auditLog.config` is set ONLY when this
// invocation opts into row-level auditing (`audit: true`); when it is, ALSO wrap
// kysely so every query is captured and the runner flushes the buffer on close.
// Without audit: true, leave the plain kysely untouched — no per-query overhead.
if (!auditLog.config) {
return { auditLog }
}
return {
auditLog,
kysely: createAuditedKysely(singletonServices.kysely, { audit: auditLog }),
}
})

View File

@@ -0,0 +1,3 @@
{
"id": "default"
}

View File

@@ -0,0 +1,215 @@
import { createTheme, type MantineColorsTuple, type MantineThemeOverride } from '@mantine/core'
import { generateColors } from '@mantine/colors-generator'
import active from './active.json'
import { themeSpecs } from './themes'
const FONT_FALLBACK = "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
const MONO_FALLBACK =
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace"
// Mono families need a monospace fallback chain, not the sans-serif one — else a
// terminal/matrix theme degrades to a proportional font before the webfont loads.
const isMonoFamily = (family: string): boolean => /\bmono\b|consol|courier/i.test(family)
// A theme is ONE Mantine theme, authored as two clearly-labelled sections:
// brand — HIGH-LEVEL: colour palette + fonts (what makes apps look different)
// structure — LOW-LEVEL Mantine: radius, shadows, spacing, component defaults
// buildMantineTheme takes the structure as the base and EXTENDS it with the
// brand, then that single merged object is registered with MantineProvider.
export type Brand = {
colors?: Record<string, string>
fonts?: { heading?: string; body?: string; mono?: string }
}
export type Structure = {
defaultRadius?: string
autoContrast?: boolean
primaryShade?: number | { light?: number; dark?: number }
shadows?: Record<string, string>
spacing?: Record<string, string>
radius?: Record<string, string>
components?: Record<string, unknown>
// The style's natural scheme — seeds ColorSchemeScript + MantineProvider so a
// light style renders light and a dark style dark by default. NOT a createTheme
// key, so it is stripped before the theme is built.
defaultColorScheme?: 'light' | 'dark' | 'auto'
// An explicit 10-step Mantine `dark` tuple used verbatim (idx0 lightest text →
// idx7 body bg → idx9 deepest). When present it overrides the auto-tint so a
// dark-first style gets its exact background. NOT a createTheme key.
darkColors?: string[]
}
export type Theme = {
name: string
description?: string
brand?: Brand
structure?: Structure
}
// Back-compat alias: older imports referred to a colours-only "Palette".
export type Palette = Theme
const fontStack = (family?: string): string | undefined =>
family ? `'${family}', ${isMonoFamily(family) ? MONO_FALLBACK : FONT_FALLBACK}` : undefined
// Brand-tinted dark surfaces. Mantine's dark scheme derives the body, surfaces,
// borders and inputs from the `dark` colour tuple; the default ramp is neutral
// grey, so every app looked identical regardless of the brand colour. We rebuild
// the dark ramp at the brand's HUE so the whole background takes on the theme —
// keeping text shades (0..3) near-neutral for contrast and saturating the deep
// background shades (6..9) for a clearly distinct, on-brand dark look.
const hexToHsl = (hex: string): [number, number, number] | null => {
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim())
if (!m) return null
const int = parseInt(m[1], 16)
const r = ((int >> 16) & 255) / 255
const g = ((int >> 8) & 255) / 255
const b = (int & 255) / 255
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const l = (max + min) / 2
let h = 0
let s = 0
if (max !== min) {
const d = max - min
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
if (max === r) h = (g - b) / d + (g < b ? 6 : 0)
else if (max === g) h = (b - r) / d + 2
else h = (r - g) / d + 4
h *= 60
}
return [h, s * 100, l * 100]
}
const hslToHex = (h: number, s: number, l: number): string => {
s /= 100
l /= 100
const k = (n: number) => (n + h / 30) % 12
const a = s * Math.min(l, 1 - l)
const f = (n: number) => {
const v = l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))
return Math.round(255 * v)
.toString(16)
.padStart(2, '0')
}
return `#${f(0)}${f(8)}${f(4)}`
}
// Lightness/saturation anchors per dark-tuple index (0 = lightest text → 9 =
// deepest background). dark[7] is the body background in Mantine's dark scheme.
// Text shades (0..3) stay near-neutral for readable contrast; the surface and
// background shades (5..9) carry real brand saturation so the dark UI reads as a
// rich, on-brand dark — NOT the flat near-grey it used to be (the "all gray" bug).
const DARK_L = [96, 88, 77, 62, 48, 36, 25, 17, 12, 9]
// Restrained saturation: just enough hue to read as on-brand, never the lurid
// over-saturated background that made dark apps look "absurd". Dark-first styles
// that want a precise background ship an explicit `darkColors` tuple instead.
const DARK_S = [8, 10, 12, 14, 16, 18, 20, 22, 24, 26]
// Below this saturation a brand reads as greyscale (e.g. a mono/neutral theme);
// tinting it at its near-arbitrary hue would turn the whole UI an off-colour
// (a grey seed sits at hue 0 → a red-tinted dark). Keep those neutral.
const MIN_TINT_SATURATION = 12
const tintedDarkTuple = (hex: string): MantineColorsTuple | null => {
const hsl = hexToHsl(hex)
if (!hsl) return null
const [h, s] = hsl
if (s < MIN_TINT_SATURATION) return null
return DARK_L.map((l, i) => hslToHex(h, DARK_S[i]!, l)) as unknown as MantineColorsTuple
}
// structure (base) → extend with brand (colours + fonts) → one Mantine theme.
export const buildMantineTheme = (spec: Theme): MantineThemeOverride => {
// Pull the two non-Mantine fields out of structure before it is spread into
// createTheme — they drive scheme/dark-tuple, not the theme object.
const {
defaultColorScheme: _scheme,
darkColors,
...structureRest
} = (spec.structure ?? {}) as Structure
const structure = structureRest as MantineThemeOverride
const brand = spec.brand ?? {}
const colors: Record<string, MantineColorsTuple> = Object.fromEntries(
Object.entries(brand.colors ?? {}).map(([role, hex]) => [role, generateColors(hex)]),
)
// An explicit dark tuple (from a dark-first style) wins verbatim; otherwise tint
// the dark surfaces/background at the brand hue so apps still look distinct.
const dark =
darkColors && darkColors.length === 10
? (darkColors as unknown as MantineColorsTuple)
: brand.colors?.primary
? tintedDarkTuple(brand.colors.primary)
: null
if (dark) colors.dark = dark
const body = fontStack(brand.fonts?.body)
const heading = fontStack(brand.fonts?.heading ?? brand.fonts?.body)
const mono = fontStack(brand.fonts?.mono)
// Mantine's default dark primaryShade is 8 — a deep, muted tone that reads as
// near-grey against the dark background, so buttons/accents/the logo all looked
// washed out (the other half of the "all gray" bug). Force a vibrant shade in
// dark mode so the brand actually pops; honour an explicit theme override.
// Cast to Mantine's narrow MantineColorShade union — structure JSON types the
// shade loosely as number, which the literal 09 union rejects.
const specShade = (spec.structure ?? {}).primaryShade
const primaryShade = (specShade ?? { light: 6, dark: 5 }) as MantineThemeOverride['primaryShade']
return createTheme({
...structure,
primaryShade,
...(body ? { fontFamily: body } : {}),
...(heading ? { headings: { fontFamily: heading } } : {}),
...(mono ? { fontFamilyMonospace: mono } : {}),
...(Object.keys(colors).length ? { colors } : {}),
// Drive the primary colour from the brand. Previously a theme could define
// colours that Mantine never used as the primary — that was the "themes
// don't react" bug.
...('primary' in colors ? { primaryColor: 'primary' } : {}),
})
}
// Back-compat alias for callers that pass a spec.
export const buildTheme = buildMantineTheme
const specs = themeSpecs as Record<string, Theme>
export const themes: Record<string, MantineThemeOverride> = Object.fromEntries(
Object.entries(specs).map(([id, s]) => [id, buildMantineTheme(s)]),
)
export const themeList = Object.entries(specs).map(([id, s]) => ({
id,
name: s.name,
description: s.description,
}))
export const activeId = active.id
export const activeTheme: MantineThemeOverride = themes[active.id] ?? themes.default
export const theme = activeTheme
// The natural color scheme each theme declares, so the app can seed
// ColorSchemeScript + MantineProvider to match (a light style boots light, a dark
// style dark). Defaults to 'dark' (the template's historical default).
export type ColorScheme = 'light' | 'dark' | 'auto'
export const themeColorSchemes: Record<string, ColorScheme> = Object.fromEntries(
Object.entries(specs).map(([id, s]) => [id, s.structure?.defaultColorScheme ?? 'dark']),
)
export const activeColorScheme: ColorScheme = themeColorSchemes[active.id] ?? 'dark'
// Distinct font families referenced by any theme, for the <head> Google Fonts
// <link> so a theme's typography actually renders.
export const brandFontFamilies: string[] = [
...new Set(
Object.values(specs).flatMap((s) => {
const f = s.brand?.fonts ?? {}
return [f.body, f.heading, f.mono].filter((x): x is string => Boolean(x))
}),
),
]
export const googleFontsHref = (families: string[] = brandFontFamilies): string | null => {
if (!families.length) return null
const params = families
.map((f) => `family=${encodeURIComponent(f)}:wght@400;500;600;700`)
.join('&')
return `https://fonts.googleapis.com/css2?${params}&display=swap`
}

View File

@@ -0,0 +1,25 @@
{
"name": "@project/mantine-themes",
"version": "0.0.1",
"private": true,
"type": "module",
"exports": {
".": "./index.ts",
"./base.json": "./base.json",
"./active.json": "./active.json"
},
"scripts": {
"tsc": "tsc --noEmit"
},
"dependencies": {
"@mantine/colors-generator": "^9.2.1",
"chroma-js": "^3.1.2"
},
"devDependencies": {
"@mantine/core": "^9.2.1",
"typescript": "^5.9"
},
"peerDependencies": {
"@mantine/core": "^9.2.1"
}
}

View File

@@ -0,0 +1,25 @@
{
"name": "Default",
"description": "Confident indigo with soft elevation — a solid, non-gray starting point.",
"brand": {
"colors": { "primary": "#4f46e5", "secondary": "#0ea5e9", "accent": "#f59e0b" },
"fonts": { "heading": "Inter", "body": "Inter" }
},
"structure": {
"defaultRadius": "md",
"autoContrast": true,
"primaryShade": { "light": 6, "dark": 5 },
"shadows": {
"xs": "0 1px 3px rgba(0,0,0,0.08)",
"sm": "0 2px 8px rgba(0,0,0,0.10)",
"md": "0 6px 18px -4px rgba(0,0,0,0.15)",
"lg": "0 12px 32px -8px rgba(0,0,0,0.20)",
"xl": "0 20px 48px -12px rgba(0,0,0,0.24)"
},
"defaultGradient": { "from": "primary.5", "to": "secondary.6", "deg": 135 },
"components": {
"Card": { "defaultProps": { "shadow": "sm", "radius": "md", "withBorder": true } },
"Paper": { "defaultProps": { "shadow": "sm", "radius": "md" } }
}
}
}

View File

@@ -0,0 +1,8 @@
// Generated by the fabric-theme tool — do not edit by hand.
// One JSON per theme. Each bundles a `brand` (colours + fonts) and a
// `structure` (radius/shadows/spacing) section. Add themes via the fabric-theme tool.
import t_default from './default.json'
export const themeSpecs: Record<string, unknown> = {
default: t_default,
}

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"noEmit": true,
"types": []
},
"include": ["index.ts", "base.json", "active.json", "palettes/**/*.ts", "palettes/**/*.json"]
}