chore: kanban template

This commit is contained in:
e2e
2026-06-21 22:13:34 +02:00
commit 4915d2c45c
209 changed files with 215578 additions and 0 deletions

49
.gitignore vendored Normal file
View File

@@ -0,0 +1,49 @@
node_modules
dist
.next
.vite
.tanstack
.deploy/
.wrangler/
.pikku-runtime/
# Local dev SQLite database (regenerated via `pikku db migrate`)
packages/functions/.pikku/dev.db
packages/functions/.pikku/dev.db-*
# Locally instantiated apps. Real projects commit these; the template
# repo never ships an instantiated user app.
!apps/fabric-design/
# Generated by @pikku/cli — regenerated on `pikku all`.
# Ignore the contents (not the directory) so negation below can whitelist
# the `api.gen.ts` stub that keeps the skeleton compilable pre-`pikku all`.
packages/functions/src/pikku/*
packages/functions/.pikku/*
!packages/functions/.pikku/client
packages/functions/.pikku/client/*
!packages/functions/.pikku/client/api.gen.ts
packages/functions/.pikku-runtime/
# Compiled .js siblings of the checked-in scaffold .gen.ts files.
packages/functions/src/scaffold/*.gen.js
# Yarn — commit the root lockfile; ignore PnP/cache and generated per-unit lockfiles
.yarn/
.pnp.*
yarn.lock
!/yarn.lock
# pikku deploy plan output — regenerated by the deploy container
packages/functions/.deploy/
# OpenCode config, skills, tools + templates — all written by the Fabric dev
# machine at startup, never committed to the project repo.
.opencode/
# Vendored pikku tarballs — a temporary packaging hack. The stable-named
# tarballs (e.g. vendor/pikku-core.tgz) are committed: package.json resolves
# `file:./vendor/*.tgz` against them and project creation seeds new repos from
# them, so they must stay tracked. The dev machine's sync_vendor_packages also
# drops versioned copies (e.g. vendor/pikku-core-0.12.20.tgz) at boot — ignore
# only those so they don't dirty the tree and block fast-forward pulls.
vendor/*-[0-9]*.tgz

4
.yarnrc.yml Normal file
View File

@@ -0,0 +1,4 @@
# Force node_modules linker — some toolchain deps (uWebSockets.js native
# addon, pikku CLI bin, esbuild native) resolve cleanly only with a
# traditional tree.
nodeLinker: node-modules

40
README.md Normal file
View File

@@ -0,0 +1,40 @@
# Kanban Board Template
Production-style Fabric demo template that exercises all core surfaces in one project:
- `http` - board APIs + workflow start routes
- `channel` - `/ws/kanban` connect/message handlers
- `queue` - `kanban-health-check` worker
- `schedule` - periodic health-check trigger
- `workflow` - demo orchestration workflow
- `mcp` - exposed command endpoint (`mcpKanbanCommand`)
- `agent` - `kanbanOpsAgent` with tool wiring
## Quick Start
1. `yarn install`
2. `yarn workspace @project/functions pikku db migrate`
3. `yarn prebuild`
4. `yarn dev`
5. Open `/kanban`
## Demo RPCs
- `listKanbanCards`
- `getKanbanMetric`
- `getKanbanWorkflowStatus`
- `createKanbanCard`
- `moveKanbanCard`
- `enqueueKanbanHealthCheck`
- `startKanbanDemoWorkflow`
- `mcpKanbanCommand`
## Template Conventions
- Same workspace and deploy layout as Fabric `starter-template`
- Root `pikku.config.json` and `packages/functions/pikku.config.json` preserved
- SQL migrations live in `db/migrations/`
- Optional `db/seed.sql` runs only via `pikku db seed` / `pikku db reset`
- DB types regenerate at `packages/functions/.pikku/db/schema.d.ts` on every `pikku db migrate`
- UI routes live in `apps/kanban/src/routes/` (TanStack Start, SSR on Cloudflare)
- Shared widgets/components live in `packages/components/`

10
apps/kanban/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
node_modules
dist
.vite
# Generated by @pikku/cli — regenerated on `pikku all`.
src/gen/
# Generated by @tanstack/router-plugin — the seed at src/routeTree.gen.ts
# is committed so tsc passes before the plugin has run; the plugin
# overwrites it as soon as dev starts.

View File

@@ -0,0 +1,33 @@
{
"extends": ["stylelint-config-standard"],
"plugins": ["stylelint-use-logical"],
"rules": {
"csstools/use-logical": [
"always",
{
"except": [
"top",
"bottom",
"width",
"height",
"min-width",
"max-width",
"min-height",
"max-height"
]
}
],
"declaration-no-important": true,
"at-rule-no-unknown": [
true,
{ "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen", "layer"] }
]
},
"ignoreFiles": [
"dist/**",
"node_modules/**",
"src/routeTree.gen.ts",
"**/*.gen.ts",
"**/*.gen.d.ts"
]
}

30
apps/kanban/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "@project/kanban",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev --port 7104",
"build": "vite build",
"preview": "vite preview --port 7104",
"tsc": "tsc --noEmit"
},
"dependencies": {
"@project/functions-sdk": "workspace:*",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-start": "^1.132.0",
"i18next": "^25.2.1",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-i18next": "^15.5.2"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4.5.2",
"typescript": "^5.9",
"vite": "^7.0.0"
}
}

View File

@@ -0,0 +1,90 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import en from './en.json'
// Typed tokens: every `t('key')` is checked against en.json's shape (i18next
// flattens it into dot-path keys), so a typo or removed key is a compile error.
// Other locale files are loaded dynamically and are not key-checked against en;
// a missing key falls back to the default locale at runtime.
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'translation'
resources: {
translation: typeof en
}
}
}
// Add a language: drop `i18n/<lang>.json` in this folder — Vite's
// import.meta.glob auto-registers it below into `resources`, and
// `supportedLocales` is derived from the files present, so no edits are needed
// here. Content is reachable via the `/<lang>` URL prefix (e.g. `/de`, `/es`);
// the default locale (`en`) needs no prefix.
const localeModules = import.meta.glob('./*.json', { eager: true }) as Record<
string,
{ default: Record<string, unknown> }
>
const resources: Record<string, { translation: Record<string, unknown> }> = {}
for (const path in localeModules) {
const code = path.slice(path.lastIndexOf('/') + 1).replace(/\.json$/, '')
resources[code] = { translation: localeModules[path].default }
}
export const supportedLocales = Object.keys(resources)
export type Locale = string
export const defaultLocale = 'en'
const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur'])
// Direction for a locale — RTL for Arabic/Hebrew/Farsi/Urdu, else LTR. Set this
// on <html dir> at the root so the browser mirrors the layout; see the
// pikku-rtl skill. Returns 'ltr' while the app is English-only.
export function localeDir(locale: string = defaultLocale): 'rtl' | 'ltr' {
return RTL_LOCALES.has(locale.split('-')[0]) ? 'rtl' : 'ltr'
}
export function detectLocale(pathname: string): Locale {
const segment = pathname.split('/')[1]
if (supportedLocales.includes(segment as Locale)) {
return segment as Locale
}
if (typeof navigator !== 'undefined') {
const browserLang = navigator.language?.split('-')[0]
if (supportedLocales.includes(browserLang as Locale)) {
return browserLang as Locale
}
}
return defaultLocale
}
// i18n debug mode: when enabled, every *translated* string is masked to block
// glyphs (█), so any readable text left on screen is text that never went
// through a token — a hardcoded/inlined string. Surfaces missing i18n at a
// glance. Toggle with `?i18n-debug` in the URL or `localStorage['i18n-debug']
// = '1'` (or `I18N_DEBUG=1` for server-rendered builds). Off by default.
export function isI18nDebug(): boolean {
if (typeof process !== 'undefined' && process.env?.I18N_DEBUG === '1') return true
if (typeof window === 'undefined') return false
const params = new URLSearchParams(window.location.search)
if (params.has('i18n-debug')) return params.get('i18n-debug') !== '0'
return window.localStorage?.getItem('i18n-debug') === '1'
}
const i18nDebugPostProcessor = {
type: 'postProcessor' as const,
name: 'i18nDebug',
process: (value: string) => (isI18nDebug() ? value.replace(/\S/g, '█') : value),
}
i18n
.use(initReactI18next)
.use(i18nDebugPostProcessor)
.init({
resources,
lng: typeof window !== 'undefined' ? detectLocale(window.location.pathname) : defaultLocale,
fallbackLng: defaultLocale,
interpolation: { escapeValue: false },
postProcess: ['i18nDebug'],
})
export default i18n

View File

@@ -0,0 +1,123 @@
{
"shell": {
"eyebrow": "Pikku Fabric template",
"title": "Kanban operations cockpit",
"subtitle": "A lightweight board for work intake, automation visibility, and deployable app surfaces. Built to grow into a real product, not stay a demo.",
"stats": {
"boardHealth": "Board health",
"cardsTracked": "Cards tracked",
"runtime": "Runtime"
},
"runtimeConnected": "connected",
"runtimeDegraded": "degraded",
"nav": {
"overview": "Overview",
"board": "Board",
"automations": "Automations"
}
},
"health": {
"idle": "idle",
"backlogged": "backlogged",
"flowing": "flowing"
},
"status": {
"todo": "Backlog",
"doing": "In progress",
"done": "Done"
},
"relativeTime": {
"seconds": "{{count}}s ago",
"minutes": "{{count}}m ago",
"hours": "{{count}}h ago",
"days": "{{count}}d ago"
},
"overview": {
"heading": "Overview",
"sub": "Snapshot of current delivery flow and template wiring.",
"loadFailed": "Failed to load cards: {{error}}",
"loading": "Loading current board state…",
"kpi": {
"boardHealth": "Board health",
"boardHealthDetail": "Live signal from current card distribution",
"backlog": "Backlog",
"backlogDetail": "Items waiting for pickup",
"inProgress": "In progress",
"inProgressDetail": "Cards currently moving",
"done": "Done",
"doneDetail": "Completed work ready to review",
"latest": "Latest card",
"latestNone": "None yet",
"latestDetail": "{{status}} · {{time}}",
"latestEmptyDetail": "Create the first card to get started"
},
"executionModel": {
"heading": "Execution model",
"sub": "What this template already includes out of the box.",
"bullets": [
"Board CRUD over Pikku RPC and HTTP wires.",
"Workflow hooks for card onboarding and notifications.",
"Queue processing for async card events.",
"MCP and CLI entrypoints for ops-driven control."
]
},
"nextScreens": {
"heading": "Suggested next screens",
"sub": "Natural expansion points once the app goes beyond the starter board.",
"bullets": [
"Card detail with history, notes, and workflow runs.",
"Release view for branch deploys and environment promotions.",
"Team inbox for triage, alerts, and assignment queues."
]
}
},
"board": {
"heading": "Board",
"sub": "Track work across the three canonical lanes.",
"loadFailed": "Failed to load cards: {{error}}",
"loading": "Loading board…",
"columnEmpty": "No cards yet.",
"createHeading": "Create card",
"createSub": "Use the generated mutation, not ad hoc fetches.",
"titleLabel": "Title",
"titlePlaceholder": "Audit workspace webhooks",
"columnLabel": "Column",
"creating": "Creating…",
"createCta": "Create card",
"createFailed": "Failed to create card: {{error}}"
},
"automations": {
"heading": "Automations",
"sub": "Operational surfaces already included in the template.",
"rails": {
"onboarding": {
"name": "Card onboarding workflow",
"detail": "Handles enrich → create → notify when new work enters the system."
},
"queue": {
"name": "Process card event queue",
"detail": "Queues side effects away from the request path for safer scaling."
},
"surface": {
"name": "Kanban MCP + CLI surface",
"detail": "Lets operators inspect or create board state from tools outside the UI."
}
},
"states": {
"active": "active",
"idle": "idle",
"warming": "warming",
"standby": "standby",
"ready": "ready"
},
"why": {
"heading": "Why routing now",
"sub": "TanStack Router gives this app room to grow past a demo board.",
"bullets": [
"Separate product surfaces without a one-file SPA.",
"Clear paths for card details, releases, and diagnostics.",
"Navigation semantics that match future expansion."
]
}
}
}

422
apps/kanban/src/index.css Normal file
View File

@@ -0,0 +1,422 @@
:root {
--bg: #081018;
--bg-accent: #101d2a;
--surface: rgba(17, 27, 39, 0.9);
--surface-strong: rgba(22, 35, 50, 0.96);
--surface-soft: rgba(255, 255, 255, 0.04);
--border: rgba(160, 196, 255, 0.16);
--border-strong: rgba(160, 196, 255, 0.28);
--text: #edf4ff;
--muted: #8ba0bb;
--primary: #82aaff;
--secondary: #5eead4;
--warning: #fbbf24;
--danger: #fb7185;
--shadow: 0 24px 60px rgba(0, 0, 0, 0.34);
font-family: 'IBM Plex Sans', 'Segoe UI', sans-serif;
color-scheme: dark;
}
* {
box-sizing: border-box;
}
html,
body,
#root {
min-height: 100%;
}
body {
margin: 0;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(94, 234, 212, 0.08), transparent 28%),
radial-gradient(circle at top right, rgba(130, 170, 255, 0.12), transparent 30%),
linear-gradient(180deg, var(--bg-accent) 0%, var(--bg) 52%, #060b11 100%);
}
a {
color: inherit;
text-decoration: none;
}
button,
input,
select,
textarea {
font: inherit;
}
.app-shell {
max-width: 1240px;
margin: 0 auto;
padding: 40px 28px 56px;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1.3fr) minmax(260px, 0.7fr);
gap: 24px;
align-items: end;
margin-bottom: 24px;
}
.eyebrow {
margin-bottom: 10px;
color: var(--secondary);
text-transform: uppercase;
letter-spacing: 0.18em;
font-size: 12px;
font-weight: 700;
}
.hero h1 {
margin: 0;
font-size: clamp(2.2rem, 5vw, 4.25rem);
line-height: 0.94;
letter-spacing: -0.05em;
}
.hero p {
max-width: 56ch;
margin: 14px 0 0;
color: var(--muted);
font-size: 16px;
line-height: 1.6;
}
.hero-status {
display: grid;
gap: 12px;
}
.hero-stat,
.panel,
.task-card,
.automation-card,
.kpi-card,
.column {
border: 1px solid var(--border);
background: var(--surface);
box-shadow: var(--shadow);
}
.hero-stat {
padding: 16px 18px;
border-radius: 18px;
backdrop-filter: blur(10px);
}
.hero-stat span,
.kpi-card span,
.task-card-meta,
.column-empty,
.automation-card p,
.section-heading p,
.bullet-list,
.field span,
.inline-error,
.empty-state {
color: var(--muted);
}
.hero-stat strong {
display: block;
margin-top: 4px;
font-size: 22px;
text-transform: capitalize;
}
.hero-stat strong[data-tone='flowing'] {
color: var(--secondary);
}
.hero-stat strong[data-tone='backlogged'] {
color: var(--warning);
}
.hero-stat strong[data-tone='idle'] {
color: var(--primary);
}
.top-nav {
display: flex;
gap: 10px;
margin-bottom: 28px;
padding: 10px;
border: 1px solid var(--border);
border-radius: 16px;
background: rgba(9, 15, 24, 0.72);
backdrop-filter: blur(12px);
}
.nav-link {
padding: 10px 14px;
border-radius: 12px;
color: var(--muted);
transition:
background-color 150ms ease,
color 150ms ease;
}
.nav-link:hover,
.nav-link.is-active {
color: var(--text);
background: rgba(130, 170, 255, 0.12);
}
.page-content {
min-height: 50vh;
}
.page-grid {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr);
gap: 20px;
}
.panel {
padding: 22px;
border-radius: 22px;
backdrop-filter: blur(12px);
}
.panel.wide {
grid-column: span 2;
}
.section-heading {
margin-bottom: 18px;
}
.section-heading h2 {
margin: 0;
font-size: 24px;
letter-spacing: -0.03em;
}
.section-heading p {
margin: 6px 0 0;
line-height: 1.5;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
}
.kpi-card {
padding: 16px;
border-radius: 16px;
background: var(--surface-strong);
}
.kpi-card strong {
display: block;
margin-top: 10px;
font-size: 22px;
line-height: 1.2;
}
.kpi-card small {
display: block;
margin-top: 8px;
color: var(--muted);
line-height: 1.45;
}
.bullet-list {
margin: 0;
padding-left: 18px;
line-height: 1.7;
}
.board-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.column {
padding: 14px;
border-radius: 18px;
background: var(--surface-strong);
}
.column-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.column-header h3 {
margin: 0;
font-size: 15px;
}
.column-header span {
min-width: 28px;
padding: 4px 8px;
border-radius: 999px;
background: rgba(130, 170, 255, 0.12);
text-align: center;
font-size: 12px;
color: var(--primary);
}
.column-cards {
display: grid;
gap: 10px;
}
.task-card {
padding: 14px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.025);
}
.task-card-title {
font-weight: 600;
line-height: 1.35;
}
.task-card-meta {
display: flex;
justify-content: space-between;
gap: 12px;
margin-top: 10px;
font-size: 12px;
}
.column-empty,
.empty-state {
padding: 18px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.025);
line-height: 1.5;
}
.error-state,
.inline-error {
color: var(--danger);
}
.card-form {
display: grid;
gap: 16px;
}
.field {
display: grid;
gap: 8px;
}
.field span {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.12em;
font-weight: 700;
}
.field input,
.field select {
width: 100%;
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: 14px;
background: rgba(255, 255, 255, 0.03);
color: var(--text);
outline: none;
}
.field input:focus,
.field select:focus {
border-color: var(--border-strong);
}
.primary-button {
padding: 13px 16px;
border: 0;
border-radius: 14px;
background: linear-gradient(135deg, var(--primary), #6ee7f9);
color: #071018;
font-weight: 800;
cursor: pointer;
}
.primary-button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.automation-list {
display: grid;
gap: 14px;
}
.automation-card {
padding: 16px 18px;
border-radius: 16px;
background: var(--surface-strong);
}
.automation-topline {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
}
.automation-topline h3 {
margin: 0;
font-size: 16px;
}
.automation-topline span {
padding: 5px 10px;
border-radius: 999px;
background: rgba(130, 170, 255, 0.12);
color: var(--primary);
text-transform: capitalize;
font-size: 12px;
font-weight: 700;
}
.automation-topline span[data-state='active'] {
background: rgba(94, 234, 212, 0.12);
color: var(--secondary);
}
.automation-topline span[data-state='warming'] {
background: rgba(251, 191, 36, 0.14);
color: var(--warning);
}
@media (max-width: 980px) {
.hero,
.page-grid,
.kpi-grid {
grid-template-columns: 1fr;
}
.panel.wide {
grid-column: auto;
}
.board-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.app-shell {
padding: 24px 16px 40px;
}
.top-nav {
overflow-x: auto;
}
}

View File

@@ -0,0 +1,46 @@
import { PikkuRPC } from '@project/functions-sdk/pikku/pikku-rpc.gen'
import { apiUrl } from './env'
// The Pikku-RPC shim. `rpc.invoke` is typed by the generated FlattenedRPCMap, so
// `makeApi().invoke('listCards', input)` checks the name and payload at compile
// time. Pikku emits unversioned latest-aliases (`listCards` → `listCards@v2`),
// so you call clean names. Used from TanStack Start loaders (SSR + client) and
// from event handlers for mutations.
export function makeApi(): PikkuRPC {
const rpc = new PikkuRPC()
rpc.setServerUrl(apiUrl())
return rpc
}
export async function formatApiError(error: unknown): Promise<string> {
if (error instanceof Response) {
const status = [error.status, error.statusText].filter(Boolean).join(' ')
try {
const payload = (await error.clone().json()) as {
message?: unknown
errorId?: unknown
}
if (typeof payload?.message === 'string') {
const detail =
typeof payload.errorId === 'string'
? `${payload.message} (${payload.errorId})`
: payload.message
return status ? `${status}: ${detail}` : detail
}
} catch {
// Fall through to plain text/body-less formatting.
}
try {
const text = (await error.clone().text()).trim()
if (text) return status ? `${status}: ${text}` : text
} catch {
// Ignore body-read failures and fall through.
}
return status || 'Request failed'
}
return error instanceof Error ? error.message : String(error)
}

View File

@@ -0,0 +1,17 @@
// Endpoints come from env, never hardcoded. Throws on a missing var rather than
// falling back to a localhost default, so a misconfigured deploy fails loudly.
//
// SSR (workerd inside the sandbox container) can't reach the external host port
// that VITE_API_URL points to. VITE_SERVER_API_URL holds the internal Caddy
// address (http://127.0.0.1:8080/api) and is set by generate-frontend-runtime.mjs
// for local sandbox mode. In deployed/hosted contexts VITE_SERVER_API_URL is
// unset and we fall back to VITE_API_URL, which is correct there.
export function apiUrl(): string {
const url = import.meta.env.SSR
? (import.meta.env.VITE_SERVER_API_URL ?? import.meta.env.VITE_API_URL)
: import.meta.env.VITE_API_URL
if (!url) {
throw new Error('VITE_API_URL is not set — point it at the Pikku API base URL')
}
return url
}

View File

@@ -0,0 +1,49 @@
import i18n from '../i18n/config'
export type CardStatus = 'todo' | 'doing' | 'done'
export interface Card {
cardId: string
title: string
status: CardStatus | string
position: number
createdAt: string
}
export const STATUSES: CardStatus[] = ['todo', 'doing', 'done']
export function sortCards(cards: Card[]) {
return [...cards].sort((a, b) => {
if (a.status !== b.status) return a.status.localeCompare(b.status)
return a.position - b.position
})
}
export function classifyHealth(cards: Card[]) {
const todo = cards.filter((card) => card.status === 'todo').length
const doing = cards.filter((card) => card.status === 'doing').length
if (doing === 0) return 'idle'
if (todo > doing * 2) return 'backlogged'
return 'flowing'
}
export function prettyStatus(status: string) {
switch (status) {
case 'todo':
return i18n.t('status.todo')
case 'doing':
return i18n.t('status.doing')
case 'done':
return i18n.t('status.done')
default:
return status
}
}
export function relativeTime(timestamp: string) {
const delta = Math.max(0, Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000))
if (delta < 60) return i18n.t('relativeTime.seconds', { count: delta })
if (delta < 3600) return i18n.t('relativeTime.minutes', { count: Math.floor(delta / 60) })
if (delta < 86400) return i18n.t('relativeTime.hours', { count: Math.floor(delta / 3600) })
return i18n.t('relativeTime.days', { count: Math.floor(delta / 86400) })
}

View File

@@ -0,0 +1,104 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as BoardRouteImport } from './routes/board'
import { Route as AutomationsRouteImport } from './routes/automations'
import { Route as IndexRouteImport } from './routes/index'
const BoardRoute = BoardRouteImport.update({
id: '/board',
path: '/board',
getParentRoute: () => rootRouteImport,
} as any)
const AutomationsRoute = AutomationsRouteImport.update({
id: '/automations',
path: '/automations',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/automations': typeof AutomationsRoute
'/board': typeof BoardRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/automations': typeof AutomationsRoute
'/board': typeof BoardRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/automations': typeof AutomationsRoute
'/board': typeof BoardRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/automations' | '/board'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/automations' | '/board'
id: '__root__' | '/' | '/automations' | '/board'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AutomationsRoute: typeof AutomationsRoute
BoardRoute: typeof BoardRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/board': {
id: '/board'
path: '/board'
fullPath: '/board'
preLoaderRoute: typeof BoardRouteImport
parentRoute: typeof rootRouteImport
}
'/automations': {
id: '/automations'
path: '/automations'
fullPath: '/automations'
preLoaderRoute: typeof AutomationsRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AutomationsRoute: AutomationsRoute,
BoardRoute: BoardRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}

View File

@@ -0,0 +1,20 @@
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
// TanStack Start discovers this `getRouter` factory to build a router per
// request (server) and once on the client (hydration). routeTree.gen.ts is
// generated by the tanstackStart() Vite plugin from src/routes/** — don't
// hand-edit it.
export function getRouter() {
return createRouter({
routeTree,
scrollRestoration: true,
defaultPreload: 'intent',
})
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>
}
}

View File

@@ -0,0 +1,111 @@
import { createRootRoute, HeadContent, Link, Outlet, Scripts } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { defaultLocale, localeDir } from '../i18n/config'
import { classifyHealth, sortCards, type Card } from '../lib/kanban'
import { formatApiError, makeApi } from '../lib/api'
import '../i18n/config'
import '../index.css'
export interface BoardData {
cards: Card[]
error: string | null
}
// Root route renders the full HTML document for SSR (owns <html lang dir> so the
// tree mirrors for RTL locales) and the shared board chrome. Its loader fetches
// the cards once on the server; child routes read this same data via
// RootRoute.useLoaderData(), and mutations call router.invalidate() to re-run it.
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'Kanban Board' },
],
}),
loader: async (): Promise<BoardData> => {
try {
const { cards } = await makeApi().invoke('listCards', {})
return { cards: sortCards(cards as Card[]), error: null }
} catch (thrown: unknown) {
let message: string
try {
message = await formatApiError(thrown)
} catch {
message = 'Request failed'
}
return { cards: [], error: message }
}
},
component: RootDocument,
})
function RootDocument() {
const locale = defaultLocale
const { t } = useTranslation()
const { cards, error } = Route.useLoaderData()
const health = classifyHealth(cards)
return (
<html lang={locale} dir={localeDir(locale)}>
<head>
<HeadContent />
</head>
<body>
<div className="app-shell">
<header className="hero">
<div className="hero-copy">
<div className="eyebrow">{t('shell.eyebrow')}</div>
<h1>{t('shell.title')}</h1>
<p>{t('shell.subtitle')}</p>
</div>
<div className="hero-status">
<div className="hero-stat">
<span>{t('shell.stats.boardHealth')}</span>
<strong data-tone={health}>{t(`health.${health}`)}</strong>
</div>
<div className="hero-stat">
<span>{t('shell.stats.cardsTracked')}</span>
<strong>{cards.length}</strong>
</div>
<div className="hero-stat">
<span>{t('shell.stats.runtime')}</span>
<strong>{error ? t('shell.runtimeDegraded') : t('shell.runtimeConnected')}</strong>
</div>
</div>
</header>
<nav className="top-nav">
<Link
to="/"
className="nav-link"
activeOptions={{ exact: true }}
activeProps={{ className: 'nav-link is-active' }}
>
{t('shell.nav.overview')}
</Link>
<Link
to="/board"
className="nav-link"
activeProps={{ className: 'nav-link is-active' }}
>
{t('shell.nav.board')}
</Link>
<Link
to="/automations"
className="nav-link"
activeProps={{ className: 'nav-link is-active' }}
>
{t('shell.nav.automations')}
</Link>
</nav>
<main className="page-content">
<Outlet />
</main>
</div>
<Scripts />
</body>
</html>
)
}

View File

@@ -0,0 +1,63 @@
import { createFileRoute } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Route as RootRoute } from './__root'
export const Route = createFileRoute('/automations')({
component: AutomationsPage,
})
function AutomationsPage() {
const { t } = useTranslation()
const { cards } = RootRoute.useLoaderData()
const total = cards.length
const active = cards.filter((card) => card.status === 'doing').length
const rails = [
{
key: 'onboarding',
state: total > 0 ? 'active' : 'idle',
},
{
key: 'queue',
state: active > 0 ? 'warming' : 'standby',
},
{
key: 'surface',
state: 'ready',
},
] as const
return (
<section className="page-grid">
<div className="panel wide">
<div className="section-heading">
<h2>{t('automations.heading')}</h2>
<p>{t('automations.sub')}</p>
</div>
<div className="automation-list">
{rails.map((rail) => (
<article key={rail.key} className="automation-card">
<div className="automation-topline">
<h3>{t(`automations.rails.${rail.key}.name`)}</h3>
<span data-state={rail.state}>{t(`automations.states.${rail.state}`)}</span>
</div>
<p>{t(`automations.rails.${rail.key}.detail`)}</p>
</article>
))}
</div>
</div>
<div className="panel">
<div className="section-heading">
<h2>{t('automations.why.heading')}</h2>
<p>{t('automations.why.sub')}</p>
</div>
<ul className="bullet-list">
{(t('automations.why.bullets', { returnObjects: true }) as string[]).map((bullet) => (
<li key={bullet}>{bullet}</li>
))}
</ul>
</div>
</section>
)
}

View File

@@ -0,0 +1,121 @@
import { useState } from 'react'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Route as RootRoute } from './__root'
import { formatApiError, makeApi } from '../lib/api'
import { type CardStatus, STATUSES, prettyStatus, relativeTime } from '../lib/kanban'
export const Route = createFileRoute('/board')({
component: BoardPage,
})
function BoardPage() {
const { t } = useTranslation()
const router = useRouter()
const { cards, error } = RootRoute.useLoaderData()
const [title, setTitle] = useState('')
const [status, setStatus] = useState<CardStatus>('todo')
const [pending, setPending] = useState(false)
const [createError, setCreateError] = useState<string | null>(null)
const columns = STATUSES.map((columnStatus) => ({
key: columnStatus,
title: prettyStatus(columnStatus),
cards: cards.filter((card) => card.status === columnStatus),
}))
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault()
if (!title.trim()) return
setPending(true)
setCreateError(null)
try {
await makeApi().invoke('createCard', { title: title.trim(), status })
setTitle('')
await router.invalidate()
} catch (err) {
setCreateError(await formatApiError(err))
} finally {
setPending(false)
}
}
return (
<section className="page-grid">
<div className="panel wide">
<div className="section-heading">
<h2>{t('board.heading')}</h2>
<p>{t('board.sub')}</p>
</div>
{error ? (
<div className="empty-state error-state">{t('board.loadFailed', { error })}</div>
) : (
<div className="board-grid">
{columns.map((column) => (
<div key={column.key} className="column">
<div className="column-header">
<h3>{column.title}</h3>
<span>{column.cards.length}</span>
</div>
<div className="column-cards">
{column.cards.length === 0 ? (
<div className="column-empty">{t('board.columnEmpty')}</div>
) : (
column.cards.map((card) => (
<article key={card.cardId} className="task-card">
<div className="task-card-title">{card.title}</div>
<div className="task-card-meta">
<span>#{card.position}</span>
<span>{relativeTime(card.createdAt)}</span>
</div>
</article>
))
)}
</div>
</div>
))}
</div>
)}
</div>
<div className="panel">
<div className="section-heading">
<h2>{t('board.createHeading')}</h2>
<p>{t('board.createSub')}</p>
</div>
<form className="card-form" onSubmit={handleSubmit}>
<label className="field">
<span>{t('board.titleLabel')}</span>
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder={t('board.titlePlaceholder')}
/>
</label>
<label className="field">
<span>{t('board.columnLabel')}</span>
<select
value={status}
onChange={(event) => setStatus(event.target.value as CardStatus)}
>
{STATUSES.map((value) => (
<option key={value} value={value}>
{prettyStatus(value)}
</option>
))}
</select>
</label>
<button type="submit" className="primary-button" disabled={pending || !title.trim()}>
{pending ? t('board.creating') : t('board.createCta')}
</button>
{createError && (
<div className="inline-error">{t('board.createFailed', { error: createError })}</div>
)}
</form>
</div>
</section>
)
}

View File

@@ -0,0 +1,105 @@
import { createFileRoute } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Route as RootRoute } from './__root'
import { classifyHealth, prettyStatus, relativeTime } from '../lib/kanban'
export const Route = createFileRoute('/')({
component: OverviewPage,
})
function KpiCard(props: { label: string; value: string; detail: string }) {
return (
<article className="kpi-card">
<span>{props.label}</span>
<strong>{props.value}</strong>
<small>{props.detail}</small>
</article>
)
}
function OverviewPage() {
const { t } = useTranslation()
const { cards, error } = RootRoute.useLoaderData()
const todo = cards.filter((card) => card.status === 'todo').length
const doing = cards.filter((card) => card.status === 'doing').length
const done = cards.filter((card) => card.status === 'done').length
const newest = cards.at(-1) ?? null
const health = classifyHealth(cards)
return (
<section className="page-grid">
<div className="panel wide">
<div className="section-heading">
<h2>{t('overview.heading')}</h2>
<p>{t('overview.sub')}</p>
</div>
{error ? (
<div className="empty-state error-state">{t('overview.loadFailed', { error })}</div>
) : (
<div className="kpi-grid">
<KpiCard
label={t('overview.kpi.boardHealth')}
value={t(`health.${health}`)}
detail={t('overview.kpi.boardHealthDetail')}
/>
<KpiCard
label={t('overview.kpi.backlog')}
value={String(todo)}
detail={t('overview.kpi.backlogDetail')}
/>
<KpiCard
label={t('overview.kpi.inProgress')}
value={String(doing)}
detail={t('overview.kpi.inProgressDetail')}
/>
<KpiCard
label={t('overview.kpi.done')}
value={String(done)}
detail={t('overview.kpi.doneDetail')}
/>
<KpiCard
label={t('overview.kpi.latest')}
value={newest ? newest.title : t('overview.kpi.latestNone')}
detail={
newest
? t('overview.kpi.latestDetail', {
status: prettyStatus(newest.status),
time: relativeTime(newest.createdAt),
})
: t('overview.kpi.latestEmptyDetail')
}
/>
</div>
)}
</div>
<div className="panel">
<div className="section-heading">
<h2>{t('overview.executionModel.heading')}</h2>
<p>{t('overview.executionModel.sub')}</p>
</div>
<ul className="bullet-list">
{(t('overview.executionModel.bullets', { returnObjects: true }) as string[]).map(
(bullet) => (
<li key={bullet}>{bullet}</li>
),
)}
</ul>
</div>
<div className="panel">
<div className="section-heading">
<h2>{t('overview.nextScreens.heading')}</h2>
<p>{t('overview.nextScreens.sub')}</p>
</div>
<ul className="bullet-list">
{(t('overview.nextScreens.bullets', { returnObjects: true }) as string[]).map(
(bullet) => (
<li key={bullet}>{bullet}</li>
),
)}
</ul>
</div>
</section>
)
}

22
apps/kanban/tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"allowImportingTsExtensions": false,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"types": ["vite/client", "node"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src", "vite.config.ts", "wrangler.jsonc"]
}

View File

@@ -0,0 +1,22 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
// Plain TanStack Start config (Node target) — used for local sandbox dev. This
// template is deploy-provider agnostic: it ships NO @cloudflare/vite-plugin. At
// deploy, fabric CI injects the CF adapter (vite.config.cf.ts merges cloudflare()
// on top of this config) to emit the CF Workers SSR bundle. Do not add cloudflare
// here — that would double-apply it and couple the template to a provider.
export default defineConfig({
plugins: [tanstackStart(), react()],
server: {
// Bind to 0.0.0.0 so the IPv4 loopback is reachable from Caddy's
// reverse_proxy. Without this, Vite binds to ::1 (IPv6 localhost) in the
// container because /etc/hosts lists ::1 before 127.0.0.1 in Node's
// resolver order, causing Caddy's 127.0.0.1:port probe to be refused.
host: '0.0.0.0',
// Allow requests from any hostname (including nip.io reverse-proxy URLs
// used in sandbox preview iframes).
allowedHosts: true,
},
})

View File

@@ -0,0 +1,9 @@
{
// Consumed by @cloudflare/vite-plugin. `main` is TanStack Start's built-in
// server entry (no hand-written worker file); the plugin wires client assets
// and the ssr environment automatically.
"name": "kanban",
"main": "@tanstack/react-start/server-entry",
"compatibility_date": "2024-12-01",
"compatibility_flags": ["nodejs_compat"],
}

6
db/annotations.ts Normal file
View File

@@ -0,0 +1,6 @@
import type { DbClassificationMap } from '../.pikku/db/classification-map.gen.d.ts'
export const classifications = {
"kanban_card": {
},
} satisfies DbClassificationMap

8
db/sqlite-seed.sql Normal file
View File

@@ -0,0 +1,8 @@
-- Demo seed data. Runs after migrations in local dev only (never in production).
-- All statements must be idempotent (INSERT OR IGNORE / ON CONFLICT DO NOTHING).
INSERT INTO kanban_card (card_id, title, description, status, priority, position)
VALUES
('card-seed-1', 'Wire up local sandbox', 'Boot the project and verify the preview stack.', 'doing', 'high', 1),
('card-seed-2', 'Polish board interactions', 'Tighten drag/drop and empty-state behavior.', 'todo', 'medium', 1)
ON CONFLICT DO NOTHING;

13
db/sqlite/0001-init.sql Normal file
View File

@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS kanban_card (
card_id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'todo',
priority TEXT NOT NULL DEFAULT 'medium',
position INTEGER NOT NULL DEFAULT 0,
due_at TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS kanban_card_status_idx ON kanban_card(status);

View File

@@ -0,0 +1,16 @@
-- Generated by `pikku db generate` from pikkuBetterAuth (Better Auth).
-- Re-run the command after changing the auth config.
create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "email_verified" integer not null, "image" text, "created_at" date not null, "updated_at" date not null);
create table "session" ("id" text not null primary key, "expires_at" date not null, "token" text not null unique, "created_at" date not null, "updated_at" date not null, "ip_address" text, "user_agent" text, "user_id" text not null references "user" ("id") on delete cascade);
create table "account" ("id" text not null primary key, "account_id" text not null, "provider_id" text not null, "user_id" text not null references "user" ("id") on delete cascade, "access_token" text, "refresh_token" text, "id_token" text, "access_token_expires_at" date, "refresh_token_expires_at" date, "scope" text, "password" text, "created_at" date not null, "updated_at" date not null);
create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expires_at" date not null, "created_at" date not null, "updated_at" date not null);
create index "session_user_id_idx" on "session" ("user_id");
create index "account_user_id_idx" on "account" ("user_id");
create index "verification_identifier_idx" on "verification" ("identifier");

View File

@@ -0,0 +1,86 @@
// A scratchpad of the "non-happy" states for a list or page — loading, empty,
// error, and skeleton. Each option is a real @mantine/core composition so the one
// you pick drops straight into your app as a component variation. Open the Design
// tab → Scratchpad to compare them on the canvas.
import { Alert, Button, Center, Group, Loader, Paper, Skeleton, Stack, Text, ThemeIcon } from '@mantine/core'
export const title = 'Empty & loading states'
export const objects = [
{
name: 'Loading',
mantine: ['Loader', 'Stack'],
render: () => (
<Stack align="center" gap="xs" maw={200} py="sm">
<Loader size="md" color="grape" />
<Text size="sm" c="dimmed">
Loading your items
</Text>
</Stack>
),
},
{
name: 'Empty',
mantine: ['ThemeIcon', 'Stack', 'Button'],
render: () => (
<Stack align="center" gap="xs" maw={224} py="sm">
<ThemeIcon size={46} radius="xl" variant="light" color="grape">
<Text size="xl"></Text>
</ThemeIcon>
<Text size="sm" fw={600}>
No items yet
</Text>
<Text size="xs" c="dimmed" ta="center">
Create your first item to get started.
</Text>
<Button size="xs" variant="light" mt={4}>
New item
</Button>
</Stack>
),
},
{
name: 'Error',
mantine: ['Alert', 'Button'],
render: () => (
<Alert color="red" radius="md" title="Couldn't load" maw={236}>
<Stack gap="xs">
<Text size="xs">Something went wrong fetching your items.</Text>
<Button size="xs" variant="white" color="red" w="fit-content">
Try again
</Button>
</Stack>
</Alert>
),
},
{
name: 'Skeleton list',
mantine: ['Skeleton', 'Group'],
render: () => (
<Stack gap="sm" maw={236}>
{[0, 1, 2].map((i) => (
<Group key={i} wrap="nowrap" gap="sm">
<Skeleton height={32} circle />
<Stack gap={6} style={{ flex: 1 }}>
<Skeleton height={8} radius="xl" />
<Skeleton height={8} width="60%" radius="xl" />
</Stack>
</Group>
))}
</Stack>
),
},
{
name: 'Inline empty',
mantine: ['Paper', 'Center'],
render: () => (
<Paper radius="md" p="lg" maw={236} style={{ borderStyle: 'dashed' }} withBorder>
<Center>
<Text size="sm" c="dimmed">
Nothing here yet drop something in.
</Text>
</Center>
</Paper>
),
},
]

View File

@@ -0,0 +1,154 @@
// A richer scratchpad: several ways to display the SAME entity (a team member)
// in a list or grid. Every option is a real @mantine/core composition — picking
// one gives you an adoptable component variation, not a pile of CSS. Open the
// Design tab → Scratchpad to compare them on the canvas.
import {
Anchor,
Avatar,
Badge,
Button,
Card,
Divider,
Group,
Paper,
Progress,
Stack,
Text,
} from '@mantine/core'
export const title = 'Member card'
export const objects = [
{
name: 'List row',
mantine: ['Group', 'Avatar', 'Badge'],
render: () => (
<Group wrap="nowrap" maw={264} gap="sm">
<Avatar radius="xl" color="grape">
AL
</Avatar>
<div style={{ flex: 1, minWidth: 0 }}>
<Text size="sm" fw={600} truncate>
Ada Lovelace
</Text>
<Text size="xs" c="dimmed" truncate>
ada@example.com
</Text>
</div>
<Badge color="green" variant="light" radius="sm">
Active
</Badge>
</Group>
),
},
{
name: 'Detail card',
mantine: ['Card', 'Avatar', 'Badge', 'Divider'],
render: () => (
<Card withBorder radius="md" padding="md" maw={248}>
<Group justify="space-between" wrap="nowrap">
<Group gap="sm" wrap="nowrap">
<Avatar radius="xl" color="grape">
AL
</Avatar>
<div style={{ minWidth: 0 }}>
<Text size="sm" fw={600} truncate>
Ada Lovelace
</Text>
<Text size="xs" c="dimmed">
Engineering
</Text>
</div>
</Group>
<Badge color="green" variant="dot">
Online
</Badge>
</Group>
<Divider my="sm" />
<Group justify="space-between">
<Stack gap={0}>
<Text size="xs" c="dimmed">
Commits
</Text>
<Text size="sm" fw={600}>
1,204
</Text>
</Stack>
<Stack gap={0}>
<Text size="xs" c="dimmed">
Reviews
</Text>
<Text size="sm" fw={600}>
318
</Text>
</Stack>
<Anchor size="xs" fw={500}>
View
</Anchor>
</Group>
</Card>
),
},
{
name: 'Grid tile',
mantine: ['Card', 'Avatar', 'Button'],
render: () => (
<Card withBorder radius="md" padding="lg" maw={184}>
<Stack align="center" gap="xs">
<Avatar size="lg" radius="xl" color="grape">
AL
</Avatar>
<Text size="sm" fw={600}>
Ada Lovelace
</Text>
<Badge color="grape" variant="light" radius="sm">
Maintainer
</Badge>
<Button size="xs" variant="light" fullWidth mt={4}>
View profile
</Button>
</Stack>
</Card>
),
},
{
name: 'Compact chip',
mantine: ['Badge', 'Avatar'],
render: () => (
<Badge
size="lg"
radius="xl"
variant="light"
color="grape"
pl={3}
leftSection={
<Avatar size={18} radius="xl" color="grape">
AL
</Avatar>
}
>
Ada Lovelace
</Badge>
),
},
{
name: 'Stat card',
mantine: ['Paper', 'Progress', 'Badge'],
render: () => (
<Paper withBorder radius="md" p="md" maw={224}>
<Group justify="space-between" mb={6}>
<Text size="xs" c="dimmed" fw={600} tt="uppercase">
Contribution
</Text>
<Badge color="teal" variant="light" size="sm">
+12%
</Badge>
</Group>
<Text size="xl" fw={700} lh={1}>
84%
</Text>
<Progress value={84} color="grape" radius="xl" mt="sm" />
</Paper>
),
},
]

View File

@@ -0,0 +1,95 @@
// A scratchpad: several real, themed ways to show one thing — here, a deploy's
// status. Each option is built from @mantine/core, so it carries your theme. The
// design agent writes files like this; open the Design tab → Scratchpad to compare
// them on the canvas and pick the one you like.
import {
Alert,
Badge,
Group,
Indicator,
Paper,
Progress,
RingProgress,
Stack,
Text,
} from '@mantine/core'
export const title = 'Status display'
export const objects = [
{
name: 'Status pill',
mantine: ['Badge'],
render: () => (
<Badge color="green" variant="light" size="lg" radius="sm">
Live
</Badge>
),
},
{
name: 'Progress card',
mantine: ['Paper', 'Progress'],
render: () => (
<Paper withBorder p="md" radius="md" maw={220}>
<Stack gap={8}>
<Group justify="space-between">
<Text size="sm" fw={600}>
Deploying
</Text>
<Text size="xs" c="dimmed">
72%
</Text>
</Group>
<Progress value={72} color="blue" radius="xl" />
</Stack>
</Paper>
),
},
{
name: 'Solid badge',
mantine: ['Badge'],
render: () => (
<Badge color="green" variant="filled" size="lg" radius="sm">
Deployed
</Badge>
),
},
{
name: 'Progress ring',
mantine: ['RingProgress'],
render: () => (
<RingProgress
size={92}
thickness={9}
roundCaps
sections={[{ value: 72, color: 'blue' }]}
label={
<Text ta="center" size="sm" fw={700}>
72%
</Text>
}
/>
),
},
{
name: 'Inline dot',
mantine: ['Indicator'],
render: () => (
<Group gap={10}>
<Indicator color="green" size={9} processing />
<Text size="sm" fw={500}>
Running
</Text>
</Group>
),
},
{
name: 'Tinted banner',
mantine: ['Alert'],
render: () => (
<Alert color="green" radius="md" title="Deployment succeeded" maw={240}>
Your changes are live in production.
</Alert>
),
},
]

122
e2e/README.md Normal file
View File

@@ -0,0 +1,122 @@
# E2E Test Harness
Cucumber + Playwright end-to-end tests for the project. The intent is that
**every locked workflow has a `.feature` file before it gets a line of
implementation code** (see "Phase 7.5 — Acceptance scenarios" in the
discovery playbook).
## Layout
```
e2e/
package.json — cucumber, @playwright/test, tsx
cucumber.mjs — runner config
tests/
features/*.feature — Gherkin scenarios (one feature per locked workflow)
steps/*.steps.ts — step definitions; common.steps.ts is generic
support/
world.ts — AppWorld (browser, page, login, gotoApp, expectText)
hooks.ts — spawns servers (optional), DB reset hook, browser lifecycle
types.ts — config (URLs, timeouts) read from env
reports/ — generated HTML reports
```
## Running
The harness assumes the app is reachable. Either:
**A. Start servers yourself** (recommended during dev):
```sh
yarn dev # in repo root — starts frontend + backend
yarn workspace @project/e2e test
```
**B. Let the harness manage the servers**:
```sh
E2E_MANAGE_SERVERS=1 \
E2E_BACKEND_CMD="yarn dev:backend" \
E2E_FRONTEND_CMD="yarn dev" \
yarn workspace @project/e2e test
```
## Configuration
All env vars optional; defaults match the kanban-board-template's ports.
| Var | Default | Purpose |
|----------------------|----------------------------|-----------------------------------------------|
| `APP_URL` | `http://localhost:7104` | Vite frontend URL |
| `API_URL` | `http://localhost:4003` | Pikku backend URL |
| `E2E_TIMEOUT` | `30000` | Per-step Playwright timeout (ms) |
| `E2E_MANAGE_SERVERS` | unset | Set to `1` to have hooks spawn dev servers |
| `E2E_BACKEND_CMD` | `yarn dev:backend` | Used when `E2E_MANAGE_SERVERS=1` |
| `E2E_FRONTEND_CMD` | `yarn dev` | Used when `E2E_MANAGE_SERVERS=1` |
| `E2E_RESET_URL` | unset | POST URL that resets DB to seed between tests |
| `HEADED` | unset | Set to `1` to run with a visible browser |
## DB reset between scenarios
Scenarios pollute each other. To get a deterministic DB state per scenario,
implement a `__test_reset` Pikku function gated behind a test-only flag, and
point `E2E_RESET_URL` at it. The harness will POST to it in the `Before`
hook. If the URL is unset or unreachable the harness logs a warning and
continues.
Example shape:
```ts
export const testReset = pikkuSessionlessFunc({
expose: true,
description: 'Reset DB to seed state. Test-only — gated by NODE_ENV.',
func: async ({ kysely, config }) => {
if (config.env !== 'test') throw new Error('forbidden')
await kysely.deleteFrom('booking').execute()
// ... drop + reseed all mutable tables
return { ok: true as const }
},
})
```
## Writing a feature
```gherkin
Feature: <one locked workflow>
Scenario: <one happy path>
Given I am logged in as "demo@yoga-retreat.example" with password "demo1234"
When I visit "/bookings"
Then I see "Summer Yoga Retreat"
```
Generic steps live in `tests/steps/common.steps.ts`:
- `Given I visit "<path>"`
- `Given I am logged in as "<email>" with password "<password>"`
- `When I log in as "..." with password "..."`
- `When I log out`
- `When I fill "<selector>" with "<value>"`
- `When I click "<button label>"`
- `Then I see "<text>"`
- `Then I do not see "<text>"`
- `Then the URL contains "<fragment>"`
Project-specific steps go in their own `*.steps.ts` files (e.g.
`bookings.steps.ts`). Keep `common.steps.ts` framework-agnostic so the next
project that copies this harness inherits a clean baseline.
## Tags
Run a subset by tag:
```sh
yarn workspace @project/e2e test:tag '@auth'
yarn workspace @project/e2e test:tag 'not @slow'
```
Mark scenarios as `@skip` to exclude from the default run.
## Reports
After a run, open `tests/reports/cucumber-report.html`.

20
e2e/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "@project/e2e",
"version": "0.0.1",
"private": true,
"type": "module",
"description": "End-to-end test harness — Cucumber + Playwright.",
"scripts": {
"test": "cucumber-js --config tests/cucumber.mjs --tags 'not @skip'",
"test:headed": "HEADED=1 cucumber-js --config tests/cucumber.mjs --tags 'not @skip'",
"test:tag": "cucumber-js --config tests/cucumber.mjs --tags",
"tsc": "tsc --noEmit"
},
"devDependencies": {
"@cucumber/cucumber": "^11.0.0",
"@playwright/test": "^1.50.0",
"@types/node": "^22",
"tsx": "^4.21.0",
"typescript": "~5.8.0"
}
}

7
e2e/tests/cucumber.mjs Normal file
View File

@@ -0,0 +1,7 @@
export default {
requireModule: ['tsx'],
require: ['tests/support/**/*.ts', 'tests/steps/**/*.ts'],
paths: ['tests/features/**/*.feature'],
format: ['progress', 'html:tests/reports/cucumber-report.html'],
forceExit: true,
}

View File

View File

@@ -0,0 +1,23 @@
@content
Feature: Card file attachments
Files attached to cards are uploaded via a presigned PUT URL and accessed via
a time-limited signed download URL. Access without a valid signature must be
rejected by the storage backend.
Scenario: Upload and download a card attachment via signed URLs
Given a card exists
When I request an upload URL for filename "hello.txt" and content type "text/plain"
And I PUT "Hello, Pikku!" to the upload URL
Then the upload response status is 200 or 201
When I request a signed download URL for filename "hello.txt"
Then the signed URL response contains a signedUrl
And downloading the signed URL returns "Hello, Pikku!"
Scenario: Unsigned access to an uploaded file is rejected
Given a card exists
When I request an upload URL for filename "secret.txt" and content type "text/plain"
And I PUT "Top secret" to the upload URL
Then the upload response status is 200 or 201
When I request a signed download URL for filename "secret.txt"
And I strip the signature from the signed URL
Then fetching the unsigned URL returns a 4xx status

View File

@@ -0,0 +1,7 @@
@smoke
Feature: App smoke
The harness can reach the running app.
Scenario: home page loads
When I visit "/"
Then the URL contains "/"

View File

@@ -0,0 +1,58 @@
import { Given, When, Then } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import type { AppWorld } from '../support/world.js'
/**
* Generic step library — shared across every project that copies this harness.
*
* Project-specific business steps (e.g. "I create a booking for <date>")
* belong in their own *.steps.ts files. Keep this file framework-agnostic.
*/
Given('I visit {string}', async function (this: AppWorld, path: string) {
await this.gotoApp(path)
})
Given(
'I am logged in as {string} with password {string}',
async function (this: AppWorld, email: string, password: string) {
await this.login(email, password)
},
)
When('I log in as {string} with password {string}', async function (
this: AppWorld,
email: string,
password: string,
) {
await this.login(email, password)
})
When('I log out', async function (this: AppWorld) {
await this.logout()
})
When(
'I fill {string} with {string}',
async function (this: AppWorld, selector: string, value: string) {
await this.page.locator(selector).fill(value)
},
)
When('I click {string}', async function (this: AppWorld, label: string) {
await this.page.getByRole('button', { name: label }).first().click()
})
Then('I see {string}', async function (this: AppWorld, text: string) {
await this.expectText(text)
})
Then('I do not see {string}', async function (this: AppWorld, text: string) {
await expect(
this.page.getByText(text, { exact: false }).first(),
).toBeHidden()
})
Then('the URL contains {string}', async function (this: AppWorld, fragment: string) {
await this.page.waitForURL((url) => url.toString().includes(fragment))
})

View File

@@ -0,0 +1,106 @@
import { Given, When, Then } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import type { AppWorld } from '../support/world.js'
import { config } from '../support/types.js'
interface ContentWorld extends AppWorld {
cardId: string
uploadUrl: string
signedUrl: string
uploadStatus: number
}
Given('a card exists', async function (this: ContentWorld) {
const res = await fetch(`${config.apiUrl}/cards`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ title: 'Attachment test card' }),
})
expect(res.ok, `POST /cards failed: ${res.status}`).toBe(true)
const body = (await res.json()) as { cardId: string }
this.cardId = body.cardId
})
When(
'I request an upload URL for filename {string} and content type {string}',
async function (this: ContentWorld, fileName: string, contentType: string) {
const res = await fetch(
`${config.apiUrl}/cards/${this.cardId}/attachments/upload-url`,
{
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ cardId: this.cardId, fileName, contentType }),
},
)
expect(res.ok, `upload-url request failed: ${res.status} ${await res.text()}`).toBe(true)
const body = (await res.json()) as { uploadUrl: string }
this.uploadUrl = body.uploadUrl
},
)
When(
'I PUT {string} to the upload URL',
async function (this: ContentWorld, content: string) {
const res = await fetch(this.uploadUrl, {
method: 'PUT',
body: content,
headers: { 'content-type': 'text/plain' },
})
this.uploadStatus = res.status
},
)
Then(
'the upload response status is 200 or 201',
function (this: ContentWorld) {
expect(
this.uploadStatus === 200 || this.uploadStatus === 201,
`expected 200 or 201 from upload PUT, got ${this.uploadStatus}`,
).toBe(true)
},
)
When(
'I request a signed download URL for filename {string}',
async function (this: ContentWorld, fileName: string) {
const res = await fetch(
`${config.apiUrl}/cards/${this.cardId}/attachments/${encodeURIComponent(fileName)}/signed-url`,
)
expect(res.ok, `signed-url request failed: ${res.status} ${await res.text()}`).toBe(true)
const body = (await res.json()) as { signedUrl: string }
this.signedUrl = body.signedUrl
},
)
Then(
'the signed URL response contains a signedUrl',
function (this: ContentWorld) {
expect(typeof this.signedUrl).toBe('string')
expect(this.signedUrl.length).toBeGreaterThan(0)
},
)
Then(
'downloading the signed URL returns {string}',
async function (this: ContentWorld, expectedContent: string) {
const res = await fetch(this.signedUrl)
expect(res.ok, `signed download failed: ${res.status}`).toBe(true)
const text = await res.text()
expect(text).toBe(expectedContent)
},
)
When('I strip the signature from the signed URL', function (this: ContentWorld) {
const url = new URL(this.signedUrl)
url.searchParams.delete('sig')
this.signedUrl = url.toString()
})
Then(
'fetching the unsigned URL returns a 4xx status',
async function (this: ContentWorld) {
const res = await fetch(this.signedUrl)
expect(res.status, `expected 4xx for unsigned access, got ${res.status}`).toBeGreaterThanOrEqual(400)
expect(res.status).toBeLessThan(500)
},
)

126
e2e/tests/support/hooks.ts Normal file
View File

@@ -0,0 +1,126 @@
import {
Before,
After,
BeforeAll,
AfterAll,
setDefaultTimeout,
} from '@cucumber/cucumber'
import { spawn, type ChildProcess } from 'child_process'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import type { AppWorld } from './world.js'
import { config } from './types.js'
setDefaultTimeout(config.responseTimeout)
let backendProcess: ChildProcess | undefined
let frontendProcess: ChildProcess | undefined
async function waitForUrl(url: string, label: string, timeoutMs = 60_000) {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
try {
const res = await fetch(url)
if (res.status < 500) return
} catch {
// not ready yet
}
await new Promise((r) => setTimeout(r, 500))
}
throw new Error(`[e2e] ${label} did not become ready at ${url} within ${timeoutMs}ms`)
}
BeforeAll(async function () {
if (!config.manageServers) {
// Caller is expected to start backend + frontend (e.g. via pm2, docker,
// or a separate `yarn dev` terminal) before running tests.
return
}
const __dirname = dirname(fileURLToPath(import.meta.url))
const repoRoot = resolve(__dirname, '../../..')
// Project-specific: override these commands by setting E2E_BACKEND_CMD /
// E2E_FRONTEND_CMD env vars. The defaults assume `yarn dev` at the repo
// root spawns the frontend, and the project provides a `dev:backend`
// script for the API.
const backendCmd = process.env.E2E_BACKEND_CMD ?? 'yarn dev:backend'
const frontendCmd = process.env.E2E_FRONTEND_CMD ?? 'yarn dev'
backendProcess = spawn(backendCmd, {
cwd: repoRoot,
env: { ...process.env, NODE_ENV: 'test' },
stdio: 'pipe',
shell: true,
detached: true,
})
backendProcess.stderr?.on('data', (d: Buffer) =>
process.stderr.write(`[backend] ${d}`),
)
backendProcess.stdout?.on('data', (d: Buffer) =>
process.stdout.write(`[backend] ${d}`),
)
frontendProcess = spawn(frontendCmd, {
cwd: repoRoot,
env: { ...process.env, NODE_ENV: 'test' },
stdio: 'pipe',
shell: true,
detached: true,
})
frontendProcess.stderr?.on('data', (d: Buffer) =>
process.stderr.write(`[frontend] ${d}`),
)
frontendProcess.stdout?.on('data', (d: Buffer) =>
process.stdout.write(`[frontend] ${d}`),
)
await Promise.all([
waitForUrl(config.apiUrl, 'backend'),
waitForUrl(config.appUrl, 'frontend'),
])
})
AfterAll(async function () {
for (const p of [backendProcess, frontendProcess]) {
if (!p?.pid) continue
try {
// Detached + pgid kill takes the whole tree (vite/wrangler spawn children).
process.kill(-p.pid, 'SIGTERM')
} catch {
try {
p.kill('SIGTERM')
} catch {
// ignore
}
}
}
})
Before(async function (this: AppWorld) {
await this.openBrowser()
if (config.resetUrl) {
const body = config.resetRpcName
? JSON.stringify({ rpcName: config.resetRpcName, data: {} })
: undefined
await fetch(config.resetUrl, {
method: 'POST',
headers: body ? { 'content-type': 'application/json' } : undefined,
body,
})
.then((res) => {
if (!res.ok) {
process.stderr.write(
`[e2e] WARN: reset hook ${config.resetUrl} returned ${res.status}\n`,
)
}
})
.catch(() => {
process.stderr.write(`[e2e] WARN: reset hook ${config.resetUrl} unreachable\n`)
})
}
})
After(async function (this: AppWorld) {
await this.closeBrowser()
})

View File

@@ -0,0 +1,28 @@
// Project-level e2e configuration. Override via env vars per environment.
//
// Defaults match the kanban-board-template's default ports:
// apps/kanban → tanstack-start (ssr) on 7104
// backend → wrangler/cfw worker on 4003 (or whatever the project wires)
export const config = {
/** URL of the running frontend (vite dev server or built preview). */
appUrl: process.env.APP_URL ?? 'http://localhost:7104',
/** URL of the running backend (Pikku API / CFW worker). */
apiUrl: process.env.API_URL ?? 'http://localhost:4003',
/** Per-step Playwright timeout (ms). */
responseTimeout: Number(process.env.E2E_TIMEOUT ?? 30_000),
/** Whether the harness should spawn the dev servers itself. */
manageServers: process.env.E2E_MANAGE_SERVERS === '1',
/**
* Reset hook URL — POST here to reset the DB to seed state between scenarios.
* Project must implement this as a sessionless RPC gated behind a test-only
* flag (e.g. NODE_ENV !== 'production'). Leave undefined to skip reset.
*/
resetUrl: process.env.E2E_RESET_URL,
/**
* RPC name to invoke at resetUrl. When set, the reset hook posts
* `{rpcName, data: {}}` as the body — matching pikku's public /rpc/:name
* envelope. Leave unset for non-pikku reset endpoints.
*/
resetRpcName: process.env.E2E_RESET_RPC_NAME,
} as const

View File

@@ -0,0 +1,84 @@
import { World, setWorldConstructor } from '@cucumber/cucumber'
import {
chromium,
type Browser,
type BrowserContext,
type Page,
} from '@playwright/test'
import { config } from './types.js'
/**
* AppWorld — generic Playwright world for portal-style apps.
*
* Exposes the primitives every feature needs:
* - openBrowser / closeBrowser — lifecycle
* - gotoApp(path) — navigate within the frontend
* - login(email, password) — fill + submit /login, wait for redirect
* - logout() — visit /logout
* - expectText(text) — assert visible text
*
* Project-specific helpers (e.g. createBooking, assignRoom) belong in
* step files, not here. Keep this generic.
*/
export class AppWorld extends World {
browser!: Browser
context!: BrowserContext
page!: Page
async openBrowser() {
const headed = process.env.HEADED === '1' || process.env.HEADED === 'true'
this.browser = await chromium.launch({
headless: !headed,
slowMo: headed ? 150 : 0,
})
this.context = await this.browser.newContext(
process.env.E2E_LOCALE ? { locale: process.env.E2E_LOCALE } : undefined,
)
this.page = await this.context.newPage()
this.page.setDefaultTimeout(config.responseTimeout)
}
async closeBrowser() {
await this.context?.close()
await this.browser?.close()
}
async gotoApp(path: string) {
const url = path.startsWith('http')
? path
: `${config.appUrl}${path.startsWith('/') ? path : `/${path}`}`
await this.page.goto(url)
}
/**
* Fills the login form on /login and waits for navigation away from it.
* Assumes the login form has inputs named/typed `email` and a password
* input. Override per-project if the form differs.
*/
async login(email: string, password: string) {
await this.gotoApp('/login')
await this.page.locator('input[type="email"]').fill(email)
await this.page.locator('input[type="password"]').fill(password)
await this.page.getByRole('button', { name: /(sign in|login|anmelden)/i }).click()
await this.page.waitForURL((url) => !url.pathname.startsWith('/login'), {
timeout: config.responseTimeout,
})
}
async logout() {
await this.gotoApp('/logout')
}
async expectText(text: string) {
await this.page.getByText(text, { exact: false }).first().waitFor({
state: 'visible',
timeout: config.responseTimeout,
})
}
async getPageText(): Promise<string> {
return this.page.innerText('body')
}
}
setWorldConstructor(AppWorld)

14
e2e/tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["node"],
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"strict": true
},
"include": ["tests/**/*.ts"]
}

42
package.json Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "@project/root",
"version": "0.0.1",
"private": true,
"workspaces": [
"packages/*",
"packages/functions/tests",
"apps/*",
"design/*"
],
"scripts": {
"prebuild": "yarn workspace @project/functions pikku all",
"dev": "yarn workspace @project/kanban dev",
"tsc": "yarn workspaces foreach --all run tsc",
"lint": "oxlint --config ../.oxlintrc.json apps packages",
"format": "oxfmt --config ../.oxfmtrc.json apps packages",
"format:check": "oxfmt --check --config ../.oxfmtrc.json apps packages",
"e2e": "cd e2e && yarn test",
"e2e:headed": "cd e2e && yarn test:headed"
},
"dependencies": {
"@pikku/addon-console": "^0.12.16",
"@pikku/ai-vercel": "^0.12.6",
"@pikku/better-auth": "^0.12.9",
"@pikku/core": "^0.12.35",
"@pikku/fetch": "^0.12.3",
"@pikku/kysely": "^0.12.16",
"@pikku/kysely-sqlite": "^0.12.6",
"@pikku/node-http-server": "^0.12.2",
"@pikku/react": "^0.12.3",
"@pikku/schedule": "^0.12.2",
"@pikku/schema-cfworker": "^0.12.2"
},
"devDependencies": {
"@pikku/cli": "^0.12.45",
"@pikku/cucumber": "^0.12.4",
"@pikku/inspector": "^0.12.23",
"@pikku/kysely-node-sqlite": "^0.12.2",
"oxfmt": "^0.53.0",
"oxlint": "^1.68.0"
}
}

View File

@@ -0,0 +1,8 @@
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'

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": "^8.3.8",
"@mantine/hooks": "^8.3.8",
"@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": "^8.3.8",
"@mantine/hooks": "^8.3.8",
"@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,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,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,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,54 @@
import type { AppStory, AppStoryMeta } from './csf.types'
import { UserCard, type User } from './UserCard'
import { stubQuery } from './fixtures/stubQuery'
// App-level widget: composed from library primitives (Avatar + Badge + Card) and
// driven by a query, so it's previewed across its named data-state scenarios in
// the Design tab's App lens — not as simple variants.
const meta: AppStoryMeta = {
title: 'UserCard',
component: UserCard,
group: 'Account',
description: 'Shows the current user, composed from the library Avatar + Badge.',
tags: ['app'],
inputs: [
{
name: 'query',
kind: 'query',
type: 'UseQueryResult<User | null>',
description: 'Fetches the user to display.',
},
],
argTypes: {
query: { description: 'User query result (loading / error / empty / success).', control: false },
},
}
export default meta
const sampleUser: User = {
id: 'u_1',
name: 'Ada Lovelace',
email: 'ada@example.com',
role: 'Principal Engineer',
status: 'active',
}
function LoadingScenario() {
return <UserCard query={stubQuery.loading<User | null>()} />
}
export const Loading: AppStory = { tag: 'query: pending', render: LoadingScenario }
function ErrorScenario() {
return <UserCard query={stubQuery.error<User | null>(new Error('Request failed'))} />
}
export const Errored: AppStory = { name: 'Error', tag: 'query: error', render: ErrorScenario }
function EmptyScenario() {
return <UserCard query={stubQuery.empty<User | null>()} />
}
export const Empty: AppStory = { tag: 'query: no data', render: EmptyScenario }
function ActiveScenario() {
return <UserCard query={stubQuery.success<User | null>(sampleUser)} />
}
export const Active: AppStory = { name: 'Active user', tag: 'query: success', render: ActiveScenario }

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,45 @@
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
/** Left-menu group in the Design tab; falls back to the first tag. */
group?: string
tags?: string[]
argTypes?: Record<string, ArgType>
}
export interface Story {
args?: Record<string, unknown>
render?: ComponentType<any>
name?: string
}
/** A query or mutation an app widget consumes. Listed in the Design tab's
* right-column inspector under the App lens. */
export interface AppInput {
name: string
kind: 'query' | 'mutation'
type?: string
description?: string
}
/** Meta for an app-level widget (`*.app.stories.tsx`). Such a widget is composed
* from the library and driven by queries/mutations, so it's previewed across
* named data-state scenarios rather than simple variants. */
export interface AppStoryMeta extends StoryMeta {
inputs?: AppInput[]
}
/** One named data-state scenario of an app widget (e.g. Loading / Error / Ready).
* `tag` annotates the data state, e.g. "query: pending". */
export interface AppStory extends Story {
tag?: 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,18 @@
{
"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.3",
"@pikku/react": "^0.12.3"
}
}

View File

@@ -0,0 +1,56 @@
/**
* This file was generated by @pikku/cli@0.12.45
*/
import { useQuery, useInfiniteQuery, useMutation, type UseQueryOptions, type UseInfiniteQueryOptions, type UseMutationOptions, type InfiniteData } from '@tanstack/react-query'
import { usePikkuRPC } from '@pikku/react'
import type { FlattenedRPCMap } from '../../../../../../../../../../../tmp/lc/.pikku/rpc/pikku-rpc-wirings-map.gen.d.js'
type RPCInvoke = <Name extends keyof FlattenedRPCMap>(name: Name, data: FlattenedRPCMap[Name]['input']) => Promise<FlattenedRPCMap[Name]['output']>
export const usePikkuQuery = <Name extends keyof FlattenedRPCMap>(
name: Name,
data: FlattenedRPCMap[Name]['input'],
options?: Omit<UseQueryOptions<FlattenedRPCMap[Name]['output'], Error>, 'queryKey' | 'queryFn'>
) => {
const rpc = usePikkuRPC<{ invoke: RPCInvoke }>()
return useQuery<FlattenedRPCMap[Name]['output'], Error>({
queryKey: [name, data],
queryFn: () => rpc.invoke(name, data),
...options,
})
}
export const usePikkuMutation = <Name extends keyof FlattenedRPCMap>(
name: Name,
options?: Omit<UseMutationOptions<FlattenedRPCMap[Name]['output'], Error, FlattenedRPCMap[Name]['input']>, 'mutationFn'>
) => {
const rpc = usePikkuRPC<{ invoke: RPCInvoke }>()
return useMutation<FlattenedRPCMap[Name]['output'], Error, FlattenedRPCMap[Name]['input']>({
mutationFn: (data) => rpc.invoke(name, data),
...options,
})
}
type PaginatedKeys = {
[K in keyof FlattenedRPCMap]: FlattenedRPCMap[K]['output'] extends { nextCursor?: string | null } ? K : never
}[keyof FlattenedRPCMap]
type InfiniteOpts<Name extends PaginatedKeys> = Omit<
UseInfiniteQueryOptions<FlattenedRPCMap[Name]['output'], Error, InfiniteData<FlattenedRPCMap[Name]['output'], string | undefined>, readonly unknown[], string | undefined>,
'queryKey' | 'queryFn' | 'getNextPageParam' | 'initialPageParam'
>
export const usePikkuInfiniteQuery = <Name extends PaginatedKeys>(
name: Name,
data: Omit<FlattenedRPCMap[Name]['input'], 'nextCursor'>,
options?: InfiniteOpts<Name>
) => {
const rpc = usePikkuRPC<{ invoke: RPCInvoke }>()
return useInfiniteQuery({
queryKey: [name, data] as const,
queryFn: ({ pageParam }: { pageParam: string | undefined }) => rpc.invoke(name, { ...data, nextCursor: pageParam } as unknown as FlattenedRPCMap[Name]['input']),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage: FlattenedRPCMap[Name]['output']) => (lastPage as { nextCursor?: string }).nextCursor ?? undefined,
...options,
})
}

View File

@@ -0,0 +1,67 @@
/**
* This file was generated by @pikku/cli@0.12.45
*/
import { CorePikkuFetch, HTTPMethod } from '@pikku/fetch'
import type { HTTPWiringsMap, HTTPWiringHandlerOf, HTTPWiringsWithMethod } from '../../../../../../../../../../../tmp/lc/.pikku/http/pikku-http-wirings-map.gen.d.js'
export class PikkuFetch extends CorePikkuFetch {
public async post<Route extends HTTPWiringsWithMethod<'POST'>>(
route: Route,
...args: null extends HTTPWiringHandlerOf<Route, 'POST'>['input']
? [data?: Exclude<HTTPWiringHandlerOf<Route, 'POST'>['input'], null>, options?: Omit<RequestInit, 'body'>]
: [data: HTTPWiringHandlerOf<Route, 'POST'>['input'], options?: Omit<RequestInit, 'body'>]
): Promise<HTTPWiringHandlerOf<Route, 'POST'>['output']> {
const [data, options] = args;
return super.api(route, 'POST', data, options);
}
public async get<Route extends HTTPWiringsWithMethod<'GET'>>(
route: Route,
...args: null extends HTTPWiringHandlerOf<Route, 'GET'>['input']
? [data?: Exclude<HTTPWiringHandlerOf<Route, 'GET'>['input'], null>, options?: Omit<RequestInit, 'body'>]
: [data: HTTPWiringHandlerOf<Route, 'GET'>['input'], options?: Omit<RequestInit, 'body'>]
): Promise<HTTPWiringHandlerOf<Route, 'GET'>['output']> {
const [data, options] = args;
return super.api(route, 'GET', data, options);
}
public async patch<Route extends HTTPWiringsWithMethod<'PATCH'>>(
route: Route,
...args: null extends HTTPWiringHandlerOf<Route, 'PATCH'>['input']
? [data?: Exclude<HTTPWiringHandlerOf<Route, 'PATCH'>['input'], null>, options?: Omit<RequestInit, 'body'>]
: [data: HTTPWiringHandlerOf<Route, 'PATCH'>['input'], options?: Omit<RequestInit, 'body'>]
): Promise<HTTPWiringHandlerOf<Route, 'PATCH'>['output']> {
const [data, options] = args;
return super.api(route, 'PATCH', data, options);
}
public async head<Route extends HTTPWiringsWithMethod<'HEAD'>>(
route: Route,
...args: null extends HTTPWiringHandlerOf<Route, 'HEAD'>['input']
? [data?: Exclude<HTTPWiringHandlerOf<Route, 'HEAD'>['input'], null>, options?: Omit<RequestInit, 'body'>]
: [data: HTTPWiringHandlerOf<Route, 'HEAD'>['input'], options?: Omit<RequestInit, 'body'>]
): Promise<HTTPWiringHandlerOf<Route, 'HEAD'>['output']> {
const [data, options] = args;
return super.api(route, 'HEAD', data, options);
}
public async delete<Route extends HTTPWiringsWithMethod<'DELETE'>>(
route: Route,
...args: null extends HTTPWiringHandlerOf<Route, 'DELETE'>['input']
? [data?: Exclude<HTTPWiringHandlerOf<Route, 'DELETE'>['input'], null>, options?: Omit<RequestInit, 'body'>]
: [data: HTTPWiringHandlerOf<Route, 'DELETE'>['input'], options?: Omit<RequestInit, 'body'>]
): Promise<HTTPWiringHandlerOf<Route, 'DELETE'>['output']> {
const [data, options] = args;
return super.api(route, 'DELETE', data, options);
}
public async fetch<
Route extends keyof HTTPWiringsMap,
Method extends keyof HTTPWiringsMap[Route]
>(route: Route, method: Method, data: HTTPWiringHandlerOf<Route, Method>['input'], options?: Omit<RequestInit, 'body'>): Promise<Response> {
return await super.fetch(route, method as HTTPMethod, data, options);
}
}
export const pikkuFetch = new PikkuFetch();

View File

@@ -0,0 +1,148 @@
/**
* This file was generated by @pikku/cli@0.12.45
*/
import { PikkuFetch } from "./pikku-fetch.gen.js"
import type { RPCInvoke, TypedAgentRun, TypedStartWorkflow, TypedRunWorkflow, TypedWorkflowStatus } from '../../../../../../../../../../../tmp/lc/.pikku/rpc/pikku-rpc-wirings-map.gen.d.js'
import type { WorkflowRunStatus } from '@pikku/core/workflow'
/**
* PikkuRPC provides a type-safe client for making Remote Procedure Calls (RPC)
* to your Pikku server. It wraps the underlying HTTP client and provides a
* simple interface for invoking server-side functions.
*/
export class PikkuRPC {
/** The underlying HTTP client used for making RPC calls */
pikkuFetch = new PikkuFetch()
/**
* Sets a custom PikkuFetch instance to use for RPC calls.
* This allows you to configure custom settings or use a shared client instance.
*
* @param pikkuFetch - The PikkuFetch instance to use
*/
setPikkuFetch(pikkuFetch: PikkuFetch): void {
this.pikkuFetch = pikkuFetch
}
/**
* Sets the base server URL for all RPC calls.
*
* @param serverUrl - The base URL of your Pikku server (e.g., 'https://api.example.com')
*/
setServerUrl(serverUrl: string): void {
this.pikkuFetch.setServerUrl(serverUrl)
}
/**
* Sets the JWT token for authorization on all RPC calls.
*
* @param jwt - The JWT token to use for authorization, or null to remove authorization
*/
setAuthorizationJWT(jwt: string | null): void {
this.pikkuFetch.setAuthorizationJWT(jwt)
}
/**
* Sets the API key for authorization on all RPC calls.
*
* @param apiKey - The API key to use for authorization, or null to remove the API key
*/
setAPIKey(apiKey: string | null): void {
this.pikkuFetch.setAPIKey(apiKey)
}
/**
* Invokes a remote procedure call on the server.
* This is a generic method that routes to the appropriate server function
* based on the function name and passes the provided data.
*
* @param rpcName - The name of the server function to invoke
* @param data - The data to pass to the server function
* @returns A promise that resolves with the function's return value
*/
invoke = ((rpcName: string, data?: unknown) => {
return this.pikkuFetch.post(`/rpc/${String(rpcName)}` as never, { rpcName: String(rpcName), data }) as any
}) as RPCInvoke
/**
* Starts a workflow by name with the given input.
* Posts to \`/workflow/:workflowName/start\`.
*
* @param workflowName - The registered workflow name
* @param input - The workflow input data
* @returns A promise that resolves with the new run ID
*/
startWorkflow: TypedStartWorkflow = async (workflowName, input) => {
return await this.pikkuFetch.post(`/workflow/${String(workflowName)}/start` as never, { data: input }) as any
}
/**
* Runs a workflow to completion and returns the output.
* Posts to \`/workflow/:workflowName/run\`.
*
* @param workflowName - The registered workflow name
* @param input - The workflow input data
* @returns A promise that resolves with the workflow output
*/
runWorkflow: TypedRunWorkflow = async (workflowName, input) => {
return await this.pikkuFetch.post(`/workflow/${String(workflowName)}/run` as never, { data: input }) as any
}
/**
* Gets the current status of a workflow run.
* GET \`/workflow/:workflowName/status/:runId\`.
*
* @param workflowName - The registered workflow name
* @param runId - The workflow run ID
* @returns A promise with the minimal run status
*/
workflowStatus: TypedWorkflowStatus = async (workflowName, runId) => {
return await this.pikkuFetch.get(`/workflow/${String(workflowName)}/status/${runId}` as never) as unknown as WorkflowRunStatus
}
/**
* Agent namespace — methods for running, streaming, and approving AI agents.
* All methods post to \`/rpc/agent/:agentName/*\`.
*/
agent = {
/**
* Runs an agent to completion and returns the result.
*
* @param agentName - The registered agent name
* @param input - The agent input (message, threadId, resourceId)
* @returns A promise with runId, result, and token usage
*/
run: (async (agentName, input) => {
return await this.pikkuFetch.post(`/rpc/agent/${String(agentName)}` as never, input) as any
}) as TypedAgentRun,
/**
* Streams agent responses via SSE. Used for real-time chat interfaces.
*
* @param agentName - The registered agent name
* @param input - The agent input (message, threadId, resourceId)
*/
stream: async (agentName: string, input: Record<string, unknown>) => {
return await this.pikkuFetch.post(`/rpc/agent/${String(agentName)}/stream` as never, input) as any
},
/**
* Approves or denies pending tool calls for an agent run.
*
* @param agentName - The registered agent name
* @param input - The approval payload (runId, approvals array)
*/
approve: async (agentName: string, input: Record<string, unknown>) => {
return await this.pikkuFetch.post(`/rpc/agent/${String(agentName)}/approve` as never, input) as any
},
}
subscribeToSSE<T = unknown>(
path: string,
handler: (event: T) => void,
onError?: (err: unknown) => void
): { close: () => void } {
return this.pikkuFetch.subscribeToSSE(path, handler, onError)
}
}
export const pikkuRPC = new PikkuRPC();

View File

@@ -0,0 +1,297 @@
/**
* This file was generated by @pikku/cli@0.12.21
*/
/**
* This provides the structure needed for typescript to be aware of RPCs and their return 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
}
export type CreateCardInput = {
title: string
status: 'todo' | 'doing' | 'done'
priority?: ('low' | 'medium' | 'high') | undefined
}
export type CreateCardOutput = {
cardId: string
title: string
status: string
position: number
createdAt: string
}
export type CreateCardToolInput = {
title: string
status: 'todo' | 'doing' | 'done'
}
export type CreateCardToolOutput = { type: 'text'; text: string }[]
export type DeleteAgentThreadInput = { threadId: string; resourceId?: string }
export type DeleteAgentThreadOutput = { deleted: boolean }
export type EnrichCardInput = {
title: string
status: 'todo' | 'doing' | 'done'
}
export type EnrichCardOutput = {
title: string
status: string
priority: 'low' | 'medium' | 'high'
labels: string[]
}
export type GetAgentThreadMessagesInput = { threadId: string; resourceId?: string }
export type GetAgentThreadMessagesOutput = any[]
export type GetAgentThreadRunsInput = { threadId: string; resourceId?: string }
export type GetAgentThreadRunsOutput = any[]
export type GetAgentThreadsInput = {
agentName?: string
resourceId?: string
limit?: number
offset?: number
}
export type GetAgentThreadsOutput = any[]
export type GetCardFileUploadUrlInput = {
cardId: string
fileName: string
contentType: string
}
export type GetCardFileUploadUrlOutput = {
uploadUrl: string
fileKey: string
expiresAt: string
uploadMethod?: ('PUT' | 'POST') | undefined
uploadHeaders?:
| {
[key: string]: string
}
| undefined
}
export type GraphStarterInput = { workflowName: string; nodeId: string; data?: unknown }
export type GraphStarterOutput = { runId: string }
export type ListCardsInput = {
status?: ('todo' | 'doing' | 'done') | undefined
}
export type ListCardsOutput = {
cards: {
cardId: string
title: string
status: string
position: number
createdAt: string
}[]
}
export type ListCardsToolOutput = { type: 'text'; text: string }[]
export type NotifyCardCreatedInput = {
cardId: string
title: string
priority: 'low' | 'medium' | 'high'
labels: string[]
}
export type OnCardEventTriggerInput = {
title: string
status: 'todo' | 'doing' | 'done'
source?: string | undefined
}
export type OnCardEventTriggerOutput = {
cardId: string
title: string
status: string
}
export type OnCardsClearSessionInput = Record<string, never>
export type OnCardsClearSessionOutput = { cleared: true }
export type OnCardsConnectInput = { type: 'hello'; count: number }
export type OnCardsEchoInput = { text: string }
export type OnCardsEchoOutput = { echo: string }
export type OnCardsGetSessionInput = Record<string, never>
export type OnCardsGetSessionOutput = { session: unknown }
export type OnCardsSetSessionInput = { userId: string; label: string }
export type OnCardsSetSessionOutput = { set: true; label: 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 ProcessCardEventInput = {
cardId: string
action: string
status: string
createdAt: string
}
export type RemoteRPCHandlerInput = { rpcName: string; data?: unknown }
export type RpcCallerInput = { rpcName: string; data?: unknown }
export type SecretSchema_notificationWebhookSecret = string
export type SignCardFileInput = {
cardId: string
fileName: string
}
export type SignCardFileOutput = {
signedUrl: string
expiresAt: string
}
export type VariableSchema_maxCardsPerColumn = number
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 createCard: RPCHandler<CreateCardInput, CreateCardOutput>
readonly getCardFileUploadUrl: RPCHandler<GetCardFileUploadUrlInput, GetCardFileUploadUrlOutput>
readonly listCards: RPCHandler<ListCardsInput, ListCardsOutput>
readonly signCardFile: RPCHandler<SignCardFileInput, SignCardFileOutput>
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
>
}
// 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 '../../../functions/.pikku/workflow/pikku-workflow-map.gen.d.js'
import type { AgentMap } from '../../../functions/.pikku/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,41 @@
import postgres from 'postgres'
import { readdir, readFile } from 'node:fs/promises'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const sqlDir = join(__dirname, '../../../sql')
const databaseUrl = process.env.DATABASE_URL
if (!databaseUrl) {
console.error('[db-migrate] DATABASE_URL is not set')
process.exit(1)
}
const sql = postgres(databaseUrl)
await sql`
CREATE TABLE IF NOT EXISTS schema_migrations (
filename TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`
const applied = new Set((await sql`SELECT filename FROM schema_migrations`).map((r) => r.filename))
const files = (await readdir(sqlDir)).filter((f) => f.endsWith('.sql')).sort()
for (const file of files) {
if (applied.has(file)) {
console.log(`[db-migrate] skip ${file} (already applied)`)
continue
}
const content = await readFile(join(sqlDir, file), 'utf8')
console.log(`[db-migrate] applying ${file}...`)
await sql.unsafe(content)
await sql`INSERT INTO schema_migrations (filename) VALUES (${file})`
console.log(`[db-migrate] applied ${file}`)
}
await sql.end()
console.log('[db-migrate] done')

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
{
"name": "@project/functions",
"version": "0.0.1",
"private": true,
"type": "module",
"imports": {
"#pikku": "./.pikku/pikku-types.gen.ts",
"#pikku/*": "./.pikku/*"
},
"dependencies": {
"@ai-sdk/openai-compatible": "^2.0.48",
"@cloudflare/workers-types": "^4.20241218.0",
"@pikku/ai-vercel": "^0.12.6",
"@pikku/better-auth": "^0.12.9",
"@pikku/kysely": "^0.12.16",
"@pikku/schema-cfworker": "^0.12.2",
"ai": "^6.0.0",
"better-auth": "^1.6.18",
"kysely": "^0.28.12",
"zod": "^4.3.6"
},
"peerDependencies": {
"@pikku/ai-vercel": "^0.12.6",
"@pikku/better-auth": "^0.12.9",
"@pikku/cloudflare": "^0.12.10",
"@pikku/core": "^0.12.35",
"@pikku/kysely": "^0.12.16",
"@pikku/kysely-node-sqlite": "^0.12.2",
"@pikku/kysely-sqlite": "^0.12.6",
"@pikku/schedule": "^0.12.2",
"@pikku/schema-cfworker": "^0.12.2"
}
}

View File

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

View File

@@ -0,0 +1,82 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
import {
CoreAIAgent,
PikkuAIMiddlewareHooks,
} from '@pikku/core/ai-agent'
import {
agent as coreAgent,
agentStream as coreAgentStream,
agentResume as coreAgentResume,
agentApprove as coreAgentApprove,
} from '@pikku/core/ai-agent'
import type { PikkuPermission, PikkuMiddleware, Services, PikkuFunctionConfig } from '../function/pikku-function-types.gen.js'
import type { StandardSchemaV1 } from '@standard-schema/spec'
import type { AIAgentMemoryConfig, AIAgentInput } from '@pikku/core/ai-agent'
import type { AgentMap } from './pikku-agent-map.gen.d.js'
type AIAgentConfig<
InputSchema extends StandardSchemaV1 | undefined = undefined,
OutputSchema extends StandardSchemaV1 | undefined = undefined
> = Omit<CoreAIAgent<PikkuPermission, PikkuMiddleware>, 'tools' | 'agents' | 'memory' | 'input' | 'output'> & {
input?: InputSchema
output?: OutputSchema
memory?: Omit<AIAgentMemoryConfig, 'workingMemory'> & { workingMemory?: StandardSchemaV1 }
tools?: object[]
agents?: AIAgentConfig<StandardSchemaV1 | undefined, StandardSchemaV1 | undefined>[]
}
export const pikkuAIAgent = <
InputSchema extends StandardSchemaV1 | undefined = undefined,
OutputSchema extends StandardSchemaV1 | undefined = undefined
>(
agent: AIAgentConfig<InputSchema, OutputSchema>
) => {
return agent
}
export const pikkuAIMiddleware = <
State extends Record<string, unknown> = Record<string, unknown>,
RequiredServices extends Services = Services,
>(
hooks: PikkuAIMiddlewareHooks<State, RequiredServices>
): PikkuAIMiddlewareHooks<State, RequiredServices> => hooks
export const agent = <Name extends keyof AgentMap>(
agentName: Name
) => {
return coreAgent<AgentMap>(agentName as string & keyof AgentMap) as PikkuFunctionConfig<
AIAgentInput,
{ runId: string; result: AgentMap[Name]['output']; usage: { inputTokens: number; outputTokens: number } },
'session' | 'rpc'
>
}
export const agentStream = <Name extends keyof AgentMap>(
agentName?: Name
) => {
return coreAgentStream<AgentMap>(agentName as string & keyof AgentMap) as PikkuFunctionConfig<
{ agentName?: string; message: string; threadId: string; resourceId: string },
void,
'session' | 'rpc'
>
}
export const agentResume = () => {
return coreAgentResume() as PikkuFunctionConfig<
{ runId: string; toolCallId: string; approved: boolean },
void,
'session' | 'rpc'
>
}
export const agentApprove = <Name extends keyof AgentMap>(
agentName: Name
) => {
return coreAgentApprove<AgentMap>(agentName as string & keyof AgentMap) as PikkuFunctionConfig<
{ runId: string; approvals: { toolCallId: string; approved: boolean }[] },
unknown,
'session' | 'rpc'
>
}

View File

@@ -0,0 +1,177 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/**
* Channel-specific type definitions for tree-shaking optimization
*/
import { CoreChannel, CorePikkuChannelMiddleware, CorePikkuChannelMiddlewareFactory, wireChannel as wireChannelCore, defineChannelRoutes as defineChannelRoutesCore, addChannelMiddleware as addChannelMiddlewareCore } from '@pikku/core/channel'
import { AssertHTTPWiringParams } from '@pikku/core/http'
import type { PikkuFunctionConfig, PikkuFunctionSessionless, PikkuPermission, PikkuMiddleware, Services } from '../function/pikku-function-types.gen.js'
import type { CorePermissionGroup } from '@pikku/core'
import type { StandardSchemaV1 } from '@standard-schema/spec'
/**
* Helper type to infer the output type from a Standard Schema
*/
type InferSchemaOutput<T> = T extends StandardSchemaV1<any, infer Output> ? Output : never
/**
* Type definition for WebSocket channels with typed data exchange.
* Supports connection, disconnection, and message handling.
* Accepts both session-based (PikkuFunction) and sessionless (PikkuFunctionSessionless) functions.
*
* @template ChannelData - Type of data exchanged through the channel
* @template Channel - String literal type for the channel name
*/
type ChannelWiring<ChannelData, Channel extends string> = CoreChannel<
ChannelData,
Channel,
PikkuFunctionConfig<void, any, 'channel' | 'session' | 'rpc'>,
PikkuFunctionConfig<void, void, 'channel' | 'session' | 'rpc'>,
PikkuFunctionConfig<any, any, 'channel' | 'session' | 'rpc'>,
PikkuPermission,
PikkuMiddleware
>
/**
* Creates a function that handles WebSocket channel connections.
* Called when a client connects to a channel.
*
* @template Out - Output type for connection response
* @param func - Function definition, either direct function or configuration object
* @returns The normalized configuration object
*/
export const pikkuChannelConnectionFunc = <Out = unknown>(
func:
| PikkuFunctionSessionless<void, Out, 'channel' | 'session' | 'rpc'>
| PikkuFunctionConfig<void, Out, 'channel' | 'session' | 'rpc'>
) => {
return typeof func === 'function' ? { func } : func
}
/**
* Creates a function that handles WebSocket channel disconnections.
* Called when a client disconnects from a channel.
*
* @param func - Function definition, either direct function or configuration object
* @returns The normalized configuration object
*/
export const pikkuChannelDisconnectionFunc = (
func:
| PikkuFunctionSessionless<void, void, 'channel'>
| PikkuFunctionConfig<void, void, 'channel' | 'session' | 'rpc'>
) => {
return typeof func === 'function' ? { func } : func
}
/**
* Configuration object for channel functions with Zod schema validation.
*/
type PikkuChannelFuncConfigWithSchema<
InputSchema extends StandardSchemaV1,
OutputSchema extends StandardSchemaV1 | undefined = undefined
> = {
name?: string
tags?: string[]
expose?: boolean
remote?: boolean
func: PikkuFunctionSessionless<
InferSchemaOutput<InputSchema>,
OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown,
'channel' | 'session' | 'rpc'
>
auth?: boolean
permissions?: CorePermissionGroup<PikkuPermission<InferSchemaOutput<InputSchema>>>
middleware?: PikkuMiddleware[]
input: InputSchema
output?: OutputSchema
}
/**
* Creates a function that handles WebSocket channel messages.
* Called when a message is received on a channel.
*
* Supports two patterns:
* 1. Generic types: `pikkuChannelFunc<Input, Output>({ func: ... })`
* 2. Zod schemas: `pikkuChannelFunc({ input: z.object(...), func: ... })`
*
* @template In - Input type for channel messages (inferred from schema if provided)
* @template Out - Output type for channel responses (inferred from schema if provided)
* @param func - Function definition, either direct function or configuration object
* @returns The normalized configuration object
*
* @example
* ```typescript
* // Pattern 1: Using generic types
* const handleMessage = pikkuChannelFunc<{text: string}, {received: boolean}>({
* func: async (_services, { text }) => ({ received: true })
* })
*
* // Pattern 2: Using Zod schemas
* const messageInput = z.object({ text: z.string() })
* const messageOutput = z.object({ received: z.boolean() })
*
* const handleMessage = pikkuChannelFunc({
* input: messageInput,
* output: messageOutput,
* func: async (_services, { text }) => ({ received: true })
* })
* ```
*/
export function pikkuChannelFunc<
InputSchema extends StandardSchemaV1,
OutputSchema extends StandardSchemaV1 | undefined = undefined
>(
config: PikkuChannelFuncConfigWithSchema<InputSchema, OutputSchema>
): PikkuFunctionConfig<InferSchemaOutput<InputSchema>, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'channel' | 'session' | 'rpc'>
export function pikkuChannelFunc<In, Out = unknown>(
func:
| PikkuFunctionSessionless<In, Out, 'channel' | 'session' | 'rpc'>
| PikkuFunctionConfig<In, Out, 'channel' | 'session' | 'rpc'>
): PikkuFunctionConfig<In, Out, 'channel' | 'session' | 'rpc'>
export function pikkuChannelFunc(func: any) {
return typeof func === 'function' ? { func } : func
}
/**
* Registers a WebSocket channel with the Pikku framework.
*
* @template ChannelData - Type of data associated with the channel
* @template Channel - String literal type for the channel name
* @param channel - Channel definition with connection, disconnection, and message handlers
*/
export const wireChannel = <ChannelData, Channel extends string>(
channel: ChannelWiring<ChannelData, Channel> & AssertHTTPWiringParams<ChannelData, Channel>
) => {
wireChannelCore(channel as any)
}
/**
* Type-safe helper for defining channel message routes that can be composed.
* Returns the routes record as-is for use with wireChannel's onMessageWiring.
*
* @template T - Record of channel route handlers
* @param routes - The channel routes record
* @returns The same routes record (identity function for type safety)
*/
export function defineChannelRoutes<T extends Record<string, any>>(routes: T): T {
return defineChannelRoutesCore(routes)
}
export type PikkuChannelMiddleware<RequiredServices extends Services = Services, Event = unknown> = CorePikkuChannelMiddleware<RequiredServices, Event>
export const pikkuChannelMiddleware = <RequiredServices extends Services = Services, Event = unknown>(
middleware: PikkuChannelMiddleware<RequiredServices, Event>
): PikkuChannelMiddleware<RequiredServices, Event> => {
return middleware
}
export const pikkuChannelMiddlewareFactory = <In = any>(
factory: CorePikkuChannelMiddlewareFactory<In>
): CorePikkuChannelMiddlewareFactory<In> => {
return factory
}
export const addChannelMiddleware = (tag: string, middleware: PikkuChannelMiddleware[]) =>
addChannelMiddlewareCore(tag, middleware, null)

View File

@@ -0,0 +1,105 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/**
* CLI-specific type definitions for tree-shaking optimization
*/
import { CoreCLI, wireCLI as wireCLICore, CorePikkuCLIRender, CoreCLICommandConfig, defineCLICommands as defineCLICommandsCore } from '@pikku/core/cli'
import type { PikkuFunctionConfig, PikkuMiddleware } from '../function/pikku-function-types.gen.js'
import type { UserSession } from '../../../../src/application-types.d.js'
import type { SingletonServices } from '../../../../src/application-types.d.js'
type Session = UserSession
/**
* Type-safe CLI renderer definition that can access your application's services.
* Use this to define custom renderers for CLI command output.
*
* @template Data - The output data type from the CLI command
* @template RequiredServices - The services required for this renderer
*/
type PikkuCLIRender<Data, RequiredServices extends SingletonServices = SingletonServices> = CorePikkuCLIRender<Data, RequiredServices, Session>
/**
* Creates a type-safe CLI renderer with access to your application's singleton services.
* The renderer receives the full singleton services and output data to format and display results.
*
* @template Data - The output data type from the CLI command
* @template RequiredServices - The minimum services required for type checking (defaults to SingletonServices)
* @param render - Function that receives singleton services and data to render output
* @returns A CLI renderer configuration
*
* @example
* ```typescript
* const myRenderer = pikkuCLIRender<MyData>(({ logger }, data) => {
* logger.info(data.message)
* })
* ```
*/
export const pikkuCLIRender = <Data, RequiredServices extends SingletonServices = SingletonServices>(
render: (services: SingletonServices, data: Data) => void | Promise<void>
): PikkuCLIRender<Data, RequiredServices> => {
return render as any
}
/**
* CLI command configuration with project-specific types.
* Uses CoreCLICommandConfig from @pikku/core with local middleware and render types.
*/
type CLICommandConfig<Func extends PikkuFunctionConfig<In, Out, 'cli' | 'rpc' | 'session'>, In = any, Out = any, Params extends string = string> = CoreCLICommandConfig<Func, PikkuMiddleware, PikkuCLIRender<any>, Params>
/**
* Type definition for CLI applications with commands and global options.
*
* @template Commands - Type describing the command structure
* @template GlobalOptions - Type for global CLI options
*/
type CLIWiring<Commands extends Record<string, CoreCLICommandConfig<any, PikkuMiddleware, PikkuCLIRender<any>, any>>, GlobalOptions> = CoreCLI<Commands, GlobalOptions, PikkuMiddleware, PikkuCLIRender<any>>
/**
* Registers a CLI application with the Pikku framework.
* Creates command-line interfaces with type-safe commands and options.
*
* @template Commands - Type describing the command structure
* @template GlobalOptions - Type for global CLI options
* @param cli - CLI definition with program name, commands, and global options
*/
export const wireCLI = <Commands extends Record<string, CoreCLICommandConfig<any, PikkuMiddleware, PikkuCLIRender<any>, any>>, GlobalOptions>(
cli: CLIWiring<Commands, GlobalOptions>
) => {
wireCLICore(cli as any)
}
/**
* Creates a CLI command definition with automatic option inference from the function's input type.
* This allows TypeScript to automatically derive CLI options from the function signature.
*
* @template FuncConfig - The Pikku function config type
* @template Params - The parameters string literal type
* @param config - CLI command configuration
* @returns CLI command configuration with inferred types
*/
export const pikkuCLICommand = <
FuncConfig extends PikkuFunctionConfig<any, any, 'cli' | 'rpc' | 'session'>,
Params extends string
>(
config: CLICommandConfig<FuncConfig, any, any, Params>
): CoreCLICommandConfig<FuncConfig, PikkuMiddleware, PikkuCLIRender<any>, string> => {
return config as any
}
/**
* Type-safe helper for defining CLI commands that can be composed and spread into wireCLI.
*
* @template T - Record of CLI command configurations
* @param commands - The CLI commands record
* @returns The same commands record (identity function for type safety)
*/
export const defineCLICommands = <T extends Record<string, CoreCLICommandConfig<any, PikkuMiddleware, PikkuCLIRender<any>, any>>>(
commands: T
): T => {
return defineCLICommandsCore(commands as any) as T
}

View File

@@ -0,0 +1,8 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
import type { FlattenedRPCMap } from '../rpc/pikku-rpc-wirings-map.internal.gen.js'
export type NodeCategory = never
export type NodeRPCName = keyof FlattenedRPCMap

View File

@@ -0,0 +1,709 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/**
* Core function, middleware, and permission types for all wirings
*/
import type { CorePikkuMiddleware, CorePermissionGroup, ListInput, ListOutput, PikkuWire, PickRequired } from '@pikku/core'
import type { CorePikkuFunctionConfig, CorePikkuAuth, CorePikkuAuthConfig, CorePikkuPermission } from '@pikku/core/function'
import { pikkuAuth as pikkuAuthCore } from '@pikku/core/function'
import {
addTagMiddleware as addTagMiddlewareCore,
addGlobalMiddleware as addGlobalMiddlewareCore,
addTagPermission as addTagPermissionCore,
addGlobalPermission as addGlobalPermissionCore,
} from '@pikku/core/middleware'
import { pikkuState as __pikkuState, CreateWireServices } from '@pikku/core/internal'
import type { NodeType } from '@pikku/core/node'
import type { StandardSchemaV1 } from '@standard-schema/spec'
import { CorePikkuFunction, CorePikkuFunctionSessionless } from '@pikku/core/function'
import type { UserSession } from '../../../../src/application-types.d.js'
import type { SingletonServices } from '../../../../src/application-types.d.js'
import type { Services } from '../../../../src/application-types.d.js'
import type { Config } from '../../../../src/application-types.d.js'
import type { TypedPikkuRPC, FlattenedRPCMap } from '../rpc/pikku-rpc-wirings-map.internal.gen.d.js'
import type { RequiredSingletonServices, RequiredWireServices } from '../pikku-services.gen.js'
import type { TypedWorkflow } from '../workflow/pikku-workflow-types.gen.js'
export type { SingletonServices as SingletonServices }
export type { Services as Services }
export type Session = UserSession
/**
* Inline node configuration for function definitions.
*/
export type NodeConfig = {
displayName: string
category: string
type: NodeType
errorOutput?: boolean
}
/**
* Type-safe API permission definition that integrates with your application's session type.
* Use this to define authorization logic for your API endpoints.
*
* @template In - The input type that the permission check will receive
* @template RequiredServices - The services required for this permission check
*/
export type PikkuPermission<In = unknown, RequiredServices extends Services = Services> = CorePikkuPermission<In, RequiredServices, PikkuWire<In, never, false, Session>>
/**
* Type-safe middleware definition that can access your application's services and session.
* Use this to define reusable middleware that can be applied to multiple wirings.
*
* @template RequiredServices - The services required for this middleware
*/
export type PikkuMiddleware<RequiredServices extends SingletonServices = SingletonServices> = CorePikkuMiddleware<RequiredServices>
/**
* Configuration object for creating a permission with metadata
*/
export type PikkuPermissionConfig<In = unknown, RequiredServices extends Services = Services> = {
/** The permission function */
func: PikkuPermission<In, RequiredServices>
/** Optional human-readable name for the permission */
name?: string
/** Optional description of what the permission checks */
description?: string
}
/**
* Factory function for creating permissions with tree-shaking support.
* Supports both direct function and configuration object syntax.
*
* @example
* ```typescript
* // Direct function syntax
* const permission = pikkuPermission(async ({ logger }, data, { session }) => {
* const session = await session?.get()
* return session?.role === 'admin'
* })
*
* // Configuration object syntax with metadata
* const adminPermission = pikkuPermission({
* name: 'Admin Permission',
* description: 'Checks if user has admin role',
* func: async ({ logger }, data, { session }) => {
* const session = await session?.get()
* return session?.role === 'admin'
* }
* })
* ```
*/
export const pikkuPermission = <In>(
permission: PikkuPermission<In> | PikkuPermissionConfig<In>
): PikkuPermission<In> => {
return typeof permission === 'function' ? permission : permission.func
}
/**
* Type-safe auth-only permission that only needs services and session.
* Use this for upfront authorization gates (MCP tools, AI agents, workflows)
* where request data isn't available yet.
*
* @template RequiredServices - The services required for this auth check
*/
export type PikkuAuth<RequiredServices extends SingletonServices = SingletonServices> = CorePikkuAuth<RequiredServices, Session>
/**
* Configuration object for creating an auth permission with metadata
*/
export type PikkuAuthConfig<RequiredServices extends SingletonServices = SingletonServices> = CorePikkuAuthConfig<RequiredServices, Session>
/**
* Factory function for creating auth-only permissions with tree-shaking support.
* Auth permissions only receive services and session (no request data),
* making them evaluable before request data is available.
*
* @example
* \`\`\`typescript
* const isAuthenticated = pikkuAuth(async ({ logger }, session) => {
* return !!session
* })
*
* const isAdmin = pikkuAuth({
* name: 'Admin Auth',
* description: 'Checks if user is an admin',
* func: async ({ logger }, session) => {
* return session?.role === 'admin'
* }
* })
* \`\`\`
*/
export const pikkuAuth = <RequiredServices extends SingletonServices = SingletonServices>(
auth: PikkuAuth<RequiredServices> | PikkuAuthConfig<RequiredServices>
): PikkuPermission<any, any> => {
return pikkuAuthCore(auth as any) as any
}
/**
* Configuration object for creating middleware with metadata
*/
export type PikkuMiddlewareConfig<RequiredServices extends SingletonServices = SingletonServices> = {
/** The middleware function */
func: PikkuMiddleware<RequiredServices>
/** Optional human-readable name for the middleware */
name?: string
/** Optional description of what the middleware does */
description?: string
}
/**
* Factory function for creating middleware with tree-shaking support.
* Supports both direct function and configuration object syntax.
*
* @example
* ```typescript
* // Direct function syntax
* const middleware = pikkuMiddleware(({ logger }, wires, next) => {
* logger.info('Middleware executed')
* await next()
* })
*
* // Configuration object syntax with metadata
* const logMiddleware = pikkuMiddleware({
* name: 'Request Logger',
* description: 'Logs all incoming requests',
* func: async ({ logger }, wires, next) => {
* logger.info('Request started')
* await next()
* }
* })
* ```
*/
export const pikkuMiddleware = <RequiredServices extends SingletonServices = SingletonServices>(
middleware: PikkuMiddleware<RequiredServices> | PikkuMiddlewareConfig<RequiredServices>
): PikkuMiddleware<RequiredServices> => {
return typeof middleware === 'function' ? middleware : middleware.func
}
/**
* Factory function for creating middleware factories
* Use this when your middleware needs configuration/input parameters
*
* @example
* ```typescript
* export const logMiddleware = pikkuMiddlewareFactory<LogOptions>(({
* message,
* level = 'info'
* }) => {
* return pikkuMiddleware(async ({ logger }, next) => {
* logger[level](message)
* await next()
* })
* })
* ```
*/
export const pikkuMiddlewareFactory = <In = any>(
factory: (input: In) => PikkuMiddleware
): ((input: In) => PikkuMiddleware) => {
return factory
}
/**
* Factory function for creating permission factories
* Use this when your permission needs configuration/input parameters
*
* @example
* ```typescript
* export const requireRole = pikkuPermissionFactory<{ role: string }>(({
* role
* }) => {
* return pikkuPermission(async ({ logger }, data, { session }) => {
* if (!session || session.role !== role) {
* logger.warn(`Permission denied: required role '${role}'`)
* return false
* }
* return true
* })
* })
* ```
*/
export const pikkuPermissionFactory = <In = any>(
factory: (input: In) => PikkuPermission<any>
): ((input: In) => PikkuPermission<any>) => {
return factory
}
/**
* A function that generates a human-readable description of a pending approval action.
* Used by AI agents to show meaningful approval prompts instead of raw tool arguments.
*
* @template In - The input type (same as the function it describes)
* @template RequiredServices - The services required for this description function
*/
export type PikkuApprovalDescription<In = unknown, RequiredServices extends Services = Services> = (
services: RequiredServices,
data: In
) => Promise<string>
/**
* Factory function for creating approval description functions with tree-shaking support.
*
* @example
* ```typescript
* export const deleteTodoApproval = pikkuApprovalDescription(
* async ({ todoStore }, { id }) => {
* const todo = await todoStore.get(id)
* return \`Delete todo: "${todo.title}"\`
* }
* )
* ```
*/
export const pikkuApprovalDescription = <In = unknown, RequiredServices extends Services = Services>(
fn: PikkuApprovalDescription<In, RequiredServices>
): PikkuApprovalDescription<In, RequiredServices> => {
return fn
}
/**
* A sessionless API function that doesn't require user authentication.
* Use this for public endpoints, health checks, or operations that don't need user context.
*
* @template In - The input type
* @template Out - The output type that the function returns
* @template RequiredServices - Services required by this function
*/
export type PikkuFunctionSessionless<
In = unknown,
Out = never,
RequiredWires extends keyof PikkuWire = never,
RequiredServices extends Services = Services
> = CorePikkuFunctionSessionless<
In,
Out,
RequiredServices,
Session,
PickRequired<PikkuWire<In, Out, false, Session, TypedPikkuRPC, null, any, TypedWorkflow>, RequiredWires>
>
/**
* A session-aware API function that requires user authentication.
* Use this for protected endpoints that need access to user session data.
*
* @template In - The input type
* @template Out - The output type that the function returns
* @template RequiredServices - Services required by this function
*/
export type PikkuFunction<
In = unknown,
Out = never,
RequiredWires extends keyof PikkuWire = 'session',
RequiredServices extends Services = Services
> = CorePikkuFunction<
In,
Out,
RequiredServices,
Session,
PickRequired<PikkuWire<In, Out, true, Session, TypedPikkuRPC, null, any, TypedWorkflow>, RequiredWires>
>
/**
* Helper type to infer the output type from a Standard Schema
*/
export type InferSchemaOutput<T> = T extends StandardSchemaV1<any, infer Output> ? Output : never
/**
* Configuration object for Pikku functions with optional middleware, permissions, tags, and documentation.
* This type wraps CorePikkuFunctionConfig with the user's custom types.
*
* @template In - The input type
* @template Out - The output type
* @template PikkuFunc - The function type (can be narrowed to PikkuFunction or PikkuFunctionSessionless)
*/
export type PikkuFunctionConfig<
In = unknown,
Out = unknown,
RequiredWires extends keyof PikkuWire = never,
PikkuFunc extends PikkuFunction<In, Out, RequiredWires> | PikkuFunctionSessionless<In, Out, RequiredWires> = PikkuFunction<In, Out, RequiredWires> | PikkuFunctionSessionless<In, Out, RequiredWires>,
InputSchema extends StandardSchemaV1 | undefined = undefined,
OutputSchema extends StandardSchemaV1 | undefined = undefined
> = CorePikkuFunctionConfig<PikkuFunc, PikkuPermission<In>, PikkuMiddleware, InputSchema, OutputSchema>
/**
* Configuration object for Pikku functions with Zod schema validation.
* Use this when you want to define input/output schemas using Zod.
* Types are automatically inferred from the schemas.
*/
type SchemaInferred<S, Fallback = unknown> = S extends StandardSchemaV1
? InferSchemaOutput<S>
: Fallback
/**
* Schema-overload variant for pikkuFunc. Derived from CorePikkuFunctionConfig
* so adding a field on the core type automatically propagates here.
*/
export type PikkuFunctionConfigWithSchema<
InputSchema extends StandardSchemaV1 | undefined = undefined,
OutputSchema extends StandardSchemaV1 | undefined = undefined,
RequiredWires extends keyof PikkuWire = never
> = Omit<
CorePikkuFunctionConfig<
| PikkuFunction<SchemaInferred<InputSchema>, SchemaInferred<OutputSchema>, RequiredWires>
| PikkuFunctionSessionless<SchemaInferred<InputSchema>, SchemaInferred<OutputSchema>, RequiredWires>
>,
'func' | 'input' | 'output' | 'permissions' | 'approvalDescription'
> & {
func:
| PikkuFunction<SchemaInferred<InputSchema>, SchemaInferred<OutputSchema>, RequiredWires>
| PikkuFunctionSessionless<SchemaInferred<InputSchema>, SchemaInferred<OutputSchema>, RequiredWires>
input?: InputSchema
output?: OutputSchema
permissions?: InputSchema extends StandardSchemaV1
? CorePermissionGroup<PikkuPermission<InferSchemaOutput<InputSchema>>>
: undefined
approvalDescription?: InputSchema extends StandardSchemaV1
? PikkuApprovalDescription<InferSchemaOutput<InputSchema>>
: never
}
/**
* Creates a Pikku function that can be either session-aware or sessionless.
* This is the main function wrapper for creating API endpoints.
*
* Supports two patterns:
* 1. Generic types: `pikkuFunc<Input, Output>({ func: ... })`
* 2. Zod schemas: `pikkuFunc({ input: z.object(...), output: z.object(...), func: ... })`
*
* @template In - Input type for the function (inferred from schema if provided)
* @template Out - Output type for the function (inferred from schema if provided)
* @param func - Function definition, either direct function or configuration object
* @returns The normalized configuration object
*
* @example
* ```typescript
* // Pattern 1: Using generic types
* const createUser = pikkuFunc<{name: string, email: string}, {id: number}>({
* func: async ({db}, input) => {
* const user = await db.users.create(input)
* return { id: user.id }
* }
* })
*
* // Pattern 2: Using Zod schemas (types inferred automatically)
* const createUserInput = z.object({ name: z.string(), email: z.string() })
* const createUserOutput = z.object({ id: z.number() })
*
* const createUser = pikkuFunc({
* input: createUserInput,
* output: createUserOutput,
* func: async ({db}, input) => {
* // input is typed as { name: string, email: string }
* const user = await db.users.create(input)
* return { id: user.id } // must match output schema
* }
* })
* ```
*/
export function pikkuFunc<
InputSchema extends StandardSchemaV1 | undefined = undefined,
OutputSchema extends StandardSchemaV1 | undefined = undefined
>(
config: PikkuFunctionConfigWithSchema<InputSchema, OutputSchema, 'session' | 'rpc'>
): PikkuFunctionConfig<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'session' | 'rpc'>
export function pikkuFunc<In, Out = unknown>(
func:
| PikkuFunction<In, Out, 'session' | 'rpc'>
| PikkuFunctionConfig<In, Out, 'session' | 'rpc'>
): PikkuFunctionConfig<In, Out, 'session' | 'rpc'>
export function pikkuFunc(func: any) {
return typeof func === 'function' ? { func } : func
}
export type PikkuListFunction<
F extends Record<string, unknown> = {},
Row = unknown,
S extends string = never
> =
| PikkuFunction<ListInput<F, S>, ListOutput<Row>, 'session' | 'rpc'>
| PikkuFunctionSessionless<
ListInput<F, S>,
ListOutput<Row>,
'session' | 'rpc'
>
export const pikkuListFunc = <
F extends Record<string, unknown> = {},
Row = unknown,
S extends string = never
>(
config: PikkuFunctionConfig<
ListInput<F, S>,
ListOutput<Row>,
'session' | 'rpc'
>
): PikkuFunctionConfig<ListInput<F, S>, ListOutput<Row>, 'session' | 'rpc'> => {
return pikkuFunc(config)
}
/**
* Configuration object for sessionless Pikku functions with Zod schema validation.
*/
/**
* Schema-overload variant for pikkuSessionlessFunc. Derived from
* CorePikkuFunctionConfig to stay in sync with the generic-typed config.
*/
export type PikkuFunctionSessionlessConfigWithSchema<
InputSchema extends StandardSchemaV1 | undefined = undefined,
OutputSchema extends StandardSchemaV1 | undefined = undefined,
RequiredWires extends keyof PikkuWire = never
> = Omit<
CorePikkuFunctionConfig<
PikkuFunctionSessionless<SchemaInferred<InputSchema>, SchemaInferred<OutputSchema>, RequiredWires>
>,
'func' | 'input' | 'output' | 'permissions' | 'approvalDescription'
> & {
func: PikkuFunctionSessionless<
SchemaInferred<InputSchema>,
SchemaInferred<OutputSchema>,
RequiredWires
>
input?: InputSchema
output?: OutputSchema
permissions?: InputSchema extends StandardSchemaV1
? CorePermissionGroup<PikkuPermission<InferSchemaOutput<InputSchema>>>
: undefined
approvalDescription?: InputSchema extends StandardSchemaV1
? PikkuApprovalDescription<InferSchemaOutput<InputSchema>>
: never
}
/**
* Creates a sessionless Pikku function that doesn't require user authentication.
* Use this for public endpoints, webhooks, or background tasks.
*
* Supports two patterns:
* 1. Generic types: `pikkuSessionlessFunc<Input, Output>({ func: ... })`
* 2. Zod schemas: `pikkuSessionlessFunc({ input: z.object(...), func: ... })`
*
* @template In - Input type for the function (inferred from schema if provided)
* @template Out - Output type for the function (inferred from schema if provided)
* @param func - Function definition, either direct function or configuration object
* @returns The normalized configuration object
*
* @example
* ```typescript
* // Pattern 1: Using generic types
* const healthCheck = pikkuSessionlessFunc<void, {status: string}>({
* func: async ({logger}) => {
* return { status: 'healthy' }
* }
* })
*
* // Pattern 2: Using Zod schemas
* const greetInput = z.object({ name: z.string() })
* const greetOutput = z.object({ message: z.string() })
*
* const greet = pikkuSessionlessFunc({
* input: greetInput,
* output: greetOutput,
* func: async (_services, { name }) => {
* return { message: `Hello, ${name}!` }
* }
* })
* ```
*/
export function pikkuSessionlessFunc<
InputSchema extends StandardSchemaV1 | undefined = undefined,
OutputSchema extends StandardSchemaV1 | undefined = undefined
>(
config: PikkuFunctionSessionlessConfigWithSchema<InputSchema, OutputSchema, 'session' | 'rpc'>
): PikkuFunctionConfig<InputSchema extends StandardSchemaV1 ? InferSchemaOutput<InputSchema> : unknown, OutputSchema extends StandardSchemaV1 ? InferSchemaOutput<OutputSchema> : unknown, 'session' | 'rpc'>
export function pikkuSessionlessFunc<In, Out = unknown>(
func:
| PikkuFunctionSessionless<In, Out, 'session' | 'rpc'>
| PikkuFunctionConfig<In, Out, 'session' | 'rpc'>
): PikkuFunctionConfig<In, Out, 'session' | 'rpc'>
export function pikkuSessionlessFunc(func: any) {
return typeof func === 'function' ? { func } : func
}
/**
* Creates a function that takes no input and returns no output.
* Useful for health checks, triggers, or cleanup operations.
*
* @param func - Function definition, either direct function or configuration object
* @returns The normalized configuration object
*
* @example
* ```typescript
* const cleanupTempFiles = pikkuVoidFunc(async ({fileSystem, logger}) => {
* logger.info('Starting cleanup of temporary files')
* await fileSystem.deleteDirectory('/tmp/uploads')
* logger.info('Cleanup completed')
* })
* ```
*/
export const pikkuVoidFunc = (
func:
| PikkuFunctionSessionless<void, void, 'session' | 'rpc'>
| PikkuFunctionConfig<void, void, 'session' | 'rpc'>
) => {
return typeof func === 'function' ? { func } : func
}
/**
* References a registered function by name for use in any wiring.
* Works for both local and addon functions — resolves via RPC at runtime.
*
* @template Name - The function name (must be a key in FlattenedRPCMap)
* @param rpcName - The name of the function to reference
* @returns A Pikku function config that proxies calls via RPC
*
* @example
* ```typescript
* // Use in agent tools
* tools: [ref('todos:listTodos'), ref('myLocalFunc')]
*
* // Use in HTTP wiring
* wireHTTP({ route: '/greet', method: 'post', func: ref('greet') })
* ```
*/
export const ref = <Name extends keyof FlattenedRPCMap>(
rpcName: Name
): PikkuFunctionConfig<
FlattenedRPCMap[Name]['input'],
FlattenedRPCMap[Name]['output'],
'session' | 'rpc'
> => {
return {
func: async (_services: any, data: FlattenedRPCMap[Name]['input'], { rpc }: any) => {
return rpc.invoke(rpcName, data)
}
} as PikkuFunctionConfig<
FlattenedRPCMap[Name]['input'],
FlattenedRPCMap[Name]['output'],
'session' | 'rpc'
>
}
/**
* Creates a Pikku config factory.
* Use this to define your application's configuration factory.
*
* @param func - Config factory function that returns your application's config
* @returns The config factory function
*
* @example
* ```typescript
* export const createConfig = pikkuConfig(async () => {
* return {
* apiUrl: process.env.API_URL || 'http://localhost:3000',
* dbUrl: process.env.DATABASE_URL
* }
* })
* ```
*/
export const pikkuConfig = (
func: (variables?: any, ...args: any[]) => Promise<Config>
) => func
/**
* Creates a Pikku singleton services factory.
* Use this to define services that are created once and shared across all requests.
*
* @param func - Singleton services factory function
* @returns The singleton services factory function
*
* @example
* ```typescript
* export const createSingletonServices = pikkuServices(async (config, existingServices) => {
* return {
* config,
* logger: new CustomLogger(),
* db: await createDatabaseConnection(config.dbUrl)
* }
* })
* ```
*/
export const pikkuServices = (
func: (config: Config, existingServices: Partial<SingletonServices>) => Promise<RequiredSingletonServices>
) => {
return async (config: Config, existingServices: Partial<SingletonServices> = {}) => {
const services = await func(config, existingServices)
__pikkuState(null, 'package', 'singletonServices', services as any)
return services
}
}
/**
* Creates a Pikku wire services factory.
* Use this to define services that are created per-request/session.
*
* @param func - Wire services factory function
* @returns The wire services factory function
*
* @example
* ```typescript
* export const createWireServices = pikkuWireServices(async (services, wire) => {
* const session = await wire.session?.get()
* return {
* userCache: new UserCache(session?.userId)
* }
* })
* ```
*/
export const pikkuWireServices = (
func: (
services: SingletonServices,
wire: any
) => Promise<RequiredWireServices>
): CreateWireServices => {
const factories = __pikkuState(null, 'package', 'factories')
__pikkuState(null, 'package', 'factories', { ...factories, createWireServices: func as any })
return func as unknown as CreateWireServices
}
/**
* Tag-scoped middleware. Applies to any wiring that carries the matching tag.
*
* @example
* addTagMiddleware('admin', [adminMiddleware])
*/
export const addTagMiddleware = (tag: string, middleware: PikkuMiddleware[]) => {
addTagMiddlewareCore(tag, middleware as any, null)
}
/**
* Wire-agnostic global middleware. Runs at the top of every wiring's
* middleware chain — before wire-, tag-, and function-level entries.
*
* Resolution order: global -> wire -> tag -> function.
*
* @example
* addGlobalMiddleware([telemetryMiddleware])
*/
export const addGlobalMiddleware = (middleware: PikkuMiddleware[]) => {
addGlobalMiddlewareCore(middleware as any, null)
}
/**
* Tag-scoped permissions. Applies to any wiring that carries the matching tag.
*
* @example
* addTagPermission('admin', [adminPermission])
*/
export const addTagPermission = <In = unknown>(tag: string, permissions: CorePermissionGroup<PikkuPermission<In>> | PikkuPermission<In>[]) => {
addTagPermissionCore(tag, permissions as any, null)
}
/**
* Wire-agnostic global permissions. Runs at the top of every wiring's
* permission resolution — before wire-, tag-, and function-level entries.
*
* Resolution order: global -> wire -> tag -> function.
*
* @example
* addGlobalPermission([signedInUser])
*/
export const addGlobalPermission = <In = unknown>(permissions: CorePermissionGroup<PikkuPermission<In>> | PikkuPermission<In>[]) => {
addGlobalPermissionCore(permissions as any, null)
}
export { wireAddon } from '@pikku/core/rpc'
export type { WireAddonConfig } from '@pikku/core/rpc'

View File

@@ -0,0 +1,728 @@
{
"pikkuConsoleSetSecret": {
"pikkuFuncId": "pikkuConsoleSetSecret",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "pikkuConsoleSetSecret",
"services": {
"optimized": true,
"services": [
"secrets"
]
},
"inputSchemaName": "PikkuConsoleSetSecretInput",
"outputSchemaName": "PikkuConsoleSetSecretOutput",
"inputs": [
"PikkuConsoleSetSecretInput"
],
"outputs": [
"PikkuConsoleSetSecretOutput"
],
"expose": true,
"implementationHash": "c33e04e6aff5ca2e",
"tags": [
"pikku"
],
"description": "Set the value of a secret",
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/console.gen.ts",
"exportedName": "pikkuConsoleSetSecret",
"contractHash": "fcb625d86fe88b8f",
"inputHash": "a0c8a043",
"outputHash": "57702b41"
},
"pikkuConsoleGetVariable": {
"pikkuFuncId": "pikkuConsoleGetVariable",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "pikkuConsoleGetVariable",
"services": {
"optimized": true,
"services": [
"variables"
]
},
"inputSchemaName": "PikkuConsoleGetVariableInput",
"outputSchemaName": "PikkuConsoleGetVariableOutput",
"inputs": [
"PikkuConsoleGetVariableInput"
],
"outputs": [
"PikkuConsoleGetVariableOutput"
],
"expose": true,
"implementationHash": "45c792dfb6f8b1a5",
"tags": [
"pikku"
],
"description": "Get the current value of a variable",
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/console.gen.ts",
"exportedName": "pikkuConsoleGetVariable",
"contractHash": "3b66dda4d82fa48a",
"inputHash": "ce4b2e06",
"outputHash": "f54f1059"
},
"pikkuConsoleSetVariable": {
"pikkuFuncId": "pikkuConsoleSetVariable",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "pikkuConsoleSetVariable",
"services": {
"optimized": true,
"services": [
"variables"
]
},
"inputSchemaName": "PikkuConsoleSetVariableInput",
"outputSchemaName": "PikkuConsoleSetVariableOutput",
"inputs": [
"PikkuConsoleSetVariableInput"
],
"outputs": [
"PikkuConsoleSetVariableOutput"
],
"expose": true,
"implementationHash": "f6806d8dc9d50055",
"tags": [
"pikku"
],
"description": "Set the value of a variable",
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/console.gen.ts",
"exportedName": "pikkuConsoleSetVariable",
"contractHash": "9dda40633b173a08",
"inputHash": "d91a4af5",
"outputHash": "91fe907b"
},
"pikkuConsoleHasSecret": {
"pikkuFuncId": "pikkuConsoleHasSecret",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "pikkuConsoleHasSecret",
"services": {
"optimized": true,
"services": [
"secrets"
]
},
"inputSchemaName": "PikkuConsoleHasSecretInput",
"outputSchemaName": "PikkuConsoleHasSecretOutput",
"inputs": [
"PikkuConsoleHasSecretInput"
],
"outputs": [
"PikkuConsoleHasSecretOutput"
],
"expose": true,
"implementationHash": "b8c818fb2291b032",
"tags": [
"pikku"
],
"description": "Check if a secret exists without reading its value",
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/console.gen.ts",
"exportedName": "pikkuConsoleHasSecret",
"contractHash": "eeff751c00461679",
"inputHash": "a67adc7c",
"outputHash": "1da66eac"
},
"pikkuConsoleGetSecret": {
"pikkuFuncId": "pikkuConsoleGetSecret",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "pikkuConsoleGetSecret",
"services": {
"optimized": true,
"services": [
"secrets"
]
},
"inputSchemaName": "PikkuConsoleGetSecretInput",
"outputSchemaName": "PikkuConsoleGetSecretOutput",
"inputs": [
"PikkuConsoleGetSecretInput"
],
"outputs": [
"PikkuConsoleGetSecretOutput"
],
"expose": true,
"implementationHash": "5c1e39a4b0cc46a1",
"tags": [
"pikku"
],
"description": "Get the current value of a secret",
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/console.gen.ts",
"exportedName": "pikkuConsoleGetSecret",
"contractHash": "d651ad93642eaeed",
"inputHash": "329251da",
"outputHash": "fb675a26"
},
"http:get:/workflow-run/:runId/stream": {
"pikkuFuncId": "http:get:/workflow-run/:runId/stream",
"functionType": "inline",
"name": "/workflow-run/:runId/stream",
"services": {
"optimized": false,
"services": []
},
"inputSchemaName": "StreamWorkflowRunInput",
"outputSchemaName": null,
"inputs": [
"StreamWorkflowRunInput"
],
"outputs": [],
"contractHash": "ab85b338c10d89e8",
"inputHash": "76838d3e",
"outputHash": "fc2fd4a0"
},
"remoteRPCHandler": {
"pikkuFuncId": "remoteRPCHandler",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "remoteRPCHandler",
"services": {
"optimized": true,
"services": []
},
"wires": {
"optimized": true,
"wires": [
"rpc"
]
},
"inputSchemaName": "RemoteRPCHandlerInput",
"outputSchemaName": null,
"inputs": [
"RemoteRPCHandlerInput"
],
"outputs": [],
"remote": true,
"implementationHash": "105018a823bdb551",
"tags": [
"pikku"
],
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/rpc-remote.gen.ts",
"exportedName": "remoteRPCHandler"
},
"workflowStarter": {
"pikkuFuncId": "workflowStarter",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "workflowStarter",
"services": {
"optimized": true,
"services": [
"workflowService"
]
},
"wires": {
"optimized": true,
"wires": [
"rpc"
]
},
"inputSchemaName": "WorkflowStarterInput",
"outputSchemaName": "WorkflowStarterOutput",
"inputs": [
"WorkflowStarterInput"
],
"outputs": [
"WorkflowStarterOutput"
],
"implementationHash": "153116058265d9d4",
"tags": [
"pikku"
],
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/workflow-routes.gen.ts",
"exportedName": "workflowStarter",
"contractHash": "770c10618e2d471b",
"inputHash": "73e8dfbd",
"outputHash": "8f2061b7"
},
"workflowRunner": {
"pikkuFuncId": "workflowRunner",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "workflowRunner",
"services": {
"optimized": true,
"services": [
"workflowService"
]
},
"wires": {
"optimized": true,
"wires": [
"rpc"
]
},
"inputSchemaName": "WorkflowRunnerInput",
"outputSchemaName": null,
"inputs": [
"WorkflowRunnerInput"
],
"outputs": [],
"implementationHash": "4807ebd71047f932",
"tags": [
"pikku"
],
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/workflow-routes.gen.ts",
"exportedName": "workflowRunner",
"contractHash": "ca617784c6d9ec8d",
"inputHash": "3c634245",
"outputHash": "316bdb87"
},
"workflowStatusChecker": {
"pikkuFuncId": "workflowStatusChecker",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "workflowStatusChecker",
"services": {
"optimized": true,
"services": [
"workflowService"
]
},
"inputSchemaName": "WorkflowStatusCheckerInput",
"outputSchemaName": "WorkflowRunStatus",
"inputs": [
"WorkflowStatusCheckerInput"
],
"outputs": [
"WorkflowRunStatus"
],
"implementationHash": "e4159df9ad118a6c",
"tags": [
"pikku"
],
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/workflow-routes.gen.ts",
"exportedName": "workflowStatusChecker",
"contractHash": "1bb7182390464bc0",
"inputHash": "458ba7ec",
"outputHash": "bc4d864f"
},
"workflowStatusStream": {
"pikkuFuncId": "workflowStatusStream",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "workflowStatusStream",
"services": {
"optimized": true,
"services": [
"workflowRunService"
]
},
"wires": {
"optimized": true,
"wires": [
"channel"
]
},
"inputSchemaName": "WorkflowStatusStreamInput",
"outputSchemaName": null,
"inputs": [
"WorkflowStatusStreamInput"
],
"outputs": [],
"implementationHash": "233d0f04a86a4237",
"tags": [
"pikku"
],
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/workflow-routes.gen.ts",
"exportedName": "workflowStatusStream",
"contractHash": "3b99296ba5064030",
"inputHash": "c6ee79ff",
"outputHash": "6180f124"
},
"workflowStatusStreamFull": {
"pikkuFuncId": "workflowStatusStreamFull",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "workflowStatusStreamFull",
"services": {
"optimized": true,
"services": [
"workflowRunService"
]
},
"wires": {
"optimized": true,
"wires": [
"channel"
]
},
"inputSchemaName": "WorkflowStatusStreamFullInput",
"outputSchemaName": null,
"inputs": [
"WorkflowStatusStreamFullInput"
],
"outputs": [],
"implementationHash": "aba5bf474596ee50",
"tags": [
"pikku"
],
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/workflow-routes.gen.ts",
"exportedName": "workflowStatusStreamFull",
"contractHash": "1e7b9f00193d10ae",
"inputHash": "2da90ff6",
"outputHash": "dcd02549"
},
"graphStarter": {
"pikkuFuncId": "graphStarter",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "graphStarter",
"services": {
"optimized": true,
"services": [
"workflowService"
]
},
"wires": {
"optimized": true,
"wires": [
"rpc"
]
},
"inputSchemaName": "GraphStarterInput",
"outputSchemaName": "GraphStarterOutput",
"inputs": [
"GraphStarterInput"
],
"outputs": [
"GraphStarterOutput"
],
"implementationHash": "d12993125e177525",
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/workflow-routes.gen.ts",
"exportedName": "graphStarter",
"contractHash": "422510c83dee8491",
"inputHash": "70868669",
"outputHash": "6f988595"
},
"rpcCaller": {
"pikkuFuncId": "rpcCaller",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "rpcCaller",
"services": {
"optimized": true,
"services": []
},
"wires": {
"optimized": true,
"wires": [
"rpc"
]
},
"inputSchemaName": "RpcCallerInput",
"outputSchemaName": null,
"inputs": [
"RpcCallerInput"
],
"outputs": [],
"implementationHash": "f92b60dda357f483",
"tags": [
"pikku"
],
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/rpc-public.gen.ts",
"exportedName": "rpcCaller",
"contractHash": "f6901fd233740bf4",
"inputHash": "a69cacc4",
"outputHash": "629e0b5a"
},
"agentCaller": {
"pikkuFuncId": "agentCaller",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "agentCaller",
"services": {
"optimized": true,
"services": []
},
"wires": {
"optimized": true,
"wires": [
"rpc"
]
},
"inputSchemaName": "AgentCallerInput",
"outputSchemaName": null,
"inputs": [
"AgentCallerInput"
],
"outputs": [],
"implementationHash": "4af198bf5bbd9bd8",
"tags": [
"pikku"
],
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/agent.gen.ts",
"exportedName": "agentCaller",
"contractHash": "76619abd99f23189",
"inputHash": "4d1eddbc",
"outputHash": "9a3af204"
},
"agentStreamCaller": {
"pikkuFuncId": "agentStreamCaller",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "agentStreamCaller",
"services": {
"optimized": true,
"services": []
},
"wires": {
"optimized": true,
"wires": [
"rpc"
]
},
"inputSchemaName": "AgentStreamCallerInput",
"outputSchemaName": null,
"inputs": [
"AgentStreamCallerInput"
],
"outputs": [],
"implementationHash": "92b3cd7c92fb3de4",
"tags": [
"pikku"
],
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/agent.gen.ts",
"exportedName": "agentStreamCaller",
"contractHash": "0e5c814e26d3294f",
"inputHash": "773d4d52",
"outputHash": "641a263e"
},
"agentApproveCaller": {
"pikkuFuncId": "agentApproveCaller",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "agentApproveCaller",
"services": {
"optimized": true,
"services": []
},
"wires": {
"optimized": true,
"wires": [
"rpc"
]
},
"inputSchemaName": "AgentApproveCallerInput",
"outputSchemaName": null,
"inputs": [
"AgentApproveCallerInput"
],
"outputs": [],
"implementationHash": "ae5d4bfb0293119a",
"tags": [
"pikku"
],
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/agent.gen.ts",
"exportedName": "agentApproveCaller",
"contractHash": "83a9293c4b51d65c",
"inputHash": "f1628b7c",
"outputHash": "ef71f08a"
},
"agentResumeCaller": {
"pikkuFuncId": "agentResumeCaller",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "agentResumeCaller",
"services": {
"optimized": true,
"services": []
},
"wires": {
"optimized": true,
"wires": [
"rpc"
]
},
"inputSchemaName": "AgentResumeCallerInput",
"outputSchemaName": null,
"inputs": [
"AgentResumeCallerInput"
],
"outputs": [],
"implementationHash": "d17ccf1259ba4a60",
"tags": [
"pikku"
],
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/agent.gen.ts",
"exportedName": "agentResumeCaller",
"contractHash": "0648ffb2cfa9d861",
"inputHash": "f25b50dd",
"outputHash": "ee89e454"
},
"getAgentThreads": {
"pikkuFuncId": "getAgentThreads",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "getAgentThreads",
"services": {
"optimized": true,
"services": [
"agentRunService"
]
},
"inputSchemaName": "GetAgentThreadsInput",
"outputSchemaName": "GetAgentThreadsOutput",
"inputs": [
"GetAgentThreadsInput"
],
"outputs": [
"GetAgentThreadsOutput"
],
"expose": true,
"implementationHash": "3823f8f3911c48e2",
"title": "Get Agent Threads",
"tags": [
"pikku",
"pikku:agent"
],
"description": "Returns a list of AI agent threads from storage. Accepts optional filters: agentName, resourceId, limit, and offset for pagination.",
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/agent.gen.ts",
"exportedName": "getAgentThreads",
"contractHash": "d629208347702d27",
"inputHash": "41f5350c",
"outputHash": "6598bdff"
},
"getAgentThreadMessages": {
"pikkuFuncId": "getAgentThreadMessages",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "getAgentThreadMessages",
"services": {
"optimized": true,
"services": [
"agentRunService"
]
},
"inputSchemaName": "GetAgentThreadMessagesInput",
"outputSchemaName": "GetAgentThreadMessagesOutput",
"inputs": [
"GetAgentThreadMessagesInput"
],
"outputs": [
"GetAgentThreadMessagesOutput"
],
"expose": true,
"implementationHash": "dffe581287205c6e",
"title": "Get Agent Thread Messages",
"tags": [
"pikku",
"pikku:agent"
],
"description": "Returns all messages for a given AI agent thread, ordered by creation time.",
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/agent.gen.ts",
"exportedName": "getAgentThreadMessages",
"contractHash": "44e43e96024680f3",
"inputHash": "a158e652",
"outputHash": "04960780"
},
"getAgentThreadRuns": {
"pikkuFuncId": "getAgentThreadRuns",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "getAgentThreadRuns",
"services": {
"optimized": true,
"services": [
"agentRunService"
]
},
"inputSchemaName": "GetAgentThreadRunsInput",
"outputSchemaName": "GetAgentThreadRunsOutput",
"inputs": [
"GetAgentThreadRunsInput"
],
"outputs": [
"GetAgentThreadRunsOutput"
],
"expose": true,
"implementationHash": "2e83ec1d0e08772d",
"title": "Get Agent Thread Runs",
"tags": [
"pikku",
"pikku:agent"
],
"description": "Returns the run history for a given AI agent thread, ordered by creation time.",
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/agent.gen.ts",
"exportedName": "getAgentThreadRuns",
"contractHash": "c51892c37c245537",
"inputHash": "fb544997",
"outputHash": "6352aca1"
},
"deleteAgentThread": {
"pikkuFuncId": "deleteAgentThread",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "deleteAgentThread",
"services": {
"optimized": true,
"services": [
"agentRunService"
]
},
"inputSchemaName": "DeleteAgentThreadInput",
"outputSchemaName": "DeleteAgentThreadOutput",
"inputs": [
"DeleteAgentThreadInput"
],
"outputs": [
"DeleteAgentThreadOutput"
],
"expose": true,
"implementationHash": "7c0d0fb40a42fc20",
"title": "Delete Agent Thread",
"tags": [
"pikku",
"pikku:agent"
],
"description": "Deletes an AI agent thread and all of its persisted state.",
"isDirectFunction": false,
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/agent.gen.ts",
"exportedName": "deleteAgentThread",
"contractHash": "b970011db1e0230e",
"inputHash": "3ed36ee5",
"outputHash": "a2094cc1"
}
}

View File

@@ -0,0 +1,404 @@
{
"pikkuConsoleSetSecret": {
"pikkuFuncId": "pikkuConsoleSetSecret",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "pikkuConsoleSetSecret",
"inputSchemaName": "PikkuConsoleSetSecretInput",
"outputSchemaName": "PikkuConsoleSetSecretOutput",
"inputs": [
"PikkuConsoleSetSecretInput"
],
"outputs": [
"PikkuConsoleSetSecretOutput"
],
"expose": true,
"implementationHash": "c33e04e6aff5ca2e",
"contractHash": "fcb625d86fe88b8f",
"inputHash": "a0c8a043",
"outputHash": "57702b41"
},
"pikkuConsoleGetVariable": {
"pikkuFuncId": "pikkuConsoleGetVariable",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "pikkuConsoleGetVariable",
"inputSchemaName": "PikkuConsoleGetVariableInput",
"outputSchemaName": "PikkuConsoleGetVariableOutput",
"inputs": [
"PikkuConsoleGetVariableInput"
],
"outputs": [
"PikkuConsoleGetVariableOutput"
],
"expose": true,
"implementationHash": "45c792dfb6f8b1a5",
"contractHash": "3b66dda4d82fa48a",
"inputHash": "ce4b2e06",
"outputHash": "f54f1059"
},
"pikkuConsoleSetVariable": {
"pikkuFuncId": "pikkuConsoleSetVariable",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "pikkuConsoleSetVariable",
"inputSchemaName": "PikkuConsoleSetVariableInput",
"outputSchemaName": "PikkuConsoleSetVariableOutput",
"inputs": [
"PikkuConsoleSetVariableInput"
],
"outputs": [
"PikkuConsoleSetVariableOutput"
],
"expose": true,
"implementationHash": "f6806d8dc9d50055",
"contractHash": "9dda40633b173a08",
"inputHash": "d91a4af5",
"outputHash": "91fe907b"
},
"pikkuConsoleHasSecret": {
"pikkuFuncId": "pikkuConsoleHasSecret",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "pikkuConsoleHasSecret",
"inputSchemaName": "PikkuConsoleHasSecretInput",
"outputSchemaName": "PikkuConsoleHasSecretOutput",
"inputs": [
"PikkuConsoleHasSecretInput"
],
"outputs": [
"PikkuConsoleHasSecretOutput"
],
"expose": true,
"implementationHash": "b8c818fb2291b032",
"contractHash": "eeff751c00461679",
"inputHash": "a67adc7c",
"outputHash": "1da66eac"
},
"pikkuConsoleGetSecret": {
"pikkuFuncId": "pikkuConsoleGetSecret",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "pikkuConsoleGetSecret",
"inputSchemaName": "PikkuConsoleGetSecretInput",
"outputSchemaName": "PikkuConsoleGetSecretOutput",
"inputs": [
"PikkuConsoleGetSecretInput"
],
"outputs": [
"PikkuConsoleGetSecretOutput"
],
"expose": true,
"implementationHash": "5c1e39a4b0cc46a1",
"contractHash": "d651ad93642eaeed",
"inputHash": "329251da",
"outputHash": "fb675a26"
},
"http:get:/workflow-run/:runId/stream": {
"pikkuFuncId": "http:get:/workflow-run/:runId/stream",
"functionType": "inline",
"name": "/workflow-run/:runId/stream",
"inputSchemaName": "StreamWorkflowRunInput",
"outputSchemaName": null,
"inputs": [
"StreamWorkflowRunInput"
],
"outputs": [],
"contractHash": "ab85b338c10d89e8",
"inputHash": "76838d3e",
"outputHash": "fc2fd4a0"
},
"remoteRPCHandler": {
"pikkuFuncId": "remoteRPCHandler",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "remoteRPCHandler",
"inputSchemaName": "RemoteRPCHandlerInput",
"outputSchemaName": null,
"inputs": [
"RemoteRPCHandlerInput"
],
"outputs": [],
"remote": true,
"implementationHash": "105018a823bdb551"
},
"workflowStarter": {
"pikkuFuncId": "workflowStarter",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "workflowStarter",
"inputSchemaName": "WorkflowStarterInput",
"outputSchemaName": "WorkflowStarterOutput",
"inputs": [
"WorkflowStarterInput"
],
"outputs": [
"WorkflowStarterOutput"
],
"implementationHash": "153116058265d9d4",
"contractHash": "770c10618e2d471b",
"inputHash": "73e8dfbd",
"outputHash": "8f2061b7"
},
"workflowRunner": {
"pikkuFuncId": "workflowRunner",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "workflowRunner",
"inputSchemaName": "WorkflowRunnerInput",
"outputSchemaName": null,
"inputs": [
"WorkflowRunnerInput"
],
"outputs": [],
"implementationHash": "4807ebd71047f932",
"contractHash": "ca617784c6d9ec8d",
"inputHash": "3c634245",
"outputHash": "316bdb87"
},
"workflowStatusChecker": {
"pikkuFuncId": "workflowStatusChecker",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "workflowStatusChecker",
"inputSchemaName": "WorkflowStatusCheckerInput",
"outputSchemaName": "WorkflowRunStatus",
"inputs": [
"WorkflowStatusCheckerInput"
],
"outputs": [
"WorkflowRunStatus"
],
"implementationHash": "e4159df9ad118a6c",
"contractHash": "1bb7182390464bc0",
"inputHash": "458ba7ec",
"outputHash": "bc4d864f"
},
"workflowStatusStream": {
"pikkuFuncId": "workflowStatusStream",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "workflowStatusStream",
"inputSchemaName": "WorkflowStatusStreamInput",
"outputSchemaName": null,
"inputs": [
"WorkflowStatusStreamInput"
],
"outputs": [],
"implementationHash": "233d0f04a86a4237",
"contractHash": "3b99296ba5064030",
"inputHash": "c6ee79ff",
"outputHash": "6180f124"
},
"workflowStatusStreamFull": {
"pikkuFuncId": "workflowStatusStreamFull",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "workflowStatusStreamFull",
"inputSchemaName": "WorkflowStatusStreamFullInput",
"outputSchemaName": null,
"inputs": [
"WorkflowStatusStreamFullInput"
],
"outputs": [],
"implementationHash": "aba5bf474596ee50",
"contractHash": "1e7b9f00193d10ae",
"inputHash": "2da90ff6",
"outputHash": "dcd02549"
},
"graphStarter": {
"pikkuFuncId": "graphStarter",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "graphStarter",
"inputSchemaName": "GraphStarterInput",
"outputSchemaName": "GraphStarterOutput",
"inputs": [
"GraphStarterInput"
],
"outputs": [
"GraphStarterOutput"
],
"implementationHash": "d12993125e177525",
"contractHash": "422510c83dee8491",
"inputHash": "70868669",
"outputHash": "6f988595"
},
"rpcCaller": {
"pikkuFuncId": "rpcCaller",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "rpcCaller",
"inputSchemaName": "RpcCallerInput",
"outputSchemaName": null,
"inputs": [
"RpcCallerInput"
],
"outputs": [],
"implementationHash": "f92b60dda357f483",
"contractHash": "f6901fd233740bf4",
"inputHash": "a69cacc4",
"outputHash": "629e0b5a"
},
"agentCaller": {
"pikkuFuncId": "agentCaller",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "agentCaller",
"inputSchemaName": "AgentCallerInput",
"outputSchemaName": null,
"inputs": [
"AgentCallerInput"
],
"outputs": [],
"implementationHash": "4af198bf5bbd9bd8",
"contractHash": "76619abd99f23189",
"inputHash": "4d1eddbc",
"outputHash": "9a3af204"
},
"agentStreamCaller": {
"pikkuFuncId": "agentStreamCaller",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "agentStreamCaller",
"inputSchemaName": "AgentStreamCallerInput",
"outputSchemaName": null,
"inputs": [
"AgentStreamCallerInput"
],
"outputs": [],
"implementationHash": "92b3cd7c92fb3de4",
"contractHash": "0e5c814e26d3294f",
"inputHash": "773d4d52",
"outputHash": "641a263e"
},
"agentApproveCaller": {
"pikkuFuncId": "agentApproveCaller",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "agentApproveCaller",
"inputSchemaName": "AgentApproveCallerInput",
"outputSchemaName": null,
"inputs": [
"AgentApproveCallerInput"
],
"outputs": [],
"implementationHash": "ae5d4bfb0293119a",
"contractHash": "83a9293c4b51d65c",
"inputHash": "f1628b7c",
"outputHash": "ef71f08a"
},
"agentResumeCaller": {
"pikkuFuncId": "agentResumeCaller",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "agentResumeCaller",
"inputSchemaName": "AgentResumeCallerInput",
"outputSchemaName": null,
"inputs": [
"AgentResumeCallerInput"
],
"outputs": [],
"implementationHash": "d17ccf1259ba4a60",
"contractHash": "0648ffb2cfa9d861",
"inputHash": "f25b50dd",
"outputHash": "ee89e454"
},
"getAgentThreads": {
"pikkuFuncId": "getAgentThreads",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "getAgentThreads",
"inputSchemaName": "GetAgentThreadsInput",
"outputSchemaName": "GetAgentThreadsOutput",
"inputs": [
"GetAgentThreadsInput"
],
"outputs": [
"GetAgentThreadsOutput"
],
"expose": true,
"implementationHash": "3823f8f3911c48e2",
"contractHash": "d629208347702d27",
"inputHash": "41f5350c",
"outputHash": "6598bdff"
},
"getAgentThreadMessages": {
"pikkuFuncId": "getAgentThreadMessages",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "getAgentThreadMessages",
"inputSchemaName": "GetAgentThreadMessagesInput",
"outputSchemaName": "GetAgentThreadMessagesOutput",
"inputs": [
"GetAgentThreadMessagesInput"
],
"outputs": [
"GetAgentThreadMessagesOutput"
],
"expose": true,
"implementationHash": "dffe581287205c6e",
"contractHash": "44e43e96024680f3",
"inputHash": "a158e652",
"outputHash": "04960780"
},
"getAgentThreadRuns": {
"pikkuFuncId": "getAgentThreadRuns",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "getAgentThreadRuns",
"inputSchemaName": "GetAgentThreadRunsInput",
"outputSchemaName": "GetAgentThreadRunsOutput",
"inputs": [
"GetAgentThreadRunsInput"
],
"outputs": [
"GetAgentThreadRunsOutput"
],
"expose": true,
"implementationHash": "2e83ec1d0e08772d",
"contractHash": "c51892c37c245537",
"inputHash": "fb544997",
"outputHash": "6352aca1"
},
"deleteAgentThread": {
"pikkuFuncId": "deleteAgentThread",
"functionType": "user",
"funcWrapper": "pikkuSessionlessFunc",
"sessionless": true,
"name": "deleteAgentThread",
"inputSchemaName": "DeleteAgentThreadInput",
"outputSchemaName": "DeleteAgentThreadOutput",
"inputs": [
"DeleteAgentThreadInput"
],
"outputs": [
"DeleteAgentThreadOutput"
],
"expose": true,
"implementationHash": "7c0d0fb40a42fc20",
"contractHash": "b970011db1e0230e",
"inputHash": "3ed36ee5",
"outputHash": "a2094cc1"
}
}

View File

@@ -0,0 +1,7 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
import { pikkuState } from '@pikku/core/internal'
import type { FunctionsMeta } from '@pikku/core'
import metaData from './pikku-functions-meta.gen.json' with { type: 'json' }
pikkuState(null, 'function', 'meta', metaData as FunctionsMeta)

View File

@@ -0,0 +1,26 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/* Import and register functions used by RPCs */
import { addFunction } from '@pikku/core/function'
import { deleteAgentThread } from '../../../../src/scaffold/agent.gen.js'
import { getAgentThreadMessages } from '../../../../src/scaffold/agent.gen.js'
import { getAgentThreadRuns } from '../../../../src/scaffold/agent.gen.js'
import { getAgentThreads } from '../../../../src/scaffold/agent.gen.js'
import { pikkuConsoleGetSecret } from '../../../../src/scaffold/console.gen.js'
import { pikkuConsoleGetVariable } from '../../../../src/scaffold/console.gen.js'
import { pikkuConsoleHasSecret } from '../../../../src/scaffold/console.gen.js'
import { pikkuConsoleSetSecret } from '../../../../src/scaffold/console.gen.js'
import { pikkuConsoleSetVariable } from '../../../../src/scaffold/console.gen.js'
import { remoteRPCHandler } from '../../../../src/scaffold/rpc-remote.gen.js'
addFunction('deleteAgentThread', deleteAgentThread)
addFunction('getAgentThreadMessages', getAgentThreadMessages)
addFunction('getAgentThreadRuns', getAgentThreadRuns)
addFunction('getAgentThreads', getAgentThreads)
addFunction('pikkuConsoleGetSecret', pikkuConsoleGetSecret)
addFunction('pikkuConsoleGetVariable', pikkuConsoleGetVariable)
addFunction('pikkuConsoleHasSecret', pikkuConsoleHasSecret)
addFunction('pikkuConsoleSetSecret', pikkuConsoleSetSecret)
addFunction('pikkuConsoleSetVariable', pikkuConsoleSetVariable)
addFunction('remoteRPCHandler', remoteRPCHandler)

View File

@@ -0,0 +1,147 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/**
* HTTP-specific type definitions for tree-shaking optimization
*/
import { AssertHTTPWiringParams, wireHTTP as wireHTTPCore, addHTTPMiddleware as addHTTPMiddlewareCore, addHTTPPermission as addHTTPPermissionCore, wireHTTPRoutes as wireHTTPRoutesCore, defineHTTPRoutes as defineHTTPRoutesCore } from '@pikku/core/http'
import type { PikkuFunction, PikkuFunctionSessionless, PikkuPermission, PikkuMiddleware, PikkuFunctionConfig } from '../function/pikku-function-types.gen.js'
import type { CoreHTTPFunctionWiring, HTTPMethod, HTTPRouteBaseConfig } from '@pikku/core/http'
/**
* Type definition for HTTP API wirings with type-safe path parameters.
* Supports both authenticated and unauthenticated functions.
*
* @template In - Input type for the HTTP wiring
* @template Out - Output type for the HTTP wiring
* @template Route - String literal type for the HTTP path (e.g., "/users/:id")
*/
type HTTPWiring<In, Out, Route extends string> = CoreHTTPFunctionWiring<In, Out, Route, PikkuFunction<In, Out, 'rpc' | 'session'>, PikkuFunctionSessionless<In, Out, 'rpc' | 'session'>, PikkuPermission<In>, PikkuMiddleware>
/**
* Registers an HTTP wiring with the Pikku framework.
*
* @template In - Input type for the HTTP wiring
* @template Out - Output type for the HTTP wiring
* @template Route - String literal type for the HTTP path (e.g., "/users/:id")
* @param httpWiring - HTTP wiring definition with handler, method, and optional middleware
*/
export const wireHTTP = <In, Out, Route extends string>(
httpWiring: HTTPWiring<In, Out, Route> & AssertHTTPWiringParams<In, Route>
) => {
wireHTTPCore(httpWiring as any)
}
/**
* Registers HTTP middleware either globally or for a specific route pattern.
*
* When a string route pattern is provided along with middleware, the middleware
* is applied only to that route. Otherwise, if an array is provided, it is treated
* as global middleware (applied to all routes).
*
* @param routeOrMiddleware - Either a global middleware array or a route pattern string
* @param middleware - The middleware array to apply when a route pattern is specified
*
* @example
* ```typescript
* // Add global HTTP middleware
* addHTTPMiddleware([authMiddleware, loggingMiddleware])
*
* // Add route-specific middleware
* addHTTPMiddleware('/api/admin/*', [adminAuthMiddleware])
* ```
*/
export const addHTTPMiddleware = (
routeOrMiddleware: PikkuMiddleware[] | string,
middleware?: PikkuMiddleware[]
) => {
addHTTPMiddlewareCore(routeOrMiddleware as any, middleware as any)
}
/**
* Registers HTTP permissions either globally or for a specific route pattern.
*
* When a string route pattern is provided along with permissions, the permissions
* are applied only to that route. Permissions can be passed as an array or as a
* permission group object.
*
* @param pattern - Route pattern string (e.g., '*' for all routes, '/api/*' for specific routes)
* @param permissions - The permissions to apply for the specified route pattern
*
* @example
* ```typescript
* // Add global HTTP permissions
* addHTTPPermission('*', { global: globalPermission })
*
* // Add route-specific permissions
* addHTTPPermission('/api/admin/*', { admin: adminPermission })
* ```
*/
export const addHTTPPermission = <In = unknown>(
pattern: string,
permissions: Record<string, PikkuPermission<In>> | PikkuPermission<In>[]
) => {
addHTTPPermissionCore(pattern, permissions as any)
}
/**
* Route configuration for wireHTTPRoutes with proper typing
*/
type HTTPRouteConfig = HTTPRouteBaseConfig & {
method: HTTPMethod
route: string
func: PikkuFunctionConfig<any, any, any, any, any, any>
auth?: boolean
permissions?: Record<string, PikkuPermission | PikkuPermission[]>
middleware?: PikkuMiddleware[]
sse?: boolean
}
/**
* Typed route map for wireHTTPRoutes
*/
type TypedHTTPRouteMap = {
[key: string]: HTTPRouteConfig | TypedHTTPRouteMap | TypedHTTPRouteContract
}
/**
* Typed route contract for defineHTTPRoutes
*/
type TypedHTTPRouteContract<T extends TypedHTTPRouteMap = TypedHTTPRouteMap> = TypedHTTPRoutesGroupConfig & {
routes: T
}
/**
* Group config with typed middleware/permissions
*/
type TypedHTTPRoutesGroupConfig = {
basePath?: string
tags?: string[]
auth?: boolean
middleware?: PikkuMiddleware[]
permissions?: Record<string, PikkuPermission | PikkuPermission[]>
}
/**
* Full config for wireHTTPRoutes
*/
type TypedWireHTTPRoutesConfig = TypedHTTPRoutesGroupConfig & {
routes: TypedHTTPRouteMap | HTTPRouteConfig[]
}
/**
* Type-safe helper for defining route contracts that can be composed.
*/
export function defineHTTPRoutes<T extends TypedHTTPRouteMap>(routes: T): TypedHTTPRouteContract<T>
export function defineHTTPRoutes<T extends TypedHTTPRouteMap>(config: TypedHTTPRoutesGroupConfig & { routes: T }): TypedHTTPRouteContract<T>
export function defineHTTPRoutes<T extends TypedHTTPRouteMap>(configOrRoutes: T | (TypedHTTPRoutesGroupConfig & { routes: T })): TypedHTTPRouteContract<T> {
return defineHTTPRoutesCore(configOrRoutes as any) as unknown as TypedHTTPRouteContract<T>
}
/**
* Wires multiple HTTP routes from a nested map or array configuration.
*/
export const wireHTTPRoutes = (config: TypedWireHTTPRoutesConfig): void => {
wireHTTPRoutesCore(config as any)
}

View File

@@ -0,0 +1,129 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/**
* This provides the structure needed for typescript to be aware of routes and their return types
*/
import type { StreamWorkflowRunInput } from '@pikku/addon-console/dist/.pikku/rpc/pikku-rpc-wirings-map.internal.gen'
import type { WorkflowRunStatus } from '@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; }
export type DeleteAgentThreadInput = { threadId: string; resourceId?: string; }
export type DeleteAgentThreadOutput = { deleted: boolean; }
export type GetAgentThreadMessagesInput = { threadId: string; resourceId?: string; }
export type GetAgentThreadMessagesOutput = any[]
export type GetAgentThreadRunsInput = { threadId: string; resourceId?: string; }
export type GetAgentThreadRunsOutput = any[]
export type GetAgentThreadsInput = { agentName?: string; resourceId?: string; limit?: number; offset?: number; }
export type GetAgentThreadsOutput = any[]
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 RemoteRPCHandlerInput = { rpcName: string; data?: unknown; }
export type RpcCallerInput = { rpcName: string; data?: unknown; }
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 StreamWorkflowRunInputParams = Pick<StreamWorkflowRunInput, 'runId'> & {}
export type StreamWorkflowRunInputBody = Omit<StreamWorkflowRunInput, 'runId'> & {}
export type RemoteRPCHandlerInputParams = Pick<RemoteRPCHandlerInput, 'rpcName'> & {}
export type RemoteRPCHandlerInputBody = Omit<RemoteRPCHandlerInput, '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'> & {}
export type RpcCallerInputParams = Pick<RpcCallerInput, 'rpcName'> & {}
export type RpcCallerInputBody = Omit<RpcCallerInput, 'rpcName'> & {}
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'> & {}
interface HTTPWiringHandler<I, O> {
input: I;
output: O;
}
export type HTTPWiringsMap = {
readonly '/workflow-run/:runId/stream': {
readonly GET: HTTPWiringHandler<StreamWorkflowRunInput, 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 '/remote/rpc/:rpcName': {
readonly POST: HTTPWiringHandler<RemoteRPCHandlerInput, 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>,
},
readonly '/rpc/:rpcName': {
readonly POST: HTTPWiringHandler<RpcCallerInput, 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>,
},
};
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,156 @@
{
"get": {
"/workflow-run/:runId/stream": {
"pikkuFuncId": "http:get:/workflow-run/:runId/stream",
"route": "/workflow-run/:runId/stream",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/console.gen.ts",
"method": "get",
"params": [
"runId"
],
"sse": true
},
"/workflow/:workflowName/status/:runId": {
"pikkuFuncId": "workflowStatusChecker",
"route": "/workflow/:workflowName/status/:runId",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/workflow-routes.gen.ts",
"method": "get",
"params": [
"workflowName",
"runId"
]
},
"/workflow/:workflowName/status/:runId/stream": {
"pikkuFuncId": "workflowStatusStream",
"route": "/workflow/:workflowName/status/:runId/stream",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/workflow-routes.gen.ts",
"method": "get",
"params": [
"workflowName",
"runId"
],
"sse": true
},
"/workflow/:workflowName/status/:runId/stream/full": {
"pikkuFuncId": "workflowStatusStreamFull",
"route": "/workflow/:workflowName/status/:runId/stream/full",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/workflow-routes.gen.ts",
"method": "get",
"params": [
"workflowName",
"runId"
],
"sse": true
}
},
"post": {
"/remote/rpc/:rpcName": {
"pikkuFuncId": "remoteRPCHandler",
"route": "/remote/rpc/:rpcName",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/rpc-remote.gen.ts",
"method": "post",
"params": [
"rpcName"
],
"middleware": [
{
"type": "wire",
"name": "pikkuRemoteAuthMiddleware",
"inline": false
}
]
},
"/workflow/:workflowName/start": {
"pikkuFuncId": "workflowStarter",
"route": "/workflow/:workflowName/start",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/workflow-routes.gen.ts",
"method": "post",
"params": [
"workflowName"
]
},
"/workflow/:workflowName/run": {
"pikkuFuncId": "workflowRunner",
"route": "/workflow/:workflowName/run",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/workflow-routes.gen.ts",
"method": "post",
"params": [
"workflowName"
]
},
"/workflow/:workflowName/graph/:nodeId": {
"pikkuFuncId": "graphStarter",
"route": "/workflow/:workflowName/graph/:nodeId",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/workflow-routes.gen.ts",
"method": "post",
"params": [
"workflowName",
"nodeId"
]
},
"/rpc/:rpcName": {
"pikkuFuncId": "rpcCaller",
"route": "/rpc/:rpcName",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/rpc-public.gen.ts",
"method": "post",
"params": [
"rpcName"
]
},
"/rpc/agent/:agentName": {
"pikkuFuncId": "agentCaller",
"route": "/rpc/agent/:agentName",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/agent.gen.ts",
"method": "post",
"params": [
"agentName"
],
"tags": [
"pikku:public"
]
},
"/rpc/agent/:agentName/stream": {
"pikkuFuncId": "agentStreamCaller",
"route": "/rpc/agent/:agentName/stream",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/agent.gen.ts",
"method": "post",
"params": [
"agentName"
],
"tags": [
"pikku:public"
],
"sse": true
},
"/rpc/agent/:agentName/approve": {
"pikkuFuncId": "agentApproveCaller",
"route": "/rpc/agent/:agentName/approve",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/agent.gen.ts",
"method": "post",
"params": [
"agentName"
],
"tags": [
"pikku:public"
]
},
"/rpc/agent/:agentName/resume": {
"pikkuFuncId": "agentResumeCaller",
"route": "/rpc/agent/:agentName/resume",
"sourceFile": "/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/src/scaffold/agent.gen.ts",
"method": "post",
"params": [
"agentName"
],
"tags": [
"pikku:public"
],
"sse": true
}
},
"put": {},
"delete": {},
"head": {},
"patch": {},
"options": {}
}

View File

@@ -0,0 +1,7 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
import { pikkuState } from '@pikku/core/internal'
import type { HTTPWiringsMeta } from '@pikku/core/http'
import metaData from './pikku-http-wirings-meta.gen.json' with { type: 'json' }
pikkuState(null, 'http', 'meta', metaData as HTTPWiringsMeta)

View File

@@ -0,0 +1,9 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/* The files with an wireHTTP function call */
import '../../../../src/scaffold/agent.gen.js'
import '../../../../src/scaffold/console.gen.js'
import '../../../../src/scaffold/rpc-public.gen.js'
import '../../../../src/scaffold/rpc-remote.gen.js'
import '../../../../src/scaffold/workflow-routes.gen.js'

View File

@@ -0,0 +1,183 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/**
* MCP-specific type definitions for tree-shaking optimization
*/
import {
CoreMCPResource,
CoreMCPPrompt,
wireMCPResource as wireMCPResourceCore,
wireMCPPrompt as wireMCPPromptCore,
MCPResourceResponse,
MCPToolResponse,
MCPPromptResponse,
AssertMCPResourceURIParams
} from '@pikku/core/mcp'
import type { PikkuFunctionConfig, PikkuFunctionSessionless, PikkuMiddleware, PikkuPermission, InferSchemaOutput } from '../function/pikku-function-types.gen.js'
import type { CorePermissionGroup } from '@pikku/core'
import type { StandardSchemaV1 } from '@standard-schema/spec'
/**
* Type definition for MCP resources that provide data to AI models.
*
* @template In - Input type for the resource request
* @template URI - URI template string type for compile-time parameter validation
*/
type MCPResourceWiring<In, URI extends string> = CoreMCPResource<PikkuFunctionConfig<In, MCPResourceResponse, 'rpc' | 'session' | 'mcp'>> & { uri: URI }
/**
* Type definition for MCP prompts that provide templates to AI models.
*
* @template In - Input type for the prompt parameters
*/
type MCPPromptWiring<In> = CoreMCPPrompt<PikkuFunctionConfig<In, MCPPromptResponse, 'rpc' | 'session' | 'mcp'>>
/**
* Registers an MCP resource with the Pikku framework.
* Resources provide data that AI models can access.
*
* @template In - Input type for the resource request
* @template URI - URI template string for compile-time parameter validation
* @param mcpResource - MCP resource definition with data provider function
*/
export const wireMCPResource = <In, URI extends string>(
mcpResource: MCPResourceWiring<In, URI> & AssertMCPResourceURIParams<In, URI>
) => {
wireMCPResourceCore(mcpResource as any)
}
/**
* Registers an MCP prompt with the Pikku framework.
* Prompts provide templates that AI models can use.
*
* @template In - Input type for the prompt parameters
* @param mcpPrompt - MCP prompt definition with template function
*/
export const wireMCPPrompt = <In>(
mcpPrompt: MCPPromptWiring<In>
) => {
wireMCPPromptCore(mcpPrompt as any)
}
/**
* Configuration for MCP prompt with Zod schema input validation.
*/
type MCPPromptFuncConfigWithSchema<InputSchema extends StandardSchemaV1> = {
func: PikkuFunctionSessionless<InferSchemaOutput<InputSchema>, MCPPromptResponse, 'mcp' | 'rpc'>
name?: string
input: InputSchema
}
/**
* Creates a function for handling MCP prompt requests.
* These functions generate prompt templates for AI models.
*
* Supports two patterns:
* 1. Generic types: `pikkuMCPPromptFunc<Input>({ func: ... })`
* 2. Zod schemas: `pikkuMCPPromptFunc({ input: z.object(...), func: ... })`
*
* @template In - Input type for the prompt parameters (inferred from schema if provided)
* @param func - Function definition, either direct function or configuration object
* @returns The unwrapped function for internal use
*/
export function pikkuMCPPromptFunc<InputSchema extends StandardSchemaV1>(
config: MCPPromptFuncConfigWithSchema<InputSchema>
): PikkuFunctionConfig<InferSchemaOutput<InputSchema>, MCPPromptResponse, 'mcp' | 'rpc'>
export function pikkuMCPPromptFunc<In>(
func:
| PikkuFunctionSessionless<In, MCPPromptResponse, 'mcp' | 'rpc'>
| {
func: PikkuFunctionSessionless<In, MCPPromptResponse, 'mcp' | 'rpc'>
name?: string
}
): PikkuFunctionConfig<In, MCPPromptResponse, 'mcp' | 'rpc'>
export function pikkuMCPPromptFunc(func: any): any {
return typeof func === 'function' ? { func } : func
}
/**
* Configuration for MCP tool with Zod schema input validation.
*/
type MCPToolFuncConfigWithSchema<InputSchema extends StandardSchemaV1> = {
func: PikkuFunctionSessionless<InferSchemaOutput<InputSchema>, MCPToolResponse, 'mcp' | 'rpc'>
description?: string
tags?: string[]
title?: string
summary?: string
name?: string
middleware?: PikkuMiddleware[]
permissions?: CorePermissionGroup<PikkuPermission<InferSchemaOutput<InputSchema>>>
input: InputSchema
}
/**
* Creates a function for handling MCP tool invocations.
* These functions perform actions that AI models can request.
*
* Supports two patterns:
* 1. Generic types: `pikkuMCPToolFunc<Input>({ func: ... })`
* 2. Zod schemas: `pikkuMCPToolFunc({ input: z.object(...), func: ... })`
*
* @template In - Input type for the tool invocation (inferred from schema if provided)
* @param func - Function definition, either direct function or configuration object
* @returns The unwrapped function for internal use
*/
export function pikkuMCPToolFunc<InputSchema extends StandardSchemaV1>(
config: MCPToolFuncConfigWithSchema<InputSchema>
): PikkuFunctionConfig<InferSchemaOutput<InputSchema>, MCPToolResponse, 'mcp' | 'rpc'>
export function pikkuMCPToolFunc<In>(
func:
| PikkuFunctionSessionless<In, MCPToolResponse, 'mcp' | 'rpc'>
| {
func: PikkuFunctionSessionless<In, MCPToolResponse, 'mcp' | 'rpc'>
description?: string
tags?: string[]
title?: string
summary?: string
name?: string
middleware?: PikkuMiddleware[]
permissions?: CorePermissionGroup<PikkuPermission<In>>
}
): PikkuFunctionConfig<In, MCPToolResponse, 'mcp' | 'rpc'>
export function pikkuMCPToolFunc(func: any): any {
return typeof func === 'function' ? { func } : func
}
/**
* Configuration for MCP resource with Zod schema input validation.
*/
type MCPResourceFuncConfigWithSchema<InputSchema extends StandardSchemaV1> = {
func: PikkuFunctionSessionless<InferSchemaOutput<InputSchema>, MCPResourceResponse, 'mcp' | 'rpc'>
name?: string
input: InputSchema
}
/**
* Creates a function for handling MCP resource requests.
* These functions provide data that AI models can access.
*
* Supports two patterns:
* 1. Generic types: `pikkuMCPResourceFunc<Input>({ func: ... })`
* 2. Zod schemas: `pikkuMCPResourceFunc({ input: z.object(...), func: ... })`
*
* @template In - Input type for the resource request (inferred from schema if provided)
* @param func - Function definition, either direct function or configuration object
* @returns The unwrapped function for internal use
*/
export function pikkuMCPResourceFunc<InputSchema extends StandardSchemaV1>(
config: MCPResourceFuncConfigWithSchema<InputSchema>
): PikkuFunctionConfig<InferSchemaOutput<InputSchema>, MCPResourceResponse, 'mcp' | 'rpc'>
export function pikkuMCPResourceFunc<In>(
func:
| PikkuFunctionSessionless<In, MCPResourceResponse, 'mcp' | 'rpc'>
| {
func: PikkuFunctionSessionless<In, MCPResourceResponse, 'mcp' | 'rpc'>
name?: string
}
): PikkuFunctionConfig<In, MCPResourceResponse, 'mcp' | 'rpc'>
export function pikkuMCPResourceFunc(func: any): any {
return typeof func === 'function' ? { func } : func
}

View File

@@ -0,0 +1,13 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
import './rpc/pikku-rpc-wirings-meta.internal.gen.js'
import './http/pikku-http-wirings-meta.gen.js'
import './function/pikku-functions-meta.gen.js'
import './queue/pikku-queue-workers-wirings-meta.gen.js'
import './schemas/register.gen.js'
import './http/pikku-http-wirings.gen.js'
import './function/pikku-functions.gen.js'
import './queue/pikku-queue-workers-wirings.gen.js'
import '../../../src/scaffold/console.gen.js'
import '@pikku/addon-console/.pikku/pikku-bootstrap.gen.js'

View File

@@ -0,0 +1,10 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
import { LocalMetaService } from '@pikku/core/services/local-meta'
export class PikkuMetaService extends LocalMetaService {
constructor() {
super('/Users/yasser/git/pikku/fabric/templates/kanban-board-template/packages/functions/packages/functions/.pikku')
}
}

View File

@@ -0,0 +1,39 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
import type { SingletonServices } from '../../../src/application-types.d.js'
import type { Services } from '../../../src/application-types.d.js'
// Singleton services map: true if required, false if available but unused
export const requiredSingletonServices = {
'agentRunService': true,
'aiAgentRunner': true,
'aiRunState': true,
'aiStorage': true,
'config': true,
'content': false,
'credentialService': true,
'deploymentService': true,
'eventHub': false,
'jwt': false,
'kysely': false,
'logger': true,
'metaService': true,
'queueService': false,
'schedulerService': true,
'schema': true,
'secrets': true,
'sessionStore': false,
'variables': true,
'workflowRunService': true,
'workflowService': true,
} as const
// Wire services map: true if required, false if available but unused
export const requiredWireServices = {
} as const
// Type exports
export type RequiredSingletonServices = Pick<SingletonServices, 'agentRunService' | 'aiAgentRunner' | 'aiRunState' | 'aiStorage' | 'config' | 'credentialService' | 'deploymentService' | 'logger' | 'metaService' | 'schedulerService' | 'schema' | 'secrets' | 'variables' | 'workflowRunService' | 'workflowService'> & Partial<Omit<SingletonServices, 'agentRunService' | 'aiAgentRunner' | 'aiRunState' | 'aiStorage' | 'config' | 'credentialService' | 'deploymentService' | 'logger' | 'metaService' | 'schedulerService' | 'schema' | 'secrets' | 'variables' | 'workflowRunService' | 'workflowService'>>
export type RequiredWireServices = Partial<Services>

View File

@@ -0,0 +1,36 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/**
* Main type export hub - re-exports all wiring-specific types
*/
// Core function, middleware, and permission types
export * from './function/pikku-function-types.gen.js'
// HTTP wiring types
export * from './http/pikku-http-types.gen.js'
// Channel wiring types
export * from './channel/pikku-channel-types.gen.js'
// Trigger wiring types
export * from './trigger/pikku-trigger-types.gen.js'
// Scheduler wiring types
export * from './scheduler/pikku-scheduler-types.gen.js'
// Queue wiring types
export * from './queue/pikku-queue-types.gen.js'
// MCP wiring types
export * from './mcp/pikku-mcp-types.gen.js'
// CLI wiring types
export * from './cli/pikku-cli-types.gen.js'
// Node wiring types
export * from './console/pikku-node-types.gen.js'
// Secret definition types
export * from './secrets/pikku-secret-types.gen.js'

View File

@@ -0,0 +1,27 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/**
* Queue-specific type definitions for tree-shaking optimization
*/
import { CoreQueueWorker, wireQueueWorker as wireQueueWorkerCore } from '@pikku/core/queue'
import type { PikkuFunctionConfig } from '../function/pikku-function-types.gen.js'
/**
* Type definition for queue workers that process background jobs.
*
* @template In - Input type for the queue job
* @template Out - Output type for the queue job
*/
type QueueWiring<In, Out> = CoreQueueWorker<PikkuFunctionConfig<In, Out, 'session' | 'rpc'>>
/**
* Registers a queue worker with the Pikku framework.
* Workers process background jobs from queues.
*
* @param queueWorker - Queue worker definition with job handler
*/
export const wireQueueWorker = (queueWorker: QueueWiring<any, any>) => {
wireQueueWorkerCore(queueWorker as any)
}

View File

@@ -0,0 +1,78 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/**
* This provides the structure needed for typescript to be aware of Queue workers and their input/output 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; }
export type DeleteAgentThreadInput = { threadId: string; resourceId?: string; }
export type DeleteAgentThreadOutput = { deleted: boolean; }
export type GetAgentThreadMessagesInput = { threadId: string; resourceId?: string; }
export type GetAgentThreadMessagesOutput = any[]
export type GetAgentThreadRunsInput = { threadId: string; resourceId?: string; }
export type GetAgentThreadRunsOutput = any[]
export type GetAgentThreadsInput = { agentName?: string; resourceId?: string; limit?: number; offset?: number; }
export type GetAgentThreadsOutput = any[]
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 RemoteRPCHandlerInput = { rpcName: string; data?: unknown; }
export type RpcCallerInput = { rpcName: string; data?: unknown; }
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; }
import type { QueueJob } from '@pikku/core/queue'
interface QueueHandler<I, O> {
input: I;
output: O;
}
export type QueueMap = {
readonly 'pikku-remote-internal-rpc': QueueHandler<RemoteRPCHandlerInput, null>,
};
type QueueAdd = <Name extends keyof QueueMap>(
name: Name,
data: QueueMap[Name]['input'],
options?: {
priority?: number
delay?: number
attempts?: number
removeOnComplete?: number
removeOnFail?: number
jobId?: string
}
) => Promise<string>
type QueueGetJob = <Name extends keyof QueueMap>(
name: Name,
jobId: string
) => Promise<QueueJob<QueueMap[Name]['input'], QueueMap[Name]['output']> | null>
export type TypedPikkuQueue = {
add: QueueAdd;
getJob: QueueGetJob;
}

View File

@@ -0,0 +1,6 @@
{
"pikku-remote-internal-rpc": {
"pikkuFuncId": "remoteRPCHandler",
"name": "pikku-remote-internal-rpc"
}
}

View File

@@ -0,0 +1,8 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
import { pikkuState } from '@pikku/core/internal'
import { QueueWorkersMeta } from '@pikku/core/queue'
import metaData from './pikku-queue-workers-wirings-meta.gen.json' with { type: 'json' }
pikkuState(null, 'queue', 'meta', metaData as QueueWorkersMeta)

View File

@@ -0,0 +1,5 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/* The files with an addQueueWorkers function call */
import '../../../../src/scaffold/rpc-remote.gen.js'

View File

@@ -0,0 +1,148 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/**
* This provides the structure needed for typescript to be aware of RPCs and their return 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; }
export type DeleteAgentThreadInput = { threadId: string; resourceId?: string; }
export type DeleteAgentThreadOutput = { deleted: boolean; }
export type GetAgentThreadMessagesInput = { threadId: string; resourceId?: string; }
export type GetAgentThreadMessagesOutput = any[]
export type GetAgentThreadRunsInput = { threadId: string; resourceId?: string; }
export type GetAgentThreadRunsOutput = any[]
export type GetAgentThreadsInput = { agentName?: string; resourceId?: string; limit?: number; offset?: number; }
export type GetAgentThreadsOutput = any[]
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 RemoteRPCHandlerInput = { rpcName: string; data?: unknown; }
export type RpcCallerInput = { rpcName: string; data?: unknown; }
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 '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 'getAgentThreads': RPCHandler<GetAgentThreadsInput, GetAgentThreadsOutput>,
readonly 'getAgentThreadMessages': RPCHandler<GetAgentThreadMessagesInput, GetAgentThreadMessagesOutput>,
readonly 'getAgentThreadRuns': RPCHandler<GetAgentThreadRunsInput, GetAgentThreadRunsOutput>,
readonly 'deleteAgentThread': RPCHandler<DeleteAgentThreadInput, DeleteAgentThreadOutput>,
};
// 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,160 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/**
* This provides the structure needed for typescript to be aware of RPCs and their return types
*/
import type { WorkflowRunStatus } from '@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; }
export type DeleteAgentThreadInput = { threadId: string; resourceId?: string; }
export type DeleteAgentThreadOutput = { deleted: boolean; }
export type GetAgentThreadMessagesInput = { threadId: string; resourceId?: string; }
export type GetAgentThreadMessagesOutput = any[]
export type GetAgentThreadRunsInput = { threadId: string; resourceId?: string; }
export type GetAgentThreadRunsOutput = any[]
export type GetAgentThreadsInput = { agentName?: string; resourceId?: string; limit?: number; offset?: number; }
export type GetAgentThreadsOutput = any[]
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 RemoteRPCHandlerInput = { rpcName: string; data?: unknown; }
export type RpcCallerInput = { rpcName: string; data?: unknown; }
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 '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 'remoteRPCHandler': RPCHandler<RemoteRPCHandlerInput, null>,
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>,
readonly 'rpcCaller': RPCHandler<RpcCallerInput, null>,
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>,
};
// 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,23 @@
{
"pikkuConsoleSetSecret": "pikkuConsoleSetSecret",
"pikkuConsoleGetVariable": "pikkuConsoleGetVariable",
"pikkuConsoleSetVariable": "pikkuConsoleSetVariable",
"pikkuConsoleHasSecret": "pikkuConsoleHasSecret",
"pikkuConsoleGetSecret": "pikkuConsoleGetSecret",
"remoteRPCHandler": "remoteRPCHandler",
"workflowStarter": "workflowStarter",
"workflowRunner": "workflowRunner",
"workflowStatusChecker": "workflowStatusChecker",
"workflowStatusStream": "workflowStatusStream",
"workflowStatusStreamFull": "workflowStatusStreamFull",
"graphStarter": "graphStarter",
"rpcCaller": "rpcCaller",
"agentCaller": "agentCaller",
"agentStreamCaller": "agentStreamCaller",
"agentApproveCaller": "agentApproveCaller",
"agentResumeCaller": "agentResumeCaller",
"getAgentThreads": "getAgentThreads",
"getAgentThreadMessages": "getAgentThreadMessages",
"getAgentThreadRuns": "getAgentThreadRuns",
"deleteAgentThread": "deleteAgentThread"
}

View File

@@ -0,0 +1,6 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
import { pikkuState } from '@pikku/core/internal'
import metaData from './pikku-rpc-wirings-meta.internal.gen.json' with { type: 'json' }
pikkuState(null, 'rpc', 'meta', metaData as Record<string, string>)

View File

@@ -0,0 +1,25 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
/**
* Scheduler-specific type definitions for tree-shaking optimization
*/
import { CoreScheduledTask, wireScheduler as wireSchedulerCore } from '@pikku/core/scheduler'
import type { PikkuFunctionConfig, PikkuMiddleware } from '../function/pikku-function-types.gen.js'
/**
* Type definition for scheduled tasks that run at specified intervals.
* These are sessionless functions that execute based on cron expressions.
*/
type SchedulerWiring = CoreScheduledTask<PikkuFunctionConfig<void, void, 'session' | 'rpc'>, PikkuMiddleware>
/**
* Registers a scheduled task with the Pikku framework.
* Tasks run based on cron expressions and are sessionless.
*
* @param task - Scheduled task definition with cron expression and handler
*/
export const wireScheduler = (task: SchedulerWiring) => {
wireSchedulerCore(task as any)
}

View File

@@ -0,0 +1,139 @@
/**
* This file was generated by @pikku/cli@0.12.23
*/
import { addSchema } from '@pikku/core/schema'
import * as PikkuConsoleSetSecretInput from './schemas/PikkuConsoleSetSecretInput.schema.json' with { type: 'json' }
addSchema('PikkuConsoleSetSecretInput', PikkuConsoleSetSecretInput)
import * as PikkuConsoleSetSecretOutput from './schemas/PikkuConsoleSetSecretOutput.schema.json' with { type: 'json' }
addSchema('PikkuConsoleSetSecretOutput', PikkuConsoleSetSecretOutput)
import * as PikkuConsoleGetVariableInput from './schemas/PikkuConsoleGetVariableInput.schema.json' with { type: 'json' }
addSchema('PikkuConsoleGetVariableInput', PikkuConsoleGetVariableInput)
import * as PikkuConsoleGetVariableOutput from './schemas/PikkuConsoleGetVariableOutput.schema.json' with { type: 'json' }
addSchema('PikkuConsoleGetVariableOutput', PikkuConsoleGetVariableOutput)
import * as PikkuConsoleSetVariableInput from './schemas/PikkuConsoleSetVariableInput.schema.json' with { type: 'json' }
addSchema('PikkuConsoleSetVariableInput', PikkuConsoleSetVariableInput)
import * as PikkuConsoleSetVariableOutput from './schemas/PikkuConsoleSetVariableOutput.schema.json' with { type: 'json' }
addSchema('PikkuConsoleSetVariableOutput', PikkuConsoleSetVariableOutput)
import * as PikkuConsoleHasSecretInput from './schemas/PikkuConsoleHasSecretInput.schema.json' with { type: 'json' }
addSchema('PikkuConsoleHasSecretInput', PikkuConsoleHasSecretInput)
import * as PikkuConsoleHasSecretOutput from './schemas/PikkuConsoleHasSecretOutput.schema.json' with { type: 'json' }
addSchema('PikkuConsoleHasSecretOutput', PikkuConsoleHasSecretOutput)
import * as PikkuConsoleGetSecretInput from './schemas/PikkuConsoleGetSecretInput.schema.json' with { type: 'json' }
addSchema('PikkuConsoleGetSecretInput', PikkuConsoleGetSecretInput)
import * as PikkuConsoleGetSecretOutput from './schemas/PikkuConsoleGetSecretOutput.schema.json' with { type: 'json' }
addSchema('PikkuConsoleGetSecretOutput', PikkuConsoleGetSecretOutput)
import * as StreamWorkflowRunInput from './schemas/StreamWorkflowRunInput.schema.json' with { type: 'json' }
addSchema('StreamWorkflowRunInput', StreamWorkflowRunInput)
import * as RemoteRPCHandlerInput from './schemas/RemoteRPCHandlerInput.schema.json' with { type: 'json' }
addSchema('RemoteRPCHandlerInput', RemoteRPCHandlerInput)
import * as WorkflowStarterInput from './schemas/WorkflowStarterInput.schema.json' with { type: 'json' }
addSchema('WorkflowStarterInput', WorkflowStarterInput)
import * as WorkflowStarterOutput from './schemas/WorkflowStarterOutput.schema.json' with { type: 'json' }
addSchema('WorkflowStarterOutput', WorkflowStarterOutput)
import * as WorkflowRunnerInput from './schemas/WorkflowRunnerInput.schema.json' with { type: 'json' }
addSchema('WorkflowRunnerInput', WorkflowRunnerInput)
import * as WorkflowStatusCheckerInput from './schemas/WorkflowStatusCheckerInput.schema.json' with { type: 'json' }
addSchema('WorkflowStatusCheckerInput', WorkflowStatusCheckerInput)
import * as WorkflowRunStatus from './schemas/WorkflowRunStatus.schema.json' with { type: 'json' }
addSchema('WorkflowRunStatus', WorkflowRunStatus)
import * as WorkflowStatusStreamInput from './schemas/WorkflowStatusStreamInput.schema.json' with { type: 'json' }
addSchema('WorkflowStatusStreamInput', WorkflowStatusStreamInput)
import * as WorkflowStatusStreamFullInput from './schemas/WorkflowStatusStreamFullInput.schema.json' with { type: 'json' }
addSchema('WorkflowStatusStreamFullInput', WorkflowStatusStreamFullInput)
import * as GraphStarterInput from './schemas/GraphStarterInput.schema.json' with { type: 'json' }
addSchema('GraphStarterInput', GraphStarterInput)
import * as GraphStarterOutput from './schemas/GraphStarterOutput.schema.json' with { type: 'json' }
addSchema('GraphStarterOutput', GraphStarterOutput)
import * as RpcCallerInput from './schemas/RpcCallerInput.schema.json' with { type: 'json' }
addSchema('RpcCallerInput', RpcCallerInput)
import * as AgentCallerInput from './schemas/AgentCallerInput.schema.json' with { type: 'json' }
addSchema('AgentCallerInput', AgentCallerInput)
import * as AgentStreamCallerInput from './schemas/AgentStreamCallerInput.schema.json' with { type: 'json' }
addSchema('AgentStreamCallerInput', AgentStreamCallerInput)
import * as AgentApproveCallerInput from './schemas/AgentApproveCallerInput.schema.json' with { type: 'json' }
addSchema('AgentApproveCallerInput', AgentApproveCallerInput)
import * as AgentResumeCallerInput from './schemas/AgentResumeCallerInput.schema.json' with { type: 'json' }
addSchema('AgentResumeCallerInput', AgentResumeCallerInput)
import * as GetAgentThreadsInput from './schemas/GetAgentThreadsInput.schema.json' with { type: 'json' }
addSchema('GetAgentThreadsInput', GetAgentThreadsInput)
import * as GetAgentThreadsOutput from './schemas/GetAgentThreadsOutput.schema.json' with { type: 'json' }
addSchema('GetAgentThreadsOutput', GetAgentThreadsOutput)
import * as GetAgentThreadMessagesInput from './schemas/GetAgentThreadMessagesInput.schema.json' with { type: 'json' }
addSchema('GetAgentThreadMessagesInput', GetAgentThreadMessagesInput)
import * as GetAgentThreadMessagesOutput from './schemas/GetAgentThreadMessagesOutput.schema.json' with { type: 'json' }
addSchema('GetAgentThreadMessagesOutput', GetAgentThreadMessagesOutput)
import * as GetAgentThreadRunsInput from './schemas/GetAgentThreadRunsInput.schema.json' with { type: 'json' }
addSchema('GetAgentThreadRunsInput', GetAgentThreadRunsInput)
import * as GetAgentThreadRunsOutput from './schemas/GetAgentThreadRunsOutput.schema.json' with { type: 'json' }
addSchema('GetAgentThreadRunsOutput', GetAgentThreadRunsOutput)
import * as DeleteAgentThreadInput from './schemas/DeleteAgentThreadInput.schema.json' with { type: 'json' }
addSchema('DeleteAgentThreadInput', DeleteAgentThreadInput)
import * as DeleteAgentThreadOutput from './schemas/DeleteAgentThreadOutput.schema.json' with { type: 'json' }
addSchema('DeleteAgentThreadOutput', DeleteAgentThreadOutput)

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":{}}

Some files were not shown because too many files have changed in this diff Show More