waterplane 0.1.17 → 0.1.19

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 (280) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/NOTICES.md +18 -7
  3. package/README.md +46 -2
  4. package/SECURITY.md +196 -12
  5. package/node_modules/@waterplane/shared/dist/desktopfiles.d.ts +24 -2
  6. package/node_modules/@waterplane/shared/dist/desktopfiles.js +73 -3
  7. package/node_modules/@waterplane/shared/dist/desktopfiles.js.map +1 -1
  8. package/node_modules/@waterplane/shared/dist/hosts.d.ts +31 -0
  9. package/node_modules/@waterplane/shared/dist/hosts.js +46 -1
  10. package/node_modules/@waterplane/shared/dist/hosts.js.map +1 -1
  11. package/node_modules/@waterplane/shared/dist/hosturl.d.ts +55 -0
  12. package/node_modules/@waterplane/shared/dist/hosturl.js +85 -0
  13. package/node_modules/@waterplane/shared/dist/hosturl.js.map +1 -1
  14. package/node_modules/@waterplane/shared/dist/index.d.ts +4 -0
  15. package/node_modules/@waterplane/shared/dist/index.js +4 -0
  16. package/node_modules/@waterplane/shared/dist/index.js.map +1 -1
  17. package/node_modules/@waterplane/shared/dist/installrow.d.ts +6 -1
  18. package/node_modules/@waterplane/shared/dist/installrow.js +20 -1
  19. package/node_modules/@waterplane/shared/dist/installrow.js.map +1 -1
  20. package/node_modules/@waterplane/shared/dist/lanroute.d.ts +5 -0
  21. package/node_modules/@waterplane/shared/dist/lanroute.js.map +1 -1
  22. package/node_modules/@waterplane/shared/dist/pairing.d.ts +96 -2
  23. package/node_modules/@waterplane/shared/dist/pairing.js +71 -3
  24. package/node_modules/@waterplane/shared/dist/pairing.js.map +1 -1
  25. package/node_modules/@waterplane/shared/dist/pake.d.ts +131 -0
  26. package/node_modules/@waterplane/shared/dist/pake.js +272 -0
  27. package/node_modules/@waterplane/shared/dist/pake.js.map +1 -0
  28. package/node_modules/@waterplane/shared/dist/peer.d.ts +63 -0
  29. package/node_modules/@waterplane/shared/dist/peer.js +41 -0
  30. package/node_modules/@waterplane/shared/dist/peer.js.map +1 -1
  31. package/node_modules/@waterplane/shared/dist/plugin/api.d.ts +15 -0
  32. package/node_modules/@waterplane/shared/dist/plugin/version.d.ts +5 -1
  33. package/node_modules/@waterplane/shared/dist/plugin/version.js +5 -1
  34. package/node_modules/@waterplane/shared/dist/plugin/version.js.map +1 -1
  35. package/node_modules/@waterplane/shared/dist/repotwins.d.ts +47 -0
  36. package/node_modules/@waterplane/shared/dist/repotwins.js +80 -0
  37. package/node_modules/@waterplane/shared/dist/repotwins.js.map +1 -1
  38. package/node_modules/@waterplane/shared/dist/sessioncost/costwords.d.ts +7 -2
  39. package/node_modules/@waterplane/shared/dist/sessioncost/costwords.js +6 -3
  40. package/node_modules/@waterplane/shared/dist/sessioncost/costwords.js.map +1 -1
  41. package/node_modules/@waterplane/shared/dist/waiting.d.ts +64 -0
  42. package/node_modules/@waterplane/shared/dist/waiting.js +86 -0
  43. package/node_modules/@waterplane/shared/dist/waiting.js.map +1 -0
  44. package/node_modules/@waterplane/shared/dist/wire/messages/hostcore.d.ts +3 -0
  45. package/node_modules/@waterplane/shared/dist/wire/notify.d.ts +2 -0
  46. package/node_modules/@waterplane/shared/dist/wire/notify.js +2 -0
  47. package/node_modules/@waterplane/shared/dist/wire/notify.js.map +1 -1
  48. package/node_modules/@waterplane/shared/dist/wire/repo.d.ts +30 -1
  49. package/node_modules/@waterplane/shared/dist/wire/repo.js.map +1 -1
  50. package/node_modules/@waterplane/shared/dist/wire/session/context.d.ts +4 -0
  51. package/node_modules/@waterplane/shared/dist/wire/session/context.js.map +1 -1
  52. package/node_modules/@waterplane/shared/dist/wire/sysstat.d.ts +7 -0
  53. package/node_modules/@waterplane/shared/dist/wire/telemetry.d.ts +3 -0
  54. package/node_modules/@waterplane/shared/package.json +3 -0
  55. package/npm-shrinkwrap.json +30 -2
  56. package/package.json +2 -1
  57. package/packages/extensions/desktop/helper/Capture.swift +245 -0
  58. package/packages/extensions/desktop/helper/H264.swift +226 -0
  59. package/packages/extensions/desktop/helper/Info.plist +20 -0
  60. package/packages/extensions/desktop/helper/Input.swift +195 -0
  61. package/packages/extensions/desktop/helper/Jpeg.swift +81 -0
  62. package/packages/extensions/desktop/helper/PROTOCOL.md +366 -0
  63. package/packages/extensions/desktop/helper/Params.swift +133 -0
  64. package/packages/extensions/desktop/helper/Probe.swift +119 -0
  65. package/packages/extensions/desktop/helper/Protocol.swift +224 -0
  66. package/packages/extensions/desktop/helper/Request.swift +51 -0
  67. package/packages/extensions/desktop/helper/Responsibility.swift +133 -0
  68. package/packages/extensions/desktop/helper/Server.swift +334 -0
  69. package/packages/extensions/desktop/helper/Synthetic.swift +281 -0
  70. package/packages/extensions/desktop/helper/build.sh +53 -0
  71. package/packages/extensions/desktop/helper/main.swift +79 -0
  72. package/packages/extensions/desktop/helper/selftest-driver +626 -0
  73. package/packages/extensions/desktop/helper/selftest.sh +16 -0
  74. package/packages/extensions/desktop/helper-win/Capture.cs +299 -0
  75. package/packages/extensions/desktop/helper-win/H264.cs +340 -0
  76. package/packages/extensions/desktop/helper-win/Input.cs +257 -0
  77. package/packages/extensions/desktop/helper-win/Jpeg.cs +75 -0
  78. package/packages/extensions/desktop/helper-win/Main.cs +118 -0
  79. package/packages/extensions/desktop/helper-win/Native.cs +276 -0
  80. package/packages/extensions/desktop/helper-win/Params.cs +138 -0
  81. package/packages/extensions/desktop/helper-win/Protocol.cs +206 -0
  82. package/packages/extensions/desktop/helper-win/Server.cs +304 -0
  83. package/packages/extensions/desktop/helper-win/Synthetic.cs +88 -0
  84. package/packages/extensions/desktop/helper-win/build.sh +57 -0
  85. package/packages/extensions/desktop/helper-win/selftest.sh +36 -0
  86. package/packages/extensions/desktop/waterplane-plugin.json +24 -0
  87. package/packages/host/dist/agentlogs.d.ts +18 -0
  88. package/packages/host/dist/agentlogs.js +58 -0
  89. package/packages/host/dist/agentlogs.js.map +1 -0
  90. package/packages/host/dist/agents.d.ts +5 -0
  91. package/packages/host/dist/agents.js +3 -2
  92. package/packages/host/dist/agents.js.map +1 -1
  93. package/packages/host/dist/alwayson.js +1 -1
  94. package/packages/host/dist/alwayson.js.map +1 -1
  95. package/packages/host/dist/answergate.js +3 -2
  96. package/packages/host/dist/answergate.js.map +1 -1
  97. package/packages/host/dist/attentionstore.d.ts +4 -0
  98. package/packages/host/dist/attentionstore.js +17 -0
  99. package/packages/host/dist/attentionstore.js.map +1 -1
  100. package/packages/host/dist/auth.d.ts +28 -0
  101. package/packages/host/dist/auth.js +65 -2
  102. package/packages/host/dist/auth.js.map +1 -1
  103. package/packages/host/dist/cli/pair.d.ts +4 -0
  104. package/packages/host/dist/cli/pair.js +18 -5
  105. package/packages/host/dist/cli/pair.js.map +1 -1
  106. package/packages/host/dist/composer.d.ts +6 -0
  107. package/packages/host/dist/composer.js +7 -1
  108. package/packages/host/dist/composer.js.map +1 -1
  109. package/packages/host/dist/ctxlog/ctxagents.d.ts +0 -29
  110. package/packages/host/dist/ctxlog/ctxagents.js +41 -8
  111. package/packages/host/dist/ctxlog/ctxagents.js.map +1 -1
  112. package/packages/host/dist/der.d.ts +19 -0
  113. package/packages/host/dist/der.js +51 -0
  114. package/packages/host/dist/der.js.map +1 -0
  115. package/packages/host/dist/doctor.d.ts +2 -0
  116. package/packages/host/dist/doctor.js +10 -2
  117. package/packages/host/dist/doctor.js.map +1 -1
  118. package/packages/host/dist/extensions/desktop/host/auto.d.mts +71 -0
  119. package/packages/host/dist/extensions/desktop/host/auto.mjs +114 -0
  120. package/packages/host/dist/extensions/desktop/host/auto.mjs.map +1 -0
  121. package/packages/host/dist/extensions/desktop/host/desktop.d.mts +51 -0
  122. package/packages/host/dist/extensions/desktop/host/desktop.mjs +113 -0
  123. package/packages/host/dist/extensions/desktop/host/desktop.mjs.map +1 -0
  124. package/packages/host/dist/extensions/desktop/host/helperbin.d.mts +71 -0
  125. package/packages/host/dist/extensions/desktop/host/helperbin.mjs +160 -0
  126. package/packages/host/dist/extensions/desktop/host/helperbin.mjs.map +1 -0
  127. package/packages/host/dist/extensions/desktop/host/index.d.mts +4 -0
  128. package/packages/host/dist/extensions/desktop/host/index.mjs +88 -0
  129. package/packages/host/dist/extensions/desktop/host/index.mjs.map +1 -0
  130. package/packages/host/dist/extensions/desktop/host/knobs.d.mts +52 -0
  131. package/packages/host/dist/extensions/desktop/host/knobs.mjs +81 -0
  132. package/packages/host/dist/extensions/desktop/host/knobs.mjs.map +1 -0
  133. package/packages/host/dist/extensions/desktop/host/probe.d.mts +57 -0
  134. package/packages/host/dist/extensions/desktop/host/probe.mjs +119 -0
  135. package/packages/host/dist/extensions/desktop/host/probe.mjs.map +1 -0
  136. package/packages/host/dist/extensions/desktop/host/records.d.mts +40 -0
  137. package/packages/host/dist/extensions/desktop/host/records.mjs +101 -0
  138. package/packages/host/dist/extensions/desktop/host/records.mjs.map +1 -0
  139. package/packages/host/dist/extensions/desktop/host/session.d.mts +86 -0
  140. package/packages/host/dist/extensions/desktop/host/session.mjs +318 -0
  141. package/packages/host/dist/extensions/desktop/host/session.mjs.map +1 -0
  142. package/packages/host/dist/extensions/desktop/host/setup.d.mts +28 -0
  143. package/packages/host/dist/extensions/desktop/host/setup.mjs +217 -0
  144. package/packages/host/dist/extensions/desktop/host/setup.mjs.map +1 -0
  145. package/packages/host/dist/extensions/desktop/host/status.d.mts +27 -0
  146. package/packages/host/dist/extensions/desktop/host/status.mjs +62 -0
  147. package/packages/host/dist/extensions/desktop/host/status.mjs.map +1 -0
  148. package/packages/host/dist/extensions/desktop/shared/wire.d.mts +206 -0
  149. package/packages/host/dist/extensions/desktop/shared/wire.mjs +174 -0
  150. package/packages/host/dist/extensions/desktop/shared/wire.mjs.map +1 -0
  151. package/packages/host/dist/git.d.ts +23 -6
  152. package/packages/host/dist/git.js +54 -26
  153. package/packages/host/dist/git.js.map +1 -1
  154. package/packages/host/dist/gitfetchfail.d.ts +24 -0
  155. package/packages/host/dist/gitfetchfail.js +62 -0
  156. package/packages/host/dist/gitfetchfail.js.map +1 -0
  157. package/packages/host/dist/gitlog.d.ts +4 -0
  158. package/packages/host/dist/gitlog.js +8 -3
  159. package/packages/host/dist/gitlog.js.map +1 -1
  160. package/packages/host/dist/gitnotify.d.ts +7 -1
  161. package/packages/host/dist/gitnotify.js +61 -1
  162. package/packages/host/dist/gitnotify.js.map +1 -1
  163. package/packages/host/dist/healthwatch.js +1 -1
  164. package/packages/host/dist/healthwatch.js.map +1 -1
  165. package/packages/host/dist/host.js +5 -1
  166. package/packages/host/dist/host.js.map +1 -1
  167. package/packages/host/dist/hoststore.js +4 -4
  168. package/packages/host/dist/hoststore.js.map +1 -1
  169. package/packages/host/dist/lantls.d.ts +145 -0
  170. package/packages/host/dist/lantls.js +337 -0
  171. package/packages/host/dist/lantls.js.map +1 -0
  172. package/packages/host/dist/main.js +10 -2
  173. package/packages/host/dist/main.js.map +1 -1
  174. package/packages/host/dist/net.d.ts +7 -0
  175. package/packages/host/dist/net.js +9 -0
  176. package/packages/host/dist/net.js.map +1 -1
  177. package/packages/host/dist/origin.d.ts +4 -0
  178. package/packages/host/dist/origin.js +6 -0
  179. package/packages/host/dist/origin.js.map +1 -1
  180. package/packages/host/dist/pair.d.ts +54 -5
  181. package/packages/host/dist/pair.js +139 -34
  182. package/packages/host/dist/pair.js.map +1 -1
  183. package/packages/host/dist/pairpake.d.ts +58 -0
  184. package/packages/host/dist/pairpake.js +208 -0
  185. package/packages/host/dist/pairpake.js.map +1 -0
  186. package/packages/host/dist/passive.d.ts +2 -0
  187. package/packages/host/dist/passive.js +4 -0
  188. package/packages/host/dist/passive.js.map +1 -1
  189. package/packages/host/dist/peers/client.d.ts +3 -1
  190. package/packages/host/dist/peers/client.js +4 -0
  191. package/packages/host/dist/peers/client.js.map +1 -1
  192. package/packages/host/dist/peers/routes.d.ts +6 -1
  193. package/packages/host/dist/peers/routes.js +39 -0
  194. package/packages/host/dist/peers/routes.js.map +1 -1
  195. package/packages/host/dist/permmode.js +3 -3
  196. package/packages/host/dist/plugins/pluginapi.js +13 -0
  197. package/packages/host/dist/plugins/pluginapi.js.map +1 -1
  198. package/packages/host/dist/plugins/pluginregistry.d.ts +6 -0
  199. package/packages/host/dist/plugins/pluginregistry.js.map +1 -1
  200. package/packages/host/dist/prepushcheck.d.ts +34 -0
  201. package/packages/host/dist/prepushcheck.js +157 -0
  202. package/packages/host/dist/prepushcheck.js.map +1 -0
  203. package/packages/host/dist/prs.js +2 -1
  204. package/packages/host/dist/prs.js.map +1 -1
  205. package/packages/host/dist/reponames.d.ts +26 -0
  206. package/packages/host/dist/reponames.js +119 -0
  207. package/packages/host/dist/reponames.js.map +1 -0
  208. package/packages/host/dist/repos.js +13 -0
  209. package/packages/host/dist/repos.js.map +1 -1
  210. package/packages/host/dist/routes/apigitcheck.d.ts +2 -0
  211. package/packages/host/dist/routes/apigitcheck.js +29 -0
  212. package/packages/host/dist/routes/apigitcheck.js.map +1 -0
  213. package/packages/host/dist/routes/apihost.js +28 -1
  214. package/packages/host/dist/routes/apihost.js.map +1 -1
  215. package/packages/host/dist/routes/apiio.js +2 -1
  216. package/packages/host/dist/routes/apiio.js.map +1 -1
  217. package/packages/host/dist/routes/apirepofiles.js +2 -1
  218. package/packages/host/dist/routes/apirepofiles.js.map +1 -1
  219. package/packages/host/dist/routes/apisystem.js +21 -3
  220. package/packages/host/dist/routes/apisystem.js.map +1 -1
  221. package/packages/host/dist/server/http.js +31 -5
  222. package/packages/host/dist/server/http.js.map +1 -1
  223. package/packages/host/dist/server/intentEvents.js +3 -2
  224. package/packages/host/dist/server/intentEvents.js.map +1 -1
  225. package/packages/host/dist/server/plugins.d.ts +13 -0
  226. package/packages/host/dist/server/plugins.js +26 -0
  227. package/packages/host/dist/server/plugins.js.map +1 -1
  228. package/packages/host/dist/server/sessionEvents.js +10 -9
  229. package/packages/host/dist/server/sessionEvents.js.map +1 -1
  230. package/packages/host/dist/server/wsops.js +3 -1
  231. package/packages/host/dist/server/wsops.js.map +1 -1
  232. package/packages/host/dist/sessionSearch.js +3 -1
  233. package/packages/host/dist/sessionSearch.js.map +1 -1
  234. package/packages/host/dist/sessions/apiretry.d.ts +14 -0
  235. package/packages/host/dist/sessions/apiretry.js +28 -2
  236. package/packages/host/dist/sessions/apiretry.js.map +1 -1
  237. package/packages/host/dist/sessions/core.d.ts +5 -0
  238. package/packages/host/dist/sysstat.js +10 -1
  239. package/packages/host/dist/sysstat.js.map +1 -1
  240. package/packages/host/dist/tokenscan.d.ts +4 -2
  241. package/packages/host/dist/tokenscan.js +5 -3
  242. package/packages/host/dist/tokenscan.js.map +1 -1
  243. package/packages/host/dist/transcript/transcriptparse.js +5 -2
  244. package/packages/host/dist/transcript/transcriptparse.js.map +1 -1
  245. package/packages/host/dist/turnagents.d.ts +8 -1
  246. package/packages/host/dist/turnagents.js +49 -16
  247. package/packages/host/dist/turnagents.js.map +1 -1
  248. package/packages/host/dist/updatekey.d.ts +22 -0
  249. package/packages/host/dist/updatekey.js +140 -0
  250. package/packages/host/dist/updatekey.js.map +1 -0
  251. package/packages/host/dist/webbundle.d.ts +15 -6
  252. package/packages/host/dist/webbundle.js +44 -7
  253. package/packages/host/dist/webbundle.js.map +1 -1
  254. package/packages/host/dist/wincpu.d.ts +13 -0
  255. package/packages/host/dist/wincpu.js +101 -0
  256. package/packages/host/dist/wincpu.js.map +1 -0
  257. package/packages/host/package.json +2 -1
  258. package/packages/web/dist/assets/demo-iVbE6mpm.js +26 -0
  259. package/packages/web/dist/assets/icons-D8RlmSLR.js +1 -0
  260. package/packages/web/dist/assets/index-DiZysQBa.css +1 -0
  261. package/packages/web/dist/assets/index-NA--kuEl.js +170 -0
  262. package/packages/web/dist/assets/noble-DV-r0OkE.js +1 -0
  263. package/packages/web/dist/assets/{react-CoSyT_xY.js → react-CjmvHigY.js} +2 -2
  264. package/packages/web/dist/assets/rolldown-runtime-4tVxZq34.js +1 -0
  265. package/packages/web/dist/assets/shared-CsHyApn-.js +51 -0
  266. package/packages/web/dist/assets/sheets-CDv71Sfe.js +256 -0
  267. package/packages/web/dist/assets/xterm-BHsKrDzh.js +16 -0
  268. package/packages/web/dist/build-id.txt +1 -1
  269. package/packages/web/dist/buildstamp.json +1 -1
  270. package/packages/web/dist/index.html +9 -9
  271. package/packages/web/dist/sw.js +2 -2
  272. package/release.json +7 -3
  273. package/packages/web/dist/assets/demo-DkCtg-SJ.js +0 -26
  274. package/packages/web/dist/assets/icons-B48F-4nA.js +0 -1
  275. package/packages/web/dist/assets/index-BYRdfW3T.js +0 -170
  276. package/packages/web/dist/assets/index-DcZrEQNl.css +0 -1
  277. package/packages/web/dist/assets/rolldown-runtime-BpQH8Ho1.js +0 -1
  278. package/packages/web/dist/assets/shared-DjRRLGfw.js +0 -50
  279. package/packages/web/dist/assets/sheets-BGN7Cs73.js +0 -248
  280. package/packages/web/dist/assets/xterm-C8zLTNGk.js +0 -16
package/CHANGELOG.md CHANGED
@@ -5,6 +5,47 @@ version — so entries are grouped by the release they shipped in. A fixed secur
5
5
  vulnerability in what ships is listed under a **Security:** bullet of the release that
6
6
  fixed it, with its CVE or GHSA id when one exists.
7
7
 
8
+ ## 0.1.19
9
+
10
+ ### New
11
+
12
+ - **A changed Markdown file can be shown rendered and edited from its diff.** The diff gets a rendered mode and a pencil. Back from the file lands on that diff, re-read, in the mode you left it.
13
+ - **A repo can be shown under a name you choose.** Set it from the repo's menu with "shown as…". It appears on every device in the launcher, tabs, Start sheets, pickers, search and push titles. Folders, ids and paths keep their own names.
14
+ - **Every reading on the connection strip has a chart behind its tap.** The health sheet charts ping, route, connection, unsaved items, and sends and data by source over 1h, 24h, 7d and 30d. Drops, route moves and what is waiting are listed, and time the app was off screen shows as not measured.
15
+ - **The connection charts share one sticky range control.** It says once how to read a value and what not measured looks like. A chart waits for three measured minutes before it draws.
16
+ - **A crash finding can be sent to a session to be fixed.** Every finding names its source.
17
+ - **The context sheet's head scrolls away with its charts on a phone.** The conversation name and the two doors no longer hold a quarter of the screen. They sit on one gutter with the charts.
18
+ - **The launcher's top gets a drag grip once it needs a second line, and the agent list gets the header's grip.** A shortened agent list scrolls.
19
+
20
+ ### Fixed
21
+
22
+ - **A session that was retried no longer looks like the retry failed.** "Retried (2/8), waiting for the reply" clears as soon as the first reply arrives, not at the end of the turn.
23
+ - **Subagents of a dynamic workflow are now counted everywhere.** One session showed 2 agents and $22 when 83 agents and about $207 had been spent. Each agent's row says what it was launched to do.
24
+ - **The context charts no longer slope between readings.** The window holds and steps at each request, and a compaction drops straight to its measured size instead of reading as an hour of draining.
25
+ - **A message was sometimes not delivered when ultracode was on.** The composer's labelled top rule was not recognised, so text that matched the proposal was taken as already typed and sent as an empty box.
26
+
27
+ ## 0.1.18
28
+
29
+ ### New
30
+
31
+ - **Another desktop that is waiting on you is named on the launcher.** A chip beside the desktop key shows what is waiting elsewhere. One tap switches to that desktop and lands on the item. The background connection now watches every saved desktop, and an alert starts with the desktop's name.
32
+ - **Pair and use a desktop on Wi-Fi without Tailscale.** Pairing by code is sealed, so the code never crosses the network in the clear. The phone pins the desktop's key and refuses a swapped one in words. Over-the-air updates are signed by the desktop and checked on the phone.
33
+ - **Lock the app with your fingerprint or screen lock.** Saved sign-ins can be sealed behind the same prompt. Settings says what locking costs each desktop before you choose, and offers a way out if the seal gets stuck.
34
+ - **Run the pre-push checks without pushing.** In the git view, Run checks shows a verdict on each unpushed commit. A commit that passed offers Push up to here, which leaves newer commits local.
35
+ - **The git sheet says how old the incoming count is, and fetches when it opens.** A stale zero now reads ↓? instead of claiming you are up to date.
36
+ - **The first screen says what the app is.** It tells you the desktop half comes first and gives the pair command. It also says to scan the QR with the phone's camera app.
37
+ - **Plain HTTP addresses are warned about.** The phone refuses fingerprints and over-the-air updates there and says why.
38
+ - **The queue strip folds away.** Queued, sending and not-delivered rows go behind the corner button. A row that is moving keeps the fold, and a new row or a failure brings it back.
39
+
40
+ ### Fixed
41
+
42
+ - **The "% of week" no longer reads wildly high on a desktop that does little of the week's work.** It refuses a figure that a paired desktop has outspent, and says so where the share would be. Settings can count every paired desktop instead.
43
+ - **A plan with no 5-hour window says so in one row** instead of showing an empty bar and a complaint about a missing reset time.
44
+ - **Reconnecting to a desktop without voice dictation no longer crashes the page.**
45
+ - **A signing-key conflict on an app install no longer offers a retry that cannot work.** The card explains the conflict, and the button says to uninstall the old copy first and what that costs in data.
46
+ - **A network failure while pulling is one info notice naming every repo.** A repo is filed as needing you only after three failures in a row, and a failed fetch says why.
47
+ - **The upstream-age note in the git sheet keeps its height when the branch list opens.**
48
+
8
49
  ## 0.1.17
9
50
 
10
51
  ### Fixed
package/NOTICES.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Third-party notices
2
2
 
3
3
  waterplane is MIT-licensed (see `LICENSE`). This file lists the runtime
4
- (non-dev) dependencies actually shipped across the three workspaces
5
- (`packages/shared`, `packages/host`, `packages/web`) and their licenses, as
4
+ (non-dev) dependencies actually shipped across the three workspaces (`packages/shared`, `packages/host`,
5
+ `packages/web`) and the Android shell (`packages/app`), and their licenses, as
6
6
  declared in each package's own `package.json` under `node_modules`.
7
7
 
8
8
  No Anthropic code is bundled in this project. It only spawns and talks to a
@@ -15,20 +15,31 @@ not a redistribution of Claude Code.
15
15
  | Package | Version | License | Project |
16
16
  | --- | --- | --- | --- |
17
17
  | `@xterm/headless` | 5.5.0 | MIT | https://github.com/xtermjs/xterm.js |
18
- | `node-pty` | 1.1.0 | MIT | https://github.com/microsoft/node-pty |
18
+ | `node-pty` | 1.2.0-beta.15 | MIT | https://github.com/microsoft/node-pty |
19
19
  | `web-push` | 3.6.7 | **MPL-2.0** | https://github.com/web-push-libs/web-push |
20
- | `ws` | 8.21.0 | MIT | https://github.com/websockets/ws |
20
+ | `ws` | 8.22.0 | MIT | https://github.com/websockets/ws |
21
21
 
22
22
  ## `packages/web`
23
23
 
24
24
  | Package | Version | License | Project |
25
25
  | --- | --- | --- | --- |
26
- | `@xterm/addon-fit` | 0.10.0 | MIT | https://github.com/xtermjs/xterm.js |
27
- | `@xterm/xterm` | 5.5.0 | MIT | https://github.com/xtermjs/xterm.js |
28
- | `lucide-react` | 1.21.0 | ISC | https://lucide.dev |
26
+ | `@noble/hashes` | 2.4.0 | MIT | https://paulmillr.com/noble/ |
27
+ | `@noble/post-quantum` | 0.7.1 | MIT | https://paulmillr.com/noble/ |
28
+ | `@xterm/addon-fit` | 0.11.0 | MIT | https://github.com/xtermjs/xterm.js |
29
+ | `@xterm/xterm` | 6.0.0 | MIT | https://github.com/xtermjs/xterm.js |
30
+ | `lucide-react` | 1.61.0 | ISC | https://lucide.dev |
31
+ | `qrcode-generator` | 2.0.4 | MIT | https://github.com/kazuhikoarase/qrcode-generator |
29
32
  | `react` | 18.3.1 | MIT | https://reactjs.org/ |
30
33
  | `react-dom` | 18.3.1 | MIT | https://reactjs.org/ |
31
34
 
35
+ ## `packages/app` (the Android shell)
36
+
37
+ | Package | Version | License | Project |
38
+ | --- | --- | --- | --- |
39
+ | `@capacitor/android` | 8.5.3 | MIT | https://capacitorjs.com |
40
+ | `@capacitor/core` | 8.5.3 | MIT | https://capacitorjs.com |
41
+ | `@capacitor/ios` | 8.5.3 | MIT | https://capacitorjs.com |
42
+
32
43
  ## `packages/extensions/ocr` (downloaded to the phone, not in the web bundle)
33
44
 
34
45
  The text-in-marks extension's engine is copied from these packages into its `assets/` folder at
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # waterplane
2
2
 
3
3
  [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/16195/badge)](https://www.bestpractices.dev/projects/16195)
4
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/waterplane-dev/waterplane/badge)](https://scorecard.dev/viewer/?uri=github.com/waterplane-dev/waterplane)
4
5
 
5
6
  <!-- site:intro -->
6
7
  **An IDE, rethought for the phone in the age of agentic coding.** The agent writes the code; what
@@ -28,6 +29,48 @@ macOS, Linux, Windows via WSL2 · Android app or web app, no iOS app yet
28
29
  will and when a fresh session is cheaper; run a build without a session; queue prompts for after
29
30
  the reset and give a session a budget.
30
31
 
32
+ <!-- site:why -->
33
+ ### Why I wrote it
34
+
35
+ waterplane started as my own itch, and that is still what it is at its core. Its first name was
36
+ USAMC, "unsuck agentic mobile coding".
37
+
38
+ - **What I build needs an emulator.** Android apps and Garmin watch apps: build, run it in the
39
+ emulator or the simulator, look at the screen. A cloud sandbox cannot do that, so the agent has
40
+ to work on my own machine.
41
+ - **The agents need to be logged in as me.** App stores, registries, admin consoles. Handing my
42
+ credentials to a hosted agent is not what those services are for, and not an idea I was
43
+ comfortable with.
44
+ - **I am away a lot, and the agents always need something from me.** A decision, an approval, a
45
+ look at a screenshot. Auto-approve gets around some of it; do you want that on a system with
46
+ real users?
47
+
48
+ So I did what everyone does: NoMachine to the Mac, VS Code to open remote sessions, a few tricks on
49
+ top. Every one of them was annoying from a phone, inconvenient, and not something I wanted exposed.
50
+ I tried the other projects that put an agent on a phone, a lot of them; none fitted my requirements
51
+ and the way I work completely. waterplane is what came out of that.
52
+
53
+ **And many projects at once.** With agents doing the typing, people work on many projects at the
54
+ same time. VS Code gives you one project per window; the Claude Code mobile app gives you one mixed
55
+ list of conversations. waterplane keeps dozens of repos apart and in view at the same time. I run
56
+ coding sessions across many projects with 100+ sessions open, and at peak hundreds of local agents
57
+ at once, on a Mac Studio (M4, 64 GB). It works the same at a smaller scale on any machine where
58
+ Claude Code runs, with little overhead.
59
+
60
+ Two more things I was not at ease with:
61
+
62
+ - **Credentials and admin logins permanently within reach of agents.** There is a vault extension
63
+ for this: a secret is released to one command for one run, with a fingerprint on the phone. It is
64
+ experimental and still private; it may be in a release soon, tagged experimental and off by
65
+ default. It has been tortured only by AI agents and reviewed by nobody else, so do not trust it
66
+ yet. And it has a limit no vault can move: a compromised desktop is a compromised desktop, and a
67
+ vault makes the attacker's work harder, not impossible.
68
+ - **Remote desktop to a Mac from a phone is miserable.** Every solution I tried was. It was
69
+ surprisingly easy to let Claude build an alternative into waterplane that is actually usable from
70
+ a phone: the [live desktop view](packages/extensions/desktop/README.md), experimental, macOS and Windows,
71
+ shipped off by default.
72
+ <!-- /site:why -->
73
+
31
74
  <!-- site:desk -->
32
75
  ### Not only on the phone
33
76
 
@@ -165,8 +208,9 @@ HTTPS + MagicDNS in the tailnet admin console once, then open `https://<machine>
165
208
  tailscale serve --bg --https=443 http://127.0.0.1:8770
166
209
  ```
167
210
 
168
- Or use the Android app, which needs no HTTPS: the APK is on the
169
- [latest release](https://github.com/waterplane-dev/waterplane/releases/latest).
211
+ Or use the Android app, which needs no HTTPS: get it on
212
+ [Google Play](https://play.google.com/store/apps/details?id=dev.waterplane.app), or download the
213
+ APK from the [latest release](https://github.com/waterplane-dev/waterplane/releases/latest).
170
214
 
171
215
  Windows, Docker, agent install, notifications, a second phone or desktop, the background service,
172
216
  updating and every environment variable: [docs/install.md](docs/install.md).
package/SECURITY.md CHANGED
@@ -87,6 +87,60 @@ summary:
87
87
  and as the WebSocket subprotocol `waterplane-tok.<tok>` on the upgrade (the host
88
88
  echoes only the base protocol, never the token). No token is valid while
89
89
  no password is set.
90
+ On the phone the page keeps its tokens in the app's private localStorage; the
91
+ APK's background link keeps a second copy of each gated desktop's token so it
92
+ can reconnect with the app swiped away, AES-256-GCM sealed under a non-exportable
93
+ Android Keystore key (`ShellLinkTokens`), bound to that desktop's id and address,
94
+ sent only to that desktop's saved tailnet address, deleted when the desktop is
95
+ removed, the token is refused or the link is switched off. **Stated residual:**
96
+ the key needs no user authentication, because nobody is holding the phone when
97
+ the service reconnects, so this protects against a copied file, not against code
98
+ running as this app. That residual holds only while the phone's sign-ins are not
99
+ locked: with *lock the desktop sign-ins* on (next bullet), the link keeps no copy at rest.
100
+ - **Sealed sign-ins (optional, off by default).** Settings › Connection ›
101
+ *lock the desktop sign-ins* (Android app) puts this phone's device tokens in one
102
+ blob under a Keystore key that each use opens with the fingerprint, or on a newer
103
+ shell the phone's PIN, and removes the plaintext. **It guards against a copy of
104
+ the app's storage (a backup, a pulled file), not against a hand holding the
105
+ unlocked phone** (that is the app lock, and the process holds the tokens in plaintext
106
+ memory while running). A desktop with no password has no token, so there is nothing
107
+ to lock for it. The background link, per APK: a newer app keeps each token in
108
+ memory only (alerts continue after a swipe-away until the phone restarts or
109
+ Android stops the app, then "locked — open the app"); an older app is handed no
110
+ token, so alerts from gated desktops wait until the app is opened. Nothing on the
111
+ host changes. **Stated residuals:** a token the desktop revoked stays in the blob
112
+ until the next rewrap; a stray bare token that belongs to no saved desktop can stay
113
+ plaintext (the row says so); passive (kids') tokens are not sealed; on an older
114
+ app the start prompt is the fingerprint only, and on a plain-HTTP route that app
115
+ refuses the key, so its card stays until the route is the tailnet or HTTPS (the row
116
+ refuses to lock in that state, and the host setup form refuses to save a plain
117
+ address while that app has both locks on); the recents thumbnail is as before; and a lost key
118
+ (screen lock removed, or a new fingerprint on a fingerprint-only key) loses the
119
+ sign-ins, so a tailnet-gated desktop must be paired again. A missing sensor never
120
+ counts as a lost key, and a lost one's blob is set aside on the phone
121
+ (`tokenSealDead`), not deleted; nothing restores it yet, and the next lock's new key
122
+ makes it unreadable. A key that is dead but not worded as such by an OEM stays a
123
+ refusal, and the person's way out is *forget the locked sign-ins* (confirmed, behind
124
+ the app lock): it discards the blob by hand, which gives a stranger nothing. "Nothing
125
+ connects before the unlock" is true of the socket only: tokenless HTTP, such as the
126
+ app-update check, runs first, carrying no token.
127
+ - **App lock (optional, off by default).** The Android app can require the
128
+ phone's fingerprint, or on a newer shell its PIN, before it shows its screen
129
+ (Settings › Connection › *lock this app*). **It is a hold-the-phone defence at
130
+ UI level and nothing more.** No host route, token, gate or wire message changes;
131
+ deny-until-set, token rotation, the passive allowlist and the origin checks are
132
+ untouched, and **the device token's storage is unchanged** unless *lock the desktop
133
+ sign-ins* is on (localStorage, plus the sealed copy above). The page stays mounted behind an opaque card. Stated
134
+ residuals: anyone who has the phone's PIN passes it (the PIN fallback, or by
135
+ enrolling a finger); the recents thumbnail, and the frame the WebView shows on
136
+ wake before the page's JS runs, still show the last screen. The lock fails
137
+ closed: a cancelled, failed, locked-out or unrecognised refusal never opens the
138
+ app, at any repeat count. It opens and switches itself off (saying so) only when
139
+ the phone's own credential had to change the key, the app's saved check is
140
+ unreadable, or a fresh status read confirms no sensor or nothing to check with.
141
+ So on an old APK with a dead sensor that still reports hardware, or on a plain
142
+ HTTP route (where the old APK's key use is refused), the way out is a newer APK,
143
+ another route (the tailnet or HTTPS), or clearing the app's data.
90
144
  - **LAN identity.** `<state dir>/lan-identity.json` (0600) holds a `hostId`
91
145
  and a secret. `GET /api/lan/identity` hands them out to loopback and
92
146
  Tailscale callers ONLY — refused (403) for every other peer kind before the
@@ -97,8 +151,137 @@ summary:
97
151
  the thing answering at a LAN address holds the secret; it cannot resist an
98
152
  **active relay** on the person's own Wi-Fi that forwards the probe to the
99
153
  real desktop and then reads the plain-HTTP traffic (the token included).
154
+ With the token that relay can drive the desktop. It could also answer the app
155
+ with a forged page. So the Android app asks of every route whether it is
156
+ *protected* (`isPlainRoute` in shared, mirrored natively by `ShellRoute.java`),
157
+ and on one that is not it refuses every fingerprint use, every over-the-air
158
+ update and every app download or install, and warns in the address form, the
159
+ health sheet, the strip ("LAN, not protected") and a toast.
160
+ **The protected-route rule.** A route is protected only if it is exactly one
161
+ of: loopback; a Tailscale address or MagicDNS name over `http`, or `https` to a
162
+ `*.ts.net` name, unless a newer app reports that the phone has no tailnet
163
+ (then a tailnet name proves nothing: with Tailscale off, `100.x` goes to the
164
+ Wi-Fi gateway); the desktop's own TLS address on a newer app that holds its
165
+ pin; or an `https` address the person typed themselves. Everything else is
166
+ plain, **whatever its scheme**: a relay with a public certificate for its own
167
+ name passes `https` validation. A reply from a desktop that has not proved
168
+ itself (anything not sealed by the pairing code, or not received over a
169
+ protected route) never sets a saved desktop's address, key, pin or identity,
170
+ so a relay cannot walk a row onto itself. **Stated residuals:** a row saved
171
+ by an older app may already hold an address someone planted (a non-`ts.net`
172
+ `https` one reads as plain and the app opens an empty field to retype it; a
173
+ planted `ts.net` or `100.x` one cannot be told apart); and a person who taps
174
+ "pair" on a pairing link someone else made pairs with whoever made it (the
175
+ confirm says so). A browser on plain HTTP has no such defence, because the page
176
+ itself is what arrives unprotected, and the token is still exposed to the
177
+ relay. An older app keeps the older answer (tailnet and HTTPS only).
100
178
  Plain HTTP on a LAN has no defence against a man in the middle; if that
101
179
  network is not trusted, do not enable `WATERPLANE_ALLOW_PUBLIC`.
180
+ - **LAN TLS and the pin** (`packages/host/src/lantls.ts`). Only where
181
+ `WATERPLANE_ALLOW_PUBLIC=1` already binds LAN addresses, the host also
182
+ listens on `WATERPLANE_TLS_PORT` (default port + 1) on those same addresses
183
+ with the same handler and the same gate, so a caller there is still peer kind
184
+ `lan` and needs its token. Off with `WATERPLANE_TLS=0`; never on loopback or
185
+ the tailnet; nothing without the opt-in. The key is P-256 in
186
+ `<state dir>/lan-tls.json` (0600), kept across restarts; the certificate is
187
+ self-signed and re-issued on the same key when the addresses change, so the
188
+ phone's pin (SHA-256 of the key) survives. The app stores the pin per desktop
189
+ and enforces it in the WebView, the background link and the pull service; a
190
+ desktop with a pin is then never reached over plain HTTP on a LAN address.
191
+ A self-signed port does not serve the web app (it answers a notice page): a
192
+ browser click-through would look protected and is not. With
193
+ `WATERPLANE_TLS_CERT` + `WATERPLANE_TLS_KEY` (a person's own leaf from a CA;
194
+ a self-signed one is read as self-signed) it serves the app, and the pin
195
+ follows that leaf. **What the pin does not defend:** the
196
+ WebView's handler runs only when validation fails, so a certificate valid
197
+ under a CA the phone trusts would skip the pin inside the WebView (pinned
198
+ addresses are private IPs and `.local` names, which public CAs do not issue,
199
+ and the app trusts no user-added CA); a first install of the APK over the
200
+ network (Android accepts any signature then); code already running in the
201
+ page; a browser; and dropping packets. The pin is a property of a newer APK:
202
+ a shipped one learns it and ignores it, and stays on plain LAN with the
203
+ warnings. The way out of a pinned desktop whose TLS port is off is a
204
+ confirmed "use plain HTTP for this desktop" on its row; nothing does it
205
+ by itself. The spike behind this (measured on two emulator WebViews only)
206
+ is in `docs/notes/protocol-and-security.md`. **None of this has been tried on
207
+ a real phone yet.**
208
+ - **Signed updates** (`packages/host/src/updatekey.ts`, `webbundle.ts`,
209
+ `packages/web/src/updateTrust.ts`, `ShellBundleVerify.java`). Each desktop
210
+ signs its web bundle's manifest with its own P-256 key
211
+ (`<state dir>/update-key.json`, 0600), not a project key: people build their
212
+ own bundles, and a private key shipped to every desktop is no key. The
213
+ manifest carries `signed` (hostId, build id, `gen`, the file list with hashes)
214
+ and `sig`. A phone learns the desktop's public key from `welcome` only when
215
+ the delivering socket's address was protected (above), so a relay cannot
216
+ hand it one. Once a phone holds a key, an update that is unsigned, badly
217
+ signed, for another desktop or older than the last it took (`gen`, which
218
+ rises when the served build id changes, so a real revert still works) is
219
+ refused and nothing is written. The check runs in the page (works on a
220
+ shipped APK) and again natively on a newer one, which also asks in its own
221
+ dialog the first time it sees a desktop's key, a changed key, or a
222
+ restored desktop; page code cannot tap it. A new key (a wiped state dir) or a
223
+ lower `gen` (a restored one) waits for that answer. **What it does not do:** a
224
+ signature proves the bundle came from the desktop you paired, never that
225
+ its code is good. **A hostile desktop can still ship anything**, and a
226
+ paired host stays fully trusted (last section). A passive (kids') phone opens
227
+ no socket, so it never learns a key and takes updates as before, over a
228
+ route that is protected or it would take none. A shipped APK without the
229
+ native check has the page's check only, so page code that is already
230
+ compromised can still persist a bundle there. A desktop that cannot read or
231
+ write its key file serves the bundle unsigned (logged once): a phone that
232
+ holds a key then refuses it and says to update or repair the desktop.
233
+ - **Pairing with no tailnet** (`pairpake.ts`, `shared/pake.ts`). A phone
234
+ pairs by CPace over ristretto255 keyed by the 8-character code, on the plain
235
+ port: the phone proves it knows the code first, the host answers with a
236
+ sealed reply (token, `hostId`, TLS pin, update key, LAN identity), and the
237
+ seal authenticates the host. A relay gets one online guess per attempt; an
238
+ impostor host answering `start` gets at most five, one per share it sends,
239
+ which is all a phone will answer. A real host offers the five newest open
240
+ codes, each confirm checked against all of them in constant time, so with
241
+ more than five open an older code never pairs this way: its phone is told
242
+ the desktop does not know that code and to show a new QR, which then works,
243
+ and the attempt counts as a miss against the five offered. A session lives
244
+ 60 s and is deleted on its first `finish`.
245
+ 10 wrong confirms close a code *for this pairing only*: it still redeems over
246
+ the tailnet, so a device on the Wi-Fi can stop an encrypted pairing (as
247
+ dropping packets does) but cannot burn a code meant for the tailnet. The
248
+ desktop refuses with no password set. **A code never crosses a plain socket
249
+ and lives:** on one that is not loopback, the tailnet, `tailscale serve` or
250
+ TLS, `/pair/<code>` and `/pair/<code>/apk` burn the code and show a page with
251
+ no link and no APK; a LAN-only desktop's screen shows the deep-link QR, which
252
+ goes camera to app. A code already sent plain is dead on that phone. The
253
+ link also carries a 128-bit commitment to the key and pin, which the seal
254
+ must match. **Stated residuals:** a person who accepts the question that
255
+ appears for an older desktop (it cannot tell an old desktop from someone
256
+ blocking encrypted pairing) spends that code and token in the clear, as
257
+ before; a relay can fill the miss count and stop pairing; whether every
258
+ camera app opens the waterplane link is unverified, and the typed code runs the
259
+ same protocol; the first APK install over the network is unprotected.
260
+ `waterplane pair` prints the update key's fingerprint beside the code.
261
+ - **Read ticket** (`auth.ts`, `POST /api/auth/read-ticket`). An image tag or a
262
+ download link cannot carry a bearer, so a phone with no tailnet could not
263
+ load them. A signed device token buys a one-hour ticket (HMAC of device id,
264
+ scope and expiry; the key lives in memory, so a restart voids every
265
+ ticket) that is accepted as `?wt=` **only** on the two opaque read routes
266
+ (`OPAQUE_READ_ROUTES`), **only** over the TLS socket, GET/HEAD only, only
267
+ while the device row exists, and held to the row's scope (a passive phone's
268
+ ticket gets what a passive token gets). Access logs, shares and copies
269
+ strip `wt`. A ticket in a URL is a credential for those two read routes for
270
+ up to an hour: do not paste such a URL.
271
+ - **A phone with Tailscale off.** It reaches the desktop only on the Wi-Fi.
272
+ A shipped APK stays on plain LAN with name-based trust as before, except
273
+ that an update key it already holds still refuses a forged bundle. A newer
274
+ APK pairs by code, holds the pin and goes over TLS. A phone that never
275
+ learned a pin cannot tell a relay from the desktop on the first
276
+ plain contact; pairing by code is the fix. The password-change case: tokens
277
+ rotate as before, so a LAN-only phone must pair again, and its gate says so
278
+ rather than asking for a password that cannot work there.
279
+ On a shell that enforces pins, a page that knows Tailscale is off sends no
280
+ device token to a tailnet address, pinned or not, since only the Wi-Fi can
281
+ answer it. Until the shell's first network reading it holds the token back on
282
+ those addresses and learns no pin, for at most 8 seconds; a shell silent past
283
+ that falls to trust by name. The background link is not covered on an unpinned
284
+ row: it sends its token to the address it holds, as before.
102
285
  - **Pairing codes** (`packages/host/src/pair.ts`). `POST /api/pair/mint`
103
286
  sits behind the normal gate; the 8-character code lives 10 minutes, is kept
104
287
  in memory only as its sha256 (a restart invalidates every outstanding one),
@@ -169,19 +352,20 @@ summary:
169
352
  The shell does not inherit the daemon's API keys and key files
170
353
  (`*_API_KEY`, `*_KEY_FILE`, `*_KEYCHAIN` are unset in the pane), and its
171
354
  commands are never written to the session's prompt history.
172
- - **Live desktop** (`packages/extensions/desktop`, macOS, experimental). Watching is a
355
+ - **Live desktop** (`packages/extensions/desktop`, macOS, and Windows with the host in WSL; experimental). Watching is a
173
356
  stream on the same device-token socket and widens no gate. **Control is
174
357
  arbitrary input on the desktop** (clicks and keys reach every app, password
175
358
  managers and a logged-in terminal included), so it carries the same trust as
176
359
  the PTY and is a separate setting, **off by default**. It is gated on the
177
360
  host, not only on the phone: with it off, `stream.input` is dropped before it
178
- reaches the helper, and with it on it still needs the macOS Accessibility
179
- grant. A peer token never reaches a stream (`/api/peer/*` only), and no job,
180
- schedule or peer desktop opens a desktop stream. The helper `wp-desktop` runs
181
- as the desktop user, is signed with a stable identifier and holds only the
182
- Screen Recording and Accessibility grants a person gave it at the Mac (it asks
183
- macOS only when a person taps Fix on a setup row), and capture runs only while a
184
- view is open. macOS shows its own recording indicator the whole time.
361
+ reaches the helper, and with it on a Mac still needs the macOS Accessibility
362
+ grant. Windows asks no app for a grant, so there the host's switch is the only
363
+ gate. A peer token never reaches a stream (`/api/peer/*` only), and no job,
364
+ schedule or peer desktop opens a desktop stream. The helper `wp-desktop` runs as the desktop user and capture runs only while a
365
+ view is open. On a Mac it is signed with a stable identifier and holds only the
366
+ Screen Recording and Accessibility grants a person gave it there (it asks macOS
367
+ only when a person taps Fix on a setup row), and macOS shows its own recording
368
+ indicator the whole time. Windows shows no such indicator.
185
369
  - **Browser origins** (`packages/host/src/origin.ts`). Every `/api/*` request
186
370
  and WebSocket upgrade is checked on `Origin` and `Host`; only `http`/`https`
187
371
  origins naming this machine on a port it serves pass, plus exactly one
@@ -386,10 +570,10 @@ do so anyway, e.g.:
386
570
  running the daemon.
387
571
  - **A paired host is fully trusted by the Android shell.** The app's UI
388
572
  updates over the air from the host it is paired with
389
- (`packages/web/src/webUpdate.ts`); the manifest's sha256 hashes come from
390
- that same host, so they catch corruption, not a hostile host. A host you
391
- pair with can ship JS that runs with the shell's native plugins. Pair only
392
- with desktops you control.
573
+ (`packages/web/src/webUpdate.ts`). Its bundles are signed with a per-desktop
574
+ key the phone pins over a protected route, which stops a relay on the network,
575
+ never the desktop itself: a host you pair with can ship JS that runs with the
576
+ shell's native plugins. Pair only with desktops you control.
393
577
 
394
578
  If you're not sure whether something is in scope, report it privately anyway
395
579
  and let us sort it out together — a false positive costs us a few minutes; a
@@ -17,6 +17,8 @@ export interface DesktopInventoryInput {
17
17
  userOnlyPlugins?: string[];
18
18
  /** `mcpServers` keys of `~/.claude.json` `projects[<cwd realpath>]` — local scope (`claude mcp add`). */
19
19
  localMcp?: string[];
20
+ /** Context hooks of the user's settings.json; null = unreadable (a row says so). */
21
+ hooks?: ContextHook[] | null;
20
22
  }
21
23
  /** Every kind word `desktopInventory` writes (plugin skills carry their plugin in brackets after it). */
22
24
  export declare const DESKTOP_KINDS: {
@@ -30,6 +32,7 @@ export declare const DESKTOP_KINDS: {
30
32
  readonly localMcp: "local MCP server";
31
33
  readonly plugin: "plugin";
32
34
  readonly managedSkills: "managed plugin skills";
35
+ readonly hook: "context hook";
33
36
  };
34
37
  /** Why a plugin row has no skills listed: `enabledPlugins` turned it off. */
35
38
  export declare const PLUGIN_DISABLED = "disabled in settings.json enabledPlugins \u2014 its skills are not carried";
@@ -37,6 +40,18 @@ export declare const PLUGIN_DISABLED = "disabled in settings.json enabledPlugins
37
40
  export declare const PLUGIN_UNREAD = "enabled in settings, but not installed under the config dir \u2014 its skills are not sized here";
38
41
  /** The one row for plugin packs no file on this desktop names (claude.ai-managed, synced). */
39
42
  export declare const MANAGED_SKILLS = "skills from claude.ai-managed or synced plugins are not on disk here; the session view's skill listing (what a session's first request carried) is the measured count";
43
+ /** The two hook events whose output reaches the model's context; every other event injects nothing. */
44
+ export declare const CONTEXT_HOOK_EVENTS: readonly ["SessionStart", "UserPromptSubmit"];
45
+ export type ContextHookEvent = (typeof CONTEXT_HOOK_EVENTS)[number];
46
+ /** One file's commands on one event. A count, never the command text: that can hold a secret, and these rows reach the phone. */
47
+ export interface ContextHook {
48
+ event: ContextHookEvent;
49
+ /** The file that declares it, in the row's own path words. */
50
+ path: string;
51
+ commands: number;
52
+ }
53
+ /** Why a context-hook row has no number, and where its size is measured instead. */
54
+ export declare const hookUnsized: (h: Pick<ContextHook, "event" | "commands">) => string;
40
55
  /** `~/.claude.json` sits in the home dir, BESIDE the config dir, so its rows name it whole (a `~` path is already words). */
41
56
  export declare const USER_CLAUDE_JSON = "~/.claude.json";
42
57
  /** This desktop's always-on rows, user files first, then skills, then the unsized MCP rows. */
@@ -82,6 +97,12 @@ export interface DesktopSources {
82
97
  localMcp: string[];
83
98
  /** `.mcp.json` servers this desktop declined (`disabledMcpjsonServers`, in `.claude.json` or any settings file). */
84
99
  mcpjsonDeclined: string[];
100
+ /** Context hooks of the user's settings.json; null = that file is there but unreadable. */
101
+ hooks: ContextHook[] | null;
102
+ /** Context hooks of the repo's settings files: repo rows, unsized. */
103
+ projectHooks: ContextHook[];
104
+ /** The repo's settings files that are there but unreadable: one row each. */
105
+ projectHooksUnread: string[];
85
106
  }
86
107
  /**
87
108
  * Which plugins and MCP servers a session started in this repo on this desktop gets, out of the
@@ -94,7 +115,8 @@ export declare function desktopSources(i: DesktopSourcesInput): DesktopSources;
94
115
  export declare const MCP_DECLINED = "declined on this desktop (disabledMcpjsonServers) \u2014 not loaded";
95
116
  /**
96
117
  * The repo rows as THIS desktop loads them: a `.mcp.json` server it declined says so instead of
97
- * claiming tool schemas, and the plugins the repo's settings turn on are listed, unsized.
118
+ * claiming tool schemas, and the plugins and context hooks the repo's settings turn on are listed,
119
+ * unsized.
98
120
  */
99
- export declare function repoRowsOnThisDesktop(files: InstructionFile[], s: Pick<DesktopSources, "mcpjsonDeclined" | "projectPlugins">): InstructionFile[];
121
+ export declare function repoRowsOnThisDesktop(files: InstructionFile[], s: Pick<DesktopSources, "mcpjsonDeclined" | "projectPlugins"> & Partial<Pick<DesktopSources, "projectHooks" | "projectHooksUnread">>): InstructionFile[];
100
122
  export {};
@@ -3,7 +3,9 @@
3
3
  * auto-memory file of the repo's project slug, user skills, plugin skills (minus the plugins
4
4
  * `enabledPlugins` turns off, which get a row saying so), user and local-scope MCP servers, and ONE
5
5
  * unsized row for the managed/synced plugin packs no file here names — the static list is what
6
- * disk shows; a session's measured skill listing is the truth (`SessionCarried`).
6
+ * disk shows; a session's measured skill listing is the truth (`SessionCarried`). Hooks that can
7
+ * put text in context (`CONTEXT_HOOK_EVENTS`) are one unsized row per file and event: the log
8
+ * records the event, never the command, so only the session's measured "hook context" has a size.
7
9
  * The per-repo half is `agentfiles.ts` `instructionInventory()`; these rows carry `scope: "desktop"`
8
10
  * so a list can hold both and say which is which.
9
11
  *
@@ -29,6 +31,7 @@ export const DESKTOP_KINDS = {
29
31
  localMcp: "local MCP server",
30
32
  plugin: "plugin",
31
33
  managedSkills: "managed plugin skills",
34
+ hook: "context hook",
32
35
  };
33
36
  /** Why a plugin row has no skills listed: `enabledPlugins` turned it off. */
34
37
  export const PLUGIN_DISABLED = "disabled in settings.json enabledPlugins — its skills are not carried";
@@ -36,6 +39,15 @@ export const PLUGIN_DISABLED = "disabled in settings.json enabledPlugins — its
36
39
  export const PLUGIN_UNREAD = "enabled in settings, but not installed under the config dir — its skills are not sized here";
37
40
  /** The one row for plugin packs no file on this desktop names (claude.ai-managed, synced). */
38
41
  export const MANAGED_SKILLS = "skills from claude.ai-managed or synced plugins are not on disk here; the session view's skill listing (what a session's first request carried) is the measured count";
42
+ /** The two hook events whose output reaches the model's context; every other event injects nothing. */
43
+ export const CONTEXT_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit"];
44
+ // closed: `event` comes from this build's own CONTEXT_HOOK_EVENTS, read off this desktop's settings, never off the wire
45
+ const HOOK_REACH = { SessionStart: "every session", UserPromptSubmit: "every prompt" };
46
+ /** Why a context-hook row has no number, and where its size is measured instead. */
47
+ export const hookUnsized = (h) => {
48
+ const [runs, it] = h.commands === 1 ? ["a command", "it"] : ["commands", "them"];
49
+ return `runs ${runs} whose output the CLI adds to ${HOOK_REACH[h.event]}; a session's context view measures the size as "hook context", and /hooks lists ${it}`;
50
+ };
39
51
  /** `~/.claude.json` sits in the home dir, BESIDE the config dir, so its rows name it whole (a `~` path is already words). */
40
52
  export const USER_CLAUDE_JSON = "~/.claude.json";
41
53
  const D = { scope: "desktop" };
@@ -51,6 +63,24 @@ function skillsUnder(fs, dir) {
51
63
  }
52
64
  return out;
53
65
  }
66
+ const hookKind = (h, plugin) => `${DESKTOP_KINDS.hook} ${h.event} (${h.commands} ${h.commands === 1 ? "command" : "commands"}${plugin ? `, plugin ${plugin}` : ""})`;
67
+ const hookRow = (h, plugin) => ({ path: h.path, kind: hookKind(h, plugin), parts: [], unsized: hookUnsized(h) });
68
+ const hooksUnreadRow = (path) => ({ path, kind: `${DESKTOP_KINDS.hook}s`, parts: [], unsized: `${path} could not be read` });
69
+ /** The hooks an enabled plugin ships. The reader cannot tell missing from unreadable, so null is no rows; text that is not JSON says so. */
70
+ function pluginHookRows(fs, p) {
71
+ const path = `${trimEndChars(p.rel, "/")}/hooks/hooks.json`;
72
+ const text = fs.read(path);
73
+ if (text == null)
74
+ return [];
75
+ let json;
76
+ try {
77
+ json = JSON.parse(text);
78
+ }
79
+ catch {
80
+ return [hooksUnreadRow(path)];
81
+ }
82
+ return isObj(json) ? contextHooksOf(json, path).map((h) => hookRow(h, p.name)) : [hooksUnreadRow(path)];
83
+ }
54
84
  const skillKind = (base, text) => (skillDescChars(text) > 0 ? base : `${base} — no description found in its frontmatter`);
55
85
  /** This desktop's always-on rows, user files first, then skills, then the unsized MCP rows. */
56
86
  export function desktopInventory(i) {
@@ -82,6 +112,14 @@ export function desktopInventory(i) {
82
112
  for (const key of i.userOnlyPlugins ?? []) {
83
113
  out.push({ path: "settings.json", kind: `${DESKTOP_KINDS.plugin} ${key}`, parts: [], unsized: PLUGIN_UNREAD, ...D });
84
114
  }
115
+ if (i.hooks === null)
116
+ out.push({ ...hooksUnreadRow("settings.json"), ...D });
117
+ for (const h of i.hooks ?? [])
118
+ out.push({ ...hookRow(h), ...D });
119
+ // `plugins` holds the enabled ones only: a disabled plugin's hooks do not load, so they are never read.
120
+ for (const p of i.plugins ?? [])
121
+ for (const r of pluginHookRows(i.fs, p))
122
+ out.push({ ...r, ...D });
85
123
  if (i.userMcp == null) {
86
124
  out.push({ path: USER_CLAUDE_JSON, kind: DESKTOP_KINDS.userMcpServers, parts: [], unsized: "~/.claude.json could not be read", ...D });
87
125
  }
@@ -99,6 +137,23 @@ export function desktopInventory(i) {
99
137
  }
100
138
  const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
101
139
  const strs = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === "string") : []);
140
+ /** A settings-shaped object's context hooks, one entry per event with a command. A missing or non-object `hooks` is none. */
141
+ function contextHooksOf(json, path) {
142
+ const hooks = json.hooks;
143
+ if (!isObj(hooks))
144
+ return [];
145
+ const out = [];
146
+ for (const event of CONTEXT_HOOK_EVENTS) {
147
+ const groups = hooks[event];
148
+ if (!Array.isArray(groups))
149
+ continue;
150
+ const commands = groups.flatMap((g) => (isObj(g) && Array.isArray(g.hooks) ? g.hooks : []))
151
+ .filter((h) => isObj(h) && h.type === "command" && typeof h.command === "string").length;
152
+ if (commands)
153
+ out.push({ event, path, commands });
154
+ }
155
+ return out;
156
+ }
102
157
  /**
103
158
  * Which plugins and MCP servers a session started in this repo on this desktop gets, out of the
104
159
  * files that decide it: `enabledPlugins` (user < project < local; `false` turns a plugin off),
@@ -117,7 +172,7 @@ export function desktopSources(i) {
117
172
  takeEnabled(i.userSettings, null);
118
173
  for (const p of i.projectSettings)
119
174
  takeEnabled(p.json, p.path);
120
- const out = { plugins: [], outside: [], disabledPlugins: [], userOnlyPlugins: [], projectPlugins: [], userMcp: [], localMcp: [], mcpjsonDeclined: [] };
175
+ const out = { plugins: [], outside: [], disabledPlugins: [], userOnlyPlugins: [], projectPlugins: [], userMcp: [], localMcp: [], mcpjsonDeclined: [], hooks: [], projectHooks: [], projectHooksUnread: [] };
121
176
  const userKeys = new Set();
122
177
  if (i.installed !== "missing") {
123
178
  if (!isObj(i.installed) || !isObj(i.installed.plugins))
@@ -153,6 +208,16 @@ export function desktopSources(i) {
153
208
  else
154
209
  out.userOnlyPlugins.push(key);
155
210
  }
211
+ if (isObj(i.userSettings))
212
+ out.hooks = contextHooksOf(i.userSettings, "settings.json");
213
+ else if (i.userSettings !== "missing")
214
+ out.hooks = null;
215
+ for (const p of i.projectSettings) {
216
+ if (isObj(p.json))
217
+ out.projectHooks.push(...contextHooksOf(p.json, p.path));
218
+ else if (p.json !== "missing")
219
+ out.projectHooksUnread.push(p.path);
220
+ }
156
221
  const declined = new Set();
157
222
  for (const p of i.projectSettings)
158
223
  if (isObj(p.json))
@@ -178,7 +243,8 @@ export function desktopSources(i) {
178
243
  export const MCP_DECLINED = "declined on this desktop (disabledMcpjsonServers) — not loaded";
179
244
  /**
180
245
  * The repo rows as THIS desktop loads them: a `.mcp.json` server it declined says so instead of
181
- * claiming tool schemas, and the plugins the repo's settings turn on are listed, unsized.
246
+ * claiming tool schemas, and the plugins and context hooks the repo's settings turn on are listed,
247
+ * unsized.
182
248
  */
183
249
  export function repoRowsOnThisDesktop(files, s) {
184
250
  const declined = new Set(s.mcpjsonDeclined);
@@ -188,6 +254,10 @@ export function repoRowsOnThisDesktop(files, s) {
188
254
  for (const p of s.projectPlugins) {
189
255
  rows.push({ path: p.path, kind: `project ${DESKTOP_KINDS.plugin} ${p.key}`, parts: [], unsized: "enabled in this repo's settings — its skills load from the plugin cache and are not sized here" });
190
256
  }
257
+ for (const path of s.projectHooksUnread ?? [])
258
+ rows.push(hooksUnreadRow(path));
259
+ for (const h of s.projectHooks ?? [])
260
+ rows.push(hookRow(h));
191
261
  return rows;
192
262
  }
193
263
  //# sourceMappingURL=desktopfiles.js.map