# Building DeepBot apps (complete guide) Source: https://deepbot.studio/developers. The same content as the skill deepbot-app-builder (https://deepbot.studio/skill/deepbot-app-builder.zip). --- name: deepbot-app-builder description: "Use when someone wants to build, change, fix, test or publish a DeepBot app (a plugin for DeepBot with its own cards in the chat, pages, a skill for the Bot and optional scripts): plans the app, writes app.json, the Bot's skill, the interface on the DeepBot bridge, the listing for the catalogue, tests it in developer mode and gets it ready to submit." metadata: version: 1.0.0 tags: [deepbot, app, plugin, developer, catalogue, cards, bridge] --- # Build a DeepBot app A DeepBot app is a folder. DeepBot reads its `app.json`, gives the Bot its skill, shows its **cards** in the chat when the Bot calls them, puts its **pages** in DeepBot (Apps), and lets the Bot run its **scripts** on the person's computer. The interface is plain HTML, CSS and JavaScript in a locked frame that talks to DeepBot through `window.deepbot` (the bridge). ``` my-app/ app.json what the app is and what it may do (references/manifest.md) skill/SKILL.md what the Bot reads: when and how to use it (references/app-skill.md) ui/ cards and pages: HTML, CSS, JS (references/surfaces.md, bridge.md, design.md) scripts/ optional: programs the Bot runs (references/scripts.md) listing.md the app's page in the catalogue (references/listing.md) listing/ icon, banner, screenshots for that page ``` Read the reference for each part before you write it: | File | Read it for | | --- | --- | | [references/manifest.md](references/manifest.md) | every field of app.json, permissions, checks | | [references/surfaces.md](references/surfaces.md) | cards and pages: where they appear, sizing, full screen, the card's life | | [references/bridge.md](references/bridge.md) | every `deepbot.*` call with arguments, results, limits | | [references/app-skill.md](references/app-skill.md) | writing the app's SKILL.md so the Bot uses the app well | | [references/design.md](references/design.md) | the kit, colours, light/dark, layout rules | | [references/scripts.md](references/scripts.md) | scripts the Bot runs on the computer | | [references/patterns.md](references/patterns.md) | big apps: data model, editors, uploads, exports, WebGL/WASM, long jobs, keys, tests | | [references/listing.md](references/listing.md) | listing.md and the pictures for the catalogue | | [references/publish.md](references/publish.md) | developer mode, the test pass, publishing, updates, review | | [references/reserved-cards.md](references/reserved-cards.md) | card names you may not use | Three complete, working apps are in `examples/` (notes-board: a card and a page with files, storage, ask and openPage; quick-weather: a card that calls a web API; image-tools: a Python script plus a result card). Start from the one closest to what you build. For anything bigger than a card and a page, read references/patterns.md first. ## 1. Understand the app Ask (in one short message, only what you do not know yet): - what the person wants to get done, and who will use it; - what the Bot should be able to do with it, in a sentence the person would type ("make me a packing list for ...", "how much did we spend on ..."); - what the person needs to **see** (a result in the chat, a screen to come back to, both); - whether it needs the internet (which services) or programs on the computer. ## 2. Choose the parts | The person needs | Build | | --- | --- | | to see a result right where they asked | a **card** (the Bot calls it with arguments) | | a screen to come back to, browse, edit | a **page** (Apps -> the app, or a card's button) | | the Bot to know when and how to use the app | the **skill** (always) | | work done on the computer (files, conversions, heavy lifting) | a **script** the Bot runs, `permissions.computer` | | data from a web service | `deepbot.net.fetch` from the card or page, hosts in `permissions.network` | | to keep files the Bot also reads and writes | a workspace folder in `permissions.files` | | to keep the person's own settings or state | `deepbot.storage` (the Bot does not see it) | | an API key or token | `deepbot.secrets` + `{{secret:name}}` in `net.fetch` | Most apps are: the Bot writes files in the app's workspace folder, then calls a card that reads them and shows them; a page shows all of them. Keep the data in files the Bot can read and write, in a format you define in the skill. ## 3. Write it 1. `app.json` (references/manifest.md). `id` is permanent. Card names are tools the Bot calls: unique, `show` + your app's name + thing (`showRecipeBox`), never one of DeepBot's own (references/reserved-cards.md). Ask only for the permissions the app uses. 2. `skill/SKILL.md` (references/app-skill.md): when to use the app, the files and their exact format, which card to call with which arguments, what to say after. 3. The interface (references/surfaces.md, bridge.md, design.md): - every page includes nothing extra: DeepBot injects the bridge (`window.deepbot`) and the design kit (CSS variables, `db-*` classes) into every HTML page it serves; - `await deepbot.ready` before reading `deepbot.args`; - a card ends with `deepbot.done(note)` or `deepbot.fail(message)`: the Bot hears it; - use the kit's colour variables so light and dark mode both work; - escape everything you put into HTML; - keep every file inside the app (no CDNs): it must work offline and pass review. 4. Scripts, if any (references/scripts.md): Python 3 by default, JSON lines out, never write into the app's own folder. 5. `listing.md` and `listing/` (references/listing.md): description, FAQ, changelog, icon, banner, 2-6 screenshots. ## 4. Test it in developer mode In DeepBot: Apps -> **Developer mode** (switch, top right) -> **From a folder** -> the app's folder -> Link. The app loads at once, every save reloads its pages and cards, and the checks show under the app. Then: - ask the Bot in a new conversation something that should use the app; check it calls the card with the right arguments and that the card reports done; - open each page; switch DeepBot to dark mode (Settings) and look again; - try the failure paths (missing file, no network): the card must say what went wrong and call `deepbot.fail`. **If you are DeepBot's own Bot** building an app: write it in the workspace under `my-apps//` (never inside DeepBot's program folder), print its full path (`$env:WORKSPACE_DIR` / `$WORKSPACE_DIR` + `/my-apps/`), and ask the person to link that folder (Apps -> Developer mode -> From a folder). After they link it, the app's skill is yours too: test it by doing the task yourself in a new conversation. ## 5. Publish (references/publish.md) Public GitHub repository, `git tag v1.0.0 && git push --tags`, then submit the repository and the tag on the DeepBot site (Developers -> sign in with GitHub). Every version is reviewed; a new version needs a higher `version` and a new tag. ## Rules that get an app rejected - a card with a name DeepBot already uses, or two cards with the same name; - network calls to hosts not in `permissions.network`, requests from the page straight to other hosts instead of `deepbot.net.fetch`, or `"*"` without a reason; - scripts that touch files outside the workspace or the app's own folders without the person asking, download and run code, or hide what they do; - obfuscated or minified code without its source, executables (`.exe`, `.dll` ...); - sending the person's data anywhere the listing does not say; - copying another app or a name that imitates one. # Where an app shows up ![A card in the chat](https://deepbot.studio/developers/img/chat-card.png) 1. The card: the app's `ui/card-note.html`, shown because the Bot called `showNote({ path })`. 2. A button that calls `deepbot.ask(...)`: a message to the Bot in this conversation. 3. A button that calls `deepbot.openPage("board", { path })`: the app's page, opened on this note. 4. The Bot's answer, written after the card reported `deepbot.done()`. ## Cards, in the conversation The Bot calls a card like any tool (`showRecipeBoxRecipe({ slug: "pancakes" })`). DeepBot puts the card's `entry` page in the conversation, in a frame as wide as the conversation column, and gives it the call's arguments. How a card lives: 1. The Bot starts the call. While it is still streaming the arguments, the frame may already be on screen: listen to `deepbot.on("args", ...)` and re-render. 2. `await deepbot.ready` gives the final arguments in `deepbot.args`. 3. The card reads what it needs (usually a file the Bot just wrote), draws, and calls `deepbot.done(note)` or `deepbot.fail(message)`. 4. The Bot hears the result (the card's `confirmation` plus your note, or the failure) and answers. ![A card that calls a web API itself](https://deepbot.studio/developers/img/card-weather.png) When a card cannot do its job it says so on screen and tells the Bot with `deepbot.fail`; DeepBot also shows the failure under the card, and the Bot fixes the cause and tries again: ![A card that failed, and the Bot recovering](https://deepbot.studio/developers/img/card-fail.png) Sizing: the card starts at `height` and then follows its content: DeepBot measures `` and resizes the frame (60 to 4000 px). Do not use `height: 100vh` in a card; let the content decide. Avoid layouts that jump in height while loading (reserve space for images). Full screen: by default DeepBot draws a full-screen button on the card's top right corner. With `"fullscreen": "app"` you draw your own (in your header) and call `deepbot.fullscreen()`. When a card is full screen, `` has class `full` and you get `deepbot.on("display", ({ full }) => ...)`: use it to show more (a bigger stage, the editing tools). A card is the place for: the result of what the person asked, a choice (buttons that `ask` the Bot), a preview with "Open in " (`openPage`). Keep it focused; long work goes to a page. ## Pages, in DeepBot ![A page](https://deepbot.studio/developers/img/page.png) 1. The page: the app's `ui/board.html`, filling DeepBot's main area. 2. Pins, kept per person with `deepbot.storage`. 3. "New note with the Bot": `deepbot.ask(...)` from a page starts a new conversation. 4. Apps, in DeepBot's bar: where the person finds every app and its pages. A page fills DeepBot's main area (everything right of the sidebar). The person reaches it: - on the **Apps** page: each page is a button on the app's row; - from a card: `deepbot.openPage("box", { slug: "pancakes" })` opens it with `deepbot.args`; - by its address: `/apps//`. ![The Apps page](https://deepbot.studio/developers/img/apps-row.png) 1. Each page of the app is a button on its row. Pages are for the things a person comes back to: a library of everything the app made, an editor, settings (keys via `deepbot.secrets`), progress of long jobs. Use `class="db-page"` on `` for DeepBot's padding and background, or lay it out yourself (full-bleed editors). A page's `deepbot.args` are the params it was opened with (`{}` when opened from the Apps page). The page reloads when opened with different params. ![A page opened on one note from a card](https://deepbot.studio/developers/img/page-args.png) ## The skill, invisible but essential The Bot only uses the app because the app's skill tells it when and how (app-skill.md). DeepBot adds it to every Bot that may use apps and keeps it in sync with the file. ## Apps page and the catalogue ![Developer mode](https://deepbot.studio/developers/img/apps-page.png) 1. The Developer mode switch. 2. Link a folder: the app loads from it and reloads as you save. The Apps page lists installed apps (activate, deactivate, delete, their pages) and, with **Developer mode** on, lets you link a folder (live reload), upload a .zip, or install from a GitHub repository. **Discover** searches the catalogue. ![Discover](https://deepbot.studio/developers/img/discover.png) ![App details](https://deepbot.studio/developers/img/details.png) # app.json The file at the root of the app. DeepBot reads it to know what the app is, what the Bot may call, what pages to show and what the app may touch. Paths in it are relative to the app's folder and may not leave it (`..`, absolute paths and drive letters are refused). ## A complete example ```json { "format": "deepbot.app/v1", "id": "recipe-box", "name": "Recipe box", "version": "1.2.0", "tier": "free", "vendor": "Your name or company", "summary": "Recipes your Bot writes, scales and shops for, in a box of their own.", "description": "The Bot writes recipes as files, shows them as cards you can scale, and keeps them all on the Recipe box page with a shopping list.", "icon": "ui/icon.svg", "permissions": { "files": ["recipes"], "ask": true, "computer": false, "network": ["api.spoonacular.com"] }, "skill": "skill/SKILL.md", "cards": [ { "name": "showRecipeBoxRecipe", "title": "Recipe", "entry": "ui/card-recipe.html", "height": 360, "description": "Show a recipe after writing recipes/.json. Pass `slug` and optionally `servings`.", "confirmation": "The recipe is on screen. Do not repeat it; one short sentence at most.", "fullscreen": "host", "parameters": { "type": "object", "properties": { "slug": { "type": "string", "description": "The file name under recipes/, without .json" }, "servings": { "type": "integer", "minimum": 1, "maximum": 40 } }, "required": ["slug"] } } ], "pages": [ { "id": "box", "title": "Recipe box", "entry": "ui/box.html" }, { "id": "shopping", "title": "Shopping list", "entry": "ui/shopping.html" } ] } ``` ## Fields | Field | Type | Required | What it is | | --- | --- | --- | --- | | `format` | `"deepbot.app/v1"` | yes | The format version. Always this string. | | `id` | string | yes | Permanent identity: lowercase letters, digits and `-`, 2-64 characters, starting with a letter or digit (`^[a-z0-9][a-z0-9-]{1,63}$`). It is the app's address in the catalogue and the key of everything stored for it. Never change it after publishing. | | `name` | string | yes | Shown everywhere: Apps page, catalogue, page titles. Short (1-3 words). | | `version` | string | yes | `MAJOR.MINOR.PATCH`. Every published version must be higher than the last one. The git tag should be `v` + this. | | `tier` | `"free"` | no | Use `"free"`. (Paid apps come later.) | | `vendor` | string | no | You or your company. | | `summary` | string | no, but do | One line (under ~140 characters) for catalogue cards and the Apps page. | | `description` | string | yes | A few sentences: what it does. Used when there is no listing.md. | | `icon` | path | no, but do | A square SVG or PNG inside the app (`ui/icon.svg`). Shown on the Apps page and in the sidebar. | | `permissions` | object | no | What the app may touch (below). Missing means nothing. | | `skill` | path | no, but almost always | The Bot's instructions (`skill/SKILL.md`). Without it the Bot does not know the app exists. | | `cards` | array | no | Things the Bot can show in the chat (below). | | `pages` | array | no | Full screens in DeepBot (below). | ## permissions Every bridge call is checked against these, by DeepBot, not by the app. Ask only for what the app uses; the person sees the list in plain words before installing. | Key | Default | Allows | | --- | --- | --- | | `files` | `[]` | Folders in the shared workspace the interface may list, read, write and delete in (`"recipes"` allows `recipes/...`). The Bot works in the same workspace, so this is how the Bot and the app share data. Use your app's own folder name. | | `ask` | `false` | `deepbot.ask(...)`: sending a message to the Bot. | | `computer` | `false` | Says the app's skill makes the Bot run its scripts on the computer. Shown to the person as "Runs commands on your computer". Required if you ship `scripts/`. | | `network` | `[]` | Hosts `deepbot.net.fetch` may reach. `"api.example.com"` also matches its subdomains; `"*.example.com"` is explicit about it; `"*"` means anywhere (avoid: review asks why). | ## cards Each card is a **tool the Bot can call**. Its name, description and parameters are what the Bot sees; when the Bot calls it, DeepBot shows `entry` in the conversation with the call's arguments. | Field | Required | What it is | | --- | --- | --- | | `name` | yes | The tool name: letters, digits and `_`, 3-64 characters, starting with a letter (`^[A-Za-z][A-Za-z0-9_]{2,63}$`). Must be unique across DeepBot: start with `show`, include your app's name (`showRecipeBoxRecipe`), and never use one of DeepBot's own names (reserved-cards.md). | | `title` | yes | A human name for the card ("Recipe"). | | `description` | yes | For the Bot: when to show it and what to pass. Name the files it reads ("after writing recipes/.json"). | | `entry` | yes | The HTML page inside the app that draws the card. | | `parameters` | no | JSON Schema of the arguments (default `{"type":"object"}`). Keep it small: an id or a path, not the whole content. The Bot writes content to files; the card reads them. | | `height` | no | Starting height in pixels (60-4000). The card then sizes itself to its content. | | `confirmation` | no | What the Bot is told once the card is on screen (default: "It is now on screen for the person."). Use it to steer the answer: "Do not repeat the numbers". | | `fullscreen` | no | `"host"` (default): DeepBot draws the full-screen button on the card's corner. `"app"`: your card draws its own and calls `deepbot.fullscreen()` (when your controls sit in that corner). | ## pages | Field | Required | What it is | | --- | --- | --- | | `id` | yes | `^[a-z0-9][a-z0-9-]{0,40}$`, used in links (`/apps//`) and `deepbot.openPage(id)`. | | `title` | yes | The page's name: shown as a button on the Apps page and in the page's header. | | `entry` | yes | The HTML page inside the app. | ## Checks DeepBot runs (developer mode and catalogue) Errors stop an install or a submission; warnings are shown. - `app.json` valid JSON and matching the format above; - every `icon`, `skill`, card `entry` and page `entry` exists; - card names unique within the app and not one of DeepBot's own; - no executables (`.exe .dll .so .dylib .msi .bat .cmd .scr .com .jar .bin`); - the app under 200 MB; - warnings for `computer: true`, `network: ["*"]`, and card names another installed app uses. # The bridge: `window.deepbot` Every HTML page DeepBot serves for an app (cards and pages) gets two things injected at the top of ``: the bridge script (which defines `window.deepbot`) and the design kit stylesheet. You do not include them yourself, and you do not import anything. The page runs in a locked frame (`sandbox="allow-scripts allow-downloads allow-forms"`, no same-origin). That means: - no cookies, no `localStorage` / `sessionStorage` / IndexedDB (they throw): use `deepbot.storage` or files; - no `alert`, `confirm`, `prompt`: draw your own confirmations ("Delete for good?" on a second click); - no popups (`window.open`): use `deepbot.open(url)`; - no direct access to the person's files or DeepBot's API: everything goes through the bridge; - downloads work (`` with a Blob URL), forms work, ES modules and relative imports work. Every method returns a Promise. A refused call (missing permission, a path outside your folders, a host not listed) rejects with an `Error` whose message says why. Calls time out after 120 s. ## Start ```js const init = await deepbot.ready; // init = { args, mode: "card" | "page", app: { id, name, version }, theme: {...}, dark: true|false } deepbot.args // the card's call arguments, or the page's params (openPage), {} when none deepbot.mode // "card" or "page" deepbot.dark // DeepBot is in dark mode (the element also has class "dark") ``` Always `await deepbot.ready` before reading `deepbot.args`. ## Events ```js const off = deepbot.on("args", (args) => { /* the Bot is still streaming the call: new args */ }); deepbot.on("theme", ({ dark, theme }) => { /* the person switched light/dark; CSS vars already updated */ }); deepbot.on("display", ({ full }) => { /* a card went in or out of full screen; has class "full" */ }); off(); // stop listening ``` ## Files: the shared workspace The Bot and the app share the workspace (the Bot's working folder). The app may touch only the folders in `permissions.files`. Paths are relative to the workspace: `"recipes/pancakes.json"`. ```js const { items } = await deepbot.files.list("recipes"); // items: [{ name: "pancakes.json", path: "recipes/pancakes.json", dir: false, size: 812, modified: 1760000000000 }] // a folder that does not exist yet: { items: [] } const text = await deepbot.files.read("recipes/pancakes.md"); // string (default "text") const data = await deepbot.files.read("recipes/pancakes.json", "json"); // parsed JSON const b64 = await deepbot.files.read("recipes/photo.jpg", "base64"); // base64 string // limits: text and json up to 8 MB, base64 up to 25 MB; a missing file rejects ("No such file.") await deepbot.files.write("recipes/pancakes.json", JSON.stringify(recipe, null, 2)); // text, up to 4 MB await deepbot.files.write("recipes/photo.jpg", b64, { encoding: "base64" }); // binary, up to 40 MB // folders are created as needed; an existing file is replaced await deepbot.files.delete("recipes/old.json"); // a file, or a folder inside your folders (not the folder itself) img.src = deepbot.files.url("recipes/photo.jpg"); // an address for , `; return; } open = null; } if (!notes.length) { view.innerHTML = `
No notes yet. Ask your Bot: “note down …”.
`; return; } const sorted = [...notes].sort((a, b) => pinned.includes(b.path) - pinned.includes(a.path)); view.innerHTML = `
${sorted .map( (n) => `
${esc(n.title)}
${render(n.body)}
`, ) .join("")}
`; } view.addEventListener("click", async (e) => { const pin = e.target.closest("[data-pin]"); if (pin) { const p = pin.dataset.pin; pinned = pinned.includes(p) ? pinned.filter((x) => x !== p) : [...pinned, p]; await deepbot.storage.set("pinned", pinned); return draw(); } const act = e.target.closest("[data-act]"); if (act?.dataset.act === "back") { open = null; return draw(); } if (act?.dataset.act === "delete") { if (act.dataset.sure !== "1") { act.dataset.sure = "1"; act.textContent = "Delete for good?"; return; } await deepbot.files.delete(act.dataset.path); open = null; return load(); } const tile = e.target.closest("[data-open]"); if (tile) { open = tile.dataset.open; draw(); } }); document.getElementById("refresh").addEventListener("click", load); // From a page there is no conversation yet: ask() starts a new one with this text. document.getElementById("new").addEventListener("click", () => deepbot.ask("Write me a new note: ")); await deepbot.ready; // Opened from a card with openPage("board", { path }): show that note straight away. open = deepbot.args.path || null; await load(); ``` ## examples/notes-board/ui/card-note.html ```html
Opening the note…
``` ## examples/notes-board/ui/card-note.js ```js /* * THE NOTE IN THE CHAT. The Bot calls showNote({ path }); DeepBot opens this page in a card with * those arguments in deepbot.args. The card reads the file, shows it, and tells the Bot it worked * (deepbot.done) or what went wrong (deepbot.fail), so the Bot never says "here it is" over an * empty card. */ import { esc, readNote, render } from "./notes.js"; const root = document.getElementById("root"); let note = null; async function show() { const path = String(deepbot.args.path || ""); try { note = await readNote(path); } catch { root.innerHTML = `There is no note at ${esc(path)}.`; deepbot.fail(`No note at ${path}. Write the file first, then call showNote.`); return; } root.innerHTML = `
${esc(note.title)}
${render(note.body)}
`; deepbot.done(); } root.addEventListener("click", (e) => { if (!note) return; // A message to the Bot in this conversation (needs "ask": true in app.json). if (e.target.closest("#longer")) deepbot.ask(`Make the note "${note.title}" (${note.path}) longer and more detailed.`); // One of the app's own pages, opened on this note: the page gets { path } as deepbot.args. if (e.target.closest("#board")) deepbot.openPage("board", { path: note.path }); }); await deepbot.ready; // arguments, light or dark, and DeepBot's colours have arrived await show(); // While the Bot is still streaming the call, the arguments can change: show the new ones. deepbot.on("args", show); ``` ## examples/notes-board/ui/icon.svg ```svg Notes board ``` ## examples/notes-board/ui/notes.js ```js /* * What the card and the board share: reading notes and turning their Markdown into safe HTML. * Notes are files in the workspace (notes/.md); the app may touch only that folder * (permissions.files in app.json). */ export const DIR = "notes"; export const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]); /** Split a note into its title (the first "# " line) and the rest. */ export function parseNote(text, path) { const lines = String(text).replace(/\r/g, "").split("\n"); const first = lines.findIndex((l) => l.trim()); let title = path.split("/").pop().replace(/\.md$/, "").replace(/-/g, " "); let body = lines; if (first >= 0 && /^#\s+/.test(lines[first])) { title = lines[first].replace(/^#\s+/, "").trim(); body = lines.slice(first + 1); } return { path, title, body: body.join("\n").trim() }; } /** A small Markdown subset: paragraphs, - lists, [ ] checkboxes, **bold**, *italic*, `code`. */ export function render(md) { const inline = (s) => esc(s) .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/\*(.+?)\*/g, "$1") .replace(/`(.+?)`/g, "$1"); const out = []; let list = null; for (const line of md.split("\n")) { const item = /^\s*[-*]\s+(\[( |x)\]\s+)?(.*)$/i.exec(line); if (item) { if (!list) { list = []; out.push(list); } const box = item[1] ? ` ` : ""; list.push(`
  • ${box}${inline(item[3])}
  • `); continue; } list = null; if (line.trim()) out.push(`

    ${inline(line)}

    `); } return out.map((x) => (Array.isArray(x) ? `
      ${x.join("")}
    ` : x)).join(""); } export async function readNote(path) { return parseNote(await deepbot.files.read(path, "text"), path); } /** All notes, newest first. */ export async function listNotes() { const { items } = await deepbot.files.list(DIR); const files = items.filter((i) => !i.dir && i.name.endsWith(".md")); files.sort((a, b) => b.modified - a.modified); return Promise.all(files.map(async (f) => ({ ...(await readNote(f.path)), modified: f.modified }))); } ``` ## examples/notes-board/ui/style.css ```css /* On top of DeepBot's kit (the host adds it to every page): its colours, so light and dark just work. */ .note-title { font-size: 16px; font-weight: 650; margin: 0 0 6px; } .note-body p { margin: 0 0 6px; } .note-body ul { margin: 0 0 6px; padding-left: 20px; } .note-body li { margin: 2px 0; } .note-body code { background: var(--muted); padding: 1px 5px; border-radius: 5px; } .paper { border-left: 4px solid var(--brand); } .acts { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; } .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; margin-top: 16px; } .tile { position: relative; cursor: pointer; min-height: 120px; } .tile:hover { border-color: var(--muted-foreground); } .tile .note-body { max-height: 140px; overflow: hidden; color: var(--muted-foreground); font-size: 13px; } .pin { position: absolute; top: 8px; right: 8px; border: 0; background: transparent; cursor: pointer; font-size: 15px; opacity: .35; } .pin.on, .tile:hover .pin { opacity: 1; } .head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; } .empty { padding: 40px; text-align: center; border: 1px dashed var(--border); border-radius: var(--radius); margin-top: 16px; } .reader { margin-top: 16px; } .note-body li:has(> input[type="checkbox"]) { list-style: none; margin-left: -20px; } .note-body li > input[type="checkbox"] { margin: 0 6px 0 0; vertical-align: -1px; } ``` # Example app: Quick weather (examples/quick-weather) A card that calls a web API itself through deepbot.net.fetch. ## examples/quick-weather/app.json ```json { "format": "deepbot.app/v1", "id": "quick-weather", "name": "Quick weather", "version": "1.0.0", "tier": "free", "vendor": "DeepBot examples", "summary": "The weather for any city as a card in the chat, straight from Open-Meteo.", "description": "Ask your Bot about the weather and it shows a card with today and the next days. The card asks Open-Meteo itself, through DeepBot, so the Bot does not have to.", "icon": "ui/icon.svg", "permissions": { "network": ["api.open-meteo.com", "geocoding-api.open-meteo.com"] }, "skill": "skill/SKILL.md", "cards": [ { "name": "showQuickWeather", "title": "Weather", "entry": "ui/card.html", "height": 200, "description": "Show the weather for a place: today and the next 5 days. Pass `city` (a city name, in any language).", "confirmation": "The forecast is on screen. Do not repeat the numbers; answer the person's actual question in a sentence.", "parameters": { "type": "object", "properties": { "city": { "type": "string" }, "units": { "type": "string", "enum": ["celsius", "fahrenheit"] } }, "required": ["city"] } } ] } ``` ## examples/quick-weather/listing.md ```md --- categories: [life] tags: [weather, forecast, example] screenshots: - The weather card in the chat --- An example app from the DeepBot docs: a weather card that fetches its own data. Ask your Bot “will it rain in Lisbon tomorrow?” and it shows today and the next five days. Data from [Open-Meteo](https://open-meteo.com) (free, no key). ## Changelog ### 1.0.0 - First version. ``` ## examples/quick-weather/listing/icon.svg ```svg Quick weather ``` ## examples/quick-weather/skill/SKILL.md ```md --- name: quick-weather description: "Use when the person asks about the weather, temperature, rain or wind somewhere, now or in the next days: shows a weather card from the Quick weather app." --- # Quick weather Call `showQuickWeather` with `city` (the place the person named; if they named none, ask which). Use `units: "fahrenheit"` only when the person uses Fahrenheit. The card fetches the forecast itself. When it reports what it found, answer the person's real question in one sentence ("Yes, take an umbrella after 3 pm"). Do not list the numbers again. ``` ## examples/quick-weather/ui/card.html ```html
    Looking up the weather…
    ``` ## examples/quick-weather/ui/card.js ```js /* * A WEATHER CARD. The card itself calls Open-Meteo through deepbot.net.fetch: the frame has no * network of its own, DeepBot makes the request, and only to the hosts in permissions.network. */ const root = document.getElementById("root"); const ICON = (code) => (code === 0 ? "☀️" : code <= 3 ? "⛅" : code <= 48 ? "🌫️" : code <= 67 ? "🌧️" : code <= 77 ? "❄️" : code <= 82 ? "🌦️" : "⛈️"); const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]); async function show() { const city = String(deepbot.args.city || "").trim(); const f = deepbot.args.units === "fahrenheit"; if (!city) return; try { // 1. the place -> coordinates const geo = await deepbot.net.fetch( `https://geocoding-api.open-meteo.com/v1/search?count=1&name=${encodeURIComponent(city)}`, { as: "json" }, ); const place = geo.body?.results?.[0]; if (!place) throw new Error(`No place called "${city}".`); // 2. the forecast const q = new URLSearchParams({ latitude: place.latitude, longitude: place.longitude, current: "temperature_2m,weather_code,wind_speed_10m", daily: "weather_code,temperature_2m_max,temperature_2m_min", timezone: "auto", forecast_days: "5", ...(f ? { temperature_unit: "fahrenheit" } : {}), }); const wx = await deepbot.net.fetch(`https://api.open-meteo.com/v1/forecast?${q}`, { as: "json" }); if (!wx.ok) throw new Error(`Open-Meteo answered ${wx.status}.`); const c = wx.body.current; const d = wx.body.daily; const u = f ? "°F" : "°C"; root.innerHTML = `
    ${ICON(c.weather_code)}
    ${Math.round(c.temperature_2m)}${u}
    ${esc(place.name)}${place.country ? `, ${esc(place.country)}` : ""} · wind ${Math.round(c.wind_speed_10m)} km/h
    ${d.time .map( (t, i) => `
    ${new Date(t).toLocaleDateString(undefined, { weekday: "short" })}
    ${ICON(d.weather_code[i])}
    ${Math.round(d.temperature_2m_max[i])}°${Math.round(d.temperature_2m_min[i])}°
    `, ) .join("")}
    `; // What the Bot hears back: a short summary it can answer from. deepbot.done(`${place.name}: now ${Math.round(c.temperature_2m)}${u}; next days max ${d.temperature_2m_max.map(Math.round).join(", ")}${u}.`); } catch (error) { root.innerHTML = `${esc(error.message)}`; deepbot.fail(error.message); } } deepbot.ready.then(show); ``` ## examples/quick-weather/ui/icon.svg ```svg Quick weather ``` # Example app: Image tools (examples/image-tools) A Python script the Bot runs on the computer, and a result card with pictures. ## examples/image-tools/app.json ```json { "format": "deepbot.app/v1", "id": "image-tools", "name": "Image tools", "version": "1.0.0", "tier": "free", "vendor": "DeepBot examples", "summary": "Resize and convert a folder of pictures on your computer, and see the results in the chat.", "description": "Your Bot runs the app's Python script on your computer to resize, convert (JPG, PNG, WebP) and compress pictures, then shows the results side by side in the chat with their sizes.", "icon": "ui/icon.svg", "permissions": { "files": ["images"], "computer": true }, "skill": "skill/SKILL.md", "cards": [ { "name": "showResizedImages", "title": "Pictures", "entry": "ui/card.html", "height": 260, "description": "Show the pictures in a folder under images/ as a grid with their sizes, after resizing. Pass `dir` (e.g. images/out/holiday) and optionally `before` (the folder they came from, to show how much smaller they got).", "parameters": { "type": "object", "properties": { "dir": { "type": "string" }, "before": { "type": "string" } }, "required": ["dir"] } } ] } ``` ## examples/image-tools/listing.md ```md --- categories: [design] tags: [images, resize, convert, webp, example] screenshots: - Resized pictures in the chat --- An example app from the DeepBot docs that runs a **script on your computer**. Ask your Bot to “make these photos smaller for the web” and it resizes and converts them (JPG, PNG, WebP) with the app's Python script, then shows the results with how much space they saved. Needs Python 3 with Pillow on the computer (the Bot installs Pillow if it is missing). ## Changelog ### 1.0.0 - First version. ``` ## examples/image-tools/listing/icon.svg ```svg Image tools ``` ## examples/image-tools/scripts/resize.py ```py """Resize and convert every picture in a folder. One JSON line per picture, a summary line at the end. python resize.py [--width 1600] [--format keep|jpg|png|webp] [--quality 82] """ import argparse import json import sys from pathlib import Path try: from PIL import Image, ImageOps except ImportError: print(json.dumps({"error": "Pillow is missing: python -m pip install --user pillow"})) sys.exit(2) EXT = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"} SAVE = {"jpg": ("JPEG", ".jpg"), "png": ("PNG", ".png"), "webp": ("WEBP", ".webp")} def main() -> int: p = argparse.ArgumentParser() p.add_argument("src") p.add_argument("dst") p.add_argument("--width", type=int, default=1600) p.add_argument("--format", default="keep", choices=["keep", "jpg", "png", "webp"]) p.add_argument("--quality", type=int, default=82) a = p.parse_args() src, dst = Path(a.src), Path(a.dst) if not src.is_dir(): print(json.dumps({"error": f"No folder {src}"})) return 1 dst.mkdir(parents=True, exist_ok=True) done = before = after = 0 for f in sorted(src.iterdir()): if f.suffix.lower() not in EXT: continue try: img = ImageOps.exif_transpose(Image.open(f)) img.thumbnail((a.width, a.width)) fmt, ext = SAVE.get(a.format) or SAVE.get(f.suffix.lower().lstrip(".").replace("jpeg", "jpg"), ("PNG", ".png")) if fmt == "JPEG" and img.mode not in ("RGB", "L"): img = img.convert("RGB") out = dst / (f.stem + ext) kw = {"quality": a.quality, "optimize": True} if fmt in ("JPEG", "WEBP") else {"optimize": True} img.save(out, fmt, **kw) b, s = f.stat().st_size, out.stat().st_size done, before, after = done + 1, before + b, after + s print(json.dumps({"file": out.name, "width": img.width, "height": img.height, "before": b, "after": s}), flush=True) except Exception as e: # one bad file does not stop the rest print(json.dumps({"file": f.name, "error": str(e)}), flush=True) print(json.dumps({"done": done, "before": before, "after": after})) return 0 if __name__ == "__main__": sys.exit(main()) ``` ## examples/image-tools/skill/SKILL.md ```md --- name: image-tools description: "Use when the person wants pictures resized, made smaller, compressed or converted (JPG, PNG, WebP), one or a whole folder: runs the Image tools script and shows the results." --- # Image tools The app's script is `{{APP_DIR}}/scripts/resize.py`. It needs Python 3 with Pillow; if `import PIL` fails, install it once with `python -m pip install --user pillow`. ## Where the pictures go The app works in the workspace folder `images/`: copy or download the person's pictures into `images/in//` first (ask where they are if they did not say), and write the results to `images/out//`. ## Running it ``` python {{APP_DIR}}/scripts/resize.py images/in/ images/out/ --width 1600 --format webp --quality 82 ``` - `--width` the longest side in pixels (keeps the proportions; never makes a picture bigger) - `--format` `jpg`, `png`, `webp` or `keep` - `--quality` 1-100 for jpg and webp (82 is a good default) It prints one JSON line per picture and a summary line at the end: `{"done": 12, "before": 48211033, "after": 6120044}`. ## Showing the result Call `showResizedImages` with `dir` set to the output folder and `before` to the input folder. Then say in one sentence how much smaller it got (from the summary line). ``` ## examples/image-tools/ui/card.html ```html
    Reading the folder…
    ``` ## examples/image-tools/ui/card.js ```js /* * THE RESULT IN THE CHAT: the pictures of a folder under images/, with their sizes. Pictures are * shown with deepbot.files.url(path), an address the card may put in . */ const root = document.getElementById("root"); const IMG = /\.(jpe?g|png|webp|gif|bmp)$/i; const kb = (n) => (n > 1e6 ? `${(n / 1e6).toFixed(1)} MB` : `${Math.round(n / 1e3)} KB`); const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]); async function sizeOf(dir) { if (!dir) return 0; const { items } = await deepbot.files.list(dir); return items.filter((i) => !i.dir && IMG.test(i.name)).reduce((n, i) => n + i.size, 0); } async function show() { const dir = String(deepbot.args.dir || ""); try { const { items } = await deepbot.files.list(dir); const pics = items.filter((i) => !i.dir && IMG.test(i.name)); if (!pics.length) throw new Error(`No pictures in ${dir}.`); const after = pics.reduce((n, i) => n + i.size, 0); const before = await sizeOf(deepbot.args.before); root.innerHTML = `
    ${pics.length} pictures
    ${esc(dir)}
    ${before ? `
    −${Math.round((1 - after / before) * 100)}%
    ${kb(before)} → ${kb(after)}
    ` : `
    ${kb(after)}
    `}
    ${pics .slice(0, 24) .map((p) => `
    ${esc(p.name)} · ${kb(p.size)}
    `) .join("")}
    `; deepbot.done(`${pics.length} pictures shown.`); } catch (error) { root.innerHTML = `${esc(error.message)}`; deepbot.fail(error.message); } } deepbot.ready.then(show); ``` ## examples/image-tools/ui/icon.svg ```svg Image tools ```