waterplane 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/NOTICES.md +18 -7
- package/README.md +43 -0
- package/SECURITY.md +196 -12
- package/node_modules/@waterplane/shared/dist/desktopfiles.d.ts +24 -2
- package/node_modules/@waterplane/shared/dist/desktopfiles.js +73 -3
- package/node_modules/@waterplane/shared/dist/desktopfiles.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/hosts.d.ts +31 -0
- package/node_modules/@waterplane/shared/dist/hosts.js +46 -1
- package/node_modules/@waterplane/shared/dist/hosts.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/hosturl.d.ts +55 -0
- package/node_modules/@waterplane/shared/dist/hosturl.js +85 -0
- package/node_modules/@waterplane/shared/dist/hosturl.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/index.d.ts +4 -0
- package/node_modules/@waterplane/shared/dist/index.js +4 -0
- package/node_modules/@waterplane/shared/dist/index.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/installrow.d.ts +6 -1
- package/node_modules/@waterplane/shared/dist/installrow.js +20 -1
- package/node_modules/@waterplane/shared/dist/installrow.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/lanroute.d.ts +5 -0
- package/node_modules/@waterplane/shared/dist/lanroute.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/pairing.d.ts +96 -2
- package/node_modules/@waterplane/shared/dist/pairing.js +71 -3
- package/node_modules/@waterplane/shared/dist/pairing.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/pake.d.ts +131 -0
- package/node_modules/@waterplane/shared/dist/pake.js +272 -0
- package/node_modules/@waterplane/shared/dist/pake.js.map +1 -0
- package/node_modules/@waterplane/shared/dist/peer.d.ts +63 -0
- package/node_modules/@waterplane/shared/dist/peer.js +41 -0
- package/node_modules/@waterplane/shared/dist/peer.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/plugin/api.d.ts +15 -0
- package/node_modules/@waterplane/shared/dist/plugin/version.d.ts +5 -1
- package/node_modules/@waterplane/shared/dist/plugin/version.js +5 -1
- package/node_modules/@waterplane/shared/dist/plugin/version.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/sessioncost/costwords.d.ts +7 -2
- package/node_modules/@waterplane/shared/dist/sessioncost/costwords.js +6 -3
- package/node_modules/@waterplane/shared/dist/sessioncost/costwords.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/waiting.d.ts +64 -0
- package/node_modules/@waterplane/shared/dist/waiting.js +86 -0
- package/node_modules/@waterplane/shared/dist/waiting.js.map +1 -0
- package/node_modules/@waterplane/shared/dist/wire/messages/hostcore.d.ts +3 -0
- package/node_modules/@waterplane/shared/dist/wire/notify.d.ts +2 -0
- package/node_modules/@waterplane/shared/dist/wire/notify.js +2 -0
- package/node_modules/@waterplane/shared/dist/wire/notify.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/wire/repo.d.ts +25 -1
- package/node_modules/@waterplane/shared/dist/wire/repo.js.map +1 -1
- package/node_modules/@waterplane/shared/dist/wire/sysstat.d.ts +7 -0
- package/node_modules/@waterplane/shared/dist/wire/telemetry.d.ts +3 -0
- package/node_modules/@waterplane/shared/package.json +3 -0
- package/npm-shrinkwrap.json +30 -2
- package/package.json +2 -1
- package/packages/extensions/desktop/helper/Capture.swift +245 -0
- package/packages/extensions/desktop/helper/H264.swift +226 -0
- package/packages/extensions/desktop/helper/Info.plist +20 -0
- package/packages/extensions/desktop/helper/Input.swift +195 -0
- package/packages/extensions/desktop/helper/Jpeg.swift +81 -0
- package/packages/extensions/desktop/helper/PROTOCOL.md +366 -0
- package/packages/extensions/desktop/helper/Params.swift +133 -0
- package/packages/extensions/desktop/helper/Probe.swift +119 -0
- package/packages/extensions/desktop/helper/Protocol.swift +224 -0
- package/packages/extensions/desktop/helper/Request.swift +51 -0
- package/packages/extensions/desktop/helper/Responsibility.swift +133 -0
- package/packages/extensions/desktop/helper/Server.swift +334 -0
- package/packages/extensions/desktop/helper/Synthetic.swift +281 -0
- package/packages/extensions/desktop/helper/build.sh +53 -0
- package/packages/extensions/desktop/helper/main.swift +79 -0
- package/packages/extensions/desktop/helper/selftest-driver +626 -0
- package/packages/extensions/desktop/helper/selftest.sh +16 -0
- package/packages/extensions/desktop/helper-win/Capture.cs +299 -0
- package/packages/extensions/desktop/helper-win/H264.cs +340 -0
- package/packages/extensions/desktop/helper-win/Input.cs +257 -0
- package/packages/extensions/desktop/helper-win/Jpeg.cs +75 -0
- package/packages/extensions/desktop/helper-win/Main.cs +118 -0
- package/packages/extensions/desktop/helper-win/Native.cs +276 -0
- package/packages/extensions/desktop/helper-win/Params.cs +138 -0
- package/packages/extensions/desktop/helper-win/Protocol.cs +206 -0
- package/packages/extensions/desktop/helper-win/Server.cs +304 -0
- package/packages/extensions/desktop/helper-win/Synthetic.cs +88 -0
- package/packages/extensions/desktop/helper-win/build.sh +57 -0
- package/packages/extensions/desktop/helper-win/selftest.sh +36 -0
- package/packages/extensions/desktop/waterplane-plugin.json +24 -0
- package/packages/host/dist/alwayson.js +1 -1
- package/packages/host/dist/alwayson.js.map +1 -1
- package/packages/host/dist/attentionstore.d.ts +4 -0
- package/packages/host/dist/attentionstore.js +17 -0
- package/packages/host/dist/attentionstore.js.map +1 -1
- package/packages/host/dist/auth.d.ts +28 -0
- package/packages/host/dist/auth.js +65 -2
- package/packages/host/dist/auth.js.map +1 -1
- package/packages/host/dist/cli/pair.d.ts +4 -0
- package/packages/host/dist/cli/pair.js +18 -5
- package/packages/host/dist/cli/pair.js.map +1 -1
- package/packages/host/dist/der.d.ts +19 -0
- package/packages/host/dist/der.js +51 -0
- package/packages/host/dist/der.js.map +1 -0
- package/packages/host/dist/doctor.d.ts +2 -0
- package/packages/host/dist/doctor.js +10 -2
- package/packages/host/dist/doctor.js.map +1 -1
- package/packages/host/dist/extensions/desktop/host/auto.d.mts +71 -0
- package/packages/host/dist/extensions/desktop/host/auto.mjs +114 -0
- package/packages/host/dist/extensions/desktop/host/auto.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/desktop.d.mts +51 -0
- package/packages/host/dist/extensions/desktop/host/desktop.mjs +113 -0
- package/packages/host/dist/extensions/desktop/host/desktop.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/helperbin.d.mts +71 -0
- package/packages/host/dist/extensions/desktop/host/helperbin.mjs +160 -0
- package/packages/host/dist/extensions/desktop/host/helperbin.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/index.d.mts +4 -0
- package/packages/host/dist/extensions/desktop/host/index.mjs +88 -0
- package/packages/host/dist/extensions/desktop/host/index.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/knobs.d.mts +52 -0
- package/packages/host/dist/extensions/desktop/host/knobs.mjs +81 -0
- package/packages/host/dist/extensions/desktop/host/knobs.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/probe.d.mts +57 -0
- package/packages/host/dist/extensions/desktop/host/probe.mjs +119 -0
- package/packages/host/dist/extensions/desktop/host/probe.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/records.d.mts +40 -0
- package/packages/host/dist/extensions/desktop/host/records.mjs +101 -0
- package/packages/host/dist/extensions/desktop/host/records.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/session.d.mts +86 -0
- package/packages/host/dist/extensions/desktop/host/session.mjs +318 -0
- package/packages/host/dist/extensions/desktop/host/session.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/setup.d.mts +28 -0
- package/packages/host/dist/extensions/desktop/host/setup.mjs +217 -0
- package/packages/host/dist/extensions/desktop/host/setup.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/host/status.d.mts +27 -0
- package/packages/host/dist/extensions/desktop/host/status.mjs +62 -0
- package/packages/host/dist/extensions/desktop/host/status.mjs.map +1 -0
- package/packages/host/dist/extensions/desktop/shared/wire.d.mts +206 -0
- package/packages/host/dist/extensions/desktop/shared/wire.mjs +174 -0
- package/packages/host/dist/extensions/desktop/shared/wire.mjs.map +1 -0
- package/packages/host/dist/git.d.ts +23 -6
- package/packages/host/dist/git.js +54 -26
- package/packages/host/dist/git.js.map +1 -1
- package/packages/host/dist/gitfetchfail.d.ts +24 -0
- package/packages/host/dist/gitfetchfail.js +62 -0
- package/packages/host/dist/gitfetchfail.js.map +1 -0
- package/packages/host/dist/gitlog.d.ts +4 -0
- package/packages/host/dist/gitlog.js +8 -3
- package/packages/host/dist/gitlog.js.map +1 -1
- package/packages/host/dist/gitnotify.d.ts +7 -1
- package/packages/host/dist/gitnotify.js +61 -1
- package/packages/host/dist/gitnotify.js.map +1 -1
- package/packages/host/dist/host.js +5 -1
- package/packages/host/dist/host.js.map +1 -1
- package/packages/host/dist/hoststore.js +4 -4
- package/packages/host/dist/hoststore.js.map +1 -1
- package/packages/host/dist/lantls.d.ts +145 -0
- package/packages/host/dist/lantls.js +337 -0
- package/packages/host/dist/lantls.js.map +1 -0
- package/packages/host/dist/main.js +7 -2
- package/packages/host/dist/main.js.map +1 -1
- package/packages/host/dist/net.d.ts +7 -0
- package/packages/host/dist/net.js +9 -0
- package/packages/host/dist/net.js.map +1 -1
- package/packages/host/dist/origin.d.ts +4 -0
- package/packages/host/dist/origin.js +6 -0
- package/packages/host/dist/origin.js.map +1 -1
- package/packages/host/dist/pair.d.ts +54 -5
- package/packages/host/dist/pair.js +139 -34
- package/packages/host/dist/pair.js.map +1 -1
- package/packages/host/dist/pairpake.d.ts +58 -0
- package/packages/host/dist/pairpake.js +208 -0
- package/packages/host/dist/pairpake.js.map +1 -0
- package/packages/host/dist/passive.d.ts +2 -0
- package/packages/host/dist/passive.js +4 -0
- package/packages/host/dist/passive.js.map +1 -1
- package/packages/host/dist/peers/client.d.ts +3 -1
- package/packages/host/dist/peers/client.js +4 -0
- package/packages/host/dist/peers/client.js.map +1 -1
- package/packages/host/dist/peers/routes.d.ts +6 -1
- package/packages/host/dist/peers/routes.js +39 -0
- package/packages/host/dist/peers/routes.js.map +1 -1
- package/packages/host/dist/plugins/pluginapi.js +13 -0
- package/packages/host/dist/plugins/pluginapi.js.map +1 -1
- package/packages/host/dist/plugins/pluginregistry.d.ts +6 -0
- package/packages/host/dist/plugins/pluginregistry.js.map +1 -1
- package/packages/host/dist/prepushcheck.d.ts +34 -0
- package/packages/host/dist/prepushcheck.js +157 -0
- package/packages/host/dist/prepushcheck.js.map +1 -0
- package/packages/host/dist/prs.js +2 -1
- package/packages/host/dist/prs.js.map +1 -1
- package/packages/host/dist/routes/apigitcheck.d.ts +2 -0
- package/packages/host/dist/routes/apigitcheck.js +29 -0
- package/packages/host/dist/routes/apigitcheck.js.map +1 -0
- package/packages/host/dist/routes/apihost.js +2 -0
- package/packages/host/dist/routes/apihost.js.map +1 -1
- package/packages/host/dist/routes/apiio.js +2 -1
- package/packages/host/dist/routes/apiio.js.map +1 -1
- package/packages/host/dist/routes/apirepofiles.js +2 -1
- package/packages/host/dist/routes/apirepofiles.js.map +1 -1
- package/packages/host/dist/routes/apisystem.js +21 -3
- package/packages/host/dist/routes/apisystem.js.map +1 -1
- package/packages/host/dist/server/http.js +31 -5
- package/packages/host/dist/server/http.js.map +1 -1
- package/packages/host/dist/server/plugins.d.ts +13 -0
- package/packages/host/dist/server/plugins.js +26 -0
- package/packages/host/dist/server/plugins.js.map +1 -1
- package/packages/host/dist/server/wsops.js +3 -1
- package/packages/host/dist/server/wsops.js.map +1 -1
- package/packages/host/dist/sysstat.js +10 -1
- package/packages/host/dist/sysstat.js.map +1 -1
- package/packages/host/dist/updatekey.d.ts +22 -0
- package/packages/host/dist/updatekey.js +140 -0
- package/packages/host/dist/updatekey.js.map +1 -0
- package/packages/host/dist/webbundle.d.ts +15 -6
- package/packages/host/dist/webbundle.js +44 -7
- package/packages/host/dist/webbundle.js.map +1 -1
- package/packages/host/dist/wincpu.d.ts +13 -0
- package/packages/host/dist/wincpu.js +101 -0
- package/packages/host/dist/wincpu.js.map +1 -0
- package/packages/host/package.json +2 -1
- package/packages/web/dist/assets/demo-BLBaz4G_.js +26 -0
- package/packages/web/dist/assets/icons-D8RlmSLR.js +1 -0
- package/packages/web/dist/assets/index-BrvcQFBj.js +170 -0
- package/packages/web/dist/assets/index-Dx3VHnr3.css +1 -0
- package/packages/web/dist/assets/noble-DV-r0OkE.js +1 -0
- package/packages/web/dist/assets/{react-CoSyT_xY.js → react-CjmvHigY.js} +2 -2
- package/packages/web/dist/assets/rolldown-runtime-4tVxZq34.js +1 -0
- package/packages/web/dist/assets/shared-BsWIJLE-.js +51 -0
- package/packages/web/dist/assets/sheets-C7CXUU6R.js +250 -0
- package/packages/web/dist/assets/xterm-BHsKrDzh.js +16 -0
- package/packages/web/dist/build-id.txt +1 -1
- package/packages/web/dist/buildstamp.json +1 -1
- package/packages/web/dist/index.html +9 -9
- package/packages/web/dist/sw.js +2 -2
- package/release.json +7 -3
- package/packages/web/dist/assets/demo-DkCtg-SJ.js +0 -26
- package/packages/web/dist/assets/icons-B48F-4nA.js +0 -1
- package/packages/web/dist/assets/index-CpdyS6Xe.css +0 -1
- package/packages/web/dist/assets/index-EYc28CfH.js +0 -170
- package/packages/web/dist/assets/rolldown-runtime-BpQH8Ho1.js +0 -1
- package/packages/web/dist/assets/shared-DjRRLGfw.js +0 -50
- package/packages/web/dist/assets/sheets-ByWNPa9p.js +0 -248
- package/packages/web/dist/assets/xterm-C8zLTNGk.js +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ 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.18
|
|
9
|
+
|
|
10
|
+
### New
|
|
11
|
+
|
|
12
|
+
- **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.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
- **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.
|
|
18
|
+
- **Plain HTTP addresses are warned about.** The phone refuses fingerprints and over-the-air updates there and says why.
|
|
19
|
+
- **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.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **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.
|
|
24
|
+
- **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.
|
|
25
|
+
- **Reconnecting to a desktop without voice dictation no longer crashes the page.**
|
|
26
|
+
- **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.
|
|
27
|
+
- **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.
|
|
28
|
+
- **The upstream-age note in the git sheet keeps its height when the branch list opens.**
|
|
29
|
+
|
|
30
|
+
## 0.1.17
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **A long build name no longer wraps one word per line in the build sheet.** The params line and the time line of a run row now sit under its name instead of beside it, so the name keeps the full row width.
|
|
35
|
+
|
|
8
36
|
## 0.1.16
|
|
9
37
|
|
|
10
38
|
### New
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
| `@
|
|
27
|
-
| `@
|
|
28
|
-
|
|
|
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
|
[](https://www.bestpractices.dev/projects/16195)
|
|
4
|
+
[](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
|
|
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
|
|
179
|
-
grant.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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`)
|
|
390
|
-
|
|
391
|
-
pair with can ship JS that runs with the
|
|
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,
|
|
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,
|
|
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
|