create-theokit 0.1.0-alpha.13 → 0.1.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-theokit",
3
- "version": "0.1.0-alpha.13",
3
+ "version": "0.1.0-alpha.15",
4
4
  "type": "module",
5
5
  "description": "Scaffold a new TheoKit project",
6
6
  "license": "Apache-2.0",
@@ -2,6 +2,8 @@
2
2
 
3
3
  TheoKit API-only project. Backend routes with Zod validation + typed responses — no frontend bundle, no React.
4
4
 
5
+ > 📚 **Full docs:** https://docs.theokit.dev
6
+
5
7
  ## Quick start
6
8
 
7
9
  ```bash
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "theokit": "^0.1.0-alpha.13",
13
+ "theokit": "^0.1.0-alpha.15",
14
14
  "react": "^19.0.0",
15
15
  "react-dom": "^19.0.0"
16
16
  },
@@ -0,0 +1,34 @@
1
+ import { defineWebhook } from 'theokit/server'
2
+ import { createHmac, timingSafeEqual } from 'node:crypto'
3
+ import { z } from 'zod'
4
+
5
+ /**
6
+ * Echo webhook — demonstrates `defineWebhook` HMAC-SHA256 pattern
7
+ * without depending on an external provider (Stripe, GitHub, etc.).
8
+ *
9
+ * Self-test:
10
+ * SECRET=$(openssl rand -base64 32)
11
+ * echo -n '{"message":"hi"}' | openssl dgst -sha256 -hmac "$SECRET"
12
+ * curl -X POST localhost:3000/api/webhooks/echo \
13
+ * -H "x-echo-signature: <hex from above>" \
14
+ * -H "Content-Type: application/json" \
15
+ * -d '{"message":"hi"}'
16
+ */
17
+ const ECHO_SECRET = process.env.ECHO_WEBHOOK_SECRET ?? ''
18
+
19
+ export const POST = defineWebhook({
20
+ verify: ({ rawBody, headers }) => {
21
+ if (ECHO_SECRET === '') return false
22
+ const sig = headers.get('x-echo-signature') ?? ''
23
+ const expected = createHmac('sha256', ECHO_SECRET).update(rawBody).digest('hex')
24
+ try {
25
+ return timingSafeEqual(Buffer.from(sig, 'utf-8'), Buffer.from(expected, 'utf-8'))
26
+ } catch {
27
+ return false
28
+ }
29
+ },
30
+ inputSchema: z.object({ message: z.string() }),
31
+ handler: async ({ input }) => {
32
+ return Response.json({ echoed: input.message, timestamp: new Date().toISOString() })
33
+ },
34
+ })
@@ -2,6 +2,8 @@
2
2
 
3
3
  TheoKit dashboard project. Build the app your agent lives in — with nested layouts and a sidebar wired from day one.
4
4
 
5
+ > 📚 **Full docs:** https://docs.theokit.dev
6
+
5
7
  ## Quick start
6
8
 
7
9
  ```bash
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "theokit": "^0.1.0-alpha.13",
13
+ "theokit": "^0.1.0-alpha.15",
14
14
  "react": "^19.0.0",
15
15
  "react-dom": "^19.0.0"
16
16
  },
@@ -0,0 +1,51 @@
1
+ import { defineCron } from 'theokit/server/cron'
2
+ import { readdir, stat, rm } from 'node:fs/promises'
3
+ import { join, resolve } from 'node:path'
4
+
5
+ /**
6
+ * Daily GC of stale conversation transcripts.
7
+ *
8
+ * The `@usetheo/sdk` Agent persists chat history under
9
+ * `.theokit/agents/<agentId>/messages.jsonl`. With no TTL the directory
10
+ * grows unbounded — production foot-gun. This cron removes any agent
11
+ * directory whose `messages.jsonl` hasn't been touched in 30 days.
12
+ */
13
+ const MAX_AGE_DAYS = 30
14
+ const AGENTS_DIR = '.theokit/agents'
15
+
16
+ export default defineCron({
17
+ name: 'cleanup-conversations',
18
+ schedule: '0 4 * * *', // Daily 04:00 UTC
19
+ handler: async ({ log }) => {
20
+ const root = resolve(process.cwd(), AGENTS_DIR)
21
+ const cutoff = Date.now() - MAX_AGE_DAYS * 24 * 60 * 60 * 1000
22
+ let removed = 0
23
+ let kept = 0
24
+ let entries: Awaited<ReturnType<typeof readdir>>
25
+ try {
26
+ entries = await readdir(root, { withFileTypes: true })
27
+ } catch {
28
+ log.info({ msg: 'No agents dir yet — first run', dir: root })
29
+ return
30
+ }
31
+ for (const entry of entries) {
32
+ if (!entry.isDirectory()) continue
33
+ const agentDir = join(root, entry.name)
34
+ const messagesFile = join(agentDir, 'messages.jsonl')
35
+ try {
36
+ const s = await stat(messagesFile)
37
+ if (s.mtimeMs < cutoff) {
38
+ await rm(agentDir, { recursive: true, force: true })
39
+ removed++
40
+ } else {
41
+ kept++
42
+ }
43
+ } catch {
44
+ // messages.jsonl missing → orphan dir, remove
45
+ await rm(agentDir, { recursive: true, force: true }).catch(() => {})
46
+ removed++
47
+ }
48
+ }
49
+ log.info({ msg: 'cleanup-conversations complete', removed, kept, maxAgeDays: MAX_AGE_DAYS })
50
+ },
51
+ })
@@ -2,6 +2,8 @@
2
2
 
3
3
  TheoKit project. Build the app your agent lives in — routing, auth, real-time, deploy — wired.
4
4
 
5
+ > 📚 **Full docs:** https://docs.theokit.dev
6
+
5
7
  ## Quick start
6
8
 
7
9
  ```bash
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "theokit": "^0.1.0-alpha.13",
13
+ "theokit": "^0.1.0-alpha.15",
14
14
  "@usetheo/sdk": "^1.2.0",
15
15
  "@usetheo/ui": "^0.12.0-next.0",
16
16
  "lucide-react": "^0.469.0",
@@ -0,0 +1,51 @@
1
+ import { defineCron } from 'theokit/server/cron'
2
+ import { readdir, stat, rm } from 'node:fs/promises'
3
+ import { join, resolve } from 'node:path'
4
+
5
+ /**
6
+ * Daily GC of stale conversation transcripts.
7
+ *
8
+ * The `@usetheo/sdk` Agent persists chat history under
9
+ * `.theokit/agents/<agentId>/messages.jsonl`. With no TTL the directory
10
+ * grows unbounded — production foot-gun. This cron removes any agent
11
+ * directory whose `messages.jsonl` hasn't been touched in 30 days.
12
+ */
13
+ const MAX_AGE_DAYS = 30
14
+ const AGENTS_DIR = '.theokit/agents'
15
+
16
+ export default defineCron({
17
+ name: 'cleanup-conversations',
18
+ schedule: '0 4 * * *', // Daily 04:00 UTC
19
+ handler: async ({ log }) => {
20
+ const root = resolve(process.cwd(), AGENTS_DIR)
21
+ const cutoff = Date.now() - MAX_AGE_DAYS * 24 * 60 * 60 * 1000
22
+ let removed = 0
23
+ let kept = 0
24
+ let entries: Awaited<ReturnType<typeof readdir>>
25
+ try {
26
+ entries = await readdir(root, { withFileTypes: true })
27
+ } catch {
28
+ log.info({ msg: 'No agents dir yet — first run', dir: root })
29
+ return
30
+ }
31
+ for (const entry of entries) {
32
+ if (!entry.isDirectory()) continue
33
+ const agentDir = join(root, entry.name)
34
+ const messagesFile = join(agentDir, 'messages.jsonl')
35
+ try {
36
+ const s = await stat(messagesFile)
37
+ if (s.mtimeMs < cutoff) {
38
+ await rm(agentDir, { recursive: true, force: true })
39
+ removed++
40
+ } else {
41
+ kept++
42
+ }
43
+ } catch {
44
+ // messages.jsonl missing → orphan dir, remove
45
+ await rm(agentDir, { recursive: true, force: true }).catch(() => {})
46
+ removed++
47
+ }
48
+ }
49
+ log.info({ msg: 'cleanup-conversations complete', removed, kept, maxAgeDays: MAX_AGE_DAYS })
50
+ },
51
+ })
@@ -39,19 +39,28 @@ export const POST = defineAgentEndpoint({
39
39
  // ANTHROPIC_API_KEY presente no env. Wire protocol: OpenAI Chat Completions
40
40
  // (universal — todos os providers implementam essa API). Consumer NÃO tem
41
41
  // conditionals sobre provider — é responsabilidade do framework.
42
- const { agent } = await createConversationHistory({
43
- request,
44
- response: { headers: cookieHeaders },
45
- options: {
46
- // model id literal — provider resolution NÃO depende de prefix inference.
47
- // Stranger pode trocar livremente sem mexer em routing.
48
- model: { id: 'gpt-4o-mini' },
49
- tools: [currentTime],
50
- },
51
- })
52
- const run = await agent.send(message, { signal })
53
- yield* streamAgentRun(run)
54
- // Intentionally NO agent.dispose() — the agent stays registered so the
55
- // next request from the same conversation resumes it (continuity).
42
+ // Wrap full agent lifecycle in try/catch — provider errors (invalid KEY,
43
+ // 401, rate-limit, model-not-found, 5xx) MUST surface as AgentEvent
44
+ // 'error' so the client renders an actionable message instead of a
45
+ // silent SSE closure. Dogfood chaos Phase 12 validates this contract.
46
+ try {
47
+ const { agent } = await createConversationHistory({
48
+ request,
49
+ response: { headers: cookieHeaders },
50
+ options: {
51
+ // model id literal — provider resolution NÃO depende de prefix inference.
52
+ // Stranger pode trocar livremente sem mexer em routing.
53
+ model: { id: 'gpt-4o-mini' },
54
+ tools: [currentTime],
55
+ },
56
+ })
57
+ const run = await agent.send(message, { signal })
58
+ yield* streamAgentRun(run)
59
+ // Intentionally NO agent.dispose() — the agent stays registered so the
60
+ // next request from the same conversation resumes it (continuity).
61
+ } catch (err) {
62
+ const msg = err instanceof Error ? err.message : String(err)
63
+ yield { type: 'error', message: `Agent error: ${msg}` }
64
+ }
56
65
  },
57
66
  })
@@ -2,6 +2,8 @@
2
2
 
3
3
  TheoKit project with Postgres + Drizzle ORM wired. Schema-first, migration-aware, typed end-to-end.
4
4
 
5
+ > 📚 **Full docs:** https://docs.theokit.dev
6
+
5
7
  ## Quick start
6
8
 
7
9
  ```bash
@@ -14,7 +14,7 @@
14
14
  "db:studio": "drizzle-kit studio"
15
15
  },
16
16
  "dependencies": {
17
- "theokit": "^0.1.0-alpha.13",
17
+ "theokit": "^0.1.0-alpha.15",
18
18
  "react": "^19.0.0",
19
19
  "react-dom": "^19.0.0",
20
20
  "drizzle-orm": "^0.45.0",
@@ -0,0 +1,27 @@
1
+ import { defineJob } from 'theokit/server/jobs'
2
+ import { z } from 'zod'
3
+ import { appendFile, mkdir } from 'node:fs/promises'
4
+ import { resolve, dirname } from 'node:path'
5
+
6
+ /**
7
+ * Background job demonstrating `defineJob` + `ctx.queue.enqueue` pattern.
8
+ *
9
+ * Triggered from `server/routes/users.ts` POST handler via:
10
+ * await ctx.queue.enqueue('log-message', { userId, message })
11
+ *
12
+ * Per ADR-0003 (transactional outbox), enqueue is deferred until the
13
+ * route handler commits successfully — handler throws → 0 jobs dispatched.
14
+ */
15
+ export default defineJob({
16
+ name: 'log-message',
17
+ input: z.object({ userId: z.string(), message: z.string() }),
18
+ handler: async ({ input, log }) => {
19
+ // v1.1 EC-9: anchor path to process.cwd() — handler CWD may differ from
20
+ // project root when running via external job runner.
21
+ const auditPath = resolve(process.cwd(), '.theo/audit.log')
22
+ await mkdir(dirname(auditPath), { recursive: true })
23
+ const line = `${new Date().toISOString()} user=${input.userId} msg=${input.message}\n`
24
+ await appendFile(auditPath, line)
25
+ log.info({ msg: 'audit logged', userId: input.userId, path: auditPath })
26
+ },
27
+ })
@@ -2,6 +2,8 @@
2
2
 
3
3
  TheoKit SaaS template — auth, sessions, billing-ready, and an agent route. The full stack for shipping an account-aware product on day one.
4
4
 
5
+ > 📚 **Full docs:** https://docs.theokit.dev
6
+
5
7
  ## Quick start
6
8
 
7
9
  ```bash
@@ -14,7 +14,7 @@
14
14
  "db:studio": "drizzle-kit studio"
15
15
  },
16
16
  "dependencies": {
17
- "theokit": "^0.1.0-alpha.13",
17
+ "theokit": "^0.1.0-alpha.15",
18
18
  "@usetheo/ui": "^0.12.0-next.0",
19
19
  "react": "^19.0.0",
20
20
  "react-dom": "^19.0.0",
@@ -1,10 +1,20 @@
1
1
  import { defineAgentEndpoint, requireAuth, type AgentEvent } from 'theokit/server'
2
+ import { trackAgentRun } from 'theokit/server/cost'
2
3
  import type { RequestContext } from '../context.js'
3
4
 
4
5
  /**
5
6
  * Protected agent endpoint. `requireAuth` fires BEFORE the stream starts;
6
7
  * unauthorized requests get 401 immediately — no SSE bytes leak.
7
8
  *
9
+ * Observability: wraps the run with `trackAgentRun` to surface per-user
10
+ * cost + token usage to the configured `UsageStorageAdapter` (configure via
11
+ * `theo.config.ts > cost.storage`). Also feeds the devtools `Agents` tab
12
+ * (when running in dev).
13
+ *
14
+ * NOTE: `costUsd: 0` is a v1 stub. Pricing table integration is a
15
+ * `@usetheo/sdk` follow-up (R0.5.11). Devtools tab renders "$0.0000" —
16
+ * indicates "cost tracking not yet calibrated for this model".
17
+ *
8
18
  * Replace the mock generator with your LLM provider call.
9
19
  */
10
20
  export const POST = defineAgentEndpoint<{ message: string }, RequestContext>({
@@ -12,10 +22,28 @@ export const POST = defineAgentEndpoint<{ message: string }, RequestContext>({
12
22
  requireAuth(ctx.session)
13
23
  const body = (await request.json()) as { message?: string }
14
24
  const msg = body.message ?? ''
15
- yield {
16
- type: 'message',
17
- content: `Hello ${ctx.session.email}, you said: "${msg}"`,
25
+ try {
26
+ yield {
27
+ type: 'message',
28
+ content: `Hello ${ctx.session.email}, you said: "${msg}"`,
29
+ }
30
+ yield { type: 'message', content: '(Replace this mock with your LLM.)' }
31
+ } finally {
32
+ // Always emit observability — even on stream error / abort.
33
+ // `storage` resolved from theo.config.ts > cost.storage (undefined =
34
+ // no-op; configure to enable persistence + devtools tab visibility).
35
+ // To enable persistent cost tracking: wire `cost: { storage }` into
36
+ // `theo.config.ts` and forward via context. Demo passes `undefined`
37
+ // (no-op storage; still fires devtools dispatcher in dev mode).
38
+ await trackAgentRun(
39
+ {
40
+ userId: ctx.session.email,
41
+ model: 'mock/echo',
42
+ tokens: { input: msg.length, output: 0 }, // crude — real impl uses tokenizer
43
+ costUsd: 0, // v1 stub
44
+ },
45
+ { storage: undefined },
46
+ )
18
47
  }
19
- yield { type: 'message', content: '(Replace this mock with your LLM.)' }
20
48
  },
21
49
  })
@@ -0,0 +1,49 @@
1
+ import { defineWebhook } from 'theokit/server'
2
+ import { createHmac, timingSafeEqual } from 'node:crypto'
3
+ import { z } from 'zod'
4
+
5
+ /**
6
+ * Stripe webhook receiver.
7
+ *
8
+ * Verifies `Stripe-Signature` header per Stripe's documented HMAC-SHA256
9
+ * scheme (https://stripe.com/docs/webhooks/signatures). Real impl would
10
+ * handle `checkout.session.completed`, `invoice.paid`, etc.
11
+ *
12
+ * Setup:
13
+ * 1. Create webhook endpoint in Stripe Dashboard pointing to /api/billing/stripe-webhook
14
+ * 2. Copy signing secret → `.env` STRIPE_WEBHOOK_SECRET
15
+ * 3. Test locally: `stripe listen --forward-to localhost:3000/api/billing/stripe-webhook`
16
+ */
17
+ const STRIPE_SECRET = process.env.STRIPE_WEBHOOK_SECRET ?? ''
18
+
19
+ export const POST = defineWebhook({
20
+ verify: ({ rawBody, headers }) => {
21
+ if (STRIPE_SECRET === '') return false
22
+ const sigHeader = headers.get('stripe-signature') ?? ''
23
+ // Stripe format: `t=<unix-ts>,v1=<hash>` (potentially also v0)
24
+ const parts: Record<string, string> = {}
25
+ for (const pair of sigHeader.split(',')) {
26
+ const [k, v] = pair.split('=')
27
+ if (k && v) parts[k.trim()] = v.trim()
28
+ }
29
+ const t = parts['t']
30
+ const v1 = parts['v1']
31
+ if (!t || !v1) return false
32
+ const signedPayload = `${t}.${rawBody}`
33
+ const expected = createHmac('sha256', STRIPE_SECRET).update(signedPayload).digest('hex')
34
+ try {
35
+ return timingSafeEqual(Buffer.from(v1, 'utf-8'), Buffer.from(expected, 'utf-8'))
36
+ } catch {
37
+ return false
38
+ }
39
+ },
40
+ inputSchema: z.object({ type: z.string(), data: z.unknown() }),
41
+ handler: async ({ input, log }) => {
42
+ log.info({ msg: 'stripe webhook received', type: input.type })
43
+ // TODO: dispatch by input.type:
44
+ // - 'checkout.session.completed' → activate subscription
45
+ // - 'invoice.paid' → extend access
46
+ // - 'invoice.payment_failed' → notify user + retry plan
47
+ return Response.json({ received: true })
48
+ },
49
+ })