@softov/ahpc 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 +21 -0
- package/README.md +296 -0
- package/dist/src/ahp/channels.d.ts +146 -0
- package/dist/src/ahp/channels.js +423 -0
- package/dist/src/ahp/connection.d.ts +656 -0
- package/dist/src/ahp/connection.js +1 -0
- package/dist/src/ahp/fake.d.ts +69 -0
- package/dist/src/ahp/fake.js +1996 -0
- package/dist/src/ahp/live.d.ts +160 -0
- package/dist/src/ahp/live.js +2641 -0
- package/dist/src/ahp/operate.d.ts +36 -0
- package/dist/src/ahp/operate.js +56 -0
- package/dist/src/ahp/publish.d.ts +39 -0
- package/dist/src/ahp/publish.js +370 -0
- package/dist/src/ahp/status.d.ts +23 -0
- package/dist/src/ahp/status.js +23 -0
- package/dist/src/ahp/types.d.ts +745 -0
- package/dist/src/ahp/types.js +27 -0
- package/dist/src/app.d.ts +54 -0
- package/dist/src/app.js +369 -0
- package/dist/src/blocks.d.ts +88 -0
- package/dist/src/blocks.js +102 -0
- package/dist/src/cli/main.d.ts +13 -0
- package/dist/src/cli/main.js +1537 -0
- package/dist/src/cli/render.d.ts +43 -0
- package/dist/src/cli/render.js +91 -0
- package/dist/src/config.d.ts +63 -0
- package/dist/src/config.js +45 -0
- package/dist/src/connect.d.ts +53 -0
- package/dist/src/connect.js +69 -0
- package/dist/src/control.d.ts +189 -0
- package/dist/src/control.js +2241 -0
- package/dist/src/diff.d.ts +44 -0
- package/dist/src/diff.js +111 -0
- package/dist/src/flags.d.ts +28 -0
- package/dist/src/flags.js +76 -0
- package/dist/src/main.d.ts +10 -0
- package/dist/src/main.js +54 -0
- package/dist/src/schedule.d.ts +46 -0
- package/dist/src/schedule.js +140 -0
- package/dist/src/screens.d.ts +79 -0
- package/dist/src/screens.js +1322 -0
- package/dist/src/state.d.ts +355 -0
- package/dist/src/state.js +531 -0
- package/dist/src/terminal.d.ts +44 -0
- package/dist/src/terminal.js +101 -0
- package/dist/src/tui.d.ts +92 -0
- package/dist/src/tui.js +372 -0
- package/dist/src/view/automations.d.ts +31 -0
- package/dist/src/view/automations.js +76 -0
- package/dist/src/view/bood/art.d.ts +90 -0
- package/dist/src/view/bood/art.js +152 -0
- package/dist/src/view/bood/beetle.d.ts +7 -0
- package/dist/src/view/bood/beetle.js +123 -0
- package/dist/src/view/bood/bunny.d.ts +8 -0
- package/dist/src/view/bood/bunny.js +228 -0
- package/dist/src/view/bood/cat.d.ts +9 -0
- package/dist/src/view/bood/cat.js +358 -0
- package/dist/src/view/bood/crab.d.ts +7 -0
- package/dist/src/view/bood/crab.js +121 -0
- package/dist/src/view/bood/index.d.ts +29 -0
- package/dist/src/view/bood/index.js +25 -0
- package/dist/src/view/bood/motion.d.ts +178 -0
- package/dist/src/view/bood/motion.js +298 -0
- package/dist/src/view/bood/owl.d.ts +9 -0
- package/dist/src/view/bood/owl.js +285 -0
- package/dist/src/view/bood/registry.d.ts +36 -0
- package/dist/src/view/bood/registry.js +218 -0
- package/dist/src/view/bood/render.d.ts +31 -0
- package/dist/src/view/bood/render.js +196 -0
- package/dist/src/view/bood/sprite.d.ts +18 -0
- package/dist/src/view/bood/sprite.js +130 -0
- package/dist/src/view/bood/sprout.d.ts +8 -0
- package/dist/src/view/bood/sprout.js +121 -0
- package/dist/src/view/bood/types.d.ts +158 -0
- package/dist/src/view/bood/types.js +57 -0
- package/dist/src/view/bubble.d.ts +75 -0
- package/dist/src/view/bubble.js +86 -0
- package/dist/src/view/changes.d.ts +35 -0
- package/dist/src/view/changes.js +22 -0
- package/dist/src/view/composer.d.ts +64 -0
- package/dist/src/view/composer.js +192 -0
- package/dist/src/view/controls.d.ts +44 -0
- package/dist/src/view/controls.js +49 -0
- package/dist/src/view/creature.d.ts +69 -0
- package/dist/src/view/creature.js +83 -0
- package/dist/src/view/customizations.d.ts +28 -0
- package/dist/src/view/customizations.js +53 -0
- package/dist/src/view/details.d.ts +65 -0
- package/dist/src/view/details.js +65 -0
- package/dist/src/view/filediff.d.ts +29 -0
- package/dist/src/view/filediff.js +24 -0
- package/dist/src/view/files.d.ts +31 -0
- package/dist/src/view/files.js +39 -0
- package/dist/src/view/hitl.d.ts +43 -0
- package/dist/src/view/hitl.js +171 -0
- package/dist/src/view/icons.d.ts +13 -0
- package/dist/src/view/icons.js +71 -0
- package/dist/src/view/picker.d.ts +42 -0
- package/dist/src/view/picker.js +71 -0
- package/dist/src/view/sessionhead.d.ts +41 -0
- package/dist/src/view/sessionhead.js +60 -0
- package/dist/src/view/sessions.d.ts +34 -0
- package/dist/src/view/sessions.js +61 -0
- package/dist/src/view/terminal.d.ts +30 -0
- package/dist/src/view/terminal.js +32 -0
- package/dist/src/view/toolcall.d.ts +27 -0
- package/dist/src/view/toolcall.js +48 -0
- package/dist/src/view/transcript.d.ts +50 -0
- package/dist/src/view/transcript.js +60 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luiz Fernando Softov <lfs@softov.dev>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# ahpc
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@softov/ahpc)
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
A terminal client for the [Agent Host Protocol](https://microsoft.github.io/agent-host-protocol/).
|
|
10
|
+
It can be used as cli (commands) or tui (interactive chat).
|
|
11
|
+
|
|
12
|
+
Connect to an AHP host, manage sessions, and work with agents directly from your terminal.
|
|
13
|
+
|
|
14
|
+
> [!NOTE]
|
|
15
|
+
> `ahpc` is a client. It does not run agents or models itself.
|
|
16
|
+
> You need an AHP-compatible host to connect to.
|
|
17
|
+
|
|
18
|
+
The interface is built with [TextUI](https://github.com/softov/textui), a component toolkit for terminal applications.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install -g @softov/ahpc
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or run it without installing, with `npx @softov/ahpc`.
|
|
27
|
+
|
|
28
|
+
The package is scoped; the command it installs is `ahpc`.
|
|
29
|
+
|
|
30
|
+
A scripted host is built in, so the screen runs with nothing else to set up:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
ahpc
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Point it at a real host:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
ahpc --host ws://127.0.0.1:9187
|
|
40
|
+
ahpc session list --host ws://127.0.0.1:9187
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
[`ahpd`](https://github.com/softov/ahpd) and VS Code's agent host are both AHP hosts.
|
|
44
|
+
|
|
45
|
+
## What it does
|
|
46
|
+
|
|
47
|
+
| | |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Sessions | List, create, configure, archive and delete sessions on a host. |
|
|
50
|
+
| Turns | Send a prompt, stream the reply, queue follow-ups, cancel. |
|
|
51
|
+
| Answering | Approve or deny tool calls, and answer questions the agent asks mid-turn. |
|
|
52
|
+
| Chats | Multiple conversations in one session. |
|
|
53
|
+
| Changes and files | Files a session changed, their diffs, and the host's filesystem. |
|
|
54
|
+
| Terminals | Shells running on the host, and their output. |
|
|
55
|
+
| Automations | Scheduled and triggered runs, with their history. |
|
|
56
|
+
| Customizations | Skills, prompts, agents and MCP servers, and which are enabled. |
|
|
57
|
+
| Telemetry | Stream the host's log. |
|
|
58
|
+
|
|
59
|
+
## Interactive
|
|
60
|
+
|
|
61
|
+
`ahpc` with no command opens the screen.
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
The list shows every session on the host: its status, the agent, the workspace and branch, and what it is doing right now. Sessions waiting on you are counted at the top. `ctrl+f` filters by title, agent or workspace, and `x` shows the archived ones with a count of how many that is.
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
Inside a session, the header shows the model, thinking level, permission mode, workspace and branch. All of it comes from the host, not from local guesses.
|
|
70
|
+
|
|
71
|
+
`ctrl+f` opens a find box in the top right. Type and the term is coloured wherever it appears; `enter` and `down` go to the next one, `up` to the one before, and both wrap. The box says which match you are on and how many there are.
|
|
72
|
+
|
|
73
|
+

|
|
74
|
+
|
|
75
|
+
Before the first message, a new session asks the agent, the model and its options, the permission mode and the workspace. The questions come from the host's `configSchema`, so options `ahpc` has never seen still get a row.
|
|
76
|
+
|
|
77
|
+
### Keys
|
|
78
|
+
|
|
79
|
+
| Key | |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `enter` | Send |
|
|
82
|
+
| `alt+enter` | Newline |
|
|
83
|
+
| `tab` | Move to the options row |
|
|
84
|
+
| `esc` | Close the menu, then leave the field, then go back |
|
|
85
|
+
| `/` | Slash commands, from the host and from `ahpc` — `/config` opens the settings palette |
|
|
86
|
+
| `@` | Complete a file path on the host |
|
|
87
|
+
| `ctrl+g` | Edit the message in `$VISUAL` or `$EDITOR` |
|
|
88
|
+
| `ctrl+p` | Command palette |
|
|
89
|
+
| `f1` | Every key that works where you are |
|
|
90
|
+
| `ctrl+f` | Filter the session list, or find in the open conversation |
|
|
91
|
+
| `ctrl+n` | New session |
|
|
92
|
+
| `ctrl+r` | Refresh |
|
|
93
|
+
| `alt+t` | Theme |
|
|
94
|
+
| `alt+m` | Markdown on or off |
|
|
95
|
+
| `ctrl+c` | Cancel the running turn, or quit |
|
|
96
|
+
|
|
97
|
+
Editing in the composer follows readline:
|
|
98
|
+
|
|
99
|
+
| Key | |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `ctrl+a` / `ctrl+e` | Start or end of the line |
|
|
102
|
+
| `ctrl+k` / `ctrl+u` | Delete to the end, or to the start |
|
|
103
|
+
| `ctrl+w`, `alt+backspace` | Delete the word before the caret |
|
|
104
|
+
| `alt+d` | Delete the word after it |
|
|
105
|
+
| `ctrl+z` / `alt+z` | Undo, redo |
|
|
106
|
+
| `ctrl+←` / `ctrl+→` | Move a word at a time |
|
|
107
|
+
|
|
108
|
+
Undo groups a run of typing into one step, so it takes back a word rather than a character.
|
|
109
|
+
|
|
110
|
+
When a tool call is waiting: `a` approves, `d` denies, `1`-`9` pick an offered option. When the agent asks a question: `tab` moves between fields, `space` selects, `enter` sends.
|
|
111
|
+
|
|
112
|
+
## Commands
|
|
113
|
+
|
|
114
|
+
`ahpc <command>` runs without the screen. Output is formatted for reading; `--json` gives the same data for scripts.
|
|
115
|
+
|
|
116
|
+
### Sessions
|
|
117
|
+
|
|
118
|
+
| Command | | |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `session list` | List sessions, newest first | `--archived` `--json` |
|
|
121
|
+
| `session show <uri>` | Session details | `--full` `--json` |
|
|
122
|
+
| `session new` | Create a new session | `--agent` `--cwd` `--set k=v` `--json` |
|
|
123
|
+
| `session rm <uri>` | Delete a session | |
|
|
124
|
+
| `session read <uri>` | Mark as read | `--unread` |
|
|
125
|
+
| `session archive <uri>` | Archive a session | `--undo` |
|
|
126
|
+
| `session history <uri>` | Show turns history | `--all` `--full` `--json` |
|
|
127
|
+
| `session config <uri>` | Show the config schema and current values | `--json` |
|
|
128
|
+
| `session set <uri> <k> <v>` | Change one config property | |
|
|
129
|
+
| `session customizations <uri>` | List skills, prompts, agents and MCP servers | `--json` |
|
|
130
|
+
| `session toggle <uri> <id>` | Toggle customization on/off | `--off` |
|
|
131
|
+
| `session export <uri>` | Export the session as one document | `--json` `--markdown` |
|
|
132
|
+
|
|
133
|
+
### Turns
|
|
134
|
+
|
|
135
|
+
| Command | | |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `prompt <uri> <text>` | Send a prompt and stream the reply | `--model` `--json` |
|
|
138
|
+
| `exec <text>` | Run one prompt in a throwaway session | `--agent` `--cwd` `--model` `--json` |
|
|
139
|
+
| `cancel <uri>` | Cancel the running turn | |
|
|
140
|
+
| `queue <uri> <text>` | Queue a prompt behind the running turn | `--model` |
|
|
141
|
+
| `unqueue <uri> <id>` | Remove a queued prompt | |
|
|
142
|
+
|
|
143
|
+
### Answering
|
|
144
|
+
|
|
145
|
+
| Command | | |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `watch <uri>` | Block until the agent needs input, print it, exit | `--until turn\|input\|idle` `--timeout` `--json` |
|
|
148
|
+
| `confirm <uri> <toolCallId>` | Approve a tool call | `--deny` `--option` |
|
|
149
|
+
| `answer <uri> <requestId>` | Answer a question | `--field k=v` `--reject` |
|
|
150
|
+
|
|
151
|
+
### Chats
|
|
152
|
+
|
|
153
|
+
| Command | | |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `chat list <uri>` | List the chats in a session | `--json` |
|
|
156
|
+
| `chat new <uri> [text]` | Start another chat | |
|
|
157
|
+
| `chat rm <chatUri>` | Close a chat | |
|
|
158
|
+
|
|
159
|
+
### The host
|
|
160
|
+
|
|
161
|
+
| Command | | |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `agents` | List the agents the host serves, with their models | `--json` |
|
|
164
|
+
| `models` | List every model, grouped by agent | `--json` |
|
|
165
|
+
| `commands` | List the slash commands the host offers | `--json` |
|
|
166
|
+
| `customizations` | List skills, prompts, agents and MCP servers | `--kind` `--json` |
|
|
167
|
+
| `completions <uri> <text>` | Show what the host would complete | `--offset` `--json` |
|
|
168
|
+
| `logs` | Stream the host's log | `--level` `--follow` |
|
|
169
|
+
| `auth` | List the resources this host protects | `--json` |
|
|
170
|
+
| `auth <resource>` | Send a token for one | `--token` `--expires-in` |
|
|
171
|
+
| `status` | Show the current connection | `--json` |
|
|
172
|
+
|
|
173
|
+
### Changes and files
|
|
174
|
+
|
|
175
|
+
| Command | | |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `changes <uri>` | List the files a session changed | `--list` `--scope` `--reviewed` `--unreviewed` `--operations` `--run` `--json` |
|
|
178
|
+
| `content <uri> <file>` | Print one changed file in full | |
|
|
179
|
+
| `resource list <uri>` | List a directory on the host | `--json` |
|
|
180
|
+
| `resource read <uri>` | Read a file on the host | |
|
|
181
|
+
| `resource stat <uri>` | Show a file's type and size | `--json` |
|
|
182
|
+
| `resource write <uri> [file]` | Write a file, from a path or stdin | `--create-only` `--force` |
|
|
183
|
+
| `resource rm <uri>` | Delete a file or directory | `--recursive` |
|
|
184
|
+
| `resource mkdir <uri>` | Create a directory | |
|
|
185
|
+
| `resource mv <uri> <to>` | Move or rename | `--fail-if-exists` |
|
|
186
|
+
| `resource cp <uri> <to>` | Copy | `--fail-if-exists` |
|
|
187
|
+
|
|
188
|
+
Writes are guarded by the file's etag unless you pass `--force`, so two clients editing the same file cannot silently overwrite each other.
|
|
189
|
+
|
|
190
|
+
### Terminals
|
|
191
|
+
|
|
192
|
+
| Command | | |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `terminal list` | List running terminals | `--json` |
|
|
195
|
+
| `terminal new` | Open a shell | `--cwd` `--name` |
|
|
196
|
+
| `terminal rm <uri>` | Close a terminal | |
|
|
197
|
+
| `terminal send <uri> <text>` | Send input to a terminal | |
|
|
198
|
+
| `terminal watch <uri>` | Follow a terminal's output | `--timeout` |
|
|
199
|
+
|
|
200
|
+
### Automations
|
|
201
|
+
|
|
202
|
+
| Command | | |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| `automation list` | List automations | `--json` |
|
|
205
|
+
| `automation show <uri>` | Show one automation | `--json` |
|
|
206
|
+
| `automation triggers` | List the triggers this host supports | `--json` |
|
|
207
|
+
| `automation runs <uri>` | Show an automation's run history | `--json` |
|
|
208
|
+
| `automation run <uri>` | Run it now | |
|
|
209
|
+
| `automation enable <uri>` / `disable <uri>` | Enable or disable it | |
|
|
210
|
+
| `automation rm <uri>` | Delete it | |
|
|
211
|
+
|
|
212
|
+
### Anything else
|
|
213
|
+
|
|
214
|
+
| Command | | |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| `dispatch <uri> <type>` | Send a raw protocol action | `--field k=v` `--chat` |
|
|
217
|
+
| `config` | Show the config file path and current values | `--json` |
|
|
218
|
+
| `help` | Print this command list | |
|
|
219
|
+
|
|
220
|
+
## AHP support
|
|
221
|
+
|
|
222
|
+
All 30 client-to-server requests are implemented, and 21 of the 45 client-dispatchable actions are used. `ahpc` subscribes to the root, session, chat, terminal and automation channels, plus the telemetry channel the host advertises for its log.
|
|
223
|
+
|
|
224
|
+
AHP is symmetrical, so a host can also request things from the client. Nine of the ten server-initiated methods are implemented; `createResourceWatch` is not. Nothing is shared until `--publish <dir>` names a directory, and it stays read-only without `--publish-writable`:
|
|
225
|
+
|
|
226
|
+
```sh
|
|
227
|
+
ahpc --host ws://127.0.0.1:9187 --publish ~/notes
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The host reads those files at `virtual://<clientId>/<path>`. Publishing lasts only while the screen is open.
|
|
231
|
+
|
|
232
|
+
[docs/CONFORMANCE.md](docs/CONFORMANCE.md) covers the full surface: which actions are dispatched, four divergences and why, undeclared fields hosts send in practice, and three defects that belong to the protocol package rather than any implementation.
|
|
233
|
+
|
|
234
|
+
## Configuration
|
|
235
|
+
|
|
236
|
+
`$XDG_CONFIG_HOME/ahpc/config.json`, or `~/.config/ahpc/config.json`:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{ "host": "ws://127.0.0.1:9187", "theme": "paper-light" }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Precedence: a flag overrides an environment variable, which overrides the file.
|
|
243
|
+
|
|
244
|
+
| | |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `--host`, `AHPC_HOST` | The host to connect to |
|
|
247
|
+
| `--token`, `AHPC_TOKEN` | A bearer token for it |
|
|
248
|
+
| `AHPC_TOKEN_<RESOURCE>` | A token for one protected resource |
|
|
249
|
+
| `--config-file` | Read this file instead |
|
|
250
|
+
|
|
251
|
+
`ahpc config` prints the file path and the values in force. It works without a host, which is what you need when the host is the problem.
|
|
252
|
+
|
|
253
|
+
### Keys
|
|
254
|
+
|
|
255
|
+
`keys` maps a chord to a command id, or to `null` to unbind it:
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"keys": {
|
|
260
|
+
"ctrl+g": "editor.open",
|
|
261
|
+
"ctrl+t": null,
|
|
262
|
+
"ctrl+y": "session.new"
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Naming a chord replaces every default on it, so a chord is either yours or the client's and never half of each. A chord bound to a name no command answers to is reported at startup rather than ignored. `ctrl+c`, `ctrl+d`, `ctrl+h`, `ctrl+i`, `ctrl+j`, `ctrl+m` and `ctrl+[` cannot be rebound usefully — a terminal sends them as interrupt, end-of-file, backspace, tab, newline, return and escape.
|
|
268
|
+
|
|
269
|
+
## Development
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
git clone https://github.com/softov/ahpc
|
|
273
|
+
cd ahpc
|
|
274
|
+
npm install
|
|
275
|
+
|
|
276
|
+
npm test
|
|
277
|
+
npm run typecheck
|
|
278
|
+
npm run build # dist/src, which is what the package ships
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Two tools check the client against the protocol itself:
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
npm run schema # a strict JSON Schema from the package's own declarations
|
|
285
|
+
npm run wire -- <capture> # check a recording against it
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`AHPC_RECORD=<file>` appends every frame sent and received. `test/conformance.test.ts` runs the same check against frames produced by the test run itself, so it cannot pass on a stale recording.
|
|
289
|
+
|
|
290
|
+
The screens, widgets and input handling come from [TextUI](https://github.com/softov/textui) — `@textui/core` for components and state, `@textui/widgets` for the catalog, `@textui/terminal` for rendering and key decoding, and `@textui/testing` for the harness the tests run in. `ahpc` began as an example inside it.
|
|
291
|
+
|
|
292
|
+
[docs/DESIGN.md](docs/DESIGN.md) covers why the client is built this way: how a terminal handles a transcript differently from a browser, and what the widget catalog was missing.
|
|
293
|
+
|
|
294
|
+
## License
|
|
295
|
+
|
|
296
|
+
MIT.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/** Who is holding which channel open, and what the host is saying on it. */
|
|
2
|
+
/** One thing the host said on a channel: an action, or a protocol notification. */
|
|
3
|
+
export interface ChannelEvent {
|
|
4
|
+
type: string;
|
|
5
|
+
params?: unknown;
|
|
6
|
+
}
|
|
7
|
+
/** A channel event with the channel it arrived on. */
|
|
8
|
+
export interface AddressedEvent {
|
|
9
|
+
channel: string;
|
|
10
|
+
event: ChannelEvent;
|
|
11
|
+
}
|
|
12
|
+
/** State as the host holds it, before this client has read anything out of it. */
|
|
13
|
+
export type ChannelState = Record<string, unknown> | null;
|
|
14
|
+
/**
|
|
15
|
+
* One reader of one channel.
|
|
16
|
+
*
|
|
17
|
+
* `opened` arrives before any `event`, and arrives *again* after a reconnect
|
|
18
|
+
* that could not be closed by replay: a consumer rebuilds from the state it is
|
|
19
|
+
* handed rather than assuming the one it had is still the truth.
|
|
20
|
+
*/
|
|
21
|
+
export interface Consumer {
|
|
22
|
+
/** The channel's state, as of the moment this reader joined it. */
|
|
23
|
+
opened(state: ChannelState): void;
|
|
24
|
+
/** Everything the host sends on the channel, in the order it sent it. */
|
|
25
|
+
event(event: ChannelEvent): void;
|
|
26
|
+
/** The host would not serve the channel, in the words it used. */
|
|
27
|
+
refused?(message: string): void;
|
|
28
|
+
}
|
|
29
|
+
/** What this registry needs of a protocol client. */
|
|
30
|
+
export interface ChannelClient {
|
|
31
|
+
subscribe(uri: string): Promise<{
|
|
32
|
+
result: {
|
|
33
|
+
snapshot?: {
|
|
34
|
+
state?: unknown;
|
|
35
|
+
fromSeq?: number;
|
|
36
|
+
} | null;
|
|
37
|
+
};
|
|
38
|
+
}>;
|
|
39
|
+
unsubscribe(uri: string): Promise<void>;
|
|
40
|
+
events(): AsyncIterableIterator<AddressedEvent>;
|
|
41
|
+
}
|
|
42
|
+
/** A reader's hold on a channel, given up by calling `release`. */
|
|
43
|
+
export interface Hold {
|
|
44
|
+
release(): void;
|
|
45
|
+
}
|
|
46
|
+
/** A snapshot the host handed back for one channel during a reconnect. */
|
|
47
|
+
export interface ResumedSnapshot {
|
|
48
|
+
resource: string;
|
|
49
|
+
state?: unknown;
|
|
50
|
+
}
|
|
51
|
+
export interface Channels {
|
|
52
|
+
/**
|
|
53
|
+
* Take a hold on a channel and start reading it.
|
|
54
|
+
*
|
|
55
|
+
* Synchronous, because every caller is a screen being drawn: the subscribe
|
|
56
|
+
* happens behind this and the consumer hears about it through `opened`. A
|
|
57
|
+
* hold released before the subscribe lands never subscribes at all.
|
|
58
|
+
*/
|
|
59
|
+
open(uri: string, consumer: Consumer): Hold;
|
|
60
|
+
/**
|
|
61
|
+
* The channel's state now, without holding it open afterwards.
|
|
62
|
+
*
|
|
63
|
+
* A read rather than a watch: this subscribes, takes the state, and
|
|
64
|
+
* unsubscribes again unless somebody else is holding the channel. A host
|
|
65
|
+
* has no other way to answer "what is in there" - there is no `getState` -
|
|
66
|
+
* so the subscription is the question and letting go is the whole point.
|
|
67
|
+
*/
|
|
68
|
+
state(uri: string): Promise<ChannelState>;
|
|
69
|
+
/** Every channel held right now, which is what `reconnect` has to be told. */
|
|
70
|
+
held(): string[];
|
|
71
|
+
/** The highest `serverSeq` this client has seen on any channel. */
|
|
72
|
+
seq(): number;
|
|
73
|
+
/**
|
|
74
|
+
* Read one channel's refusal, if it was refused.
|
|
75
|
+
*
|
|
76
|
+
* A refusal is an answer about what the host is - `-32001` for a session
|
|
77
|
+
* whose agent has gone - and asking again on every keystroke turns one
|
|
78
|
+
* refusal into a stream of them.
|
|
79
|
+
*/
|
|
80
|
+
refusal(uri: string): string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Let a refusal be asked again: one channel, or every one of them.
|
|
83
|
+
*
|
|
84
|
+
* What a host refuses can stop being refused - a session is created, an
|
|
85
|
+
* agent comes back - and the answer is only worth keeping until something
|
|
86
|
+
* happens that could have changed it.
|
|
87
|
+
*/
|
|
88
|
+
forget(uri?: string): void;
|
|
89
|
+
/** Start reading a connection's event stream. Ends when the stream does. */
|
|
90
|
+
drain(client: ChannelClient): void;
|
|
91
|
+
/**
|
|
92
|
+
* Take up the same channels again on a new connection.
|
|
93
|
+
*
|
|
94
|
+
* `replayed` is the branch where the host could name everything missed:
|
|
95
|
+
* the envelopes go to their consumers in order and nobody is re-opened.
|
|
96
|
+
* `resumed` is the branch where it could not, and every consumer is handed
|
|
97
|
+
* a fresh state. `missing` is refused either way.
|
|
98
|
+
*/
|
|
99
|
+
resume(client: ChannelClient, result: {
|
|
100
|
+
replayed?: readonly unknown[];
|
|
101
|
+
resumed?: readonly ResumedSnapshot[];
|
|
102
|
+
missing?: readonly string[];
|
|
103
|
+
}): void;
|
|
104
|
+
/**
|
|
105
|
+
* Take a channel the handshake already opened, with the state it answered.
|
|
106
|
+
*
|
|
107
|
+
* `initialize` accepts `initialSubscriptions` and answers with a snapshot
|
|
108
|
+
* for each, which leaves the channel subscribed at the host before anything
|
|
109
|
+
* here has asked for it. Without this the first reader would subscribe again
|
|
110
|
+
* - the round trip the handshake exists to save, and a second `subscribe`
|
|
111
|
+
* for a channel this connection already holds.
|
|
112
|
+
*/
|
|
113
|
+
adopt(uri: string, state: ChannelState): void;
|
|
114
|
+
/** Forget every subscription without unsubscribing: the socket is already gone. */
|
|
115
|
+
detach(): void;
|
|
116
|
+
}
|
|
117
|
+
export interface ChannelsOptions {
|
|
118
|
+
client: ChannelClient;
|
|
119
|
+
/** Told when a channel is refused, so the reason reaches a person. */
|
|
120
|
+
onRefusal?(uri: string, message: string): void;
|
|
121
|
+
/** Told when the host refuses an action this client dispatched. */
|
|
122
|
+
onRejection?(uri: string, message: string): void;
|
|
123
|
+
/**
|
|
124
|
+
* This client's id, as `initialize` gave it.
|
|
125
|
+
*
|
|
126
|
+
* Only so that a refusal can be told from somebody else's: an envelope
|
|
127
|
+
* naming another client is still not applied, and is not reported here.
|
|
128
|
+
*/
|
|
129
|
+
clientId?: string;
|
|
130
|
+
/** The host's own words for a failure, as a client would show them. */
|
|
131
|
+
reason(error: unknown): string;
|
|
132
|
+
/**
|
|
133
|
+
* How long to keep a channel nobody is reading, in milliseconds.
|
|
134
|
+
*
|
|
135
|
+
* Not laziness about letting go - a pause before it. Reading a snapshot and
|
|
136
|
+
* opening a view are two holds on the same channel a moment apart, and
|
|
137
|
+
* navigating away and back is two more, so releasing on the instant the
|
|
138
|
+
* count reaches zero puts an `unsubscribe` between every pair of them. A
|
|
139
|
+
* host is entitled to act on that: the reference one evicts a session from
|
|
140
|
+
* memory when its last subscriber leaves and restores it from disk on the
|
|
141
|
+
* next subscribe, and a client that unsubscribes and immediately subscribes
|
|
142
|
+
* again is racing that restore. `0` releases at once.
|
|
143
|
+
*/
|
|
144
|
+
lingerMs?: number;
|
|
145
|
+
}
|
|
146
|
+
export declare function openChannels(options: ChannelsOptions): Channels;
|