@forgeailab/create-spark 0.1.1 → 0.1.2

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.
Files changed (133) hide show
  1. package/package.json +7 -4
  2. package/packs/README.md +132 -0
  3. package/packs/ai-anthropic/files/app/api/ai/route.ts +57 -0
  4. package/packs/ai-anthropic/files/lib/anthropic.ts +15 -0
  5. package/packs/ai-anthropic/pack.toml +32 -0
  6. package/packs/ai-anthropic/skills/ai-feature-patterns/SKILL.md +87 -0
  7. package/packs/ai-anthropic/tasks.yaml +9 -0
  8. package/packs/ai-openai/files/app/api/ai-openai/route.ts +55 -0
  9. package/packs/ai-openai/files/lib/openai.ts +21 -0
  10. package/packs/ai-openai/pack.toml +30 -0
  11. package/packs/ai-openai/tasks.yaml +9 -0
  12. package/packs/analytics-posthog/files/components/PostHogProvider.tsx +19 -0
  13. package/packs/analytics-posthog/files/lib/posthog/client.ts +20 -0
  14. package/packs/analytics-posthog/files/lib/posthog/server.ts +24 -0
  15. package/packs/analytics-posthog/pack.toml +35 -0
  16. package/packs/analytics-posthog/tasks.yaml +15 -0
  17. package/packs/auth-better-auth/files/app/(auth)/login/page.tsx +58 -0
  18. package/packs/auth-better-auth/files/app/api/auth/[...all]/route.ts +4 -0
  19. package/packs/auth-better-auth/files/lib/auth.ts +21 -0
  20. package/packs/auth-better-auth/pack.toml +32 -0
  21. package/packs/auth-better-auth/tasks.yaml +10 -0
  22. package/packs/auth-better-auth-pg/files/app/api/auth/[...all]/route.ts +4 -0
  23. package/packs/auth-better-auth-pg/files/lib/auth.ts +86 -0
  24. package/packs/auth-better-auth-pg/pack.toml +32 -0
  25. package/packs/auth-better-auth-pg/tasks.yaml +17 -0
  26. package/packs/auth-supabase/files/app/(auth)/login/page.tsx +64 -0
  27. package/packs/auth-supabase/files/app/auth/callback/route.ts +15 -0
  28. package/packs/auth-supabase/files/middleware.ts +41 -0
  29. package/packs/auth-supabase/pack.toml +34 -0
  30. package/packs/auth-supabase/tasks.yaml +10 -0
  31. package/packs/db-postgres/files/compose/postgres.yml +28 -0
  32. package/packs/db-postgres/files/docker-compose.include.yml +1 -0
  33. package/packs/db-postgres/files/docker-compose.yml +6 -0
  34. package/packs/db-postgres/files/drizzle.config.ts +10 -0
  35. package/packs/db-postgres/files/lib/db/index.ts +10 -0
  36. package/packs/db-postgres/files/lib/db/schema.ts +11 -0
  37. package/packs/db-postgres/pack.toml +53 -0
  38. package/packs/db-postgres/tasks.yaml +11 -0
  39. package/packs/db-sqlite/files/drizzle.config.ts +10 -0
  40. package/packs/db-sqlite/files/lib/db.ts +8 -0
  41. package/packs/db-sqlite/files/lib/schema.ts +13 -0
  42. package/packs/db-sqlite/pack.toml +34 -0
  43. package/packs/db-sqlite/tasks.yaml +6 -0
  44. package/packs/db-supabase/files/lib/supabase/client.ts +8 -0
  45. package/packs/db-supabase/files/lib/supabase/server.ts +27 -0
  46. package/packs/db-supabase/pack.toml +32 -0
  47. package/packs/db-supabase/skills/supabase-patterns/SKILL.md +82 -0
  48. package/packs/db-supabase/tasks.yaml +6 -0
  49. package/packs/deploy-vercel/files/docs/deploy.md +21 -0
  50. package/packs/deploy-vercel/files/vercel.json +4 -0
  51. package/packs/deploy-vercel/pack.toml +30 -0
  52. package/packs/deploy-vercel/tasks.yaml +14 -0
  53. package/packs/docker-compose-dev/files/.env.docker.example +2 -0
  54. package/packs/docker-compose-dev/files/compose/redis.yml +17 -0
  55. package/packs/docker-compose-dev/files/docker-compose.include.yml +1 -0
  56. package/packs/docker-compose-dev/files/docker-compose.yml +6 -0
  57. package/packs/docker-compose-dev/pack.toml +38 -0
  58. package/packs/docker-compose-dev/tasks.yaml +9 -0
  59. package/packs/email-resend/files/app/api/email/test/route.ts +38 -0
  60. package/packs/email-resend/files/emails/welcome.tsx +66 -0
  61. package/packs/email-resend/files/lib/email.ts +40 -0
  62. package/packs/email-resend/pack.toml +34 -0
  63. package/packs/email-resend/tasks.yaml +9 -0
  64. package/packs/example/pack.toml +69 -0
  65. package/packs/payments-stripe/files/app/api/billing-portal/route.ts +24 -0
  66. package/packs/payments-stripe/files/app/api/checkout/route.ts +58 -0
  67. package/packs/payments-stripe/files/app/api/webhooks/stripe/route.ts +84 -0
  68. package/packs/payments-stripe/files/lib/stripe.ts +60 -0
  69. package/packs/payments-stripe/pack.toml +49 -0
  70. package/packs/payments-stripe/skills/stripe-patterns/SKILL.md +93 -0
  71. package/packs/payments-stripe/tasks.yaml +16 -0
  72. package/packs/sync-zero/files/components/ZeroProvider.tsx +3 -0
  73. package/packs/sync-zero/files/compose/zero-cache.yml +26 -0
  74. package/packs/sync-zero/files/docker-compose.include.yml +1 -0
  75. package/packs/sync-zero/files/docker-compose.yml +6 -0
  76. package/packs/sync-zero/files/lib/zero/client.ts +18 -0
  77. package/packs/sync-zero/files/lib/zero/schema.ts +17 -0
  78. package/packs/sync-zero/files/zero.config.ts +26 -0
  79. package/packs/sync-zero/pack.toml +61 -0
  80. package/packs/sync-zero/skills/zero-patterns/SKILL.md +69 -0
  81. package/packs/sync-zero/tasks.yaml +16 -0
  82. package/packs/testing-playwright/files/e2e/example.spec.ts +7 -0
  83. package/packs/testing-playwright/files/playwright.config.ts +33 -0
  84. package/packs/testing-playwright/pack.toml +25 -0
  85. package/packs/testing-playwright/tasks.yaml +9 -0
  86. package/packs/ui-shadcn/files/app/globals.css +56 -0
  87. package/packs/ui-shadcn/files/components/ui/button.tsx +47 -0
  88. package/packs/ui-shadcn/files/components/ui/card.tsx +33 -0
  89. package/packs/ui-shadcn/files/lib/utils.ts +6 -0
  90. package/packs/ui-shadcn/files/postcss.config.mjs +7 -0
  91. package/packs/ui-shadcn/files/tailwind.config.ts +57 -0
  92. package/packs/ui-shadcn/pack.toml +44 -0
  93. package/packs/ui-shadcn/skills/shadcn-dashboard-patterns/SKILL.md +85 -0
  94. package/packs/ui-shadcn/tasks.yaml +6 -0
  95. package/presets/docs-site.toml +4 -0
  96. package/presets/internal-tool.toml +4 -0
  97. package/presets/lean-saas.toml +4 -0
  98. package/presets/local-ai-mvp.toml +4 -0
  99. package/presets/saas-classic.toml +4 -0
  100. package/src/paths.ts +22 -4
  101. package/templates/README.md +43 -0
  102. package/templates/astro/README.md +3 -0
  103. package/templates/astro/template.toml +4 -0
  104. package/templates/astro-starlight/README.md +3 -0
  105. package/templates/astro-starlight/template.toml +4 -0
  106. package/templates/nextjs/.ai/architecture.md +13 -0
  107. package/templates/nextjs/.ai/board.md +7 -0
  108. package/templates/nextjs/.ai/product-spec.md +11 -0
  109. package/templates/nextjs/.claude/skills/.gitkeep +0 -0
  110. package/templates/nextjs/.codex/skills/.gitkeep +0 -0
  111. package/templates/nextjs/AGENTS.md +95 -0
  112. package/templates/nextjs/CLAUDE.md +3 -0
  113. package/templates/nextjs/README.md +20 -0
  114. package/templates/nextjs/anvil.config.json +4 -0
  115. package/templates/nextjs/app/(app)/home/page.tsx +43 -0
  116. package/templates/nextjs/app/(app)/home/posts-panel.tsx +83 -0
  117. package/templates/nextjs/app/(app)/layout.tsx +12 -0
  118. package/templates/nextjs/app/(auth)/login/page.tsx +97 -0
  119. package/templates/nextjs/app/globals.css +23 -0
  120. package/templates/nextjs/app/layout.tsx +20 -0
  121. package/templates/nextjs/app/page.tsx +39 -0
  122. package/templates/nextjs/lib/auth-placeholder.ts +21 -0
  123. package/templates/nextjs/lib/posts-placeholder.ts +30 -0
  124. package/templates/nextjs/next.config.ts +5 -0
  125. package/templates/nextjs/package.json +26 -0
  126. package/templates/nextjs/postcss.config.mjs +7 -0
  127. package/templates/nextjs/template.toml +4 -0
  128. package/templates/nextjs/tsconfig.json +27 -0
  129. package/templates/nextjs/types/post.ts +13 -0
  130. package/templates/one/README.md +5 -0
  131. package/templates/one/template.toml +4 -0
  132. package/templates/vite-react/README.md +3 -0
  133. package/templates/vite-react/template.toml +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeailab/create-spark",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Interactive scaffolder for spark projects with guided pack picker.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -8,14 +8,17 @@
8
8
  },
9
9
  "files": [
10
10
  "src",
11
- "README.md"
11
+ "README.md",
12
+ "templates",
13
+ "packs",
14
+ "presets"
12
15
  ],
13
16
  "bin": {
14
17
  "create-spark": "./src/cli.ts"
15
18
  },
16
19
  "dependencies": {
17
- "@forgeailab/spark": "^0.1.1",
18
- "@forgeailab/spark-schema": "^0.1.1",
20
+ "@forgeailab/spark": "^0.1.2",
21
+ "@forgeailab/spark-schema": "^0.1.2",
19
22
  "@clack/prompts": "latest",
20
23
  "citty": "latest",
21
24
  "picocolors": "latest"
@@ -0,0 +1,132 @@
1
+ # Packs
2
+
3
+ A **pack** is a self-contained unit of capability — auth, db, payments, UI, AI SDK, email, deploy target, … — that the `spark` CLI can install into a scaffolded project. Packs are TOML-manifested, declarative (no shell hooks), and capability-resolved.
4
+
5
+ ## Directory layout
6
+
7
+ ```
8
+ packs/<name>/
9
+ ├── pack.toml # manifest (REQUIRED)
10
+ ├── files/ # tree of files to copy into the project (optional)
11
+ ├── skills/ # SKILL.md folders shipped with this pack (optional)
12
+ └── tasks.yaml # board tasks seeded into .ai/board.md on install (optional)
13
+ ```
14
+
15
+ ## `pack.toml`
16
+
17
+ See [`docs/pack-spec.md`](../docs/pack-spec.md) for the full schema. The Zod source of truth is `packages/spark-schema/src/pack.ts`.
18
+
19
+ A minimum-viable pack:
20
+
21
+ ```toml
22
+ name = "db-sqlite"
23
+ version = "0.1.0"
24
+ category = "db"
25
+ description = "Local SQLite database via bun:sqlite + drizzle-orm."
26
+
27
+ provides = ["db"]
28
+ requires = []
29
+ conflicts = ["db"]
30
+ requires_runtime = ["server"]
31
+ compatible_scaffolds = ["nextjs"]
32
+
33
+ [dependencies]
34
+ runtime = ["drizzle-orm"]
35
+ dev = ["drizzle-kit"]
36
+
37
+ [env]
38
+ required = ["DATABASE_URL"]
39
+
40
+ [[files]]
41
+ mode = "create"
42
+ from = "files/lib/db.ts"
43
+ to = "lib/db.ts"
44
+
45
+ [tasks]
46
+ file = "tasks.yaml"
47
+ ```
48
+
49
+ ## File modes
50
+
51
+ | Mode | Behavior |
52
+ |---|---|
53
+ | `create` | Fails if the destination already exists |
54
+ | `append` | Idempotent; uses `# >>> spark:<pack> >>>` / `# <<<` markers |
55
+ | `merge-json` | Deep-merges into an existing JSON file with deterministic key order |
56
+ | `template` | Handlebars-style substitution from `spark.config.json` (e.g. `{{appName}}`) |
57
+
58
+ ## Capability enums (closed)
59
+
60
+ **Pack capabilities** (`provides` / `requires` / `conflicts`):
61
+ `db, auth, payments, email, ui-kit, local-runtime, deploy-target, e2e, ai-sdk, blob-storage, analytics, sync`
62
+
63
+ Exclusive (one provider per project): `db, auth, payments, ui-kit, sync`
64
+ Non-exclusive (multiple providers OK): `ai-sdk, analytics, email, blob-storage, e2e, deploy-target, local-runtime`
65
+
66
+ **Template capabilities** (`requires_runtime`):
67
+ `static, server, react, native, vue, svelte, mdx-content, edge-runtime`
68
+
69
+ The two enums are separate and never overlap. Adding a new value requires a registry-wide change.
70
+
71
+ ## What is NOT allowed
72
+
73
+ - `post_install`, `hooks`, `pre_add`, `scripts` — packs MUST be declarative. If your pack needs a setup step that can't be expressed in `[[files]]`, ship it as a seeded board task the user runs manually.
74
+ - Pack-name conflicts (`conflicts = ["other-pack-name"]`). Use capability tags only.
75
+ - Cross-pack file ownership. Two packs MUST NOT write the same `to` path with `create` mode.
76
+
77
+ ## Adding a new pack
78
+
79
+ ```bash
80
+ # In Claude Code:
81
+ /new-pack realtime-supabase category=db
82
+ ```
83
+
84
+ Then fill in the generated `packs/realtime-supabase/pack.toml`. Validate with:
85
+
86
+ ```bash
87
+ bun -e "import {parsePackToml} from './packages/spark-schema/src/parse.ts'; import {readFileSync} from 'node:fs'; const r = parsePackToml(readFileSync('packs/realtime-supabase/pack.toml','utf8')); console.log(r.ok ? 'OK' : r.error);"
88
+ ```
89
+
90
+ See `packs/example/pack.toml` for a manifest that exercises every field.
91
+
92
+ ## v1 catalog
93
+
94
+ Each pack is either **copy** (ships file trees the user owns) or **hybrid** (ships thin wiring + imports runtime logic from a versioned `@forgeailab/spark-*` helper under `libs/`). The mode is inferred from the presence of a `[runtime_package]` block in the manifest.
95
+
96
+ | Pack | Category | Mode | Runtime helper |
97
+ |---|---|---|---|
98
+ | `auth-better-auth` | auth | **hybrid** | `@forgeailab/spark-auth-better-auth` (SQLite) |
99
+ | `auth-better-auth-pg` | auth | **hybrid** | `@forgeailab/spark-auth-better-auth` (Postgres) |
100
+ | `auth-supabase` | auth | copy | — |
101
+ | `db-sqlite` | db | copy | — |
102
+ | `db-postgres` | db | copy | — |
103
+ | `db-supabase` | db | copy | — |
104
+ | `sync-zero` | infra | **hybrid** | `@forgeailab/spark-sync-zero` |
105
+ | `payments-stripe` | payments | **hybrid** | `@forgeailab/spark-stripe-helpers` |
106
+ | `ai-anthropic` | ai | **hybrid** | `@forgeailab/spark-anthropic` |
107
+ | `ai-openai` | ai | copy | — |
108
+ | `ui-shadcn` | ui | copy | — |
109
+ | `email-resend` | email | copy | — |
110
+ | `analytics-posthog` | analytics | copy | — |
111
+ | `docker-compose-dev` | infra | copy | — |
112
+ | `testing-playwright` | testing | copy | — |
113
+ | `deploy-vercel` | deploy | copy | — |
114
+
115
+ ### Picking a db + auth pair
116
+
117
+ `auth-better-auth` and `auth-better-auth-pg` share the same runtime helper
118
+ (`@forgeailab/spark-auth-better-auth`) — they differ only in the `provider:`
119
+ their generated `lib/auth.ts` template hands to `drizzleAdapter`. Pair them:
120
+
121
+ - `db-sqlite` + `auth-better-auth` — fastest path, single file db, no infra.
122
+ - `db-postgres` + `auth-better-auth-pg` — production-shaped, **required for `sync-zero`** (Zero needs Postgres logical replication).
123
+ - `db-supabase` + `auth-better-auth-pg` — Supabase-hosted Postgres + Better Auth on top.
124
+
125
+ The two auth packs both `conflicts = ["auth"]`, so the resolver prevents
126
+ installing both. Mixing wrong pairs (e.g. `db-sqlite` + `auth-better-auth-pg`)
127
+ typechecks but fails at runtime — the drizzle adapter will reject sqlite tables
128
+ with `provider: 'pg'`.
129
+
130
+ The hybrid packs were authored against [`reference/full-stack-saas/`](../reference/full-stack-saas/) — the canonical integration showing all four helpers working together. When debugging a hybrid pack, start there.
131
+
132
+ See the root `README.md` for the catalog summary.
@@ -0,0 +1,57 @@
1
+ import { anthropic, streamResponse, type AnthropicChatMessage } from '@/lib/anthropic';
2
+ import { NextResponse, type NextRequest } from 'next/server';
3
+
4
+ export const runtime = 'nodejs';
5
+
6
+ const DEFAULT_MODEL = 'claude-sonnet-4-5';
7
+ const DEFAULT_MAX_TOKENS = 1_024;
8
+ const HARD_MAX_TOKENS = 4_096;
9
+
10
+ type ChatRequest = {
11
+ messages?: unknown;
12
+ system?: string;
13
+ model?: string;
14
+ maxTokens?: number;
15
+ };
16
+
17
+ function normalizeMessages(value: unknown): AnthropicChatMessage[] | undefined {
18
+ if (!Array.isArray(value)) return undefined;
19
+ const messages: AnthropicChatMessage[] = [];
20
+ for (const entry of value) {
21
+ if (typeof entry !== 'object' || entry === null) return undefined;
22
+ const role = 'role' in entry ? entry.role : undefined;
23
+ const content = 'content' in entry ? entry.content : undefined;
24
+ if ((role !== 'user' && role !== 'assistant') || typeof content !== 'string') return undefined;
25
+ messages.push({ role, content });
26
+ }
27
+ return messages.length > 0 ? messages : undefined;
28
+ }
29
+
30
+ export async function POST(request: NextRequest) {
31
+ const body = (await request.json().catch(() => ({}))) as ChatRequest;
32
+ const messages = normalizeMessages(body.messages);
33
+
34
+ if (!messages) {
35
+ return NextResponse.json(
36
+ { error: 'messages must be a non-empty array of user/assistant strings' },
37
+ { status: 400 },
38
+ );
39
+ }
40
+
41
+ const maxTokens = Math.min(Math.max(1, body.maxTokens ?? DEFAULT_MAX_TOKENS), HARD_MAX_TOKENS);
42
+
43
+ const stream = streamResponse(anthropic, {
44
+ model: body.model ?? DEFAULT_MODEL,
45
+ max_tokens: maxTokens,
46
+ system: body.system,
47
+ messages,
48
+ });
49
+
50
+ return new Response(stream, {
51
+ headers: {
52
+ 'Content-Type': 'text/event-stream; charset=utf-8',
53
+ 'Cache-Control': 'no-cache, no-transform',
54
+ Connection: 'keep-alive',
55
+ },
56
+ });
57
+ }
@@ -0,0 +1,15 @@
1
+ import type Anthropic from '@anthropic-ai/sdk';
2
+ import { createAnthropicClient, streamResponse } from '@forgeailab/spark-anthropic';
3
+
4
+ function requireEnv(name: string): string {
5
+ const value = process.env[name];
6
+ if (!value) {
7
+ throw new Error(`Missing required environment variable: ${name}`);
8
+ }
9
+ return value;
10
+ }
11
+
12
+ export const anthropic = createAnthropicClient(requireEnv('ANTHROPIC_API_KEY'));
13
+
14
+ export { streamResponse };
15
+ export type AnthropicChatMessage = Anthropic.Messages.MessageParam;
@@ -0,0 +1,32 @@
1
+ name = "ai-anthropic"
2
+ version = "1.0.0"
3
+ category = "ai"
4
+ description = "Anthropic SDK client and streaming chat endpoint."
5
+ provides = ["ai-sdk"]
6
+ requires = []
7
+ conflicts = []
8
+ requires_runtime = ["server"]
9
+ compatible_scaffolds = []
10
+
11
+ [runtime_package]
12
+ package = "@forgeailab/spark-anthropic"
13
+ version = "^0.1"
14
+
15
+ [env]
16
+ required = ["ANTHROPIC_API_KEY"]
17
+
18
+ [[files]]
19
+ mode = "create"
20
+ from = "lib/anthropic.ts"
21
+ to = "lib/anthropic.ts"
22
+
23
+ [[files]]
24
+ mode = "create"
25
+ from = "app/api/ai/route.ts"
26
+ to = "app/api/ai/route.ts"
27
+
28
+ [skills]
29
+ copy = ["skills/ai-feature-patterns"]
30
+
31
+ [tasks]
32
+ file = "tasks.yaml"
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: ai-feature-patterns
3
+ description: Build Anthropic-backed AI features with streaming UX, prompt discipline, and cost controls. Use when implementing or reviewing features after the ai-anthropic pack is installed.
4
+ allowed-tools:
5
+ - Read
6
+ - Write
7
+ - Edit
8
+ - Bash
9
+ ---
10
+
11
+ # Skill: ai-feature-patterns
12
+
13
+ ## Goal
14
+
15
+ Add one focused AI capability that helps the product journey without turning the
16
+ MVP into a generic chat app. Keep prompts inspectable, streams responsive, and
17
+ cost bounded by server-side controls.
18
+
19
+ ## Recommended model
20
+
21
+ Opus 4.7 or GPT-5.5 for prompt architecture and evaluation. Sonnet 4.6 or GPT-5
22
+ family executor for endpoint, UI, and test wiring.
23
+
24
+ ## Inputs
25
+
26
+ Read these before changing AI behavior:
27
+
28
+ - `.ai/product-spec.md` for the user workflow and non-goals
29
+ - `.ai/architecture.md` for persistence, auth, and runtime boundaries
30
+ - `.ai/board.md` for the exact AI task and acceptance criteria
31
+ - `lib/anthropic.ts` for shared client setup
32
+ - `app/api/ai/route.ts` for request, stream, and guardrail behavior
33
+
34
+ If the spec does not name the decision or artifact the AI should improve, stop
35
+ and ask. Do not add chat just because an AI SDK is installed.
36
+
37
+ ## Prompt Patterns
38
+
39
+ - Put durable behavior in a server-owned system prompt.
40
+ - Put user-specific state in structured context, not prose pasted from the UI.
41
+ - Keep task prompts narrow: role, input facts, output format, refusal boundary.
42
+ - Prefer JSON or Markdown output contracts only when the UI actually needs them.
43
+ - Do not ask the model to enforce authorization, billing, or data access rules.
44
+ - Include the smallest useful context window; retrieve or summarize before
45
+ sending long histories.
46
+
47
+ ## Streaming UX
48
+
49
+ - Stream text when the user is waiting on generation or analysis.
50
+ - Show partial output in the destination surface, not a separate debug pane.
51
+ - Preserve a cancel path when requests can take more than a few seconds.
52
+ - Persist the final answer only after the stream completes successfully.
53
+ - Treat stream errors as recoverable UI state with a retry action.
54
+
55
+ ## Cost Controls
56
+
57
+ - Cap `max_tokens` on the server, even when the client sends a smaller hint.
58
+ - Rate limit by user, organization, or IP before calling Anthropic.
59
+ - Add a cheap preflight check for empty prompts and unsupported file sizes.
60
+ - Log model, token caps, latency, and user/account id for cost review.
61
+ - Use smaller models or cached summaries for low-stakes transformations.
62
+ - Keep generated artifacts small enough to review and edit.
63
+
64
+ ## Safety and Privacy
65
+
66
+ - Send only the data needed for the requested feature.
67
+ - Redact secrets and credentials from context before model calls.
68
+ - Avoid storing raw prompts if they may contain sensitive customer data.
69
+ - Make model output advisory unless the product spec explicitly automates action.
70
+ - Require human confirmation before sending emails, charging users, or deleting data.
71
+
72
+ ## Common Pitfalls
73
+
74
+ - Do not stream from the browser directly with the API key.
75
+ - Do not let the client choose unlimited tokens or arbitrary expensive models.
76
+ - Do not hide prompt changes in component code; keep prompts close to API routes.
77
+ - Do not make acceptance criteria depend on subjective model quality alone.
78
+ - Do not add vector search, agents, or tool calls unless the board asks for them.
79
+
80
+ ## Verification
81
+
82
+ Use a small real prompt and confirm all of these:
83
+
84
+ - The endpoint streams incremental `text` events.
85
+ - The final event is `done`.
86
+ - Invalid input returns a 400 before calling Anthropic.
87
+ - Token caps and rate limits are enforced server-side.
@@ -0,0 +1,9 @@
1
+ epic: AI
2
+
3
+ tasks:
4
+ - id: AI-001
5
+ title: Add cost guardrails (token caps, rate limits)
6
+ status: Clarifying
7
+ acceptance:
8
+ - AI requests have server-side max token caps.
9
+ - Repeated requests are rate limited before calling Anthropic.
@@ -0,0 +1,55 @@
1
+ import { getOpenAIClient, OPENAI_CHAT_MODEL, type OpenAIChatMessage } from "../../../lib/openai";
2
+
3
+ export const runtime = "nodejs";
4
+
5
+ type ChatRequest = {
6
+ messages?: OpenAIChatMessage[];
7
+ prompt?: string;
8
+ };
9
+
10
+ function normalizeMessages(body: ChatRequest): OpenAIChatMessage[] {
11
+ if (Array.isArray(body.messages) && body.messages.length > 0) {
12
+ return body.messages;
13
+ }
14
+
15
+ if (body.prompt) {
16
+ return [{ role: "user", content: body.prompt }];
17
+ }
18
+
19
+ throw new Error("Request body must include messages or prompt.");
20
+ }
21
+
22
+ export async function POST(request: Request): Promise<Response> {
23
+ const body = (await request.json()) as ChatRequest;
24
+ const messages = normalizeMessages(body);
25
+
26
+ const stream = await getOpenAIClient().chat.completions.create({
27
+ model: OPENAI_CHAT_MODEL,
28
+ messages,
29
+ stream: true,
30
+ });
31
+
32
+ const encoder = new TextEncoder();
33
+ const readable = new ReadableStream<Uint8Array>({
34
+ async start(controller) {
35
+ try {
36
+ for await (const chunk of stream) {
37
+ const content = chunk.choices[0]?.delta?.content;
38
+ if (content) {
39
+ controller.enqueue(encoder.encode(content));
40
+ }
41
+ }
42
+ controller.close();
43
+ } catch (error) {
44
+ controller.error(error);
45
+ }
46
+ },
47
+ });
48
+
49
+ return new Response(readable, {
50
+ headers: {
51
+ "Cache-Control": "no-cache",
52
+ "Content-Type": "text/plain; charset=utf-8",
53
+ },
54
+ });
55
+ }
@@ -0,0 +1,21 @@
1
+ import OpenAI from "openai";
2
+
3
+ let client: OpenAI | undefined;
4
+
5
+ export type OpenAIChatMessage = {
6
+ role: "developer" | "system" | "user" | "assistant";
7
+ content: string;
8
+ };
9
+
10
+ export const OPENAI_CHAT_MODEL = "gpt-5.2";
11
+
12
+ export function getOpenAIClient(): OpenAI {
13
+ const apiKey = process.env.OPENAI_API_KEY;
14
+
15
+ if (!apiKey) {
16
+ throw new Error("OPENAI_API_KEY is required to use the OpenAI client.");
17
+ }
18
+
19
+ client ??= new OpenAI({ apiKey });
20
+ return client;
21
+ }
@@ -0,0 +1,30 @@
1
+ name = "ai-openai"
2
+ version = "1.0.0"
3
+ category = "ai"
4
+ description = "OpenAI SDK client wrapper and streaming chat endpoint."
5
+ provides = ["ai-sdk"]
6
+ requires = []
7
+ conflicts = []
8
+ requires_runtime = ["server"]
9
+ compatible_scaffolds = []
10
+
11
+ [dependencies]
12
+ runtime = ["openai"]
13
+ dev = []
14
+
15
+ [env]
16
+ required = ["OPENAI_API_KEY"]
17
+ optional = []
18
+
19
+ [[files]]
20
+ mode = "create"
21
+ from = "lib/openai.ts"
22
+ to = "lib/openai.ts"
23
+
24
+ [[files]]
25
+ mode = "create"
26
+ from = "app/api/ai-openai/route.ts"
27
+ to = "app/api/ai-openai/route.ts"
28
+
29
+ [tasks]
30
+ file = "tasks.yaml"
@@ -0,0 +1,9 @@
1
+ epic: AI
2
+ tasks:
3
+ - id: AI-101
4
+ title: Add usage tracking + cost guardrails
5
+ status: Clarifying
6
+ acceptance:
7
+ - OpenAI requests record model, token usage, and user or workspace context.
8
+ - The endpoint enforces a documented per-user or per-workspace usage limit.
9
+ - Over-limit requests return a clear non-200 response without calling OpenAI.
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { useEffect } from "react";
5
+ import posthog from "posthog-js";
6
+ import { PostHogProvider as Provider } from "posthog-js/react";
7
+ import { getPostHogClient } from "../lib/posthog/client";
8
+
9
+ type PostHogProviderProps = {
10
+ children: ReactNode;
11
+ };
12
+
13
+ export function PostHogProvider({ children }: PostHogProviderProps) {
14
+ useEffect(() => {
15
+ getPostHogClient();
16
+ }, []);
17
+
18
+ return <Provider client={posthog}>{children}</Provider>;
19
+ }
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import posthog from "posthog-js";
4
+
5
+ const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
6
+
7
+ export function getPostHogClient(): typeof posthog {
8
+ const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
9
+ const host = process.env.NEXT_PUBLIC_POSTHOG_HOST ?? DEFAULT_POSTHOG_HOST;
10
+ const loaded = (posthog as typeof posthog & { __loaded?: boolean }).__loaded;
11
+
12
+ if (typeof window !== "undefined" && key && !loaded) {
13
+ posthog.init(key, {
14
+ api_host: host,
15
+ capture_pageview: false,
16
+ });
17
+ }
18
+
19
+ return posthog;
20
+ }
@@ -0,0 +1,24 @@
1
+ import { PostHog } from "posthog-node";
2
+
3
+ const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
4
+
5
+ let client: PostHog | undefined;
6
+
7
+ export function getPostHogServerClient(): PostHog | undefined {
8
+ const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
9
+
10
+ if (!key) {
11
+ return undefined;
12
+ }
13
+
14
+ client ??= new PostHog(key, {
15
+ host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? DEFAULT_POSTHOG_HOST,
16
+ });
17
+
18
+ return client;
19
+ }
20
+
21
+ export async function shutdownPostHogServerClient(): Promise<void> {
22
+ await client?.shutdown();
23
+ client = undefined;
24
+ }
@@ -0,0 +1,35 @@
1
+ name = "analytics-posthog"
2
+ version = "1.0.0"
3
+ category = "analytics"
4
+ description = "PostHog browser and server analytics helpers."
5
+ provides = ["analytics"]
6
+ requires = []
7
+ conflicts = []
8
+ requires_runtime = ["server"]
9
+ compatible_scaffolds = []
10
+
11
+ [dependencies]
12
+ runtime = ["posthog-js", "posthog-node"]
13
+ dev = []
14
+
15
+ [env]
16
+ required = ["NEXT_PUBLIC_POSTHOG_KEY"]
17
+ optional = ["NEXT_PUBLIC_POSTHOG_HOST"]
18
+
19
+ [[files]]
20
+ mode = "create"
21
+ from = "lib/posthog/client.ts"
22
+ to = "lib/posthog/client.ts"
23
+
24
+ [[files]]
25
+ mode = "create"
26
+ from = "lib/posthog/server.ts"
27
+ to = "lib/posthog/server.ts"
28
+
29
+ [[files]]
30
+ mode = "create"
31
+ from = "components/PostHogProvider.tsx"
32
+ to = "components/PostHogProvider.tsx"
33
+
34
+ [tasks]
35
+ file = "tasks.yaml"
@@ -0,0 +1,15 @@
1
+ epic: Analytics
2
+ tasks:
3
+ - id: ANALYTICS-001
4
+ title: Wire posthog provider into app root
5
+ status: Clarifying
6
+ acceptance:
7
+ - The app root wraps client routes with PostHogProvider.
8
+ - Page views are captured intentionally, either by router events or a documented manual call.
9
+ - id: ANALYTICS-002
10
+ title: Add consent banner
11
+ status: Clarifying
12
+ acceptance:
13
+ - Users can accept or decline analytics before tracking starts.
14
+ - The consent choice persists across reloads.
15
+ - PostHog capture is disabled when consent is declined.
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import { useState, useTransition } from 'react';
4
+ import { createAuthClient } from 'better-auth/react';
5
+
6
+ const authClient = createAuthClient();
7
+ type SocialProvider = 'github' | 'google';
8
+
9
+ export default function LoginPage() {
10
+ const [error, setError] = useState<string | null>(null);
11
+ const [isPending, startTransition] = useTransition();
12
+
13
+ function signIn(provider: SocialProvider) {
14
+ setError(null);
15
+ startTransition(() => {
16
+ void authClient.signIn
17
+ .social({
18
+ provider,
19
+ callbackURL: '/',
20
+ })
21
+ .then((result) => {
22
+ if (result.error) {
23
+ setError(result.error.message ?? 'Unable to sign in.');
24
+ }
25
+ });
26
+ });
27
+ }
28
+
29
+ return (
30
+ <main className="mx-auto flex min-h-screen w-full max-w-sm flex-col justify-center px-6">
31
+ <div className="space-y-2">
32
+ <h1 className="text-2xl font-semibold tracking-tight">Sign in</h1>
33
+ <p className="text-sm text-muted-foreground">Continue with a configured OAuth provider.</p>
34
+ </div>
35
+
36
+ <div className="mt-8 grid gap-3">
37
+ <button
38
+ className="inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
39
+ disabled={isPending}
40
+ onClick={() => signIn('github')}
41
+ type="button"
42
+ >
43
+ Continue with GitHub
44
+ </button>
45
+ <button
46
+ className="inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
47
+ disabled={isPending}
48
+ onClick={() => signIn('google')}
49
+ type="button"
50
+ >
51
+ Continue with Google
52
+ </button>
53
+ </div>
54
+
55
+ {error ? <p className="mt-4 text-sm text-destructive">{error}</p> : null}
56
+ </main>
57
+ );
58
+ }
@@ -0,0 +1,4 @@
1
+ import { createAuthHandler } from '@forgeailab/spark-auth-better-auth';
2
+ import { auth } from '@/lib/auth';
3
+
4
+ export const { GET, POST } = createAuthHandler(auth);