summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-07-16 10:07:06 +0700
committerpolwex <polwex@sortug.com>2025-07-16 10:07:06 +0700
commitff3078e93411c3467d797258744a7f17a7dbdf0a (patch)
treeb13df65dbae32a3298afc0ada39b73a7d8aa469d
parentb26f4a03e15feddeb4bee8c06cd5078c1a54e5c5 (diff)
m
-rw-r--r--CLAUDE.md66
-rw-r--r--README.md8
-rw-r--r--app/.gitignore7
-rw-r--r--app/bun.lock588
-rw-r--r--app/package.json28
-rw-r--r--app/postcss.config.js5
-rw-r--r--app/public/images/favicon.pngbin0 -> 5713 bytes
-rw-r--r--app/public/robots.txt2
-rw-r--r--app/src/components/bookmark-fetcher.tsx143
-rw-r--r--app/src/components/bookmark-list.tsx109
-rw-r--r--app/src/components/cat/Entry.tsx214
-rw-r--r--app/src/components/cat/Form.tsx5
-rw-r--r--app/src/components/counter.tsx21
-rw-r--r--app/src/components/footer.tsx18
-rw-r--r--app/src/components/header.tsx9
-rw-r--r--app/src/lib/bookmark-models.ts46
-rw-r--r--app/src/lib/bookmark-storage.ts62
-rw-r--r--app/src/lib/categorization.ts102
-rw-r--r--app/src/lib/llm-prompts.ts50
-rw-r--r--app/src/lib/llm-service.ts205
-rw-r--r--app/src/lib/testData.json1
-rw-r--r--app/src/lib/testData.ts50
-rw-r--r--app/src/lib/twitter-api.ts308
-rw-r--r--app/src/pages.gen.ts27
-rw-r--r--app/src/pages/_layout.tsx39
-rw-r--r--app/src/pages/about.tsx35
-rw-r--r--app/src/pages/categorize.tsx171
-rw-r--r--app/src/pages/index.tsx72
-rw-r--r--app/src/styles.css3
-rw-r--r--app/testData.json1
-rw-r--r--app/tsconfig.json17
-rw-r--r--devenv.nix24
32 files changed, 2428 insertions, 8 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..c08d8c8
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,66 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+SORMARK is a bookmark management application that uses LLMs to analyze and categorize bookmarks from Twitter (X), browser bookmarks, and other sources. The LLM processes each bookmark to categorize it, read linked content, and decide actions like storing in an Obsidian-like vault or creating calendar events.
+
+## Tech Stack
+- **Frontend**: Waku (React framework with server components)
+- **Runtime**: Bun
+- **Styling**: Tailwind CSS v4
+- **Language**: TypeScript with strict configuration
+- **Development**: Devenv for environment management
+
+## Development Commands
+
+### Getting Started
+```bash
+# Enter development environment
+direnv allow # or devenv shell
+
+# Install dependencies
+bun install
+```
+
+### Running the Application
+```bash
+# Development server
+bun dev
+
+# Build for production
+bun build
+
+# Start production server
+bun start
+```
+
+### Environment Setup
+The project uses devenv for environment management. Key environment variables:
+- `ANTHROPIC_BASE_URL`: Set to "https://api.moonshot.ai/anthropic"
+- `ANTHROPIC_AUTH_TOKEN`: API key for LLM integration (already configured)
+- `TWITTER_COKI`: Cookie used on the x.com frontend.
+
+## Project Structure
+```
+app/
+├── src/
+│ ├── pages/ # Waku pages (file-based routing)
+│ │ ├── _layout.tsx # Root layout component
+│ │ ├── index.tsx # Home page
+│ │ └── about.tsx # About page
+│ ├── components/ # React components
+│ │ ├── counter.tsx # Client-side counter example
+│ │ ├── header.tsx # Site header
+│ │ └── footer.tsx # Site footer
+│ └── styles.css # Global styles with Tailwind
+├── public/ # Static assets
+└── package.json # Dependencies and scripts
+```
+
+## Architecture Notes
+- Uses Waku framework with React Server Components
+- File-based routing in `src/pages/` directory
+- Static rendering configured for all pages
+- Client components use `'use client'` directive
+- Tailwind CSS v4 configured with PostCSS
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fb8cb0d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+
+# SORMARK
+
+This is an app to handle bookmarks with the help of an LLM. The app will ingest Twitter (well now X) bookmarks, browser bookmarks and later on other saved stuff. It will go through them one by one, and the LLM will analyze the bookmarks, read any existing links, categorize them and decide what to do with them: either store them in an Obsidian like vault, add a calendar event, or some other.
+
+## Getting started
+
+Just enter the devenv shell. This is a typescript app using Bun. Fullstack webapp using Waku with server react components.
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..ad58343
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+dist
+.env*
+*.tsbuildinfo
+.cache
+.DS_Store
+*.pem
diff --git a/app/bun.lock b/app/bun.lock
new file mode 100644
index 0000000..c3f3fba
--- /dev/null
+++ b/app/bun.lock
@@ -0,0 +1,588 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "app",
+ "dependencies": {
+ "@google/genai": "^1.9.0",
+ "openai": "^5.9.0",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "react-server-dom-webpack": "19.1.0",
+ "waku": "0.23.3",
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "4.1.10",
+ "@types/bun": "latest",
+ "@types/react": "19.1.8",
+ "@types/react-dom": "19.1.6",
+ "postcss": "8.5.6",
+ "tailwindcss": "4.1.10",
+ "typescript": "5.8.3",
+ },
+ },
+ },
+ "packages": {
+ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
+
+ "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
+
+ "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="],
+
+ "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
+
+ "@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
+
+ "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="],
+
+ "@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="],
+
+ "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
+
+ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
+
+ "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
+
+ "@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="],
+
+ "@babel/types": ["@babel/types@7.28.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.6", "", { "os": "aix", "cpu": "ppc64" }, "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.25.6", "", { "os": "android", "cpu": "arm" }, "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.6", "", { "os": "android", "cpu": "arm64" }, "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.25.6", "", { "os": "android", "cpu": "x64" }, "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.6", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.6", "", { "os": "linux", "cpu": "arm" }, "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.6", "", { "os": "linux", "cpu": "ia32" }, "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.6", "", { "os": "linux", "cpu": "x64" }, "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.6", "", { "os": "none", "cpu": "arm64" }, "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.6", "", { "os": "none", "cpu": "x64" }, "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.6", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.6", "", { "os": "openbsd", "cpu": "x64" }, "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.6", "", { "os": "none", "cpu": "arm64" }, "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.6", "", { "os": "sunos", "cpu": "x64" }, "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA=="],
+
+ "@google/genai": ["@google/genai@1.9.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.11.0" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-w9P93OXKPMs9H1mfAx9+p3zJqQGrWBGdvK/SVc7cLZEXNHr/3+vW2eif7ZShA6wU24rNLn9z9MK2vQFUvNRI2Q=="],
+
+ "@hono/node-server": ["@hono/node-server@1.14.4", "", { "peerDependencies": { "hono": "^4" } }, "sha512-DnxpshhYewr2q9ZN8ez/M5mmc3sucr8CT1sIgIy1bkeUXut9XWDkqHoFHRhWIQgkYnKpVRxunyhK7WzpJeJ6qQ=="],
+
+ "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/source-map": ["@jridgewell/source-map@0.3.10", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.19", "", {}, "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.0", "", { "os": "android", "cpu": "arm" }, "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.0", "", { "os": "android", "cpu": "arm64" }, "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.0", "", { "os": "linux", "cpu": "arm" }, "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q=="],
+
+ "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.0", "", { "os": "linux", "cpu": "none" }, "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg=="],
+
+ "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.0", "", { "os": "linux", "cpu": "none" }, "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.0", "", { "os": "linux", "cpu": "none" }, "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ=="],
+
+ "@swc/core": ["@swc/core@1.12.6", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.12.6", "@swc/core-darwin-x64": "1.12.6", "@swc/core-linux-arm-gnueabihf": "1.12.6", "@swc/core-linux-arm64-gnu": "1.12.6", "@swc/core-linux-arm64-musl": "1.12.6", "@swc/core-linux-x64-gnu": "1.12.6", "@swc/core-linux-x64-musl": "1.12.6", "@swc/core-win32-arm64-msvc": "1.12.6", "@swc/core-win32-ia32-msvc": "1.12.6", "@swc/core-win32-x64-msvc": "1.12.6" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-TEpta6Gi02X1b2yDIzBOIr7dFprvq9jD8RbEVI2OcMrwklbCUx0Dz9TrAnklSOwRvYvH5JjCx8ht9E94oWiG7A=="],
+
+ "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.12.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yLiw+XzG+MilfFh0ON7qt67bfIr7UxB9JprhYReVOmLTBDmDVQSC3T4/vIuc+GwlX08ydnHy0ud4lIjTNW4uWg=="],
+
+ "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.12.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-qwg8ux5x5Gd1LmSUtL4s9mbyfzAjr5M6OtjO281dKHwc/GYiSc4j1urb2jNSo9FcMkfT78oAOW2L6HQiWv+j1A=="],
+
+ "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.12.6", "", { "os": "linux", "cpu": "arm" }, "sha512-pnkqH59JXBZu+MedaykMAC2or7tlUKeya7GKjzub+hkwxBP0ywWoFd+QYEdzp7QSziOt1VIHc4Wb9iZ2EfnzmA=="],
+
+ "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.12.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-h8+Ltx0NSEzIFHetkOYoQ+UQ59unYLuJ4wF6kCpxzS4HskRLjcngr1HgN0F/PRpptnrmJUPVQmfms/vjN8ndAQ=="],
+
+ "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.12.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-GZu3MnB/5qtBxKEH46hgVDaplEe4mp3ZmQ1O2UpFCv/u/Ji3Gar5w5g2wHCZoT5AOouAhP1bh7IAEqjG/fbVfg=="],
+
+ "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.12.6", "", { "os": "linux", "cpu": "x64" }, "sha512-WwJLQFzMW9ufVjM6k3le4HUgBFNunyt2oghjcgn2YjnKj0Ka2LrrBHCxfS7lgFSCQh/shib2wIlKXUnlTEWQJw=="],
+
+ "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.12.6", "", { "os": "linux", "cpu": "x64" }, "sha512-rVGPNpI/sm8VVAhnB09Z/23OJP3ymouv6F4z4aYDbq/2JIwxqgpnl8gtMYP+Jw3XqabaFNjQmPiL15TvKCQaxQ=="],
+
+ "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.12.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-EKDJ1+8vaIlJGMl2yvd2HklV4GNbpKKwNQcUQid6j91tFYz4/aByw+9vh/sDVG7ZNqdmdywSnLRo317UTt0zFg=="],
+
+ "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.12.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-jnULikZkR2fpZgFUQs7NsNIztavM1JdX+8Y+8FsfChBvMvziKxXtvUPGjeVJ8nzU1wgMnaeilJX9vrwuDGkA0Q=="],
+
+ "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.12.6", "", { "os": "win32", "cpu": "x64" }, "sha512-jL2Dcdcc/QZiQnwByP1uIE4k/mTlapzUng7owtLD2tSBBi1d+jPIdXIefdv+nccYJKRA+lKG3rRB6Tk9GrC7Kg=="],
+
+ "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
+
+ "@swc/types": ["@swc/types@0.1.23", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw=="],
+
+ "@tailwindcss/node": ["@tailwindcss/node@4.1.10", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.10" } }, "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.10", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.10", "@tailwindcss/oxide-darwin-arm64": "4.1.10", "@tailwindcss/oxide-darwin-x64": "4.1.10", "@tailwindcss/oxide-freebsd-x64": "4.1.10", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.10", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.10", "@tailwindcss/oxide-linux-arm64-musl": "4.1.10", "@tailwindcss/oxide-linux-x64-gnu": "4.1.10", "@tailwindcss/oxide-linux-x64-musl": "4.1.10", "@tailwindcss/oxide-wasm32-wasi": "4.1.10", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.10", "@tailwindcss/oxide-win32-x64-msvc": "4.1.10" } }, "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.10", "", { "os": "android", "cpu": "arm64" }, "sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.10", "", { "os": "linux", "cpu": "arm" }, "sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.10", "", { "os": "linux", "cpu": "x64" }, "sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.10", "", { "os": "linux", "cpu": "x64" }, "sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.10", "", { "os": "win32", "cpu": "x64" }, "sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA=="],
+
+ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.10", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.10", "@tailwindcss/oxide": "4.1.10", "postcss": "^8.4.41", "tailwindcss": "4.1.10" } }, "sha512-B+7r7ABZbkXJwpvt2VMnS6ujcDoR2OOcFaqrLIo1xbcdxje4Vf+VgJdBzNNbrAjBj/rLZ66/tlQ1knIGNLKOBQ=="],
+
+ "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
+
+ "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
+
+ "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
+
+ "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
+
+ "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
+
+ "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
+
+ "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+ "@types/node": ["@types/node@24.0.14", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="],
+
+ "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
+
+ "@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
+
+ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.6.0", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.19", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ=="],
+
+ "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
+
+ "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
+
+ "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="],
+
+ "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="],
+
+ "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="],
+
+ "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="],
+
+ "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="],
+
+ "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="],
+
+ "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="],
+
+ "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="],
+
+ "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="],
+
+ "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="],
+
+ "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="],
+
+ "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="],
+
+ "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
+
+ "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
+
+ "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
+
+ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+
+ "acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="],
+
+ "acorn-loose": ["acorn-loose@8.5.2", "", { "dependencies": { "acorn": "^8.15.0" } }, "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A=="],
+
+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
+
+ "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
+
+ "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
+
+ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
+
+ "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
+
+ "browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
+
+ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
+
+ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
+
+ "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="],
+
+ "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
+
+ "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
+
+ "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
+ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
+
+ "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
+
+ "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
+
+ "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
+
+ "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
+
+ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
+
+ "dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="],
+
+ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.183", "", {}, "sha512-vCrDBYjQCAEefWGjlK3EpoSKfKbT10pR4XXPdn65q7snuNOZnthoVpBfZPykmDapOKfoD+MMIPG8ZjKyyc9oHA=="],
+
+ "enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
+
+ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
+ "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
+
+ "esbuild": ["esbuild@0.25.6", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.6", "@esbuild/android-arm": "0.25.6", "@esbuild/android-arm64": "0.25.6", "@esbuild/android-x64": "0.25.6", "@esbuild/darwin-arm64": "0.25.6", "@esbuild/darwin-x64": "0.25.6", "@esbuild/freebsd-arm64": "0.25.6", "@esbuild/freebsd-x64": "0.25.6", "@esbuild/linux-arm": "0.25.6", "@esbuild/linux-arm64": "0.25.6", "@esbuild/linux-ia32": "0.25.6", "@esbuild/linux-loong64": "0.25.6", "@esbuild/linux-mips64el": "0.25.6", "@esbuild/linux-ppc64": "0.25.6", "@esbuild/linux-riscv64": "0.25.6", "@esbuild/linux-s390x": "0.25.6", "@esbuild/linux-x64": "0.25.6", "@esbuild/netbsd-arm64": "0.25.6", "@esbuild/netbsd-x64": "0.25.6", "@esbuild/openbsd-arm64": "0.25.6", "@esbuild/openbsd-x64": "0.25.6", "@esbuild/openharmony-arm64": "0.25.6", "@esbuild/sunos-x64": "0.25.6", "@esbuild/win32-arm64": "0.25.6", "@esbuild/win32-ia32": "0.25.6", "@esbuild/win32-x64": "0.25.6" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+ "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
+
+ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
+
+ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
+
+ "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
+
+ "gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+ "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
+
+ "google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
+
+ "google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="],
+
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+ "hono": ["hono@4.8.3", "", {}, "sha512-jYZ6ZtfWjzBdh8H/0CIFfCBHaFL75k+KMzaM177hrWWm2TWL39YMYaJgB74uK/niRc866NMlH9B8uCvIo284WQ=="],
+
+ "html-dom-parser": ["html-dom-parser@5.1.1", "", { "dependencies": { "domhandler": "5.0.3", "htmlparser2": "10.0.0" } }, "sha512-+o4Y4Z0CLuyemeccvGN4bAO20aauB2N9tFEAep5x4OW34kV4PTarBHm6RL02afYt2BMKcr0D2Agep8S3nJPIBg=="],
+
+ "html-react-parser": ["html-react-parser@5.2.5", "", { "dependencies": { "domhandler": "5.0.3", "html-dom-parser": "5.1.1", "react-property": "2.0.2", "style-to-js": "1.1.16" }, "peerDependencies": { "@types/react": "0.14 || 15 || 16 || 17 || 18 || 19", "react": "0.14 || 15 || 16 || 17 || 18 || 19" }, "optionalPeers": ["@types/react"] }, "sha512-bRPdv8KTqG9CEQPMNGksDqmbiRfVQeOidry8pVetdh/1jQ1Edx4KX5m0lWvDD89Pt4CqTYjK1BLz6NoNVxN/Uw=="],
+
+ "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
+
+ "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
+
+ "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
+
+ "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
+
+ "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
+
+ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
+
+ "jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
+
+ "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
+
+ "loader-runner": ["loader-runner@4.3.0", "", {}, "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg=="],
+
+ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
+
+ "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
+
+ "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
+ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
+
+ "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
+
+ "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
+
+ "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
+
+ "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
+
+ "openai": ["openai@5.9.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-cmLC0pfqLLhBGxE4aZPyRPjydgYCncppV2ClQkKmW79hNjCvmzkfhz8rN5/YVDmjVQlFV+UsF1JIuNjNgeagyQ=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
+
+ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+
+ "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
+
+ "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
+
+ "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
+
+ "react-property": ["react-property@2.0.2", "", {}, "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug=="],
+
+ "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
+
+ "react-server-dom-webpack": ["react-server-dom-webpack@19.1.0", "", { "dependencies": { "acorn-loose": "^8.3.0", "neo-async": "^2.6.1", "webpack-sources": "^3.2.0" }, "peerDependencies": { "react": "^19.1.0", "react-dom": "^19.1.0", "webpack": "^5.59.0" } }, "sha512-GUbawkNSN0oj8GnuNhMzsvyIHpXqqpAmyOY5NRqNNQ/M8wvUUN8YBoGjDUj9lbmBrmAHS65BByp6325CcWA0eg=="],
+
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+ "rollup": ["rollup@4.44.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.0", "@rollup/rollup-android-arm64": "4.44.0", "@rollup/rollup-darwin-arm64": "4.44.0", "@rollup/rollup-darwin-x64": "4.44.0", "@rollup/rollup-freebsd-arm64": "4.44.0", "@rollup/rollup-freebsd-x64": "4.44.0", "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", "@rollup/rollup-linux-arm-musleabihf": "4.44.0", "@rollup/rollup-linux-arm64-gnu": "4.44.0", "@rollup/rollup-linux-arm64-musl": "4.44.0", "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", "@rollup/rollup-linux-riscv64-gnu": "4.44.0", "@rollup/rollup-linux-riscv64-musl": "4.44.0", "@rollup/rollup-linux-s390x-gnu": "4.44.0", "@rollup/rollup-linux-x64-gnu": "4.44.0", "@rollup/rollup-linux-x64-musl": "4.44.0", "@rollup/rollup-win32-arm64-msvc": "4.44.0", "@rollup/rollup-win32-ia32-msvc": "4.44.0", "@rollup/rollup-win32-x64-msvc": "4.44.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA=="],
+
+ "rsc-html-stream": ["rsc-html-stream@0.0.6", "", {}, "sha512-oZUJ5AH0oDo9QywxD9yMY6N5Z3VwX2YfQg0FanNdCmvXmO0itTfv7BMkbMSwxg7JmBjYmefU8DTW0EcLsePPgQ=="],
+
+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
+ "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
+
+ "schema-utils": ["schema-utils@4.3.2", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="],
+
+ "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
+
+ "style-to-js": ["style-to-js@1.1.16", "", { "dependencies": { "style-to-object": "1.0.8" } }, "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw=="],
+
+ "style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
+
+ "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
+
+ "tailwindcss": ["tailwindcss@4.1.10", "", {}, "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA=="],
+
+ "tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
+
+ "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
+
+ "terser": ["terser@5.43.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg=="],
+
+ "terser-webpack-plugin": ["terser-webpack-plugin@5.3.14", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw=="],
+
+ "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
+
+ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
+
+ "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
+
+ "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
+
+ "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
+
+ "vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="],
+
+ "waku": ["waku@0.23.3", "", { "dependencies": { "@hono/node-server": "1.14.4", "@swc/core": "1.12.6", "@vitejs/plugin-react": "4.6.0", "dotenv": "16.5.0", "hono": "4.8.3", "html-react-parser": "5.2.5", "rollup": "4.44.0", "rsc-html-stream": "0.0.6", "vite": "7.0.0" }, "peerDependencies": { "react": "~19.1.0", "react-dom": "~19.1.0", "react-server-dom-webpack": "~19.1.0" }, "bin": { "waku": "cli.js" } }, "sha512-BUTrJ//QHQcm/LkxX4wXQv0pTyvovXwFxsPv0b6/Hcv6iq1RAjEVa4oL+9VaiaKqxp00ZZtUT2cLUgNRSA5vkg=="],
+
+ "watchpack": ["watchpack@2.4.4", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA=="],
+
+ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
+
+ "webpack": ["webpack@5.100.1", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.2", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.2", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-YJB/ESPUe2Locd0NKXmw72Dx8fZQk1gTzI6rc9TAT4+Sypbnhl8jd8RywB1bDsDF9Dy1RUR7gn3q/ZJTd0OZZg=="],
+
+ "webpack-sources": ["webpack-sources@3.3.3", "", {}, "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg=="],
+
+ "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
+
+ "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
+
+ "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.4", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
+ "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
+ }
+}
diff --git a/app/package.json b/app/package.json
new file mode 100644
index 0000000..1583658
--- /dev/null
+++ b/app/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "app",
+ "version": "0.0.0",
+ "type": "module",
+ "private": true,
+ "scripts": {
+ "dev": "bunx --bun waku dev --port 3030",
+ "build": "bunx --bun waku build",
+ "start": "bunx --bun waku start"
+ },
+ "dependencies": {
+ "@google/genai": "^1.9.0",
+ "openai": "^5.9.0",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "react-server-dom-webpack": "19.1.0",
+ "waku": "0.23.3"
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ "@tailwindcss/postcss": "4.1.10",
+ "@types/react": "19.1.8",
+ "@types/react-dom": "19.1.6",
+ "postcss": "8.5.6",
+ "tailwindcss": "4.1.10",
+ "typescript": "5.8.3"
+ }
+}
diff --git a/app/postcss.config.js b/app/postcss.config.js
new file mode 100644
index 0000000..a34a3d5
--- /dev/null
+++ b/app/postcss.config.js
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+};
diff --git a/app/public/images/favicon.png b/app/public/images/favicon.png
new file mode 100644
index 0000000..cd90d79
--- /dev/null
+++ b/app/public/images/favicon.png
Binary files differ
diff --git a/app/public/robots.txt b/app/public/robots.txt
new file mode 100644
index 0000000..b4d27bb
--- /dev/null
+++ b/app/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /RSC/ \ No newline at end of file
diff --git a/app/src/components/bookmark-fetcher.tsx b/app/src/components/bookmark-fetcher.tsx
new file mode 100644
index 0000000..6522310
--- /dev/null
+++ b/app/src/components/bookmark-fetcher.tsx
@@ -0,0 +1,143 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { TwitterBookmark } from '../lib/bookmark-models';
+import { BookmarkStorageService } from '../lib/bookmark-storage';
+import { BookmarkList } from './bookmark-list';
+
+export function BookmarkFetcher() {
+ const [bookmarks, setBookmarks] = useState<TwitterBookmark[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+ const [username, setUsername] = useState('');
+ const [authToken, setAuthToken] = useState('');
+
+ // Load existing bookmarks on mount
+ useEffect(() => {
+ const existingBookmarks = BookmarkStorageService.getBookmarks();
+ setBookmarks(existingBookmarks);
+ }, []);
+
+ const handleFetchBookmarks = async () => {
+ if (!username || !authToken) {
+ setError('Please enter your Twitter username and auth token');
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ // Simple fetch to our API endpoint
+ const response = await fetch('/api/sync-bookmarks', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ username, authToken }),
+ });
+
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.error || 'Failed to fetch bookmarks');
+ }
+
+ // Load the newly saved bookmarks
+ const updatedBookmarks = BookmarkStorageService.getBookmarks();
+ setBookmarks(updatedBookmarks);
+
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to fetch bookmarks');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleClearBookmarks = () => {
+ BookmarkStorageService.clearAll();
+ setBookmarks([]);
+ };
+
+ return (
+ <div className="space-y-6">
+ {/* Input fields */}
+ <div className="bg-white border border-gray-200 rounded-lg p-6">
+ <h2 className="text-xl font-semibold mb-4">Twitter Credentials</h2>
+
+ <div className="space-y-4">
+ <div>
+ <label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
+ Twitter Username
+ </label>
+ <input
+ id="username"
+ type="text"
+ value={username}
+ onChange={(e) => setUsername(e.target.value)}
+ placeholder="your_twitter_username"
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+ </div>
+
+ <div>
+ <label htmlFor="authToken" className="block text-sm font-medium text-gray-700 mb-1">
+ Auth Token
+ </label>
+ <input
+ id="authToken"
+ type="password"
+ value={authToken}
+ onChange={(e) => setAuthToken(e.target.value)}
+ placeholder="your_auth_token"
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* Action buttons */}
+ <div className="flex gap-4">
+ <button
+ onClick={handleFetchBookmarks}
+ disabled={loading}
+ className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-blue-300 disabled:cursor-not-allowed font-medium"
+ >
+ {loading ? (
+ <>
+ <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+ </svg>
+ Fetching...
+ </>
+ ) : (
+ 'Fetch Twitter Bookmarks'
+ )}
+ </button>
+
+ {bookmarks.length > 0 && (
+ <button
+ onClick={handleClearBookmarks}
+ className="px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 font-medium"
+ >
+ Clear All
+ </button>
+ )}
+ </div>
+
+ {/* Error message */}
+ {error && (
+ <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
+ {error}
+ </div>
+ )}
+
+ {/* Bookmark list */}
+ <div className="bg-white border border-gray-200 rounded-lg p-6">
+ <h2 className="text-xl font-semibold mb-4">Your Bookmarks</h2>
+ <BookmarkList bookmarks={bookmarks} />
+ </div>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/app/src/components/bookmark-list.tsx b/app/src/components/bookmark-list.tsx
new file mode 100644
index 0000000..6138e31
--- /dev/null
+++ b/app/src/components/bookmark-list.tsx
@@ -0,0 +1,109 @@
+"use client";
+
+import { useState } from "react";
+
+import { BookmarkStorageService } from "../lib/bookmark-storage";
+import { TwitterBookmark } from "../lib/twitter-api";
+
+interface BookmarkListProps {
+ bookmarks: TwitterBookmark[];
+}
+
+function formatDate(dateString: string): string {
+ const date = new Date(dateString);
+ return date.toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
+function truncateText(text: string, maxLength: number = 200): string {
+ if (text.length <= maxLength) return text;
+ return text.substring(0, maxLength) + "...";
+}
+
+export function BookmarkList({ bookmarks }: BookmarkListProps) {
+ if (bookmarks.length === 0) {
+ return (
+ <div className="text-center py-8 text-gray-500">
+ No bookmarks found. Click "Fetch Twitter Bookmarks" to load your
+ bookmarks.
+ </div>
+ );
+ }
+
+ return (
+ <div className="space-y-4">
+ <div className="text-sm text-gray-600 mb-4">
+ Found {bookmarks.length} bookmark{bookmarks.length !== 1 ? "s" : ""}
+ </div>
+
+ {bookmarks.map((bookmark) => (
+ <BookmarkEntry bookmark={bookmark} key={bookmark.id} />
+ ))}
+ </div>
+ );
+}
+
+function BookmarkEntry({ bookmark }: { bookmark: TwitterBookmark }) {
+ console.log({ bookmark });
+ return (
+ <div className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow">
+ <div className="flex items-start justify-between mb-2">
+ <div className="flex-1">
+ <div className="flex items-center gap-2 mb-1">
+ <span className="font-semibold text-gray-900">
+ @{bookmark.author.username}
+ </span>
+ <span className="text-sm text-gray-500">•</span>
+ <span className="text-sm text-gray-500">
+ {formatDate(bookmark.createdAt)}
+ </span>
+ </div>
+ <div className="text-sm text-gray-700 mb-2">
+ {truncateText(bookmark.text)}
+ </div>
+ <div className="flex flex-wrap">
+ {bookmark.media.pics.map((p) => (
+ <img width="400" src={p} />
+ ))}
+ </div>
+ {bookmark.media.video.url && (
+ <video src={bookmark.media.video.url} controls />
+ )}
+ {bookmark.urls.length > 0 && (
+ <div className="space-y-1">
+ {bookmark.urls.map((url, index) => (
+ <div key={index} className="text-xs">
+ <a
+ href={url.expandedUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="text-blue-600 hover:underline"
+ >
+ {url.displayUrl}
+ </a>
+ </div>
+ ))}
+ </div>
+ )}
+ {bookmark.hashtags.length > 0 && (
+ <div className="flex flex-wrap gap-1 mt-2">
+ {bookmark.hashtags.map((tag, index) => (
+ <span
+ key={index}
+ className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded"
+ >
+ #{tag}
+ </span>
+ ))}
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/app/src/components/cat/Entry.tsx b/app/src/components/cat/Entry.tsx
new file mode 100644
index 0000000..1ded145
--- /dev/null
+++ b/app/src/components/cat/Entry.tsx
@@ -0,0 +1,214 @@
+import {
+ CategorizationResponse,
+ userCategories,
+} from "../../lib/categorization";
+import { TwitterApiService, TwitterBookmark } from "../../lib/twitter-api";
+
+export default function ({
+ bookmark,
+ categorization,
+ currentIndex,
+ totalCount,
+}: {
+ bookmark: TwitterBookmark;
+ categorization: CategorizationResponse;
+ currentIndex: number;
+ totalCount: number;
+}) {
+ async function processEntry() {
+ "use server";
+ const cookie = Bun.env.TWATTER_COKI;
+ const api = new TwitterApiService(cookie!);
+ await api.removeBookmark(bookmark.id);
+ }
+
+ return (
+ <div className="bg-white rounded-lg shadow-lg overflow-hidden">
+ {/* Bookmark Content */}
+ <div className="p-6 border-b">
+ <div className="flex items-start space-x-3 mb-4">
+ <img
+ src={bookmark.author.avatar}
+ alt={bookmark.author.name}
+ className="w-12 h-12 rounded-full"
+ />
+ <div>
+ <h3 className="font-semibold text-gray-900">
+ {bookmark.author.name}
+ </h3>
+ <p className="text-gray-600">@{bookmark.author.username}</p>
+ <p className="text-sm text-gray-500">
+ {new Date(bookmark.createdAt).toLocaleDateString()}
+ </p>
+ </div>
+ </div>
+
+ <div className="prose max-w-none mb-4">
+ <p className="whitespace-pre-wrap">{bookmark.text}</p>
+ </div>
+
+ {bookmark.media.pics.length > 0 && (
+ <div className="mb-4">
+ <h4 className="text-sm font-semibold text-gray-700 mb-2">Images</h4>
+ <div className="grid grid-cols-3 gap-2">
+ {bookmark.media.pics.map((url, index) => (
+ <img
+ key={index}
+ src={url}
+ alt={`Bookmark image ${index + 1}`}
+ className="rounded-lg border"
+ />
+ ))}
+ </div>
+ </div>
+ )}
+
+ <div className="flex flex-wrap gap-2">
+ {bookmark.hashtags.map((tag) => (
+ <span
+ key={tag}
+ className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-sm"
+ >
+ #{tag}
+ </span>
+ ))}
+ </div>
+ </div>
+
+ {/* LLM Analysis */}
+ <div className="p-6 border-b bg-gray-50">
+ <h3 className="text-lg font-semibold mb-3">AI Analysis</h3>
+ <p className="text-sm text-gray-600 mb-3">{categorization.summary}</p>
+
+ <div className="mb-4">
+ <h4 className="text-sm font-semibold mb-2">Key Topics</h4>
+ <div className="flex flex-wrap gap-2">
+ {categorization.keyTopics.map((topic) => (
+ <span
+ key={topic}
+ className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-sm"
+ >
+ {topic}
+ </span>
+ ))}
+ </div>
+ </div>
+
+ <div>
+ <h4 className="text-sm font-semibold mb-2">Suggested Categories</h4>
+ {categorization.suggestedCategories.map((suggestion, index) => (
+ <div key={index} className="mb-2 p-2 bg-white rounded border">
+ <div className="flex items-center justify-between mb-1">
+ <span className="font-medium">
+ {suggestion.categories.join(", ")}
+ </span>
+ <span className="text-sm text-gray-600">
+ {(suggestion.confidence * 100).toFixed(0)}%
+ </span>
+ </div>
+ <p className="text-sm text-gray-600">{suggestion.reasoning}</p>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {/* Category Selection */}
+ <form method="POST" action="/api/save-categorization">
+ <div className="p-6">
+ <h3 className="text-lg font-semibold mb-3">Select Categories</h3>
+
+ <div className="mb-4">
+ <h4 className="text-sm font-semibold mb-2">User Categories</h4>
+ <div className="flex flex-wrap gap-2">
+ {userCategories.map((category) => (
+ <label
+ key={category.name}
+ className="flex items-center cursor-pointer"
+ >
+ <input
+ type="checkbox"
+ name="categories"
+ value={category.name}
+ defaultChecked={categorization.suggestedCategories[0]?.categories.includes(
+ category.name,
+ )}
+ className="mr-2"
+ />
+ <span className="px-3 py-1 bg-gray-200 text-gray-700 rounded-full text-sm">
+ {category.name}
+ </span>
+ </label>
+ ))}
+ </div>
+ </div>
+
+ {categorization.newCategories.length > 0 && (
+ <div className="mb-4">
+ <h4 className="text-sm font-semibold mb-2">
+ New Category Suggestions
+ </h4>
+ <div className="flex flex-wrap gap-2">
+ {categorization.newCategories.map((category) => (
+ <label
+ key={category}
+ className="flex items-center cursor-pointer"
+ >
+ <input
+ type="checkbox"
+ name="categories"
+ value={category}
+ className="mr-2"
+ />
+ <span className="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm">
+ {category}
+ </span>
+ </label>
+ ))}
+ </div>
+ </div>
+ )}
+
+ <div className="mb-4">
+ <label className="block text-sm font-semibold mb-2">
+ Custom Categories
+ </label>
+ <input
+ type="text"
+ name="customCategories"
+ placeholder="Add custom categories, separated by commas"
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+ </div>
+
+ <input type="hidden" name="bookmarkId" value={bookmark.id} />
+ <input type="hidden" name="nextIndex" value={currentIndex + 1} />
+
+ <div className="flex space-x-4">
+ {currentIndex > 0 && (
+ <a
+ href={`/categorize?idx=${currentIndex - 1}`}
+ className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
+ >
+ Previous
+ </a>
+ )}
+ <button
+ type="submit"
+ className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700"
+ >
+ Save & Next
+ </button>
+ {currentIndex + 1 < totalCount && (
+ <a
+ href={`/categorize?idx=${currentIndex + 1}`}
+ className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
+ >
+ Skip
+ </a>
+ )}
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+}
diff --git a/app/src/components/cat/Form.tsx b/app/src/components/cat/Form.tsx
new file mode 100644
index 0000000..7e58be4
--- /dev/null
+++ b/app/src/components/cat/Form.tsx
@@ -0,0 +1,5 @@
+"use client";
+
+export default function ProcessEntry() {
+ return <form method="POST" action="/api/save-categorization"></form>;
+}
diff --git a/app/src/components/counter.tsx b/app/src/components/counter.tsx
new file mode 100644
index 0000000..0e540b8
--- /dev/null
+++ b/app/src/components/counter.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+import { useState } from 'react';
+
+export const Counter = () => {
+ const [count, setCount] = useState(0);
+
+ const handleIncrement = () => setCount((c) => c + 1);
+
+ return (
+ <section className="border-blue-400 -mx-4 mt-4 rounded-sm border border-dashed p-4">
+ <div>Count: {count}</div>
+ <button
+ onClick={handleIncrement}
+ className="rounded-xs bg-black px-2 py-0.5 text-sm text-white"
+ >
+ Increment
+ </button>
+ </section>
+ );
+};
diff --git a/app/src/components/footer.tsx b/app/src/components/footer.tsx
new file mode 100644
index 0000000..d9d2511
--- /dev/null
+++ b/app/src/components/footer.tsx
@@ -0,0 +1,18 @@
+export const Footer = () => {
+ return (
+ <footer className="p-6 lg:fixed lg:bottom-0 lg:left-0">
+ <div>
+ visit{" "}
+ <a
+ href="https://sortug.com/"
+ target="_blank"
+ rel="noreferrer"
+ className="mt-4 inline-block underline"
+ >
+ sortug.com
+ </a>{" "}
+ to learn more
+ </div>
+ </footer>
+ );
+};
diff --git a/app/src/components/header.tsx b/app/src/components/header.tsx
new file mode 100644
index 0000000..390043b
--- /dev/null
+++ b/app/src/components/header.tsx
@@ -0,0 +1,9 @@
+import { Link } from "waku";
+
+export const Header = () => {
+ return (
+ <header className="flex items-center gap-4 p-6 lg:fixed lg:left-0 lg:top-0">
+ <h2 className="text-lg font-bold tracking-tight"></h2>
+ </header>
+ );
+};
diff --git a/app/src/lib/bookmark-models.ts b/app/src/lib/bookmark-models.ts
new file mode 100644
index 0000000..298bbec
--- /dev/null
+++ b/app/src/lib/bookmark-models.ts
@@ -0,0 +1,46 @@
+import { TwitterBookmark } from "./twitter-api";
+
+export interface Bookmark {
+ created: number | null;
+ origin: { twatter: TwitterBookmark } | { url: string };
+ tags: string[];
+}
+
+export interface ProcessedBookmark {
+ id: string;
+ originalTweet: TwitterBookmark;
+ category: string;
+ summary: string;
+ keyPoints: string[];
+ action: "store" | "schedule" | "archive" | "ignore";
+ actionData?: {
+ vaultPath?: string;
+ eventDate?: string;
+ eventTitle?: string;
+ eventDescription?: string;
+ };
+ processedAt: string;
+ llmAnalysis: {
+ model: string;
+ promptTokens: number;
+ completionTokens: number;
+ reasoning: string;
+ };
+}
+
+export interface BookmarkSyncStatus {
+ lastSync: string | null;
+ totalBookmarks: number;
+ processedBookmarks: number;
+ pendingBookmarks: number;
+ error?: string;
+}
+
+export interface BookmarkFilters {
+ category?: string;
+ author?: string;
+ dateFrom?: string;
+ dateTo?: string;
+ hasAction?: boolean;
+ actionType?: "store" | "schedule" | "archive" | "ignore";
+}
diff --git a/app/src/lib/bookmark-storage.ts b/app/src/lib/bookmark-storage.ts
new file mode 100644
index 0000000..2c4f397
--- /dev/null
+++ b/app/src/lib/bookmark-storage.ts
@@ -0,0 +1,62 @@
+import { TwitterBookmark, ProcessedBookmark, BookmarkSyncStatus } from './bookmark-models';
+
+export class BookmarkStorageService {
+ private static readonly STORAGE_KEYS = {
+ BOOKMARKS: 'twitter_bookmarks',
+ PROCESSED_BOOKMARKS: 'processed_bookmarks',
+ SYNC_STATUS: 'sync_status',
+ };
+
+ static saveBookmarks(bookmarks: TwitterBookmark[]): void {
+ if (typeof window === 'undefined') return;
+
+ const existing = this.getBookmarks();
+ const newBookmarks = bookmarks.filter(
+ (newBookmark) => !existing.some((existing) => existing.id === newBookmark.id)
+ );
+
+ const allBookmarks = [...existing, ...newBookmarks];
+ localStorage.setItem(this.STORAGE_KEYS.BOOKMARKS, JSON.stringify(allBookmarks));
+ }
+
+ static getBookmarks(): TwitterBookmark[] {
+ if (typeof window === 'undefined') return [];
+ const data = localStorage.getItem(this.STORAGE_KEYS.BOOKMARKS);
+ return data ? JSON.parse(data) : [];
+ }
+
+ static getBookmarkById(id: string): TwitterBookmark | null {
+ if (typeof window === 'undefined') return null;
+ const bookmarks = this.getBookmarks();
+ return bookmarks.find((bookmark) => bookmark.id === id) || null;
+ }
+
+ static clearAll(): void {
+ if (typeof window === 'undefined') return;
+ localStorage.removeItem(this.STORAGE_KEYS.BOOKMARKS);
+ localStorage.removeItem(this.STORAGE_KEYS.PROCESSED_BOOKMARKS);
+ localStorage.removeItem(this.STORAGE_KEYS.SYNC_STATUS);
+ }
+
+ static exportBookmarks(): string {
+ if (typeof window === 'undefined') return '';
+ const bookmarks = this.getBookmarks();
+ const data = {
+ bookmarks,
+ exportedAt: new Date().toISOString(),
+ };
+ return JSON.stringify(data, null, 2);
+ }
+
+ static importBookmarks(jsonData: string): void {
+ if (typeof window === 'undefined') return;
+ try {
+ const data = JSON.parse(jsonData);
+ if (data.bookmarks) {
+ localStorage.setItem(this.STORAGE_KEYS.BOOKMARKS, JSON.stringify(data.bookmarks));
+ }
+ } catch (error) {
+ throw new Error('Invalid bookmark data format');
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/lib/categorization.ts b/app/src/lib/categorization.ts
new file mode 100644
index 0000000..a692240
--- /dev/null
+++ b/app/src/lib/categorization.ts
@@ -0,0 +1,102 @@
+// import { Database } from "bun:sqlite";
+// const db = new Database("data/bookmarks.db", { create: true });
+import { TwitterBookmark } from "./twitter-api";
+
+export interface CategorySuggestion {
+ categories: string[];
+ confidence: number;
+ reasoning: string;
+}
+
+export interface CategorizationRequest {
+ bookmark: TwitterBookmark;
+ userCategories: Array<{ name: string; criteria: string }>;
+}
+
+export interface CategorizationResponse {
+ suggestedCategories: CategorySuggestion[];
+ newCategories: string[];
+ summary: string;
+ keyTopics: string[];
+}
+
+export interface UserCategories {
+ categories: string[];
+}
+
+export const userCategories = [
+ { name: "AI", criteria: "AI, machine learning, LLMs, all AI related stuff" },
+ { name: "Nix", criteria: "Nix and NixOS" },
+ { name: "Urbit", criteria: "anything related to Urbit" },
+ {
+ name: "Code",
+ criteria:
+ "other stuff related to software or software development (excludes the previous)",
+ },
+ { name: "history", criteria: "posts about history or archeology" },
+ { name: "Politics", criteria: "posts about politics" },
+ {
+ name: "China",
+ criteria:
+ "posts about China or in Chinese which don't fall under any other category",
+ },
+ {
+ name: "Japan",
+ criteria:
+ "posts about Japan or in Japanese which don't fall under any other category",
+ },
+ { name: "Movies", criteria: "posts related to movies, anime or TV shows" },
+ { name: "Demographics", criteria: "posts about demographics or birth rates" },
+ { name: "Memes", criteria: "posts including memes or otherwise funny" },
+];
+
+type FileTree = Map<string, FileEntry[]>;
+type FileEntry = { file: string } | { dir: FileTree };
+// type FileTree = { dir: `${string}/`; files: string[]; dirs?: Record<string, FileTree> };
+
+class Obsidian {
+ auth = `Bearer ${Bun.env.OBSIDIAN_API_KEY!}`;
+ url = `http://127.0.0.1:27123`;
+ tree: FileTree = new Map();
+ async getFiles() {}
+ async list() {
+ this.populate(this.tree, "/");
+ }
+ async populate(parent: FileTree, dir: `${string}/`) {
+ const entries: FileEntry[] = [];
+ const res = (await this.call("/vault/" + dir)) as { files: string[] };
+ for (let entry of res.files) {
+ if (entry.endsWith(".md")) entries.push({ file: entry });
+ else if (entry.endsWith("/")) this.populate(new Map(), entry as any);
+ // entries.push({ dir: new Map() });
+ }
+ parent.set(dir, entries);
+ }
+ async call(path: string) {
+ const headers = { Authorization: this.auth };
+ const opts = { headers };
+ const res = await fetch(this.url + path, opts);
+ const j = await res.json();
+ return j;
+ }
+ async post(path: string, body: string) {
+ const headers = { Authorization: this.auth };
+ const opts = { headers, method: "POST", body };
+ const res = await fetch(this.url + path, opts);
+ return res;
+ }
+ async saveToDisk(
+ bookmark: TwitterBookmark,
+ categorization: CategorizationResponse,
+ tags: string[],
+ ) {
+ // append to file
+ const body = `\n\n`;
+ const opts = { method: "POST", headers, body };
+ const res = await this.post("/vault/", body);
+ if (!res.ok) {
+ const j = await res.json();
+ return { error: j.message };
+ } else return { ok: "" };
+ }
+}
diff --git a/app/src/lib/llm-prompts.ts b/app/src/lib/llm-prompts.ts
new file mode 100644
index 0000000..d06b010
--- /dev/null
+++ b/app/src/lib/llm-prompts.ts
@@ -0,0 +1,50 @@
+export const CATEGORIZATION_PROMPT = `You are an expert content analyst tasked with categorizing social media bookmarks. Your goal is to analyze the provided bookmark content and suggest appropriate categories based on the content, context, and user's predefined categories.
+
+## Task
+Analyze the bookmark content and provide intelligent category suggestions. Consider the text, any images, hashtags, URLs, and overall context.
+
+## Input Format
+You will receive:
+1. **Bookmark details**: text content, author info, language, hashtags, URLs
+2. **User-defined categories**: A list of existing categories the user has defined and the criteria to use them
+3. **Media**: Optional images (as URLs) that may be relevant to categorization
+
+## Analysis Guidelines
+- **Primary Focus**: Use the text content as the main source for categorization
+- **Image Analysis**: Briefly describe any images if provided - look for visual themes, text in images, or context that might suggest categories
+- **URL Context**: Consider the domains and content of linked URLs
+- **Language**: Factor in the post's language when relevant
+- **Hashtags**: Use hashtags as strong indicators of topics/themes
+- **Multi-categorization**: A single bookmark can belong to multiple categories
+- **Confidence**: Rate your confidence for each suggestion (0-1 scale)
+
+## Response Structure
+Provide your analysis in this JSON format:
+{
+ "suggestedCategories": [
+ {
+ "categories": ["category1", "category2"],
+ "confidence": 0.9,
+ "reasoning": "Brief explanation of why these categories fit"
+ }
+ ],
+ "newCategories": ["suggested_new_category1", "suggested_new_category2"],
+ "summary": "Brief 1-2 sentence summary of the bookmark's main topic",
+ "keyTopics": ["topic1", "topic2", "topic3"]
+}
+
+## Rules
+1. **Use existing categories** when they clearly fit
+2. **Suggest new categories** when content doesn't match existing ones
+3. **Be specific** - prefer specific categories over broad ones when appropriate
+4. **Combine categories** when a bookmark spans multiple topics
+5. **Avoid redundancy** - don't suggest similar/overlapping categories
+6. **Be concise** in reasoning and summary
+
+## Image Analysis Instructions
+When images are provided:
+- Describe the main visual elements in 1-2 sentences
+- Note any text visible in images
+- Identify visual themes that might suggest categories
+- Skip video content (not provided)
+`;
diff --git a/app/src/lib/llm-service.ts b/app/src/lib/llm-service.ts
new file mode 100644
index 0000000..af82fe2
--- /dev/null
+++ b/app/src/lib/llm-service.ts
@@ -0,0 +1,205 @@
+// import OpenAI from "openai";
+import { createUserContent, GoogleGenAI } from "@google/genai";
+
+import type {
+ CategorizationRequest,
+ CategorizationResponse,
+} from "./categorization";
+import { CATEGORIZATION_PROMPT } from "./llm-prompts";
+
+export interface LLMRequestOptions {
+ model?: string;
+ temperature?: number;
+ maxTokens?: number;
+}
+
+export class LLMService {
+ private defaultOptions: LLMRequestOptions = {};
+ api;
+ model;
+ constructor(
+ // baseURL: string,
+ apiKey: string,
+ options: LLMRequestOptions = {},
+ ) {
+ // const client = new OpenAI({ baseURL, apiKey });
+ const client = new GoogleGenAI({ apiKey });
+ this.api = client;
+ this.model = "gemini-2.5-flash";
+ // this.defaultOptions = {
+ // model: "gemini-2.5-flash",
+ // temperature: 0.3,
+ // maxTokens: 1000,
+ // ...options,
+ // ...options
+ // };
+ }
+
+ async categorizeBookmark(
+ request: CategorizationRequest,
+ options?: LLMRequestOptions,
+ ): Promise<CategorizationResponse> {
+ const mergedOptions = { ...this.defaultOptions, ...options };
+
+ const prompt = this.buildCategorizationPrompt(request);
+ const media = [];
+ const allPics = request.bookmark.media.pics;
+ if (request.bookmark.media.video.thumb)
+ allPics.push(request.bookmark.media.video.thumb);
+ for (const pic of allPics) {
+ const imgdata = await fetch(pic);
+ const imageArrayBuffer = await imgdata.arrayBuffer();
+ const base64ImageData = Buffer.from(imageArrayBuffer).toString("base64");
+ const mimeType = imgdata.headers.get("content-type") || "image/jpeg";
+ const con = { inlineData: { mimeType, data: base64ImageData } };
+ media.push(con);
+ }
+
+ const contents = media
+ ? createUserContent([
+ prompt[0],
+ `**Media**: The bookmark included the following images:`,
+ ...media,
+ prompt[1],
+ ])
+ : [prompt[0] + "\n\n" + prompt[1]];
+
+ try {
+ const response = await this.api.models.generateContent({
+ model: this.model,
+ contents,
+ config: { systemInstruction: CATEGORIZATION_PROMPT },
+ });
+ console.log("llm res", response);
+
+ if (!response.text) {
+ throw new Error("No response content from LLM");
+ }
+
+ return this.parseCategorizationResponse(response.text);
+ } catch (error) {
+ console.error("Error in LLM categorization:", error);
+ throw error;
+ }
+ }
+
+ private buildCategorizationPrompt(
+ request: CategorizationRequest,
+ ): [string, string] {
+ const { bookmark, userCategories } = request;
+
+ const prompt: [string, string] = [
+ `
+Analyze the following bookmark and provide your categorization suggestions.
+
+## Bookmark Details
+**Text**: "${bookmark.text}"
+**Language**: ${bookmark.language}
+**Author**: ${bookmark.author.name} (@${bookmark.author.username})
+**Hashtags**: ${bookmark.hashtags.join(", ") || "None"}
+**URLs**: ${bookmark.urls.map((u) => u.expandedUrl).join(", ") || "None"}`,
+
+ `## User Categories
+${userCategories.map((cat) => `- ${cat.name}: ${cat.criteria}`).join("\n")}
+Please provide your categorization analysis in the requested JSON format.
+`,
+ ];
+
+ return prompt;
+ }
+
+ private parseCategorizationResponse(content: string): CategorizationResponse {
+ try {
+ // Try to extract JSON from the response
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
+ if (!jsonMatch) {
+ throw new Error("No JSON found in response");
+ }
+
+ const parsed = JSON.parse(jsonMatch[0]);
+
+ // Validate response structure
+ if (
+ !parsed.suggestedCategories ||
+ !Array.isArray(parsed.suggestedCategories)
+ ) {
+ throw new Error(
+ "Invalid response structure: missing suggestedCategories",
+ );
+ }
+
+ return {
+ suggestedCategories: parsed.suggestedCategories.map((s: any) => ({
+ categories: Array.isArray(s.categories)
+ ? s.categories
+ : [s.categories],
+ confidence: typeof s.confidence === "number" ? s.confidence : 0.5,
+ reasoning: s.reasoning || "No reasoning provided",
+ })),
+ newCategories: Array.isArray(parsed.newCategories)
+ ? parsed.newCategories
+ : [],
+ summary: parsed.summary || "No summary provided",
+ keyTopics: Array.isArray(parsed.keyTopics) ? parsed.keyTopics : [],
+ };
+ } catch (error) {
+ console.error("Error parsing LLM response:", error);
+
+ // Return fallback response
+ return {
+ suggestedCategories: [
+ {
+ categories: ["Uncategorized"],
+ confidence: 0.1,
+ reasoning: "Failed to parse LLM response",
+ },
+ ],
+ newCategories: [],
+ summary: "Analysis failed",
+ keyTopics: [],
+ };
+ }
+ }
+
+ // Generic LLM request method for future use
+ // async sendPrompt(
+ // prompt: string,
+ // options?: LLMRequestOptions,
+ // ): Promise<string> {
+ // const mergedOptions = { ...this.defaultOptions, ...options };
+
+ // try {
+ // const response = await fetch(`${this.baseUrl}/v1/messages`, {
+ // method: "POST",
+ // headers: {
+ // "Content-Type": "application/json",
+ // Authorization: `Bearer ${this.apiKey}`,
+ // "anthropic-version": "2023-06-01",
+ // },
+ // body: JSON.stringify({
+ // model: mergedOptions.model,
+ // max_tokens: mergedOptions.maxTokens,
+ // temperature: mergedOptions.temperature,
+ // messages: [
+ // {
+ // role: "user",
+ // content: prompt,
+ // },
+ // ],
+ // }),
+ // });
+
+ // if (!response.ok) {
+ // throw new Error(
+ // `LLM API request failed: ${response.status} ${response.statusText}`,
+ // );
+ // }
+
+ // const data = await response.json();
+ // return data.content[0]?.text || "";
+ // } catch (error) {
+ // console.error("Error in generic LLM request:", error);
+ // throw error;
+ // }
+ // }
+}
diff --git a/app/src/lib/testData.json b/app/src/lib/testData.json
new file mode 100644
index 0000000..327abe9
--- /dev/null
+++ b/app/src/lib/testData.json
@@ -0,0 +1 @@
+[{"id":"1944967214496411839","text":"how does this have only 415 views ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1753913053672857600/JSKl-7sD_normal.jpg","name":"varepsilon","username":"var_epsilon"},"createdAt":"Tue Jul 15 03:48:14 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/Gv3pNOLWcAEUVCI.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944720279319498929","text":"Unhappy with current apps on the market, I built my own voice tracking teleprompter app with @expo using @cursor_ai \n\nIt was really difficult, particularly as it required fuzzy matching and ways to handle mispronunciation. I think I have it nailed now though and I now have the ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1899346428448632833/OFfFFjPF_normal.jpg","name":"Gregory John","username":"_gregoryjohn"},"createdAt":"Mon Jul 14 11:27:00 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1872029913852862736","text":"SnapEdit is in the a16z's Top 50 AI Mobile Apps. https://t.co/guT2O9kdsH\n\nhttps://t.co/jKfDyNf45i #AIMobileApps #AIPhotoEditor #a16z #snapedit ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1871368318646415360/0Wqpb9nX_normal.jpg","name":"Oscar Le","username":"oscarle_x"},"createdAt":"Wed Dec 25 21:21:26 +0000 2024","urls":[{"expandedUrl":"https://a16z.com/100-gen-ai-apps/","displayUrl":"a16z.com/100-gen-ai-app…"},{"expandedUrl":"https://apps.apple.com/us/app/id1611282499","displayUrl":"apps.apple.com/us/app/id16112…"}],"media":{"pics":["https://pbs.twimg.com/media/GfrIJGtWwAAoPh7.jpg"],"video":{"thumb":"","url":""}},"hashtags":["AIMobileApps","AIPhotoEditor","a16z","snapedit"]},{"id":"1944575986118627684","text":"Let alone that the MAGA grift machine did campaign on this being the root conspiracy behind why we can't have nice things and that 180 alone deserves a chimpout. McVeigh bombed a bunch of shit for less","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1919365688868659200/vCoE927n_normal.jpg","name":"Spandrell","username":"spandrell4"},"createdAt":"Mon Jul 14 01:53:38 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944795907637297506","text":"when i first learned Machine Learning, our professor ingrained into us how every ML problem starts by splitting data into train, test, and validation\n\nthese days there is just train and test. in many cases there is just train and more train\n\nwhere’d all the validation sets go?","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1920483027303399424/IxcwX1P7_normal.jpg","name":"jxmo","username":"jxmnop"},"createdAt":"Mon Jul 14 16:27:31 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944556815892750689","text":"Here's her raw interview. She loved him. \n\nhttps://t.co/3G44ox77Ep","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1863012867085099008/tH0b4l25_normal.jpg","name":"Joshua","username":"FuzzyManStudios"},"createdAt":"Mon Jul 14 00:37:27 +0000 2025","urls":[{"expandedUrl":"https://www.youtube.com/watch?v=X9g7a_HOptk","displayUrl":"youtube.com/watch?v=X9g7a_…"}],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944660683205095653","text":"This is interesting. One dev is training an AI from scratch on books from 1800s London.\n\nIt's called TimeCapsuleLLM, not a fine-tuned modern model, but one trained entirely on historical data. No modern language or context.\n\nBuilt on nanoGPT by @karpathy. https://t.co/oJyyeqnG3Z","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1726902134770454528/YCYCCwrz_normal.jpg","name":"👋 Jan","username":"jandotai"},"createdAt":"Mon Jul 14 07:30:11 +0000 2025","urls":[{"expandedUrl":"https://github.com/haykgrigo3/TimeCapsuleLLM","displayUrl":"github.com/haykgrigo3/Tim…"}],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944705474059718789","text":"Claude Code pro tip:\n\nUse Kimi K2 as your agent by changing the base URL and setting the API key before running claude.\n\nall prompts and code requests will now go through Kimi, in the CC terminal you're used to! ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1932749344836390914/JUlNRQim_normal.jpg","name":"Ian Nuttall","username":"iannuttall"},"createdAt":"Mon Jul 14 10:28:10 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/Gvz6-GfWQAAEtfy.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944721069400871018","text":"我去,老马终于整了一个好活\n\n你现在可以在 Grok APP 里面跟 3D 虚拟角色实时聊天了\n\n而且聊天背景还能根据你们聊天的内容实时更换\n\nhttps://t.co/BbhxkAmyAd","language":"zh","author":{"avatar":"https://pbs.twimg.com/profile_images/1636981205504786434/xDl77JIw_normal.jpg","name":"歸藏(guizang.ai)","username":"op7418"},"createdAt":"Mon Jul 14 11:30:08 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944723471990235340","text":"Fear and Hunger, Felvidek, Disco Elysium...","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1690074184624287745/aBPeoMVj_normal.jpg","name":"Marko Jukic","username":"mmjukic"},"createdAt":"Mon Jul 14 11:39:41 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944588006448570843","text":"Solving Leetcode hards becomes a joke with multi-shot continuations.\n\nBelow is N-Queens in ~20 lines of simple, readable code. Learning functional programming means you never have to fear a technical screen again. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1857824460159725568/KKGWrwlN_normal.jpg","name":"Adam Hearn","username":"adamhearn_"},"createdAt":"Mon Jul 14 02:41:24 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/GvyQIWBacAAltOr.jpg","https://pbs.twimg.com/media/GvyP-rDW8AAbjje.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944753309761638663","text":"This weekend I played around with Engine. It's an async AI software engineering agent.\n\nI gave it a few tasks, it was easy to use and handled things smoothly without much setup.\n\nThey've built something solid with multiple integrations and support for top models like Claude 4, o3 ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1687401026805256192/uQ1o2wVQ_normal.jpg","name":"AshutoshShrivastava","username":"ai_for_success"},"createdAt":"Mon Jul 14 13:38:15 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944266132221047036","text":"去年12月,大连工业大学学生李欣莳与乌克兰电竞选手Zeus(37岁,已婚已育)发生性关系,事后,Zeus将未打码的私密视频发布至社交平台,并标注中国女性为“Eazy ","language":"zh","author":{"avatar":"https://pbs.twimg.com/profile_images/1527433965824802816/PgXCsVL6_normal.jpg","name":"王局志安","username":"wangzhian8848"},"createdAt":"Sun Jul 13 05:22:23 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/GvtroCIWsAAS6co.jpg","https://pbs.twimg.com/media/GvtroCOW0AAIwkR.jpg","https://pbs.twimg.com/media/GvtroEEWgAAiVSh.jpg","https://pbs.twimg.com/media/GvtroBrbsAMjq61.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944686305650049427","text":"Movie scenes inspired by anime 🎥\n https://t.co/VVwZfreQi2","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1385452682517745667/mfcIUnoX_normal.jpg","name":"cinesthetic.","username":"TheCinesthetic"},"createdAt":"Mon Jul 14 09:12:00 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944580587895038421","text":"Apparently it's not illegal to pay others to sterilize themselves, and \"Project Prevention\" has paid 8,000 crack addicts $300 to get IUDs, vasectomies, and tubal ligations\n\n&gt; The organization has used slogans such as \"Don't let pregnancy get in the way of your crack habit\" ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1632229521683513344/_kSyjhE3_normal.jpg","name":"Arjun Panickssery","username":"panickssery"},"createdAt":"Mon Jul 14 02:11:55 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/GvyJTH9agAAzvfZ.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944412444316561738","text":"Hunch: @claude_code is just as useful for product / design as it is for engineers\n\nSo I'm experimenting with the following claude commands: \n\n/polish - a command which accepts a page route and polishes the UI / components based on common errors, best practices and a pixel-perfect","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1545142518983032832/LiHomIUy_normal.jpg","name":"Trist","username":"trist_adlington"},"createdAt":"Sun Jul 13 15:03:46 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944669636886196519","text":"New Success Story: Secure Internet Services with OCaml and MirageOS 🔒\n\nRobur, a worker-owned collective, builds secure, high-performance, and resource-efficient software solutions!\n\nOCaml's static typing eliminates runtime errors with predictable performance \ud83d","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1707339550131933184/Zc0v3QdU_normal.jpg","name":"OCaml","username":"ocaml_org"},"createdAt":"Mon Jul 14 08:05:46 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944326308210921652","text":"Quick start project for Claude Code on Kimi:","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1279600070145437696/eocLhSLu_normal.jpg","name":"Jeremy Howard","username":"jeremyphoward"},"createdAt":"Sun Jul 13 09:21:30 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944418521301364787","text":"I got these amazing character sprites from itchio by SmallScaleInt.\n\nThe character portrait and some minor elements were made with retrodiffusion.\n\nAnd the map was generated with Midjourney!","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1653760191542996994/gxsaTn-0_normal.png","name":"Danny Limanseta","username":"DannyLimanseta"},"createdAt":"Sun Jul 13 15:27:55 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1895493789353120094","text":"I've built 4 game prototypes with @grok so far. I've always wanted to make games since young but I suck at coding, until... Grok happened.\n\nI think I need to go lie down for a bit. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1653760191542996994/gxsaTn-0_normal.png","name":"Danny Limanseta","username":"DannyLimanseta"},"createdAt":"Fri Feb 28 15:18:30 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944415010836599003","text":"I took Grok 4 for a spin this weekend to build this game prototype. \n\nI used SuperGrok Chat to generate the initial game prototype and then brought it over to Cursor to continue coding with Grok 4 MAX.\n\nGrok 4 in Cursor is like a no-nonsense agent. Doesn't speak much, but ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1653760191542996994/gxsaTn-0_normal.png","name":"Danny Limanseta","username":"DannyLimanseta"},"createdAt":"Sun Jul 13 15:13:58 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944320164889284947","text":"Kimi K2 - On-par with Claude 4, but 80% cheaper!!\n\nI connected Kimi K2 to Claude Code to get a sense of real performance (Kimi Code!)\n\nOverall findings:\n1. Exceptional coding capability\n2. Cost only 20% of Claude 4 (Huge!)\n2. Only downside is API is a bit slow\n\n🧵 Below is some","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1613651966663749632/AuQiWkVc_normal.jpg","name":"Jason Zhou","username":"jasonzhou1993"},"createdAt":"Sun Jul 13 08:57:05 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/Gvt2zqzbsAAQIMD.png","https://pbs.twimg.com/media/Gvt3T01XEAA04nu.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944561737317179734","text":"There is a specific kind of Chinese person that can only be described as a 指黑逼. Their negativity isn’t about practical constructive input but petty malcontent that socially perfect utopian fantasyland doesn’t come at the speed of their self righteous moral preening.","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/620173069487714305/x-wYA0Pd_normal.jpg","name":"Lei Gong","username":"gonglei89"},"createdAt":"Mon Jul 14 00:57:01 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944512201185956286","text":"nice trend over the last year is that folks in AI have finally produced a few libraries with the right abstractions\n\nfinally our code can be both hackable and fast, not just one or the other. this never used to happen\n\nvLLM, sglang, verl..\nthis is the dawn of Good Software in AI","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1920483027303399424/IxcwX1P7_normal.jpg","name":"jxmo","username":"jxmnop"},"createdAt":"Sun Jul 13 21:40:10 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944522045792116831","text":"A free and opensource app that lets you gain an unfair advantage. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1679831765744259073/hoVtsOZ9_normal.jpg","name":"GitHub Projects Community","username":"GithubProjects"},"createdAt":"Sun Jul 13 22:19:17 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/GvxUfShXkAAX_7r.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944444946246726000","text":"The Interactive DeepResearch Reports by Kimi-K2 look pretty sleek ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1831493788679761920/-q9w6dzd_normal.jpg","name":"Lisan al Gaib","username":"scaling01"},"createdAt":"Sun Jul 13 17:12:55 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/GvwOIKOW4AEgBGY.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944325644244107621","text":"run this project directly, avoiding the complicated environment variable setup process. 1 million tokens cost just a little over 2 dollars, which is very cheap.\n\nhttps://t.co/IjfpeTFJ0v","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1636981205504786434/xDl77JIw_normal.jpg","name":"歸藏(guizang.ai)","username":"op7418"},"createdAt":"Sun Jul 13 09:18:52 +0000 2025","urls":[{"expandedUrl":"https://github.com/LLM-Red-Team/kimi-cc","displayUrl":"github.com/LLM-Red-Team/k…"}],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944459901952246152","text":"Kimi is the real deal. Unless it's really Sonnet in a trench coat, this is the best agentic open-source model I've tested - BY A MILE.\n\nHere's a slice* of a 4 HOUR run (~1 second per minute) with not much more than 'keep going' from me every 90 minutes or so.\n\nThe task involved ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1929595014734319616/tlqu7nHF_normal.jpg","name":"Hrishi","username":"hrishioa"},"createdAt":"Sun Jul 13 18:12:21 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1942610414514425940","text":"That said, studies on female political radicalism are much less robust, less interesting, and less useful than studies on female marketing. \n\nYou, an incel: \"lol women bad\"\nZhang, an entrepreneur: \"My labubu supplier factory reached 8 digits MRR\"","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1865201729459097604/gPS9TgXr_normal.jpg","name":"zhil","username":"zhil_arf"},"createdAt":"Tue Jul 08 15:43:09 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944495244973838647","text":"Chapter 4 of NASA’s Systems Engineering Handbook doubles as an *extremely* high-quality prompting guide for AI.\n\nIt’s a “How to work effectively with coding agents” masterclass in disguise.\n\nHandbook below. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1552979440547704832/WX5crG9I_normal.jpg","name":"Mckay Wrigley","username":"mckaywrigley"},"createdAt":"Sun Jul 13 20:32:48 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/Gvw8HZhWoAANZOs.jpg","https://pbs.twimg.com/media/Gvw8HZXXAAAVWsb.jpg","https://pbs.twimg.com/media/Gvw8HY5bIAAWHXL.jpg","https://pbs.twimg.com/media/Gvw8HY5boAAlnYx.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944414987679637998","text":"K2 is not «up there», it's straight up the best writing model now, with near 200 Elo gain over the next non-reasoner. I don't know how much of that is just our unfamiliarity with its new characteristic verbal tics, but EQ-Bench isn't that easy to game. Immaculate taste.","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1652169745037242368/KRPTShbG_normal.jpg","name":"Teortaxes▶️ (DeepSeek 推特🐋铁粉 2023 – ∞)","username":"teortaxesTex"},"createdAt":"Sun Jul 13 15:13:53 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944390781147480398","text":"A flux kontext dev lora to make an image old and damaged, using Replicate's prototype trainer. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1732174612178350080/X1uR3MvQ_normal.jpg","name":"fofr","username":"fofrAI"},"createdAt":"Sun Jul 13 13:37:41 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/Gvvc4kUWIAAf8-I.jpg","https://pbs.twimg.com/media/Gvvc7IhXwAENWgM.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944404781964918865","text":"I'm glad I had headphones on...","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1837802996723597312/Jk4w7gjp_normal.jpg","name":"Zephyr","username":"zephyr_z9"},"createdAt":"Sun Jul 13 14:33:19 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1943757786284011656","text":"5 minutes and 53 seconds of total screen time earned Ned Beatty's the nomination for the Oscar for Best Supporting Actor in \"Network\" (1976), shortest ever. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1793592262586146816/9pQ2VB5T_normal.jpg","name":"VIKARE","username":"vikare06"},"createdAt":"Fri Jul 11 19:42:24 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944011517642658017","text":"Introducing Zenith - the Cursor for hardware. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1943411125506445316/I5HzZiLe_normal.jpg","name":"Harish Ashok","username":"habril27"},"createdAt":"Sat Jul 12 12:30:38 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944254257235898711","text":"Edit your frontend UI live, with AI prompts on real DOM elements. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1679831765744259073/hoVtsOZ9_normal.jpg","name":"GitHub Projects Community","username":"GithubProjects"},"createdAt":"Sun Jul 13 04:35:12 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944026310050767076","text":"You probably wondered how websites like this were made. \n\nSo here is a quick breakdown: ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1561868205005721601/dKdHIdm7_normal.jpg","name":"Kabarza","username":"kabarza_"},"createdAt":"Sat Jul 12 13:29:25 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944192051068727527","text":"My greatest fear is that this guy was right ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1145820424879689728/ZUas1hp0_normal.jpg","name":"James Kirkpatrick","username":"VDAREJamesK"},"createdAt":"Sun Jul 13 00:28:00 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/GvsoXedXoAAZegq.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944337629723463983","text":"Actually, even better.\n\nWrite a program to parse a logic formula and pretty print its truth table.\n\nIt’ll teach you 3 months of CS.","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1734291475309940737/csd2XGDA_normal.jpg","name":"Dmitrii Kovanikov","username":"ChShersh"},"createdAt":"Sun Jul 13 10:06:29 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944058501900513388","text":"What a cool behind-the-scenes look at how @Beelinkofficial builds their mini PCs. Didn't know they did so much pre-shipping testing! https://t.co/b3P3cDXQsZ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1746980162607140864/fG9Fj4K__normal.jpg","name":"DHH","username":"dhh"},"createdAt":"Sat Jul 12 15:37:20 +0000 2025","urls":[{"expandedUrl":"https://youtu.be/ohwI3V207Ts?si=XhQVRoGIcU6vLUX2","displayUrl":"youtu.be/ohwI3V207Ts?si…"}],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]}] \ No newline at end of file
diff --git a/app/src/lib/testData.ts b/app/src/lib/testData.ts
new file mode 100644
index 0000000..d06b010
--- /dev/null
+++ b/app/src/lib/testData.ts
@@ -0,0 +1,50 @@
+export const CATEGORIZATION_PROMPT = `You are an expert content analyst tasked with categorizing social media bookmarks. Your goal is to analyze the provided bookmark content and suggest appropriate categories based on the content, context, and user's predefined categories.
+
+## Task
+Analyze the bookmark content and provide intelligent category suggestions. Consider the text, any images, hashtags, URLs, and overall context.
+
+## Input Format
+You will receive:
+1. **Bookmark details**: text content, author info, language, hashtags, URLs
+2. **User-defined categories**: A list of existing categories the user has defined and the criteria to use them
+3. **Media**: Optional images (as URLs) that may be relevant to categorization
+
+## Analysis Guidelines
+- **Primary Focus**: Use the text content as the main source for categorization
+- **Image Analysis**: Briefly describe any images if provided - look for visual themes, text in images, or context that might suggest categories
+- **URL Context**: Consider the domains and content of linked URLs
+- **Language**: Factor in the post's language when relevant
+- **Hashtags**: Use hashtags as strong indicators of topics/themes
+- **Multi-categorization**: A single bookmark can belong to multiple categories
+- **Confidence**: Rate your confidence for each suggestion (0-1 scale)
+
+## Response Structure
+Provide your analysis in this JSON format:
+{
+ "suggestedCategories": [
+ {
+ "categories": ["category1", "category2"],
+ "confidence": 0.9,
+ "reasoning": "Brief explanation of why these categories fit"
+ }
+ ],
+ "newCategories": ["suggested_new_category1", "suggested_new_category2"],
+ "summary": "Brief 1-2 sentence summary of the bookmark's main topic",
+ "keyTopics": ["topic1", "topic2", "topic3"]
+}
+
+## Rules
+1. **Use existing categories** when they clearly fit
+2. **Suggest new categories** when content doesn't match existing ones
+3. **Be specific** - prefer specific categories over broad ones when appropriate
+4. **Combine categories** when a bookmark spans multiple topics
+5. **Avoid redundancy** - don't suggest similar/overlapping categories
+6. **Be concise** in reasoning and summary
+
+## Image Analysis Instructions
+When images are provided:
+- Describe the main visual elements in 1-2 sentences
+- Note any text visible in images
+- Identify visual themes that might suggest categories
+- Skip video content (not provided)
+`;
diff --git a/app/src/lib/twitter-api.ts b/app/src/lib/twitter-api.ts
new file mode 100644
index 0000000..6dc41c0
--- /dev/null
+++ b/app/src/lib/twitter-api.ts
@@ -0,0 +1,308 @@
+const TWITTER_INTERNAL_API_KEY =
+ "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
+
+interface TwitterBookmarkResponse {
+ data: {
+ bookmark_timeline_v2: {
+ timeline: {
+ instructions: Array<{
+ type: string;
+ entries?: Array<{
+ content: {
+ entryType: string;
+ itemContent?: {
+ tweet_results: {
+ result: {
+ rest_id: string;
+ core: {
+ user_results: {
+ result: {
+ name: string;
+ screen_name: string;
+ };
+ };
+ };
+ legacy: {
+ full_text: string;
+ created_at: string;
+ entities: {
+ urls?: Array<{
+ expanded_url: string;
+ display_url: string;
+ }>;
+ media?: Array<{
+ expanded_url: string;
+ display_url: string;
+ media_url_https: string;
+ media_key: string;
+ type: "photo" | "string";
+ }>;
+ hashtags?: Array<{
+ text: string;
+ }>;
+ };
+ };
+ };
+ };
+ };
+ };
+ }>;
+ }>;
+ };
+ };
+ };
+}
+
+export interface TwitterBookmark {
+ id: string;
+ text: string;
+ language: string;
+ author: {
+ avatar: string;
+ name: string;
+ username: string;
+ };
+ createdAt: string;
+ urls: Array<{
+ expandedUrl: string;
+ displayUrl: string;
+ }>;
+ media: { pics: string[]; video: { thumb: string; url: string } };
+ hashtags: string[];
+}
+
+export class TwitterApiService {
+ cookie: string;
+ constructor(cookie: string) {
+ this.cookie = cookie;
+ }
+ private static readonly BOOKMARKS_URL =
+ "https://x.com/i/api/graphql/C7CReOA1R0PwKorWAxnNUQ/Bookmarks";
+
+ private static readonly FEATURES = {
+ rweb_video_screen_enabled: false,
+ payments_enabled: false,
+ profile_label_improvements_pcf_label_in_post_enabled: true,
+ rweb_tipjar_consumption_enabled: true,
+ verified_phone_label_enabled: false,
+ creator_subscriptions_tweet_preview_api_enabled: true,
+ responsive_web_graphql_timeline_navigation_enabled: true,
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
+ premium_content_api_read_enabled: false,
+ communities_web_enable_tweet_community_results_fetch: true,
+ c9s_tweet_anatomy_moderator_badge_enabled: true,
+ responsive_web_grok_analyze_button_fetch_trends_enabled: false,
+ responsive_web_grok_analyze_post_followups_enabled: true,
+ responsive_web_jetfuel_frame: true,
+ responsive_web_grok_share_attachment_enabled: true,
+ articles_preview_enabled: true,
+ responsive_web_edit_tweet_api_enabled: true,
+ graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
+ view_counts_everywhere_api_enabled: true,
+ longform_notetweets_consumption_enabled: true,
+ responsive_web_twitter_article_tweet_consumption_enabled: true,
+ tweet_awards_web_tipping_enabled: false,
+ responsive_web_grok_show_grok_translated_post: false,
+ responsive_web_grok_analysis_button_from_backend: true,
+ creator_subscriptions_quote_tweet_preview_enabled: false,
+ freedom_of_speech_not_reach_fetch_enabled: true,
+ standardized_nudges_misinfo: true,
+ tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:
+ true,
+ longform_notetweets_rich_text_read_enabled: true,
+ longform_notetweets_inline_media_enabled: true,
+ responsive_web_grok_image_annotation_enabled: true,
+ responsive_web_grok_community_note_auto_translation_is_enabled: false,
+ responsive_web_enhance_cards_enabled: false,
+ };
+
+ private static readonly HEADERS = {
+ "User-Agent":
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
+ Accept: "*/*",
+ Referer: "https://x.com/i/bookmarks",
+ "Content-Type": "application/json",
+ "X-Twitter-Auth-Type": "OAuth2Session",
+ "X-Twitter-Active-User": "yes",
+ "X-Twitter-Client-Language": "en",
+ };
+
+ async fetchBookmarks(cursor?: string): Promise<{
+ bookmarks: TwitterBookmark[];
+ nextCursor?: string;
+ hasMore: boolean;
+ }> {
+ const variables = {
+ count: 40,
+ cursor: cursor || null,
+ includePromotedContent: true,
+ };
+
+ const uri = new URL(TwitterApiService.BOOKMARKS_URL);
+ uri.searchParams.set("variables", JSON.stringify(variables));
+ uri.searchParams.set(
+ "features",
+ JSON.stringify(TwitterApiService.FEATURES),
+ );
+ const url = uri.toString();
+ const csrfm = this.cookie.match(/ct0=([^;]+)/);
+ const csrf = csrfm![1];
+ const nheaders = { "X-Csrf-Token": csrf! };
+
+ const headers = {
+ ...TwitterApiService.HEADERS,
+ Authorization: TWITTER_INTERNAL_API_KEY,
+ Cookie: this.cookie,
+ ...nheaders,
+ };
+ const response = await fetch(url, {
+ method: "GET",
+ headers,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `Twitter API request failed: ${response.status} ${response.statusText}`,
+ );
+ }
+
+ const data = (await response.json()) as TwitterBookmarkResponse;
+
+ return TwitterApiService.parseBookmarkResponse(data);
+ }
+
+ private static parseBookmarkResponse(response: TwitterBookmarkResponse): {
+ bookmarks: TwitterBookmark[];
+ nextCursor?: string;
+ hasMore: boolean;
+ } {
+ const bookmarks: TwitterBookmark[] = [];
+ let nextCursor: string | undefined;
+ let hasMore = false;
+
+ const instructions =
+ response.data?.bookmark_timeline_v2?.timeline?.instructions || [];
+
+ for (const instruction of instructions) {
+ if (instruction.type === "TimelineAddEntries") {
+ for (const entry of instruction.entries || []) {
+ if (entry.content.entryType === "TimelineTimelineItem") {
+ const tweetData = entry.content.itemContent?.tweet_results?.result;
+ if (tweetData) {
+ const bookmark = this.extractBookmarkData(tweetData);
+ if (bookmark) {
+ bookmarks.push(bookmark);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ const ret = { bookmarks, hasMore };
+ return nextCursor ? { ...ret, nextCursor } : ret;
+ }
+
+ private static extractBookmarkData(tweetData: any): TwitterBookmark | null {
+ try {
+ const legacy = tweetData.legacy;
+ const userResults = tweetData.core?.user_results?.result;
+
+ if (!legacy || !userResults) {
+ return null;
+ }
+ const mediaEntities = legacy.entities?.media || [];
+ const media = mediaEntities.reduce(
+ (acc: { pics: string[]; video: string }, item: any) => {
+ if (item.type === "photo")
+ return {
+ pics: [...acc.pics, item.media_url_https],
+ video: acc.video,
+ };
+ if (item.type === "video") {
+ const video = item.video_info.variants.reduce(
+ (acc: any, vid: any) => {
+ if (!vid.bitrate) return acc;
+ if (vid.bitrate > acc.bitrate) return vid;
+ else return acc;
+ },
+ {},
+ );
+ return {
+ pics: acc.pics,
+ video: { url: video.url, thumb: video.media_url_https },
+ };
+ }
+ },
+ { pics: [], video: { thumb: "", url: "" } },
+ );
+
+ return {
+ id: tweetData.rest_id,
+ text: legacy.full_text.slice(
+ legacy.display_text_range[0],
+ legacy.display_text_range[1] + 1,
+ ),
+ language: legacy.lang,
+ author: {
+ avatar: userResults.avatar.image_url,
+ name: userResults.core.name,
+ username: userResults.core.screen_name,
+ },
+ createdAt: legacy.created_at,
+ urls:
+ legacy.entities?.urls?.map((url: any) => ({
+ expandedUrl: url.expanded_url,
+ displayUrl: url.display_url,
+ })) || [],
+ media,
+ hashtags: legacy.entities?.hashtags?.map((tag: any) => tag.text) || [],
+ };
+ } catch (error) {
+ console.error("Error extracting bookmark data:", error);
+ return null;
+ }
+ }
+
+ async fetchAllBookmarks(): Promise<TwitterBookmark[]> {
+ const allBookmarks: TwitterBookmark[] = [];
+ let cursor: string | undefined;
+ let hasMore = true;
+
+ while (hasMore) {
+ try {
+ const result = await this.fetchBookmarks(cursor);
+ allBookmarks.push(...result.bookmarks);
+ cursor = result.nextCursor;
+ hasMore = result.hasMore && !!cursor;
+
+ // Rate limiting - be nice to Twitter's API
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ } catch (error) {
+ console.error("Error fetching bookmarks batch:", error);
+ break;
+ }
+ }
+
+ return allBookmarks;
+ }
+ async removeBookmark(tweet_id: string) {
+ const queryId = `Wlmlj2-xzyS1GN3a6cj-mQ`;
+ const url = `https://x.com/i/api/graphql/${queryId}/DeleteBookmark`;
+ const body = JSON.stringify({ variables: { tweet_id }, queryId });
+ const nheaders = { "Content-type": "application/json" };
+ const headers = {
+ ...TwitterApiService.HEADERS,
+ Authorization: TWITTER_INTERNAL_API_KEY,
+ Cookie: this.cookie,
+ ...nheaders,
+ };
+ const opts = {
+ method: "POST",
+ headers,
+ body,
+ };
+ const res = await fetch(url, opts);
+ console.log("bookmark deleted", res);
+ }
+}
diff --git a/app/src/pages.gen.ts b/app/src/pages.gen.ts
new file mode 100644
index 0000000..7953e50
--- /dev/null
+++ b/app/src/pages.gen.ts
@@ -0,0 +1,27 @@
+// deno-fmt-ignore-file
+// biome-ignore format: generated types do not need formatting
+// prettier-ignore
+import type { PathsForPages, GetConfigResponse } from 'waku/router';
+
+// prettier-ignore
+import type { getConfig as File_Categorize_getConfig } from './pages/categorize';
+// prettier-ignore
+import type { getConfig as File_About_getConfig } from './pages/about';
+// prettier-ignore
+import type { getConfig as File_Index_getConfig } from './pages/index';
+
+// prettier-ignore
+type Page =
+| ({ path: '/categorize' } & GetConfigResponse<typeof File_Categorize_getConfig>)
+| ({ path: '/about' } & GetConfigResponse<typeof File_About_getConfig>)
+| ({ path: '/' } & GetConfigResponse<typeof File_Index_getConfig>);
+
+// prettier-ignore
+declare module 'waku/router' {
+ interface RouteConfig {
+ paths: PathsForPages<Page>;
+ }
+ interface CreatePagesConfig {
+ pages: Page;
+ }
+}
diff --git a/app/src/pages/_layout.tsx b/app/src/pages/_layout.tsx
new file mode 100644
index 0000000..6d227c9
--- /dev/null
+++ b/app/src/pages/_layout.tsx
@@ -0,0 +1,39 @@
+import '../styles.css';
+
+import type { ReactNode } from 'react';
+
+import { Header } from '../components/header';
+import { Footer } from '../components/footer';
+
+type RootLayoutProps = { children: ReactNode };
+
+export default async function RootLayout({ children }: RootLayoutProps) {
+ const data = await getData();
+
+ return (
+ <div className="font-['Nunito']">
+ <meta name="description" content={data.description} />
+ <link rel="icon" type="image/png" href={data.icon} />
+ <Header />
+ <main className="m-6 flex items-center *:min-h-64 *:min-w-64 lg:m-0 lg:min-h-svh lg:justify-center">
+ {children}
+ </main>
+ <Footer />
+ </div>
+ );
+}
+
+const getData = async () => {
+ const data = {
+ description: 'An internet website!',
+ icon: '/images/favicon.png',
+ };
+
+ return data;
+};
+
+export const getConfig = async () => {
+ return {
+ render: 'static',
+ } as const;
+};
diff --git a/app/src/pages/about.tsx b/app/src/pages/about.tsx
new file mode 100644
index 0000000..c641af0
--- /dev/null
+++ b/app/src/pages/about.tsx
@@ -0,0 +1,35 @@
+import { Link } from "waku";
+import { listObsidian } from "../lib/categorization";
+
+export default async function AboutPage() {
+ const data = await getData();
+
+ return (
+ <div>
+ <title>{data.title}</title>
+ <h1 className="text-4xl font-bold tracking-tight">{data.headline}</h1>
+ <p>{data.body}</p>
+ <Link to="/" className="mt-4 inline-block underline">
+ Return home
+ </Link>
+ </div>
+ );
+}
+
+const getData = async () => {
+ const obsidian = await listObsidian();
+
+ const data = {
+ title: "About",
+ headline: "About Waku",
+ body: "The minimal React framework",
+ };
+
+ return data;
+};
+
+export const getConfig = async () => {
+ return {
+ render: "static",
+ } as const;
+};
diff --git a/app/src/pages/categorize.tsx b/app/src/pages/categorize.tsx
new file mode 100644
index 0000000..d91d8c1
--- /dev/null
+++ b/app/src/pages/categorize.tsx
@@ -0,0 +1,171 @@
+import * as Bun from "bun";
+import { Suspense } from "react";
+import { TwitterApiService, TwitterBookmark } from "../lib/twitter-api";
+import {
+ userCategories,
+ type CategorizationResponse,
+} from "../lib/categorization";
+import { LLMService } from "../lib/llm-service";
+import ProcessedBookmark from "../components/cat/Entry";
+
+interface CategorizePageProps {
+ bookmarks: Awaited<ReturnType<TwitterApiService["fetchAllBookmarks"]>>;
+ currentBookmarkIndex: number;
+ categorization?: CategorizationResponse;
+ error?: string;
+}
+
+async function CategorizationFetcher({
+ bookmarks,
+ currentIndex,
+}: {
+ bookmarks: TwitterBookmark[];
+ currentIndex: number;
+}) {
+ if (currentIndex >= bookmarks.length) {
+ return (
+ <div className="text-center py-12">
+ <h2 className="text-2xl font-bold text-gray-900 mb-4">
+ All Bookmarks Categorized!
+ </h2>
+ <p className="text-gray-600 mb-6">
+ You've successfully categorized all your bookmarks.
+ </p>
+ <a
+ href="/"
+ className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
+ >
+ Back to Bookmarks
+ </a>
+ </div>
+ );
+ }
+
+ const bookmark = bookmarks[currentIndex];
+ const llmRes = await callLLM(bookmark!);
+ if ("error" in llmRes)
+ return (
+ <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
+ Error categorizing bookmark: {llmRes.error}
+ </div>
+ );
+ return (
+ <div className="">
+ <ProcessedBookmark
+ bookmark={bookmark!}
+ categorization={llmRes.ok}
+ currentIndex={currentIndex}
+ totalCount={bookmarks.length}
+ />
+ </div>
+ );
+}
+
+function LoadingSpinner() {
+ return (
+ <div className="flex items-center justify-center py-12">
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
+ <span className="ml-3 text-gray-600">
+ Loading your Twitter bookmarks...
+ </span>
+ </div>
+ );
+}
+
+export default async function CategorizePage(props: any) {
+ // console.log({ props });
+ const params = new URLSearchParams(props.query);
+ const currentIndex = Number(params.get("idx") || "0");
+ const cookie = Bun.env.TWATTER_COKI;
+
+ if (!cookie) {
+ return (
+ <div className="text-red-600 text-center py-12">
+ Missing Twitter cookie configuration
+ </div>
+ );
+ }
+ const twbookmarks = await getTwData(cookie);
+ if ("error" in twbookmarks) {
+ return (
+ <div className="text-red-600 text-center py-12">
+ Error fetching Twatter bookmarks
+ </div>
+ );
+ }
+ // const currentIndex = parseInt(searchParams.index || '0', 10);
+ const totalCount = twbookmarks.ok.length;
+
+ return (
+ <div className="min-h-screen bg-gray-50 py-8">
+ <div className="max-w-4xl mx-auto px-4">
+ <title>Categorize Bookmarks - SORMARK</title>
+
+ <div className="mb-8">
+ <h1 className="text-4xl font-bold tracking-tight mb-4">
+ Categorize Bookmarks
+ </h1>
+ <p className="text-lg text-gray-600">
+ Review and categorize your Twitter bookmarks one by one
+ </p>
+ </div>
+
+ <Suspense fallback={<LoadingSpinner />}>
+ <>
+ <div className="bg-blue-600 text-white p-4">
+ <p className="text-blue-100 mt-1">
+ Bookmark {currentIndex + 1} of {totalCount}
+ </p>
+ <div className="w-full bg-blue-800 rounded-full h-2 mt-2">
+ <div
+ className="bg-white h-2 rounded-full transition-all duration-300"
+ style={{
+ width: `${((currentIndex + 1) / totalCount) * 100}%`,
+ }}
+ ></div>
+ </div>
+ </div>
+ <CategorizationFetcher
+ bookmarks={twbookmarks.ok}
+ currentIndex={currentIndex}
+ />
+ </>
+ </Suspense>
+ </div>
+ </div>
+ );
+}
+
+import bmarks from "../lib/testData.json";
+async function getTwData(cookie: string) {
+ try {
+ // const twitterService = new TwitterApiService(cookie);
+ // const bookmarks = await twitterService.fetchAllBookmarks();
+ // return { ok: bookmarks };
+ return { ok: bmarks } as any;
+ } catch (error) {
+ return { error: `${error}` };
+ }
+}
+
+async function callLLM(bookmark: TwitterBookmark) {
+ const apiKey = Bun.env.GEMINI_API_KEY!;
+ try {
+ const llmService = new LLMService(apiKey);
+
+ const categorization = await llmService.categorizeBookmark({
+ bookmark,
+ userCategories,
+ });
+
+ return { ok: categorization };
+ } catch (error) {
+ return { error: `${error}` };
+ }
+}
+
+export const getConfig = async () => {
+ return {
+ render: "dynamic",
+ } as const;
+};
diff --git a/app/src/pages/index.tsx b/app/src/pages/index.tsx
new file mode 100644
index 0000000..99202e5
--- /dev/null
+++ b/app/src/pages/index.tsx
@@ -0,0 +1,72 @@
+import * as Bun from "bun";
+import { Suspense } from "react";
+import { TwitterApiService } from "../lib/twitter-api";
+import { BookmarkList } from "../components/bookmark-list";
+
+async function BookmarkFetcher() {
+ const cookie = Bun.env.TWATTER_COKI;
+
+ if (!cookie) {
+ return (
+ <div className="text-red-600">Missing Twitter cookie configuration</div>
+ );
+ }
+
+ try {
+ const twitterService = new TwitterApiService(cookie);
+ const bookmarks = await twitterService.fetchAllBookmarks();
+ const file = Bun.file("testData.json");
+ await file.write(JSON.stringify(bookmarks));
+
+ return (
+ <div className="space-y-6">
+ <div className="bg-white border border-gray-200 rounded-lg p-6">
+ <h2 className="text-xl font-semibold mb-4">Your Bookmarks</h2>
+ <BookmarkList bookmarks={bookmarks} />
+ </div>
+ </div>
+ );
+ } catch (error) {
+ return (
+ <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
+ Error loading bookmarks:{" "}
+ {error instanceof Error ? error.message : "Unknown error"}
+ </div>
+ );
+ }
+}
+
+function LoadingSpinner() {
+ return (
+ <div className="flex items-center justify-center py-12">
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
+ <span className="ml-3 text-gray-600">
+ Loading your Twitter bookmarks...
+ </span>
+ </div>
+ );
+}
+
+export default async function HomePage() {
+ return (
+ <div className="max-w-4xl mx-auto">
+ <title>SORMARK - Twitter Bookmark Manager</title>
+ <div className="mb-8">
+ <h1 className="text-4xl font-bold tracking-tight mb-4">SORMARK</h1>
+ <p className="text-lg text-gray-600">
+ Your Twitter bookmark manager powered by AI
+ </p>
+ </div>
+
+ <Suspense fallback={<LoadingSpinner />}>
+ <BookmarkFetcher />
+ </Suspense>
+ </div>
+ );
+}
+
+export const getConfig = async () => {
+ return {
+ render: "static",
+ } as const;
+};
diff --git a/app/src/styles.css b/app/src/styles.css
new file mode 100644
index 0000000..f1d3c37
--- /dev/null
+++ b/app/src/styles.css
@@ -0,0 +1,3 @@
+@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,400;0,700;1,400;1,700&display=swap')
+layer(base);
+@import 'tailwindcss';
diff --git a/app/testData.json b/app/testData.json
new file mode 100644
index 0000000..327abe9
--- /dev/null
+++ b/app/testData.json
@@ -0,0 +1 @@
+[{"id":"1944967214496411839","text":"how does this have only 415 views ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1753913053672857600/JSKl-7sD_normal.jpg","name":"varepsilon","username":"var_epsilon"},"createdAt":"Tue Jul 15 03:48:14 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/Gv3pNOLWcAEUVCI.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944720279319498929","text":"Unhappy with current apps on the market, I built my own voice tracking teleprompter app with @expo using @cursor_ai \n\nIt was really difficult, particularly as it required fuzzy matching and ways to handle mispronunciation. I think I have it nailed now though and I now have the ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1899346428448632833/OFfFFjPF_normal.jpg","name":"Gregory John","username":"_gregoryjohn"},"createdAt":"Mon Jul 14 11:27:00 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1872029913852862736","text":"SnapEdit is in the a16z's Top 50 AI Mobile Apps. https://t.co/guT2O9kdsH\n\nhttps://t.co/jKfDyNf45i #AIMobileApps #AIPhotoEditor #a16z #snapedit ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1871368318646415360/0Wqpb9nX_normal.jpg","name":"Oscar Le","username":"oscarle_x"},"createdAt":"Wed Dec 25 21:21:26 +0000 2024","urls":[{"expandedUrl":"https://a16z.com/100-gen-ai-apps/","displayUrl":"a16z.com/100-gen-ai-app…"},{"expandedUrl":"https://apps.apple.com/us/app/id1611282499","displayUrl":"apps.apple.com/us/app/id16112…"}],"media":{"pics":["https://pbs.twimg.com/media/GfrIJGtWwAAoPh7.jpg"],"video":{"thumb":"","url":""}},"hashtags":["AIMobileApps","AIPhotoEditor","a16z","snapedit"]},{"id":"1944575986118627684","text":"Let alone that the MAGA grift machine did campaign on this being the root conspiracy behind why we can't have nice things and that 180 alone deserves a chimpout. McVeigh bombed a bunch of shit for less","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1919365688868659200/vCoE927n_normal.jpg","name":"Spandrell","username":"spandrell4"},"createdAt":"Mon Jul 14 01:53:38 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944795907637297506","text":"when i first learned Machine Learning, our professor ingrained into us how every ML problem starts by splitting data into train, test, and validation\n\nthese days there is just train and test. in many cases there is just train and more train\n\nwhere’d all the validation sets go?","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1920483027303399424/IxcwX1P7_normal.jpg","name":"jxmo","username":"jxmnop"},"createdAt":"Mon Jul 14 16:27:31 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944556815892750689","text":"Here's her raw interview. She loved him. \n\nhttps://t.co/3G44ox77Ep","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1863012867085099008/tH0b4l25_normal.jpg","name":"Joshua","username":"FuzzyManStudios"},"createdAt":"Mon Jul 14 00:37:27 +0000 2025","urls":[{"expandedUrl":"https://www.youtube.com/watch?v=X9g7a_HOptk","displayUrl":"youtube.com/watch?v=X9g7a_…"}],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944660683205095653","text":"This is interesting. One dev is training an AI from scratch on books from 1800s London.\n\nIt's called TimeCapsuleLLM, not a fine-tuned modern model, but one trained entirely on historical data. No modern language or context.\n\nBuilt on nanoGPT by @karpathy. https://t.co/oJyyeqnG3Z","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1726902134770454528/YCYCCwrz_normal.jpg","name":"👋 Jan","username":"jandotai"},"createdAt":"Mon Jul 14 07:30:11 +0000 2025","urls":[{"expandedUrl":"https://github.com/haykgrigo3/TimeCapsuleLLM","displayUrl":"github.com/haykgrigo3/Tim…"}],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944705474059718789","text":"Claude Code pro tip:\n\nUse Kimi K2 as your agent by changing the base URL and setting the API key before running claude.\n\nall prompts and code requests will now go through Kimi, in the CC terminal you're used to! ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1932749344836390914/JUlNRQim_normal.jpg","name":"Ian Nuttall","username":"iannuttall"},"createdAt":"Mon Jul 14 10:28:10 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/Gvz6-GfWQAAEtfy.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944721069400871018","text":"我去,老马终于整了一个好活\n\n你现在可以在 Grok APP 里面跟 3D 虚拟角色实时聊天了\n\n而且聊天背景还能根据你们聊天的内容实时更换\n\nhttps://t.co/BbhxkAmyAd","language":"zh","author":{"avatar":"https://pbs.twimg.com/profile_images/1636981205504786434/xDl77JIw_normal.jpg","name":"歸藏(guizang.ai)","username":"op7418"},"createdAt":"Mon Jul 14 11:30:08 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944723471990235340","text":"Fear and Hunger, Felvidek, Disco Elysium...","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1690074184624287745/aBPeoMVj_normal.jpg","name":"Marko Jukic","username":"mmjukic"},"createdAt":"Mon Jul 14 11:39:41 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944588006448570843","text":"Solving Leetcode hards becomes a joke with multi-shot continuations.\n\nBelow is N-Queens in ~20 lines of simple, readable code. Learning functional programming means you never have to fear a technical screen again. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1857824460159725568/KKGWrwlN_normal.jpg","name":"Adam Hearn","username":"adamhearn_"},"createdAt":"Mon Jul 14 02:41:24 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/GvyQIWBacAAltOr.jpg","https://pbs.twimg.com/media/GvyP-rDW8AAbjje.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944753309761638663","text":"This weekend I played around with Engine. It's an async AI software engineering agent.\n\nI gave it a few tasks, it was easy to use and handled things smoothly without much setup.\n\nThey've built something solid with multiple integrations and support for top models like Claude 4, o3 ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1687401026805256192/uQ1o2wVQ_normal.jpg","name":"AshutoshShrivastava","username":"ai_for_success"},"createdAt":"Mon Jul 14 13:38:15 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944266132221047036","text":"去年12月,大连工业大学学生李欣莳与乌克兰电竞选手Zeus(37岁,已婚已育)发生性关系,事后,Zeus将未打码的私密视频发布至社交平台,并标注中国女性为“Eazy ","language":"zh","author":{"avatar":"https://pbs.twimg.com/profile_images/1527433965824802816/PgXCsVL6_normal.jpg","name":"王局志安","username":"wangzhian8848"},"createdAt":"Sun Jul 13 05:22:23 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/GvtroCIWsAAS6co.jpg","https://pbs.twimg.com/media/GvtroCOW0AAIwkR.jpg","https://pbs.twimg.com/media/GvtroEEWgAAiVSh.jpg","https://pbs.twimg.com/media/GvtroBrbsAMjq61.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944686305650049427","text":"Movie scenes inspired by anime 🎥\n https://t.co/VVwZfreQi2","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1385452682517745667/mfcIUnoX_normal.jpg","name":"cinesthetic.","username":"TheCinesthetic"},"createdAt":"Mon Jul 14 09:12:00 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944580587895038421","text":"Apparently it's not illegal to pay others to sterilize themselves, and \"Project Prevention\" has paid 8,000 crack addicts $300 to get IUDs, vasectomies, and tubal ligations\n\n&gt; The organization has used slogans such as \"Don't let pregnancy get in the way of your crack habit\" ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1632229521683513344/_kSyjhE3_normal.jpg","name":"Arjun Panickssery","username":"panickssery"},"createdAt":"Mon Jul 14 02:11:55 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/GvyJTH9agAAzvfZ.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944412444316561738","text":"Hunch: @claude_code is just as useful for product / design as it is for engineers\n\nSo I'm experimenting with the following claude commands: \n\n/polish - a command which accepts a page route and polishes the UI / components based on common errors, best practices and a pixel-perfect","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1545142518983032832/LiHomIUy_normal.jpg","name":"Trist","username":"trist_adlington"},"createdAt":"Sun Jul 13 15:03:46 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944669636886196519","text":"New Success Story: Secure Internet Services with OCaml and MirageOS 🔒\n\nRobur, a worker-owned collective, builds secure, high-performance, and resource-efficient software solutions!\n\nOCaml's static typing eliminates runtime errors with predictable performance \ud83d","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1707339550131933184/Zc0v3QdU_normal.jpg","name":"OCaml","username":"ocaml_org"},"createdAt":"Mon Jul 14 08:05:46 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944326308210921652","text":"Quick start project for Claude Code on Kimi:","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1279600070145437696/eocLhSLu_normal.jpg","name":"Jeremy Howard","username":"jeremyphoward"},"createdAt":"Sun Jul 13 09:21:30 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944418521301364787","text":"I got these amazing character sprites from itchio by SmallScaleInt.\n\nThe character portrait and some minor elements were made with retrodiffusion.\n\nAnd the map was generated with Midjourney!","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1653760191542996994/gxsaTn-0_normal.png","name":"Danny Limanseta","username":"DannyLimanseta"},"createdAt":"Sun Jul 13 15:27:55 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1895493789353120094","text":"I've built 4 game prototypes with @grok so far. I've always wanted to make games since young but I suck at coding, until... Grok happened.\n\nI think I need to go lie down for a bit. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1653760191542996994/gxsaTn-0_normal.png","name":"Danny Limanseta","username":"DannyLimanseta"},"createdAt":"Fri Feb 28 15:18:30 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944415010836599003","text":"I took Grok 4 for a spin this weekend to build this game prototype. \n\nI used SuperGrok Chat to generate the initial game prototype and then brought it over to Cursor to continue coding with Grok 4 MAX.\n\nGrok 4 in Cursor is like a no-nonsense agent. Doesn't speak much, but ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1653760191542996994/gxsaTn-0_normal.png","name":"Danny Limanseta","username":"DannyLimanseta"},"createdAt":"Sun Jul 13 15:13:58 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944320164889284947","text":"Kimi K2 - On-par with Claude 4, but 80% cheaper!!\n\nI connected Kimi K2 to Claude Code to get a sense of real performance (Kimi Code!)\n\nOverall findings:\n1. Exceptional coding capability\n2. Cost only 20% of Claude 4 (Huge!)\n2. Only downside is API is a bit slow\n\n🧵 Below is some","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1613651966663749632/AuQiWkVc_normal.jpg","name":"Jason Zhou","username":"jasonzhou1993"},"createdAt":"Sun Jul 13 08:57:05 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/Gvt2zqzbsAAQIMD.png","https://pbs.twimg.com/media/Gvt3T01XEAA04nu.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944561737317179734","text":"There is a specific kind of Chinese person that can only be described as a 指黑逼. Their negativity isn’t about practical constructive input but petty malcontent that socially perfect utopian fantasyland doesn’t come at the speed of their self righteous moral preening.","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/620173069487714305/x-wYA0Pd_normal.jpg","name":"Lei Gong","username":"gonglei89"},"createdAt":"Mon Jul 14 00:57:01 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944512201185956286","text":"nice trend over the last year is that folks in AI have finally produced a few libraries with the right abstractions\n\nfinally our code can be both hackable and fast, not just one or the other. this never used to happen\n\nvLLM, sglang, verl..\nthis is the dawn of Good Software in AI","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1920483027303399424/IxcwX1P7_normal.jpg","name":"jxmo","username":"jxmnop"},"createdAt":"Sun Jul 13 21:40:10 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944522045792116831","text":"A free and opensource app that lets you gain an unfair advantage. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1679831765744259073/hoVtsOZ9_normal.jpg","name":"GitHub Projects Community","username":"GithubProjects"},"createdAt":"Sun Jul 13 22:19:17 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/GvxUfShXkAAX_7r.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944444946246726000","text":"The Interactive DeepResearch Reports by Kimi-K2 look pretty sleek ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1831493788679761920/-q9w6dzd_normal.jpg","name":"Lisan al Gaib","username":"scaling01"},"createdAt":"Sun Jul 13 17:12:55 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/GvwOIKOW4AEgBGY.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944325644244107621","text":"run this project directly, avoiding the complicated environment variable setup process. 1 million tokens cost just a little over 2 dollars, which is very cheap.\n\nhttps://t.co/IjfpeTFJ0v","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1636981205504786434/xDl77JIw_normal.jpg","name":"歸藏(guizang.ai)","username":"op7418"},"createdAt":"Sun Jul 13 09:18:52 +0000 2025","urls":[{"expandedUrl":"https://github.com/LLM-Red-Team/kimi-cc","displayUrl":"github.com/LLM-Red-Team/k…"}],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944459901952246152","text":"Kimi is the real deal. Unless it's really Sonnet in a trench coat, this is the best agentic open-source model I've tested - BY A MILE.\n\nHere's a slice* of a 4 HOUR run (~1 second per minute) with not much more than 'keep going' from me every 90 minutes or so.\n\nThe task involved ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1929595014734319616/tlqu7nHF_normal.jpg","name":"Hrishi","username":"hrishioa"},"createdAt":"Sun Jul 13 18:12:21 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1942610414514425940","text":"That said, studies on female political radicalism are much less robust, less interesting, and less useful than studies on female marketing. \n\nYou, an incel: \"lol women bad\"\nZhang, an entrepreneur: \"My labubu supplier factory reached 8 digits MRR\"","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1865201729459097604/gPS9TgXr_normal.jpg","name":"zhil","username":"zhil_arf"},"createdAt":"Tue Jul 08 15:43:09 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944495244973838647","text":"Chapter 4 of NASA’s Systems Engineering Handbook doubles as an *extremely* high-quality prompting guide for AI.\n\nIt’s a “How to work effectively with coding agents” masterclass in disguise.\n\nHandbook below. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1552979440547704832/WX5crG9I_normal.jpg","name":"Mckay Wrigley","username":"mckaywrigley"},"createdAt":"Sun Jul 13 20:32:48 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/Gvw8HZhWoAANZOs.jpg","https://pbs.twimg.com/media/Gvw8HZXXAAAVWsb.jpg","https://pbs.twimg.com/media/Gvw8HY5bIAAWHXL.jpg","https://pbs.twimg.com/media/Gvw8HY5boAAlnYx.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944414987679637998","text":"K2 is not «up there», it's straight up the best writing model now, with near 200 Elo gain over the next non-reasoner. I don't know how much of that is just our unfamiliarity with its new characteristic verbal tics, but EQ-Bench isn't that easy to game. Immaculate taste.","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1652169745037242368/KRPTShbG_normal.jpg","name":"Teortaxes▶️ (DeepSeek 推特🐋铁粉 2023 – ∞)","username":"teortaxesTex"},"createdAt":"Sun Jul 13 15:13:53 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944390781147480398","text":"A flux kontext dev lora to make an image old and damaged, using Replicate's prototype trainer. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1732174612178350080/X1uR3MvQ_normal.jpg","name":"fofr","username":"fofrAI"},"createdAt":"Sun Jul 13 13:37:41 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/Gvvc4kUWIAAf8-I.jpg","https://pbs.twimg.com/media/Gvvc7IhXwAENWgM.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944404781964918865","text":"I'm glad I had headphones on...","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1837802996723597312/Jk4w7gjp_normal.jpg","name":"Zephyr","username":"zephyr_z9"},"createdAt":"Sun Jul 13 14:33:19 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1943757786284011656","text":"5 minutes and 53 seconds of total screen time earned Ned Beatty's the nomination for the Oscar for Best Supporting Actor in \"Network\" (1976), shortest ever. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1793592262586146816/9pQ2VB5T_normal.jpg","name":"VIKARE","username":"vikare06"},"createdAt":"Fri Jul 11 19:42:24 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944011517642658017","text":"Introducing Zenith - the Cursor for hardware. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1943411125506445316/I5HzZiLe_normal.jpg","name":"Harish Ashok","username":"habril27"},"createdAt":"Sat Jul 12 12:30:38 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944254257235898711","text":"Edit your frontend UI live, with AI prompts on real DOM elements. ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1679831765744259073/hoVtsOZ9_normal.jpg","name":"GitHub Projects Community","username":"GithubProjects"},"createdAt":"Sun Jul 13 04:35:12 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944026310050767076","text":"You probably wondered how websites like this were made. \n\nSo here is a quick breakdown: ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1561868205005721601/dKdHIdm7_normal.jpg","name":"Kabarza","username":"kabarza_"},"createdAt":"Sat Jul 12 13:29:25 +0000 2025","urls":[],"media":{"pics":[],"video":{}},"hashtags":[]},{"id":"1944192051068727527","text":"My greatest fear is that this guy was right ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1145820424879689728/ZUas1hp0_normal.jpg","name":"James Kirkpatrick","username":"VDAREJamesK"},"createdAt":"Sun Jul 13 00:28:00 +0000 2025","urls":[],"media":{"pics":["https://pbs.twimg.com/media/GvsoXedXoAAZegq.jpg"],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944337629723463983","text":"Actually, even better.\n\nWrite a program to parse a logic formula and pretty print its truth table.\n\nIt’ll teach you 3 months of CS.","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1734291475309940737/csd2XGDA_normal.jpg","name":"Dmitrii Kovanikov","username":"ChShersh"},"createdAt":"Sun Jul 13 10:06:29 +0000 2025","urls":[],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]},{"id":"1944058501900513388","text":"What a cool behind-the-scenes look at how @Beelinkofficial builds their mini PCs. Didn't know they did so much pre-shipping testing! https://t.co/b3P3cDXQsZ","language":"en","author":{"avatar":"https://pbs.twimg.com/profile_images/1746980162607140864/fG9Fj4K__normal.jpg","name":"DHH","username":"dhh"},"createdAt":"Sat Jul 12 15:37:20 +0000 2025","urls":[{"expandedUrl":"https://youtu.be/ohwI3V207Ts?si=XhQVRoGIcU6vLUX2","displayUrl":"youtu.be/ohwI3V207Ts?si…"}],"media":{"pics":[],"video":{"thumb":"","url":""}},"hashtags":[]}] \ No newline at end of file
diff --git a/app/tsconfig.json b/app/tsconfig.json
new file mode 100644
index 0000000..c85d769
--- /dev/null
+++ b/app/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "target": "esnext",
+ "noEmit": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "downlevelIteration": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "skipLibCheck": true,
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+ "jsx": "react-jsx"
+ }
+}
diff --git a/devenv.nix b/devenv.nix
index 45b3cd7..3a93345 100644
--- a/devenv.nix
+++ b/devenv.nix
@@ -6,18 +6,26 @@
...
}: {
# https://devenv.sh/basics/
- env.GREET = "devenva";
+ env.GREET = "devenvaa";
+ env.OBSIDIAN_API_KEY = "17462e14f3d0f02bb35d8bf1307fbc9b8670adc65442d7c4fad1eac927fb18f9";
+ env.ANTHROPIC_BASE_URL = "https://api.moonshot.ai/anthropic";
+ env.ANTHROPIC_AUTH_TOKEN = "sk-JXxvETDsMRTX7CP69dQZ34PTELQWVHAEk0PSxGDUh3OlnFWx";
+ env.GEMINI_API_KEY = "AIzaSyCIYpwIKkRnRKhhu13YBb1Ib7IHvwbnWn8";
- # https://devenv.sh/packages/
- packages = with pkgs; [git];
+ env.TWATTER_COKI = ''guest_id_marketing=v1:173815032724456303; guest_id_ads=v:173815032724456303; personalization_id="v1_KKOJ4dZoYTZuKLY9eadfIg=="; night_mode=2; kdt=camHTtNmw0pQUK0ZHaYd6jJnJRRYihbuZ0Ii7yN2; auth_token=f10fe04281e0bdf50e9dba703e2cba86adb84847; dnt=1; auth_multi="1710606417324015616:651d779cb99b54e7ce81bf8f8050e597c452b50b"; twid=u=1761426801275097088; ads_prefs="HBESAAA="; guest_id=v:173815032724456303; ct0=8e701469358130a26157f02b7836e3a17585153e07ff333ad6f79409f9f5db9cb544eee4c8c56e7b18670beb159e36ace3206471c8bab809602292d60975006c02331f898b8114dbaca1aa2a1c4b0719; cf_clearance=KzFqh7S1eP8KW3jTDxoqJSA7wHoUFaU8Pd.SnFK7vpI-1752517603-1.2.1.1-IJsXxsVSGoUIhnuYX.jp8Q7BI47TUmy90fjmkj2VV6fjCGREbY92GKSfXUzVTHpnZCIe2B5TYBBqQBFX4eeaCjFKXtXQNwG3HEO.qOxqFgdIvvOB03di0ZEHhhDoPGDZfny62hpWMTFZOBbh9bdr5RRA7I8eUdHHBt5thctu.bRV7._mev_T_6SE4nt_SGxpAQRW5NHzBJLfyoWG6W6SfYNl59LCYnlDIcSfcvGq6.U; lang=en; __cf_bm=_rT5xRA6wTXq8eU3g5GKsmTkRcis.Kio4eMiItRdf1o-1752577372-1.0.1.1-0GbYnb9cjm30LzBGKItZ2zXdUlOiGkKUR3A0hgzCX.t4xnOIdBqhNW4uGuTXmeGtBh7iCIWOKtbI1zwI09AhoTX3QKEHiMjxwLv9N4sE1SI'';
+
+ packages = with pkgs; [
+ git
+ nodePackages.typescript-language-server
+ nodePackages.prettier
+ ];
# https://devenv.sh/languages/
- languages.rust.enable = true;
- # languages.javascript = {
- # enable = true;
- # bun.enable = true;
- # };
+ languages.javascript = {
+ enable = true;
+ bun.enable = true;
+ };
# https://devenv.sh/processes/
# processes.cargo-watch.exec = "cargo-watch";