@kdonev/termscape 0.1.0 → 0.1.2
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/README.md +211 -79
- package/dist/agents/detect.d.ts +27 -0
- package/dist/agents/detect.d.ts.map +1 -0
- package/dist/agents/detect.js +210 -0
- package/dist/agents/detect.js.map +1 -0
- package/dist/agents/profiles.d.ts +119 -2
- package/dist/agents/profiles.d.ts.map +1 -1
- package/dist/agents/profiles.js +224 -4
- package/dist/agents/profiles.js.map +1 -1
- package/dist/agents/templates.d.ts +100 -0
- package/dist/agents/templates.d.ts.map +1 -0
- package/dist/agents/templates.js +165 -0
- package/dist/agents/templates.js.map +1 -0
- package/dist/agents/wiring.d.ts +30 -1
- package/dist/agents/wiring.d.ts.map +1 -1
- package/dist/agents/wiring.js +182 -10
- package/dist/agents/wiring.js.map +1 -1
- package/dist/cli.js +51 -14
- package/dist/cli.js.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +73 -0
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/store.d.ts +53 -1
- package/dist/db/store.d.ts.map +1 -1
- package/dist/db/store.js +118 -5
- package/dist/db/store.js.map +1 -1
- package/dist/hub.d.ts +264 -6
- package/dist/hub.d.ts.map +1 -1
- package/dist/hub.js +549 -33
- package/dist/hub.js.map +1 -1
- package/dist/hub.tgz +0 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +27 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/protocol/domain.d.ts +88 -0
- package/dist/protocol/domain.d.ts.map +1 -1
- package/dist/protocol/domain.js +114 -0
- package/dist/protocol/domain.js.map +1 -1
- package/dist/protocol/mcp-tools.d.ts +43 -0
- package/dist/protocol/mcp-tools.d.ts.map +1 -1
- package/dist/protocol/mcp-tools.js +65 -0
- package/dist/protocol/mcp-tools.js.map +1 -1
- package/dist/protocol/peer.d.ts +38 -1
- package/dist/protocol/peer.d.ts.map +1 -1
- package/dist/protocol/peer.js +41 -2
- package/dist/protocol/peer.js.map +1 -1
- package/dist/protocol/ws.d.ts +259 -0
- package/dist/protocol/ws.d.ts.map +1 -1
- package/dist/protocol/ws.js +169 -5
- package/dist/protocol/ws.js.map +1 -1
- package/dist/remote/deployer.d.ts.map +1 -1
- package/dist/remote/deployer.js +15 -2
- package/dist/remote/deployer.js.map +1 -1
- package/dist/remote/join-script.d.ts.map +1 -1
- package/dist/remote/join-script.js +34 -2
- package/dist/remote/join-script.js.map +1 -1
- package/dist/remote/lan.d.ts +73 -5
- package/dist/remote/lan.d.ts.map +1 -1
- package/dist/remote/lan.js +119 -10
- package/dist/remote/lan.js.map +1 -1
- package/dist/remote/peer-serve.d.ts.map +1 -1
- package/dist/remote/peer-serve.js +32 -3
- package/dist/remote/peer-serve.js.map +1 -1
- package/dist/remote/peer.d.ts +7 -0
- package/dist/remote/peer.d.ts.map +1 -1
- package/dist/remote/peer.js +22 -0
- package/dist/remote/peer.js.map +1 -1
- package/dist/remote/registry.d.ts +70 -2
- package/dist/remote/registry.d.ts.map +1 -1
- package/dist/remote/registry.js +134 -10
- package/dist/remote/registry.js.map +1 -1
- package/dist/server.d.ts +22 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +239 -117
- package/dist/server.js.map +1 -1
- package/dist/session/manager.d.ts +60 -4
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +198 -41
- package/dist/session/manager.js.map +1 -1
- package/dist/session/pty.d.ts +19 -0
- package/dist/session/pty.d.ts.map +1 -1
- package/dist/session/pty.js +84 -1
- package/dist/session/pty.js.map +1 -1
- package/package.json +1 -1
- package/web/assets/{index-BOqZOoep.js → index-9Z_8xZqk.js} +10 -6
- package/web/assets/index-DheUm5dl.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-B2JtCtHa.css +0 -1
package/README.md
CHANGED
|
@@ -8,9 +8,11 @@ hub exposes. Agents can look each other up, send each other messages, spawn
|
|
|
8
8
|
helpers into their workspace, and check on each other's terminals. A message
|
|
9
9
|
from one agent is delivered by typing it into the other's terminal, immediately.
|
|
10
10
|
|
|
11
|
-
Local-first: the hub runs on your machine
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Local-first: the hub runs on your machine and the UI is a browser tab. It binds
|
|
12
|
+
every interface, so the canvas opens on your phone or a second screen as well —
|
|
13
|
+
with its token — and another machine can attach itself from the join page.
|
|
14
|
+
`--listen loopback` keeps the whole thing to this machine. Other machines run
|
|
15
|
+
the same hub as a daemon and appear on the same canvas.
|
|
14
16
|
|
|
15
17
|
## Getting started
|
|
16
18
|
|
|
@@ -23,9 +25,22 @@ if you would rather it did not; the URL is printed either way. Nothing is
|
|
|
23
25
|
installed system-wide: state lives in `~/.termscape`, and deleting that
|
|
24
26
|
directory is the uninstall.
|
|
25
27
|
|
|
28
|
+
The hub is reachable from your network, so the same URL — token and all — opens
|
|
29
|
+
the canvas on a phone or a second screen, and the **+ machine** dialog already
|
|
30
|
+
has a join link in it for the second machine; see
|
|
31
|
+
[Adding another machine](#adding-another-machine). To keep the hub to this
|
|
32
|
+
machine entirely — no join page, nothing off 127.0.0.1:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx @kdonev/termscape --listen loopback
|
|
36
|
+
```
|
|
37
|
+
|
|
26
38
|
Slide out the **machines** panel: every machine, the workspaces on it, and
|
|
27
39
|
the agents in each. Point a workspace at a folder there, and start an agent
|
|
28
|
-
in it.
|
|
40
|
+
in it. Adding, editing and removing all open a dialog over the canvas, so the
|
|
41
|
+
node you acted on stays where it was and a refusal — a folder that is not
|
|
42
|
+
there, a rename the addresses will not allow — arrives in the dialog next to
|
|
43
|
+
the field, with what you typed still in it.
|
|
29
44
|
|
|
30
45
|
- **Scroll** to pan, **Ctrl/⌘ + scroll** to zoom, **Ctrl/⌘ + 1** to fit,
|
|
31
46
|
**Ctrl/⌘ + 2** to zoom to one terminal
|
|
@@ -35,6 +50,8 @@ in it.
|
|
|
35
50
|
ordinary pace is left alone
|
|
36
51
|
- Below 60% zoom terminals become preview cards — zoom in to interact
|
|
37
52
|
- Clicking an agent in the panel brings the canvas to it
|
|
53
|
+
- Clicking the canvas closes the panel, as does **Escape** — which closes the
|
|
54
|
+
panel first and clears the selection only once it is shut
|
|
38
55
|
|
|
39
56
|
## Requirements
|
|
40
57
|
|
|
@@ -66,20 +83,18 @@ next window.
|
|
|
66
83
|
|
|
67
84
|
## Adding another machine
|
|
68
85
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
npx @kdonev/termscape --listen lan
|
|
74
|
-
```
|
|
86
|
+
Nothing to turn on: a hub started with no flags at all hands out join links.
|
|
87
|
+
The startup banner prints an `enroll:` URL alongside the usual one, and the
|
|
88
|
+
**+ machine** dialog shows the same URL with a copy button.
|
|
75
89
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
That is a deliberate trade and worth knowing about, because `/join` is the one
|
|
91
|
+
page served without your token — it has to be typed by hand on a machine that
|
|
92
|
+
has nothing yet. So on a network you do not trust, anyone who can reach this
|
|
93
|
+
hub can pull the installer and put a machine on your canvas. `--listen
|
|
94
|
+
loopback` is how you say no, and it turns off the network entirely.
|
|
80
95
|
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
Open the join URL **on the machine you want to add** and run the command it
|
|
97
|
+
shows:
|
|
83
98
|
|
|
84
99
|
```bash
|
|
85
100
|
curl -fsSL http://studio:7777/join.sh | sh # macOS, Linux
|
|
@@ -134,9 +149,12 @@ have actually changed, and checks that tree really loads before trusting it.
|
|
|
134
149
|
|
|
135
150
|
Two things worth knowing:
|
|
136
151
|
|
|
137
|
-
-
|
|
138
|
-
|
|
139
|
-
|
|
152
|
+
- The **join page** answers by default, on the same interfaces the canvas does.
|
|
153
|
+
Anyone who can reach your hub can load that page and attach a machine to your
|
|
154
|
+
canvas; every *other* route still requires the token. It was opt-in once, and
|
|
155
|
+
the reason it is not any more is that the add-machine dialog had nothing to
|
|
156
|
+
show but an instruction to restart the hub with a flag — a feature reachable
|
|
157
|
+
only that way is a feature nobody uses. `--listen loopback` is the way back.
|
|
140
158
|
- Each download carries a single-use key that expires in 15 minutes. Once a
|
|
141
159
|
machine has joined it keeps a durable token in `~/.termscape/host-token` and
|
|
142
160
|
rejoins by itself after a reboot or a dropped link — its agents keep running
|
|
@@ -153,20 +171,72 @@ tunnel. Same protocol, opposite direction.
|
|
|
153
171
|
|---|---|
|
|
154
172
|
| `whoami` | your address, workspace and working directory |
|
|
155
173
|
| `list_agents` | everyone on the canvas, across every host, and whether they are busy |
|
|
174
|
+
| `list_templates` | the saved ways of starting an agent, and what each one runs |
|
|
156
175
|
| `send_message` | type a message into another agent's terminal, right now |
|
|
157
176
|
| `spawn_agent` | start a helper in your workspace, with an optional first task |
|
|
158
177
|
| `read_screen` | look at another agent's terminal without interrupting it |
|
|
159
178
|
| `set_status` | label your own window so the human can see what you are doing |
|
|
160
179
|
| `stop_agent` | stop an agent you spawned |
|
|
180
|
+
| `propose_template` | ask the human to save a way of starting an agent, under a name |
|
|
181
|
+
|
|
182
|
+
`list_templates` is what makes `spawn_agent` usable for anything but a bare
|
|
183
|
+
CLI: its `profile` takes a template id, and this is how an agent finds out
|
|
184
|
+
which ids exist. It reports the *names* of the environment variables a template
|
|
185
|
+
sets and never their values, which are credentials more often than not.
|
|
186
|
+
|
|
187
|
+
`propose_template` is the only one that asks rather than does. A template
|
|
188
|
+
changes how *future* agents are launched, on every machine, with nobody
|
|
189
|
+
necessarily watching — so the canvas shows it to a human, who can edit it
|
|
190
|
+
before accepting or decline it outright. The tool returns as soon as they have
|
|
191
|
+
been shown it rather than blocking until they answer, and the decision is typed
|
|
192
|
+
back into the agent's terminal. Nothing is stored unless someone says yes.
|
|
161
193
|
|
|
162
194
|
Every agent is also given a brief explaining its address, its peers, and that
|
|
163
195
|
text arriving as `[from <address>] ...` is a colleague rather than the human.
|
|
164
196
|
|
|
165
197
|
## Agent profiles
|
|
166
198
|
|
|
167
|
-
An agent CLI is configuration, not code. Built-ins are `claude
|
|
168
|
-
|
|
169
|
-
|
|
199
|
+
An agent CLI is configuration, not code. Built-ins are `claude`, `codex`,
|
|
200
|
+
`gemini`, `opencode` and `shell`.
|
|
201
|
+
|
|
202
|
+
Every one of them except `shell` is wired to the hub's MCP endpoint: each gets an
|
|
203
|
+
address, a brief and the `send_message` tool set. They arrive there by four
|
|
204
|
+
different routes, because no two of these CLIs configure an MCP server the same
|
|
205
|
+
way — and **none of them writes to a file you own**, so there is nothing left
|
|
206
|
+
behind when a session ends or when the hub is killed rather than stopped.
|
|
207
|
+
|
|
208
|
+
| agent | how it reaches the hub | brief | resume |
|
|
209
|
+
| --- | --- | --- | --- |
|
|
210
|
+
| `claude` | `--mcp-config` on a generated file | `--append-system-prompt-file` | `--resume <uuid>` |
|
|
211
|
+
| `codex` | `-c mcp_servers.…` overrides, one run only | typed in at startup | restarts clean |
|
|
212
|
+
| `gemini` | `GEMINI_CLI_SYSTEM_SETTINGS_PATH` at a generated file | typed in at startup | restarts clean |
|
|
213
|
+
| `opencode` | `OPENCODE_CONFIG_CONTENT`, no file anywhere | typed in at startup | restarts clean |
|
|
214
|
+
|
|
215
|
+
The last three are typed at rather than handed a brief because none of them can
|
|
216
|
+
*append* to its system prompt — Codex's `base_instructions` and Gemini's
|
|
217
|
+
`GEMINI_SYSTEM_MD` each replace the whole thing, which would cost the agent its
|
|
218
|
+
own tool instructions. None of the three is resumable either: Codex mints a
|
|
219
|
+
session id it will not accept from us, and Gemini accepts one but resumes by
|
|
220
|
+
list index instead. They restart clean, and are briefed again when they do.
|
|
221
|
+
|
|
222
|
+
Two details worth knowing, because both are easy to get wrong:
|
|
223
|
+
|
|
224
|
+
- **Codex's bearer token goes in the environment, never `-c`.** Config
|
|
225
|
+
overrides land in the command line, where any other user on the machine can
|
|
226
|
+
read them. `bearer_token_env_var` exists precisely for this.
|
|
227
|
+
- **opencode is configured entirely from the environment.** Its config is
|
|
228
|
+
handed over as a string, merged with your own rather than replacing it, so
|
|
229
|
+
your models, themes and your own MCP servers survive the session. Note that
|
|
230
|
+
`opencode mcp add` is *not* how this is done: that command writes to
|
|
231
|
+
`~/.config/opencode/opencode.json` and ignores `OPENCODE_CONFIG` while doing
|
|
232
|
+
it. Setting the variable also stops opencode writing its default config file
|
|
233
|
+
on start, so a session leaves nothing behind at all.
|
|
234
|
+
|
|
235
|
+
`shell` is the one profile that is not an agent, and it is not one in a way no
|
|
236
|
+
flag can fix: it runs a shell, so text typed at it is executed rather than read.
|
|
237
|
+
It gets no brief, and its profile says so rather than relying on a default.
|
|
238
|
+
|
|
239
|
+
Override or add profiles in `~/.termscape/agents.toml`:
|
|
170
240
|
|
|
171
241
|
```toml
|
|
172
242
|
[my-agent]
|
|
@@ -175,8 +245,116 @@ args = ["--mcp-config", "{{mcp_config_path}}"]
|
|
|
175
245
|
status = "heuristic" # or "hooks", for exact turn boundaries
|
|
176
246
|
ready_hint = "[$#>%] ?$" # prompt regex, for the idle indicator
|
|
177
247
|
inject = "bracketed" # bracketed paste, or "raw"
|
|
248
|
+
brief = "typed" # "flag" if it can append a system prompt,
|
|
249
|
+
# "none" for a terminal that would execute one
|
|
250
|
+
version_args = ["--version"] # how to ask its version, for the panel
|
|
251
|
+
models_args = ["models"] # optional: one model per line on stdout
|
|
252
|
+
models = ["opus", "sonnet"] # the answer when it has no listing command
|
|
253
|
+
model_args = ["--model", "{{model}}"] # how it spells a model, if it takes one
|
|
254
|
+
effort_args = ["--effort", "{{effort}}"] # and an effort
|
|
255
|
+
efforts = ["low", "high"] # the levels it documents
|
|
178
256
|
```
|
|
179
257
|
|
|
258
|
+
### Templates
|
|
259
|
+
|
|
260
|
+
The picker offers **templates**, not CLIs. A template is an agent plus a model,
|
|
261
|
+
an effort and an opening instruction — a saved answer to all four, picked once
|
|
262
|
+
instead of typed every time. Every agent gets a bare template under its own
|
|
263
|
+
name, so `claude` and `shell` are still there and nothing that worked stops
|
|
264
|
+
working.
|
|
265
|
+
|
|
266
|
+
Templates are a root of their own in the panel, next to the machines — they are
|
|
267
|
+
config rather than a place, and one template is used on every machine, so it
|
|
268
|
+
does not live under one. **+ template** makes one, `edit` changes it, `×`
|
|
269
|
+
removes it. The list sits below the machines and starts collapsed: machines are
|
|
270
|
+
what you work in every day, templates are what you set up once and then forget.
|
|
271
|
+
|
|
272
|
+
They can also be written by hand, and a hand-written one wins:
|
|
273
|
+
|
|
274
|
+
```toml
|
|
275
|
+
[template.reviewer]
|
|
276
|
+
agent = "claude"
|
|
277
|
+
model = "opus"
|
|
278
|
+
effort = "high"
|
|
279
|
+
prompt = "Review the diff on this branch for correctness bugs. Report, do not fix."
|
|
280
|
+
|
|
281
|
+
# Set for the agents this template starts, on top of what the CLI already gets.
|
|
282
|
+
[template.reviewer.env]
|
|
283
|
+
ANTHROPIC_BASE_URL = "https://proxy.internal"
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- Templates made in the panel are stored in `state.db`, the same place
|
|
287
|
+
workspaces and hosts already live. `agents.toml` is a second, read-only
|
|
288
|
+
source: the hub never writes it, so a formatter cannot eat the comments and
|
|
289
|
+
ordering of a file you edit by hand.
|
|
290
|
+
- When both declare the same name **the file wins**, and the dialog refuses the
|
|
291
|
+
name rather than storing a row that would never appear. Someone who wrote a
|
|
292
|
+
template by hand meant it.
|
|
293
|
+
- An agent's own bare template is derived, not stored. Editing one makes a
|
|
294
|
+
stored template that shadows it; removing that reveals the bare one again
|
|
295
|
+
rather than leaving a gap.
|
|
296
|
+
- Removing a template takes nothing from the agents it already started. They
|
|
297
|
+
keep their model, their effort and their ability to resume, because a session
|
|
298
|
+
records what its template resolved to rather than looking it up again.
|
|
299
|
+
|
|
300
|
+
Three words, kept apart deliberately: an **agent** is the CLI program, a
|
|
301
|
+
**template** is what you pick from the list, and a **session** is one running
|
|
302
|
+
instance with an address and a window.
|
|
303
|
+
|
|
304
|
+
- **A template holds values, not arguments.** Claude Code takes `--model` and
|
|
305
|
+
`--effort`; opencode takes `-m provider/model` and has no effort setting on
|
|
306
|
+
its TUI at all; Codex takes `-m` but spells effort as a config override,
|
|
307
|
+
`-c model_reasoning_effort=…`, because it has no `--effort` flag. So the
|
|
308
|
+
template says *which* model, and the agent declares how to spell it. An agent that declares nothing takes nothing, and a template
|
|
309
|
+
asking for a model or an effort it cannot spell is a configuration error
|
|
310
|
+
reported when the file loads — visible in the dialog, not a flag silently
|
|
311
|
+
dropped at launch.
|
|
312
|
+
- **The first instruction is typed in once the CLI is up**, not passed as an
|
|
313
|
+
argument, and it does not repeat when a session is resumed. It is how the
|
|
314
|
+
session started, not what it is.
|
|
315
|
+
- **A resumed session comes back on the model it left with.** What the template
|
|
316
|
+
resolved to is recorded on the session, because resume rebuilds the command
|
|
317
|
+
line rather than replaying it — and because a template can be edited
|
|
318
|
+
afterwards.
|
|
319
|
+
- **Starting an agent on another machine sends values, not a template name.**
|
|
320
|
+
The two machines do not share config, so the name is resolved here first.
|
|
321
|
+
- **A template can set environment variables**, in the panel as `NAME=value`
|
|
322
|
+
lines or as a `[template.<name>.env]` table in the file. They are set for the
|
|
323
|
+
agents that template starts, on top of whatever the CLI would inherit
|
|
324
|
+
anyway, and they win over what the agent profile sets on the rare name both
|
|
325
|
+
name. This is where an API key, a base URL or a feature flag that
|
|
326
|
+
distinguishes two otherwise identical templates belongs — before it, the
|
|
327
|
+
difference could only live in whichever shell the hub happened to be started
|
|
328
|
+
from, which is not a per-template answer at all. Like the model, they are
|
|
329
|
+
recorded on the session, so a resumed agent comes back in the environment it
|
|
330
|
+
was launched in. An agent proposing a template cannot ask for any: choosing
|
|
331
|
+
what the next agent's credentials are is not something to review one dialog
|
|
332
|
+
at a time.
|
|
333
|
+
|
|
334
|
+
### What is actually installed
|
|
335
|
+
|
|
336
|
+
Each machine probes its own `PATH` and reports back, so the panel shows, per
|
|
337
|
+
machine, which agents are there, what version each is, and the models it
|
|
338
|
+
offers. That is per machine on purpose: a host has its own `PATH`, and starting
|
|
339
|
+
an agent it does not have used to fail at launch inside a terminal window,
|
|
340
|
+
where the error reads like the hub is broken.
|
|
341
|
+
|
|
342
|
+
- A declared agent that is **not** installed stays in the list, greyed out,
|
|
343
|
+
naming the command that was not found — rather than vanishing, which looks
|
|
344
|
+
like the config was ignored.
|
|
345
|
+
- Models are enumerated where the CLI can be asked (`opencode models` returns
|
|
346
|
+
a few hundred) and declared in the profile where it cannot. Claude Code has
|
|
347
|
+
no listing command; its `--help` documents the aliases instead, and it takes
|
|
348
|
+
a full model name as readily as an alias. Codex is declared too, for a
|
|
349
|
+
different reason: `codex debug models` does render the real catalog, but it
|
|
350
|
+
is a debug command answering with half a megabyte of JSON rather than the
|
|
351
|
+
one-per-line stdout the profile reads, so the profile carries the slugs that
|
|
352
|
+
catalog marks visible. In every case a full model name outside the list is
|
|
353
|
+
still accepted — the list is what the dropdown suggests, never a limit.
|
|
354
|
+
- Probing runs after the hub is already serving and never blocks it. The first
|
|
355
|
+
page load usually shows *checking…*, and fills in a moment later. **check
|
|
356
|
+
again** in the start-an-agent dialog re-probes every machine.
|
|
357
|
+
|
|
180
358
|
## State and restart
|
|
181
359
|
|
|
182
360
|
SQLite at `~/.termscape/state.db` holds everything needed to redraw the canvas
|
|
@@ -213,7 +391,14 @@ Agents can type into each other's terminals and spawn more agents. That is the
|
|
|
213
391
|
feature, and it is the risk surface: an agent that reads a hostile repository
|
|
214
392
|
could be talked into sending an attacker's text to a peer.
|
|
215
393
|
|
|
216
|
-
- The hub binds
|
|
394
|
+
- The hub binds every interface, so the canvas is reachable from your network —
|
|
395
|
+
but only with its token, which is minted per run and never printed anywhere
|
|
396
|
+
the network can read. `--listen loopback` narrows it to this machine.
|
|
397
|
+
- A hub running `--headless` — one that joined a canvas, or was deployed over
|
|
398
|
+
ssh and is reached through its tunnel — stays on `127.0.0.1` regardless.
|
|
399
|
+
- `/join` is the only route served without the token, and it answers by
|
|
400
|
+
default on every interface the hub bound. `--listen loopback` turns it off,
|
|
401
|
+
along with the rest of the network.
|
|
217
402
|
- Every agent gets its own bearer token. The sender of a message is taken from
|
|
218
403
|
that token, never from the arguments, so attribution cannot be forged.
|
|
219
404
|
- Messages are length-capped, rate-limited per sender, and always arrive with a
|
|
@@ -225,63 +410,10 @@ could be talked into sending an attacker's text to a peer.
|
|
|
225
410
|
- An agent may only stop agents it spawned.
|
|
226
411
|
- `--dangerously-skip-permissions` is never a default.
|
|
227
412
|
|
|
228
|
-
##
|
|
229
|
-
|
|
230
|
-
From a clone, rather than the published package:
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
git clone https://github.com/kdonev/termscape.git
|
|
234
|
-
cd termscape
|
|
235
|
-
npm install
|
|
236
|
-
npm run build
|
|
237
|
-
npm run dev # hub with the built UI
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
If `npm install` or `npm ci` tries to compile `better-sqlite3` and fails for
|
|
241
|
-
want of a C++ toolchain, install this way instead:
|
|
242
|
-
|
|
243
|
-
```bash
|
|
244
|
-
npm ci --ignore-scripts && npm rebuild node-pty
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
Any install that reads `package-lock.json` misses better-sqlite3's
|
|
248
|
-
`gypfile: false` and runs `node-gyp` on a module that already ships working
|
|
249
|
-
prebuilds. Skipping install scripts avoids that; the rebuild puts back the one
|
|
250
|
-
native build that is real, which is a no-op except on Linux. This is what CI
|
|
251
|
-
does. It does not affect anyone installing the published package.
|
|
252
|
-
|
|
253
|
-
```bash
|
|
254
|
-
npm test # unit + integration, no LLM required
|
|
255
|
-
npm run typecheck
|
|
256
|
-
npm run dev:web # vite dev server, expects a hub on :7777
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
The integration tests spawn real PTYs and drive the real MCP endpoint, so
|
|
260
|
-
`npm test` genuinely exercises message delivery and cross-host routing.
|
|
261
|
-
|
|
262
|
-
### Known quirks
|
|
263
|
-
|
|
264
|
-
- On Windows, node-pty prints `AttachConsole failed` to stderr when killing a
|
|
265
|
-
PTY from a process with no console attached (notably under the test runner).
|
|
266
|
-
It is noise from a helper process and does not affect behaviour.
|
|
267
|
-
|
|
268
|
-
### Releasing
|
|
269
|
-
|
|
270
|
-
The repo is a workspace of three private packages; what gets published is a
|
|
271
|
-
single package assembled by `packages/hub/scripts/pack-npm.mjs` — the hub,
|
|
272
|
-
the built UI it serves, and `@termscape/protocol` bundled inside it.
|
|
273
|
-
|
|
274
|
-
```bash
|
|
275
|
-
npm run pack:npm # build everything, then stage and pack the tarball
|
|
276
|
-
```
|
|
413
|
+
## Contributing
|
|
277
414
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
tarball on macOS, Windows and Linux and checks each one starts and serves the
|
|
281
|
-
canvas, and only then publishes to npm with provenance. Bump the version in
|
|
282
|
-
`packages/hub/package.json` **and** `HUB_VERSION` in `packages/hub/src/hub.ts`
|
|
283
|
-
together — peer compatibility is gated on the constant, and a test fails if
|
|
284
|
-
the two drift.
|
|
415
|
+
Building from a clone, running the tests and cutting a release:
|
|
416
|
+
see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
285
417
|
|
|
286
418
|
## License
|
|
287
419
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { AgentProfileInfo } from '../protocol/index.js';
|
|
3
|
+
import type { ProfileRegistry } from './profiles.js';
|
|
4
|
+
export declare class AgentDetector extends EventEmitter {
|
|
5
|
+
private readonly profiles;
|
|
6
|
+
private known;
|
|
7
|
+
private readonly cache;
|
|
8
|
+
private running;
|
|
9
|
+
constructor(profiles: ProfileRegistry);
|
|
10
|
+
/**
|
|
11
|
+
* What is known right now, without probing anything.
|
|
12
|
+
*
|
|
13
|
+
* Never async, and never empty: a profile that has not been probed yet is
|
|
14
|
+
* reported with `available: null`, which the panel draws as "checking".
|
|
15
|
+
* Detection must not be able to hold up a hub starting or a page loading.
|
|
16
|
+
*/
|
|
17
|
+
snapshot(): AgentProfileInfo[];
|
|
18
|
+
/**
|
|
19
|
+
* Probe every profile. Safe to call from anywhere, including on a timer or
|
|
20
|
+
* from the browser; concurrent calls share the one run in flight rather
|
|
21
|
+
* than spawning every CLI twice.
|
|
22
|
+
*/
|
|
23
|
+
refresh(): Promise<AgentProfileInfo[]>;
|
|
24
|
+
private run;
|
|
25
|
+
private probe;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=detect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../src/agents/detect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,eAAe,CAAC;AAkDnE,qBAAa,aAAc,SAAQ,YAAY;IAKjC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJrC,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IACvD,OAAO,CAAC,OAAO,CAA4C;gBAE9B,QAAQ,EAAE,eAAe;IAItD;;;;;;OAMG;IACH,QAAQ,IAAI,gBAAgB,EAAE;IAI9B;;;;OAIG;IACH,OAAO,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAQxB,GAAG;YAWH,KAAK;CA8DpB"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { extname } from 'node:path';
|
|
4
|
+
import { platform } from 'node:process';
|
|
5
|
+
import { which } from './resolve.js';
|
|
6
|
+
/**
|
|
7
|
+
* What is actually installed on this machine, and what each one can be
|
|
8
|
+
* pointed at.
|
|
9
|
+
*
|
|
10
|
+
* The picker used to offer whatever agents.toml declared, whether or not the
|
|
11
|
+
* command existed, and said nothing about models. Two things make that worse
|
|
12
|
+
* than it sounds. A declared agent that is not installed fails at launch,
|
|
13
|
+
* inside a terminal window on the canvas, where the error reads like the hub
|
|
14
|
+
* is broken. And a model has to be typed from memory, which is how you find
|
|
15
|
+
* out that the CLI spells it differently than you thought.
|
|
16
|
+
*
|
|
17
|
+
* This answers both, per machine - and per machine is the point, because a
|
|
18
|
+
* host has its own PATH and this hub's answer says nothing about it.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* How long any one probe gets before it is killed.
|
|
22
|
+
*
|
|
23
|
+
* Generous on purpose, and measured rather than guessed. `opencode models` was
|
|
24
|
+
* about 16 seconds on Windows through the .cmd shim npm installs when this was
|
|
25
|
+
* written; on opencode 1.18.29 it is nearer 38, which is how the old 30-second
|
|
26
|
+
* bound came to report a CLI with no models at all rather than its 367.
|
|
27
|
+
*
|
|
28
|
+
* That is the argument for erring long, not for chasing the number: a tighter
|
|
29
|
+
* bound does not make anything faster, it just turns a working CLI into one
|
|
30
|
+
* that reports no version and no models. Nothing waits on detection - it
|
|
31
|
+
* starts after the hub is already serving and the browser is told when each
|
|
32
|
+
* answer lands - so the only thing a long timeout costs is a late answer from
|
|
33
|
+
* a CLI that was never going to reply.
|
|
34
|
+
*/
|
|
35
|
+
const PROBE_TIMEOUT_MS = 90_000;
|
|
36
|
+
/** Nobody scrolls a dropdown past this, and 395 lines is a real answer. */
|
|
37
|
+
const MAX_MODELS = 500;
|
|
38
|
+
export class AgentDetector extends EventEmitter {
|
|
39
|
+
profiles;
|
|
40
|
+
known = new Map();
|
|
41
|
+
cache = new Map();
|
|
42
|
+
running = null;
|
|
43
|
+
constructor(profiles) {
|
|
44
|
+
super();
|
|
45
|
+
this.profiles = profiles;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* What is known right now, without probing anything.
|
|
49
|
+
*
|
|
50
|
+
* Never async, and never empty: a profile that has not been probed yet is
|
|
51
|
+
* reported with `available: null`, which the panel draws as "checking".
|
|
52
|
+
* Detection must not be able to hold up a hub starting or a page loading.
|
|
53
|
+
*/
|
|
54
|
+
snapshot() {
|
|
55
|
+
return this.profiles.list().map((p) => this.known.get(p.id) ?? unprobed(p));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Probe every profile. Safe to call from anywhere, including on a timer or
|
|
59
|
+
* from the browser; concurrent calls share the one run in flight rather
|
|
60
|
+
* than spawning every CLI twice.
|
|
61
|
+
*/
|
|
62
|
+
refresh() {
|
|
63
|
+
if (this.running)
|
|
64
|
+
return this.running;
|
|
65
|
+
this.running = this.run().finally(() => {
|
|
66
|
+
this.running = null;
|
|
67
|
+
});
|
|
68
|
+
return this.running;
|
|
69
|
+
}
|
|
70
|
+
async run() {
|
|
71
|
+
// In parallel: each of these is a process spawn that mostly sits waiting,
|
|
72
|
+
// and doing them in series makes the first load as slow as their sum.
|
|
73
|
+
const found = await Promise.all(this.profiles.list().map((p) => this.probe(p)));
|
|
74
|
+
const next = new Map(found.map((info) => [info.id, info]));
|
|
75
|
+
const changed = !same(this.known, next);
|
|
76
|
+
this.known = next;
|
|
77
|
+
if (changed)
|
|
78
|
+
this.emit('changed', found);
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
async probe(profile) {
|
|
82
|
+
const base = unprobed(profile);
|
|
83
|
+
const path = which(profile.command);
|
|
84
|
+
if (!path) {
|
|
85
|
+
// Stays in the list rather than vanishing: a declared agent that
|
|
86
|
+
// silently disappeared looks like the config was ignored, and the
|
|
87
|
+
// command that was not found is the thing worth showing.
|
|
88
|
+
return {
|
|
89
|
+
...base,
|
|
90
|
+
available: false,
|
|
91
|
+
detail: `not found on PATH: ${profile.command}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const version = profile.versionArgs
|
|
95
|
+
? await run(path, profile.versionArgs, profile.probeEnv).catch(() => null)
|
|
96
|
+
: null;
|
|
97
|
+
const key = `${path}|${version ?? ''}`;
|
|
98
|
+
const cached = this.cache.get(profile.id);
|
|
99
|
+
if (cached?.key === key) {
|
|
100
|
+
return {
|
|
101
|
+
...base,
|
|
102
|
+
available: true,
|
|
103
|
+
commandPath: path,
|
|
104
|
+
version,
|
|
105
|
+
models: cached.models,
|
|
106
|
+
modelSource: cached.source,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const listed = profile.modelsArgs
|
|
110
|
+
? await run(path, profile.modelsArgs, profile.probeEnv).catch(() => null)
|
|
111
|
+
: null;
|
|
112
|
+
let models = [];
|
|
113
|
+
let source = 'none';
|
|
114
|
+
if (listed !== null) {
|
|
115
|
+
models = listed
|
|
116
|
+
.split(/\r?\n/)
|
|
117
|
+
.map((l) => l.trim())
|
|
118
|
+
.filter((l) => l.length > 0)
|
|
119
|
+
.slice(0, MAX_MODELS);
|
|
120
|
+
source = 'listed';
|
|
121
|
+
}
|
|
122
|
+
if (models.length === 0 && profile.models?.length) {
|
|
123
|
+
// Either the CLI has no listing command, or its command failed. The
|
|
124
|
+
// declared list is the answer in both cases.
|
|
125
|
+
models = profile.models;
|
|
126
|
+
source = 'declared';
|
|
127
|
+
}
|
|
128
|
+
this.cache.set(profile.id, { key, models, source });
|
|
129
|
+
return {
|
|
130
|
+
...base,
|
|
131
|
+
available: true,
|
|
132
|
+
commandPath: path,
|
|
133
|
+
version,
|
|
134
|
+
models,
|
|
135
|
+
modelSource: source,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A profile before anything has been asked of it.
|
|
141
|
+
*
|
|
142
|
+
* `shell` is the exception that is born answered: it is whatever COMSPEC or
|
|
143
|
+
* SHELL points at, it is always there, and there is nothing to probe.
|
|
144
|
+
*/
|
|
145
|
+
function unprobed(p) {
|
|
146
|
+
const detectable = p.versionArgs !== undefined || p.modelsArgs !== undefined;
|
|
147
|
+
return {
|
|
148
|
+
id: p.id,
|
|
149
|
+
description: p.description,
|
|
150
|
+
mcp: p.mcp,
|
|
151
|
+
resumable: !!p.resumeArgs,
|
|
152
|
+
command: p.command,
|
|
153
|
+
commandPath: null,
|
|
154
|
+
available: detectable ? null : true,
|
|
155
|
+
version: null,
|
|
156
|
+
detail: null,
|
|
157
|
+
models: p.models ?? [],
|
|
158
|
+
modelSource: p.models?.length ? 'declared' : 'none',
|
|
159
|
+
takesModel: p.modelArgs !== undefined,
|
|
160
|
+
takesEffort: p.effortArgs !== undefined,
|
|
161
|
+
efforts: p.efforts ?? [],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Run one probe and return its stdout.
|
|
166
|
+
*
|
|
167
|
+
* Timed and killed rather than awaited indefinitely: the whole promise of
|
|
168
|
+
* this file is that a CLI which hangs on `--version` costs a slow dropdown
|
|
169
|
+
* and not a hub that will not boot. Windows shims (.cmd/.bat) are not
|
|
170
|
+
* executable images, so they go through cmd.exe exactly as spawning an agent
|
|
171
|
+
* does - `shell: true` would be the short version and would also hand the
|
|
172
|
+
* command line to a shell that reinterprets it.
|
|
173
|
+
*/
|
|
174
|
+
function run(path, args, env) {
|
|
175
|
+
const ext = extname(path).toLowerCase();
|
|
176
|
+
const viaCmd = platform === 'win32' && (ext === '.cmd' || ext === '.bat');
|
|
177
|
+
const file = viaCmd ? (process.env.COMSPEC ?? 'cmd.exe') : path;
|
|
178
|
+
const argv = viaCmd ? ['/c', path, ...args] : args;
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
execFile(file, argv, {
|
|
181
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
182
|
+
windowsHide: true,
|
|
183
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
184
|
+
// Inherited and then overlaid: a probe still needs PATH and HOME to
|
|
185
|
+
// find and run anything at all.
|
|
186
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
187
|
+
}, (err, stdout) => {
|
|
188
|
+
// Some CLIs print their version and exit non-zero, or write it to
|
|
189
|
+
// stderr and nothing to stdout. Stdout with content wins over the
|
|
190
|
+
// exit code; nothing on stdout is a failure whatever the code said.
|
|
191
|
+
const out = stdout.trim();
|
|
192
|
+
if (out)
|
|
193
|
+
resolve(out);
|
|
194
|
+
else
|
|
195
|
+
reject(err ?? new Error('no output'));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/** Whether two detection results are the same, so nothing is broadcast. */
|
|
200
|
+
function same(a, b) {
|
|
201
|
+
if (a.size !== b.size)
|
|
202
|
+
return false;
|
|
203
|
+
for (const [id, x] of a) {
|
|
204
|
+
const y = b.get(id);
|
|
205
|
+
if (!y || JSON.stringify(x) !== JSON.stringify(y))
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=detect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect.js","sourceRoot":"","sources":["../../src/agents/detect.ts"],"names":[],"mappings":"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"}
|