sidebud 0.2.0 → 0.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidebud",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "description": "Sidebud companion: talk to the agents on your computer from your phone.",
5
5
  "keywords": [
6
6
  "sidebud",
@@ -33,9 +33,11 @@
33
33
  "@modelcontextprotocol/sdk": "^1.30.0",
34
34
  "@noble/curves": "^2.0.1",
35
35
  "@noble/hashes": "^2.0.1",
36
+ "@sentry/node": "10.75.3",
36
37
  "diff": "^8.0.4",
37
38
  "qrcode": "^1.5.4",
38
39
  "ws": "^8.18.3",
40
+ "yaml": "^2.9.1",
39
41
  "zod": "^4.5.4"
40
42
  }
41
43
  }
@@ -20,7 +20,7 @@ A pack folder holds:
20
20
 
21
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
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.
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. Declare **every** tool it lists in `permissions.tools`, not only the ones the widget uses: the owner sees everything the pack can do, and the fast path and the execution agent can then use any of it. `packs__preview` and the connection check report tools the pack leaves out.
24
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
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
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.
@@ -43,26 +43,26 @@ A pack folder holds:
43
43
  | `name`, `description`, `version`, `author`, `license` | Shown before install. `version` is semver. |
44
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
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}}`. |
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
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
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
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). 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. |
50
+ | `permissions.tools` | **Every** tool the server offers (nothing undeclared is ever called, and a pack that leaves some out fails its connection check), 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. A server whose tool list depends on the user's setup (one tool per device kind or user script) sets `permissions.toolsVary: true` and declares the tools every setup has; the ones it adds are never called. Nothing else is ever called. `permissions.network` lists hosts, for review. `permissions.reach` declares what the pack reaches beyond its tools, shown first when someone adds or turns it on: `computer` when its tools start agents, run commands, or otherwise act anywhere on the computer (an agents app, a shell, computer control); `files` for the user's files; `messages` for messages it reads or sends as the user; `devices` for home devices. Declare every one that applies; understating it gets a pack rejected in library 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
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
53
  | `skill` | `{ "path": "SKILL.md" }`. |
54
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
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`, or `records` ("Key: value" blocks). |
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
57
  | `glance` | `{ "stats": [≤2 × { "label", "value", "tone" }] }` or `{ "line", "tone" }`. Tones: neutral, info, success, warning, danger. |
58
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`; `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. |
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
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), `messages` (`{{detail/…}}`, the result's message array), and `message.role/text/at`. |
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
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
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
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`, and `|sort:-updatedAt` (comma-separated keys, `-` descending, `field=value` first). A string that is exactly one template yields the raw value, so `"ids": "{{unread/threads|pluck:id}}"` passes an array.
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
66
 
67
67
  ## Writing a pack server
68
68
 
@@ -192,6 +192,7 @@ Every pack gets a `SKILL.md` with frontmatter (`name`, `description`) and these
192
192
  - Every consequential control needs `confirm` text that states the exact effect and count ("Archive {{newsletters/threads|count}} threads?").
193
193
  - Controls take fixed arguments (plus settings and query data); no free-form inputs.
194
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).
195
196
  - Ask for the narrowest scopes and tokens that make the pack work.
196
197
  - Secrets and tokens appear only in `mcp` (`env`, `headers`, `args`); validation rejects them anywhere else.
197
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.
@@ -19,6 +19,10 @@ A widget comes from a **widget pack**: a folder on this computer that says which
19
19
 
20
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
21
 
22
+ Sidebud installs a library pack only when the library's signature checks out and its files match what was signed. If an update lets a pack do more (a new tool, a tool that now changes things, a new address, key, or account access, or new server code), the page lists those changes and asks you first; your agent can never approve that for you. An update never replaces a pack you edited on this computer: customize a copy to keep your changes. After an update, **Roll back** returns to the version it replaced.
23
+
24
+ If Sidebud pulls a release from the library (for example, it turned out to be unsafe), a computer with that version turns the pack off, says why on this page and in your phone's conversation, and keeps it off until you update or roll back.
25
+
22
26
  ## Ask your agent to build one
23
27
 
24
28
  You can ask for a new widget, or a change to one, by voice or typed chat. For example:
@@ -136,4 +140,5 @@ Besides packs, your agent can put a simple widget of its own on the phone ("add
136
140
  - **Needs setup** on a tile: open the pack's page and choose **Set up with my agent**, or fill in what it lists.
137
141
  - **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
142
  - **The library is empty**: this computer cannot reach the widget library. Packs you already installed keep working.
143
+ - **Sidebud did not install a pack**: its download failed the signature or file check. Try again later; if it keeps failing, update the companion.
139
144
  - **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,6 +35,10 @@
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
@@ -406,6 +406,47 @@ button.theme-toggle {
406
406
  background: var(--amber-soft);
407
407
  color: var(--amber-text);
408
408
  }
409
+ .label-text {
410
+ display: inline-flex;
411
+ flex-wrap: wrap;
412
+ align-items: center;
413
+ gap: 8px;
414
+ }
415
+ /* A setting's source (ADR 0014): Account, This computer, or Default, and its pin toggle. */
416
+ .badge.source {
417
+ font-size: 11px;
418
+ padding: 4px 8px;
419
+ }
420
+ .badge.source.account {
421
+ background: var(--amber-soft);
422
+ color: var(--amber-text);
423
+ }
424
+ .badge.source.override {
425
+ background: var(--green-soft);
426
+ color: var(--green);
427
+ }
428
+ button.link.pin-toggle {
429
+ font-size: 12px;
430
+ font-weight: 500;
431
+ color: var(--muted);
432
+ }
433
+ button.link.pin-toggle:hover {
434
+ color: var(--text);
435
+ }
436
+ .tag-beta {
437
+ display: inline-block;
438
+ margin-left: 8px;
439
+ padding: 2px 7px;
440
+ border-radius: 999px;
441
+ border: 1px solid var(--amber-text);
442
+ color: var(--amber-text);
443
+ font-size: 10px;
444
+ font-weight: 700;
445
+ letter-spacing: 0.06em;
446
+ line-height: 1.4;
447
+ text-transform: uppercase;
448
+ vertical-align: middle;
449
+ }
409
450
  .badge.bad {
410
451
  background: var(--red-soft);
411
452
  color: var(--red);
@@ -1033,6 +1074,16 @@ a.button {
1033
1074
  a.button:hover {
1034
1075
  background: var(--amber-hover);
1035
1076
  }
1077
+ /* A link that leads to a review step rather than acting (a full-control or community pack): not the page's call to action. */
1078
+ a.button.quiet {
1079
+ background: var(--raised);
1080
+ border: 1px solid var(--border);
1081
+ color: var(--text);
1082
+ text-align: center;
1083
+ }
1084
+ a.button.quiet:hover {
1085
+ background: var(--hover);
1086
+ }
1036
1087
 
1037
1088
  /* Setup */
1038
1089
  .setup-steps {
@@ -1226,6 +1277,24 @@ ol.steps {
1226
1277
  margin: 0;
1227
1278
  padding-left: 20px;
1228
1279
  }
1280
+ /* Account settings sync */
1281
+ .sync-block {
1282
+ border-top: 1px solid var(--border-soft);
1283
+ padding-top: 14px;
1284
+ }
1285
+ .sync-list {
1286
+ margin: 4px 0 8px;
1287
+ padding-left: 20px;
1288
+ }
1289
+ .sync-conflict {
1290
+ border-top: 1px solid var(--border-soft);
1291
+ padding-top: 12px;
1292
+ }
1293
+ .sync-conflict .hint {
1294
+ margin: 0;
1295
+ overflow-wrap: anywhere;
1296
+ }
1297
+
1229
1298
  .segmented {
1230
1299
  display: inline-flex;
1231
1300
  gap: 4px;
@@ -1406,6 +1475,24 @@ details.menu .menu-list a.button:hover {
1406
1475
  overflow: hidden;
1407
1476
  white-space: nowrap;
1408
1477
  }
1478
+ .phone-row-button {
1479
+ flex: 0 0 auto;
1480
+ align-self: center;
1481
+ border-radius: 10px;
1482
+ padding: 4px 10px;
1483
+ font-size: 12px;
1484
+ font-weight: 600;
1485
+ line-height: 1.4;
1486
+ background: var(--phone-raised);
1487
+ }
1488
+ /* A row with a button: the text takes the room, the status and button sit at the end. */
1489
+ .phone-list > div:has(> .phone-row-button) > div:first-child {
1490
+ flex: 1 1 auto;
1491
+ min-width: 0;
1492
+ }
1493
+ .phone-list > div:has(> .phone-row-button) {
1494
+ align-items: center;
1495
+ }
1409
1496
  .phone-row-end {
1410
1497
  flex: 0 0 auto;
1411
1498
  white-space: nowrap;
@@ -1988,3 +2075,45 @@ pre.skill {
1988
2075
  gap: 8px;
1989
2076
  font-weight: 500;
1990
2077
  }
2078
+
2079
+ /* How much a pack can control (packAccessOverview): shown first before it is added or turned on. */
2080
+ .access-overview {
2081
+ margin: 16px 0;
2082
+ }
2083
+ .access-overview.level-computer {
2084
+ border-color: var(--red);
2085
+ background: var(--red-soft);
2086
+ }
2087
+ .access-overview.level-computer h3 {
2088
+ color: var(--red);
2089
+ }
2090
+ .access-points {
2091
+ display: grid;
2092
+ gap: 8px;
2093
+ margin: 0;
2094
+ padding-left: 20px;
2095
+ font-size: 14px;
2096
+ line-height: 1.5;
2097
+ }
2098
+ .access-points li.danger {
2099
+ color: var(--text);
2100
+ font-weight: 600;
2101
+ }
2102
+ .access-points li.danger::marker {
2103
+ color: var(--red);
2104
+ }
2105
+ .access-points li.caution::marker {
2106
+ color: var(--amber-text);
2107
+ }
2108
+ .access-points li.info {
2109
+ color: var(--muted);
2110
+ }
2111
+ .access-line {
2112
+ display: grid;
2113
+ justify-items: start;
2114
+ gap: 6px;
2115
+ margin-top: 6px;
2116
+ }
2117
+ .access-line:empty {
2118
+ display: none;
2119
+ }