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 +5 -5
- package/.claude/settings.local.json +0 -18
- package/.env.example +0 -9
- package/.yarn-metadata.json +0 -113
- package/templates/.claude/skills/assets.md +0 -39
- package/templates/.claude/skills/check-errors.md +0 -35
- package/templates/.claude/skills/check-feedback.md +0 -23
- package/templates/.claude/skills/check-perf.md +0 -22
- package/templates/.claude/skills/create-test-users.md +0 -34
- package/templates/.claude/skills/extend-api.md +0 -37
- package/templates/.claude/skills/fix-code.md +0 -5
- package/templates/.claude/skills/fix-errors.md +0 -9
- package/templates/.claude/skills/fix-feedback.md +0 -8
- package/templates/.claude/skills/fix-perf.md +0 -8
- package/templates/.claude/skills/uploads.md +0 -38
- package/templates/.claude/skills/use-ai.md +0 -49
- package/templates/.env.example +0 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ugly-app",
|
|
3
|
-
"version": "0.1.
|
|
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": "
|
|
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": "^
|
|
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
package/.yarn-metadata.json
DELETED
|
@@ -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,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 -->
|
package/templates/.env.example
DELETED
|
@@ -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)
|