Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Each plugin lives in `plugins/<slug>`. 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. |

Expand Down
21 changes: 21 additions & 0 deletions plugins/valtown/LICENSE.valtown
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions plugins/valtown/NOTICE.valtown
Original file line number Diff line number Diff line change
@@ -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.
45 changes: 45 additions & 0 deletions plugins/valtown/README.md
Original file line number Diff line number Diff line change
@@ -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 bundled guidance 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`.
6 changes: 6 additions & 0 deletions plugins/valtown/assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions plugins/valtown/index.ts
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions plugins/valtown/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
}
109 changes: 109 additions & 0 deletions plugins/valtown/skills/valtown-blob-storage/SKILL.md
Original file line number Diff line number Diff line change
@@ -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/
54 changes: 54 additions & 0 deletions plugins/valtown/skills/valtown-cron-and-intervals/SKILL.md
Original file line number Diff line number Diff line change
@@ -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.
73 changes: 73 additions & 0 deletions plugins/valtown/skills/valtown-email/SKILL.md
Original file line number Diff line number Diff line change
@@ -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<string, string>
// }
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.
Loading