diff --git a/README.md b/README.md index a400e81c..0d18d729 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Each plugin lives in `plugins/`. The directory name is the install keyword | `nanobanana` | Image generation through OpenRouter and Gemini image models. | | `speak` | Speaks completed Cline replies with ElevenLabs text to speech. | | `typescript-lsp` | TypeScript language service `goto_definition` support. | +| `valtown` | Val Town MCP plus platform skills for HTTP vals, schedules, storage, email, OAuth, UI, and integrations. | | `weather-metrics` | Demo weather tool plus runtime metrics hooks. | | `web-search` | Exa-backed web search as a Cline tool. | diff --git a/plugins/valtown/LICENSE.valtown b/plugins/valtown/LICENSE.valtown new file mode 100644 index 00000000..d94f86b3 --- /dev/null +++ b/plugins/valtown/LICENSE.valtown @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Val Town + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/valtown/NOTICE.valtown b/plugins/valtown/NOTICE.valtown new file mode 100644 index 00000000..3425b251 --- /dev/null +++ b/plugins/valtown/NOTICE.valtown @@ -0,0 +1,5 @@ +This plugin includes Val Town skills and icon materials derived from Val Town plugin materials. + +Source project: https://github.com/val-town/plugins + +The included materials are licensed under MIT. diff --git a/plugins/valtown/README.md b/plugins/valtown/README.md new file mode 100644 index 00000000..d2fb0a15 --- /dev/null +++ b/plugins/valtown/README.md @@ -0,0 +1,45 @@ +# valtown + +Build and deploy TypeScript vals on Val Town. + +## What It Does + +Registers the `valtown` MCP server and installs Val Town platform skills for HTTP endpoints, cron and interval vals, blob storage, SQLite storage, email, OAuth, React UI, templates, and third-party integrations. + +The MCP server exposes Val Town actions for working with vals and their runtime resources. The bundled skills add platform-specific guidance for choosing file types, using Val Town standard libraries, remixing templates, verifying live endpoints, handling scheduled runs, and storing secrets in environment variables. + +## Install + +```bash +cline plugin install valtown +``` + +For local development from this repository: + +```bash +cline plugin install ./plugins/valtown --cwd . +``` + +## Example Usage + +After installation, ask Cline: + +```text +Create a small Val Town HTTP val with a React UI and SQLite-backed state. +``` + +## Requirements + +- A Val Town account. +- Authorization for the Val Town MCP server when Cline prompts for MCP access. +- Any third-party API credentials needed by the val being built. + +## Security Notes + +Setup registers the remote Val Town MCP server and installs bundled skills only. It does not create vals, call Val Town APIs, send email, start schedules, create databases, upload blobs, or store secrets during installation. + +Live Val Town work can create public URLs, send mail, change schedules, persist data, and call external services. The bundled skills require explicit approval for those actions and keep credentials in Val Town environment variables rather than source, blobs, logs, or public output. + +## Attribution + +Bundled Val Town skills and icon are derived from Val Town plugin materials, licensed under MIT. See `LICENSE.valtown` and `NOTICE.valtown`. diff --git a/plugins/valtown/assets/icon.svg b/plugins/valtown/assets/icon.svg new file mode 100644 index 00000000..1562c517 --- /dev/null +++ b/plugins/valtown/assets/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/valtown/index.ts b/plugins/valtown/index.ts new file mode 100644 index 00000000..64a0697b --- /dev/null +++ b/plugins/valtown/index.ts @@ -0,0 +1,20 @@ +import type { AgentPlugin } from "@cline/sdk" + +const plugin: AgentPlugin = { + name: "valtown", + manifest: { + capabilities: ["mcp", "skills"], + }, + + setup(api) { + api.registerMcpServer({ + name: "valtown", + transport: { + type: "streamableHttp", + url: "https://api.val.town/v3/mcp", + }, + }) + }, +} + +export default plugin diff --git a/plugins/valtown/package.json b/plugins/valtown/package.json new file mode 100644 index 00000000..e968b87d --- /dev/null +++ b/plugins/valtown/package.json @@ -0,0 +1,20 @@ +{ + "name": "valtown", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Cline plugin for building and deploying Val Town vals.", + "cline": { + "plugins": [ + { + "paths": [ + "./index.ts" + ], + "capabilities": [ + "mcp", + "skills" + ] + } + ] + } +} diff --git a/plugins/valtown/skills/valtown-blob-storage/SKILL.md b/plugins/valtown/skills/valtown-blob-storage/SKILL.md new file mode 100644 index 00000000..c019da48 --- /dev/null +++ b/plugins/valtown/skills/valtown-blob-storage/SKILL.md @@ -0,0 +1,109 @@ +--- +name: valtown-blob-storage +description: Use when a val needs simple key/value persistence - JSON documents, cached responses, uploaded files, or binary assets. Covers the std/blob API, listing and deleting keys, account-global or val scoping, and storage limits. +triggers: [blob, storage, kv, key-value, persistence, cache, store, upload, file, json, asset, binary] +--- + +# Blob Storage + +Val Town provides built-in key/value blob storage via the `std/blob` module. Reach for it whenever a val needs to persist simple values - JSON documents, cached API responses, uploaded files, or binary assets - keyed by a string. For relational or structured data you query with SQL, prefer `std/sqlite` instead. + +## Scoping: account-global or per-val depending on import + +There are two exports of the blob utility: `global.ts`, which is scoped to the user account, and `main.ts`, which is scoped to the val itself. Prefer the `main.ts` interface and val scoping for new vals. + +Here is the scoped import: + +```ts +/** + * Importing from `main.ts` provides an interface to val-scoped blobs. + */ +import { blob } from "https://esm.town/v/std/blob/main.ts"; +``` + +Here are the global imports: + +```ts +/** + * Importing from `global.ts` provides a blob interface that is scoped + * to your account. + */ +import { blob } from "https://esm.town/v/std/blob/global.ts"; +/** + * This entrypoint is also available as `v/std/blob`. This is common + * in older vals. + */ +import { blob } from "https://esm.town/v/std/blob"; +``` + +The scoped & global `blob` interfaces have the same methods. + +Scoped & global blobs are stored separately: you cannot access global blobs with the scoped interface or vice versa. + +## Basic usage (JSON) + +```ts +import { blob } from "https://esm.town/v/std/blob/main.ts"; + +await blob.setJSON("config", { theme: "dark", count: 0 }); + +const config = await blob.getJSON("config"); +// config = { theme: "dark", count: 0 }, or undefined if the key doesn't exist +``` + +`getJSON` returns `undefined` when the key is missing, so guard before using the result: + +```ts +const config = (await blob.getJSON("config")) ?? { theme: "light", count: 0 }; +``` + +## Raw and binary data + +Use `set`/`get` for strings, binary, or any `BodyInit`. `get` returns a standard `Response`, so use its body helpers (`.text()`, `.json()`, `.arrayBuffer()`, `.blob()`): + +```ts +await blob.set("logo.png", imageBytes); // string | BodyInit (Blob, ArrayBuffer, ReadableStream, ...) + +const res = await blob.get("logo.png"); +const bytes = await res.arrayBuffer(); +``` + +Unlike `getJSON`, `get` throws `ValTownBlobNotFoundError` if the key doesn't exist - wrap it in `try/catch` when the key may be absent. + +## Listing, deleting, copying + +```ts +const entries = await blob.list("user_"); // optional key prefix filter +// entries = [{ key, size, lastModified }, ...] + +for (const { key } of entries) { + await blob.delete(key); +} + +await blob.copy("config", "config.bak"); // duplicate under a new key +await blob.move("draft", "published"); // rename / relocate +``` + +`list(prefix?)` returns an array of `{ key: string; size: number; lastModified: string }` - objects, not bare key strings. + + +## Limits + +- Key length: up to 512 characters. +- Total storage: 10 MB on the free plan, 1 GB on Pro - shared across all blobs in the account. +- Store large or structured datasets in `std/sqlite` rather than as one giant blob. + +## Reading/writing blobs via tools + +When using the `storeBlob`, `readBlob`, `listBlobs`, or `deleteBlob` tools against a val owned by an organization (not your personal account), pass the org handle as the `org` parameter so the call hits that organization's blob storage. Example: `{ key: "myapp:config", org: "some-org" }`. This only matters for the tool calls - code inside the val reads and writes its owning account's storage automatically. Note `storeBlob` accepts UTF-8 text up to 100 KB; write larger or binary blobs from code with `blob.set`. + +## Rules + +- Treat keys as a flat namespace. Use prefixes (`feature:subkey`) for organization and to scope `list`. +- `getJSON` returns `undefined` for missing keys; `get` throws `ValTownBlobNotFoundError`. Handle the absent case accordingly. +- Don't store secrets in blobs - use environment variables for credentials. +- Ask before deleting, moving, or overwriting existing blobs, especially account-global or organization-owned blobs. + +## Reference + +Full API docs: https://docs.val.town/std/blob/ diff --git a/plugins/valtown/skills/valtown-cron-and-intervals/SKILL.md b/plugins/valtown/skills/valtown-cron-and-intervals/SKILL.md new file mode 100644 index 00000000..8268a162 --- /dev/null +++ b/plugins/valtown/skills/valtown-cron-and-intervals/SKILL.md @@ -0,0 +1,54 @@ +--- +name: valtown-cron-and-intervals +description: Use when building a val that runs on a schedule - periodic jobs, recurring tasks, polling, cron jobs, monitoring, alerting. Covers the interval handler signature, cron expressions, the UTC timezone constraint, and the `lastRunAt` pattern for detecting new items since the previous run. +triggers: [cron, interval, scheduled, schedule, recurring, periodic, timer, poll, polling, job, daily, hourly] +--- + +# Cron / Interval Vals + +Interval vals (`fileType: "interval"`) run on a recurring schedule defined by a cron expression. Use them for polling external APIs, sending reminders, running cleanups, generating reports, or any work that should happen on a clock rather than in response to a request. + +## Basic handler + +```ts +// Learn more: https://docs.val.town/vals/cron/ +export default async function (interval: Interval) { + // interval.lastRunAt: Date | undefined + console.log(interval); +} +``` + +The file must have an `export` - `export default` for the handler. + +## Timezone + +Cron expressions run in UTC. Convert any human-readable schedule (e.g. "9am Eastern") to UTC before writing the cron expression. Daylight savings is not handled - pick a UTC time that's close enough year-round. + +## The `lastRunAt` pattern + +`interval.lastRunAt` is the timestamp of the previous successful run (or `undefined` on the first run). Use it to fetch only items created since the last run, instead of re-scanning everything: + +```ts +export default async function (interval: Interval) { + const since = interval.lastRunAt ?? new Date(Date.now() - 24 * 60 * 60 * 1000); + const newItems = await fetchItemsSince(since); + for (const item of newItems) { + await handle(item); + } +} +``` + +This makes the val idempotent against missed runs and avoids reprocessing. + +## Reading and updating the schedule + +- `read_interval_settings` - fetch the current cron expression and active state of an interval file. +- `write_interval_settings` - change the cron expression or pause/resume an interval. + +## When to skip a template + +For simple scheduled jobs, create a new val with a single `interval`-type file directly - no template needed. Templates are for more complex shapes (dashboards, AI agents, webhook + UI combos). + +## Verifying changes + +After editing an interval val, ask before using `run_file` because scheduled handlers may send messages, mutate storage, or call external APIs. When approved, invoke the handler manually instead of waiting for the next scheduled run. diff --git a/plugins/valtown/skills/valtown-email/SKILL.md b/plugins/valtown/skills/valtown-email/SKILL.md new file mode 100644 index 00000000..438d8a9a --- /dev/null +++ b/plugins/valtown/skills/valtown-email/SKILL.md @@ -0,0 +1,73 @@ +--- +name: valtown-email +description: Use when a val sends email, receives email, or is triggered by an incoming email. Covers email-type vals (the Email handler shape, attachment limits, the assigned val email address) and sending mail via std/email. +triggers: [email, mail, inbox, send, sending, smtp, imap, attachment, message, notification] +--- + +# Email + +Val Town supports both directions: vals can be triggered by incoming mail (email-type vals) and can send mail via `std/email`. + +## Receiving email - email-type vals + +Email vals (`fileType: "email"`) run when a message is delivered to the val's assigned address. + +```ts +// Learn more: https://docs.val.town/vals/email/ +// Email type: { +// from: string, +// to: string[], +// subject?: string, +// text?: string, +// html?: string, +// attachments: File[], +// headers: Record +// } +export default async function (e: Email) { + console.log(e.from, e.subject, e.text); +} +``` + +The file must have an `export` - `export default` for the handler. + +Maximum 30MB per message, including attachments. Larger messages will be rejected. + +### Reading the assigned address + +When you list files or create an email-type file, the response includes `links.email` - the address that triggers this val. Always read this from the API response. Never construct an email address yourself - the format is owned by the platform and may change. + +## Sending email - `std/email` + +For outgoing mail, import from `std/email`: + +```ts +import { email } from "https://esm.town/v/std/email"; + +await email({ + to: "user@example.com", + subject: "Hello", + text: "Message body", +}); +``` + +`std/email` exports `email` as the send function itself - call it directly (`email({ ... })`); there is no `email.send` method. It accepts the shape you'd expect: `to`, `subject`, `text`, `html`, plus `from`, `cc`, `bcc`, `replyTo`, `attachments`, and `headers`. If no `to` field is specified, it defaults to sending mail to the val owner's address. + +## Replying to an incoming message + +Combine the two - read the inbound `from` in an email-type handler, then call `email` to reply: + +```ts +import { email } from "https://esm.town/v/std/email"; + +export default async function (e: Email) { + await email({ + to: e.from, + subject: `Re: ${e.subject ?? ""}`, + text: "Got it, thanks!", + }); +} +``` + +## Verifying changes + +After editing an email-type val, ask before using `run_file` because handlers may send mail or call external services. When approved, use a sample `Email` payload to invoke the handler manually instead of waiting for a real incoming message. For send-only vals, confirm the recipient and content before running the script, then check `get_logs` for delivery errors. diff --git a/plugins/valtown/skills/valtown-http-endpoints/SKILL.md b/plugins/valtown/skills/valtown-http-endpoints/SKILL.md new file mode 100644 index 00000000..f8b67171 --- /dev/null +++ b/plugins/valtown/skills/valtown-http-endpoints/SKILL.md @@ -0,0 +1,70 @@ +--- +name: valtown-http-endpoints +description: Use when building an HTTP val - a web endpoint, API route, webhook receiver, or any val that responds to HTTP requests. Covers the handler signature, Hono usage, the endpoint URL, CORS behavior, redirects, and Val Town-specific limitations. +triggers: [http, endpoint, webhook, api, request, response, hono, web, route, fetch, cors, redirect] +--- + +# HTTP Endpoints + +HTTP vals (`fileType: "http"`) export a request handler and run on every incoming HTTP request. Each HTTP file is assigned a public live URL - never construct it yourself; read `links.endpoint` from `list_files` or `create_file` responses, or call `fetch_val_endpoint`. + +## Basic handler + +```ts +// Learn more: https://docs.val.town/vals/http/ +export default async function (req: Request): Promise { + return Response.json({ ok: true }); +} +``` + +The file must have an `export` - `export default` for the handler. + +## Hono + +When using Hono, export `app.fetch` (not `app`): + +```ts +import { Hono } from "npm:hono"; + +const app = new Hono(); + +app.get("/", (c) => c.text("hello")); + +// Always add this for full stack traces on errors: +app.onError((err) => Promise.reject(err)); + +export default app.fetch; +``` + +`serveStatic` and `cors` middleware from Hono do not work on Val Town. Use `serveFile` / `staticHTTPServer` from `std/utils` for static files, and rely on Val Town's default CORS (see below). For the full `std/utils` API (`readFile`, `serveFile`, `staticHTTPServer`, `listFiles`, `listFilesByPath`, `httpEndpoint`, `parseVal`, ...), fetch `https://utilities.val.run/docs.md`. + +## CORS + +Val Town adds permissive CORS headers by default (`Access-Control-Allow-Origin: *`). If you set any CORS header yourself, Val Town stops adding all default headers - so either handle CORS completely yourself or don't touch it at all. + +## Redirects + +`Response.redirect` is broken on Val Town. Use one of: + +```ts +return new Response(null, { status: 302, headers: { Location: "/path" } }); +// or, with Hono: +return c.redirect("/path"); +``` + +## What's not available + +- WebSockets: Val Town does not accept incoming WebSocket connections. Use polling, long polling, or server-sent events instead. +- Filesystem access: see the platform constraints. For persistent state, use `std/sqlite` or `std/blob`. + +## Surfacing client-side errors + +For HTML responses, add this script tag to send browser errors back to val logs (visible via `get_logs`): + +```html + +``` + +## Verifying changes + +After editing an HTTP val, ask before calling `fetch_val_endpoint` if the request may hit live integrations, mutate state, reveal private data, or consume third-party quota. When approved, confirm it returns the expected status and body before reporting the endpoint as ready. diff --git a/plugins/valtown/skills/valtown-oauth/SKILL.md b/plugins/valtown/skills/valtown-oauth/SKILL.md new file mode 100644 index 00000000..c9dcb8c7 --- /dev/null +++ b/plugins/valtown/skills/valtown-oauth/SKILL.md @@ -0,0 +1,106 @@ +--- +name: valtown-oauth +description: Use when a val needs to require login with a Val Town account - gating routes behind authentication, identifying the current user, building user-specific dashboards. Covers std/oauth's `oauthMiddleware` and `getOAuthUserData`, the auto-managed `/auth/*` routes, and session behavior. For third-party OAuth providers (Google, GitHub, etc.) see the `valtown-third-party-integrations` skill instead. +triggers: [oauth, auth, login, signin, sign-in, logout, session, user, authentication, account, gated, protected, private] +--- + +# OAuth (std/oauth) + +Val Town provides zero-config "Log in with Val Town" via `std/oauth`. No database setup, no provider config - wrap your Hono fetch handler and you get login, logout, and session management for free. Sessions are stored in encrypted cookies and last 30 days. + +This is for Val Town account login only. For Google / GitHub / Slack / etc. OAuth, see the `valtown-third-party-integrations` skill - those flows are documented per-service. + +## Imports + +```ts +import { + getOAuthUserData, + oauthMiddleware, +} from "https://esm.town/v/std/oauth/middleware.ts"; +``` + +## Wrapping your app + +`oauthMiddleware(handler)` takes your Hono fetch handler and returns a wrapped handler that injects three auto-managed routes: + +- `GET /auth/login` - starts the login flow +- `GET /auth/callback` - completes the login flow +- `POST /auth/logout` - clears the session + +Export the wrapped handler as the val's default: + +```ts +import { Hono } from "npm:hono"; +import { oauthMiddleware } from "https://esm.town/v/std/oauth/middleware.ts"; + +const app = new Hono(); +app.onError((err) => Promise.reject(err)); + +app.get("/", (c) => c.text("hello")); + +export default oauthMiddleware(app.fetch); +``` + +You don't write the `/auth/*` routes yourself - the middleware adds them. Don't shadow them in your own app. + +## Reading the current user + +Call `getOAuthUserData(rawRequest)` from any route. In Hono, `rawRequest` is `c.req.raw`. It returns the session data if the request is authenticated, or `null` otherwise. + +```ts +interface SessionData { + user: { + id: string; + username: string | null; + email: string | null; + bio: string | null; + tier: "free" | "pro" | null; + type: "user" | "org"; + url: string; + links: { + self: string; + profileImageUrl: string | null; + }; + }; + accessToken: string; // Val Town API token (act on behalf of the user) + refreshToken?: string; + idToken?: string; + expiresAt: number; // Unix timestamp (ms) + isOrgMember?: boolean; // true if user belongs to this val's org +} +``` + +```ts +app.get("/", async (c) => { + const session = await getOAuthUserData(c.req.raw); + if (session?.user) { + return c.html( + `

Logged in as ${session.user.username}

` + + `
` + ); + } + return c.html(`Log in with Val Town`); +}); +``` + +## Gating routes + +There's no built-in "require login" helper - gate routes by checking `getOAuthUserData` and returning a 401 or redirecting to `/auth/login` when the session is missing: + +```ts +app.get("/dashboard", async (c) => { + const session = await getOAuthUserData(c.req.raw); + if (!session?.user) return c.redirect("/auth/login"); + return c.html(`

Welcome ${session.user.username}

`); +}); +``` + +## What you don't need to configure + +- No env vars - credentials and redirect URLs are handled by the platform. +- No callback URL setup - `/auth/callback` is wired automatically. +- No session store - sessions live in encrypted cookies. + +## Verifying changes + +After adding OAuth, call `fetch_val_endpoint` on a gated route to confirm it redirects or 401s when unauthenticated. The full login flow requires a real browser session and can't be exercised by `fetch_val_endpoint` alone - share the live URL and have the user try logging in. diff --git a/plugins/valtown/skills/valtown-react-ui/SKILL.md b/plugins/valtown/skills/valtown-react-ui/SKILL.md new file mode 100644 index 00000000..5a412978 --- /dev/null +++ b/plugins/valtown/skills/valtown-react-ui/SKILL.md @@ -0,0 +1,79 @@ +--- +name: valtown-react-ui +description: Use when building any val with a user interface - dashboards, web apps, landing pages, forms, admin tools, anything users see in a browser. Covers JSX/React conventions, Twind/Tailwind styling, React version pinning, the view-source link requirement, and what to avoid (template-string HTML, external assets). +triggers: [react, jsx, tsx, ui, frontend, component, dashboard, page, app, twind, tailwind, styling, css, html] +--- + +# React UI + +For any val that renders a UI, prefer to build it with React components in `.tsx` files, unless the user states otherwise. The `templates/react-hono-starter` template is set up for this - start there with `remix_val` instead of building from scratch. + +## File conventions + +Put markup, styles, and scripts in real files - avoid template literal strings (e.g. `new Response(\`...\`)`). Code in template strings has no syntax highlighting, no linting, no type checking, and is unreviewable. + +- `.tsx` - React/JSX components, any UI with logic or interactivity +- `.html` - purely static markup +- `.ts` - server code and scripts + +Build UI component by component in `.tsx` files. Compose small components rather than rendering one giant page. + +## Styling: Twind + Tailwind + +Prefer Twind to apply Tailwind utility classes at runtime - no build step required. Add the script to your HTML shell: + +```html + +``` + +Then use Tailwind classes directly in JSX: + +```tsx +
+

Hello

+
+``` + +Avoid inline `