relaydeck 0.1.0__py3-none-any.whl

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 (301) hide show
  1. plugins/__init__.py +19 -0
  2. plugins/bundle.toml +30 -0
  3. plugins/dashboard/SKILL.md +66 -0
  4. plugins/dashboard/__init__.py +1 -0
  5. plugins/dashboard/plugin.py +69 -0
  6. plugins/dashboard/plugin.toml +21 -0
  7. plugins/external_agents/__init__.py +2 -0
  8. plugins/external_agents/detector.py +223 -0
  9. plugins/external_agents/harness.py +83 -0
  10. plugins/external_agents/models.py +183 -0
  11. plugins/external_agents/plugin.py +640 -0
  12. plugins/external_agents/plugin.toml +30 -0
  13. plugins/external_agents/probes.py +165 -0
  14. plugins/external_agents/static/panel.js +365 -0
  15. plugins/external_agents/store.py +97 -0
  16. plugins/file_watcher/__init__.py +0 -0
  17. plugins/file_watcher/plugin.py +341 -0
  18. plugins/file_watcher/plugin.toml +9 -0
  19. plugins/gateway/__init__.py +0 -0
  20. plugins/gateway/plugin.py +218 -0
  21. plugins/gateway/plugin.toml +15 -0
  22. plugins/github/__init__.py +0 -0
  23. plugins/github/enrich.py +135 -0
  24. plugins/github/plugin.py +912 -0
  25. plugins/github/plugin.toml +50 -0
  26. plugins/github/poller.py +447 -0
  27. plugins/github/rules.py +302 -0
  28. plugins/github/spawn.py +151 -0
  29. plugins/github/static/panel.js +1440 -0
  30. plugins/harnesses/__init__.py +1 -0
  31. plugins/harnesses/antigravity/__init__.py +1 -0
  32. plugins/harnesses/antigravity/agent.py +302 -0
  33. plugins/harnesses/antigravity/plugin.py +17 -0
  34. plugins/harnesses/antigravity/plugin.toml +9 -0
  35. plugins/harnesses/claude_code/__init__.py +0 -0
  36. plugins/harnesses/claude_code/agent.py +471 -0
  37. plugins/harnesses/claude_code/plugin.py +23 -0
  38. plugins/harnesses/claude_code/plugin.toml +9 -0
  39. plugins/harnesses/codex/__init__.py +0 -0
  40. plugins/harnesses/codex/agent.py +613 -0
  41. plugins/harnesses/codex/plugin.py +17 -0
  42. plugins/harnesses/codex/plugin.toml +9 -0
  43. plugins/harnesses/cursor/__init__.py +1 -0
  44. plugins/harnesses/cursor/agent.py +390 -0
  45. plugins/harnesses/cursor/plugin.py +18 -0
  46. plugins/harnesses/cursor/plugin.toml +9 -0
  47. plugins/harnesses/opencode/__init__.py +0 -0
  48. plugins/harnesses/opencode/agent.py +394 -0
  49. plugins/harnesses/opencode/plugin.py +19 -0
  50. plugins/harnesses/opencode/plugin.toml +9 -0
  51. plugins/harnesses/pi/__init__.py +0 -0
  52. plugins/harnesses/pi/agent.py +390 -0
  53. plugins/harnesses/pi/plugin.py +56 -0
  54. plugins/harnesses/pi/plugin.toml +17 -0
  55. plugins/harnesses/relaydeck_native/__init__.py +0 -0
  56. plugins/harnesses/relaydeck_native/agent.py +365 -0
  57. plugins/harnesses/relaydeck_native/pi_engine.py +930 -0
  58. plugins/harnesses/relaydeck_native/pi_extension.ts +186 -0
  59. plugins/harnesses/relaydeck_native/pi_startup.ts +210 -0
  60. plugins/harnesses/relaydeck_native/plugin.py +135 -0
  61. plugins/harnesses/relaydeck_native/plugin.toml +24 -0
  62. plugins/harnesses/relaydeck_native/prompt.py +255 -0
  63. plugins/harnesses/relaydeck_native/static/calls.js +111 -0
  64. plugins/harnesses/relaydeck_native/static/context.js +109 -0
  65. plugins/harnesses/relaydeck_native/static/pi_install.js +183 -0
  66. plugins/harnesses/relaydeck_native/static/widget_chat.js +235 -0
  67. plugins/harnesses/relaydeck_native/tools.py +405 -0
  68. plugins/hitl/__init__.py +1 -0
  69. plugins/hitl/plugin.py +389 -0
  70. plugins/hitl/plugin.toml +27 -0
  71. plugins/loop/__init__.py +0 -0
  72. plugins/loop/agent.py +415 -0
  73. plugins/loop/plugin.py +51 -0
  74. plugins/loop/plugin.toml +13 -0
  75. plugins/messaging/SKILL.md +133 -0
  76. plugins/messaging/__init__.py +0 -0
  77. plugins/messaging/plugin.py +1403 -0
  78. plugins/messaging/plugin.toml +45 -0
  79. plugins/messaging/static/tile_compose.js +108 -0
  80. plugins/messaging/static/tile_inbox.js +253 -0
  81. plugins/metering/__init__.py +0 -0
  82. plugins/metering/plugin.py +354 -0
  83. plugins/metering/plugin.toml +25 -0
  84. plugins/metering/static/fleet.js +191 -0
  85. plugins/metering/static/tile_cost.js +86 -0
  86. plugins/metering/static/tile_tokens.js +95 -0
  87. plugins/prompts/SKILL.md +74 -0
  88. plugins/prompts/__init__.py +1 -0
  89. plugins/prompts/plugin.py +465 -0
  90. plugins/prompts/plugin.toml +64 -0
  91. plugins/prompts/static/tile_prompts.js +113 -0
  92. plugins/providers/__init__.py +0 -0
  93. plugins/providers/anthropic/__init__.py +0 -0
  94. plugins/providers/anthropic/plugin.py +109 -0
  95. plugins/providers/anthropic/plugin.toml +10 -0
  96. plugins/providers/ollama/__init__.py +0 -0
  97. plugins/providers/ollama/plugin.py +12 -0
  98. plugins/providers/ollama/plugin.toml +9 -0
  99. plugins/providers/openai/__init__.py +0 -0
  100. plugins/providers/openai/plugin.py +100 -0
  101. plugins/providers/openai/plugin.toml +10 -0
  102. plugins/providers/openrouter/__init__.py +0 -0
  103. plugins/providers/openrouter/plugin.py +224 -0
  104. plugins/providers/openrouter/plugin.toml +10 -0
  105. plugins/py.typed +1 -0
  106. plugins/registry.yaml +43 -0
  107. plugins/skills/FLEET_SKILL.md +62 -0
  108. plugins/skills/PLUGIN_AUTHORING_SKILL.md +215 -0
  109. plugins/skills/__init__.py +0 -0
  110. plugins/skills/commands.py +203 -0
  111. plugins/skills/manager.py +295 -0
  112. plugins/skills/plugin.py +204 -0
  113. plugins/skills/plugin.toml +44 -0
  114. plugins/skills/routes.py +121 -0
  115. plugins/skills/static/panel.js +374 -0
  116. plugins/telegram/SKILL.md +169 -0
  117. plugins/telegram/__init__.py +0 -0
  118. plugins/telegram/conversations.py +188 -0
  119. plugins/telegram/inbound_map.py +123 -0
  120. plugins/telegram/plugin.py +2421 -0
  121. plugins/telegram/plugin.toml +112 -0
  122. plugins/telegram/routes.py +378 -0
  123. plugins/telegram/static/conversations.js +71 -0
  124. plugins/telegram/static/panel.js +1487 -0
  125. plugins/telegram/static/tile_chats.js +107 -0
  126. plugins/telegram/typing_sessions.py +132 -0
  127. plugins/telegram/worker.py +600 -0
  128. plugins/theme/SKILL.md +70 -0
  129. plugins/theme/__init__.py +1 -0
  130. plugins/theme/plugin.py +75 -0
  131. plugins/theme/plugin.toml +21 -0
  132. plugins/usage_limits/__init__.py +0 -0
  133. plugins/usage_limits/core.py +153 -0
  134. plugins/usage_limits/plugin.py +469 -0
  135. plugins/usage_limits/plugin.toml +43 -0
  136. plugins/usage_limits/static/panel.js +135 -0
  137. plugins/usage_limits/static/tile.js +105 -0
  138. plugins/vault/__init__.py +0 -0
  139. plugins/vault/plugin.py +518 -0
  140. plugins/vault/plugin.toml +24 -0
  141. plugins/vault/static/vault.js +171 -0
  142. plugins/vault/store.py +338 -0
  143. relaydeck/__init__.py +55 -0
  144. relaydeck/__main__.py +8 -0
  145. relaydeck/agents_base.py +108 -0
  146. relaydeck/atomicio.py +37 -0
  147. relaydeck/audit.py +222 -0
  148. relaydeck/auth.py +155 -0
  149. relaydeck/auth_tokens.py +245 -0
  150. relaydeck/automation/__init__.py +42 -0
  151. relaydeck/automation/actions.py +481 -0
  152. relaydeck/automation/schedule.py +63 -0
  153. relaydeck/automation_runs.py +296 -0
  154. relaydeck/bundles.py +85 -0
  155. relaydeck/channels.py +337 -0
  156. relaydeck/config.py +449 -0
  157. relaydeck/daemon.py +258 -0
  158. relaydeck/dashboard_commands.py +170 -0
  159. relaydeck/db.py +1790 -0
  160. relaydeck/harness/__init__.py +27 -0
  161. relaydeck/harness/base.py +1246 -0
  162. relaydeck/harness_model.py +297 -0
  163. relaydeck/harness_options.py +686 -0
  164. relaydeck/integrations/__init__.py +239 -0
  165. relaydeck/integrations/classifier.py +79 -0
  166. relaydeck/integrations/claude.py +324 -0
  167. relaydeck/layouts.py +172 -0
  168. relaydeck/local_providers.py +143 -0
  169. relaydeck/maintenance.py +109 -0
  170. relaydeck/messages.py +616 -0
  171. relaydeck/metrics.py +382 -0
  172. relaydeck/model_invocations.py +296 -0
  173. relaydeck/model_roles.py +282 -0
  174. relaydeck/models/__init__.py +0 -0
  175. relaydeck/models_dev.py +372 -0
  176. relaydeck/orchestrator.py +1354 -0
  177. relaydeck/plugin.py +2088 -0
  178. relaydeck/plugin_disabled.py +88 -0
  179. relaydeck/plugin_install.py +478 -0
  180. relaydeck/plugin_lock.py +195 -0
  181. relaydeck/plugin_manifest.py +340 -0
  182. relaydeck/plugin_registry.py +126 -0
  183. relaydeck/plugin_settings.py +216 -0
  184. relaydeck/preferences.py +194 -0
  185. relaydeck/prompt_composition.py +185 -0
  186. relaydeck/prompts.py +711 -0
  187. relaydeck/provider.py +13 -0
  188. relaydeck/provider_config.py +102 -0
  189. relaydeck/providers_extra.py +284 -0
  190. relaydeck/providers_ollama.py +145 -0
  191. relaydeck/py.typed +1 -0
  192. relaydeck/screen.py +93 -0
  193. relaydeck/sdk.py +1689 -0
  194. relaydeck/semantic_engine.py +263 -0
  195. relaydeck/skills.py +639 -0
  196. relaydeck/skills_cache.py +247 -0
  197. relaydeck/state.py +299 -0
  198. relaydeck/tasks.py +322 -0
  199. relaydeck/testing.py +391 -0
  200. relaydeck/themes.py +451 -0
  201. relaydeck/tls.py +144 -0
  202. relaydeck/transports/__init__.py +0 -0
  203. relaydeck/transports/api.py +3668 -0
  204. relaydeck/transports/attach.py +429 -0
  205. relaydeck/transports/cli.py +7000 -0
  206. relaydeck/transports/view.py +1246 -0
  207. relaydeck/transports/viewers/__init__.py +212 -0
  208. relaydeck/transports/viewers/ghostty.py +362 -0
  209. relaydeck/transports/viewers/print_viewer.py +43 -0
  210. relaydeck/transports/viewers/tmux.py +175 -0
  211. relaydeck/utils/__init__.py +1 -0
  212. relaydeck/vault.py +75 -0
  213. relaydeck/version_check.py +134 -0
  214. relaydeck/web/__init__.py +0 -0
  215. relaydeck/web/static/app.js +1132 -0
  216. relaydeck/web/static/data.js +373 -0
  217. relaydeck/web/static/favicon.svg +10 -0
  218. relaydeck/web/static/fonts/LICENSE +13 -0
  219. relaydeck/web/static/fonts/fonts.css +253 -0
  220. relaydeck/web/static/fonts/ibm-plex-mono-300-latin-ext.woff2 +0 -0
  221. relaydeck/web/static/fonts/ibm-plex-mono-300-latin.woff2 +0 -0
  222. relaydeck/web/static/fonts/ibm-plex-mono-400-latin-ext.woff2 +0 -0
  223. relaydeck/web/static/fonts/ibm-plex-mono-400-latin.woff2 +0 -0
  224. relaydeck/web/static/fonts/ibm-plex-mono-500-latin-ext.woff2 +0 -0
  225. relaydeck/web/static/fonts/ibm-plex-mono-500-latin.woff2 +0 -0
  226. relaydeck/web/static/fonts/ibm-plex-mono-600-latin-ext.woff2 +0 -0
  227. relaydeck/web/static/fonts/ibm-plex-mono-600-latin.woff2 +0 -0
  228. relaydeck/web/static/fonts/ibm-plex-sans-300-latin-ext.woff2 +0 -0
  229. relaydeck/web/static/fonts/ibm-plex-sans-300-latin.woff2 +0 -0
  230. relaydeck/web/static/fonts/ibm-plex-sans-400-latin-ext.woff2 +0 -0
  231. relaydeck/web/static/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
  232. relaydeck/web/static/fonts/ibm-plex-sans-500-latin-ext.woff2 +0 -0
  233. relaydeck/web/static/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
  234. relaydeck/web/static/fonts/ibm-plex-sans-600-latin-ext.woff2 +0 -0
  235. relaydeck/web/static/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
  236. relaydeck/web/static/fonts/ibm-plex-sans-700-latin-ext.woff2 +0 -0
  237. relaydeck/web/static/fonts/ibm-plex-sans-700-latin.woff2 +0 -0
  238. relaydeck/web/static/fonts/jetbrains-mono-400-latin-ext.woff2 +0 -0
  239. relaydeck/web/static/fonts/jetbrains-mono-400-latin.woff2 +0 -0
  240. relaydeck/web/static/fonts/jetbrains-mono-500-latin-ext.woff2 +0 -0
  241. relaydeck/web/static/fonts/jetbrains-mono-500-latin.woff2 +0 -0
  242. relaydeck/web/static/fonts/jetbrains-mono-600-latin-ext.woff2 +0 -0
  243. relaydeck/web/static/fonts/jetbrains-mono-600-latin.woff2 +0 -0
  244. relaydeck/web/static/harness_brand.js +124 -0
  245. relaydeck/web/static/home.js +1564 -0
  246. relaydeck/web/static/icon_data.js +69 -0
  247. relaydeck/web/static/index.html +35 -0
  248. relaydeck/web/static/lenses/agents.js +582 -0
  249. relaydeck/web/static/lenses/appearance.js +446 -0
  250. relaydeck/web/static/lenses/messages.js +378 -0
  251. relaydeck/web/static/lenses/models.js +1250 -0
  252. relaydeck/web/static/lenses/plugins.js +414 -0
  253. relaydeck/web/static/lenses/workers.js +752 -0
  254. relaydeck/web/static/lenses/workspaces.js +403 -0
  255. relaydeck/web/static/model_select.js +213 -0
  256. relaydeck/web/static/models.css +495 -0
  257. relaydeck/web/static/new_agent.js +1014 -0
  258. relaydeck/web/static/onboarding.js +517 -0
  259. relaydeck/web/static/overlays.js +1540 -0
  260. relaydeck/web/static/panels.css +1394 -0
  261. relaydeck/web/static/primitives.js +288 -0
  262. relaydeck/web/static/shell.js +292 -0
  263. relaydeck/web/static/styles.css +1406 -0
  264. relaydeck/web/static/theme.css +102 -0
  265. relaydeck/web/static/theme.js +91 -0
  266. relaydeck/web/static/tile_system.js +293 -0
  267. relaydeck/web/static/tiles/config.js +70 -0
  268. relaydeck/web/static/tiles/context.js +135 -0
  269. relaydeck/web/static/tiles/events.js +193 -0
  270. relaydeck/web/static/tiles/identity.js +468 -0
  271. relaydeck/web/static/tiles/raw.js +82 -0
  272. relaydeck/web/static/tiles/terminal.js +403 -0
  273. relaydeck/web/static/uikit/README.md +143 -0
  274. relaydeck/web/static/uikit/base.js +225 -0
  275. relaydeck/web/static/uikit/format.js +27 -0
  276. relaydeck/web/static/uikit/index.js +56 -0
  277. relaydeck/web/static/uikit/modal.js +84 -0
  278. relaydeck/web/static/uikit/reactive.js +84 -0
  279. relaydeck/web/static/uikit/settings-form.js +159 -0
  280. relaydeck/web/static/uikit/toggle.js +53 -0
  281. relaydeck/web/static/uikit/uikit.css +78 -0
  282. relaydeck/web/static/uikit/widgets.js +162 -0
  283. relaydeck/web/static/update_banner.js +163 -0
  284. relaydeck/web/static/vendor/lit-all.min.js +116 -0
  285. relaydeck/web/static/vendor/xterm/LICENSE +29 -0
  286. relaydeck/web/static/vendor/xterm/addon-fit.min.js +8 -0
  287. relaydeck/web/static/vendor/xterm/addon-webgl.min.js +8 -0
  288. relaydeck/web/static/vendor/xterm/xterm.min.css +8 -0
  289. relaydeck/web/static/vendor/xterm/xterm.min.js +8 -0
  290. relaydeck/web/static/worker_form.js +414 -0
  291. relaydeck/web/static/workers.css +717 -0
  292. relaydeck/web_runtime.py +47 -0
  293. relaydeck/workers.py +445 -0
  294. relaydeck/workspace_health.py +113 -0
  295. relaydeck/workspace_plugins.py +111 -0
  296. relaydeck/worktrees.py +685 -0
  297. relaydeck-0.1.0.dist-info/METADATA +335 -0
  298. relaydeck-0.1.0.dist-info/RECORD +301 -0
  299. relaydeck-0.1.0.dist-info/WHEEL +4 -0
  300. relaydeck-0.1.0.dist-info/entry_points.txt +3 -0
  301. relaydeck-0.1.0.dist-info/licenses/LICENSE +21 -0
plugins/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """Official relaydeck plugins — relaydeck-maintained, all in one tree.
2
+
3
+ Every relaydeck-managed plugin lives here: the infrastructure plugins the
4
+ daemon can't boot without (``vault``, ``github``, ``loop``, ``external_agents``,
5
+ ``harnesses``) alongside the separable extensions (``messaging``, ``skills``,
6
+ ``theme``, ``metering``, ``telegram``, ``gateway``, ``file_watcher``,
7
+ ``usage_limits``, ``hitl``, ``dashboard``, ``providers``).
8
+
9
+ These load through real package import via the daemon plugin loader
10
+ (``PluginRegistry._scan_package("plugins")``). Core never imports this package
11
+ statically — the boundary is one-directional: plugins import the public core
12
+ facades (``relaydeck.sdk``, ``relaydeck.harness``, ``relaydeck.provider``,
13
+ ``relaydeck.vault``, ``relaydeck.automation``, ``relaydeck.testing``), never the
14
+ other way around.
15
+
16
+ The tree ships in the single ``relaydeck`` wheel today; it is laid out so a
17
+ future split into a separately-versioned ``relaydeck-plugins`` distribution (or
18
+ per-plugin wheels) is mechanical. See ``AGENTS.md`` (Package separation).
19
+ """
plugins/bundle.toml ADDED
@@ -0,0 +1,30 @@
1
+ # Recommended bundles of OFFICIAL relaydeck plugins.
2
+ #
3
+ # Every official plugin ships in the single `relaydeck` wheel today, so a
4
+ # bundle is a *recommended set* — the plugins `relaydeck doctor` expects to be
5
+ # present and the dashboard surfaces as the default experience. This file is
6
+ # also the pinned manifest of what an eventual split `relaydeck-plugins` wheel
7
+ # (or per-plugin wheels) must provide, so the recommended set has one source of
8
+ # truth that travels with the plugins.
9
+ #
10
+ # Names are plugin manifest names (the `name = ...` in each plugin.toml), which
11
+ # is what discovery and the lockfile compare against.
12
+
13
+ [bundle.default]
14
+ description = "The full relaydeck experience — every official plugin."
15
+ plugins = [
16
+ # Infrastructure (engine depends on these)
17
+ "vault", "github", "loop", "external",
18
+ # Harnesses
19
+ "pi-harness", "claude-code-harness", "codex-harness", "cursor-harness",
20
+ "opencode-harness", "antigravity-harness", "relaydeck-native",
21
+ # Extensions
22
+ "messaging", "skills", "theme", "metering", "telegram",
23
+ "gateway", "file-watcher", "usage-limits", "hitl", "prompts", "dashboard",
24
+ # Provider catalogs
25
+ "openai", "anthropic", "ollama", "openrouter",
26
+ ]
27
+
28
+ [bundle.minimal]
29
+ description = "Engine-essential only — one harness, secrets, and the operator."
30
+ plugins = ["vault", "pi-harness", "relaydeck-native"]
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: relaydeck-dashboard
3
+ description: Reshape the live relaydeck web dashboard. Read this when asked to rearrange, add, remove, move, or resize Home widgets, switch the theme/density/glow, or "make the dashboard look like X". Changes apply instantly to every open dashboard. Drive it with the `relaydeck dashboard` CLI.
4
+ metadata:
5
+ short-description: Restyle and rearrange the relaydeck dashboard
6
+ ---
7
+
8
+ # Controlling the relaydeck dashboard
9
+
10
+ The dashboard is a 12-column Home widget grid plus an appearance layer
11
+ (theme / density / glow). You change it through the **`relaydeck dashboard`**
12
+ CLI — every command POSTs to the daemon, which broadcasts the change so it
13
+ applies **instantly** to every open dashboard (no reload). A daemon must be
14
+ running.
15
+
16
+ ## See what's there first
17
+
18
+ ```sh
19
+ relaydeck dashboard get # theme / density / glow + saved widget grid
20
+ ```
21
+
22
+ For the full theme list use `relaydeck theme list`. To author or recolor a
23
+ theme (vs. just switching to one), use the **relaydeck-theme** skill.
24
+
25
+ ## Appearance
26
+
27
+ ```sh
28
+ relaydeck dashboard theme ink # any theme from `relaydeck theme list`
29
+ relaydeck dashboard density compact # compact | comfy | regular
30
+ relaydeck dashboard glow off # on | off
31
+ ```
32
+
33
+ ## Widgets (the Home grid)
34
+
35
+ The grid is **12 columns wide**; `x`/`y` are 0-based cell coordinates and
36
+ `w`/`h` are cell spans. Widget keys:
37
+
38
+ `fleet` · `usage` · `agents` · `feed` · `workspaces` · `spawn` · `workers`
39
+ · `worktrees` · `clock` · `notes` · `focus`
40
+
41
+ ```sh
42
+ relaydeck dashboard add workers # add a widget
43
+ relaydeck dashboard remove clock # remove it
44
+ relaydeck dashboard move usage 8 0 # move 'usage' to column 8, row 0
45
+ relaydeck dashboard resize agents 6 4 # resize 'agents' to 6 wide x 4 tall
46
+ relaydeck dashboard tidy # auto-arrange, remove gaps
47
+ relaydeck dashboard reset # back to the default layout
48
+ ```
49
+
50
+ ## Notes
51
+
52
+ - Invalid values are rejected with the allowed set (e.g. an unknown widget or
53
+ theme) — read the error and retry; nothing partial is applied.
54
+ - **`theme` / `density` / `glow` persist server-side** and `dashboard get`
55
+ reflects them immediately — no browser needs to be open. They default to the
56
+ global (dashboard-wide) scope; pass `--workspace <name>` to scope one
57
+ workspace, and `dashboard get --workspace <name>` to read that scope.
58
+ - **The saved Home widget grid** is also in `dashboard get` (from
59
+ `preferences.yaml`). When nothing is saved yet, `get` shows the package
60
+ default layout. Live `add`/`move`/`resize`/`tidy`/`reset` ops apply instantly
61
+ in open browsers; the browser persists the grid back to the server.
62
+ - **Widget ops (`add`/`remove`/`move`/`resize`/`tidy`/`reset`) apply to open
63
+ dashboards live** — the `✓` from the command is your confirmation once the
64
+ browser applies it.
65
+ - This is the same capability the relaydeck-native agent's `dashboard` tool
66
+ has, exposed as a CLI so any harness can use it.
@@ -0,0 +1 @@
1
+ """Dashboard plugin — ships the `relaydeck-dashboard` skill."""
@@ -0,0 +1,69 @@
1
+ """
2
+ Dashboard plugin — the skill-shipping surface for live dashboard control.
3
+
4
+ The control surface itself is core: the validator
5
+ (`relaydeck/dashboard_commands.py`), the endpoint
6
+ (`POST /api/dashboard/command`), and the CLI (`relaydeck dashboard …`). The
7
+ browser applies the emitted `dashboard.command` events live. This plugin
8
+ exists for one reason: to materialize the `relaydeck-dashboard` SKILL.md into
9
+ workspaces via the generic `[plugin.skills]` mechanism, so an agent on ANY
10
+ harness can reshape the dashboard through the `relaydeck dashboard` CLI — the
11
+ same capability the native `dashboard` tool gives the relaydeck-native agent.
12
+
13
+ Daemon-wide (not workspace-scoped) and opt-out via the `inject_skill`
14
+ setting; mirrors the theme plugin.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from typing import Any
21
+
22
+ from relaydeck.sdk import Plugin, PluginHost
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ PLUGIN_NAME = "dashboard"
27
+
28
+
29
+ def _setting(key: str, default: Any) -> Any:
30
+ from relaydeck.plugin_settings import get_setting
31
+ return get_setting(PLUGIN_NAME, key, default=default)
32
+
33
+
34
+ def _inject_skill_enabled() -> bool:
35
+ v = _setting("inject_skill", True)
36
+ if isinstance(v, bool):
37
+ return v
38
+ if isinstance(v, str):
39
+ return v.strip().lower() not in ("false", "0", "no", "off", "")
40
+ return bool(v)
41
+
42
+
43
+ class DashboardPlugin(Plugin):
44
+ """Daemon-wide plugin that ships the `relaydeck-dashboard` skill."""
45
+
46
+ def on_load(self, host: PluginHost) -> None:
47
+ self.host = host
48
+ self._config_home = host.config_home
49
+
50
+ def on_unload(self) -> None:
51
+ # The skills plugin strips materialized skills on plugin unload.
52
+ pass
53
+
54
+ def on_settings_changed(self, new_values: dict[str, Any]) -> None:
55
+ del new_values
56
+ try:
57
+ self.host.events.emit("plugin.skills.changed", {"plugin": PLUGIN_NAME})
58
+ except Exception:
59
+ pass
60
+
61
+ def skill_target_workspaces(self, all_workspaces: list[str]) -> list[str]:
62
+ """Dashboard control is universal — ship to every workspace unless the
63
+ operator turned `inject_skill` off."""
64
+ if not _inject_skill_enabled():
65
+ return []
66
+ return list(all_workspaces)
67
+
68
+
69
+ PLUGIN = DashboardPlugin()
@@ -0,0 +1,21 @@
1
+ [plugin]
2
+ name = "dashboard"
3
+ version = "0.1.0"
4
+ description = "Live dashboard control — ships the `relaydeck-dashboard` skill so any harness agent can reshape the dashboard"
5
+ author = "relaydeck"
6
+ license = "MIT"
7
+ category = "tool"
8
+ host_api_version = 1
9
+ # Daemon-wide: the control surface (validator, POST /api/dashboard/command,
10
+ # `relaydeck dashboard` CLI) lives in core. This plugin's only job is to ship
11
+ # the `relaydeck-dashboard` SKILL.md to workspaces so agents learn the CLI.
12
+ workspace_scoped = false
13
+ declared_capabilities = [
14
+ "events.emit",
15
+ ]
16
+
17
+ [plugin.settings]
18
+ inject_skill = { type = "bool", default = true, description = "Materialize the `relaydeck-dashboard` skill into every workspace so agents can reshape the dashboard." }
19
+
20
+ [plugin.skills]
21
+ relaydeck-dashboard = "SKILL.md"
@@ -0,0 +1,2 @@
1
+ """Bundled external-agents plugin: discover, observe, and surface Hermes /
2
+ OpenClaw runtimes that relaydeck does not run (read-only)."""
@@ -0,0 +1,223 @@
1
+ """Detect whether a path is a Hermes or OpenClaw runtime.
2
+
3
+ Detection is **filesystem-first and side-effect-free**: it reads a few marker
4
+ files and directory names and scores the evidence. The only host probe is an
5
+ injectable `which(<cli>)` check ("is the runtime's CLI installed?") which is a
6
+ soft signal — it never executes the CLI. We deliberately do NOT run
7
+ `hermes`/`openclaw` here (that's `probes.py`, and only on demand) so `detect`
8
+ stays fast and safe to call while scanning many directories.
9
+
10
+ Returns a `Detection` (scored evidence). The caller decides whether the
11
+ confidence is high enough to register or surface as a candidate.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import shutil
17
+ from pathlib import Path
18
+
19
+ from .models import (
20
+ HERMES,
21
+ OPENCLAW,
22
+ T_GATEWAY_WS,
23
+ T_MCP,
24
+ UNKNOWN,
25
+ Detection,
26
+ )
27
+
28
+ # Confidence weights per signal. Tuned so a single strong marker (a
29
+ # pyproject naming hermes-agent, an openclaw.mjs, or a `.hermes`/`.openclaw`
30
+ # home) clears the 0.5 "candidate" bar, and two independent markers clear
31
+ # the 0.9 "confident" bar.
32
+ _W_HOME_NAME = 0.6 # dir literally named .hermes / .openclaw
33
+ _W_STRONG_REPO = 0.6 # pyproject/package.json/openclaw.mjs naming the project
34
+ _W_CONFIG_FILE = 0.35 # config.yaml in a plausible home
35
+ _W_CONTEXT_FILE = 0.15 # .hermes.md / SOUL.md etc.
36
+ _W_DIR_HINT = 0.12 # a hermes/openclaw-looking subdir
37
+ _W_CLI_ON_PATH = 0.25 # the runtime's CLI is installed on this host
38
+
39
+ _CONFIDENCE_CAP = 0.99
40
+ CANDIDATE_THRESHOLD = 0.5
41
+
42
+
43
+ def _which(name: str) -> str | None:
44
+ """Indirection point so tests can monkeypatch CLI presence without
45
+ depending on whether hermes/openclaw is actually installed."""
46
+ return shutil.which(name)
47
+
48
+
49
+ def _read_head(path: Path, limit: int = 8192) -> str:
50
+ try:
51
+ with path.open("r", encoding="utf-8", errors="replace") as f:
52
+ return f.read(limit)
53
+ except OSError:
54
+ return ""
55
+
56
+
57
+ def _score_hermes(path: Path) -> tuple[float, list[str], str | None]:
58
+ signals: list[str] = []
59
+ score = 0.0
60
+ config_home: str | None = None
61
+
62
+ if path.name == ".hermes":
63
+ score += _W_HOME_NAME
64
+ signals.append("dir named .hermes")
65
+ config_home = str(path)
66
+
67
+ cfg = path / "config.yaml"
68
+ if cfg.exists():
69
+ score += _W_CONFIG_FILE
70
+ signals.append("config.yaml present")
71
+ config_home = config_home or str(path)
72
+
73
+ pyproject = path / "pyproject.toml"
74
+ if pyproject.exists():
75
+ head = _read_head(pyproject).lower()
76
+ if "hermes-agent" in head or "hermes_agent" in head or "hermes agent" in head:
77
+ score += _W_STRONG_REPO
78
+ signals.append("pyproject names hermes-agent")
79
+ elif "hermes" in head:
80
+ score += _W_DIR_HINT
81
+ signals.append("pyproject mentions hermes")
82
+
83
+ for ctx in (".hermes.md", "SOUL.md"):
84
+ if (path / ctx).exists():
85
+ score += _W_CONTEXT_FILE
86
+ signals.append(f"{ctx} present")
87
+
88
+ for d in ("hermes_cli", "hermes_agent"):
89
+ if (path / d).is_dir():
90
+ score += _W_DIR_HINT
91
+ signals.append(f"{d}/ dir")
92
+
93
+ if _which("hermes"):
94
+ score += _W_CLI_ON_PATH
95
+ signals.append("hermes CLI on PATH")
96
+
97
+ # A conventional home alongside a repo checkout.
98
+ if config_home is None:
99
+ home = Path.home() / ".hermes"
100
+ if home.exists():
101
+ config_home = str(home)
102
+
103
+ return score, signals, config_home
104
+
105
+
106
+ def _score_openclaw(path: Path) -> tuple[float, list[str], str | None]:
107
+ signals: list[str] = []
108
+ score = 0.0
109
+ config_home: str | None = None
110
+
111
+ if path.name == ".openclaw":
112
+ score += _W_HOME_NAME
113
+ signals.append("dir named .openclaw")
114
+ config_home = str(path)
115
+
116
+ if (path / "config.yaml").exists() or (path / "config.json").exists():
117
+ score += _W_CONFIG_FILE
118
+ signals.append("config file present")
119
+ config_home = config_home or str(path)
120
+
121
+ if (path / "openclaw.mjs").exists():
122
+ score += _W_STRONG_REPO
123
+ signals.append("openclaw.mjs present")
124
+
125
+ pkg = path / "package.json"
126
+ if pkg.exists():
127
+ head = _read_head(pkg).lower()
128
+ if "openclaw" in head:
129
+ score += _W_STRONG_REPO
130
+ signals.append("package.json names openclaw")
131
+
132
+ if (path / "pnpm-workspace.yaml").exists():
133
+ score += _W_DIR_HINT
134
+ signals.append("pnpm-workspace.yaml")
135
+
136
+ for d in ("extensions", "apps"):
137
+ if (path / d).is_dir():
138
+ score += _W_DIR_HINT * 0.5
139
+ signals.append(f"{d}/ dir")
140
+
141
+ if _which("openclaw"):
142
+ score += _W_CLI_ON_PATH
143
+ signals.append("openclaw CLI on PATH")
144
+
145
+ if config_home is None:
146
+ home = Path.home() / ".openclaw"
147
+ if home.exists():
148
+ config_home = str(home)
149
+
150
+ return score, signals, config_home
151
+
152
+
153
+ def detect(path: str | Path) -> Detection:
154
+ """Score `path` as a hermes/openclaw runtime. Filesystem-only."""
155
+ p = Path(path).expanduser()
156
+ root = str(p)
157
+ if not p.exists():
158
+ return Detection(
159
+ kind=UNKNOWN, confidence=0.0, root=root,
160
+ warnings=[f"path does not exist: {root}"],
161
+ )
162
+ if not p.is_dir():
163
+ return Detection(
164
+ kind=UNKNOWN, confidence=0.0, root=root,
165
+ warnings=[f"not a directory: {root}"],
166
+ )
167
+
168
+ h_score, h_signals, h_home = _score_hermes(p)
169
+ o_score, o_signals, o_home = _score_openclaw(p)
170
+
171
+ if h_score == 0.0 and o_score == 0.0:
172
+ return Detection(
173
+ kind=UNKNOWN, confidence=0.0, root=root,
174
+ warnings=["no hermes/openclaw markers found"],
175
+ )
176
+
177
+ if h_score >= o_score:
178
+ kind, score, signals, home, transport = (
179
+ HERMES, h_score, h_signals, h_home, T_MCP,
180
+ )
181
+ else:
182
+ kind, score, signals, home, transport = (
183
+ OPENCLAW, o_score, o_signals, o_home, T_GATEWAY_WS,
184
+ )
185
+
186
+ return Detection(
187
+ kind=kind,
188
+ confidence=min(score, _CONFIDENCE_CAP),
189
+ signals=signals,
190
+ recommended_transport=transport,
191
+ root=root,
192
+ config_home=home,
193
+ )
194
+
195
+
196
+ def scan_candidates(extra_roots: list[str] | None = None) -> list[Detection]:
197
+ """Auto-detect external runtimes on this machine.
198
+
199
+ Scans the conventional config homes (`~/.hermes`, `~/.openclaw`) plus any
200
+ `extra_roots` (e.g. registered relaydeck workspace paths) and returns
201
+ detections at or above the candidate threshold. The caller filters out
202
+ anything already registered before surfacing "detected — add?" affordances.
203
+ """
204
+ seen: set[str] = set()
205
+ out: list[Detection] = []
206
+ roots: list[Path] = [Path.home() / ".hermes", Path.home() / ".openclaw"]
207
+ for r in extra_roots or []:
208
+ try:
209
+ roots.append(Path(r).expanduser())
210
+ except (OSError, ValueError):
211
+ continue
212
+ for root in roots:
213
+ key = str(root)
214
+ if key in seen:
215
+ continue
216
+ seen.add(key)
217
+ if not root.exists() or not root.is_dir():
218
+ continue
219
+ det = detect(root)
220
+ if det.matched and det.confidence >= CANDIDATE_THRESHOLD:
221
+ out.append(det)
222
+ out.sort(key=lambda d: d.confidence, reverse=True)
223
+ return out
@@ -0,0 +1,83 @@
1
+ """
2
+ Harnesses that run a linked external runtime's interactive chat TUI as a
3
+ relaydeck agent.
4
+
5
+ The `external` plugin observes Hermes/OpenClaw read-only — but once a
6
+ runtime is linked, the most useful next step is to actually *use* it: drop
7
+ an instance of its chat TUI into any workspace, live in the dashboard
8
+ terminal tile alongside your pi/claude agents (attach, watch, message it).
9
+
10
+ relaydeck already wraps CLIs (pi, claude-code, codex) as PTY-backed
11
+ `HarnessAgent`s, so the external runtime's CLI is just another harness. We
12
+ spawn `<cli> <subcommand>` in the *workspace's* directory (so the runtime
13
+ operates on that workspace) using the runtime's OWN config/auth home
14
+ (`~/.hermes`, `~/.openclaw`). We never copy secrets or mutate the external
15
+ config — relaydeck only runs the binary.
16
+
17
+ Unlike the native harnesses this deliberately does NOT inject relaydeck's
18
+ model preset or `--append-system-prompt`: Hermes/OpenClaw own their model
19
+ selection + prompting, and those flags differ per runtime. The command is
20
+ just `<CLI> <DEFAULT_ARGS>` plus optional per-agent `config.command` (full
21
+ override) / `config.args` (additive) — so an operator can tune it
22
+ (`-m`, `--continue`, `--worktree`, …) without code changes.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import shlex
28
+
29
+ from relaydeck.harness import HarnessAgent
30
+
31
+
32
+ class ExternalChatAgent(HarnessAgent):
33
+ """Base harness for an external runtime's interactive chat CLI."""
34
+
35
+ CLI = "" # set by subclass
36
+ DEFAULT_ARGS: list[str] = []
37
+
38
+ def _build_command(self) -> list[str]:
39
+ cmd = [self.CLI] + list(self.DEFAULT_ARGS)
40
+ user_cmd = self.config.get("command")
41
+ if isinstance(user_cmd, list):
42
+ cmd = [str(a) for a in user_cmd]
43
+ elif isinstance(user_cmd, str) and user_cmd.strip():
44
+ cmd = shlex.split(user_cmd)
45
+ extra = self.config.get("args")
46
+ if isinstance(extra, list):
47
+ cmd.extend(str(a) for a in extra)
48
+ elif isinstance(extra, str) and extra.strip():
49
+ cmd.extend(shlex.split(extra))
50
+ return cmd
51
+
52
+
53
+ class HermesChatAgent(ExternalChatAgent):
54
+ """Hermes Agent — `hermes chat` (verified: "Interactive chat with the agent")."""
55
+
56
+ CLI = "hermes"
57
+ DEFAULT_ARGS = ["chat"]
58
+
59
+
60
+ class OpenClawChatAgent(ExternalChatAgent):
61
+ """OpenClaw — bare `openclaw` launches its TUI.
62
+
63
+ The exact chat subcommand is best-effort (OpenClaw needs Node >= 22.19;
64
+ if the local node is older the CLI exits with an upgrade hint, which the
65
+ operator will see in the terminal tile). Override per-agent with
66
+ `config.args` / `config.command` if the invocation differs.
67
+ """
68
+
69
+ CLI = "openclaw"
70
+ DEFAULT_ARGS: list[str] = []
71
+
72
+
73
+ # External-agent kind -> registered relaydeck harness type name.
74
+ KIND_TO_TYPE: dict[str, str] = {
75
+ "hermes": "hermes",
76
+ "openclaw": "openclaw",
77
+ }
78
+
79
+ # Harness type name -> class, registered by the plugin's on_load.
80
+ HARNESS_TYPES: dict[str, type[ExternalChatAgent]] = {
81
+ "hermes": HermesChatAgent,
82
+ "openclaw": OpenClawChatAgent,
83
+ }