sidebud 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/main.js +9361 -4209
  3. package/package.json +3 -2
  4. package/skills/widget-packs/SKILL.md +199 -0
  5. package/src/manage/guide/app-arrange.png +0 -0
  6. package/src/manage/guide/app-disk.png +0 -0
  7. package/src/manage/guide/app-open.png +0 -0
  8. package/src/manage/guide/app-thread.png +0 -0
  9. package/src/manage/guide/app-tiles.png +0 -0
  10. package/src/manage/guide/draft.png +0 -0
  11. package/src/manage/guide/layouts.png +0 -0
  12. package/src/manage/guide/library.png +0 -0
  13. package/src/manage/guide/pack-page.png +0 -0
  14. package/src/manage/guide/permissions.png +0 -0
  15. package/src/manage/guide/phone-home.png +0 -0
  16. package/src/manage/guide/phone-open.png +0 -0
  17. package/src/manage/guide/widget-voice.png +0 -0
  18. package/src/manage/guide.js +155 -0
  19. package/src/manage/guide.md +139 -0
  20. package/src/manage/index.html +14 -0
  21. package/src/manage/manage.css +782 -0
  22. package/src/manage/manage.js +155 -39
  23. package/src/manage/packs.js +347 -0
  24. package/src/manage/settings.js +1 -1
  25. package/src/manage/setup.js +83 -23
  26. package/packs/BRAND_ASSETS.md +0 -8
  27. package/packs/calendar/logo.png +0 -0
  28. package/packs/calendar/pack.json +0 -127
  29. package/packs/discord/pack.json +0 -107
  30. package/packs/filesystem/pack.json +0 -84
  31. package/packs/github/pack.json +0 -127
  32. package/packs/gmail/logo.png +0 -0
  33. package/packs/gmail/pack.json +0 -274
  34. package/packs/obsidian/pack.json +0 -88
  35. package/packs/t3-agents/logo.png +0 -0
  36. package/packs/t3-agents/pack.json +0 -492
  37. package/packs/telegram/pack.json +0 -137
  38. package/packs/trello/pack.json +0 -102
  39. package/packs/whatsapp/pack.json +0 -118
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidebud",
3
- "version": "0.1.0",
3
+ "version": "0.4.0",
4
4
  "description": "Sidebud companion: talk to the agents on your computer from your phone.",
5
5
  "keywords": [
6
6
  "sidebud",
@@ -21,7 +21,7 @@
21
21
  "files": [
22
22
  "dist",
23
23
  "src/manage",
24
- "packs",
24
+ "skills",
25
25
  "THIRD_PARTY_NOTICES.md",
26
26
  "CHANGELOG.md"
27
27
  ],
@@ -36,6 +36,7 @@
36
36
  "diff": "^8.0.4",
37
37
  "qrcode": "^1.5.4",
38
38
  "ws": "^8.18.3",
39
+ "yaml": "^2.9.1",
39
40
  "zod": "^4.5.4"
40
41
  }
41
42
  }
@@ -0,0 +1,199 @@
1
+ ---
2
+ name: widget-packs
3
+ description: Build, change, and share Sidebud widget packs for the user. Use when the user asks for a new widget or integration ("show my disk space on my phone", "add a widget for my homelab", "connect Hermes"), wants to change what an existing widget shows or does, or wants to share one.
4
+ ---
5
+
6
+ # Widget packs
7
+
8
+ A **widget pack** is a folder the user owns. It tells the companion how to reach an MCP server, which read-only calls feed a home-screen tile, which controls the user may tap, and how you (the execution agent) should use its tools. Sidebud ships only the phone's UI library (the tile and detail layouts below) and the runtime that renders packs. Every integration, including the ones in the Sidebud widget library, is a pack like the one you are about to build, and the user can change or share any of them.
9
+
10
+ A pack folder holds:
11
+
12
+ | File | What it is |
13
+ | --- | --- |
14
+ | `pack.json` | The manifest (schema version 3; `packs__schema` returns the full JSON Schema). |
15
+ | `SKILL.md` | Optional but expected: how an agent should use this pack's tools. You read it whenever the pack is enabled. |
16
+ | `logo.png` | The app's real logo: PNG/JPEG/WebP, ≤128 KB (resize to about 256 px). Or a built-in icon or an emoji when the app has no logo. |
17
+ | `server/` | Optional: a small MCP server the pack ships, when no existing server fits. Scripts, JSON, and text only (no binaries), ≤2 MB, ≤50 files, no install step. |
18
+
19
+ ## Steps
20
+
21
+ 1. **Find the server.** Prefer an MCP server that already exists: the app's own (`hermes mcp serve`, `openclaw mcp serve`), the vendor's hosted endpoint, or a maintained community server. Note its transport (a stdio command, or an HTTP URL) and credentials: an API key is a pack **secret**; OAuth is an `auth` block; files on disk are a **setting** holding the path.
22
+ 2. **If none exists, write one** in `server/` (see [Writing a pack server](#writing-a-pack-server)). Wrap a CLI or a local API; keep it small and read-mostly.
23
+ 3. **Draft in a working folder** the user owns (for example `~/sidebud-packs/<name>/`): `pack.json` with identity and `mcp` only, then `packs__probe` to list the server's tools with their read-only hints and input schemas.
24
+ 4. **Look at real results.** `packs__try` calls one read-only tool and returns the result, so you map the shape you actually get.
25
+ 5. **Map results onto the UI library**: up to two glance stats (or one line), detail blocks, an optional tapped-row detail, spoken updates, and controls. Templates are lookups (below).
26
+ 6. **See it as the phone will.** `packs__preview` validates the draft, runs its data queries against its server, and renders the widget exactly as the phone receives it. It returns a text rendering of the tile and every block, any query errors, and `previewUrl`: the companion's **Phone preview** page showing the draft first on the phone's home screen, beside the draft opened. Open `previewUrl` with your browser tools and look at it (a screenshot shows both phones at once). Or give the link to the user when they want to see it. Fix what looks wrong (empty lists, raw ids, cut-off titles, statuses on every row, the wrong tile size) and preview again after each change, until it reads well at a glance.
27
+ 7. **Write `SKILL.md`** for the pack (see [The pack's skill](#the-packs-skill)).
28
+ 8. **`packs__validate`**, fix every error, then **`packs__save`**. The pack installs disabled. Set its settings and keys with `integrations__configure`, connect an account with `integrations__connect_account`, check it with `integrations__test`, and turn it on with `integrations__configure` (`enabled: true`) once the user agrees to what it launches.
29
+ 9. Tell the user what the widget shows and which controls they can add from the widget's Controls button on the phone. Once the pack is on, its widget appears on the Phone preview page and on the pack's own page (**MCP & widgets** → the pack).
30
+
31
+ **Changing an installed pack:** `integrations__status` with its id gives its folder. Copy the folder to a working folder, edit, `packs__preview` it, validate, then `packs__save` with `replace: true`. Settings and keys are kept; if the launch or server files changed, the pack is turned off until the user enables it again. Bump `version` on every change.
32
+
33
+ **What the owner controls:** each installed pack has its own page in the companion's management UI (**MCP & widgets →** the pack) with its settings, keys, account sign-in, and a switch per declared tool. A tool the owner turned off fails with "turned off for this integration"; tell the user rather than working around it. **Phone preview** in the same UI shows the home screen with live data, so after saving a pack, point the user there (or at the pack's page) to see the widget before they open the phone.
34
+
35
+ **Sharing:** `packs__export` writes a bundle file the user can send, import on another computer, or upload to the Sidebud widget library. Keys, settings, and account tokens never leave the computer. `packs__remove` uninstalls a pack (its keys and tokens are deleted); the user can install it again from the library or a bundle.
36
+
37
+ ## The manifest
38
+
39
+ | Field | Meaning |
40
+ | --- | --- |
41
+ | `schemaVersion` | `3`. |
42
+ | `id` | Reverse-DNS, lowercase, e.g. `dev.<user>.disk-space`. It never changes between versions. `app.sidebud.*` is reserved for the library's own packs. |
43
+ | `name`, `description`, `version`, `author`, `license` | Shown before install. `version` is semver. |
44
+ | `logo` | `{ "kind": "icon", "name": "tools" }` (agents, mail, calendar, chat, code, github, files, chart, bell, bolt, globe, home, music, note, star, tools), `{ "kind": "emoji", "value": "💾" }`, or `{ "kind": "file", "path": "logo.png", "source": "https://…" }`. For a pack that wraps an app, use the app's own logo: download it from its official site or repository (its app icon or favicon), save it in the pack, and set `source` to where it came from. The file is what is shown; the phone never loads the link. |
45
+ | `tile.size` | `small` (1×1), `medium` (2×1), or `large` (2×2); users can resize. |
46
+ | `mcp` | `{ "transport": "stdio", "command", "args", "env" }` or `{ "transport": "http", "url", "headers" }`. Commands run in the pack folder, so a pack server is `"command": "node", "args": ["server/index.mjs"]`. `args`, `env`, and `headers` may use `{{setting.x}}`, `{{secret.NAME}}`, `{{env.NAME}}`, and `{{oauth.accessToken}}`. An http `url` may be a setting plus a path, `"{{setting.baseUrl}}/api/mcp"`, for a server on the user's own network. |
47
+ | `platforms` | A different launch per OS: `{ "win32": { …launch… }, "darwin": …, "linux": … }`. Use it when an app installs to different places, e.g. `{{env.LOCALAPPDATA}}\\Programs\\…` on Windows. |
48
+ | `auth` | OAuth 2.0: `authorizationUrl`, `tokenUrl`, `scopes`, `clientIdSecret`, optional `clientSecretSecret` (both declared in `secrets`), `authorizationParams`. The companion runs consent on this computer, keeps tokens in the keychain, and refreshes them. An http server gets `Authorization: Bearer`; a stdio server must read `{{oauth.accessToken}}` from its `env` and is restarted when the token refreshes. |
49
+ | `secrets` | `[{ "name": "API_KEY", "description": "…" }]`: names only; values live in the keychain. |
50
+ | `permissions.tools` | **Every** tool the pack may call, with `access: read \| write`, optional `description`, and optional `argumentSettings` (`{ "repo": "repo" }` pins that argument to a required setting on every call), and `optional: true` for a tool the server offers only in some setups (for example only when the user has that kind of device), so checking the connection does not require it. Nothing else is ever called. `permissions.network` lists hosts, for review. |
51
+ | `settings` | `text`, `boolean`, or `select` fields with defaults; `required` and `format` (`directory`, `http-url`) for text. An `http-url` must use HTTPS unless it points at this computer or the local network (private addresses, Tailscale, `.local`). |
52
+ | `setup` | `steps` (what you or the user do to set it up), `prerequisites` (`[{ "name": "Node.js", "command": "node" }]`), and `detect.anyPath` (paths that exist when the app is installed; `~` and `{{env.NAME}}` allowed), which lets setup suggest the pack. |
53
+ | `skill` | `{ "path": "SKILL.md" }`. |
54
+ | `listing` | For the widget library: `category` (agents, communication, developer, notes, productivity, system), `account` (what the user signs in with or selects), `connectsTo`, optional `homepage`. Packs without one are not listed. |
55
+ | `preview` | Made-up sample data the widget library shows before anyone installs it: `data` is one sample result per query id (the shape its tool returns, after `extract`), optional `settings` for the templates that read them, and `at`, the time the sample was written; times in the sample move forward so "3m ago" stays true. Never real accounts, names, or messages. ≤32 KB. A listed pack without one shows no phone preview in the library. |
56
+ | `data` | `{ "id", "tool", "arguments", "extract", "refreshSeconds" }` (≤10). Only `read` tools; they run automatically, never if the server marks the tool as writing. `extract`: `json` (default; structured content, else JSON text), `text-json`, `text`, `records` ("Key: value" blocks), or `yaml` (a heading line or two before the YAML is skipped). `document`: a JSON pointer, for a server whose JSON text wraps the payload in a string field (`{"success": true, "result": "<yaml>"}` → `"document": "/result"`); `extract` then applies to that field, and a result without it fails with the server's `error` text. |
57
+ | `glance` | `{ "stats": [≤2 × { "label", "value", "tone" }] }` or `{ "line", "tone" }`. Tones: neutral, info, success, warning, danger. |
58
+ | `badge` | `{ "text", "tone" }`; hidden when it renders empty or `0`. |
59
+ | `blocks` | ≤8: `stats` (≤4 items; three read best), `list` (`items` resolves to an array; `item.title/subtitle/status` use `{{item/…}}`; `item.statusTones` colors rows by status text, and an empty status shows none, so emit a status only when it is news, e.g. "running"; `item.time` is an ISO date-time shown relative, "3m ago" or "in 12m"; `item.key` makes rows tappable when the pack has a `detail`; `item.buttons` (needs `item.key`) puts a small button at the row's end that calls one declared tool for that row's item straight from a tap, with no agent: `{ "id", "label", "tool", "arguments" (with `{{item/…}}`), "when" (the `where` filter's syntax, e.g. `domain=light,state=on`), optional "confirm" }`; a row shows its first button whose `when` matches, and the companion checks `when` again against fresh data before calling, so a stale button does nothing; `limit` ≤20; `title` is the list's heading, needed when there are several lists), `board` (columns of items, a kanban board: `items`, `column` renders each item's column value, `columns` ≤6 of `{ id, title, tone, values }` where `values` lists the column values it collects (default its id), `item.title/subtitle/key/time`, `limit` ≤10 per column; each column shows its count), `text`. A `large` tile previews its first list's top four rows or, with two lists or boards, each one's title and top row (a board shows its column counts); choose `large` when two are worth seeing at a glance. |
60
+ | `subtitle` | Optional live line under the title when the widget is open, e.g. `"{{agents/count}} agents · {{sessions/active}} running"`; the description is shown when absent. |
61
+ | `detail` | What a tapped row opens: a read `tool` with `arguments` (`{{item/…}}` is the row; an argument that is exactly one template and finds no value is left out, so one detail can serve rows of different shapes), `extract`/`document` as for `data`, `messages` (`{{detail/…}}`, the result's message array), and `message.role/text/at`. |
62
+ | `notify` | Spoken updates during calls (≤6): `items`, `key`, optional `state`, `category` (agents, messages, mail, calendar, updates), and `rules` (`{ "state": "failed" }` or `{ "new": true }`, `priority` needs_you/failed/finished/update, `text`, optional `withLatest`). The first read is a baseline. |
63
+ | `controls` | ≤12. `prompt` controls send their `prompt` as the user's own words. `tool` controls call one declared tool with fixed `arguments` (optional `forEach` + `maxCalls`), `effect` read or consequential, and `confirm` text for consequential ones. Mark two or three `default: true`. |
64
+
65
+ **Templates** are lookups, never code: `{{setting.path}}`, `{{disks}}` (a query's data), `{{disks/drives/0/free}}` (JSON pointer), `{{item/name}}` (inside lists and `forEach`), with filters `|count`, `|pluck:id`, `|first`, `|join:", "`, `|default:none`, `|truncate:40`, `|sort:-updatedAt` (comma-separated keys, `-` descending, `field=value` first), `|where:domain=light,state=on` (items where every `field=value` holds; `!=` negates, `;` separates alternatives: `where:domain=lock,state!=locked;domain=cover,state=open`), `|group:area` (`[{ key, count, items }]` per distinct value; items without it are left out), and `|split:", "` (text into a list). A string that is exactly one template yields the raw value, so `"ids": "{{unread/threads|pluck:id}}"` passes an array.
66
+
67
+ ## Writing a pack server
68
+
69
+ A pack server is an MCP server over stdio in `server/`, run with the Node the companion already uses (or an interpreter named in `setup.prerequisites`). With no dependencies there is nothing to install. Start `server/mcp.mjs` with this helper and keep it as is:
70
+
71
+ ```js
72
+ // Minimal MCP over stdio: newline-delimited JSON-RPC with initialize, ping, tools/list, tools/call.
73
+ import { createInterface } from 'node:readline'
74
+ export function serve({ name, version, tools }) {
75
+ const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`)
76
+ const handlers = {
77
+ initialize: (params) => ({ protocolVersion: params?.protocolVersion ?? '2025-06-18', capabilities: { tools: {} }, serverInfo: { name, version } }),
78
+ ping: () => ({}),
79
+ 'tools/list': () => ({ tools: tools.map(({ run: _run, ...tool }) => tool) }),
80
+ 'tools/call': async (params) => {
81
+ const tool = tools.find((candidate) => candidate.name === params?.name)
82
+ if (!tool) return { content: [{ type: 'text', text: `Unknown tool ${params?.name}` }], isError: true }
83
+ try {
84
+ const data = await tool.run(params.arguments ?? {})
85
+ return { content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data }
86
+ } catch (error) {
87
+ return { content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }], isError: true }
88
+ }
89
+ },
90
+ }
91
+ const input = createInterface({ input: process.stdin })
92
+ input.on('line', async (line) => {
93
+ let message
94
+ try { message = JSON.parse(line) } catch { return }
95
+ if (message?.id === undefined || message.id === null) return
96
+ const handler = handlers[message.method]
97
+ if (!handler) return send({ jsonrpc: '2.0', id: message.id, error: { code: -32601, message: `Method not found: ${message.method}` } })
98
+ try { send({ jsonrpc: '2.0', id: message.id, result: await handler(message.params) }) }
99
+ catch (error) { send({ jsonrpc: '2.0', id: message.id, error: { code: -32603, message: String(error?.message ?? error) } }) }
100
+ })
101
+ input.on('close', () => process.exit(0))
102
+ }
103
+ ```
104
+
105
+ Rules for pack servers:
106
+ - Each tool is `{ name, description, inputSchema, annotations, run }`. Mark read tools `annotations: { readOnlyHint: true }` and anything that changes state `readOnlyHint: false`; the companion refuses to poll a tool the server says writes.
107
+ - Check every argument before using it. Never pass arguments to a shell; call programs with an argument array (`execFile`), never a command string.
108
+ - Credentials come from `process.env`, filled from the pack's `mcp.env`; never write them to disk or into results.
109
+ - Write to stdout only through the helper; log to stderr if needed.
110
+ - Work on every OS the user has, or add `platforms` launches.
111
+
112
+ ### Worked example: disk space on a home PC
113
+
114
+ `server/index.mjs`:
115
+
116
+ ```js
117
+ import { execFile } from 'node:child_process'
118
+ import { promisify } from 'node:util'
119
+ import { serve } from './mcp.mjs'
120
+
121
+ const run = promisify(execFile)
122
+ const gb = (bytes) => Math.round((bytes / 1024 ** 3) * 10) / 10
123
+
124
+ async function drives() {
125
+ if (process.platform === 'win32') {
126
+ const { stdout } = await run('powershell.exe', ['-NoProfile', '-Command', 'Get-PSDrive -PSProvider FileSystem | Select-Object Name,Used,Free | ConvertTo-Json'])
127
+ return [JSON.parse(stdout)].flat().filter((d) => d.Used !== null).map((d) => ({ name: `${d.Name}:`, usedGb: gb(d.Used), freeGb: gb(d.Free), usedPercent: Math.round((d.Used / (d.Used + d.Free)) * 100) }))
128
+ }
129
+ const { stdout } = await run('df', ['-kP'])
130
+ return stdout.trim().split('\n').slice(1).map((line) => line.split(/\s+/)).filter((c) => c[0].startsWith('/dev/')).map((c) => ({ name: c[5], usedGb: gb(c[2] * 1024), freeGb: gb(c[3] * 1024), usedPercent: parseInt(c[4], 10) }))
131
+ }
132
+
133
+ serve({
134
+ name: 'disk-space',
135
+ version: '1.0.0',
136
+ tools: [{
137
+ name: 'list_drives',
138
+ description: 'Free and used space on each drive of this computer.',
139
+ inputSchema: { type: 'object', properties: {} },
140
+ annotations: { readOnlyHint: true },
141
+ run: async () => {
142
+ const level = (percent) => (percent >= 95 ? 'critical' : percent >= 90 ? 'low' : 'ok')
143
+ const list = (await drives()).map((d) => ({ ...d, level: level(d.usedPercent) })).sort((a, b) => b.usedPercent - a.usedPercent)
144
+ return { drives: list, fullest: list[0] ?? null, low: list.filter((d) => d.level !== 'ok').length }
145
+ },
146
+ }],
147
+ })
148
+ ```
149
+
150
+ `pack.json`:
151
+
152
+ ```json
153
+ {
154
+ "schemaVersion": 3,
155
+ "id": "dev.home.disk-space",
156
+ "name": "Disk space",
157
+ "description": "Free space on this computer's drives.",
158
+ "version": "1.0.0",
159
+ "author": { "name": "Home" },
160
+ "license": "MIT",
161
+ "logo": { "kind": "emoji", "value": "💾" },
162
+ "tile": { "size": "small" },
163
+ "mcp": { "transport": "stdio", "command": "node", "args": ["server/index.mjs"] },
164
+ "permissions": { "tools": [{ "name": "list_drives", "access": "read" }] },
165
+ "setup": { "prerequisites": [{ "name": "Node.js", "command": "node" }] },
166
+ "skill": { "path": "SKILL.md" },
167
+ "data": [{ "id": "disks", "tool": "list_drives", "refreshSeconds": 600 }],
168
+ "glance": { "stats": [
169
+ { "label": "Fullest", "value": "{{disks/fullest/usedPercent}}%", "tone": "info" },
170
+ { "label": "Low", "value": "{{disks/low}}", "tone": "warning" }
171
+ ] },
172
+ "blocks": [{ "type": "list", "items": "{{disks/drives}}", "item": { "title": "{{item/name}}", "subtitle": "{{item/freeGb}} GB free", "status": "{{item/level}}", "statusTones": { "ok": "success", "low": "warning", "critical": "danger" } }, "emptyText": "No drives found" }],
173
+ "notify": [{ "id": "low", "category": "updates", "items": "{{disks/drives}}", "key": "{{item/name}}", "state": "{{item/level}}", "rules": [{ "state": "critical", "priority": "update", "text": "Drive {{item/name}} is {{item/usedPercent}}% full." }] }],
174
+ "controls": [{ "id": "summary", "kind": "prompt", "label": "How's my disk space?", "prompt": "How much space is left on my computer's drives?", "default": true }]
175
+ }
176
+ ```
177
+
178
+ ## The pack's skill
179
+
180
+ Every pack gets a `SKILL.md` with frontmatter (`name`, `description`) and these sections, so skills read the same across packs:
181
+
182
+ 1. **What it is** and its terms (thread, session, conversation, board…). Say that tool names may carry the integration's prefix.
183
+ 2. **Finding things from the user's words**: which list tool and filters, how to match a description to one item, and to ask when several fit.
184
+ 3. **Reading for voice**: which tool gives the latest state; summarize in a sentence or two, never read ids or logs aloud.
185
+ 4. **Changing things**: each write tool, when to use it, and what to confirm first. Approvals and permission answers only with the user's explicit decision.
186
+ 5. **Stopping and undo**: the reverse of every action, where one exists.
187
+ 6. **Limits and errors**: what the tools cannot do, and what common errors mean.
188
+
189
+ ## Rules that keep packs safe
190
+
191
+ - Never make a write tool a `data` query, and never mark a write tool's control as `read`.
192
+ - Every consequential control needs `confirm` text that states the exact effect and count ("Archive {{newsletters/threads|count}} threads?").
193
+ - Controls take fixed arguments (plus settings and query data); no free-form inputs.
194
+ - Keep `forEach` bounded with `maxCalls`; prefer a batch tool when there is one.
195
+ - Give row buttons a `when` that admits only items the button is safe for, and a `confirm` for anything hard to undo. Never put a button on something that lets people in (unlocking, opening a garage door or gate, disarming an alarm).
196
+ - Ask for the narrowest scopes and tokens that make the pack work.
197
+ - Secrets and tokens appear only in `mcp` (`env`, `headers`, `args`); validation rejects them anywhere else.
198
+ - A pack that launches a program or ships a server runs code on the user's computer. Say what it runs before the user enables it, and never enable a pack from a bundle or the library that you have not read.
199
+ - A library pack whose id is not `app.sidebud.*` is a **community** pack: Sidebud did not make or review it. `integrations__install` refuses it until the user has agreed after you told them who made it and what it can do; then pass `userConfirmed: true`.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,155 @@
1
+ // The Guide section: how widgets and packs work, for people (guide.md), and the full pack format as the
2
+ // execution agent reads it (the widget-packs skill). Loaded after manage.js and shares its helpers (esc, render…).
3
+
4
+ let guideTab = 'guide';
5
+ const guideText = { guide: null, reference: null };
6
+ let guideError = null;
7
+
8
+ /** Inline Markdown: code, bold, italics, links (http(s) and in-page only). Input is escaped first. */
9
+ function inlineMarkdown(text) {
10
+ return esc(text)
11
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
12
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
13
+ .replace(/(^|[^*])\*([^*\s][^*]*)\*/g, '$1<em>$2</em>')
14
+ .replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (match, label, href) => {
15
+ const url = href.replace(/&amp;/g, '&');
16
+ if (url.startsWith('#')) return `<a href="#guide/${encodeURIComponent(url.slice(1))}" data-guide-anchor="${esc(url.slice(1))}">${label}</a>`;
17
+ if (/^https:\/\//.test(url)) return `<a href="${esc(url)}" target="_blank" rel="noreferrer">${label}</a>`;
18
+ return label;
19
+ });
20
+ }
21
+
22
+ /** A heading's anchor, as GitHub makes them. */
23
+ const slug = (text) => text.toLowerCase().replace(/[^a-z0-9 -]/g, '').trim().replace(/\s+/g, '-');
24
+
25
+ /**
26
+ * The Markdown the guide and the skill use: headings, paragraphs, lists,
27
+ * tables, fenced code, and images from the page's own /guide/ folder.
28
+ * Everything is escaped; nothing from the text runs.
29
+ */
30
+ function markdownHtml(source) {
31
+ const lines = source.replace(/^---\n[\s\S]*?\n---\n/, '').split('\n');
32
+ const out = [];
33
+ let index = 0;
34
+ const cells = (row) => row.trim().replace(/^\||\|$/g, '').split(/(?<!\\)\|/).map((cell) => cell.trim().replace(/\\\|/g, '|'));
35
+ while (index < lines.length) {
36
+ const line = lines[index];
37
+ if (!line.trim()) { index += 1; continue; }
38
+ const fence = line.match(/^```/);
39
+ if (fence) {
40
+ const code = [];
41
+ index += 1;
42
+ while (index < lines.length && !lines[index].startsWith('```')) code.push(lines[index++]);
43
+ index += 1;
44
+ out.push(`<pre class="guide-code"><code>${esc(code.join('\n'))}</code></pre>`);
45
+ continue;
46
+ }
47
+ const heading = line.match(/^(#{1,4})\s+(.*)$/);
48
+ if (heading) {
49
+ const level = Math.min(heading[1].length + 1, 5);
50
+ out.push(`<h${level} id="guide-${slug(heading[2])}">${inlineMarkdown(heading[2])}</h${level}>`);
51
+ index += 1;
52
+ continue;
53
+ }
54
+ // Images from the page's /guide/ folder; images on consecutive lines (or separated by one blank line) sit side by side.
55
+ const imagePattern = /^!\[([^\]]*)\]\(\/guide\/([a-z0-9-]+\.png)(?:\s+"([^"]*)")?\)$/;
56
+ if (imagePattern.test(line)) {
57
+ const figures = [];
58
+ while (index < lines.length) {
59
+ const image = lines[index].match(imagePattern);
60
+ if (image) {
61
+ // Phone preview shots carry their own device frame.
62
+ const framed = !/^(phone-|draft)/.test(image[2]);
63
+ figures.push(`<figure class="guide-figure ${framed ? 'framed' : ''}"><img src="/guide/${image[2]}" alt="${esc(image[1])}" loading="lazy">${image[3] ? `<figcaption>${inlineMarkdown(image[3])}</figcaption>` : ''}</figure>`);
64
+ index += 1;
65
+ } else if (!lines[index].trim() && imagePattern.test(lines[index + 1] ?? '')) index += 1;
66
+ else break;
67
+ }
68
+ out.push(figures.length > 1 ? `<div class="guide-figures">${figures.join('')}</div>` : figures[0]);
69
+ continue;
70
+ }
71
+ if (line.trim().startsWith('|') && lines[index + 1]?.match(/^\s*\|?\s*:?-{3,}/)) {
72
+ const head = cells(line);
73
+ const rows = [];
74
+ index += 2;
75
+ while (index < lines.length && lines[index].trim().startsWith('|')) rows.push(cells(lines[index++]));
76
+ out.push(`<div class="guide-table"><table><thead><tr>${head.map((cell) => `<th>${inlineMarkdown(cell)}</th>`).join('')}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${inlineMarkdown(cell)}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`);
77
+ continue;
78
+ }
79
+ const bullet = /^\s*[-*]\s+/;
80
+ const numbered = /^\s*\d+\.\s+/;
81
+ if (bullet.test(line) || numbered.test(line)) {
82
+ const ordered = numbered.test(line);
83
+ const pattern = ordered ? numbered : bullet;
84
+ const items = [];
85
+ while (index < lines.length && (pattern.test(lines[index]) || (lines[index].startsWith(' ') && lines[index].trim() && items.length))) {
86
+ if (pattern.test(lines[index])) items.push(lines[index].replace(pattern, ''));
87
+ else items[items.length - 1] += ` ${lines[index].trim()}`;
88
+ index += 1;
89
+ }
90
+ const tag = ordered ? 'ol' : 'ul';
91
+ out.push(`<${tag}>${items.map((item) => `<li>${inlineMarkdown(item)}</li>`).join('')}</${tag}>`);
92
+ continue;
93
+ }
94
+ const paragraph = [];
95
+ while (index < lines.length && lines[index].trim() && !/^(#{1,4}\s|```|\s*[-*]\s|\s*\d+\.\s|!\[|\|)/.test(lines[index])) paragraph.push(lines[index++].trim());
96
+ if (paragraph.length) out.push(`<p>${inlineMarkdown(paragraph.join(' '))}</p>`);
97
+ else index += 1;
98
+ }
99
+ return out.join('\n');
100
+ }
101
+
102
+ /** The Guide section: tabs for the guide and the reference, and a contents list of the open one. */
103
+ function guideView() {
104
+ const text = guideText[guideTab];
105
+ if (text === null && !guideError) void loadGuide();
106
+ const body = guideError ? `<div class="card muted">${esc(guideError)}</div>` : text === null ? '<div class="card muted">Loading…</div>' : markdownHtml(text);
107
+ const headings = text ? [...text.matchAll(/^##\s+(.*)$/gm)].map((match) => match[1]) : [];
108
+ return `<section class="section guide"><div><h2>Guide</h2><p class="lede">How widgets work, how to add and set them up, and how you or your agent can build new ones.</p></div><div class="segmented" role="tablist" aria-label="Guide">${[['guide', 'Guide'], ['reference', 'Pack reference']].map(([id, label]) => `<button role="tab" aria-pressed="${guideTab === id}" aria-selected="${guideTab === id}" data-guide-tab="${id}">${label}</button>`).join('')}</div>${guideTab === 'reference' ? '<p class="hint">The full pack format, exactly as your agent reads it when it builds or changes a pack.</p>' : ''}<div class="guide-layout">${headings.length ? `<nav class="guide-contents" aria-label="On this page">${headings.map((heading) => `<a href="#guide/${encodeURIComponent(slug(heading))}" data-guide-anchor="${esc(slug(heading))}">${esc(heading.replace(/\*\*|`/g, ''))}</a>`).join('')}</nav>` : ''}<article class="guide-body">${body}</article></div></section>`;
109
+ }
110
+
111
+ async function loadGuide() {
112
+ const tab = guideTab;
113
+ try {
114
+ const response = await fetch(tab === 'guide' ? '/guide.md' : '/reference.md', { cache: 'no-store' });
115
+ if (!response.ok) throw new Error(`The ${tab === 'guide' ? 'guide' : 'reference'} could not be loaded (${response.status}).`);
116
+ guideText[tab] = await response.text();
117
+ guideError = null;
118
+ } catch (error) {
119
+ guideError = error.message;
120
+ }
121
+ if (section() === 'guide') {
122
+ render();
123
+ scrollToGuideAnchor();
124
+ }
125
+ }
126
+
127
+ /** `#guide/<anchor>` scrolls to that heading once the text is drawn. */
128
+ function scrollToGuideAnchor() {
129
+ const anchor = decodeURIComponent(location.hash.split('/')[1] || '');
130
+ if (!anchor) return;
131
+ const heading = document.getElementById(`guide-${anchor}`);
132
+ if (heading) heading.scrollIntoView({ block: 'start' });
133
+ // A link from another page names a Guide heading; open the Guide tab once its text has loaded.
134
+ else if (guideTab !== 'guide' && guideText[guideTab] !== null) {
135
+ guideTab = 'guide';
136
+ render();
137
+ }
138
+ }
139
+
140
+ document.addEventListener('click', (event) => {
141
+ const tab = event.target.closest?.('[data-guide-tab]');
142
+ if (tab) {
143
+ guideTab = tab.dataset.guideTab;
144
+ guideError = null;
145
+ history.replaceState(null, '', '#guide');
146
+ render();
147
+ return;
148
+ }
149
+ const anchor = event.target.closest?.('[data-guide-anchor]');
150
+ if (anchor) {
151
+ event.preventDefault();
152
+ history.replaceState(null, '', `#guide/${encodeURIComponent(anchor.dataset.guideAnchor)}`);
153
+ scrollToGuideAnchor();
154
+ }
155
+ });
@@ -0,0 +1,139 @@
1
+ # Widgets on your phone
2
+
3
+ Sidebud's phone home screen is made of **widgets**. Each widget is a **tile** you see at a glance, and a fuller view that opens when you tap it. Everything a widget shows comes from this computer. The phone only draws it.
4
+
5
+ A widget comes from a **widget pack**: a folder on this computer that says which app or service to reach (through an MCP server), what to read for the tile, which buttons you can tap, what to say during calls, and how your agent should use it. Every integration, including the ones in the Sidebud widget library, is a pack you own and can change or share.
6
+
7
+ ![Three widgets on the phone's home screen](/guide/app-tiles.png "Tiles on the phone: a small agents tile, a medium Disk space tile with a badge, and a large Studio agents tile that previews its sessions and board.")
8
+
9
+ ## Add a widget from the library
10
+
11
+ 1. Open **MCP & widgets** and scroll to **Widget library**. Packs whose app is already on this computer are marked **App found**.
12
+ 2. Choose a pack's name (or **What it can do and how it looks**) to see its page before installing: its widget on a phone with sample data (tap it to open it), every tool it may call with whether it only reads or can make changes and what uses it, what it runs on this computer, the keys and accounts it needs, where it connects, and how it is set up.
13
+ 3. Choose **Install**. The pack installs switched off, and this computer's agent sets it up. It finds settings and keys that already exist here, connects accounts, tests the connection, and reports what it needs from you.
14
+ 4. When it reports back, the widget appears on your phone and on **Phone preview**.
15
+
16
+ ![The widget library on MCP & widgets](/guide/library.png "The widget library. Each card says what the pack reaches and what it needs, and Access lists every tool it may call.")
17
+
18
+ **Community packs** are made by other people, not by Sidebud. Sidebud does not review them or support them, and they may not work, may stop working, or may do more than they should. They carry a **Community** badge, and you install one from its page after reading what it can do and ticking that you understand. Your agent and the phone app ask you to confirm too. Every pack, even one made by Sidebud, runs on this computer with the access its page lists, and the tools you turn off on a pack's page are never called.
19
+
20
+ A library pack that has a newer version shows **Update to …**. Updating keeps your settings and keys. If what the pack runs changed, it stays off until setup turns it on again. A pack installed before a format change shows **Reinstall**, which also keeps your settings and keys.
21
+
22
+ ## Ask your agent to build one
23
+
24
+ You can ask for a new widget, or a change to one, by voice or typed chat. For example:
25
+
26
+ - "Make a widget that shows the free space on my drives."
27
+ - "Add a widget for my homelab: which containers are running and which stopped."
28
+ - "Change my Hermes widget to show only the coder agent's sessions."
29
+ - "Put my Trello board's Doing column on a large tile."
30
+
31
+ The agent follows the widget-packs guide (the **Reference** tab). It looks for an existing MCP server for the app, or writes a small one. It tries the server's read-only tools to see real data, then maps that data onto the tile and the opened view. While it builds, it previews the draft on **Phone preview**, where you can see it too. It asks before turning anything on.
32
+
33
+ ![A draft shown on the phone preview](/guide/draft.png "While it builds a pack, your agent previews the draft: first on your home screen (left) and opened (right), with real data and nothing installed.")
34
+
35
+ ## See how it looks
36
+
37
+ **Phone preview** draws your home screen as the app does, with live data from this computer. Tap a tile to open it, and **‹ Home** to go back. Controls are shown there but not run. If more than one phone is paired, choose which phone's layout to show.
38
+
39
+ ![Phone preview: the home screen](/guide/phone-home.png "Phone preview draws the home screen with live data.")
40
+
41
+ ![Phone preview: a widget opened](/guide/phone-open.png "Tap a tile to open it: stats, lists with times, and a board you scroll sideways.")
42
+
43
+ When your agent (or `rva-companion pack preview <folder>`) previews a draft, Phone preview shows two phones: the draft first on your home screen, and the draft opened. Drafts are not installed. The ten newest stay until the companion restarts.
44
+
45
+ Each installed pack also has its own page (**MCP & widgets** → the pack's name), with its widget on a phone beside its settings.
46
+
47
+ On the phone, the same widget opens the same way, and tapping a row opens it:
48
+
49
+ ![A widget opened in the Sidebud app](/guide/app-open.png "The Studio agents widget opened in the Sidebud app.")
50
+
51
+ ![A tapped row in the Sidebud app](/guide/app-thread.png "A tapped session: its latest messages. Ask Sidebud about this hands it to your assistant.")
52
+
53
+ ## Set up and control a pack
54
+
55
+ A pack's page has everything about it:
56
+
57
+ ![A pack's page](/guide/pack-page.png "A pack's page: its settings, permissions, and more on the left, and its widget on a phone on the right.")
58
+
59
+ - **Settings and keys.** Keys are saved in this computer's keychain and never shown again. **Clear** removes one.
60
+ - **Account.** Packs that use an account sign-in open the provider's consent page in this computer's browser. Tokens stay in the keychain; **Disconnect** forgets them here.
61
+ - **Permissions.** A switch for every tool the pack declares, marked **reads** or **changes things**. A tool you turn off is never called: not by the widget, a control, or your agent.
62
+ - **Refresh.** How often the widget reads new data.
63
+ - **Spoken updates.** Whether the voice agent mentions this widget's news during calls: something finished, failed, or needs you, or new items. Each pack has its own On or Off; packs about agents start on, others start off.
64
+ - **Controls.** Buttons the pack offers, such as "Read my agenda". Each phone chooses which appear, from the widget's **Controls** button. Controls that change things ask first.
65
+
66
+ ![Permissions for each tool](/guide/permissions.png "A switch for every tool. Turned off, a tool is never called by the widget, a control, or your agent.")
67
+
68
+ ![Refresh, spoken updates, and controls](/guide/widget-voice.png "Refresh, spoken updates, and controls for one pack.")
69
+
70
+ ![A widget with a control in the Sidebud app](/guide/app-disk.png "A control on the phone: What can I clean up? sends that question to your assistant.")
71
+ - **What it runs**, its setup steps, and the **skill** your agent reads when the pack is on.
72
+
73
+ **Turn off** stops a pack without removing it. **Customize a copy** makes your own version under a new id. **Export** writes a file you can share. **Remove** uninstalls it and deletes its keys and tokens.
74
+
75
+ ## Arrange the phone
76
+
77
+ On the phone, hold a tile to move it, resize it, or take it off the home screen. **Phone layouts** shows each paired phone's arrangement, and you can change it here too. Your agent can also arrange tiles when you ask ("move Calendar to the top", "make Hermes large").
78
+
79
+ ![Arranging tiles on the phone](/guide/app-arrange.png "Hold a tile to arrange: drag it to move it, drag its corner to resize it, and − takes it off the home screen (Add widgets brings it back). Done saves.")
80
+
81
+ ![Phone layouts](/guide/layouts.png "Phone layouts: each paired phone's tiles, their size, order, and visibility.")
82
+
83
+ ## Build one yourself
84
+
85
+ A pack is a folder:
86
+
87
+ | File | What it is |
88
+ | --- | --- |
89
+ | `pack.json` | The manifest: identity, the MCP server to run or reach, tools with their access, data queries, and what the tile and opened view show. |
90
+ | `SKILL.md` | How an agent should use the pack's tools. |
91
+ | `logo.png` | The app's real logo (≤128 KB), with its `source` link. Or a built-in icon or an emoji. |
92
+ | `server/` | Optional: a small MCP server the pack ships, when none exists. Plain scripts, no install step. |
93
+
94
+ ### The building blocks
95
+
96
+ | Part | What it shows |
97
+ | --- | --- |
98
+ | Tile size | `small` (one cell), `medium` (a row), or `large` (two rows, with previews). |
99
+ | Glance | Up to two stats ("Unread · 12"), or one line, under the tile's title. |
100
+ | Badge | A small count or word on the tile, hidden when empty or 0. |
101
+ | Subtitle | A live line under the title when the widget is open. |
102
+ | Stats block | Up to four numbers with labels. |
103
+ | List block | Rows with a title, subtitle, a status in a color, and a time ("3m ago", "in 12m"). Give each list a title when there are several. |
104
+ | Board block | Items in columns, like a kanban board, each column with its count. |
105
+ | Text block | A paragraph. |
106
+ | Detail | What opens when a row is tapped: the item's latest messages, read by one of the pack's tools. |
107
+ | Controls | Buttons that say something to your assistant, or run one of the pack's tools. |
108
+ | Spoken updates | Rules that announce changes ("an agent finished", "a task is blocked"). |
109
+
110
+ A **large** tile previews its first list's top rows, or with two lists or boards, each one's heading and top row (a board shows its column counts).
111
+
112
+ Values come from **templates**, which are lookups and never code: `{{unread/count}}` reads a query's result, `{{item/title}}` reads a row, and filters like `|count`, `|default:0`, `|truncate:60`, and `|sort:-updatedAt` shape them. Data queries only ever call tools that are declared as reads, and a tool the server marks as writing is never polled.
113
+
114
+ ### From the terminal
115
+
116
+ ```
117
+ rva-companion pack tools <folder> # the server's tools and whether they only read
118
+ rva-companion pack try <folder> <tool> [json] # call one read-only tool and see its result
119
+ rva-companion pack preview <folder> # render the widget and show it on Phone preview
120
+ rva-companion pack validate <folder> # check everything
121
+ rva-companion pack install <folder> # install it (switched off); --replace to update
122
+ ```
123
+
124
+ The **Reference** tab has the full format, the rules for pack servers, and a worked example.
125
+
126
+ ## Agent widgets
127
+
128
+ Besides packs, your agent can put a simple widget of its own on the phone ("add a widget with my trip plan"). It has fixed content and buttons that send a prompt back to your assistant, and no server. Ask your agent to change or remove it.
129
+
130
+ ## Share
131
+
132
+ **Export** on a pack's page writes a pack file. It never includes settings, keys, or account tokens. Choose a pack file under **Import a widget pack** on MCP & widgets to add one someone sent you. It installs switched off, and your agent sets it up. The Sidebud widget library lists packs anyone can install.
133
+
134
+ ## When something is wrong
135
+
136
+ - **Needs setup** on a tile: open the pack's page and choose **Set up with my agent**, or fill in what it lists.
137
+ - **An error in the widget**: the pack's server or account had a problem, and the widget shows its last good data with the error. **Check connection** on the pack's page tests it. Turning off a tool the widget reads also shows an error there.
138
+ - **The library is empty**: this computer cannot reach the widget library. Packs you already installed keep working.
139
+ - **A widget looks different on the phone than here**: update the Sidebud app on the phone. An older app skips widget parts it does not know yet.
@@ -35,14 +35,26 @@
35
35
  ><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 8 4 4-4 4M12 17h7" /></svg
36
36
  >Execution</a
37
37
  >
38
+ <a href="#decisions"
39
+ ><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13 2.5 4.5 13.5H12L11 21.5l8.5-11H12l1-8Z" /></svg
40
+ >Fast decisions<span class="tag-beta">Beta</span></a
41
+ >
38
42
  <a href="#mcp"
39
43
  ><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="7" height="7" rx="1.5" /><rect x="13.5" y="3.5" width="7" height="7" rx="1.5" /><rect x="3.5" y="13.5" width="7" height="7" rx="1.5" /><path d="M17 14v6M14 17h6" /></svg
40
44
  >MCP &amp; widgets</a
41
45
  >
46
+ <a href="#phone"
47
+ ><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6.5" y="2.5" width="11" height="19" rx="2.5" /><path d="M10.5 18.5h3" /></svg
48
+ >Phone preview</a
49
+ >
42
50
  <a href="#layouts"
43
51
  ><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="2.5" /><path d="M3.5 10h17M10 10v10.5" /></svg
44
52
  >Phone layouts</a
45
53
  >
54
+ <a href="#guide"
55
+ ><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3H20v15H6.5A2.5 2.5 0 0 0 4 20.5v-15Z" /><path d="M4 20.5A2.5 2.5 0 0 1 6.5 18H20v3H6.5A2.5 2.5 0 0 1 4 20.5ZM8 7h8M8 10.5h6" /></svg
56
+ >Guide</a
57
+ >
46
58
  <a href="#settings" class="nav-end"
47
59
  ><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z" /></svg
48
60
  >Settings</a
@@ -87,5 +99,7 @@
87
99
  <script src="/manage.js" defer></script>
88
100
  <script src="/setup.js" defer></script>
89
101
  <script src="/settings.js" defer></script>
102
+ <script src="/packs.js" defer></script>
103
+ <script src="/guide.js" defer></script>
90
104
  </body>
91
105
  </html>