@hwip/mcp-server 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/NOTICE +10 -0
- package/README.md +413 -2
- package/dist/agent-target.d.ts +45 -0
- package/dist/agent-target.js +81 -0
- package/dist/api-client.d.ts +162 -0
- package/dist/api-client.js +517 -0
- package/dist/ask-context-attach.d.ts +67 -0
- package/dist/ask-context-attach.js +147 -0
- package/dist/ask-seal.d.ts +118 -0
- package/dist/ask-seal.js +194 -0
- package/dist/command-seal.d.ts +49 -0
- package/dist/command-seal.js +90 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +147 -0
- package/dist/config.d.ts +93 -0
- package/dist/config.js +561 -0
- package/dist/crypto.d.ts +234 -0
- package/dist/crypto.js +606 -0
- package/dist/error-format.d.ts +7 -0
- package/dist/error-format.js +77 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +30 -0
- package/dist/key-changes.d.ts +50 -0
- package/dist/key-changes.js +163 -0
- package/dist/mime.d.ts +16 -0
- package/dist/mime.js +58 -0
- package/dist/peer-key.d.ts +36 -0
- package/dist/peer-key.js +35 -0
- package/dist/poll.d.ts +24 -0
- package/dist/poll.js +156 -0
- package/dist/push-audience.d.ts +35 -0
- package/dist/push-audience.js +64 -0
- package/dist/redirect.d.ts +8 -0
- package/dist/redirect.js +17 -0
- package/dist/remote-state.d.ts +38 -0
- package/dist/remote-state.js +81 -0
- package/dist/resources/channels.d.ts +4 -0
- package/dist/resources/channels.js +24 -0
- package/dist/resources/devices.d.ts +4 -0
- package/dist/resources/devices.js +24 -0
- package/dist/sanitize.d.ts +41 -0
- package/dist/sanitize.js +106 -0
- package/dist/self-register.d.ts +34 -0
- package/dist/self-register.js +51 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +63 -0
- package/dist/tools/agent-local.d.ts +17 -0
- package/dist/tools/agent-local.js +139 -0
- package/dist/tools/agent-send.d.ts +6 -0
- package/dist/tools/agent-send.js +89 -0
- package/dist/tools/ask.d.ts +9 -0
- package/dist/tools/ask.js +275 -0
- package/dist/tools/broadcast.d.ts +5 -0
- package/dist/tools/broadcast.js +42 -0
- package/dist/tools/clipboard.d.ts +5 -0
- package/dist/tools/clipboard.js +47 -0
- package/dist/tools/dismiss.d.ts +5 -0
- package/dist/tools/dismiss.js +49 -0
- package/dist/tools/file.d.ts +5 -0
- package/dist/tools/file.js +169 -0
- package/dist/tools/list.d.ts +4 -0
- package/dist/tools/list.js +44 -0
- package/dist/tools/lookup.d.ts +10 -0
- package/dist/tools/lookup.js +45 -0
- package/dist/tools/notify.d.ts +5 -0
- package/dist/tools/notify.js +118 -0
- package/dist/tools/rename.d.ts +5 -0
- package/dist/tools/rename.js +45 -0
- package/dist/trust-pins.d.ts +70 -0
- package/dist/trust-pins.js +269 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +2 -0
- package/dist/url-check.d.ts +11 -0
- package/dist/url-check.js +57 -0
- package/dist/ws-wait.d.ts +72 -0
- package/dist/ws-wait.js +200 -0
- package/package.json +70 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to the Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding any notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
Copyright 2026 Hyungtak Jin
|
|
179
|
+
|
|
180
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
|
+
you may not use this file except in compliance with the License.
|
|
182
|
+
You may obtain a copy of the License at
|
|
183
|
+
|
|
184
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
185
|
+
|
|
186
|
+
Unless required by applicable law or agreed to in writing, software
|
|
187
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
188
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
189
|
+
See the License for the specific language governing permissions and
|
|
190
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Hwip MCP server
|
|
2
|
+
Copyright 2026 Hyungtak Jin
|
|
3
|
+
|
|
4
|
+
This product is a fork of zeph-to/mcp-server
|
|
5
|
+
(https://github.com/zeph-to/mcp-server), Copyright 2026 Zeph,
|
|
6
|
+
licensed under the Apache License, Version 2.0.
|
|
7
|
+
Forked from upstream commit 7b581c9dd47943dfec0445292e6502f19f1958d3
|
|
8
|
+
(2026-09-27). Upstream git history was not carried over.
|
|
9
|
+
|
|
10
|
+
Files modified from the original carry the same license. See LICENSE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,414 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hwip/mcp-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@hwip/mcp-server)
|
|
4
|
+
[](https://www.npmjs.com/package/@hwip/mcp-server)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://docs.hwip.app)
|
|
8
|
+
|
|
9
|
+
**Your agent calls `hwip_ask`; the question lands on your phone as buttons + a text field; your reply comes back into the same tool call and the agent keeps going.**
|
|
10
|
+
|
|
11
|
+
Hwip's MCP server is the agent side of that round trip — plus one-way notifications, clipboard, files, and channel broadcasts, all over the [Model Context Protocol](https://modelcontextprotocol.io). Works with Claude Code, Cursor, Windsurf, Gemini CLI, and any MCP client.
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<img src="https://hwip.app/readme/demo.gif" alt="Agent calls hwip_ask; the question hits the phone; you tap Deploy; the tool call returns" width="560"><br>
|
|
15
|
+
<sub><em>Your agent calls <code>hwip_ask</code> → the question hits your phone → you tap <b>Deploy</b> → the call returns and the agent ships.</em></sub>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
Part of the Hwip toolchain: [`@hwip/cli`](https://github.com/hwipit/cli) (installer, push CLI, tmux remote control) · [`hwipit/plugin`](https://github.com/hwipit/plugin) (Claude Code plugin bundling this server) · the [Hwip app](https://hwip.app) on your phone.
|
|
19
|
+
|
|
20
|
+
> **New here?** [docs.hwip.app](https://docs.hwip.app) walks the whole setup — one command on your machine, the app on your phone, and a restart. The reference below assumes that is already done.
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
The easiest way to set up for all agents at once:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g @hwip/cli
|
|
28
|
+
hwip install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Needs Node 18.17 or later. On Node 22 or later `hwip_ask` hears the answer over a WebSocket the moment it is given; on older Node it polls, and the server says so on stderr at start.
|
|
32
|
+
|
|
33
|
+
This saves credentials to `~/.hwip/config.json` and configures your agents automatically. The MCP server reads from this file — no env vars needed. Install globally so `hwip cc` (phone-driven sessions) works and hooks skip an npx cold-start; `npx @hwip/cli install` is a notifications-only alternative.
|
|
34
|
+
|
|
35
|
+
### Claude Code (manual)
|
|
36
|
+
|
|
37
|
+
Install the server once, so each session starts the `hwip-mcp` binary directly instead of resolving the package through `npx` first:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g @hwip/mcp-server
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Add to `~/.claude/settings.json`:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"hwip": {
|
|
49
|
+
"command": "hwip-mcp"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Without a global install, `"command": "npx", "args": ["-y", "@hwip/mcp-server"]` works too; it only starts slower, because every session resolves the package first.
|
|
56
|
+
|
|
57
|
+
No `env` block needed: credentials come from `~/.hwip/config.json` (written
|
|
58
|
+
by `hwip install`). Add env vars only to override the file —
|
|
59
|
+
e.g. a second account:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
"env": { "HWIP_API_KEY": "ak_other_account" }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Cursor / Other MCP Clients
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"command": "hwip-mcp"
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
(after `npm install -g @hwip/mcp-server`, or `"command": "npx", "args": ["-y", "@hwip/mcp-server"]` without it.)
|
|
74
|
+
|
|
75
|
+
## Environment Variables
|
|
76
|
+
|
|
77
|
+
| Variable | Required | Description |
|
|
78
|
+
|----------|----------|-------------|
|
|
79
|
+
| `HWIP_API_KEY` | Yes* | API key from Settings > API Keys. One that cannot be sent as a header — a line break or another control character inside it, a character no key has — stops the server at start, with a message that says where the key is set and nothing of the key. So does an `apiKey` in `~/.hwip/config.json` that is not text |
|
|
80
|
+
| `HWIP_HOOK_ID` | No | Hook ID (optional — only needed for `hwip_ask`) |
|
|
81
|
+
| `HWIP_DEVICE_ID` | No | Default target device for `hwip_notify`, `hwip_clipboard` and `hwip_file` (each call's `targetDeviceId` wins). Omit to send to all devices. `hwip_ask` does not use it |
|
|
82
|
+
| `HWIP_BASE_URL` | No | API base URL (default: `https://api.hwip.app/v1`). A base URL that answers with a redirect is refused (`REDIRECT_REFUSED`): requests that carry your API key never follow one |
|
|
83
|
+
| `HWIP_WS_URL` | No | **Deprecated.** WebSocket endpoint for the hook-response fast path. `wsUrl` in `~/.hwip/config.json` wins over it and is where the value belongs; this is read only when the file has none, so a machine that predates the config field keeps working. It will stop being read |
|
|
84
|
+
| `HWIP_DISABLE_SESSION_CACHE` | No | Set to `1`/`true` to skip writing the session-id handoff file under `~/.cache/hwip/`. Useful for read-only filesystems, ephemeral CI runners, or sandboxed envs that audit filesystem writes. The plugin's stop hook still works without it (transcript-path UUID extraction is the primary path; the cache is a fallback for older Claude Code versions). |
|
|
85
|
+
| `HWIP_SESSION_ID` | No | Override the session id attached to pushes (grouping in the app). Auto-detected from the newest Claude Code transcript when unset |
|
|
86
|
+
|
|
87
|
+
\* If env vars are not set, the server reads from `~/.hwip/config.json` (created by `hwip install` or `hwip login`). Unresolved `${...}` interpolations are also treated as unset.
|
|
88
|
+
|
|
89
|
+
### `~/.hwip/config.json`
|
|
90
|
+
|
|
91
|
+
| Key | Same as | Notes |
|
|
92
|
+
|-----|---------|-------|
|
|
93
|
+
| `apiKey` | `HWIP_API_KEY` | The environment variable wins |
|
|
94
|
+
| `baseUrl` | `HWIP_BASE_URL` | The environment variable wins |
|
|
95
|
+
| `wsUrl` | `HWIP_WS_URL` (deprecated) | The file wins. With neither, the prod API (`https://api.hwip.app/v1`) gets `wss://ws.hwip.app`, the cli listener's rule; another API base gets no socket, and `hwip_ask` polls |
|
|
96
|
+
| `hookId` | `HWIP_HOOK_ID` | The environment variable wins |
|
|
97
|
+
| `deviceId` | `HWIP_DEVICE_ID` | The environment variable wins |
|
|
98
|
+
|
|
99
|
+
A file that is not used because it is not valid JSON or UTF-8 text, or cannot be read, is named on stderr ("…so it is being ignored") even when `HWIP_API_KEY` is set: its `hookId` and `deviceId` are dropped with it.
|
|
100
|
+
|
|
101
|
+
Without an API key the server stops at start and says why: the file does not exist, it is not valid JSON, it is not valid UTF-8 text (such a file is not used at all, as in the cli), it has no `apiKey`, or it was ignored (below).
|
|
102
|
+
|
|
103
|
+
The file holds the key and says where the key is sent, so the server checks it before reading, by the same rule as the cli:
|
|
104
|
+
|
|
105
|
+
- **Another user owns it, or anyone can write it** (mode `002`): the file is ignored — `HWIP_API_KEY` still works — and a line on stderr says so. Run `chmod 600 ~/.hwip/config.json`.
|
|
106
|
+
- **The group can read or write it, or others can read it**: used, with a warning. The server only reads the file; `hwip login` is what narrows it to `0600`.
|
|
107
|
+
- A symlinked file (a dotfiles checkout) is judged by what it points at.
|
|
108
|
+
- **A file that is not valid JSON, or cannot be read**, may hold the `baseUrl` your key belongs to (staging, self-hosted), so a key from `HWIP_API_KEY` is not sent to the default API in its place: the server stops at start, names the file, and goes ahead once `HWIP_BASE_URL` says where the key goes. (A file ignored for its mode is not trusted for a base URL either, and the default stands.)
|
|
109
|
+
- **A key from the file does not follow an address from the environment.** The key in `~/.hwip/config.json` belongs to the addresses the file says (`baseUrl`, else the default API). If `HWIP_BASE_URL` names another origin, the server stops at start, naming both origins and no more, and goes ahead once `HWIP_API_KEY` is set too (a key you gave with the address) or the variable is unset or on the same origin. The deprecated `HWIP_WS_URL` is held to the same rule — with the file's key it is used only for the default socket's origin, otherwise it is skipped with a line on stderr (`hwip_ask` polls) — and putting `wsUrl` in the file is the way to name another socket.
|
|
110
|
+
- `wsUrl` has to be a `ws://` or `wss://` URL without a space or `#fragment`; one that is not is ignored with a warning (the default socket, or polling). `ws://` and `http://` to a host other than this machine work, with a warning that the key travels in the clear.
|
|
111
|
+
|
|
112
|
+
## Limits on what the server reads
|
|
113
|
+
|
|
114
|
+
A reply from the API is read only up to 8 MiB (an error body, 64 KiB), counted on the bytes that arrive rather than on `Content-Length`; a file download, up to 1 GiB. Past that the call fails with `RESPONSE_TOO_LARGE` or `FILE_TOO_LARGE`. The message the API words for an error reaches the agent as one line of at most 300 characters, control characters turned into spaces, and an error code that is not made of letters, digits and `_.:-` becomes `UNKNOWN_ERROR`.
|
|
115
|
+
|
|
116
|
+
## Tools
|
|
117
|
+
|
|
118
|
+
Push titles are automatically prefixed with the project directory name — `myapp · Build complete` — so the phone feed stays scannable when several sessions push at once.
|
|
119
|
+
|
|
120
|
+
### hwip_notify
|
|
121
|
+
|
|
122
|
+
Send a one-way push notification. An optional `url` opens on the device; the push stays a notification (a long body goes as a `response.md` file push instead).
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
title: "Build complete"
|
|
126
|
+
body: "All 42 tests passed"
|
|
127
|
+
url: "https://github.com/org/repo/actions/runs/123" (optional)
|
|
128
|
+
priority: "low" | "normal" | "high" | "urgent"
|
|
129
|
+
targetDeviceId: "dev_..." (optional, overrides HWIP_DEVICE_ID)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### hwip_clipboard
|
|
133
|
+
|
|
134
|
+
Copy text to the user's device clipboard. At most 96 KiB, measured as the server does (the text as JSON, UTF-8); longer text is refused with `BODY_TOO_LONG` before it is sent — use `hwip_file` with `content`. Not end-to-end encrypted (see [Encryption](#encryption)): do not use it for secrets (keys, tokens, passwords); use `hwip_notify` or `hwip_file` for those.
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
text: "npm install @hwip/mcp-server"
|
|
138
|
+
targetDeviceId: "dev_..." (optional)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### hwip_list
|
|
142
|
+
|
|
143
|
+
List recent push notifications.
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
limit: 5 (1-20, default: 5)
|
|
147
|
+
type: "note" (optional filter: note, link, file, clipboard, hook)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Returns: `{ pushes: [...], total: 5, hasMore: true }`. A body is cut to 100 characters. An end-to-end encrypted push shows `encrypted: true` and no title or body — only the user's devices can open it.
|
|
151
|
+
|
|
152
|
+
### hwip_dismiss
|
|
153
|
+
|
|
154
|
+
Mark a specific push as read.
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
pushId: "push_01HX..."
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### hwip_dismiss_all
|
|
161
|
+
|
|
162
|
+
Clear all notifications at once. No parameters.
|
|
163
|
+
|
|
164
|
+
Returns: `{ dismissed: 12, badge: 0 }`
|
|
165
|
+
|
|
166
|
+
### hwip_devices · hwip_channels
|
|
167
|
+
|
|
168
|
+
Read-only lookups for the ids other tools take: `hwip_devices` returns `{ devices }` (a `deviceId` is `hwip_notify`'s `targetDeviceId`), `hwip_channels` returns `{ channels }` (a `channelId` for `hwip_broadcast`). The same lists are the `hwip://devices` and `hwip://channels` resources, for clients that read resources.
|
|
169
|
+
|
|
170
|
+
### hwip_broadcast
|
|
171
|
+
|
|
172
|
+
Send a notification to all subscribers of a channel.
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
channelId: "ch_..."
|
|
176
|
+
title: "Deploy complete"
|
|
177
|
+
body: "v2.1.0 is live"
|
|
178
|
+
url: "https://..." (optional)
|
|
179
|
+
priority: "normal"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### hwip_file
|
|
183
|
+
|
|
184
|
+
Send a file to the user's device. Either `filePath` (a file already on disk) or
|
|
185
|
+
`content` (text you generated) is required.
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
filePath: "/abs/path/screenshot.png" (images, PDFs, logs — anything on disk)
|
|
189
|
+
content: "{\"status\": \"ok\"}" (text only; requires fileName)
|
|
190
|
+
fileName: "report.json" (required with content; defaults to basename of filePath)
|
|
191
|
+
title: "Build Report" (optional, defaults to fileName)
|
|
192
|
+
targetDeviceId: "dev_..." (optional)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Images are delivered with their real mime type and render inline on the device.
|
|
196
|
+
Never base64 a binary file into `content` — pass `filePath` and the server reads
|
|
197
|
+
the bytes off disk.
|
|
198
|
+
|
|
199
|
+
The file is read into memory whole, so a path that is not a regular file, or is
|
|
200
|
+
over 1 GiB (the most any plan sends), is refused with `FILE_READ_FAILED` or
|
|
201
|
+
`FILE_TOO_LARGE` before it is read; the account's own limit (25 MiB on Free) is
|
|
202
|
+
checked by the server. Upload and download time out after 30 s plus one second
|
|
203
|
+
per MiB, so a large file on a slow link is not cut off at 30 s.
|
|
204
|
+
|
|
205
|
+
Returns: `{ pushId: "...", fileKey: "...", fileSize: 42, encrypted: true, delivery: "Sent via cloud" }`
|
|
206
|
+
|
|
207
|
+
**Always the cloud.** A file an agent sends goes through Hwip, whatever network
|
|
208
|
+
the target is on, and the result says `delivery: "Sent via cloud"`. The direct
|
|
209
|
+
hand-over over the local network belongs to the phone's share sheets, where a person picks
|
|
210
|
+
the route for a file they chose; an agent writing a report has no such consent
|
|
211
|
+
to read from, and a copy in the cloud is the one every device on the account
|
|
212
|
+
can open later.
|
|
213
|
+
|
|
214
|
+
### hwip_session_rename
|
|
215
|
+
|
|
216
|
+
Set a custom display name for the **current** agent session, shown in the Hwip app's **Streams › Agents** list. Lets an agent label what it's working on — `"Prod deploy"`, `"Auth refactor"` — so parallel sessions are easy to tell apart on your phone. Renames the session this server runs in (resolved from the listener device id + tmux session name); the name persists until changed.
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
alias: "Prod deploy watcher" (1-60 chars)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Returns: `{ renamed: true, session: "hwip-myapp", alias: "Prod deploy watcher" }`, or `{ renamed: false, reason: "..." }` when there's no active session to rename (not running inside a `hwip listener` tmux session).
|
|
223
|
+
|
|
224
|
+
### hwip_agent_send
|
|
225
|
+
|
|
226
|
+
Type a message into **another** agent session — Claude Code, Codex, pi, … on any of the user's machines — as if the user had sent it from their phone. It shows in that session's chat in the app, and, like any command from the phone, puts that session in remote mode.
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
target: "dev_listener_ab12cd34:hwip-api" session key <deviceId>:<tmuxName>, or a name/alias that names one session
|
|
230
|
+
message: "Build is green on main — rebase and rerun the e2e suite"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The receiver reads `[from <your alias or tmux name>@<your machine> · reply: <your key>] <message>`, so it can answer with the same tool. A name also matches the alias, the name the agent gives its session, or its label. Your own session is left out of name matching, so a project you run on two PCs resolves to the other one. A name that still matches two sessions is refused with both keys listed, and an unknown name is refused with the other sessions listed. A `hwip sh` shell session is never a target: it is a real shell, and a message typed into it would run as a command on that machine. A session on a machine that is offline is refused (`TARGET_OFFLINE`): its listener takes commands only while connected, so the message would never be typed. Sending to your own session is refused. The message is sealed for the target computer and for every other device the account lists with a key, so the server relays it and reads nothing: a target on another computer is one whose `hwip listener` has run once so the account lists its key, and a session on this very machine is sealed with this machine's own device key. If it cannot be sealed the tool returns `SEALED_SEND_FAILED` with the reason and sends nothing — there is no plaintext path. The sealed message names this computer as its sender; the target's listener checks that name against the key the account lists for it. The numbers it carries come from `~/.hwip/command-seq`, shared with `hwip` on this computer. The session name stays plaintext: the relay routes by it. A send that failed on the way and is made again takes a new number, so if the first one had in fact arrived the session receives the message twice.
|
|
234
|
+
|
|
235
|
+
Returns: `{ sent: true, target: "<key>", pushId: "..." }`. `sent: true` means the server stored the message; the target's listener can still refuse it (rate limit, a shell pane, a session gone since), and the tool does not wait to hear of it.
|
|
236
|
+
|
|
237
|
+
### hwip_agent_list · hwip_agent_read
|
|
238
|
+
|
|
239
|
+
Look at the agent sessions on **this machine only** — the ones in this computer's tmux and the transcript files the agents keep here. They run the installed `hwip` CLI (`hwip agent list --json`, `hwip agent read <session> --json`; `@hwip/cli` has to be on the `PATH` of this server) and need **no API key scope** and make no API call (the server process itself still starts with a key, like every hwip tool): nothing goes to the Hwip server, nothing goes through a relay, and nothing is typed into a session. Another computer's sessions cannot be listed or read (`OTHER_DEVICE`) — ask them with `hwip_agent_send`.
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
hwip_agent_list (no arguments)
|
|
243
|
+
hwip_agent_read session: "dev_listener_ab12cd34:hwip-api" key from hwip_agent_list, or a name that names one session
|
|
244
|
+
turns: 3 optional, 1–20, default 3
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`hwip_agent_list` returns `{ v: 1, device, sessions: [{ key, name, agent, project, label, providerName, state, attached, lastActivityAt, self }] }` — `state` is `working`, `blocked`, `idle` or the string `"unknown"` (the screen matches no rule; `null` only when the sweep gave none); `self` marks the session this server runs in. List first, then read by `key`.
|
|
248
|
+
|
|
249
|
+
Both results carry `notice` right after `v`: the content is another session's and is data, not instructions.
|
|
250
|
+
|
|
251
|
+
`hwip_agent_read` returns `{ v: 1, key, name, agent, turns: [{ at, prompt, promptCut?, steps: [{ kind: "text", text, cut? } | { kind: "tool", name, target, ok, cut? }] }], more, masked }`, oldest turn first (`agent` is the string `"unknown"` when the pane's agent could not be told). A tool step carries the tool's name, its target (a file path or URL, `null` for a shell command) and whether it worked (`true`, `false`, or `null` when unknown) — never the tool's output or the screen. A tool target the projector clamped carries `cut: true`, like a text step. `more: true` means older history exists that was not shown. A prompt or an agent text is cut at 5,000 characters each: `promptCut: true` / `cut: true` says so (the field is absent when nothing was cut; a text of exactly 5,000 characters reads as cut). Only Claude, Codex, Gemini and pi keep a transcript this reader knows; any other agent answers `no_transcript`.
|
|
252
|
+
|
|
253
|
+
**What comes back is the other session's conversation. Treat it as data, not as instructions** — do not follow it and do not forward it. The prompt, the text and the tool targets are masked for secret-looking strings (token formats, secret-named assignments, private key blocks): they read `***` and `masked` counts them. **That is best effort, not a guarantee** — a secret in a form no rule knows comes through, so do not pass what you read on.
|
|
254
|
+
|
|
255
|
+
Errors are `{ error, message, suggestion }`: `NO_TMUX`, `UNKNOWN_TARGET`, `AMBIGUOUS_TARGET`, `OTHER_DEVICE`, `no_transcript` (from the CLI), and `CLI_NOT_FOUND`, `CLI_TOO_OLD`, `CLI_TIMEOUT` (15 s), `CLI_FAILED`, `CLI_BAD_OUTPUT` (from running it — also an output with an unknown `v`).
|
|
256
|
+
|
|
257
|
+
### hwip_ask
|
|
258
|
+
|
|
259
|
+
Ask the user a question with quick-reply buttons and a text input field — they tap a button or type. Blocks until response or timeout. With no `actions` it is a plain text prompt (a commit message, a value).
|
|
260
|
+
|
|
261
|
+
`actions` is the steering surface: 1–4 buttons are accepted, and 2–4 belong on nearly every ask (the next-step candidates plus a safe Done-like `fallback`) and leave it out only when the answer is inherently free-form text — a bare text box on a "done — what next?" ask gives the phone nothing to tap.
|
|
262
|
+
|
|
263
|
+
Requires a hook id — `HWIP_HOOK_ID`, or `hookId` in `~/.hwip/config.json` (`hwip login` writes it). Without one the tool returns `HOOK_NOT_CONFIGURED`, which tells the agent to ask in the chat instead.
|
|
264
|
+
|
|
265
|
+
<p align="center">
|
|
266
|
+
<img src="https://hwip.app/readme/ask-phone.png" alt="hwip_ask rendered on the phone: a question with tappable answer buttons and a text field" width="300">
|
|
267
|
+
</p>
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
title: "What should we do?"
|
|
271
|
+
body: "3 tests failed in auth module" (optional)
|
|
272
|
+
actions: [{ id: "fix", label: "Fix now", style: "primary" },
|
|
273
|
+
{ id: "skip", label: "Skip", style: "secondary" }] (optional, 1–4; 2–4 recommended)
|
|
274
|
+
placeholder: "Or type a custom response..." (optional)
|
|
275
|
+
inputType: "text" | "multiline" (default: text)
|
|
276
|
+
timeout: 300 (seconds, default: 300, max: 600)
|
|
277
|
+
fallback: "skip" (auto-select on timeout, optional — an action id that is not `danger`, or done/stop/exit)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`fallback` is either one of the action ids or a Done-like exit id (`done` / `stop` / `exit`, any case). The exit ids need not be buttons and work on a text-only ask too — that is how an unanswered REMOTE ask ends the loop quietly. Anything else (a typo such as `don`) is refused with `INVALID_FALLBACK` before anything is sent, since it would come back as an answer the user never saw. So is a `danger` action, whatever its id and however the id is cased: a fallback is the one answer nobody gave, and silence does not get to pick the choice marked as the dangerous one.
|
|
281
|
+
|
|
282
|
+
**`hwip_ask` seals both ways.** The title, body, placeholder and button wording go to the phone inside the envelope, with a random 128-bit id the answer has to echo; the server keeps only each button's `id` and `style`, the `inputType`, the timeout, the `fallback`, the `hookType` and `acceptsExit` flags, the envelope's `senderPublicKey` and `deviceKeyMap` (which devices it is wrapped for), the push priority, the session fields (including the session name, which can be a tmux session name) and, once it is answered, which device answered. The phone seals its answer for this machine, and the tool opens it only if the envelope was sealed with the key the device list registers for the answering device and holds an `answer` for this question's id. The question is sealed for every other device the account lists with a key. Nothing is sent in the clear: an account with no iPhone or Android app signed in gets `NO_APP_DEVICE`, and a question that cannot be sealed (no device to seal for, keys that do not load, a device list that does not load, registering this machine's key failed) gets `SEALED_ASK_FAILED` saying why — nothing is sent either way. A choice id over 64 characters, a label over 100 or more than 10 choices is refused with its own message. The device-list lookup before the send is not counted in `timeout`. A relay can still make a sealed ask come back unanswered: it can return `timed_out` or hold the delivery back, which forces your `fallback` choice — the result says `timedOut: true`, and a `fallback` picked that way did not come from the user. The answer carries no attachments.
|
|
283
|
+
|
|
284
|
+
**A sealed `hwip_ask` question carries its session's context.** The tool runs `hwip ask-context show` (the `hwip` CLI, in this server's directory and with its environment, so its tmux pane): what the session changed (`git diff` against HEAD with new files, never a `.gitignore`d one) and the last 60 lines of its terminal, masked for secret-looking text first — best effort, a secret in a form the masking does not know stays. A summary (files, lines, at most 20 paths) rides inside the sealed body and the masked text itself (64 KiB at most) is one attachment sealed for the same devices, which the phone fetches when the summary is opened. The server sees that attachment the same way on every sealed question — named `file`, `application/octet-stream`, 64 KiB — also when there is nothing to carry, `hwip ask-context off` is set for the project, or no `hwip` CLI that knows `ask-context show` is installed, where it goes with nothing in it. It is kept as long as any attachment (30 days). The context never holds a question back: if it cannot be stored the question goes without it (which the server can tell) and stderr says why; a summary, then the attachment, that would take the envelope over the server's 48 KiB limit is left out; with a `timeout` under 20 seconds nothing is gathered, and at the minimum of 10 nothing is attached.
|
|
285
|
+
|
|
286
|
+
Returns: `{ actionId: "fix", timedOut: false }` or `{ value: "custom text", timedOut: false }`
|
|
287
|
+
|
|
288
|
+
No answer before `timeout`: `{ actionId: "<fallback>", timedOut: true }`, or `{ timedOut: true, message }` without a fallback — the same shape either way, never an error.
|
|
289
|
+
|
|
290
|
+
When the wait ends any other way the result is an error whose `error` code says what to do next:
|
|
291
|
+
|
|
292
|
+
| `error` | Meaning | Next step |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| `CANCELLED` | The user dismissed the question on the phone | Do not ask it again |
|
|
295
|
+
| `POLL_FAILED` | The server could not be read for the last stretch before the deadline (each failed read is retried with backoff first). `fallback` is not applied | Do not re-ask blindly — the question may still be on the phone; tell the user in the terminal |
|
|
296
|
+
| `ABORTED` | The agent cancelled the tool call. Cancelled before the send, nothing reaches the phone; cancelled during it, the message says the question may still have arrived | If it may have arrived, check `hwip_list` before asking again |
|
|
297
|
+
| `NETWORK_ERROR` | No connection to the hwip API could be made while sending the question (nothing listening, no route, a name that does not resolve), on every try | Retry once later, or carry on in the chat |
|
|
298
|
+
| `SEND_UNCONFIRMED` | The send failed, the tries ran out, and at least one of them may have reached the server all the same: the hwip API was slow to answer, answered with something that is not its own reply (a proxy's page, a body cut short, a reply too large to read) or with a 5xx, still held the first send as running, or the connection dropped once made. The message names the last failure, with the transport's code for it when there is one (`ECONNRESET`) | Check `hwip_list` before asking again |
|
|
299
|
+
| `UNAUTHORIZED` · `FORBIDDEN` · … | A server refusal that a retry would repeat, returned on the first read | Fix the key or hook |
|
|
300
|
+
|
|
301
|
+
`NETWORK_ERROR`, `API_TIMEOUT` and `BAD_RESPONSE` come from the other tools, which send once: no reply at all, or one that could not be read, is told apart from the API's own refusals, and a write that timed out or was answered unreadably says it may have landed. From `hwip_ask` the last two, and a `NETWORK_ERROR` of a connection that was made and then dropped, arrive as `SEND_UNCONFIRMED`, whichever try they were on: a slow first try followed by three with no connection is still a question that may be on the phone. One `BAD_RESPONSE` does come from `hwip_ask`: the API took the question and its reply held no event id the answer could be read by. The question may be on the phone, this call cannot read an answer to it, and it is not sent again — check `hwip_list`. A `NETWORK_ERROR` names the transport's code when there is one (`ECONNREFUSED`) and never its words, which can quote what was sent; when there is no code the words go to stderr, less the user and password of an address. One for a base URL that is not an address (`ERR_INVALID_URL`) says the same however often it is tried: correct `HWIP_BASE_URL` instead.
|
|
302
|
+
|
|
303
|
+
**A question goes up once.** `hwip_ask` sends it under an `Idempotency-Key`, a new random one per question, so sending it again cannot open a second question: the server answers a repeat with the first one's ids. A send that fails in a way that may pass — no connection, no answer in 30 seconds, an answer that cannot be read, a 5xx, 408 or 429 — is sent again with the same key and the same bytes, after 1, 2 and 4 seconds (four tries). While the server says the first request is still running under the key (409 `IDEMPOTENCY_IN_PROGRESS`), the send is repeated every 5 seconds instead, for up to 35 seconds from the first such answer: a request the server lost to its own timeout holds the key for 30. No try is made that would start after the question's `timeout` has passed, a cancelled call stops at once, and any other refusal ends the ask on the first answer. One case still opens a second question: a first request that wrote the question and died before it marked the key is taken over after those 30 seconds. The other tools send once.
|
|
304
|
+
|
|
305
|
+
A repeat the server had already taken is answered with the question's ids alone, which do not say whether the phone was notified — that was the first request's to say. So when a question went up on a retry, the answer does not say the phone was notified, and the ask then ends unanswered, the result carries a `notice` saying so: the question is in the app either way, but nobody may have been pinged.
|
|
306
|
+
|
|
307
|
+
### Client timeouts
|
|
308
|
+
|
|
309
|
+
`hwip_ask` blocks until the user responds, up to its `timeout` (max 600s) from when the question is up. Sending it comes first and normally takes no time; a send that fails and is repeated adds its own, with no try started once `timeout` has passed since the first. With `wsUrl` set in `~/.hwip/config.json` the response arrives over WebSocket the instant it's submitted; otherwise the server polls. Either way the MCP request stays open the whole time. To keep the client from giving up early, the server emits a `notifications/progress` between its reads of the answer: every 5 s while it polls, about every 10 s while the WebSocket is connected (polling is then only the backstop). Before that, while the question is being sent, it emits one before each repeat of a send that failed — so the longest stretch with no progress is a pause (5 s), one send (30 s), the first read of the answer (up to 15 s) and, when that read came back early, the wait before the next one: about a minute. Clients must either set a per-request timeout above the tool's `timeout`, or reset their timeout on progress notifications. Claude Code does the latter by default.
|
|
310
|
+
|
|
311
|
+
## Resources
|
|
312
|
+
|
|
313
|
+
### hwip://devices
|
|
314
|
+
|
|
315
|
+
Lists connected devices with online status. Use to check which devices will receive notifications.
|
|
316
|
+
|
|
317
|
+
### hwip://channels
|
|
318
|
+
|
|
319
|
+
Lists channels the user owns or subscribes to. Use to find `channelId` for `hwip_broadcast`.
|
|
320
|
+
|
|
321
|
+
## Usage Guide
|
|
322
|
+
|
|
323
|
+
### When to use each tool
|
|
324
|
+
|
|
325
|
+
| Situation | Tool | Example |
|
|
326
|
+
|-----------|------|---------|
|
|
327
|
+
| Long task finished | `hwip_notify` | Build complete, test results, deploy done |
|
|
328
|
+
| Need a decision (buttons + optional free text) | `hwip_ask` | "Tests green. Deploy?" with a custom-instruction escape hatch |
|
|
329
|
+
| Free-form input only | `hwip_ask` without `actions` | Commit message, env var value, description |
|
|
330
|
+
| Share code/logs | `hwip_file` | Error logs, test reports, generated config |
|
|
331
|
+
| Share snippet | `hwip_clipboard` | URL, shell command |
|
|
332
|
+
| Label this session | `hwip_session_rename` | Name the run "Prod deploy" so parallel sessions stay distinguishable on the phone |
|
|
333
|
+
| Pass work to another agent | `hwip_agent_send` | "Send the summary to the pi session on my other PC" |
|
|
334
|
+
| See what the agents on this PC are doing | `hwip_agent_list`, then `hwip_agent_read` | "What is the codex session working on?" (this machine only, no API call) |
|
|
335
|
+
|
|
336
|
+
### Recommended patterns
|
|
337
|
+
|
|
338
|
+
**Decision gate with an escape hatch (preferred):**
|
|
339
|
+
```
|
|
340
|
+
hwip_ask(
|
|
341
|
+
title: "Tests green. Deploy to production?",
|
|
342
|
+
actions: [
|
|
343
|
+
{ id: "deploy", label: "Deploy", style: "primary" },
|
|
344
|
+
{ id: "hold", label: "Hold", style: "secondary" }
|
|
345
|
+
],
|
|
346
|
+
placeholder: "Or tell me what to do instead...",
|
|
347
|
+
fallback: "hold"
|
|
348
|
+
)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Task completion notification:**
|
|
352
|
+
```
|
|
353
|
+
hwip_notify(
|
|
354
|
+
title: "Build complete: web app",
|
|
355
|
+
body: "All 42 tests passed. Bundle size: 1.2MB (-3%)"
|
|
356
|
+
)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Error alert with link:**
|
|
360
|
+
```
|
|
361
|
+
hwip_notify(
|
|
362
|
+
title: "CI failed: lint errors",
|
|
363
|
+
body: "2 errors in src/auth.ts",
|
|
364
|
+
url: "https://github.com/org/repo/actions/runs/456",
|
|
365
|
+
priority: "high"
|
|
366
|
+
)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### When NOT to use
|
|
370
|
+
|
|
371
|
+
- Short responses the user can see immediately in the terminal
|
|
372
|
+
- Read-only operations (file search, code analysis)
|
|
373
|
+
- Every single tool call — only notify on meaningful milestones
|
|
374
|
+
|
|
375
|
+
### Multi-session workflow
|
|
376
|
+
|
|
377
|
+
When running multiple AI agent sessions in parallel, use `hwip_notify` to signal completion so the user knows which session finished without checking each terminal.
|
|
378
|
+
|
|
379
|
+
## API Key Permissions
|
|
380
|
+
|
|
381
|
+
The API key needs the following scopes:
|
|
382
|
+
|
|
383
|
+
- `push:read` — for `hwip_list`
|
|
384
|
+
- `push:write` — for `hwip_notify`, `hwip_clipboard`, `hwip_dismiss`, `hwip_dismiss_all`, `hwip_file`, `hwip_agent_send`
|
|
385
|
+
- `hook:write` — for `hwip_ask`
|
|
386
|
+
- `device:write` — for `hwip_session_rename`
|
|
387
|
+
- `channel:read` — for `hwip_channels` and the `hwip://channels` resource
|
|
388
|
+
|
|
389
|
+
`hwip_agent_list` and `hwip_agent_read` need no scope: they run the local `hwip` CLI and never call the API.
|
|
390
|
+
|
|
391
|
+
Reading the device list (`hwip_devices`, the `hwip://devices` resource, `hwip_agent_send` finding its target, and picking who an encrypted push is sealed for) takes any valid key, no scope. Putting this machine's key on the list takes `push:read`, `push:write` or `hook:write`; replacing a key it already lists takes `push:read` (see Encryption).
|
|
392
|
+
|
|
393
|
+
Create an API key with the **MCP** preset in Settings > API Keys for the correct permissions.
|
|
394
|
+
|
|
395
|
+
## Encryption
|
|
396
|
+
|
|
397
|
+
End-to-end encryption is **always on**: nothing in the account or on this machine turns it off. `hwip_notify` and `hwip_file` seal what they send, `hwip_ask` seals the question and its answer (see `hwip_ask` above), and `hwip_agent_send` is sealed for the target computer (see `hwip_agent_send` above). Push bodies and file contents are encrypted with AES-256-GCM. `hwip_clipboard` and `hwip_broadcast` are not sealed yet. This machine holds one ECDH P-256 keypair in `~/.hwip/device-keys.json`, shared with `hwip listener` and created by whichever starts first (the keypair older builds kept in `~/.config/hwip/device-keys.json` is deleted) — the private half never leaves the machine, and the backend stores public keys only and rejects a private-key upload. Each push is encrypted once, and its AES key is wrapped separately for every device on your account that lists a public key, using ECDH against that key. Start-up loads the keypair without asking the server anything, and stderr says whether it loaded. A keypair that would not load at start-up is tried again by the next send, so a key file fixed meanwhile needs no restart.
|
|
398
|
+
|
|
399
|
+
**This machine puts its own key on the device list.** The phone seals an answer, and the target listener admits a command, by the key the account lists for this machine. When the list a send reads lacks this machine, or lists an older key for it, the send first puts its key there with one `PUT /devices/{id}/public-key` (named after this host when the entry is new) — so no `hwip listener` has to have run here. Only an API key with `push:read` may replace a key the list already holds. When that fails, `hwip_ask` and `hwip_agent_send` send nothing and say why (`could not register this machine's key in the account's device list (…)`); `hwip_notify` and `hwip_file` send all the same, as the app then cannot tell who sent them, and the result ends with `Note: could not register this machine's key (…) — the app cannot verify who sent this push` — once per server process for the same reason, since it comes back on every send (stderr says it each time).
|
|
400
|
+
|
|
401
|
+
A notify, file, question or agent message is never sent in the clear. One that cannot be sealed — this machine's keys did not load, the device list does not load, registering this machine's key failed (a question or agent message), no device lists a key, sealing fails — sends nothing and returns an error with the reason (`E2E_REQUIRED` from `hwip_notify` and `hwip_file`, `SEALED_ASK_FAILED`, `SEALED_SEND_FAILED`). An account with no iPhone or Android app signed in has nobody to read a push, so `hwip_notify`, `hwip_file` and `hwip_ask` return `NO_APP_DEVICE` ("sign in to the Hwip app on an iPhone or Android phone") and send nothing; the macOS app does not count, and `hwip_agent_send` needs no app.
|
|
402
|
+
|
|
403
|
+
**Trust is automatic.** Every device the account lists with a key is pinned in `~/.hwip/trusted-devices.json` the first time a send sees it (this machine itself is not). When a pinned device lists another key — a reinstalled app, a new phone under the same id — the pin moves to the new key, and the next result of `hwip_notify`, `hwip_file`, `hwip_ask` or `hwip_agent_send` ends with one line: `Note: <nickname or deviceId> changed its encryption key — the new key is trusted. If you did not expect this, check it with: hwip trust`. The change waits in `~/.hwip/key-changes.json` (beside the pins, shared with the `hwip` CLI and the Claude Code plugin) until one of them shows it, so it is shown once, wherever the pin was replaced: the one that takes it holds the lock below, so a pin being replaced meanwhile cannot bring it back. A change whose record cannot be written is noted in this server's next result all the same. A pin never stops a send or an answer. The file is shared with the `hwip` CLI, which keeps its listener's sealed-command numbers there too, so this server changes it only under `~/.hwip/trusted-devices.lock`, re-reads it first and leaves every other field as it was; when the lock is busy for a second the pin is skipped and the send goes on, and a changed key is still noted in this server's next result. A file that cannot be read is left alone with one stderr line (`[hwip] trust: the trust file is unreadable — pins not checked`), and sends go on. A key already pinned for another device id is not pinned again, and no more than 64 devices are pinned. `hwip trust` shows the pins and their fingerprints, to compare with the device screens.
|
|
404
|
+
|
|
405
|
+
**Threat model:** against a passive backend — a leaked snapshot, an operator reading the table — the stored ciphertext and wrapped keys are useless, so push contents stay private. Three limits worth knowing:
|
|
406
|
+
- **No protection from an active malicious operator.** Recipient public keys come from `GET /devices` on that same server, unsigned, and every listed key is sealed for. A backend that injects a device record carrying its own key, or swaps a device's key, gets the message key wrapped for it and reads what is sent. A swapped key shows as the one-line `changed its encryption key` note, and `hwip trust` lets a person compare fingerprints; neither blocks the send.
|
|
407
|
+
- **No forward secrecy.** The ECDH secret for a given sender/device pair is static, so compromising either private key opens every past push wrapped for that pair.
|
|
408
|
+
- **`senderPublicKey` is unsigned**, so a swapped one makes a push undecryptable — that direction fails closed rather than leaking.
|
|
409
|
+
|
|
410
|
+
## License
|
|
411
|
+
|
|
412
|
+
Apache-2.0
|
|
413
|
+
|
|
414
|
+
Contributions are welcome: see [CONTRIBUTING.md](./CONTRIBUTING.md). Report security issues as described in [SECURITY.md](./SECURITY.md).
|