@spencerboucher/switchyard 0.0.0-stage → 0.3.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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -2
  3. package/dist/bridge/switchyard-bridge.js +105 -0
  4. package/dist/switchyard.mjs +18207 -0
  5. package/package.json +52 -4
  6. package/web-dist/apple-touch-icon.png +0 -0
  7. package/web-dist/assets/AutomationView-Bt_v8JDx.js +2 -0
  8. package/web-dist/assets/AutomationView-Bt_v8JDx.js.map +1 -0
  9. package/web-dist/assets/InteractionCard-BRI9KgrO.js +4 -0
  10. package/web-dist/assets/InteractionCard-BRI9KgrO.js.map +1 -0
  11. package/web-dist/assets/Markdown-jXkvZeRp.js +31 -0
  12. package/web-dist/assets/Markdown-jXkvZeRp.js.map +1 -0
  13. package/web-dist/assets/Menu-Cqil5qVF.js +2 -0
  14. package/web-dist/assets/Menu-Cqil5qVF.js.map +1 -0
  15. package/web-dist/assets/Overlay-B9h5oAe6.js +2 -0
  16. package/web-dist/assets/Overlay-B9h5oAe6.js.map +1 -0
  17. package/web-dist/assets/SegmentedControl-ChdVilXb.js +2 -0
  18. package/web-dist/assets/SegmentedControl-ChdVilXb.js.map +1 -0
  19. package/web-dist/assets/TextArea-BsQHOQMe.js +2 -0
  20. package/web-dist/assets/TextArea-BsQHOQMe.js.map +1 -0
  21. package/web-dist/assets/arrow-left-C6YWM6y6.js +2 -0
  22. package/web-dist/assets/arrow-left-C6YWM6y6.js.map +1 -0
  23. package/web-dist/assets/attention-VkuGuoSf.js +2 -0
  24. package/web-dist/assets/attention-VkuGuoSf.js.map +1 -0
  25. package/web-dist/assets/automations-BY88rXFv.js +2 -0
  26. package/web-dist/assets/automations-BY88rXFv.js.map +1 -0
  27. package/web-dist/assets/board-OzxtnPgh.js +10 -0
  28. package/web-dist/assets/board-OzxtnPgh.js.map +1 -0
  29. package/web-dist/assets/calendar-clock-B8zE4JsX.js +2 -0
  30. package/web-dist/assets/calendar-clock-B8zE4JsX.js.map +1 -0
  31. package/web-dist/assets/chevron-right-evVLS6hq.js +2 -0
  32. package/web-dist/assets/chevron-right-evVLS6hq.js.map +1 -0
  33. package/web-dist/assets/code-BR4PKt8F.js +8 -0
  34. package/web-dist/assets/code-BR4PKt8F.js.map +1 -0
  35. package/web-dist/assets/copy-oWgGHmkS.js +2 -0
  36. package/web-dist/assets/copy-oWgGHmkS.js.map +1 -0
  37. package/web-dist/assets/describe-CRXLn3O6.js +5 -0
  38. package/web-dist/assets/describe-CRXLn3O6.js.map +1 -0
  39. package/web-dist/assets/dist-C4YmDHPG.js +56 -0
  40. package/web-dist/assets/dist-C4YmDHPG.js.map +1 -0
  41. package/web-dist/assets/enroll-C9-fBIKO.js +2 -0
  42. package/web-dist/assets/enroll-C9-fBIKO.js.map +1 -0
  43. package/web-dist/assets/external-link-3jjTxo92.js +2 -0
  44. package/web-dist/assets/external-link-3jjTxo92.js.map +1 -0
  45. package/web-dist/assets/hand-UPoHamrn.js +2 -0
  46. package/web-dist/assets/hand-UPoHamrn.js.map +1 -0
  47. package/web-dist/assets/index-B11DJFHW.js +11 -0
  48. package/web-dist/assets/index-B11DJFHW.js.map +1 -0
  49. package/web-dist/assets/index-DGGOgcLZ.css +2 -0
  50. package/web-dist/assets/jsx-runtime-D13aM9HY.js +2 -0
  51. package/web-dist/assets/jsx-runtime-D13aM9HY.js.map +1 -0
  52. package/web-dist/assets/key-round-VDbE9E4X.js +2 -0
  53. package/web-dist/assets/key-round-VDbE9E4X.js.map +1 -0
  54. package/web-dist/assets/link-2-CaMlUDRG.js +2 -0
  55. package/web-dist/assets/link-2-CaMlUDRG.js.map +1 -0
  56. package/web-dist/assets/logic-D_Eq9DMW.js +2 -0
  57. package/web-dist/assets/logic-D_Eq9DMW.js.map +1 -0
  58. package/web-dist/assets/more-BUvq82hI.js +2 -0
  59. package/web-dist/assets/more-BUvq82hI.js.map +1 -0
  60. package/web-dist/assets/now-DR2wt1DG.js +2 -0
  61. package/web-dist/assets/now-DR2wt1DG.js.map +1 -0
  62. package/web-dist/assets/pencil-DI8QbCN8.js +2 -0
  63. package/web-dist/assets/pencil-DI8QbCN8.js.map +1 -0
  64. package/web-dist/assets/play-CjoDg3ws.js +2 -0
  65. package/web-dist/assets/play-CjoDg3ws.js.map +1 -0
  66. package/web-dist/assets/plus-BLP-wIYD.js +2 -0
  67. package/web-dist/assets/plus-BLP-wIYD.js.map +1 -0
  68. package/web-dist/assets/presence-05bJD5G3.js +2 -0
  69. package/web-dist/assets/presence-05bJD5G3.js.map +1 -0
  70. package/web-dist/assets/save-DEvZPWlS.js +2 -0
  71. package/web-dist/assets/save-DEvZPWlS.js.map +1 -0
  72. package/web-dist/assets/search-C6cn5WOo.js +2 -0
  73. package/web-dist/assets/search-C6cn5WOo.js.map +1 -0
  74. package/web-dist/assets/send-CFwekPNi.js +2 -0
  75. package/web-dist/assets/send-CFwekPNi.js.map +1 -0
  76. package/web-dist/assets/server-SxFxIScj.js +2 -0
  77. package/web-dist/assets/server-SxFxIScj.js.map +1 -0
  78. package/web-dist/assets/settings-C4tDpSCU.js +7 -0
  79. package/web-dist/assets/settings-C4tDpSCU.js.map +1 -0
  80. package/web-dist/assets/states-DrTQuYXw.js +2 -0
  81. package/web-dist/assets/states-DrTQuYXw.js.map +1 -0
  82. package/web-dist/assets/trash-C6X2OLrI.js +2 -0
  83. package/web-dist/assets/trash-C6X2OLrI.js.map +1 -0
  84. package/web-dist/assets/useNow-ChB9z-N6.js +2 -0
  85. package/web-dist/assets/useNow-ChB9z-N6.js.map +1 -0
  86. package/web-dist/icon-192.png +0 -0
  87. package/web-dist/icon-512.png +0 -0
  88. package/web-dist/icon.svg +12 -0
  89. package/web-dist/index.html +25 -0
  90. package/web-dist/manifest.webmanifest +1 -0
  91. package/web-dist/maskable-512.png +0 -0
  92. package/web-dist/registerSW.js +1 -0
  93. package/web-dist/sw.js +2 -0
  94. package/web-dist/sw.js.map +1 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Spencer Boucher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,162 @@
1
- # Temporary Holding Version
1
+ # Switchyard
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Run [Pi](https://github.com/earendil-works/pi) coding agents on your own machines, and drive them from your phone, your browser or your terminal.**
4
+
5
+ A coding agent has to run where your repositories, toolchains and logins are: your laptop, a workstation, a home server. You, on the other hand, are not always sitting in front of that machine. Switchyard keeps the agents where the work is and lets you follow along from anywhere: start a session from your phone, answer the question it asks while you are on your laptop, take over from a terminal, and get a notification when the result is ready for review. Every client looks at the same live session, and closing one never stops the work.
6
+
7
+ On top of interactive sessions it adds tasks, a board, scheduled automations and a review flow, so an agent can work through a backlog while you are away and hand everything back for you to accept.
8
+
9
+ ## How it fits together
10
+
11
+ ```mermaid
12
+ flowchart TB
13
+ subgraph clients["Clients: windows onto the same work"]
14
+ phone["Phone<br/>(installable web app)"]
15
+ browser["Desktop browser"]
16
+ term["Terminal client and CLI"]
17
+ end
18
+
19
+ coord["<b>Coordinator</b><br/>tasks, schedules, notifications,<br/>sign-in, sync<br/><i>never runs code</i>"]
20
+
21
+ subgraph hostA["Host: your laptop"]
22
+ agentA["Host agent"] --> piA["Pi sessions"]
23
+ piA --> repoA[("Repositories,<br/>worktrees, logins")]
24
+ end
25
+
26
+ subgraph hostB["Host: your server"]
27
+ agentB["Host agent"] --> piB["Pi sessions"]
28
+ piB --> repoB[("Repositories,<br/>worktrees, logins")]
29
+ end
30
+
31
+ phone & browser & term <-->|"HTTPS + WebSocket"| coord
32
+ agentA -->|"dials out"| coord
33
+ agentB -->|"dials out"| coord
34
+ ```
35
+
36
+ There are three kinds of parts:
37
+
38
+ - **The coordinator** is the one always-on service. It keeps tasks, schedules, notifications, devices and the shared view of every session, and it decides what runs where. It never runs an agent and never holds your provider credentials, so it can live anywhere: a container, a small server, or your laptop.
39
+ - **Hosts** are your machines. A host agent runs Pi as your OS user, with your repositories, worktrees, Pi packages and model logins, and connects *outbound* to the coordinator. A host needs no open ports, and a laptop can come and go: its sessions keep running while the coordinator is down, and the events catch up when it returns.
40
+ - **Clients** are views. The web app works on desktop and as an installed app on a phone; the terminal client and the scriptable CLI work over the same API. Any client can watch, answer and steer a session; none of them owns it.
41
+
42
+ For a single machine, `switchyard up` runs the coordinator and a host in one process. For more, run the coordinator as a server and enroll each machine as a host. The protocol is the same either way.
43
+
44
+ ## A piece of work, start to finish
45
+
46
+ ```mermaid
47
+ sequenceDiagram
48
+ actor You
49
+ participant Phone
50
+ participant Laptop as Laptop browser
51
+ participant Coord as Coordinator
52
+ participant Host as Host (Pi)
53
+
54
+ You->>Phone: Create a task, make it ready
55
+ Phone->>Coord: Task: Ready
56
+ Coord->>Host: Dispatch (once, even across restarts)
57
+ Host->>Host: Create a git worktree, start Pi
58
+ Host-->>Coord: A plugin asks a question
59
+ Coord-->>Phone: Notification: needs input
60
+ You->>Laptop: Answer
61
+ Laptop->>Coord: Answer
62
+ Coord->>Host: Deliver the answer to Pi
63
+ Host-->>Coord: Turn finished, checks pass
64
+ Coord-->>Phone: Ready for review
65
+ You->>Phone: Review the diff, accept
66
+ ```
67
+
68
+ 1. **You describe the work.** A task has an objective, acceptance criteria, dependencies and a priority. Moving it to Ready lets Switchyard start it.
69
+ 2. **Switchyard starts it on a host.** The coordinator picks an eligible host, the host creates a git worktree for the task inside the repository (`<repo>/.worktrees/<branch>` by default), and Pi starts there with your normal extensions. A launch is recorded before it is sent, so a lost connection or a coordinator restart never starts it twice.
70
+ 3. **The agent asks, you answer anywhere.** Questions and approvals from Pi and its plugins appear as cards in every client and as a notification on the device you are most likely to see. Answer once; every client shows the result.
71
+ 4. **You can step in.** Join from any client to watch and send messages, steer the current turn or queue a follow-up. *Take over* pauses automation (no automatic continuations or retries) until you *return it*. *Stop* aborts the run. Detaching never stops anything.
72
+ 5. **It ends in Review, not in Done.** When the agent finishes, your configured checks run (tests, linters). If they pass, the task waits in Review with its diff; you can comment line by line, send all comments as one follow-up, request changes or accept. Accepting never merges or pushes for you.
73
+ 6. **Background work uses the same path.** An automation can open a task on a schedule, from a webhook or when another task finishes, then continue it, append to it, or report only when something changed.
74
+
75
+ ## Features
76
+
77
+ | Area | What you get |
78
+ |---|---|
79
+ | **Sessions** | Persistent Pi conversations on any host; the same live session in every client; steer or queue follow-ups; take over and return to automation; resume after crashes with the same conversation (never a silent new one); import existing Pi sessions |
80
+ | **Workspaces** | A git worktree per task or session inside the repository, discovery of worktrees you made yourself, setup commands, and removal that refuses to lose uncommitted or unmerged work |
81
+ | **Tasks and board** | Backlog, Ready, In progress, Review, Done; dependencies with cycle detection; priorities; per-project concurrency limits; a Now view explaining why queued work waits |
82
+ | **Review** | Changed files and diffs on desktop and phone, line comments sent as one follow-up, request changes, accept |
83
+ | **Bounded continuation** | Optional: keep the agent working toward an objective, within a maximum number of continuations, a time limit and token or cost caps, ending in your checks and your review |
84
+ | **Automations** | Cron (time-zone aware), one-time, webhook and dependency triggers; fresh, continue or watch modes; skip, queue or append when work overlaps; skip or catch up missed runs; a preview of the resulting task |
85
+ | **Plugins** | Pi dialogs (select, confirm, input, editor) and extension status and widgets shown in every client; adapters turn specific plugins into structured forms, cards and panels, and plugin packages can ship their own. Terminal-only plugin UI is reported honestly instead of silently dropped |
86
+ | **Attention and notifications** | One queue of what needs you (input, failures, reviews); Web Push routed to the device you are using, quiet hours, private previews, one notification per session |
87
+ | **Sign-in and devices** | Pair devices by QR code, sign in with passkeys or any OpenID Connect provider; every device has its own revocable credential |
88
+ | **Operations** | One container image, SQLite with migrations, backup and restore, health checks, diagnostics, `switchyard doctor` |
89
+
90
+ ## Words used everywhere
91
+
92
+ | Term | Meaning |
93
+ |---|---|
94
+ | **Host** | A machine that runs Pi for Switchyard, through its host agent |
95
+ | **Project** | A repository Switchyard knows about; a project can have a checkout on several hosts |
96
+ | **Workspace** | A working directory for a project: the main checkout or a git worktree |
97
+ | **Session** | One persistent Pi conversation in a workspace on a host |
98
+ | **Task** | A unit of work on the board, with an objective and acceptance criteria |
99
+ | **Run** | One attempt at a task, carried out in a session |
100
+ | **Device** | A signed-in client: a browser, an installed web app or a terminal |
101
+ | **Adapter** | Code that renders a specific Pi plugin's questions and state as structured UI |
102
+
103
+ ## Quick start
104
+
105
+ **Try it on one machine** (Node 24 or newer, git, and Pi 1.4 or 1.5 on your PATH):
106
+
107
+ ```sh
108
+ npm install -g @spencerboucher/switchyard
109
+ switchyard up
110
+ ```
111
+
112
+ `up` runs the coordinator and a host in one process at `http://localhost:7400`, sets the server up on first start and opens a single-use sign-in link in your browser. Add a project in Settings › Projects (a path to a git repository), then start a session or create a task.
113
+
114
+ **Run it as a server** with the container image (it runs only the coordinator):
115
+
116
+ ```sh
117
+ docker run -d --name switchyard -p 127.0.0.1:7400:7400 -v switchyard-data:/data \
118
+ -e SWITCHYARD_PUBLIC_URL=https://switchyard.example.com \
119
+ ghcr.io/justmytwospence/switchyard:0.3.0
120
+ docker exec switchyard switchyard init # prints a single-use sign-in link for your first device
121
+ ```
122
+
123
+ Put HTTPS in front of it (a reverse proxy, or a VPN or tailnet; the installed phone app, notifications and passkeys need HTTPS). Then, on each machine that should run agents, create a host token in Settings › Hosts › Add host and run:
124
+
125
+ ```sh
126
+ npm install -g @spencerboucher/switchyard
127
+ switchyard host enroll --coordinator https://switchyard.example.com --token <token>
128
+ switchyard host install-service # launchd on macOS, a systemd user service on Linux
129
+ ```
130
+
131
+ Settings › Hosts › Add host shows these three commands with your token filled in. To build from source instead, see [docs/development.md](docs/development.md).
132
+
133
+ [docs/deployment.md](docs/deployment.md) has recipes for Caddy, Traefik, nginx and Tailscale, a compose file, and every setting.
134
+
135
+ ## Security model
136
+
137
+ - **A server is owned before it serves anything.** Ownership comes from `switchyard init` on the server, or from an owner declared in an OpenID Connect configuration. There is never an "unclaimed" web page.
138
+ - **Only you add devices.** A new device gets in by a pairing link or QR code made from a device you already use, by a passkey, or by single sign-on that matches the configured owner. Each device has its own credential that you can revoke; signing out of an identity provider never breaks an installed app.
139
+ - **Hosts dial out** with their own revocable credentials; the coordinator holds no provider credentials and needs no access to your home directory or Docker socket.
140
+ - **Agents run with the host user's privileges.** Running the coordinator in a container does not sandbox the agents; run hosts as a user whose access you are comfortable giving an agent.
141
+
142
+ Details: [docs/auth.md](docs/auth.md) and the [privilege model](docs/deployment.md#privilege-model).
143
+
144
+ ## Status
145
+
146
+ License: [MIT](LICENSE).
147
+
148
+
149
+ Switchyard is young and moving quickly. It is tested against Pi `>=1.4.0 <1.6.0` (see [docs/compatibility.md](docs/compatibility.md) for supported and unsupported Pi capabilities) and runs on macOS and Linux hosts. What has and has not been verified is recorded in [docs/acceptance-report.md](docs/acceptance-report.md).
150
+
151
+ ## Documentation
152
+
153
+ | Guide | Covers |
154
+ |---|---|
155
+ | [Deployment](docs/deployment.md) | Single machine, server, reverse proxies, VPNs, hosts, environment reference, privilege model |
156
+ | [Sign-in and devices](docs/auth.md) | The owner model, pairing, passkeys, single sign-on, host enrollment, revocation |
157
+ | [CLI and terminal client](docs/cli.md) | Every `switchyard` command, credentials, terminal key bindings |
158
+ | [Operations](docs/operations.md) | Backup, restore, migrations, retention, logs, health, diagnostics |
159
+ | [Adapters](docs/adapters.md) | Rendering a Pi plugin's UI in every client; shipping an adapter with your Pi package |
160
+ | [Architecture](docs/architecture.md) | Components, data ownership, the host and client protocols, packages |
161
+ | [Development](docs/development.md) | Building, testing (including against real Pi), contributing |
162
+ | [Specification](docs/spec.md) | The product specification Switchyard is built to |
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Switchyard bridge extension, loaded into every managed Pi with `-e`.
3
+ *
4
+ * Forwards `ui_prompt_start` / `ui_prompt_end` (kinds select|confirm|input|editor|custom) as JSON
5
+ * lines over the Unix socket named by `SWITCHYARD_BRIDGE_SOCKET`. This reveals terminal-only
6
+ * `ctx.ui.custom()` calls (which RPC mode answers with `undefined`) and dialogs that Pi resolved
7
+ * itself (timeout or abort). It imports only Node built-ins, is a no-op without the variable, and
8
+ * never throws: any socket error disables forwarding for the rest of the process.
9
+ */
10
+ import { createConnection } from "node:net";
11
+ export default function switchyardBridge(pi) {
12
+ const socketPath = process.env.SWITCHYARD_BRIDGE_SOCKET;
13
+ if (!socketPath)
14
+ return;
15
+ let socket = null;
16
+ let disabled = false;
17
+ let seq = 0;
18
+ const ensureSocket = () => {
19
+ if (disabled)
20
+ return null;
21
+ if (socket)
22
+ return socket;
23
+ try {
24
+ const s = createConnection(socketPath);
25
+ s.on("error", () => {
26
+ disabled = true;
27
+ try {
28
+ s.destroy();
29
+ }
30
+ catch {
31
+ // ignore
32
+ }
33
+ });
34
+ s.on("close", () => {
35
+ // A clean close (session_shutdown) allows a later reconnect; errors disabled us above.
36
+ if (socket === s)
37
+ socket = null;
38
+ });
39
+ s.unref();
40
+ socket = s;
41
+ send(s, { type: "hello" });
42
+ return s;
43
+ }
44
+ catch {
45
+ disabled = true;
46
+ return null;
47
+ }
48
+ };
49
+ const send = (s, msg) => {
50
+ const full = {
51
+ type: msg.type,
52
+ ...(msg.kind !== undefined ? { kind: msg.kind } : {}),
53
+ title: msg.title ?? null,
54
+ seq: ++seq,
55
+ at: new Date().toISOString(),
56
+ pid: process.pid,
57
+ };
58
+ try {
59
+ s.write(`${JSON.stringify(full)}\n`);
60
+ }
61
+ catch {
62
+ disabled = true;
63
+ }
64
+ };
65
+ const forward = (event) => {
66
+ try {
67
+ const s = ensureSocket();
68
+ if (!s)
69
+ return;
70
+ send(s, {
71
+ type: event?.type === "ui_prompt_end" ? "ui_prompt_end" : "ui_prompt_start",
72
+ kind: typeof event?.kind === "string" ? event.kind : "unknown",
73
+ title: typeof event?.title === "string" ? event.title : undefined,
74
+ });
75
+ }
76
+ catch {
77
+ disabled = true;
78
+ }
79
+ };
80
+ try {
81
+ pi.on("session_start", () => {
82
+ try {
83
+ ensureSocket();
84
+ }
85
+ catch {
86
+ disabled = true;
87
+ }
88
+ });
89
+ pi.on("ui_prompt_start", forward);
90
+ pi.on("ui_prompt_end", forward);
91
+ pi.on("session_shutdown", () => {
92
+ try {
93
+ socket?.end();
94
+ }
95
+ catch {
96
+ // ignore
97
+ }
98
+ socket = null;
99
+ });
100
+ }
101
+ catch {
102
+ disabled = true;
103
+ }
104
+ }
105
+ //# sourceMappingURL=switchyard-bridge.js.map