@kdonev/termscape 0.1.0 → 0.1.1
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 +176 -13
- 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 +110 -0
- package/dist/agents/profiles.d.ts.map +1 -1
- package/dist/agents/profiles.js +215 -0
- package/dist/agents/profiles.js.map +1 -1
- package/dist/agents/templates.d.ts +85 -0
- package/dist/agents/templates.d.ts.map +1 -0
- package/dist/agents/templates.js +133 -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 +179 -10
- package/dist/agents/wiring.js.map +1 -1
- package/dist/cli.js +41 -14
- package/dist/cli.js.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +55 -0
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/store.d.ts +41 -0
- package/dist/db/store.d.ts.map +1 -1
- package/dist/db/store.js +75 -2
- package/dist/db/store.js.map +1 -1
- package/dist/hub.d.ts +227 -6
- package/dist/hub.d.ts.map +1 -1
- package/dist/hub.js +504 -33
- package/dist/hub.js.map +1 -1
- package/dist/hub.tgz +0 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +19 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/protocol/domain.d.ts +87 -0
- package/dist/protocol/domain.d.ts.map +1 -1
- package/dist/protocol/domain.js +104 -0
- package/dist/protocol/domain.js.map +1 -1
- package/dist/protocol/mcp-tools.d.ts +30 -0
- package/dist/protocol/mcp-tools.d.ts.map +1 -1
- package/dist/protocol/mcp-tools.js +50 -0
- package/dist/protocol/mcp-tools.js.map +1 -1
- package/dist/protocol/peer.d.ts +36 -0
- package/dist/protocol/peer.d.ts.map +1 -1
- package/dist/protocol/peer.js +35 -1
- package/dist/protocol/peer.js.map +1 -1
- package/dist/protocol/ws.d.ts +254 -0
- package/dist/protocol/ws.d.ts.map +1 -1
- package/dist/protocol/ws.js +160 -5
- package/dist/protocol/ws.js.map +1 -1
- package/dist/remote/lan.d.ts +45 -0
- package/dist/remote/lan.d.ts.map +1 -1
- package/dist/remote/lan.js +44 -0
- package/dist/remote/lan.js.map +1 -1
- package/dist/remote/peer-serve.d.ts.map +1 -1
- package/dist/remote/peer-serve.js +31 -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 +69 -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 +237 -117
- package/dist/server.js.map +1 -1
- package/dist/session/manager.d.ts +44 -4
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +161 -34
- package/dist/session/manager.js.map +1 -1
- package/package.json +1 -1
- package/web/assets/{index-BOqZOoep.js → index-Bq_Y0JcC.js} +6 -6
- package/web/assets/{index-B2JtCtHa.css → index-DQYDkOwJ.css} +1 -1
- package/web/index.html +2 -2
package/README.md
CHANGED
|
@@ -8,9 +8,10 @@ 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; `--listen loopback` keeps it to this machine. Other machines run
|
|
14
|
+
the same hub as a daemon and appear on the same canvas.
|
|
14
15
|
|
|
15
16
|
## Getting started
|
|
16
17
|
|
|
@@ -23,9 +24,21 @@ if you would rather it did not; the URL is printed either way. Nothing is
|
|
|
23
24
|
installed system-wide: state lives in `~/.termscape`, and deleting that
|
|
24
25
|
directory is the uninstall.
|
|
25
26
|
|
|
27
|
+
The hub is reachable from your network, so the same URL — token and all — opens
|
|
28
|
+
the canvas on a phone or a second screen. Attaching another machine is a
|
|
29
|
+
separate opt-in; see [Adding another machine](#adding-another-machine). To keep
|
|
30
|
+
the hub to this machine entirely:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @kdonev/termscape --listen loopback
|
|
34
|
+
```
|
|
35
|
+
|
|
26
36
|
Slide out the **machines** panel: every machine, the workspaces on it, and
|
|
27
37
|
the agents in each. Point a workspace at a folder there, and start an agent
|
|
28
|
-
in it.
|
|
38
|
+
in it. Adding, editing and removing all open a dialog over the canvas, so the
|
|
39
|
+
node you acted on stays where it was and a refusal — a folder that is not
|
|
40
|
+
there, a rename the addresses will not allow — arrives in the dialog next to
|
|
41
|
+
the field, with what you typed still in it.
|
|
29
42
|
|
|
30
43
|
- **Scroll** to pan, **Ctrl/⌘ + scroll** to zoom, **Ctrl/⌘ + 1** to fit,
|
|
31
44
|
**Ctrl/⌘ + 2** to zoom to one terminal
|
|
@@ -35,6 +48,8 @@ in it.
|
|
|
35
48
|
ordinary pace is left alone
|
|
36
49
|
- Below 60% zoom terminals become preview cards — zoom in to interact
|
|
37
50
|
- Clicking an agent in the panel brings the canvas to it
|
|
51
|
+
- Clicking the canvas closes the panel, as does **Escape** — which closes the
|
|
52
|
+
panel first and clears the selection only once it is shut
|
|
38
53
|
|
|
39
54
|
## Requirements
|
|
40
55
|
|
|
@@ -66,8 +81,11 @@ next window.
|
|
|
66
81
|
|
|
67
82
|
## Adding another machine
|
|
68
83
|
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
The hub is already reachable from your network, but it does not hand out join
|
|
85
|
+
links until asked. `/join` is the one page served without your token — it has
|
|
86
|
+
to be typed by hand on a machine that has nothing yet — so being reachable and
|
|
87
|
+
being enrollable are kept as two separate permissions. Turn the second one on,
|
|
88
|
+
then let the other machine come to you:
|
|
71
89
|
|
|
72
90
|
```bash
|
|
73
91
|
npx @kdonev/termscape --listen lan
|
|
@@ -134,9 +152,10 @@ have actually changed, and checks that tree really loads before trusting it.
|
|
|
134
152
|
|
|
135
153
|
Two things worth knowing:
|
|
136
154
|
|
|
137
|
-
-
|
|
138
|
-
|
|
139
|
-
your canvas. Every other route still requires the token
|
|
155
|
+
- The **join page** is opt-in and off by default; `--listen lan` is what turns
|
|
156
|
+
it on. With it, anyone who can reach your hub can load that page and attach a
|
|
157
|
+
machine to your canvas. Every other route still requires the token, and being
|
|
158
|
+
reachable — which a default install already is — grants none of this.
|
|
140
159
|
- Each download carries a single-use key that expires in 15 minutes. Once a
|
|
141
160
|
machine has joined it keeps a durable token in `~/.termscape/host-token` and
|
|
142
161
|
rejoins by itself after a reboot or a dropped link — its agents keep running
|
|
@@ -158,15 +177,61 @@ tunnel. Same protocol, opposite direction.
|
|
|
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
|
+
`propose_template` is the only one that asks rather than does. A template
|
|
183
|
+
changes how *future* agents are launched, on every machine, with nobody
|
|
184
|
+
necessarily watching — so the canvas shows it to a human, who can edit it
|
|
185
|
+
before accepting or decline it outright. The tool returns as soon as they have
|
|
186
|
+
been shown it rather than blocking until they answer, and the decision is typed
|
|
187
|
+
back into the agent's terminal. Nothing is stored unless someone says yes.
|
|
161
188
|
|
|
162
189
|
Every agent is also given a brief explaining its address, its peers, and that
|
|
163
190
|
text arriving as `[from <address>] ...` is a colleague rather than the human.
|
|
164
191
|
|
|
165
192
|
## Agent profiles
|
|
166
193
|
|
|
167
|
-
An agent CLI is configuration, not code. Built-ins are `claude
|
|
168
|
-
|
|
169
|
-
|
|
194
|
+
An agent CLI is configuration, not code. Built-ins are `claude`, `codex`,
|
|
195
|
+
`gemini`, `opencode` and `shell`.
|
|
196
|
+
|
|
197
|
+
Every one of them except `shell` is wired to the hub's MCP endpoint: each gets an
|
|
198
|
+
address, a brief and the `send_message` tool set. They arrive there by four
|
|
199
|
+
different routes, because no two of these CLIs configure an MCP server the same
|
|
200
|
+
way — and **none of them writes to a file you own**, so there is nothing left
|
|
201
|
+
behind when a session ends or when the hub is killed rather than stopped.
|
|
202
|
+
|
|
203
|
+
| agent | how it reaches the hub | brief | resume |
|
|
204
|
+
| --- | --- | --- | --- |
|
|
205
|
+
| `claude` | `--mcp-config` on a generated file | `--append-system-prompt-file` | `--resume <uuid>` |
|
|
206
|
+
| `codex` | `-c mcp_servers.…` overrides, one run only | typed in at startup | restarts clean |
|
|
207
|
+
| `gemini` | `GEMINI_CLI_SYSTEM_SETTINGS_PATH` at a generated file | typed in at startup | restarts clean |
|
|
208
|
+
| `opencode` | `OPENCODE_CONFIG_CONTENT`, no file anywhere | typed in at startup | restarts clean |
|
|
209
|
+
|
|
210
|
+
The last three are typed at rather than handed a brief because none of them can
|
|
211
|
+
*append* to its system prompt — Codex's `base_instructions` and Gemini's
|
|
212
|
+
`GEMINI_SYSTEM_MD` each replace the whole thing, which would cost the agent its
|
|
213
|
+
own tool instructions. None of the three is resumable either: Codex mints a
|
|
214
|
+
session id it will not accept from us, and Gemini accepts one but resumes by
|
|
215
|
+
list index instead. They restart clean, and are briefed again when they do.
|
|
216
|
+
|
|
217
|
+
Two details worth knowing, because both are easy to get wrong:
|
|
218
|
+
|
|
219
|
+
- **Codex's bearer token goes in the environment, never `-c`.** Config
|
|
220
|
+
overrides land in the command line, where any other user on the machine can
|
|
221
|
+
read them. `bearer_token_env_var` exists precisely for this.
|
|
222
|
+
- **opencode is configured entirely from the environment.** Its config is
|
|
223
|
+
handed over as a string, merged with your own rather than replacing it, so
|
|
224
|
+
your models, themes and your own MCP servers survive the session. Note that
|
|
225
|
+
`opencode mcp add` is *not* how this is done: that command writes to
|
|
226
|
+
`~/.config/opencode/opencode.json` and ignores `OPENCODE_CONFIG` while doing
|
|
227
|
+
it. Setting the variable also stops opencode writing its default config file
|
|
228
|
+
on start, so a session leaves nothing behind at all.
|
|
229
|
+
|
|
230
|
+
`shell` is the one profile that is not an agent, and it is not one in a way no
|
|
231
|
+
flag can fix: it runs a shell, so text typed at it is executed rather than read.
|
|
232
|
+
It gets no brief, and its profile says so rather than relying on a default.
|
|
233
|
+
|
|
234
|
+
Override or add profiles in `~/.termscape/agents.toml`:
|
|
170
235
|
|
|
171
236
|
```toml
|
|
172
237
|
[my-agent]
|
|
@@ -175,8 +240,100 @@ args = ["--mcp-config", "{{mcp_config_path}}"]
|
|
|
175
240
|
status = "heuristic" # or "hooks", for exact turn boundaries
|
|
176
241
|
ready_hint = "[$#>%] ?$" # prompt regex, for the idle indicator
|
|
177
242
|
inject = "bracketed" # bracketed paste, or "raw"
|
|
243
|
+
brief = "typed" # "flag" if it can append a system prompt,
|
|
244
|
+
# "none" for a terminal that would execute one
|
|
245
|
+
version_args = ["--version"] # how to ask its version, for the panel
|
|
246
|
+
models_args = ["models"] # optional: one model per line on stdout
|
|
247
|
+
models = ["opus", "sonnet"] # the answer when it has no listing command
|
|
248
|
+
model_args = ["--model", "{{model}}"] # how it spells a model, if it takes one
|
|
249
|
+
effort_args = ["--effort", "{{effort}}"] # and an effort
|
|
250
|
+
efforts = ["low", "high"] # the levels it documents
|
|
178
251
|
```
|
|
179
252
|
|
|
253
|
+
### Templates
|
|
254
|
+
|
|
255
|
+
The picker offers **templates**, not CLIs. A template is an agent plus a model,
|
|
256
|
+
an effort and an opening instruction — a saved answer to all four, picked once
|
|
257
|
+
instead of typed every time. Every agent gets a bare template under its own
|
|
258
|
+
name, so `claude` and `shell` are still there and nothing that worked stops
|
|
259
|
+
working.
|
|
260
|
+
|
|
261
|
+
Templates are a root of their own in the panel, next to the machines — they are
|
|
262
|
+
config rather than a place, and one template is used on every machine, so it
|
|
263
|
+
does not live under one. **+ template** makes one, `edit` changes it, `×`
|
|
264
|
+
removes it. The list sits below the machines and starts collapsed: machines are
|
|
265
|
+
what you work in every day, templates are what you set up once and then forget.
|
|
266
|
+
|
|
267
|
+
They can also be written by hand, and a hand-written one wins:
|
|
268
|
+
|
|
269
|
+
```toml
|
|
270
|
+
[template.reviewer]
|
|
271
|
+
agent = "claude"
|
|
272
|
+
model = "opus"
|
|
273
|
+
effort = "high"
|
|
274
|
+
prompt = "Review the diff on this branch for correctness bugs. Report, do not fix."
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
- Templates made in the panel are stored in `state.db`, the same place
|
|
278
|
+
workspaces and hosts already live. `agents.toml` is a second, read-only
|
|
279
|
+
source: the hub never writes it, so a formatter cannot eat the comments and
|
|
280
|
+
ordering of a file you edit by hand.
|
|
281
|
+
- When both declare the same name **the file wins**, and the dialog refuses the
|
|
282
|
+
name rather than storing a row that would never appear. Someone who wrote a
|
|
283
|
+
template by hand meant it.
|
|
284
|
+
- An agent's own bare template is derived, not stored. Editing one makes a
|
|
285
|
+
stored template that shadows it; removing that reveals the bare one again
|
|
286
|
+
rather than leaving a gap.
|
|
287
|
+
- Removing a template takes nothing from the agents it already started. They
|
|
288
|
+
keep their model, their effort and their ability to resume, because a session
|
|
289
|
+
records what its template resolved to rather than looking it up again.
|
|
290
|
+
|
|
291
|
+
Three words, kept apart deliberately: an **agent** is the CLI program, a
|
|
292
|
+
**template** is what you pick from the list, and a **session** is one running
|
|
293
|
+
instance with an address and a window.
|
|
294
|
+
|
|
295
|
+
- **A template holds values, not arguments.** Claude Code takes `--model` and
|
|
296
|
+
`--effort`; opencode takes `-m provider/model` and has no effort setting on
|
|
297
|
+
its TUI at all; Codex takes `-m` but spells effort as a config override,
|
|
298
|
+
`-c model_reasoning_effort=…`, because it has no `--effort` flag. So the
|
|
299
|
+
template says *which* model, and the agent declares how to spell it. An agent that declares nothing takes nothing, and a template
|
|
300
|
+
asking for a model or an effort it cannot spell is a configuration error
|
|
301
|
+
reported when the file loads — visible in the dialog, not a flag silently
|
|
302
|
+
dropped at launch.
|
|
303
|
+
- **The first instruction is typed in once the CLI is up**, not passed as an
|
|
304
|
+
argument, and it does not repeat when a session is resumed. It is how the
|
|
305
|
+
session started, not what it is.
|
|
306
|
+
- **A resumed session comes back on the model it left with.** What the template
|
|
307
|
+
resolved to is recorded on the session, because resume rebuilds the command
|
|
308
|
+
line rather than replaying it — and because a template can be edited
|
|
309
|
+
afterwards.
|
|
310
|
+
- **Starting an agent on another machine sends values, not a template name.**
|
|
311
|
+
The two machines do not share config, so the name is resolved here first.
|
|
312
|
+
|
|
313
|
+
### What is actually installed
|
|
314
|
+
|
|
315
|
+
Each machine probes its own `PATH` and reports back, so the panel shows, per
|
|
316
|
+
machine, which agents are there, what version each is, and the models it
|
|
317
|
+
offers. That is per machine on purpose: a host has its own `PATH`, and starting
|
|
318
|
+
an agent it does not have used to fail at launch inside a terminal window,
|
|
319
|
+
where the error reads like the hub is broken.
|
|
320
|
+
|
|
321
|
+
- A declared agent that is **not** installed stays in the list, greyed out,
|
|
322
|
+
naming the command that was not found — rather than vanishing, which looks
|
|
323
|
+
like the config was ignored.
|
|
324
|
+
- Models are enumerated where the CLI can be asked (`opencode models` returns
|
|
325
|
+
a few hundred) and declared in the profile where it cannot. Claude Code has
|
|
326
|
+
no listing command; its `--help` documents the aliases instead, and it takes
|
|
327
|
+
a full model name as readily as an alias. Codex is declared too, for a
|
|
328
|
+
different reason: `codex debug models` does render the real catalog, but it
|
|
329
|
+
is a debug command answering with half a megabyte of JSON rather than the
|
|
330
|
+
one-per-line stdout the profile reads, so the profile carries the slugs that
|
|
331
|
+
catalog marks visible. In every case a full model name outside the list is
|
|
332
|
+
still accepted — the list is what the dropdown suggests, never a limit.
|
|
333
|
+
- Probing runs after the hub is already serving and never blocks it. The first
|
|
334
|
+
page load usually shows *checking…*, and fills in a moment later. **check
|
|
335
|
+
again** in the start-an-agent dialog re-probes every machine.
|
|
336
|
+
|
|
180
337
|
## State and restart
|
|
181
338
|
|
|
182
339
|
SQLite at `~/.termscape/state.db` holds everything needed to redraw the canvas
|
|
@@ -213,7 +370,13 @@ Agents can type into each other's terminals and spawn more agents. That is the
|
|
|
213
370
|
feature, and it is the risk surface: an agent that reads a hostile repository
|
|
214
371
|
could be talked into sending an attacker's text to a peer.
|
|
215
372
|
|
|
216
|
-
- The hub binds
|
|
373
|
+
- The hub binds every interface, so the canvas is reachable from your network —
|
|
374
|
+
but only with its token, which is minted per run and never printed anywhere
|
|
375
|
+
the network can read. `--listen loopback` narrows it to this machine.
|
|
376
|
+
- A hub running `--headless` — one that joined a canvas, or was deployed over
|
|
377
|
+
ssh and is reached through its tunnel — stays on `127.0.0.1` regardless.
|
|
378
|
+
- `/join` is the only route served without the token, and it is off unless you
|
|
379
|
+
passed `--listen lan`.
|
|
217
380
|
- Every agent gets its own bearer token. The sender of a message is taken from
|
|
218
381
|
that token, never from the arguments, so attribution cannot be forged.
|
|
219
382
|
- Messages are length-capped, rate-limited per sender, and always arrive with a
|
|
@@ -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":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAGrC;;;;;;;;;;;;;GAaG;AAEH;;;;;;;;;;;;;;GAcG;AACH,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC,2EAA2E;AAC3E,MAAM,UAAU,GAAG,GAAG,CAAC;AAevB,MAAM,OAAO,aAAc,SAAQ,YAAY;IAKhB;IAJrB,KAAK,GAAG,IAAI,GAAG,EAA4B,CAAC;IACnC,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC/C,OAAO,GAAuC,IAAI,CAAC;IAE3D,YAA6B,QAAyB;QACpD,KAAK,EAAE,CAAC;QADmB,aAAQ,GAAR,QAAQ,CAAiB;IAEtD,CAAC;IAED;;;;;;OAMG;IACH,QAAQ;QACN,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9E,CAAC;IAED;;;;OAIG;IACH,OAAO;QACL,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC;QACtC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YACrC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,CAAC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAEO,KAAK,CAAC,GAAG;QACf,0EAA0E;QAC1E,sEAAsE;QACtE,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,OAAO;YAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACzC,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,OAAqB;QACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC/B,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,iEAAiE;YACjE,kEAAkE;YAClE,yDAAyD;YACzD,OAAO;gBACL,GAAG,IAAI;gBACP,SAAS,EAAE,KAAK;gBAChB,MAAM,EAAE,sBAAsB,OAAO,CAAC,OAAO,EAAE;aAChD,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,OAAO,CAAC,WAAW;YACjC,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC;YAC1E,CAAC,CAAC,IAAI,CAAC;QAET,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,OAAO,IAAI,EAAE,EAAE,CAAC;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC1C,IAAI,MAAM,EAAE,GAAG,KAAK,GAAG,EAAE,CAAC;YACxB,OAAO;gBACL,GAAG,IAAI;gBACP,SAAS,EAAE,IAAI;gBACf,WAAW,EAAE,IAAI;gBACjB,OAAO;gBACP,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,WAAW,EAAE,MAAM,CAAC,MAAM;aAC3B,CAAC;QACJ,CAAC;QAED,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU;YAC/B,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC;YACzE,CAAC,CAAC,IAAI,CAAC;QAET,IAAI,MAAM,GAAa,EAAE,CAAC;QAC1B,IAAI,MAAM,GAAoC,MAAM,CAAC;QACrD,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,MAAM,GAAG,MAAM;iBACZ,KAAK,CAAC,OAAO,CAAC;iBACd,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;iBAC3B,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;YACxB,MAAM,GAAG,QAAQ,CAAC;QACpB,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;YAClD,oEAAoE;YACpE,6CAA6C;YAC7C,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YACxB,MAAM,GAAG,UAAU,CAAC;QACtB,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QACpD,OAAO;YACL,GAAG,IAAI;YACP,SAAS,EAAE,IAAI;YACf,WAAW,EAAE,IAAI;YACjB,OAAO;YACP,MAAM;YACN,WAAW,EAAE,MAAM;SACpB,CAAC;IACJ,CAAC;CACF;AAED;;;;;GAKG;AACH,SAAS,QAAQ,CAAC,CAAe;IAC/B,MAAM,UAAU,GAAG,CAAC,CAAC,WAAW,KAAK,SAAS,IAAI,CAAC,CAAC,UAAU,KAAK,SAAS,CAAC;IAC7E,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU;QACzB,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,WAAW,EAAE,IAAI;QACjB,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;QACnC,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE;QACtB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM;QACnD,UAAU,EAAE,CAAC,CAAC,SAAS,KAAK,SAAS;QACrC,WAAW,EAAE,CAAC,CAAC,UAAU,KAAK,SAAS;QACvC,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,EAAE;KACzB,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,GAAG,CAAC,IAAY,EAAE,IAAc,EAAE,GAA4B;IACrE,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;IACxC,MAAM,MAAM,GAAG,QAAQ,KAAK,OAAO,IAAI,CAAC,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,MAAM,CAAC,CAAC;IAC1E,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAChE,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEnD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CACN,IAAI,EACJ,IAAI,EACJ;YACE,OAAO,EAAE,gBAAgB;YACzB,WAAW,EAAE,IAAI;YACjB,SAAS,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;YAC1B,oEAAoE;YACpE,gCAAgC;YAChC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG;SACpD,EACD,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YACd,kEAAkE;YAClE,kEAAkE;YAClE,oEAAoE;YACpE,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;YAC1B,IAAI,GAAG;gBAAE,OAAO,CAAC,GAAG,CAAC,CAAC;;gBACjB,MAAM,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;QAC7C,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,2EAA2E;AAC3E,SAAS,IAAI,CACX,CAAgC,EAChC,CAAgC;IAEhC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACpC,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACpB,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IAClE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -16,19 +16,129 @@ export interface AgentProfile {
|
|
|
16
16
|
/** Regex matched against the last non-empty line when status = heuristic. */
|
|
17
17
|
readyHint?: string;
|
|
18
18
|
inject: InjectMode;
|
|
19
|
+
/**
|
|
20
|
+
* How the brief reaches the agent.
|
|
21
|
+
*
|
|
22
|
+
* `flag` - the profile's own args point at `{{brief_path}}`, the way Claude
|
|
23
|
+
* Code's `--append-system-prompt-file` does. The brief is part of the
|
|
24
|
+
* system prompt and the agent never sees it as a turn.
|
|
25
|
+
*
|
|
26
|
+
* `typed` - the CLI has no way to *append* to its system prompt, so the
|
|
27
|
+
* brief is typed into the terminal once the CLI is up, ahead of any opening
|
|
28
|
+
* instruction. Both of the other wired CLIs are in this position, and for
|
|
29
|
+
* the same reason: Codex's `base_instructions` and Gemini's
|
|
30
|
+
* `GEMINI_SYSTEM_MD` each *replace* the whole system prompt rather than add
|
|
31
|
+
* to it, so using either would cost the agent its own tool instructions -
|
|
32
|
+
* a far worse trade than a first turn that arrives as text.
|
|
33
|
+
*
|
|
34
|
+
* `none` - nothing is sent. `shell` is the reason this value exists and it
|
|
35
|
+
* is not a value any agent should want: a shell *executes* what is typed at
|
|
36
|
+
* it, so a brief there is not context, it is a series of commands that fail
|
|
37
|
+
* loudly.
|
|
38
|
+
*
|
|
39
|
+
* Absent means `flag` for a wired agent - what every profile written before
|
|
40
|
+
* this did - and `none` for an unwired one, because `mcp: false` has always
|
|
41
|
+
* meant "a plain terminal" and a profile that declared one should not start
|
|
42
|
+
* having text typed into it. An unwired agent that *is* an agent says
|
|
43
|
+
* `brief = "typed"` and gets the shorter brief: no tool list, because it has
|
|
44
|
+
* no tools, but it is still told its address and still told that a
|
|
45
|
+
* `[from ...]` line is a colleague rather than the human.
|
|
46
|
+
*/
|
|
47
|
+
brief?: 'flag' | 'typed' | 'none';
|
|
19
48
|
/**
|
|
20
49
|
* Argument template used to bring a session back with its prior
|
|
21
50
|
* conversation. `{{uuid}}` is replaced with agent_session_uuid. When absent,
|
|
22
51
|
* the profile is not resumable and restarts clean.
|
|
23
52
|
*/
|
|
24
53
|
resumeArgs?: string[];
|
|
54
|
+
/**
|
|
55
|
+
* Environment for the *detection* probes only, not for a session.
|
|
56
|
+
*
|
|
57
|
+
* Detection runs these CLIs on a machine whose owner may never have run them
|
|
58
|
+
* by hand, so a probe that leaves something behind is the hub writing to
|
|
59
|
+
* somebody's home directory uninvited. `opencode models` does exactly that -
|
|
60
|
+
* it creates ~/.config/opencode/opencode.jsonc - and this is how it is told
|
|
61
|
+
* not to.
|
|
62
|
+
*/
|
|
63
|
+
probeEnv?: Record<string, string>;
|
|
64
|
+
/**
|
|
65
|
+
* How to ask this CLI its version. Its stdout is shown to the user and is
|
|
66
|
+
* also the cache key for the model list, so a CLI that updates underneath
|
|
67
|
+
* us re-lists rather than serving last week's answer.
|
|
68
|
+
*/
|
|
69
|
+
versionArgs?: string[];
|
|
70
|
+
/**
|
|
71
|
+
* How to ask this CLI what models it can be pointed at: one
|
|
72
|
+
* `provider/model` (or bare name) per line on stdout. Absent when the CLI
|
|
73
|
+
* has no such command, which is the common case.
|
|
74
|
+
*/
|
|
75
|
+
modelsArgs?: string[];
|
|
76
|
+
/**
|
|
77
|
+
* The answer for a CLI that cannot be asked. Claude Code is the example:
|
|
78
|
+
* it has no listing command, and its --help documents the aliases instead.
|
|
79
|
+
* Used only when `modelsArgs` is absent or its command fails.
|
|
80
|
+
*/
|
|
81
|
+
models?: string[];
|
|
82
|
+
/**
|
|
83
|
+
* How this agent spells a model on the command line. `{{model}}` carries
|
|
84
|
+
* the value a template chose.
|
|
85
|
+
*
|
|
86
|
+
* A template holds *values*, not argv, because no two of these agents agree:
|
|
87
|
+
* Claude Code takes `--model`, opencode takes `-m provider/model`. So the
|
|
88
|
+
* template says which model and the agent says how to write it down.
|
|
89
|
+
*
|
|
90
|
+
* Absent means the agent takes no model here, and a template that names one
|
|
91
|
+
* for it is a configuration error reported at load rather than a flag
|
|
92
|
+
* silently dropped at launch. The whole fragment disappears when a template
|
|
93
|
+
* leaves the value unset - it does not expand to an empty string.
|
|
94
|
+
*/
|
|
95
|
+
modelArgs?: string[];
|
|
96
|
+
/** The same for effort, with `{{effort}}`. */
|
|
97
|
+
effortArgs?: string[];
|
|
98
|
+
/** The effort levels this agent documents, for the dialog to offer. */
|
|
99
|
+
efforts?: string[];
|
|
25
100
|
}
|
|
26
101
|
/**
|
|
27
102
|
* Built-in profiles. All Claude Code flags here are verified against the CLI:
|
|
28
103
|
* --mcp-config, --strict-mcp-config, --session-id, --settings,
|
|
29
104
|
* --append-system-prompt-file, --resume.
|
|
105
|
+
*
|
|
106
|
+
* Codex and Gemini are wired too, and every flag and key below was verified
|
|
107
|
+
* against the CLI actually installed - codex-cli 0.153.4 and gemini 0.58.0 -
|
|
108
|
+
* rather than written from memory. Both reach the hub the same way Claude
|
|
109
|
+
* Code does, over one streamable-HTTP MCP server carrying a bearer token, and
|
|
110
|
+
* neither needs a byte written to config the user owns:
|
|
111
|
+
*
|
|
112
|
+
* - **Codex** takes `-c <dotted.key>=<toml>` on any invocation, so the server
|
|
113
|
+
* is declared in argv for the one run. The token is *not* in argv - it goes
|
|
114
|
+
* in the environment, and `bearer_token_env_var` names the variable to read
|
|
115
|
+
* it from, which keeps it out of every process listing on the machine.
|
|
116
|
+
* - **Gemini** has no per-run config flag, but
|
|
117
|
+
* `GEMINI_CLI_SYSTEM_SETTINGS_PATH` repoints its system settings layer at a
|
|
118
|
+
* file of our choosing, which the hub generates per session.
|
|
119
|
+
*
|
|
120
|
+
* - **opencode** turned out to be the least invasive of the three and was
|
|
121
|
+
* assumed to be the hardest. It reads its entire config from
|
|
122
|
+
* `OPENCODE_CONFIG_CONTENT`, so nothing is written anywhere at all, and what
|
|
123
|
+
* it is given is merged with the user's own config rather than replacing it.
|
|
124
|
+
* The assumption that it had no per-run route came from reading
|
|
125
|
+
* `opencode mcp add` - which does write to the user's file, and ignores
|
|
126
|
+
* `OPENCODE_CONFIG` when it does - and mistaking that command for the only
|
|
127
|
+
* way in.
|
|
128
|
+
*
|
|
129
|
+
* `shell` is the only profile left that is not an agent, and it is not one in
|
|
130
|
+
* a way no flag can fix: it runs a shell, so text typed at it is executed
|
|
131
|
+
* rather than read.
|
|
30
132
|
*/
|
|
31
133
|
export declare const BUILTIN_PROFILES: Record<string, AgentProfile>;
|
|
134
|
+
/**
|
|
135
|
+
* How this profile's brief travels, with the defaults applied.
|
|
136
|
+
*
|
|
137
|
+
* One place, because three callers need the same answer and the interesting
|
|
138
|
+
* part is the default rather than the field: absent means `flag` for a wired
|
|
139
|
+
* agent and `none` for an unwired one.
|
|
140
|
+
*/
|
|
141
|
+
export declare function briefMode(p: AgentProfile): 'flag' | 'typed' | 'none';
|
|
32
142
|
export declare class ProfileRegistry {
|
|
33
143
|
private profiles;
|
|
34
144
|
constructor(profiles?: Record<string, AgentProfile>);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../src/agents/profiles.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGtD;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,0EAA0E;IAC1E,GAAG,EAAE,OAAO,CAAC;IACb,mCAAmC;IACnC,MAAM,EAAE,OAAO,GAAG,WAAW,CAAC;IAC9B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../src/agents/profiles.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGtD;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,0EAA0E;IAC1E,GAAG,EAAE,OAAO,CAAC;IACb,mCAAmC;IACnC,MAAM,EAAE,OAAO,GAAG,WAAW,CAAC;IAC9B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,CAAC;IACnB;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IAClC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAItB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAIlB;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAoNzD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAEpE;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAA+B;gBAEnC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC;IAInD,oEAAoE;IACpE,MAAM,CAAC,IAAI,IAAI,eAAe;IAyC9B,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAIpC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY;IAMjC,IAAI,IAAI,YAAY,EAAE;IAItB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;CAIjC;AAED,+EAA+E;AAC/E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAI5E;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,CAElF"}
|