@kdonev/termscape 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 +288 -0
- package/dist/agents/env.d.ts +36 -0
- package/dist/agents/env.d.ts.map +1 -0
- package/dist/agents/env.js +61 -0
- package/dist/agents/env.js.map +1 -0
- package/dist/agents/profiles.d.ts +45 -0
- package/dist/agents/profiles.d.ts.map +1 -0
- package/dist/agents/profiles.js +123 -0
- package/dist/agents/profiles.js.map +1 -0
- package/dist/agents/resolve.d.ts +16 -0
- package/dist/agents/resolve.d.ts.map +1 -0
- package/dist/agents/resolve.js +71 -0
- package/dist/agents/resolve.js.map +1 -0
- package/dist/agents/router.d.ts +29 -0
- package/dist/agents/router.d.ts.map +1 -0
- package/dist/agents/router.js +83 -0
- package/dist/agents/router.js.map +1 -0
- package/dist/agents/tokens.d.ts +21 -0
- package/dist/agents/tokens.d.ts.map +1 -0
- package/dist/agents/tokens.js +39 -0
- package/dist/agents/tokens.js.map +1 -0
- package/dist/agents/wiring.d.ts +30 -0
- package/dist/agents/wiring.d.ts.map +1 -0
- package/dist/agents/wiring.js +110 -0
- package/dist/agents/wiring.js.map +1 -0
- package/dist/browser.d.ts +13 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +34 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +200 -0
- package/dist/cli.js.map +1 -0
- package/dist/db/index.d.ts +14 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +24 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrations.d.ts +15 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +176 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/store.d.ts +71 -0
- package/dist/db/store.d.ts.map +1 -0
- package/dist/db/store.js +341 -0
- package/dist/db/store.js.map +1 -0
- package/dist/hub.d.ts +188 -0
- package/dist/hub.d.ts.map +1 -0
- package/dist/hub.js +636 -0
- package/dist/hub.js.map +1 -0
- package/dist/hub.tgz +0 -0
- package/dist/mcp/server.d.ts +31 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +66 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/paths.d.ts +27 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +31 -0
- package/dist/paths.js.map +1 -0
- package/dist/protocol/address.d.ts +15 -0
- package/dist/protocol/address.d.ts.map +1 -0
- package/dist/protocol/address.js +39 -0
- package/dist/protocol/address.js.map +1 -0
- package/dist/protocol/domain.d.ts +151 -0
- package/dist/protocol/domain.d.ts.map +1 -0
- package/dist/protocol/domain.js +104 -0
- package/dist/protocol/domain.js.map +1 -0
- package/dist/protocol/index.d.ts +7 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +7 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/inject.d.ts +29 -0
- package/dist/protocol/inject.d.ts.map +1 -0
- package/dist/protocol/inject.js +40 -0
- package/dist/protocol/inject.js.map +1 -0
- package/dist/protocol/mcp-tools.d.ts +45 -0
- package/dist/protocol/mcp-tools.d.ts.map +1 -0
- package/dist/protocol/mcp-tools.js +46 -0
- package/dist/protocol/mcp-tools.js.map +1 -0
- package/dist/protocol/peer.d.ts +304 -0
- package/dist/protocol/peer.d.ts.map +1 -0
- package/dist/protocol/peer.js +183 -0
- package/dist/protocol/peer.js.map +1 -0
- package/dist/protocol/ws.d.ts +416 -0
- package/dist/protocol/ws.d.ts.map +1 -0
- package/dist/protocol/ws.js +119 -0
- package/dist/protocol/ws.js.map +1 -0
- package/dist/remote/deployer.d.ts +78 -0
- package/dist/remote/deployer.d.ts.map +1 -0
- package/dist/remote/deployer.js +216 -0
- package/dist/remote/deployer.js.map +1 -0
- package/dist/remote/enroll.d.ts +14 -0
- package/dist/remote/enroll.d.ts.map +1 -0
- package/dist/remote/enroll.js +244 -0
- package/dist/remote/enroll.js.map +1 -0
- package/dist/remote/join-script.d.ts +29 -0
- package/dist/remote/join-script.d.ts.map +1 -0
- package/dist/remote/join-script.js +709 -0
- package/dist/remote/join-script.js.map +1 -0
- package/dist/remote/join.d.ts +19 -0
- package/dist/remote/join.d.ts.map +1 -0
- package/dist/remote/join.js +195 -0
- package/dist/remote/join.js.map +1 -0
- package/dist/remote/lan.d.ts +45 -0
- package/dist/remote/lan.d.ts.map +1 -0
- package/dist/remote/lan.js +105 -0
- package/dist/remote/lan.js.map +1 -0
- package/dist/remote/peer-serve.d.ts +27 -0
- package/dist/remote/peer-serve.d.ts.map +1 -0
- package/dist/remote/peer-serve.js +274 -0
- package/dist/remote/peer-serve.js.map +1 -0
- package/dist/remote/peer.d.ts +62 -0
- package/dist/remote/peer.d.ts.map +1 -0
- package/dist/remote/peer.js +250 -0
- package/dist/remote/peer.js.map +1 -0
- package/dist/remote/registry.d.ts +104 -0
- package/dist/remote/registry.d.ts.map +1 -0
- package/dist/remote/registry.js +359 -0
- package/dist/remote/registry.js.map +1 -0
- package/dist/remote/tarball.d.ts +9 -0
- package/dist/remote/tarball.d.ts.map +1 -0
- package/dist/remote/tarball.js +22 -0
- package/dist/remote/tarball.js.map +1 -0
- package/dist/server.d.ts +54 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +438 -0
- package/dist/server.js.map +1 -0
- package/dist/session/manager.d.ts +104 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +421 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/session/pty.d.ts +114 -0
- package/dist/session/pty.d.ts.map +1 -0
- package/dist/session/pty.js +300 -0
- package/dist/session/pty.js.map +1 -0
- package/package.json +49 -0
- package/web/assets/index-B2JtCtHa.css +1 -0
- package/web/assets/index-BOqZOoep.js +107 -0
- package/web/index.html +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kdonev
|
|
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,288 @@
|
|
|
1
|
+
# Termscape
|
|
2
|
+
|
|
3
|
+
Run a crew of AI coding agents on one infinite canvas, and let them talk to
|
|
4
|
+
each other.
|
|
5
|
+
|
|
6
|
+
Each terminal window on the canvas runs an agent CLI wired to an MCP server the
|
|
7
|
+
hub exposes. Agents can look each other up, send each other messages, spawn
|
|
8
|
+
helpers into their workspace, and check on each other's terminals. A message
|
|
9
|
+
from one agent is delivered by typing it into the other's terminal, immediately.
|
|
10
|
+
|
|
11
|
+
Local-first: the hub runs on your machine, binds loopback only, and the UI is a
|
|
12
|
+
browser tab. Other machines run the same hub as a daemon and appear on the same
|
|
13
|
+
canvas.
|
|
14
|
+
|
|
15
|
+
## Getting started
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @kdonev/termscape
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That starts the hub and opens the canvas in your browser — pass `--no-open`
|
|
22
|
+
if you would rather it did not; the URL is printed either way. Nothing is
|
|
23
|
+
installed system-wide: state lives in `~/.termscape`, and deleting that
|
|
24
|
+
directory is the uninstall.
|
|
25
|
+
|
|
26
|
+
Slide out the **machines** panel: every machine, the workspaces on it, and
|
|
27
|
+
the agents in each. Point a workspace at a folder there, and start an agent
|
|
28
|
+
in it.
|
|
29
|
+
|
|
30
|
+
- **Scroll** to pan, **Ctrl/⌘ + scroll** to zoom, **Ctrl/⌘ + 1** to fit,
|
|
31
|
+
**Ctrl/⌘ + 2** to zoom to one terminal
|
|
32
|
+
- A **quick flick** of the zoom — a fast pinch, or a fast spin of the wheel —
|
|
33
|
+
navigates instead of zooming: in over a terminal maximizes it, out steps
|
|
34
|
+
back to the workspace around it and then to everything. Zooming at any
|
|
35
|
+
ordinary pace is left alone
|
|
36
|
+
- Below 60% zoom terminals become preview cards — zoom in to interact
|
|
37
|
+
- Clicking an agent in the panel brings the canvas to it
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
- Node 22 or newer
|
|
42
|
+
- An agent CLI on your `PATH` — [Claude Code](https://claude.com/claude-code)
|
|
43
|
+
is the profile that ships configured
|
|
44
|
+
- macOS, Windows, or Linux
|
|
45
|
+
|
|
46
|
+
Everything native is prebuilt on all three platforms, so no compiler is
|
|
47
|
+
needed.
|
|
48
|
+
|
|
49
|
+
## How it fits together
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Browser (canvas, xterm.js)
|
|
53
|
+
│ WebSocket: JSON control + binary PTY frames
|
|
54
|
+
Hub (Node)
|
|
55
|
+
│ node-pty ── agent CLI processes
|
|
56
|
+
│ MCP over HTTP ── the tools agents call
|
|
57
|
+
│ SQLite ── workspaces, layout, sessions, screens
|
|
58
|
+
│ hub↔hub WebSocket, either direction
|
|
59
|
+
Remote hub (same binary, --headless)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Agents never talk to each other directly. They call `send_message` on their own
|
|
63
|
+
hub; the hub does the delivery, locally or by forwarding to a peer. That is why
|
|
64
|
+
an agent addresses a peer on another machine exactly as it addresses one in the
|
|
65
|
+
next window.
|
|
66
|
+
|
|
67
|
+
## Adding another machine
|
|
68
|
+
|
|
69
|
+
Start the hub so the other machine can see it, then let that machine come to
|
|
70
|
+
you:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx @kdonev/termscape --listen lan
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
(From a clone that is `npm run dev -- --listen lan`, or
|
|
77
|
+
`npm run dev -w @termscape/hub -- --listen lan` if you are calling the
|
|
78
|
+
workspace directly — npm needs the `--` to hand flags to the hub rather than
|
|
79
|
+
reading them itself.)
|
|
80
|
+
|
|
81
|
+
The hub prints an `enroll:` URL alongside the usual one. Open it **on the
|
|
82
|
+
machine you want to add** and run the command it shows:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
curl -fsSL http://studio:7777/join.sh | sh # macOS, Linux
|
|
86
|
+
irm http://studio:7777/join.ps1 | iex # Windows
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Both halves of that URL are chosen to be typeable, because the join page is the
|
|
90
|
+
one address you have to carry to another machine and enter by hand:
|
|
91
|
+
|
|
92
|
+
- **The host** is this machine's own name when that name actually resolves to
|
|
93
|
+
the address the hub bound — checked, not assumed. Windows resolves bare names
|
|
94
|
+
over LLMNR/NetBIOS and macOS/Linux over mDNS, and neither is guaranteed, so
|
|
95
|
+
the hub prints the numeric URL underneath as a fallback and the machines
|
|
96
|
+
panel offers it too. The canvas itself is reachable by name as well, token and all,
|
|
97
|
+
which is how you open it on a second screen or a phone.
|
|
98
|
+
- **The port** is the first free one from `7777, 4242, 7333, 3333, ...`.
|
|
99
|
+
`--port <n>` overrides it; `--port 0` takes whatever the OS hands out.
|
|
100
|
+
|
|
101
|
+
That installs the hub into `~/.termscape` there and connects it back. The machine
|
|
102
|
+
appears in the **machines** panel with a node of its own, and a workspace added
|
|
103
|
+
under that node runs its agents over there — same addresses, same
|
|
104
|
+
`send_message`, same canvas.
|
|
105
|
+
|
|
106
|
+
The installer works out what is missing before it changes anything:
|
|
107
|
+
|
|
108
|
+
- **Node 22.** If the machine has none, or an older one, it fetches a private
|
|
109
|
+
copy into `~/.termscape/node` — checksum-verified against nodejs.org's own
|
|
110
|
+
`SHASUMS256.txt`, since it is a binary about to be executed. Private rather
|
|
111
|
+
than system-wide, so it needs no administrator rights, no package manager,
|
|
112
|
+
and no fresh shell to pick up a PATH change; uninstalling is deleting the
|
|
113
|
+
directory. A copy already there is reused.
|
|
114
|
+
- **Native modules.** It prefers prebuilt binaries for `node-pty` and
|
|
115
|
+
`better-sqlite3`, falls back to compiling, and if neither works tells you
|
|
116
|
+
exactly which toolchain to install.
|
|
117
|
+
|
|
118
|
+
If the machine does not appear, the installer says why rather than reporting
|
|
119
|
+
success: a spent or expired key, a version gap, or a hub that exited. It waits
|
|
120
|
+
for the canvas to actually accept the machine, not merely for the hub to start
|
|
121
|
+
listening. `~/.termscape/hub.log` on that machine has the detail.
|
|
122
|
+
|
|
123
|
+
Re-running the join command on a machine that already joined is the supported
|
|
124
|
+
way to update or repair it: it stops the hub running there, replaces the
|
|
125
|
+
install, and rejoins with the token it already holds — or, if you had removed
|
|
126
|
+
that host from the canvas, with the fresh key the command carries. Removing a
|
|
127
|
+
machine from the panel stops its hub too, so it is not left running a daemon
|
|
128
|
+
that belongs to nobody — and takes the workspaces and agents on it with it,
|
|
129
|
+
which the panel says before it does it.
|
|
130
|
+
|
|
131
|
+
A re-join is quick after the first one: it keeps the dependency tree already
|
|
132
|
+
installed there unless the dependencies, the Node version, or the platform
|
|
133
|
+
have actually changed, and checks that tree really loads before trusting it.
|
|
134
|
+
|
|
135
|
+
Two things worth knowing:
|
|
136
|
+
|
|
137
|
+
- `--listen` is opt-in and off by default. With it, your hub is reachable on
|
|
138
|
+
that network, and anyone who can load the join page can attach a machine to
|
|
139
|
+
your canvas. Every other route still requires the token.
|
|
140
|
+
- Each download carries a single-use key that expires in 15 minutes. Once a
|
|
141
|
+
machine has joined it keeps a durable token in `~/.termscape/host-token` and
|
|
142
|
+
rejoins by itself after a reboot or a dropped link — its agents keep running
|
|
143
|
+
in the meantime.
|
|
144
|
+
|
|
145
|
+
**If you can't stand at the other machine**, the panel's *deploy over ssh* tab
|
|
146
|
+
does the reverse: it connects with your SSH agent or a key, installs the hub
|
|
147
|
+
over SFTP, starts it bound to that machine's loopback, and reaches it through a
|
|
148
|
+
tunnel. Same protocol, opposite direction.
|
|
149
|
+
|
|
150
|
+
## Tools agents get
|
|
151
|
+
|
|
152
|
+
| Tool | What it does |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `whoami` | your address, workspace and working directory |
|
|
155
|
+
| `list_agents` | everyone on the canvas, across every host, and whether they are busy |
|
|
156
|
+
| `send_message` | type a message into another agent's terminal, right now |
|
|
157
|
+
| `spawn_agent` | start a helper in your workspace, with an optional first task |
|
|
158
|
+
| `read_screen` | look at another agent's terminal without interrupting it |
|
|
159
|
+
| `set_status` | label your own window so the human can see what you are doing |
|
|
160
|
+
| `stop_agent` | stop an agent you spawned |
|
|
161
|
+
|
|
162
|
+
Every agent is also given a brief explaining its address, its peers, and that
|
|
163
|
+
text arriving as `[from <address>] ...` is a colleague rather than the human.
|
|
164
|
+
|
|
165
|
+
## Agent profiles
|
|
166
|
+
|
|
167
|
+
An agent CLI is configuration, not code. Built-ins are `claude` and `shell`
|
|
168
|
+
(a plain terminal, no agent wiring). Override or add profiles in
|
|
169
|
+
`~/.termscape/agents.toml`:
|
|
170
|
+
|
|
171
|
+
```toml
|
|
172
|
+
[my-agent]
|
|
173
|
+
command = "my-cli"
|
|
174
|
+
args = ["--mcp-config", "{{mcp_config_path}}"]
|
|
175
|
+
status = "heuristic" # or "hooks", for exact turn boundaries
|
|
176
|
+
ready_hint = "[$#>%] ?$" # prompt regex, for the idle indicator
|
|
177
|
+
inject = "bracketed" # bracketed paste, or "raw"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## State and restart
|
|
181
|
+
|
|
182
|
+
SQLite at `~/.termscape/state.db` holds everything needed to redraw the canvas
|
|
183
|
+
and relaunch every agent. It deliberately does not hold conversation history —
|
|
184
|
+
Claude Code already keeps that, and we store the pointer to it.
|
|
185
|
+
|
|
186
|
+
Kill the hub and start it again: the canvas comes back with its window
|
|
187
|
+
positions, its zoom, and each terminal's last screen, with the sessions marked
|
|
188
|
+
stopped. **Resume** relaunches an agent with `--resume`, continuing its prior
|
|
189
|
+
conversation.
|
|
190
|
+
|
|
191
|
+
Two consequences worth knowing:
|
|
192
|
+
|
|
193
|
+
- Only the **last screen** of each terminal survives a restart. Scrollback above
|
|
194
|
+
it is not persisted. (While the hub is running, full scrollback is held in
|
|
195
|
+
memory and replayed when you zoom back into a window.)
|
|
196
|
+
- Resume relaunches in the **same working directory**, because that is how
|
|
197
|
+
Claude Code finds a conversation. Move the folder and resume starts fresh.
|
|
198
|
+
|
|
199
|
+
## Remote machines
|
|
200
|
+
|
|
201
|
+
Add a machine in the panel. The hub is copied over SSH, installed, and started as a
|
|
202
|
+
detached daemon bound to the remote machine's loopback interface, reached only
|
|
203
|
+
through an SSH tunnel. Because it is a daemon and not an `ssh` subprocess,
|
|
204
|
+
remote agents keep running when the connection drops, and reattach with their
|
|
205
|
+
state when it comes back.
|
|
206
|
+
|
|
207
|
+
The remote host needs Node 22+ and a toolchain able to build `node-pty` and
|
|
208
|
+
`better-sqlite3`.
|
|
209
|
+
|
|
210
|
+
## Security
|
|
211
|
+
|
|
212
|
+
Agents can type into each other's terminals and spawn more agents. That is the
|
|
213
|
+
feature, and it is the risk surface: an agent that reads a hostile repository
|
|
214
|
+
could be talked into sending an attacker's text to a peer.
|
|
215
|
+
|
|
216
|
+
- The hub binds `127.0.0.1` only, never a network interface. Remote hubs too.
|
|
217
|
+
- Every agent gets its own bearer token. The sender of a message is taken from
|
|
218
|
+
that token, never from the arguments, so attribution cannot be forged.
|
|
219
|
+
- Messages are length-capped, rate-limited per sender, and always arrive with a
|
|
220
|
+
visible `[from <address>]` prefix.
|
|
221
|
+
- Every delivery attempt is recorded with its outcome and shown in the message
|
|
222
|
+
log. Nothing is delivered invisibly.
|
|
223
|
+
- `spawn_agent` is capped per workspace, so a confused agent cannot recurse the
|
|
224
|
+
machine to death.
|
|
225
|
+
- An agent may only stop agents it spawned.
|
|
226
|
+
- `--dangerously-skip-permissions` is never a default.
|
|
227
|
+
|
|
228
|
+
## Development
|
|
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
|
+
```
|
|
277
|
+
|
|
278
|
+
Tagging `v<version>` runs `.github/workflows/release.yml`, which refuses a
|
|
279
|
+
tag that disagrees with `packages/hub/package.json`, installs the packed
|
|
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.
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment sanitizing for spawned agents.
|
|
3
|
+
*
|
|
4
|
+
* Agents inherit the hub's environment so they can find their tools and
|
|
5
|
+
* credentials. But when the hub itself is launched from inside an agent CLI —
|
|
6
|
+
* which is not exotic; it is how you would develop this app — that parent's
|
|
7
|
+
* private session state is in the environment too, and inheriting it is
|
|
8
|
+
* actively harmful:
|
|
9
|
+
*
|
|
10
|
+
* CLAUDE_CODE_CHILD_SESSION marks the child as a sub-session, which turns
|
|
11
|
+
* transcript saving OFF. Since --resume reads
|
|
12
|
+
* that transcript, inheriting this silently
|
|
13
|
+
* breaks the resume feature entirely.
|
|
14
|
+
* CLAUDE_CODE_MESSAGING_SOCKET / _TOKEN
|
|
15
|
+
* the parent session's private IPC channel. A
|
|
16
|
+
* spawned agent must not be handed it.
|
|
17
|
+
* CLAUDE_CODE_SESSION_ID / _HOST_SESSION_ID / CLAUDE_PID
|
|
18
|
+
* identify the parent run; a child adopting them
|
|
19
|
+
* confuses session bookkeeping on both sides.
|
|
20
|
+
*
|
|
21
|
+
* So anything that identifies the *parent agent process* is stripped, while
|
|
22
|
+
* ordinary user configuration (PATH, HOME, ANTHROPIC_API_KEY, proxy settings)
|
|
23
|
+
* is passed through untouched.
|
|
24
|
+
*/
|
|
25
|
+
export declare function shouldStrip(name: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Base environment for a spawned agent: the hub's environment minus the
|
|
28
|
+
* parent-session variables, plus the profile's own overrides.
|
|
29
|
+
*
|
|
30
|
+
* Profile overrides win, so a profile can deliberately re-add something that
|
|
31
|
+
* would otherwise be stripped.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildAgentEnv(base: NodeJS.ProcessEnv, overrides?: Record<string, string>): Record<string, string>;
|
|
34
|
+
/** Names that were removed, for logging and for the tests to assert on. */
|
|
35
|
+
export declare function strippedNames(base: NodeJS.ProcessEnv): string[];
|
|
36
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/agents/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAYH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAIjD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,CAAC,UAAU,EACvB,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACrC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,EAAE,CAE/D"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment sanitizing for spawned agents.
|
|
3
|
+
*
|
|
4
|
+
* Agents inherit the hub's environment so they can find their tools and
|
|
5
|
+
* credentials. But when the hub itself is launched from inside an agent CLI —
|
|
6
|
+
* which is not exotic; it is how you would develop this app — that parent's
|
|
7
|
+
* private session state is in the environment too, and inheriting it is
|
|
8
|
+
* actively harmful:
|
|
9
|
+
*
|
|
10
|
+
* CLAUDE_CODE_CHILD_SESSION marks the child as a sub-session, which turns
|
|
11
|
+
* transcript saving OFF. Since --resume reads
|
|
12
|
+
* that transcript, inheriting this silently
|
|
13
|
+
* breaks the resume feature entirely.
|
|
14
|
+
* CLAUDE_CODE_MESSAGING_SOCKET / _TOKEN
|
|
15
|
+
* the parent session's private IPC channel. A
|
|
16
|
+
* spawned agent must not be handed it.
|
|
17
|
+
* CLAUDE_CODE_SESSION_ID / _HOST_SESSION_ID / CLAUDE_PID
|
|
18
|
+
* identify the parent run; a child adopting them
|
|
19
|
+
* confuses session bookkeeping on both sides.
|
|
20
|
+
*
|
|
21
|
+
* So anything that identifies the *parent agent process* is stripped, while
|
|
22
|
+
* ordinary user configuration (PATH, HOME, ANTHROPIC_API_KEY, proxy settings)
|
|
23
|
+
* is passed through untouched.
|
|
24
|
+
*/
|
|
25
|
+
/** Exact names that must never reach a spawned agent. */
|
|
26
|
+
const STRIP_EXACT = new Set(['CLAUDECODE', 'CLAUDE_PID', 'CLAUDE_EFFORT']);
|
|
27
|
+
/** Prefixes whose whole family is parent-session state. */
|
|
28
|
+
const STRIP_PREFIXES = [
|
|
29
|
+
'CLAUDE_CODE_',
|
|
30
|
+
'CLAUDE_AGENT_SDK_',
|
|
31
|
+
'CLAUDE_PREVIEW_',
|
|
32
|
+
];
|
|
33
|
+
export function shouldStrip(name) {
|
|
34
|
+
const upper = name.toUpperCase();
|
|
35
|
+
if (STRIP_EXACT.has(upper))
|
|
36
|
+
return true;
|
|
37
|
+
return STRIP_PREFIXES.some((p) => upper.startsWith(p));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Base environment for a spawned agent: the hub's environment minus the
|
|
41
|
+
* parent-session variables, plus the profile's own overrides.
|
|
42
|
+
*
|
|
43
|
+
* Profile overrides win, so a profile can deliberately re-add something that
|
|
44
|
+
* would otherwise be stripped.
|
|
45
|
+
*/
|
|
46
|
+
export function buildAgentEnv(base, overrides = {}) {
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const [k, v] of Object.entries(base)) {
|
|
49
|
+
if (v === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
if (shouldStrip(k))
|
|
52
|
+
continue;
|
|
53
|
+
out[k] = v;
|
|
54
|
+
}
|
|
55
|
+
return { ...out, ...overrides };
|
|
56
|
+
}
|
|
57
|
+
/** Names that were removed, for logging and for the tests to assert on. */
|
|
58
|
+
export function strippedNames(base) {
|
|
59
|
+
return Object.keys(base).filter(shouldStrip).sort();
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/agents/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,yDAAyD;AACzD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,YAAY,EAAE,eAAe,CAAC,CAAC,CAAC;AAE3E,2DAA2D;AAC3D,MAAM,cAAc,GAAG;IACrB,cAAc;IACd,mBAAmB;IACnB,iBAAiB;CAClB,CAAC;AAEF,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,IAAI,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAuB,EACvB,YAAoC,EAAE;IAEtC,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,KAAK,SAAS;YAAE,SAAS;QAC9B,IAAI,WAAW,CAAC,CAAC,CAAC;YAAE,SAAS;QAC7B,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACb,CAAC;IACD,OAAO,EAAE,GAAG,GAAG,EAAE,GAAG,SAAS,EAAE,CAAC;AAClC,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,aAAa,CAAC,IAAuB;IACnD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,EAAE,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { InjectMode } from '../protocol/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* An agent profile describes how to launch one kind of CLI and how to talk to
|
|
4
|
+
* it. Adding a new agent CLI should be a config change, not a code change.
|
|
5
|
+
*/
|
|
6
|
+
export interface AgentProfile {
|
|
7
|
+
id: string;
|
|
8
|
+
description: string;
|
|
9
|
+
command: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
env: Record<string, string>;
|
|
12
|
+
/** false = plain terminal: no MCP wiring, no brief, no agent identity. */
|
|
13
|
+
mcp: boolean;
|
|
14
|
+
/** How busy/idle is determined. */
|
|
15
|
+
status: 'hooks' | 'heuristic';
|
|
16
|
+
/** Regex matched against the last non-empty line when status = heuristic. */
|
|
17
|
+
readyHint?: string;
|
|
18
|
+
inject: InjectMode;
|
|
19
|
+
/**
|
|
20
|
+
* Argument template used to bring a session back with its prior
|
|
21
|
+
* conversation. `{{uuid}}` is replaced with agent_session_uuid. When absent,
|
|
22
|
+
* the profile is not resumable and restarts clean.
|
|
23
|
+
*/
|
|
24
|
+
resumeArgs?: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Built-in profiles. All Claude Code flags here are verified against the CLI:
|
|
28
|
+
* --mcp-config, --strict-mcp-config, --session-id, --settings,
|
|
29
|
+
* --append-system-prompt-file, --resume.
|
|
30
|
+
*/
|
|
31
|
+
export declare const BUILTIN_PROFILES: Record<string, AgentProfile>;
|
|
32
|
+
export declare class ProfileRegistry {
|
|
33
|
+
private profiles;
|
|
34
|
+
constructor(profiles?: Record<string, AgentProfile>);
|
|
35
|
+
/** Built-ins, overlaid with ~/.termscape/agents.toml if present. */
|
|
36
|
+
static load(): ProfileRegistry;
|
|
37
|
+
get(id: string): AgentProfile | null;
|
|
38
|
+
require(id: string): AgentProfile;
|
|
39
|
+
list(): AgentProfile[];
|
|
40
|
+
isResumable(id: string): boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Replace `{{key}}` placeholders. Unknown placeholders are left untouched. */
|
|
43
|
+
export declare function template(input: string, vars: Record<string, string>): string;
|
|
44
|
+
export declare function templateAll(args: string[], vars: Record<string, string>): string[];
|
|
45
|
+
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -0,0 +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;CACvB;AAOD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CA8CzD,CAAC;AAEF,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;IA8B9B,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"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { platform } from 'node:process';
|
|
3
|
+
import { parse as parseToml } from 'smol-toml';
|
|
4
|
+
import { paths } from '../paths.js';
|
|
5
|
+
function defaultShell() {
|
|
6
|
+
if (platform === 'win32')
|
|
7
|
+
return process.env.COMSPEC ?? 'powershell.exe';
|
|
8
|
+
return process.env.SHELL ?? '/bin/bash';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Built-in profiles. All Claude Code flags here are verified against the CLI:
|
|
12
|
+
* --mcp-config, --strict-mcp-config, --session-id, --settings,
|
|
13
|
+
* --append-system-prompt-file, --resume.
|
|
14
|
+
*/
|
|
15
|
+
export const BUILTIN_PROFILES = {
|
|
16
|
+
claude: {
|
|
17
|
+
id: 'claude',
|
|
18
|
+
description: 'Claude Code, wired to the hub MCP endpoint',
|
|
19
|
+
command: 'claude',
|
|
20
|
+
args: [
|
|
21
|
+
'--mcp-config',
|
|
22
|
+
'{{mcp_config_path}}',
|
|
23
|
+
'--strict-mcp-config',
|
|
24
|
+
'--session-id',
|
|
25
|
+
'{{session_uuid}}',
|
|
26
|
+
'--settings',
|
|
27
|
+
'{{settings_path}}',
|
|
28
|
+
'--append-system-prompt-file',
|
|
29
|
+
'{{brief_path}}',
|
|
30
|
+
],
|
|
31
|
+
env: {},
|
|
32
|
+
mcp: true,
|
|
33
|
+
status: 'hooks',
|
|
34
|
+
inject: 'bracketed',
|
|
35
|
+
// Resume swaps --session-id for --resume; everything else is re-templated
|
|
36
|
+
// identically, and crucially the session is relaunched in the same cwd,
|
|
37
|
+
// because Claude Code keys its conversation store by working directory.
|
|
38
|
+
resumeArgs: [
|
|
39
|
+
'--mcp-config',
|
|
40
|
+
'{{mcp_config_path}}',
|
|
41
|
+
'--strict-mcp-config',
|
|
42
|
+
'--resume',
|
|
43
|
+
'{{session_uuid}}',
|
|
44
|
+
'--settings',
|
|
45
|
+
'{{settings_path}}',
|
|
46
|
+
'--append-system-prompt-file',
|
|
47
|
+
'{{brief_path}}',
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
shell: {
|
|
51
|
+
id: 'shell',
|
|
52
|
+
description: 'Plain terminal, no agent wiring',
|
|
53
|
+
command: defaultShell(),
|
|
54
|
+
args: [],
|
|
55
|
+
env: {},
|
|
56
|
+
mcp: false,
|
|
57
|
+
status: 'heuristic',
|
|
58
|
+
readyHint: '[$#>%] ?$',
|
|
59
|
+
inject: 'raw',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
export class ProfileRegistry {
|
|
63
|
+
profiles;
|
|
64
|
+
constructor(profiles) {
|
|
65
|
+
this.profiles = profiles ?? { ...BUILTIN_PROFILES };
|
|
66
|
+
}
|
|
67
|
+
/** Built-ins, overlaid with ~/.termscape/agents.toml if present. */
|
|
68
|
+
static load() {
|
|
69
|
+
const merged = { ...BUILTIN_PROFILES };
|
|
70
|
+
const file = paths.profiles();
|
|
71
|
+
if (existsSync(file)) {
|
|
72
|
+
try {
|
|
73
|
+
const raw = parseToml(readFileSync(file, 'utf8'));
|
|
74
|
+
for (const [id, v] of Object.entries(raw)) {
|
|
75
|
+
if (typeof v !== 'object' || v === null)
|
|
76
|
+
continue;
|
|
77
|
+
const base = merged[id];
|
|
78
|
+
merged[id] = {
|
|
79
|
+
id,
|
|
80
|
+
description: v.description ?? base?.description ?? id,
|
|
81
|
+
command: v.command ?? base?.command ?? id,
|
|
82
|
+
args: v.args ?? base?.args ?? [],
|
|
83
|
+
env: v.env ?? base?.env ?? {},
|
|
84
|
+
mcp: v.mcp ?? base?.mcp ?? true,
|
|
85
|
+
status: v.status ?? base?.status ?? 'heuristic',
|
|
86
|
+
readyHint: v.ready_hint ?? v.readyHint ?? base?.readyHint,
|
|
87
|
+
inject: v.inject ?? base?.inject ?? 'bracketed',
|
|
88
|
+
resumeArgs: v.resume_args ?? v.resumeArgs ?? base?.resumeArgs,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// A broken config must not take the hub down; fall back to built-ins.
|
|
94
|
+
console.error(`[profiles] ignoring ${file}: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return new ProfileRegistry(merged);
|
|
98
|
+
}
|
|
99
|
+
get(id) {
|
|
100
|
+
return this.profiles[id] ?? null;
|
|
101
|
+
}
|
|
102
|
+
require(id) {
|
|
103
|
+
const p = this.get(id);
|
|
104
|
+
if (!p)
|
|
105
|
+
throw new Error(`unknown agent profile "${id}"`);
|
|
106
|
+
return p;
|
|
107
|
+
}
|
|
108
|
+
list() {
|
|
109
|
+
return Object.values(this.profiles);
|
|
110
|
+
}
|
|
111
|
+
isResumable(id) {
|
|
112
|
+
const p = this.get(id);
|
|
113
|
+
return !!p?.resumeArgs;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Replace `{{key}}` placeholders. Unknown placeholders are left untouched. */
|
|
117
|
+
export function template(input, vars) {
|
|
118
|
+
return input.replace(/\{\{(\w+)\}\}/g, (whole, key) => key in vars ? vars[key] : whole);
|
|
119
|
+
}
|
|
120
|
+
export function templateAll(args, vars) {
|
|
121
|
+
return args.map((a) => template(a, vars));
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=profiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.js","sourceRoot":"","sources":["../../src/agents/profiles.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Absolute path to `command`, or null when it is not on PATH. */
|
|
2
|
+
export declare function which(command: string): string | null;
|
|
3
|
+
export interface ResolvedCommand {
|
|
4
|
+
argv: string[];
|
|
5
|
+
/** The executable that was found, before any shell wrapping. */
|
|
6
|
+
resolved: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Turn a profile's (command, args) into an argv node-pty can actually spawn.
|
|
10
|
+
*
|
|
11
|
+
* Windows batch shims (.cmd/.bat) are not executable images, so CreateProcess
|
|
12
|
+
* refuses them. Many CLIs installed via npm are exactly that, so those get
|
|
13
|
+
* wrapped in `cmd.exe /c`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveCommand(command: string, args: string[]): ResolvedCommand;
|
|
16
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/agents/resolve.ts"],"names":[],"mappings":"AA4BA,kEAAkE;AAClE,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuBpD;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CAgB/E"}
|