{"ok":true,"result":{"project_schema_version":1,"root_files":[{"name":"schema.js","singleton":true}],"directories":[{"name":"lib","nested":true,"synced":true,"runnable":false},{"name":"handlers","nested":false,"synced":true,"runnable":true,"allowed_names":["message","edited_message","stopped_message_generation","channel_post","edited_channel_post","business_connection","business_message","edited_business_message","deleted_business_messages","inline_query","chosen_inline_result","callback_query","guest_message","shipping_query","pre_checkout_query","purchased_paid_media","subscription","message_reaction","message_reaction_count","poll","poll_answer","my_chat_member","chat_member","chat_join_request","chat_boost","removed_chat_boost","managed_bot"]},{"name":"endpoints","nested":false,"synced":true,"runnable":true}],"scaffold":["lib\/","handlers\/","endpoints\/","schema.js","AGENTS.md","docs\/tgcloud-sdk.md","handlers\/message.js"],"templates":{"AGENTS.md":"# AGENTS.md\n\nOrientation for AI coding assistants (and humans) working in this project.\nThis file is auto-loaded by Claude Code, Cursor, and similar tools — keep it short\nand true. For the full SDK reference (db, Bot API, fetch), see\n[docs\/tgcloud-sdk.md](docs\/tgcloud-sdk.md).\n\n## What this project is\n\nA **Telegram Mini App bot** running on Telegram's serverless platform. You write\nJavaScript modules (database schema, shared library code, update handlers); the\nplatform runs them in a V8 isolate. The `tgcloud` CLI syncs this local project\nwith the bot's cloud environment — think `wrangler`\/`vercel` + `drizzle-kit`.\n\nThere is no server to run locally and no `node_modules` to import from at runtime:\nthe only things available inside a module are the platform SDK and other modules\nin this project.\n\n## Layout\n\nEverything the platform runs lives in the **`tgcloud\/`** folder. The rest of the\nproject root is yours (a mini-app front-end, config, docs).\n\n| Path                  | What it is                                                        |\n|-----------------------|-------------------------------------------------------------------|\n| `tgcloud\/schema.js`   | Database schema — tables as **named exports**. One file.          |\n| `tgcloud\/lib\/`        | Shared modules. Subdirectories allowed (`lib\/internal\/util.js`).  |\n| `tgcloud\/handlers\/`   | Update handlers, **one level only**. Names match Telegram Bot API update types (`message`, `callback_query`, …). |\n| `tgcloud\/endpoints\/`  | Functions the Mini App calls (`POST \/api\/<name>`), **one level only**. |\n| `tgcloud.jsonc`       | Project config. Its `\"static\"` block names the build folder hosted as the Mini App. |\n| `docs\/`               | Reference docs (this project's, for you). Not deployed.           |\n| `.tgcloud\/`           | CLI state (credentials, snapshot, cached layout). **Never edit or read from here** — it's gitignored machine state. |\n\nOnly `.js` files under `tgcloud\/` (`schema.js`, `lib\/`, `handlers\/`, `endpoints\/`) are deployed\nas code, plus — when `tgcloud.jsonc` names one — the front-end's build folder as\nthe Mini App's static files. Everything else (Markdown, config, `.tgcloud\/`, the\nfront-end's sources) is local-only. Module names are relative to `tgcloud\/`: the\nfile `tgcloud\/handlers\/message.js` is the module `handlers\/message`.\n\n## Mini App front-end\n\nIf the project root holds a front-end (Vite, React, Vue, …), its **build output**\nis deployed next to the bot and served as the Mini App at\n`https:\/\/app<app_id>.tgcloud.ai\/` — `push` prints the exact URL.\n\n- `tgcloud.jsonc` → `{ \"static\": { \"source\": \"dist\" } }` — `source` names the\n  build folder; the other keys of the `static` object are the serving config\n  (e.g. `\"spa\": true` serves `index.html` for unknown extension-less paths —\n  client-side routing; `immutable`, `headers`, `redirects`). `tgcloud init` writes\n  the file when it recognizes the front-end; after that it's a normal project\n  file. JSON with comments and trailing commas (a project may use a strict-JSON\n  `tgcloud.json` instead — never both). `\"static\": \"dist\"` is accepted as a\n  shorthand for `{ \"source\": \"dist\" }`.\n  `\"static\": false` removes the deployed site on the next push.\n- **Build before you push.** `push` uploads the folder as it is on disk; it never\n  runs the build. `npm run deploy` (build + push) is the safe default.\n- The front-end is ordinary browser code with npm packages — none of the\n  `tgcloud\/` module rules apply to it, and `tgcloud\/` modules can't import it.\n  It talks to the bot's code only through **endpoints** (below).\n- The site is served from the host root, so the bundler's default `base` (`\/`)\n  is right. Don't add `X-Frame-Options` or a restrictive `frame-ancestors`:\n  Telegram Web opens Mini Apps in an iframe.\n\n## Endpoints — the Mini App's server side\n\n`tgcloud\/endpoints\/<name>.js` is called by the Mini App as `POST \/api\/<name>` on\nthe same host as the static site. Use it for anything the front-end can't do\nitself: the database, the Bot API, secrets.\n\n- `export default async function (input, ctx)` — `input` is the JSON body (an\n  object); the return value goes back as JSON.\n- `ctx.initData` is the Mini App's init data **verified by the platform** (same\n  shape as `Telegram.WebApp.initDataUnsafe`): `ctx.initData.user` is the caller.\n  There is no `ctx.user`. Invalid init data → `401`, the endpoint never runs.\n- Refuse with `throw new EndpointError('Out of stock', { code: 'OUT_OF_STOCK' })`\n  (`import { EndpointError } from 'sdk'`) → `400`, the Mini App gets\n  `description` + `parameters`. Any other exception → generic `500`; never\n  rely on its message reaching the client.\n- Front-end call: `Telegram.WebApp.Serverless.call('<name>', input, (err, result) => …)`\n  from the official `telegram-web-app.js` — it sends the init data; `err` has\n  `message` (description), `type` (`'ENDPOINT_ERROR'` for your `EndpointError`)\n  and `parameters`. Don't hand-roll `fetch` + init data.\n- Names: letters, digits, `_`, not starting with a digit (`getProfile`).\n  Scaffold with `npx tgcloud add endpoints\/<name>`; test with\n  `npx tgcloud run endpoints\/<name> '{}' --ctx '{ initData: { user: { id: 1 } } }'`.\n\n## Module system — the rules that bite\n\n- **Import project modules by relative path WITH the `.js` extension; import\n  the SDK by name.** (from `tgcloud\/handlers\/message.js`:)\n  - ✅ `import { users } from '..\/schema.js'`\n  - ✅ `import { addItem } from '..\/lib\/cart.js'`\n  - ✅ `import { db, api, fetch } from 'sdk'` \/ `import { eq, sql } from 'sdk\/db'`\n  - ❌ `import { users } from '..\/schema'` → a relative import must end in `.js`\n  - ❌ `import x from '..\/..\/src\/x.js'` → nothing outside `tgcloud\/` is reachable\n  - A module's *name* is its path inside `tgcloud\/` without `.js` (`lib\/cart`);\n    `tgcloud run` takes it, and `from 'lib\/cart'` (no extension) also resolves —\n    but write relative imports.\n- **No filesystem, no npm packages** at runtime. Only `sdk` (and its submodules\n  like `sdk\/db`) and your own project modules exist.\n- A handler module's `export default` is what the platform invokes, with the\n  update's **payload** as the first argument — for `handlers\/message` that's the\n  `Message` (i.e. `update.message`), for `handlers\/callback_query` the\n  `CallbackQuery`, and so on. The full `Update` (with `update_id`) is on the\n  second argument: `ctx.update`. An endpoint's `export default` gets\n  `(input, ctx)` — see *Endpoints* above.\n\n## Platform SDK (`import … from 'sdk'`)\n\n- **`db`** — the database (query builder + schema DSL). Full API: [docs\/tgcloud-sdk.md](docs\/tgcloud-sdk.md).\n- **`api`** — the Telegram Bot API. `api.<method>({...})` (e.g. `api.sendMessage`,\n  `api.getMe`) returns the **unwrapped** result and **throws `BotApiError`** on\n  failure (`import { BotApiError } from 'sdk'`; it has `.code`\/`.description`\/`.parameters`).\n- **`fetch`** — outbound HTTP, web-`fetch`-like (`res.status\/ok`, `res.json()`,\n  `res.text()`, streaming via `for await`, redirects followed).\n- **`EndpointError`** — the one way an endpoint refuses a call (see *Endpoints*).\n\n## Database — the rules that bite\n\nFull API in [docs\/tgcloud-sdk.md](docs\/tgcloud-sdk.md). The non-obvious parts:\n\n- **Every DB call is async — always `await`.** `.all()`, `.get()`, `.values()`,\n  `.run()`, `db.$count()` and the raw `db.run\/all\/get` all return Promises.\n- **No foreign keys.** `.references()` and `foreignKey()` **throw at declaration**\n  — the runtime runs with FKs off, so they'd be silently inert. Enforce integrity\n  in application code (delete children before parents, etc.).\n- **Drops happen only via `.deprecated('reason')`** on a column\/table\/index.\n  Deleting the declaration does *not* drop anything.\n- **Type changes aren't automatic** — do them by hand with `db.run(...)`.\n\n## Deploy & migrate workflow\n\n**Deploying never touches the database.** Schema sync is a separate, explicit step.\n\nThe CLI is a local dev-dependency, so run it with `npx tgcloud <command>` (or use\nthe `npm run` scripts in package.json — e.g. `npm run deploy`):\n\n```\nnpx tgcloud status     # what changed locally vs the cloud\nnpx tgcloud push       # deploy modules (and the static build) to the cloud\nnpx tgcloud migrate    # apply tgcloud\/schema.js changes to the database (interactive)\nnpx tgcloud run <module> [args]   # execute a handler or endpoint server-side\nnpx tgcloud pull       # bring the local project in line with the cloud\nnpx tgcloud login      # link this project to a bot\nnpx tgcloud webhook    # show the bot's webhook and whether it matches your handlers\n```\n\nAfter you change `tgcloud\/schema.js`, `push` reports what the DB *would* change but applies\nnothing — run `npx tgcloud migrate` to actually apply it.\n\nThe platform manages the bot's webhook for you, derived from your deployed\n`handlers\/*`, and refreshes it on `push`. If it ever drifts — e.g. someone called\n`setWebhook` with the raw bot token — `npx tgcloud webhook` shows the mismatch and\n`npx tgcloud webhook sync` repairs it.\n","docs\/tgcloud-sdk.md":"# tgcloud SDK reference\n\nEverything your bot's modules can import from `sdk`: the database (`db`), the\nTelegram Bot API (`api`), HTTP (`fetch`), and `console` logging. The database is\nthe largest surface, so it comes first; `api`, `fetch`, and `console` are at the\nend.\n\n> Orientation and project rules live in [..\/AGENTS.md](..\/AGENTS.md). This file is\n> the API reference.\n\n## `sdk` at a glance\n\n```js\nimport { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk'\n\/\/ or from submodules:\nimport { table, integer, text, eq, sql } from 'sdk\/db'\nimport { api } from 'sdk\/api'\nimport { fetch } from 'sdk\/fetch'\n```\n\n- **`db`** — SQLite query builder + schema DSL. → most of this file.\n- **`api`** — Telegram Bot API (`api.sendMessage(...)`). → *Telegram Bot API* below.\n- **`fetch`** — outbound HTTP. → *HTTP* below.\n- **`InputFile`** — a file (bytes + name) to upload, for `api` or `fetch`. → *Files* below.\n- **`EndpointError`** — refuse a Mini App's call from an `endpoints\/` module. → *Endpoints* below.\n- **`console`** — logging that surfaces in `npx tgcloud run` output. → *Logging* below.\n\n## Rules that bite\n\nThe runtime is not stock Node — these defaults don't hold. Detail is in the\nsections below; project rules are in [..\/AGENTS.md](..\/AGENTS.md).\n\n- **Import project modules by relative path with `.js`** — `from '..\/schema.js'`, `from '..\/lib\/cart.js'` (a relative import without `.js` fails). The SDK is imported by name: `from 'sdk'`, `from 'sdk\/db'`.\n- **Every DB call is async — always `await`.** `.all()`\/`.get()`\/`.values()`\/`.run()`, `db.$count()`, and raw `db.run\/all\/get` all return Promises. A forgotten `await` returns the builder, not rows.\n- **No foreign keys.** `.references()` and table-level `foreignKey()` **throw when declared** — a schema using them won't deploy. Enforce integrity in app code.\n- **Drops happen only via `.deprecated('reason')`** on a column\/table\/index. Deleting the declaration does *not* drop it; type changes are manual (`db.run(...)`).\n- **A handler's `export default (input, ctx)`** gets the update's *payload* as `input` — `handlers\/message` receives the `Message` (i.e. `update.message`), `handlers\/callback_query` the `CallbackQuery`. The full `Update` (with `update_id`) is `ctx.update`.\n- **An endpoint's `export default (input, ctx)`** gets the Mini App's JSON body as `input` and the **verified** init data as `ctx.initData` (`ctx.initData.user` is the caller). There is no `ctx.user`. → *Endpoints* below.\n\n# Database (`db`)\n\n## Importing\n\n`db` (the default export of `sdk\/db`) holds the whole API; the same functions are\nalso named exports.\n\n```js\nimport { table, integer, text, eq, sql } from 'sdk\/db'   \/\/ named imports\nimport db, { sql } from 'sdk\/db'                          \/\/ or namespace + names\n```\n\nImport your own modules by relative path with the `.js` extension — from a\nhandler, `from '..\/schema.js'`, `from '..\/lib\/cart.js'`.\n\n## Defining the schema (`tgcloud\/schema.js`)\n\nTables are **named exports**. `table()` builds a descriptor at runtime (no DB call);\nthe platform discovers the exported tables and runs migrations when you deploy\n`schema.js` (the file `tgcloud\/schema.js`; module name `schema`).\n\n```js\nimport { table, integer, text, boolean, json, index, check, sql } from 'sdk\/db'\n\nexport const users = table('users', {\n  id:      integer('id').primaryKey({ autoIncrement: true }),\n  tgId:    integer('tg_id').unique(),\n  name:    text('name').notNull(),\n  lang:    text('lang').default('en'),\n  isAdmin: boolean('is_admin').default(false),\n  prefs:   json('prefs'),\n  created: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),\n}, (t) => ({\n  createdIdx: index('idx_users_created').on(t.created),\n}))\n\nexport const todos = table('todos', {\n  id:     integer('id').primaryKey({ autoIncrement: true }),\n  userId: integer('user_id').notNull(),   \/\/ logical link to users.id — FKs are NOT enforced\n  text:   text('text').notNull(),\n  done:     boolean('done').default(false),\n  priority: integer('priority').default(0),\n}, (t) => ({\n  userDoneIdx:   index('idx_todos_user_done').on(t.userId, t.done),\n  priorityCheck: check('priority_check', sql`${t.priority} >= 0`),\n}))\n```\n\nThe third argument to `table()` is a callback `(t) => ({...})` where `t` exposes the\ncolumns (`t.userId` → a column ref); declare indexes and table-level constraints there.\n\n## Column types\n\n| factory     | SQLite | notes                                          |\n|-------------|--------|------------------------------------------------|\n| `text()`    | TEXT   |                                                |\n| `integer()` | INTEGER|                                                |\n| `real()`    | REAL   | alias `float()`                                |\n| `numeric()` | NUMERIC|                                                |\n| `blob()`    | BLOB   | takes\/returns `Uint8Array`                     |\n| `boolean()` | INTEGER| stored 0\/1, read as `true`\/`false`             |\n| `json()`    | TEXT   | auto `JSON.stringify` \/ `JSON.parse`           |\n\nSignature: `integer(name?, opts?)`. The name is optional — if omitted it's taken\nfrom the JS key. `opts.mode` sets runtime conversion:\n\n| mode           | stored as          | JS value              |\n|----------------|--------------------|-----------------------|\n| `boolean`      | INTEGER 0\/1        | `boolean`             |\n| `json`         | TEXT (JSON)        | any object\/array      |\n| `timestamp`    | INTEGER (unix sec) | `Date`                |\n| `timestamp_ms` | INTEGER (unix ms)  | `Date`                |\n| `bytes`        | BLOB               | `Uint8Array`          |\n\n`boolean()` and `json()` are sugar over `integer(name, { mode: 'boolean' })` and\n`text(name, { mode: 'json' })`. A `blob()` column takes and returns a `Uint8Array`\nautomatically — that's handled at the wire level, so it needs no mode.\n\n## Column modifiers (chainable)\n\n```js\ninteger('id').primaryKey()\ninteger('id').primaryKey({ autoIncrement: true })\ntext('name').notNull()\ntext('tg').unique()\ntext('lang').default('en')\ninteger('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`)\ntext('slug').generatedAlwaysAs(sql`lower(name)`, { mode: 'stored' })  \/\/ 'virtual' | 'stored'\ntext('name').constraint('COLLATE NOCASE')       \/\/ arbitrary column-level DDL\ntext('email').deprecated('replaced by login')   \/\/ marks for drop; invisible at runtime\n```\n\n- `.default()` on a `json()` column encodes the value automatically; a `` sql`...` ``\n  default is passed through verbatim.\n- `.deprecated()` is terminal — don't chain methods after it.\n\n## No foreign keys\n\nThe runtime runs with `PRAGMA foreign_keys` **off**, so a declared FK would be\nsilently inert (no cascades, no orphan protection). To make that impossible,\n`.references()` and table-level `foreignKey()` **throw when declared** — a schema\nusing them won't deploy. `REFERENCES`\/`FOREIGN KEY` smuggled in via `.constraint()`\nor a raw `db.run('CREATE TABLE …')` stay inert too. Enforce integrity in app code:\ndelete children before parents, insert parents before children, sweep orphans with\n`LEFT JOIN … WHERE parent.id IS NULL`.\n\n## Table-level constraints & indexes (in the callback)\n\n```js\ntable('t', { ... }, (t) => ({\n  pk:    primaryKey({ columns: [t.a, t.b] }),\n  uq:    unique('uq_email').on(t.email),\n  chk:   check('chk_done', sql`${t.done} in (0, 1)`),\n  idx:   index('idx_name').on(t.col),\n  uidx:  uniqueIndex('uidx_email').on(t.email),\n  lower: index('idx_lower').on(sql`lower(${t.email})`),          \/\/ expression index\n  active:index('idx_active').on(t.userId).where(sql`done = 0`),  \/\/ partial index\n  \/\/ foreignKey(...) is NOT supported — it throws (see \"No foreign keys\")\n}))\n```\n\nTable modifiers (chained after `table(...)`): `.strict()`, `.withoutRowid()`,\n`.constraint('CHECK (x > 0)', 'chk_x')`, `.deprecated('reason')`.\n\n## Query builder\n\n### select\n\n`select(projection?)` → `.from(table)` → builder. No projection = `SELECT *`.\n\n```js\nawait db.select().from(todos).all()                          \/\/ all rows\nawait db.select().from(todos).where(eq(todos.id, 1)).get()   \/\/ first row or null\nawait db.$count(todos)                                        \/\/ COUNT(*)\n\nawait db.select().from(todos)\n  .where(and(eq(todos.userId, uid), eq(todos.done, false)))\n  .orderBy(desc(todos.priority), asc(todos.id))\n  .limit(10).offset(20)\n  .all()\n\n\/\/ projection: { alias: colRef | sqlExpr }\nawait db.select({ id: todos.id, title: todos.text, n: sql`count(*)` })\n  .from(todos).groupBy(todos.userId).having(sql`count(*) > ${1}`).all()\n```\n\nChain: `.where()` `.orderBy()` `.limit()` `.offset()` `.groupBy()` `.having()`\n`.distinct()`; terminals `.all()` \/ `.get()` \/ `.values()`.\n\nThe builder is **awaitable** — `await db.select().from(todos)` (with `.where(…)`\netc. as needed) runs `.all()` and resolves to the row array, so `.all()` is\noptional. Use `.get()`\/`.values()` for the other shapes; count rows with\n`db.$count(table, where?)`.\n\n### insert \/ update \/ delete\n\n```js\nawait db.insert(todos).values({ userId: 1, text: 'Buy milk' }).run()\nawait db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run()   \/\/ batch\nawait db.insert(todos).values({ text: 'X' }).returning().run()        \/\/ RETURNING *\n\nawait db.insert(users).values({ tgId: 42, name: 'Ann' })\n  .onConflictDoNothing({ target: users.tgId }).run()\nawait db.insert(users).values({ tgId: 42, name: 'Ann' })\n  .onConflictDoUpdate({ target: users.tgId, set: { name: 'Ann' } }).run()\n\nawait db.update(todos).set({ done: true }).where(eq(todos.id, 1)).run()   \/\/ .set() required\nawait db.delete(todos).where(eq(todos.id, 1)).run()\n```\n\nA plain insert\/update\/delete resolves to a **run result**:\n\n```js\nawait db.insert(todos).values({ text: 'buy milk' })\n\/\/ → { rowsAffected: 1, lastInsertRowid: 42, rows: [] }\n```\n\nAdd `.returning()` (→ `RETURNING *`) or `.returning({ id: todos.id })` to get the\naffected rows back instead (converted, since they're bound to the table) — with\n`.returning()` the result **is** the row array, not the run result:\n\n```js\nawait db.insert(todos).values({ text: 'buy milk' }).returning({ id: todos.id })\n\/\/ → [{ id: 42 }]\n```\n\nNothing is lost by that swap: `RETURNING` emits one row per affected row, so\n`rows.length` *is* the affected count, and the ids come back directly rather than\nas a single `lastInsertRowid`. Like select, these builders are **awaitable**:\n`await db.insert(todos).values({ … })` runs without an explicit `.run()`.\n\n### Raw SQL — `db.run` \/ `db.all` \/ `db.get` \/ `db.values`\n\nMode is by method, not auto-detected: `run` = write\/exec, `all` = all rows,\n`get` = first row (or `null`), `values` = rows as positional arrays. Each takes a\n`` sql`…` `` object or a `(queryString, params)` pair.\n\n```js\nawait db.run('UPDATE todos SET done = 1 WHERE id = :id', { ':id': 5 })\n\/\/ → { rowsAffected: 3, lastInsertRowid: 0, rows: [] }\n\nawait db.run(sql`INSERT INTO todos (text) VALUES (${'buy milk'}) RETURNING id`)\n\/\/ → { rowsAffected: 1, lastInsertRowid: 42, rows: [{ id: 42 }] }\n\nawait db.all(sql`SELECT * FROM todos WHERE done = ${false}`)   \/\/ → [{ id: 1, … }]\nawait db.get(sql`SELECT count(*) AS c FROM todos`)             \/\/ → { c: 7 } | null\nawait db.values(sql`SELECT id, text FROM todos`)               \/\/ → [[1, 'buy milk']]\n```\n\nUse `db.values` when a query projects two columns of the **same name** — a join\nlike `SELECT u.id, o.id` — because `db.all` keys rows by column name and the\nsecond `id` overwrites the first. Positional rows have no such collision.\n\n> Raw methods are **not bound to a table**, so rows come back without mode\n> conversion (boolean\/json\/timestamp arrive as 0\/1, a JSON string, unix seconds).\n> Only the table-bound builder converts.\n\n## `sql` tagged template\n\n`` sql`...` `` interpolates values as named parameters, column refs as identifiers,\nand splices nested `sql`.\n\n```js\nsql`count = ${n}`                 \/\/ count = :p1\nsql`${todos.priority} > ${min}`   \/\/ priority > :p2  (column as identifier)\nsql`WHERE ${cond}`                \/\/ nested sql spliced in\nsql.raw('datetime(\"now\")')        \/\/ literal, no parameters\n```\n\nIn DDL contexts (DEFAULT \/ CHECK \/ GENERATED) parameters don't work — use literal SQL.\n\n## Operators\n\nImport from `sdk\/db`:\n\n```js\nimport {\n  eq, ne, gt, gte, lt, lte,\n  like, notLike,\n  isNull, isNotNull, and, or, not,\n  between, notBetween, inArray, notInArray,\n  count, sum, avg, min, max,\n  asc, desc,\n} from 'sdk\/db'\n```\n\n`.where(e1, e2)` with multiple args is equivalent to `and(e1, e2)`.\n\n## Migrations (summary)\n\n- **Adding** columns\/tables\/indexes — applied automatically on `npx tgcloud migrate`\n  after you deploy `schema.js`.\n- **Dropping** — only via `.deprecated()`; the CLI shows what will be removed.\n- **Changing a column's type** — not automatic; do it manually with `db.run`.\n\n`npx tgcloud push` reports pending DB changes but never applies them. Run\n`npx tgcloud migrate` to apply.\n\n# Telegram Bot API (`api`)\n\n`import { api, BotApiError } from 'sdk'`. Call any Bot API method as\n`api.<method>(params)` — a Proxy dispatches the name, so every current (and\nfuture) method works with no SDK update.\n\n```js\nconst me = await api.getMe()                          \/\/ → the unwrapped `result`\nawait api.sendMessage({ chat_id: id, text: 'Hello!' })\nawait api.editMessageText({ chat_id, message_id, text: 'Updated' })\nawait api.answerCallbackQuery({ callback_query_id, text: 'Done' })\n```\n\n- **The envelope is unwrapped.** On `{ ok: true }` the call resolves to `result`\n  directly — no `.ok`\/`.result` wrapper. Params are one object using the Bot API's\n  snake_case names (`chat_id`, `message_id`, …).\n- **Failures throw `BotApiError`.** On `{ ok: false }` it throws; the error carries\n  `.code` (Bot API `error_code` — 400\/403\/429\/…), `.description`, `.method`, and\n  `.parameters` (e.g. `retry_after` on 429, `migrate_to_chat_id`). Catch and inspect\n  `.code` to handle an expected failure:\n\n```js\ntry {\n  await api.deleteMessage({ chat_id, message_id });\n} catch (e) {\n  if (e.code !== 400) throw e;   \/\/ 400 = already gone; anything else is a real error\n}\n```\n\n## Files (upload \/ download)\n\nMove file **bytes**, not just `file_id`s.\n\n- **Upload — `InputFile`.** `import { InputFile } from 'sdk'`. `new InputFile(bytes, filename, { type })` (bytes = `Uint8Array`\/`ArrayBuffer`; `type` = optional MIME). Pass it as any file param, at **any depth** — the SDK rewrites it to a real multipart upload:\n\n```js\nawait api.sendDocument({ chat_id, document: new InputFile(bytes, 'a.pdf', { type: 'application\/pdf' }) })\nawait api.sendMediaGroup({ chat_id, media: [\n  { type: 'photo', media: new InputFile(a, 'a.jpg', { type: 'image\/jpeg' }) },\n  { type: 'photo', media: new InputFile(b, 'b.jpg', { type: 'image\/jpeg' }) },\n]})\n```\n  Bot API upload limits apply: currently **50 MB**\/file (**10 MB** for a photo).\n\n- **Download — by `file_id` only** (a raw `file_path` is rejected):\n\n```js\nconst bytes = await api.getFileContent(file_id)          \/\/ → Uint8Array (whole file)\nconst stream = await api.getFileStream(file_id)          \/\/ { file, body, bytes(), bodyUsed }\nstream.file.file_unique_id                               \/\/ getFile info, minus file_path\nfor await (const chunk of stream.body) { \/* Uint8Array *\/ }   \/\/ or: await stream.bytes()\n```\n  Accepts a `file_id` string or `{ file_id }`. `getFile` cap is **20 MB**. A bad\/expired `file_id` throws `BotApiError` (`.code === 400`) — the same failure as a direct `api.getFile`.\n\n# HTTP (`fetch`)\n\n`import { fetch } from 'sdk'`. A `fetch`-like client for outbound HTTP.\n\n```js\nconst res = await fetch('https:\/\/api.example.com\/users', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application\/json' },\n  body: JSON.stringify({ name: 'Alice' }),\n});\nif (!res.ok) throw new Error(res.statusText);\nconst data = await res.json();\n```\n\nThe response: `res.status`, `res.statusText`, `res.ok` (true for 200–299),\n`res.url`, `res.headers` (`.get()` \/ `.has()` \/ `.keys()` \/ `.entries()`), and body\nreaders `await res.json()` \/ `await res.text()`. Or stream:\n`for await (const chunk of res.body)`.\n\nBody helpers set the matching `Content-Type` for you:\n\n```js\nawait fetch(url, { method: 'POST', body: fetch.body.json({ a: 1 }) }); \/\/ application\/json\nawait fetch(url, { method: 'POST', body: fetch.body.form({ a: 1 }) }); \/\/ x-www-form-urlencoded\nawait fetch(url, { method: 'POST', body: fetch.body.text('hi') });     \/\/ text\/plain\n```\n\nA request `body` can also be binary\/files — a `Uint8Array`, an `InputFile`, or a\n`FormData` (multipart); the bytes stream out (same upload plumbing as `api`):\n\n```js\nimport { fetch, FormData, InputFile } from 'sdk'\nconst form = new FormData()\nform.append('file', new InputFile(bytes, 'photo.jpg', { type: 'image\/jpeg' }))\nform.append('caption', 'hi')\nawait fetch(url, { method: 'POST', body: form })\n```\n\nNotes: a body can be read **once** — a second `.json()`\/`.text()`\/stream throws\n`TypeError: body used already` (check `res.bodyUsed`). Redirects are followed\nautomatically (web-parity) — `res.url` is the final URL after any hops. A 404 (or\nany HTTP status) resolves normally with `res.ok === false`; only real network\nerrors (bad host, invalid URL) reject.\n\n# Logging (`console`)\n\nPlain `console` works, and its output shows up in `npx tgcloud run`:\n\n```js\nconsole.log('processing', { chatId: id });  \/\/ log \/ debug — plain\nconsole.info('started');                     \/\/ info  — blue\nconsole.warn('rate limited');                \/\/ warn  — yellow\nconsole.error(err);                          \/\/ error — red, includes a stack\n```\n\nEach line is tagged with its `[file:line]`. `console.error` and `console.trace`\nappend a full stack trace; `console.warn` does not.\n\n# Endpoints (`endpoints\/`)\n\nA module in `tgcloud\/endpoints\/<name>.js` is called by the bot's Mini App as\n`POST \/api\/<name>` (same host as the static site). Names are identifier-like:\nletters, digits, `_`, not starting with a digit — `getProfile`, `place_order`.\n\n```js\n\/\/ tgcloud\/endpoints\/getProfile.js\nimport { db, EndpointError } from 'sdk'\nimport { eq } from 'sdk\/db'\nimport { profiles } from '..\/schema.js'\n\nexport default async function (input, ctx) {\n  const user = ctx.initData.user            \/\/ verified by the platform\n  const row = await db.select().from(profiles).where(eq(profiles.userId, user.id)).get()\n  if (!row) throw new EndpointError('Profile not found', { code: 'NOT_FOUND' })\n  return row                                \/\/ → { ok: true, result: row }\n}\n```\n\n- `input` — the JSON object the Mini App sent (always an object).\n- `ctx.initData` — same shape as `Telegram.WebApp.initDataUnsafe` on the client\n  (`user`, `chat`, `start_param`, `auth_date`, …), but verified: a call without\n  valid init data gets `401` and the endpoint never runs.\n- Return value → JSON `result`.\n- `throw new EndpointError(description, parameters?)` → `400`,\n  `{ ok: false, error_type: \"ENDPOINT_ERROR\", description, parameters }`.\n  `description` must be a string and `parameters` a plain object (a `TypeError`\n  otherwise, at the throw site). Put a machine-readable code in `parameters`.\n- Any other exception → `500`, `error_type: \"INTERNAL\"`, generic text; the real\n  message is only in the logs.\n\nCalling it from the front-end — the official `telegram-web-app.js` sends the\ninit data for you:\n\n```js\nTelegram.WebApp.Serverless.call('getProfile', { lang: 'en' }, function (err, profile) {\n  if (err) {\n    \/\/ err.message = description, err.type = error_type ('ENDPOINT_ERROR' = your\n    \/\/ EndpointError; anything else is the platform's), err.parameters\n    return\n  }\n  \/\/ use profile\n})\n```\n\nTest without the Mini App — `run` passes `--ctx` through unverified:\n\n```\nnpx tgcloud run endpoints\/getProfile '{}' --ctx '{ initData: { user: { id: 1 } } }'\n```\n","endpoints\/":"import { EndpointError } from 'sdk';\n\n\/\/ $$BASENAME$$ — an HTTP endpoint of the Mini App.\n\/\/ Once deployed, it answers at:\n\/\/   POST \/api\/$$BASENAME$$\nexport default async function (input, ctx) {\n  \/\/ ctx.initData is already verified by the platform\n\n  \/\/ your logic here\n}\n","handlers\/":"import { api } from 'sdk';\n\n\/\/ runs on each `$$BASENAME$$` update\nexport default async function ($$BASENAME$$, ctx) {\n  \/\/ your logic here\n}","handlers\/message.js":"\/\/ handlers\/message.js — runs on each `message` update. The file name is the\n\/\/ Telegram update type; the platform calls the default export with the matching\n\/\/ payload (here, a Message). The full update is available as `ctx.update`.\n\nimport { api } from 'sdk';\n\nexport default async function (message, ctx) {\n  \/\/ Echo the text back — replace with your own logic.\n  await api.sendMessage({\n    chat_id: message.chat.id,\n    text: `You said: ${message.text ?? '(no text)'}`,\n  });\n}\n","lib\/":"\/\/ $$MODULE$$ — shared module. Export functions or values\n\/\/ below, then import them elsewhere, e.g. from a handler:\n\/\/   import { thing } from '..\/$$MODULE$$.js';\n","schema.js":"import { table, integer, text } from 'sdk\/db';\n\n\/\/ Your database tables go here as named exports. Deploying this file registers the\n\/\/ schema; applying the changes (a migration) updates the database.\n\n\/\/ Uncomment to define your first table:\n\/\/ export const users = table('users', {\n\/\/   id:   integer('id').primaryKey({ autoIncrement: true }),\n\/\/   name: text('name').notNull(),\n\/\/ });\n"}}}