ugly-app 0.1.0 → 0.1.1

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": "ugly-app",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "main": "./dist/server/index.js",
6
6
  "exports": {
@@ -13,12 +13,13 @@
13
13
  }
14
14
  },
15
15
  "bin": {
16
- "ugly-app": "./dist/cli/index.js"
16
+ "ugly-app": "dist/cli/index.js"
17
17
  },
18
18
  "scripts": {
19
19
  "build": "tsc",
20
20
  "test": "vitest run",
21
- "test:watch": "vitest"
21
+ "test:watch": "vitest",
22
+ "release": "npm version patch --force && npm run build && npm publish && git push --follow-tags"
22
23
  },
23
24
  "dependencies": {
24
25
  "@anthropic-ai/sdk": "^0.80.0",
@@ -31,7 +32,6 @@
31
32
  "commander": "^12.0.0",
32
33
  "concurrently": "^9.0.0",
33
34
  "cookie-parser": "^1.4.7",
34
- "elevenlabs": "^1.59.0",
35
35
  "express": "^4.21.1",
36
36
  "fs-extra": "^11.0.0",
37
37
  "groq-sdk": "^1.1.1",
@@ -64,7 +64,7 @@
64
64
  "@typescript-eslint/parser": "^8.0.0",
65
65
  "@vitejs/plugin-react": "^6.0.1",
66
66
  "eslint": "^8.57.0",
67
- "eslint-watch": "^7.0.0",
67
+ "eslint-watch": "^8.0.0",
68
68
  "html2canvas": "^1.4.1",
69
69
  "jsdom": "^29.0.1",
70
70
  "mongodb-memory-server": "^10.0.0",
@@ -1,18 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(git add:*)",
5
- "Bash(git check-ignore:*)",
6
- "Bash(ls -la /Users/admin/Documents/GitHub/app/server/backend/DB*)",
7
- "Bash(npx tsc:*)",
8
- "Bash(npx vitest:*)",
9
- "Bash(grep -E \"\\\\.ts$\")",
10
- "Bash(npm test:*)",
11
- "Bash(git push:*)",
12
- "Bash(git rm:*)",
13
- "Bash(npm run:*)",
14
- "Bash(git worktree:*)",
15
- "Bash(git remote:*)"
16
- ]
17
- }
18
- }
package/.env.example DELETED
@@ -1,9 +0,0 @@
1
- # AI Providers (all optional — Together AI works with TOGETHER_API_KEY alone)
2
- ANTHROPIC_API_KEY=
3
- OPENAI_API_KEY=
4
- GOOGLE_API_KEY=
5
- GROQ_API_KEY=
6
- FAL_API_KEY=
7
- ELEVENLABS_API_KEY=
8
- DEEPGRAM_API_KEY=
9
- MAINTAIN_BOT_USER_ID=
@@ -1,113 +0,0 @@
1
- {
2
- "manifest": {
3
- "name": "ugly-app",
4
- "version": "0.1.0",
5
- "type": "module",
6
- "main": "./dist/server/index.js",
7
- "exports": {
8
- ".": "./dist/server/index.js",
9
- "./shared": "./dist/shared/index.js",
10
- "./client": "./dist/client/index.js",
11
- "./playwright": "./dist/playwright/index.js",
12
- "./webrtc": {
13
- "default": "./dist/webrtc/index.js"
14
- }
15
- },
16
- "bin": {
17
- "web": "dist/cli/index.js"
18
- },
19
- "scripts": {
20
- "build": "tsc",
21
- "test": "vitest run",
22
- "test:watch": "vitest"
23
- },
24
- "dependencies": {
25
- "@anthropic-ai/sdk": "^0.80.0",
26
- "@deepgram/sdk": "^5.0.0",
27
- "elevenlabs": "^1.59.0",
28
- "@aws-sdk/client-s3": "^3.700.0",
29
- "@aws-sdk/s3-request-presigner": "^3.700.0",
30
- "@fal-ai/client": "^1.9.4",
31
- "@google/generative-ai": "^0.24.1",
32
- "bcrypt": "^6.0.0",
33
- "commander": "^12.0.0",
34
- "concurrently": "^9.0.0",
35
- "cookie-parser": "^1.4.7",
36
- "express": "^4.21.1",
37
- "fs-extra": "^11.0.0",
38
- "groq-sdk": "^1.1.1",
39
- "ioredis": "^5.10.1",
40
- "jose": "^6.0.13",
41
- "lru-cache": "^11.0.0",
42
- "mongodb": "^6.18.0",
43
- "nanoid": "^5.1.5",
44
- "nats": "^2.28.2",
45
- "openai": "^6.32.0",
46
- "together-ai": "^0.13.0",
47
- "ws": "^8.18.0",
48
- "zod": "^4.3.6",
49
- "handlebars": "^4.7.8",
50
- "onnxruntime-node": "^1.23.0"
51
- },
52
- "devDependencies": {
53
- "@playwright/test": "^1.58.2",
54
- "@testing-library/dom": "^10.4.1",
55
- "@testing-library/react": "^16.3.2",
56
- "@types/bcrypt": "^5.0.2",
57
- "@types/cookie-parser": "^1.4.10",
58
- "@types/express": "^5.0.0",
59
- "@types/fs-extra": "^11.0.4",
60
- "@types/node": "^22.0.0",
61
- "@types/react": "^19.2.14",
62
- "@types/react-dom": "^19.2.3",
63
- "@types/ws": "^8.5.13",
64
- "@typescript-eslint/eslint-plugin": "^8.0.0",
65
- "@typescript-eslint/parser": "^8.0.0",
66
- "@vitejs/plugin-react": "^6.0.1",
67
- "eslint": "^8.57.0",
68
- "eslint-watch": "^7.0.0",
69
- "html2canvas": "^1.4.1",
70
- "jsdom": "^29.0.1",
71
- "mongodb-memory-server": "^10.0.0",
72
- "react": "^19.2.4",
73
- "react-dom": "^19.2.4",
74
- "typescript": "^5.9.3",
75
- "vitest": "^4.0.18"
76
- },
77
- "peerDependencies": {
78
- "@types/react": ">=18.0.0",
79
- "html2canvas": ">=1.0.0",
80
- "preact": ">=10.0.0",
81
- "react": ">=18.0.0",
82
- "react-dom": ">=18.0.0"
83
- },
84
- "peerDependenciesMeta": {
85
- "@types/react": {
86
- "optional": true
87
- },
88
- "html2canvas": {
89
- "optional": true
90
- },
91
- "preact": {
92
- "optional": true
93
- },
94
- "mediasoup": {
95
- "optional": true
96
- }
97
- },
98
- "_registry": "npm",
99
- "_loc": "/Users/admin/Library/Caches/Yarn/v6/npm-ugly-app-0.1.0-d3eac800-7702-4eda-affb-29c511f7037a-1774140424109/node_modules/ugly-app/package.json",
100
- "readmeFilename": "README.md",
101
- "readme": "# website-core\n\nA full-stack TypeScript framework for building production-ready web applications. Provides an opinionated architecture combining an Express backend, React frontend, and MongoDB database with built-in authentication, real-time communication, storage, AI integration, and audio streaming.\n\n## What's Included\n\n- **Server**: Express with type-safe RPC routing and Zod schema validation\n- **WebSockets**: Bidirectional socket server/client with RPC calls, document tracking, and file uploads\n- **Database**: MongoDB with typed collections, cascade delete, indexes, migrations, and Change Streams\n- **Auth**: JWT + OAuth (ugly.bot by default, extensible via `AuthProvider` interface)\n- **Storage**: AWS S3 / Cloudflare R2 with presigned URLs and file promotion\n- **AI**: 5 text generation providers (Together, Claude, OpenAI, Google, Groq) + 3 image providers (Together, FAL, Google)\n- **Audio**: Text-to-speech and speech-to-text streaming with React hooks (`useTTS`, `useSTT`), optional viseme data for lip sync, and word-level timestamps\n- **Logging**: Multi-channel logging to MongoDB (error, console, perf, feedback) with server error capture, deduplication, and classification\n- **Queues**: NATS JetStream worker queues, Redis pub/sub with in-memory fallback\n- **Rate Limiting**: Per-user/per-operation token-bucket limiting with queue management\n- **Push Notifications**: Real-time delivery via WebSocket + Redis, with FCM support\n- **Feedback**: Built-in user feedback collection with screenshot capture\n- **CLI**: `web` command for dev, build, deploy, migrations, log queries, and auth utilities\n\n## Installation\n\n```bash\nnpm install website-core\n```\n\nPeer dependencies:\n\n```bash\nnpm install react react-dom html2canvas\n```\n\n## Quick Start\n\n### Scaffold a new project\n\n```bash\nnpx web init my-app\n```\n\n### Start development\n\n```bash\nyarn dev\n# Starts Docker services, Express server, Vite, TypeScript watcher, and ESLint concurrently\n```\n\n## Usage\n\n### Server\n\n```typescript\nimport { createApp } from 'website-core';\n\nconst app = await createApp({\n authProvider, // optional — defaults to ugly.bot OAuth\n pages, // optional — page definitions with SSR support\n onSocketMessage, // optional — handle custom WebSocket messages\n workerQueues, // optional — background job queue configs\n extendContext, // optional — add fields to handler context\n});\n```\n\n### Shared types and API definitions\n\n```typescript\nimport { fn, req, defineFunctions, defineRequests } from 'website-core/shared';\nimport { z } from 'website-core/shared';\n\nexport const functions = defineFunctions({\n greet: fn(\n z.object({ name: z.string() }),\n z.object({ message: z.string() })\n ),\n});\n```\n\n### React client\n\n```typescript\nimport { AppProvider, useApp, createRouter, createSocket } from 'website-core/client';\n```\n\n## Key APIs\n\n### Server — `createApp()`\n\nRegisters handlers with full context (user, db, storage, textGen, imageGen, logger, rateLimit):\n\n```typescript\napp.registerFunction('greet', functions.greet, async ({ input, userId, db, logger }) => {\n return { message: `Hello, ${input.name}` };\n});\n\napp.registerRequest('stream', requests.stream, async ({ input, res }) => {\n // streaming HTTP handler\n});\n```\n\nBuilt-in routes: `GET /health`, `POST /auth/verify`, `GET /auth/url`, `POST /logs/client`, `GET /my_feedback`\n\n### Database — `createTypedDB()`\n\n```typescript\nimport { defineCollections, defineDbIndexes } from 'website-core/shared';\n\nconst collections = defineCollections({\n notes: { schema: noteSchema, cascadeDelete: ['attachments'] },\n});\n\nconst db = createTypedDB(mongoClient, collections);\nawait db.notes.setDoc(id, data);\nawait db.notes.getDocs({ userId });\n```\n\n### WebSocket Client\n\n```typescript\nconst socket = createSocket({ url, registry });\n\nawait socket.call('greet', { name: 'world' }); // RPC call\nconst doc = await socket.getDoc('notes', id); // fetch document\nsocket.trackDoc('notes', id, (doc) => { /* live */ }); // live updates\nawait socket.uploadFile(file, { bucket: 'temp' }); // file upload\n```\n\n### AI — Text Generation\n\n```typescript\nconst textGen = createTextGen({ provider: 'claude' });\n\nconst text = await textGen.generate({ messages, model });\nconst json = await textGen.generateJson({ messages, schema, model });\nconst result = await textGen.generateWithTools({ messages, tools, model });\n```\n\nSupported providers: `together`, `claude`, `openai`, `google`, `groq`\n\n### AI — Image Generation\n\n```typescript\nconst imageGen = createImageGen({ provider: 'fal' });\nconst url = await imageGen.generate(prompt, { width: 1024, height: 1024 });\n```\n\nSupported providers: `together`, `fal`, `google`\n\n### Audio — TTS/STT React Hooks\n\n```typescript\n// Text-to-speech\nconst { playing, currentWord, play, stop } = useTTS(socket);\nawait play('Hello world');\n\n// With viseme data for lip sync (opt-in — adds latency)\nawait play('Hello world', { requestVisemes: true });\n// viseme events arrive via onViseme callback in useTTS options\n\n// Speech-to-text\nconst { transcript, isFinal, listening, start, stop } = useSTT(socket, {\n mode: 'realtime', // 'realtime' | 'batch' | 'auto'\n});\n\n// With word-level timestamps (opt-in — uses Groq verbose_json, slower)\nconst { transcript, words } = useSTT(socket, {\n mode: 'batch',\n requestWords: true,\n});\n// words: STTWord[] — each has { word, startMs, durationMs }\n```\n\nViseme and word types are exported from `website-core/shared`:\n\n```typescript\nimport type { TTSViseme, TTSVisemeName, STTWord } from 'website-core/shared';\n```\n\n### Rate Limiting\n\n```typescript\nconst rateLimit = createRateLimiter({\n windowMs: 3600_000, // 1 hour\n maxUnits: 1.0,\n queueDepth: 5,\n});\n\nawait rateLimit.check(userId, 'generate', 0.1);\nawait rateLimit.charge(userId, 'generate', actualCost);\n```\n\n### Worker Queues\n\n```typescript\nconst queue = createWorkerQueue({ stream: 'jobs', subject: 'jobs.process' });\n\nawait queue.enqueue({ type: 'email', to: 'user@example.com' });\nqueue.registerHandler('email', async (job) => { /* process */ });\nawait queue.start();\n```\n\n### Storage\n\n```typescript\nconst storage = createStorageClient();\n\nconst key = await storage.put(buffer, { bucket: 'temp', ext: 'png' });\nconst publicKey = await storage.moveToPublic(key);\nconst url = storage.url(publicKey);\n\n// Browser direct upload\nconst { uploadUrl, key } = await socket.call('uploadUrl', { ext: 'jpg' });\n```\n\n### Logging\n\n```typescript\nconst logger = createLogger({ db, userId });\n\nlogger.info('User signed in', { userId });\nlogger.perf('query', durationMs, { collection: 'notes' });\nlogger.feedback('Feature request', { screenshot, context });\nlogger.error('Something failed', error);\n```\n\n### Server Error Capture\n\n`captureServerError` persists unexpected errors to a MongoDB `errorLog` collection with automatic deduplication (repeated errors increment a `count` field instead of inserting duplicates). Expected/recoverable errors can be suppressed from persistence by registering patterns.\n\n```typescript\nimport {\n captureServerError,\n registerExpectedErrorPattern,\n classifyError,\n} from 'website-core';\n\n// Register known-recoverable patterns at startup — matched errors log to\n// console only and are never written to MongoDB\nregisterExpectedErrorPattern('ECONNRESET')\nregisterExpectedErrorPattern('[STT] Session ended before audio was received')\n\n// Capture an error in a catch block\ntry {\n await riskyOperation()\n} catch (err) {\n captureServerError('[MyService] Failed to process request', err, { userId })\n}\n\n// Classify a message manually ('expected' | 'unexpected')\nconst classification = classifyError('[STT] Session ended before audio was received')\n```\n\n### Push Notifications\n\n```typescript\nimport { sendPush } from 'website-core';\n\nawait sendPush(userId, { title: 'New message', body: 'You have a reply' });\n```\n\n## CLI Commands\n\n| Command | Description |\n|---|---|\n| `web init <name>` | Scaffold a new project |\n| `web dev` | Start all dev services (Docker, server, Vite, TS, ESLint) |\n| `web build` | Production build |\n| `web db:init` | Create/update MongoDB indexes |\n| `web db:migrate` | Run pending database migrations (`--status` to preview) |\n| `web publish:assets` | Deploy static assets to CDN (`--dry-run` to preview) |\n| `web purge:assets` | Remove old build artifacts (keeps last 3 by default) |\n| `web logs:local` | Query local dev logs |\n| `web logs:server` | Query server logs from MongoDB |\n| `web error:local` | Query local error logs |\n| `web error:server` | Query server error logs from MongoDB |\n| `web perf:local` | Query local performance metrics |\n| `web perf:server` | Query server performance metrics from MongoDB |\n| `web feedback` | Query user feedback submissions |\n| `web auth:create-account` | Create an account directly in the database |\n| `web auth:create-token` | Generate a JWT for a userId |\n| `web test:e2e` | Run Playwright end-to-end tests (`--headed` for browser UI) |\n\n## React Components\n\n```typescript\nimport {\n Button, Card, Text, Input, Modal, Toast, PageLayout,\n FeedbackButton,\n} from 'website-core/client';\n```\n\nThe `FeedbackButton` captures a screenshot and submits it with user context automatically.\n\n## Routing (Client)\n\n```typescript\nconst { useRouter, RouterProvider } = createRouter(pages);\n\nfunction App() {\n const { push, replace, back } = useRouter();\n return <RouterProvider fallback={<NotFound />} authGuard={<Login />} />;\n}\n```\n\nPage transitions are animated by default using `ViewFlipper`. The transition type (`PUSH`, `POP`, `REPLACE`, `NONE`) is inferred automatically from navigation calls.\n\n### Popups\n\n```typescript\nimport { usePopup } from 'website-core/client';\n\nfunction MyComponent() {\n const { showPopup } = usePopup();\n\n function openMenu() {\n const handle = showPopup(<MyMenu />, {\n mode: 'contextMenu', // 'block' | 'transient' | 'contextMenu'\n slideFrom: 'bottom', // 'left' | 'right' | 'top' | 'bottom' | 'none'\n onClose: () => console.log('closed'),\n animConfig: { duration: 250 },\n });\n\n // Dismiss programmatically\n handle.hide();\n }\n}\n```\n\n- **`block`** — modal overlay, blocks interaction behind it\n- **`transient`** — tapping the backdrop closes it\n- **`contextMenu`** — same as transient but typically for menus/pickers\n\n### Animation Primitives\n\n```typescript\nimport {\n createAnimatedValue,\n useAnimatedValue,\n Animated,\n easingFunctions,\n} from 'website-core/client';\nimport type { EasingFunction } from 'website-core/client';\n\n// Imperative animated value (RAF-driven, no React re-renders)\nconst spring = createAnimatedValue(0);\nspring.animateTo(1, { duration: 300, easing: easingFunctions.easeInOut });\n\n// React hook version\nconst opacity = useAnimatedValue(0);\n\n// Apply to DOM via ref — zero re-renders\n<Animated value={spring} style={(v) => ({ opacity: v, transform: `scale(${v})` })}>\n <div>Content</div>\n</Animated>\n```\n\n## Project Structure\n\nProjects built with `website-core` follow this layout:\n\n```\nmy-app/\n├── src/\n│ ├── server/ # Express handlers, DB collections, migrations\n│ ├── client/ # React pages and components\n│ └── shared/ # API definitions and shared types\n├── static/ # Build-time static assets\n└── docker-compose.dev.yml\n```\n\n## Environment Variables\n\n| Variable | Description |\n|---|---|\n| `MONGO_URL` | MongoDB connection string |\n| `JWT_SECRET` | Secret for signing JWT tokens |\n| `REDIS_URL` | Redis connection (optional, uses in-memory fallback) |\n| `NATS_URL` | NATS server URL |\n| `NATS_CREDS` | NATS credentials file path (Synadia Cloud) |\n| `R2_ACCOUNT_ID` | Cloudflare R2 account ID |\n| `R2_ACCESS_KEY_ID` | R2 access key |\n| `R2_SECRET_ACCESS_KEY` | R2 secret key |\n| `R2_BUCKET` | R2 bucket name |\n| `TOGETHER_API_KEY` | Together AI API key |\n| `ANTHROPIC_API_KEY` | Anthropic Claude API key |\n| `OPENAI_API_KEY` | OpenAI API key |\n| `GOOGLE_API_KEY` | Google Gemini API key |\n| `IS_CLOCK_SERVER` | Set to `true` on the instance that processes worker queues |\n\nClient-side variables must be prefixed with `VITE_`.\n\n## Tech Stack\n\n- **Runtime**: Node.js, TypeScript (ES2022, NodeNext modules)\n- **Server**: Express, ws (WebSockets)\n- **Frontend**: React 18, Vite, Tailwind CSS\n- **Database**: MongoDB\n- **Messaging**: NATS with JetStream\n- **Cache**: Redis (in-memory fallback for dev)\n- **Storage**: AWS S3 / Cloudflare R2\n- **Auth**: JWT (jose), OAuth\n- **AI**: Together AI, Anthropic, OpenAI, Google, Groq, FAL\n- **Audio**: InWorld TTS, Groq Whisper STT\n- **Validation**: Zod\n- **Testing**: Vitest, Playwright, mongodb-memory-server\n\n## Development\n\n```bash\nnpm run build # Compile TypeScript\nnpm test # Run unit tests\nnpm run test:watch # Watch mode\n```\n",
102
- "description": "A full-stack TypeScript framework for building production-ready web applications. Provides an opinionated architecture combining an Express backend, React frontend, and MongoDB database with built-in authentication, real-time communication, storage, AI integration, and audio streaming."
103
- },
104
- "artifacts": [],
105
- "remote": {
106
- "type": "copy",
107
- "registry": "npm",
108
- "hash": "1f1f44d3-e29b-4eb4-a53b-7939ba38f3b9-1774221755838",
109
- "reference": "/Users/admin/Documents/GitHub/app/.worktrees/ugly-app"
110
- },
111
- "registry": "npm",
112
- "hash": "1f1f44d3-e29b-4eb4-a53b-7939ba38f3b9-1774221755838"
113
- }
@@ -1,39 +0,0 @@
1
- # Managing Static Assets
2
-
3
- ## Static assets (images, fonts, icons)
4
- Place in `client/assets/` — Vite serves them at `/assets/filename.ext`
5
-
6
- ```tsx
7
- // Reference in React
8
- <img src="/assets/logo.png" alt="Logo" />
9
- ```
10
-
11
- ## Generating assets with imageGen
12
- ```ts
13
- app.registerFunction('createAsset', async (ctx, input: { prompt: string; name: string }) => {
14
- // Generate image
15
- const tempUrl = await ctx.imageGen.generate(input.prompt)
16
- // Extract temp key from URL
17
- const tempKey = tempUrl.split('/temp/')[1]
18
- // Promote to public assets
19
- const publicUrl = await ctx.storage.moveToPublic(tempKey, `assets/${input.name}.png`)
20
- return { publicUrl }
21
- })
22
- ```
23
-
24
- ## Recommended asset organization
25
- ```
26
- client/assets/
27
- icons/ — UI icons
28
- backgrounds/ — Background images
29
- generated/ — imageGen output (promoted to public bucket)
30
- public/ — Static files served at root (favicon.ico, robots.txt)
31
- ```
32
-
33
- ## Image sizes for flux schnell
34
- - Default: 1024×1024
35
- - Banners: use prompt guidance ("wide landscape format", "16:9 ratio")
36
- - Icons: generate at 1024×1024, scale down in CSS
37
-
38
- # Notes
39
- <!-- Claude: append observations here — record generated asset URLs -->
@@ -1,35 +0,0 @@
1
- # Checking Error Logs
2
-
3
- ## Query recent errors
4
- ```bash
5
- mongosh "$MONGODB_URI" --eval "
6
- db.errorLog.find({}, {message:1, stack:1, userId:1, created:1})
7
- .sort({created:-1}).limit(20).pretty()
8
- "
9
- ```
10
-
11
- ## Group by message (find patterns)
12
- ```bash
13
- mongosh "$MONGODB_URI" --eval "
14
- db.errorLog.aggregate([
15
- { \$group: { _id: '\$message', count: { \$sum: 1 }, last: { \$max: '\$created' } } },
16
- { \$sort: { count: -1 } },
17
- { \$limit: 10 }
18
- ]).pretty()
19
- "
20
- ```
21
-
22
- ## Filter by level
23
- ```bash
24
- mongosh "$MONGODB_URI" --eval "
25
- db.errorLog.find({level:'error'}).sort({created:-1}).limit(10).pretty()
26
- "
27
- ```
28
-
29
- ## Tips
30
- - `source: 'server'` = server-side error, `source: 'browser'` = client-side
31
- - Check `stack` field for full stack trace
32
- - `userId: null` means unauthenticated user
33
-
34
- # Notes
35
- <!-- Claude: append observations here -->
@@ -1,23 +0,0 @@
1
- # Checking Feedback Logs
2
-
3
- ## Query all feedback
4
- ```bash
5
- mongosh "$MONGODB_URI" --eval "
6
- db.feedbackLog.find().sort({created:-1}).limit(20).pretty()
7
- "
8
- ```
9
-
10
- ## Group by type
11
- ```bash
12
- mongosh "$MONGODB_URI" --eval "
13
- db.feedbackLog.aggregate([
14
- { \$group: { _id: '\$type', count: { \$sum: 1 }, messages: { \$push: '\$message' } } }
15
- ]).pretty()
16
- "
17
- ```
18
-
19
- ## Types: `bug`, `design`, `feature`
20
- ## Screenshots available at `screenshotUrl` field
21
-
22
- # Notes
23
- <!-- Claude: append observations here -->
@@ -1,22 +0,0 @@
1
- # Checking Performance Logs
2
-
3
- ## Slowest operations
4
- ```bash
5
- mongosh "$MONGODB_URI" --eval "
6
- db.perfLog.aggregate([
7
- { \$group: { _id: '\$operation', avgMs: { \$avg: '\$durationMs' }, maxMs: { \$max: '\$durationMs' }, count: { \$sum: 1 } } },
8
- { \$sort: { avgMs: -1 } },
9
- { \$limit: 10 }
10
- ]).pretty()
11
- "
12
- ```
13
-
14
- ## Recent slow calls (>500ms)
15
- ```bash
16
- mongosh "$MONGODB_URI" --eval "
17
- db.perfLog.find({durationMs:{\$gt:500}}).sort({created:-1}).limit(20).pretty()
18
- "
19
- ```
20
-
21
- # Notes
22
- <!-- Claude: append observations here -->
@@ -1,34 +0,0 @@
1
- # Creating Test Users
2
-
3
- ## Register a test user
4
- ```bash
5
- curl -X POST http://localhost:3000/auth/register \
6
- -H "Content-Type: application/json" \
7
- -d '{"email":"test1@test.com","password":"testpass123"}'
8
- # Returns: {"token":"..."}
9
- ```
10
-
11
- ## Save token for use in tests
12
- ```bash
13
- TOKEN=$(curl -s -X POST http://localhost:3000/auth/login \
14
- -H "Content-Type: application/json" \
15
- -d '{"email":"test1@test.com","password":"testpass123"}' | jq -r .token)
16
- echo $TOKEN
17
- ```
18
-
19
- ## Seed multiple users
20
- ```bash
21
- for i in 1 2 3 4 5; do
22
- curl -s -X POST http://localhost:3000/auth/register \
23
- -H "Content-Type: application/json" \
24
- -d "{\"email\":\"bot${i}@test.com\",\"password\":\"botpass123\"}"
25
- done
26
- ```
27
-
28
- ## Delete all test users
29
- ```bash
30
- mongosh "$MONGODB_URI" --eval "db.user.deleteMany({email:/test\.com$/})"
31
- ```
32
-
33
- # Notes
34
- <!-- Claude: append observations here — record which test users exist -->
@@ -1,37 +0,0 @@
1
- # Extending the API
2
-
3
- Use this skill when adding new server functions or requests.
4
-
5
- ## Steps
6
-
7
- 1. **Add to `shared/api.ts`**
8
- ```ts
9
- export const functions = defineFunctions({
10
- // existing...
11
- myNewFunction: {} as FunctionDef<{ param: string }, { result: string }>,
12
- })
13
- ```
14
-
15
- 2. **Register the handler in `server/index.ts`**
16
- ```ts
17
- app.registerFunction('myNewFunction', async (ctx, input) => {
18
- // input: { param: string } — fully typed
19
- // ctx.userId, ctx.db, ctx.storage, ctx.textGen, ctx.imageGen, ctx.log
20
- return { result: `Hello ${input.param}` }
21
- })
22
- ```
23
-
24
- 3. **Call from client**
25
- ```ts
26
- const { result } = await socket.call('myNewFunction', { param: 'world' })
27
- ```
28
-
29
- 4. **For read-only operations**, use `defineRequests` + `app.registerRequest` + `socket.request()` instead.
30
-
31
- ## Rules
32
- - Functions = mutating (writes, AI calls, side effects) → `call()`
33
- - Requests = read-only → `request()`
34
- - Both always have `ctx.userId: string`
35
-
36
- # Notes
37
- <!-- Claude: append observations here -->
@@ -1,5 +0,0 @@
1
- Run type and lint checks, fix all issues.
2
-
3
- Run: `yarn build`
4
-
5
- This runs: tsc + vite build. Fix all TypeScript errors and lint warnings reported. Run again to confirm all issues are resolved.
@@ -1,9 +0,0 @@
1
- Fetch production errors and fix them.
2
-
3
- Run: `yarn error:server`
4
-
5
- This fetches recent errors from the production MongoDB errorLog collection. For each error:
6
- 1. Find the relevant source file
7
- 2. Understand the root cause
8
- 3. Fix the code
9
- 4. Run `yarn build` to verify the fix compiles
@@ -1,8 +0,0 @@
1
- Fetch user feedback and fix reported issues.
2
-
3
- Run: `yarn feedback`
4
-
5
- This fetches recent user feedback from the production MongoDB feedbackLog collection. For each piece of feedback:
6
- 1. Understand the user's issue or request
7
- 2. Implement the fix or feature
8
- 3. Run `yarn build` to verify it compiles
@@ -1,8 +0,0 @@
1
- Fetch performance issues and optimize slow paths.
2
-
3
- Run: `yarn perf:server`
4
-
5
- This fetches recent performance log entries from production. For each slow path:
6
- 1. Find the source of the slowdown
7
- 2. Optimize the code (avoid blocking operations, add caching, etc.)
8
- 3. Run `yarn build` to verify
@@ -1,38 +0,0 @@
1
- # Uploading and Using Files
2
-
3
- ## Client → upload to temp
4
- ```ts
5
- // In a React component
6
- const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
7
- const file = e.target.files?.[0]
8
- if (!file) return
9
- const tempUrl = await socket.uploadFile(file, `uploads/${file.name}`)
10
- // tempUrl is now usable — pass to a server function to promote
11
- }
12
- ```
13
-
14
- ## Server → promote temp to public
15
- ```ts
16
- app.registerFunction('saveUpload', async (ctx, input: { tempKey: string; destKey: string }) => {
17
- const publicUrl = await ctx.storage.moveToPublic(input.tempKey, input.destKey)
18
- return { publicUrl }
19
- })
20
- ```
21
-
22
- ## Server → direct write to public (server-generated content only)
23
- ```ts
24
- const url = await ctx.storage.put('public', 'assets/logo.png', buffer, 'image/png')
25
- ```
26
-
27
- ## Get a public URL without uploading
28
- ```ts
29
- const url = ctx.storage.url('public', 'assets/logo.png')
30
- ```
31
-
32
- ## Rules
33
- - Only server can write to `public` bucket
34
- - Client uploads always go to `temp`
35
- - `temp` files expire — promote to `public` if keeping long-term
36
-
37
- # Notes
38
- <!-- Claude: append observations here -->
@@ -1,49 +0,0 @@
1
- # Using Text and Image Generation
2
-
3
- ## Text generation (llama4)
4
- ```ts
5
- app.registerFunction('generateText', async (ctx, input: { prompt: string }) => {
6
- const text = await ctx.textGen.generate([
7
- { role: 'user', content: input.prompt }
8
- ])
9
- return { text }
10
- })
11
- ```
12
-
13
- ## With system prompt
14
- ```ts
15
- const text = await ctx.textGen.generate(messages, {
16
- systemPrompt: 'You are a helpful assistant.',
17
- maxTokens: 512,
18
- temperature: 0.7,
19
- })
20
- ```
21
-
22
- ## Image generation (flux schnell) — returns temp URL
23
- ```ts
24
- app.registerFunction('generateImage', async (ctx, input: { prompt: string }) => {
25
- const tempUrl = await ctx.imageGen.generate(input.prompt)
26
- // Promote to public if keeping permanently:
27
- // const publicUrl = await ctx.storage.moveToPublic('imagegen/xxx.png', 'assets/xxx.png')
28
- return { url: tempUrl }
29
- })
30
- ```
31
-
32
- ## Spend limits
33
- - Budget set via `AI_MAX_SPEND_PER_HOUR` env var (default $1.00/hr)
34
- - Calls over budget are queued (max 5 min wait) then execute when window clears
35
- - If queue is full (100 calls), throws `SpendLimitError` — show user a friendly message
36
-
37
- ## Check current spend
38
- ```bash
39
- mongosh "$MONGODB_URI" --eval "
40
- const since = new Date(Date.now() - 3600000);
41
- db.aiSpend.aggregate([
42
- { \$match: { created: { \$gte: since } } },
43
- { \$group: { _id: '\$model', total: { \$sum: '\$costUsd' } } }
44
- ]).pretty()
45
- "
46
- ```
47
-
48
- # Notes
49
- <!-- Claude: append observations here -->
@@ -1,49 +0,0 @@
1
- # ── Core ────────────────────────────────────────────────────────────────────
2
- JWT_SECRET=CHANGE_ME_RUN_web_init_to_generate
3
- APP_DOMAIN=
4
- NODE_ENV=development
5
- PORT=3000
6
-
7
- # ── MongoDB ──────────────────────────────────────────────────────────────────
8
- # Leave blank to use local docker MongoDB
9
- # Get a free cluster at https://mongodb.com/atlas
10
- MONGODB_URI=
11
-
12
- # ── File Storage ─────────────────────────────────────────────────────────────
13
- # Leave blank to use local docker MinIO
14
- STORAGE_ACCOUNT_ID=
15
- STORAGE_ACCESS_KEY_ID=
16
- STORAGE_SECRET_ACCESS_KEY=
17
- STORAGE_PUBLIC_BUCKET=
18
- STORAGE_TEMP_BUCKET=
19
- STORAGE_PUBLIC_URL=
20
- STORAGE_TEMP_URL=
21
-
22
- # ── NATS ─────────────────────────────────────────────────────────────────────
23
- # Leave blank to use local docker NATS
24
- NATS_URL=
25
- NATS_CREDS=
26
-
27
- # ── Redis ────────────────────────────────────────────────────────────────────
28
- # Leave blank to use local docker Redis
29
- REDIS_URL=
30
-
31
- # ── AI Generation ────────────────────────────────────────────────────────────
32
- TOGETHER_API_KEY=
33
- AI_MAX_SPEND_PER_HOUR=1.00
34
-
35
- # ── Push Notifications ───────────────────────────────────────────────────────
36
- FIREBASE_PROJECT_ID=
37
- FIREBASE_PRIVATE_KEY=
38
- FIREBASE_CLIENT_EMAIL=
39
-
40
- # ── Email ────────────────────────────────────────────────────────────────────
41
- MAILGUN_API_KEY=
42
- MAILGUN_DOMAIN=
43
- MAILGUN_FROM=
44
-
45
- # ── Deploy:multi only ────────────────────────────────────────────────────────
46
- IS_CLOCK_SERVER=
47
-
48
- # ── Maintain Bot ──────────────────────────────────────────────────────────────
49
- MAINTAIN_BOT_USER_ID= # userId of maintain bot account (from web auth:create-account)