576 lines
21 KiB
Markdown
576 lines
21 KiB
Markdown
This file provides guidance to Claude Code (claude.ai/code) when working with code in
|
|
this repository.
|
|
|
|
## Project Overview
|
|
|
|
AI agent with Telegram bot interface and SvelteKit web frontend, using Convex (
|
|
self-hosted) as the backend database.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
stealth-ai-relay/
|
|
├── backend/ # Python Telegram bot (aiogram + pydantic-ai)
|
|
├── frontend/ # SvelteKit + Svelte 5 + Tailwind CSS 4 + Convex
|
|
└── caddy/ # Reverse proxy config
|
|
```
|
|
|
|
- **Convex is self-hosted** - all env vars are configured, use `CONVEX_SELF_HOSTED_URL`
|
|
for backend
|
|
- `/api` path is reserved - use `/service` for internal HTTP endpoints in frontend
|
|
- Frontend lives at root, Convex at `/convex`, Convex HTTP at `/convex-http`
|
|
- **No code comments** - do not write comments in code, code should be self-documenting
|
|
|
|
## Commands
|
|
|
|
### Backend (Python)
|
|
|
|
```bash
|
|
cd backend
|
|
|
|
# Type checking
|
|
ty check
|
|
|
|
# Linting and formatting
|
|
ruff format
|
|
ruff check --fix
|
|
```
|
|
|
|
### Frontend (SvelteKit)
|
|
|
|
```bash
|
|
cd frontend
|
|
|
|
bun check # Type check
|
|
bun format # Format with Prettier
|
|
bun lint # Lint with ESLint + Prettier
|
|
```
|
|
|
|
## MCP Tools
|
|
|
|
### Context7
|
|
|
|
Use `mcp__context7__resolve-library-id` then `mcp__context7__query-docs` to fetch
|
|
up-to-date documentation for any library (Convex, aiogram, pydantic-ai, etc.).
|
|
|
|
### Svelte MCP (REQUIRED for Svelte work)
|
|
|
|
1. `mcp__svelte__list-sections` - discover available docs
|
|
2. `mcp__svelte__get-documentation` - fetch relevant sections
|
|
3. `mcp__svelte__svelte-autofixer` - MUST use before sending any Svelte code
|
|
4. `mcp__svelte__playground-link` - generate playground links (ask user first)
|
|
|
|
## Svelte 5 + SvelteKit
|
|
|
|
### Setup Convex in SvelteKit
|
|
|
|
```sveltehtml
|
|
<!-- src/routes/+layout.svelte -->
|
|
<script lang="ts">
|
|
import {PUBLIC_CONVEX_URL} from '$env/static/public';
|
|
import {setupConvex} from 'convex-svelte';
|
|
|
|
const {children} = $props();
|
|
setupConvex(PUBLIC_CONVEX_URL);
|
|
</script>
|
|
|
|
{@render children()}
|
|
```
|
|
|
|
### Using Convex queries/mutations
|
|
|
|
```sveltehtml
|
|
|
|
<script lang="ts">
|
|
import {useQuery, useMutation} from 'convex-svelte';
|
|
import {api} from '$lib/convex/_generated/api.js';
|
|
|
|
const tasks = useQuery(api.tasks.list, {});
|
|
const createTask = useMutation(api.tasks.create);
|
|
</script>
|
|
|
|
{#if tasks.isLoading}
|
|
Loading...
|
|
{:else if tasks.error}
|
|
Error: {tasks.error.toString()}
|
|
{:else}
|
|
{#each tasks.data as task}
|
|
<div>{task.text}</div>
|
|
{/each}
|
|
{/if}
|
|
```
|
|
|
|
## Python Backend
|
|
|
|
Uses aiogram 3.x for Telegram bot, pydantic-settings for config, pydantic-ai for AI
|
|
features.
|
|
|
|
### Logging pattern
|
|
|
|
```python
|
|
from utils.logging import logger
|
|
|
|
logger.info("Test")
|
|
```
|
|
|
|
---
|
|
|
|
# Svelte
|
|
|
|
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte
|
|
5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
|
|
|
## Available MCP Tools:
|
|
|
|
### 1. list-sections
|
|
|
|
Use this FIRST to discover all available documentation sections. Returns a structured
|
|
list with titles, use_cases, and paths.
|
|
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the
|
|
chat to find relevant sections.
|
|
|
|
### 2. get-documentation
|
|
|
|
Retrieves full documentation content for specific sections. Accepts single or multiple
|
|
sections.
|
|
After calling the list-sections tool, you MUST analyze the returned documentation
|
|
sections (especially the use_cases field) and then use the get-documentation tool to
|
|
fetch ALL documentation sections that are relevant for the user's task.
|
|
|
|
### 3. svelte-autofixer
|
|
|
|
Analyzes Svelte code and returns issues and suggestions.
|
|
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep
|
|
calling it until no issues or suggestions are returned.
|
|
|
|
### 4. playground-link
|
|
|
|
Generates a Svelte Playground link with the provided code.
|
|
After completing the code, ask the user if they want a playground link. Only call this
|
|
tool after user confirmation and NEVER if code was written to files in their project.
|
|
|
|
# Convex guidelines
|
|
|
|
## Function guidelines
|
|
|
|
### New function syntax
|
|
|
|
- ALWAYS use the new function syntax for Convex functions. For example:
|
|
|
|
```typescript
|
|
import {query} from "./_generated/server";
|
|
import {v} from "convex/values";
|
|
|
|
export const f = query({
|
|
args: {},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
// Function body
|
|
},
|
|
});
|
|
```
|
|
|
|
### Http endpoint syntax
|
|
|
|
- HTTP endpoints are defined in `convex/http.ts` and require an `httpAction` decorator.
|
|
For example:
|
|
|
|
```typescript
|
|
import {httpRouter} from "convex/server";
|
|
import {httpAction} from "./_generated/server";
|
|
|
|
const http = httpRouter();
|
|
http.route({
|
|
path: "/echo",
|
|
method: "POST",
|
|
handler: httpAction(async (ctx, req) => {
|
|
const body = await req.bytes();
|
|
return new Response(body, {status: 200});
|
|
}),
|
|
});
|
|
```
|
|
|
|
- HTTP endpoints are always registered at the exact path you specify in the `path`
|
|
field. For example, if you specify `/api/someRoute`, the endpoint will be registered
|
|
at `/api/someRoute`.
|
|
|
|
### Validators
|
|
|
|
- Below is an example of an array validator:
|
|
|
|
```typescript
|
|
import {mutation} from "./_generated/server";
|
|
import {v} from "convex/values";
|
|
|
|
export default mutation({
|
|
args: {
|
|
simpleArray: v.array(v.union(v.string(), v.number())),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
//...
|
|
},
|
|
});
|
|
```
|
|
|
|
- Below is an example of a schema with validators that codify a discriminated union
|
|
type:
|
|
|
|
```typescript
|
|
import {defineSchema, defineTable} from "convex/server";
|
|
import {v} from "convex/values";
|
|
|
|
export default defineSchema({
|
|
results: defineTable(
|
|
v.union(
|
|
v.object({
|
|
kind: v.literal("error"),
|
|
errorMessage: v.string(),
|
|
}),
|
|
v.object({
|
|
kind: v.literal("success"),
|
|
value: v.number(),
|
|
}),
|
|
),
|
|
)
|
|
});
|
|
```
|
|
|
|
- Always use the `v.null()` validator when returning a null value. Below is an example
|
|
query that returns a null value:
|
|
|
|
```typescript
|
|
import {query} from "./_generated/server";
|
|
import {v} from "convex/values";
|
|
|
|
export const exampleQuery = query({
|
|
args: {},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
console.log("This query returns a null value");
|
|
return null;
|
|
},
|
|
});
|
|
```
|
|
- Here are the valid Convex types along with their respective validators:
|
|
Convex Type | TS/JS type | Example Usage | Validator for argument validation and schemas | Notes |
|
|
| ----------- | ------------| -----------------------| -----------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
| Id | string | `doc._id` | `v.id(tableName)` | |
|
|
| Null | null | `null` | `v.null()` | JavaScript's `undefined` is not a valid Convex value. Functions the return `undefined` or do not return will return `null` when called from a client. Use `null` instead. |
|
|
| Int64 | bigint | `3n` | `v.int64()` | Int64s only support BigInts between -2^63 and 2^63-1. Convex supports `bigint`s in most modern browsers. |
|
|
| Float64 | number | `3.1` | `v.number()` | Convex supports all IEEE-754 double-precision floating point numbers (such as NaNs). Inf and NaN are JSON serialized as strings. |
|
|
| Boolean | boolean | `true` | `v.boolean()` |
|
|
| String | string | `"abc"` | `v.string()` | Strings are stored as UTF-8 and must be valid Unicode sequences. Strings must be smaller than the 1MB total size limit when encoded as UTF-8. |
|
|
| Bytes | ArrayBuffer | `new ArrayBuffer(8)` | `v.bytes()` | Convex supports first class bytestrings, passed in as `ArrayBuffer`s. Bytestrings must be smaller than the 1MB total size limit for Convex types. |
|
|
| Array | Array | `[1, 3.2, "abc"]` | `v.array(values)` | Arrays can have at most 8192 values. |
|
|
| Object | Object | `{a: "abc"}` | `v.object({property: value})` | Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "_". |
|
|
| Record | Record | `{"a": "1", "b": "2"}` | `v.record(keys, values)` | Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "_". |
|
|
|
|
### Function registration
|
|
|
|
- Use `internalQuery`, `internalMutation`, and `internalAction` to register internal
|
|
functions. These functions are private and aren't part of an app's API. They can only
|
|
be called by other Convex functions. These functions are always imported from
|
|
`./_generated/server`.
|
|
- Use `query`, `mutation`, and `action` to register public functions. These functions
|
|
are part of the public API and are exposed to the public Internet. Do NOT use `query`,
|
|
`mutation`, or `action` to register sensitive internal functions that should be kept
|
|
private.
|
|
- You CANNOT register a function through the `api` or `internal` objects.
|
|
- ALWAYS include argument and return validators for all Convex functions. This includes
|
|
all of `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and
|
|
`internalAction`. If a function doesn't return anything, include `returns: v.null()`
|
|
as its output validator.
|
|
- If the JavaScript implementation of a Convex function doesn't have a return value, it
|
|
implicitly returns `null`.
|
|
|
|
### Function calling
|
|
|
|
- Use `ctx.runQuery` to call a query from a query, mutation, or action.
|
|
- Use `ctx.runMutation` to call a mutation from a mutation or action.
|
|
- Use `ctx.runAction` to call an action from an action.
|
|
- ONLY call an action from another action if you need to cross runtimes (e.g. from V8 to
|
|
Node). Otherwise, pull out the shared code into a helper async function and call that
|
|
directly instead.
|
|
- Try to use as few calls from actions to queries and mutations as possible. Queries and
|
|
mutations are transactions, so splitting logic up into multiple calls introduces the
|
|
risk of race conditions.
|
|
- All of these calls take in a `FunctionReference`. Do NOT try to pass the callee
|
|
function directly into one of these calls.
|
|
- When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in
|
|
the same file, specify a type annotation on the return value to work around TypeScript
|
|
circularity limitations. For example,
|
|
|
|
```
|
|
export const f = query({
|
|
args: { name: v.string() },
|
|
returns: v.string(),
|
|
handler: async (ctx, args) => {
|
|
return "Hello " + args.name;
|
|
},
|
|
});
|
|
|
|
export const g = query({
|
|
args: {},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });
|
|
return null;
|
|
},
|
|
});
|
|
```
|
|
|
|
### Function references
|
|
|
|
- Function references are pointers to registered Convex functions.
|
|
- Use the `api` object defined by the framework in `convex/_generated/api.ts` to call
|
|
public functions registered with `query`, `mutation`, or `action`.
|
|
- Use the `internal` object defined by the framework in `convex/_generated/api.ts` to
|
|
call internal (or private) functions registered with `internalQuery`,
|
|
`internalMutation`, or `internalAction`.
|
|
- Convex uses file-based routing, so a public function defined in `convex/example.ts`
|
|
named `f` has a function reference of `api.example.f`.
|
|
- A private function defined in `convex/example.ts` named `g` has a function reference
|
|
of `internal.example.g`.
|
|
- Functions can also registered within directories nested within the `convex/` folder.
|
|
For example, a public function `h` defined in `convex/messages/access.ts` has a
|
|
function reference of `api.messages.access.h`.
|
|
|
|
### Api design
|
|
|
|
- Convex uses file-based routing, so thoughtfully organize files with public query,
|
|
mutation, or action functions within the `convex/` directory.
|
|
- Use `query`, `mutation`, and `action` to define public functions.
|
|
- Use `internalQuery`, `internalMutation`, and `internalAction` to define private,
|
|
internal functions.
|
|
|
|
### Pagination
|
|
|
|
- Paginated queries are queries that return a list of results in incremental pages.
|
|
- You can define pagination using the following syntax:
|
|
|
|
```ts
|
|
import {v} from "convex/values";
|
|
import {query, mutation} from "./_generated/server";
|
|
import {paginationOptsValidator} from "convex/server";
|
|
|
|
export const listWithExtraArg = query({
|
|
args: {paginationOpts: paginationOptsValidator, author: v.string()},
|
|
handler: async (ctx, args) => {
|
|
return await ctx.db
|
|
.query("messages")
|
|
.filter((q) => q.eq(q.field("author"), args.author))
|
|
.order("desc")
|
|
.paginate(args.paginationOpts);
|
|
},
|
|
});
|
|
```
|
|
|
|
Note: `paginationOpts` is an object with the following properties:
|
|
|
|
- `numItems`: the maximum number of documents to return (the validator is `v.number()`)
|
|
- `cursor`: the cursor to use to fetch the next page of documents (the validator is
|
|
`v.union(v.string(), v.null())`)
|
|
- A query that ends in `.paginate()` returns an object that has the following
|
|
properties:
|
|
- page (contains an array of documents that you fetches)
|
|
- isDone (a boolean that represents whether or not this is the last page of
|
|
documents)
|
|
- continueCursor (a string that represents the cursor to use to fetch the next page
|
|
of documents)
|
|
|
|
## Validator guidelines
|
|
|
|
- `v.bigint()` is deprecated for representing signed 64-bit integers. Use `v.int64()`
|
|
instead.
|
|
- Use `v.record()` for defining a record type. `v.map()` and `v.set()` are not
|
|
supported.
|
|
|
|
## Schema guidelines
|
|
|
|
- Always define your schema in `convex/schema.ts`.
|
|
- Always import the schema definition functions from `convex/server`.
|
|
- System fields are automatically added to all documents and are prefixed with an
|
|
underscore. The two system fields that are automatically added to all documents are
|
|
`_creationTime` which has the validator `v.number()` and `_id` which has the validator
|
|
`v.id(tableName)`.
|
|
- Always include all index fields in the index name. For example, if an index is defined
|
|
as `["field1", "field2"]`, the index name should be "by_field1_and_field2".
|
|
- Index fields must be queried in the same order they are defined. If you want to be
|
|
able to query by "field1" then "field2" and by "field2" then "field1", you must create
|
|
separate indexes.
|
|
|
|
## Typescript guidelines
|
|
|
|
- You can use the helper typescript type `Id` imported from './_generated/dataModel' to
|
|
get the type of the id for a given table. For example if there is a table called '
|
|
users' you can use `Id<'users'>` to get the type of the id for that table.
|
|
- If you need to define a `Record` make sure that you correctly provide the type of the
|
|
key and value in the type. For example a validator
|
|
`v.record(v.id('users'), v.string())` would have the type
|
|
`Record<Id<'users'>, string>`. Below is an example of using `Record` with an `Id` type
|
|
in a query:
|
|
|
|
```ts
|
|
import {query} from "./_generated/server";
|
|
import {Doc, Id} from "./_generated/dataModel";
|
|
|
|
export const exampleQuery = query({
|
|
args: {userIds: v.array(v.id("users"))},
|
|
returns: v.record(v.id("users"), v.string()),
|
|
handler: async (ctx, args) => {
|
|
const idToUsername: Record<Id<"users">, string> = {};
|
|
for (const userId of args.userIds) {
|
|
const user = await ctx.db.get("users", userId);
|
|
if (user) {
|
|
idToUsername[user._id] = user.username;
|
|
}
|
|
}
|
|
|
|
return idToUsername;
|
|
},
|
|
});
|
|
```
|
|
|
|
- Be strict with types, particularly around id's of documents. For example, if a
|
|
function takes in an id for a document in the 'users' table, take in `Id<'users'>`
|
|
rather than `string`.
|
|
- Always use `as const` for string literals in discriminated union types.
|
|
- When using the `Array` type, make sure to always define your arrays as
|
|
`const array: Array<T> = [...];`
|
|
- When using the `Record` type, make sure to always define your records as
|
|
`const record: Record<KeyType, ValueType> = {...};`
|
|
- Always add `@types/node` to your `package.json` when using any Node.js built-in
|
|
modules.
|
|
|
|
## Full text search guidelines
|
|
|
|
- A query for "10 messages in channel '#general' that best match the query 'hello hi' in
|
|
their body" would look like:
|
|
|
|
const messages = await ctx.db
|
|
.query("messages")
|
|
.withSearchIndex("search_body", (q) =>
|
|
q.search("body", "hello hi").eq("channel", "#general"),
|
|
)
|
|
.take(10);
|
|
|
|
## Query guidelines
|
|
|
|
- Do NOT use `filter` in queries. Instead, define an index in the schema and use
|
|
`withIndex` instead.
|
|
- Convex queries do NOT support `.delete()`. Instead, `.collect()` the results, iterate
|
|
over them, and call `ctx.db.delete(row._id)` on each result.
|
|
- Use `.unique()` to get a single document from a query. This method will throw an error
|
|
if there are multiple documents that match the query.
|
|
- When using async iteration, don't use `.collect()` or `.take(n)` on the result of a
|
|
query. Instead, use the `for await (const row of query)` syntax.
|
|
|
|
### Ordering
|
|
|
|
- By default Convex always returns documents in ascending `_creationTime` order.
|
|
- You can use `.order('asc')` or `.order('desc')` to pick whether a query is in
|
|
ascending or descending order. If the order isn't specified, it defaults to ascending.
|
|
- Document queries that use indexes will be ordered based on the columns in the index
|
|
and can avoid slow table scans.
|
|
|
|
## Mutation guidelines
|
|
|
|
- Use `ctx.db.replace` to fully replace an existing document. This method will throw an
|
|
error if the document does not exist. Syntax:
|
|
`await ctx.db.replace('tasks', taskId, { name: 'Buy milk', completed: false })`
|
|
- Use `ctx.db.patch` to shallow merge updates into an existing document. This method
|
|
will throw an error if the document does not exist. Syntax:
|
|
`await ctx.db.patch('tasks', taskId, { completed: true })`
|
|
|
|
## Action guidelines
|
|
|
|
- Always add `"use node";` to the top of files containing actions that use Node.js
|
|
built-in modules.
|
|
- Never use `ctx.db` inside of an action. Actions don't have access to the database.
|
|
- Below is an example of the syntax for an action:
|
|
|
|
```ts
|
|
import {action} from "./_generated/server";
|
|
|
|
export const exampleAction = action({
|
|
args: {},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
console.log("This action does not return anything");
|
|
return null;
|
|
},
|
|
});
|
|
```
|
|
|
|
## Scheduling guidelines
|
|
|
|
### Cron guidelines
|
|
|
|
- Only use the `crons.interval` or `crons.cron` methods to schedule cron jobs. Do NOT
|
|
use the `crons.hourly`, `crons.daily`, or `crons.weekly` helpers.
|
|
- Both cron methods take in a FunctionReference. Do NOT try to pass the function
|
|
directly into one of these methods.
|
|
- Define crons by declaring the top-level `crons` object, calling some methods on it,
|
|
and then exporting it as default. For example,
|
|
|
|
```ts
|
|
import {cronJobs} from "convex/server";
|
|
import {internal} from "./_generated/api";
|
|
import {internalAction} from "./_generated/server";
|
|
|
|
const empty = internalAction({
|
|
args: {},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
console.log("empty");
|
|
},
|
|
});
|
|
|
|
const crons = cronJobs();
|
|
|
|
// Run `internal.crons.empty` every two hours.
|
|
crons.interval("delete inactive users", {hours: 2}, internal.crons.empty, {});
|
|
|
|
export default crons;
|
|
```
|
|
|
|
- You can register Convex functions within `crons.ts` just like any other file.
|
|
- If a cron calls an internal function, always import the `internal` object from '_
|
|
generated/api', even if the internal function is registered in the same file.
|
|
|
|
## File storage guidelines
|
|
|
|
- Convex includes file storage for large files like images, videos, and PDFs.
|
|
- The `ctx.storage.getUrl()` method returns a signed URL for a given file. It returns
|
|
`null` if the file doesn't exist.
|
|
- Do NOT use the deprecated `ctx.storage.getMetadata` call for loading a file's
|
|
metadata.
|
|
|
|
Instead, query the `_storage` system table. For example, you can use `ctx.db.system.get` to get an `Id<"_storage">`.
|
|
|
|
```
|
|
import { query } from "./_generated/server";
|
|
import { Id } from "./_generated/dataModel";
|
|
|
|
type FileMetadata = {
|
|
_id: Id<"_storage">;
|
|
_creationTime: number;
|
|
contentType?: string;
|
|
sha256: string;
|
|
size: number;
|
|
}
|
|
|
|
export const exampleQuery = query({
|
|
args: { fileId: v.id("_storage") },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
const metadata: FileMetadata | null = await ctx.db.system.get("_storage", args.fileId);
|
|
console.log(metadata);
|
|
return null;
|
|
},
|
|
});
|
|
```
|
|
|
|
- Convex storage stores items as `Blob` objects. You must convert all items to/from a
|
|
`Blob` when using Convex storage.
|