meldivo 2.3.0 → 2.4.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 +53 -237
- package/dist/server/harnesses/claude-warm.js +236 -0
- package/dist/server/harnesses/claude-warm.js.map +1 -0
- package/dist/server/harnesses/claude.js +129 -115
- package/dist/server/harnesses/claude.js.map +1 -1
- package/dist/server/harnesses/pi-rpc.js +260 -0
- package/dist/server/harnesses/pi-rpc.js.map +1 -0
- package/dist/server/harnesses/pi-voice-extension.js +6 -2
- package/dist/server/harnesses/pi-voice-extension.js.map +1 -1
- package/dist/server/harnesses/pi.js +217 -147
- package/dist/server/harnesses/pi.js.map +1 -1
- package/dist/server/hub.js +7 -0
- package/dist/server/hub.js.map +1 -1
- package/dist/server/index.js +160 -31
- package/dist/server/index.js.map +1 -1
- package/dist/server/machine-client.js +6 -0
- package/dist/server/machine-client.js.map +1 -1
- package/dist/server/protocol.js.map +1 -1
- package/dist/web/assets/index-C4QjuAnl.js +80 -0
- package/dist/web/assets/index-D29QAZ8k.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BQ_QqjNG.css +0 -1
- package/dist/web/assets/index-Qdl_mLvJ.js +0 -80
package/README.md
CHANGED
|
@@ -1,262 +1,78 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/amirrouh/meldivo/main/assets/meldivo-logo.png" alt="meldivo" width="120">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
voice to your [Pi](https://pi.dev), [OpenCode](https://opencode.ai), and
|
|
5
|
-
[Claude Code](https://claude.com/claude-code) sessions from any browser or
|
|
6
|
-
phone. Speech recognition and speech synthesis run locally on your machine:
|
|
7
|
-
no cloud speech API, no API keys for speech, no extra LLM. If you run your own
|
|
8
|
-
speech server, you can switch to a bigger or more realistic model instead.
|
|
5
|
+
<h1 align="center">meldivo</h1>
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`meldivo start` installs a user service that keeps the hub running in the
|
|
16
|
-
background, then prints a local link and a QR code:
|
|
17
|
-
|
|
18
|
-
```text
|
|
19
|
-
http://127.0.0.1:4100/#token=...
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Open the link (or scan the QR code) to see every Pi, OpenCode, and Claude
|
|
23
|
-
Code session on the machine, plus a "new chat" tile for each installed agent.
|
|
24
|
-
Tap any tile to start talking.
|
|
25
|
-
|
|
26
|
-
## Features
|
|
27
|
-
|
|
28
|
-
- **One hub for every session.** The hub page lists your Pi, OpenCode, and
|
|
29
|
-
Claude Code sessions automatically, found from their own session files —
|
|
30
|
-
no plugins or extensions to install in each agent.
|
|
31
|
-
- **Hands-free conversation.** Your speech becomes a turn in the chosen
|
|
32
|
-
session, run headlessly with that session's own model and credentials.
|
|
33
|
-
Replies are spoken back to you.
|
|
34
|
-
- **Safe alongside your terminal.** If a session is already open in a
|
|
35
|
-
terminal, talking to it by voice continues it as a fork, so the voice
|
|
36
|
-
copy never conflicts with what you're typing.
|
|
37
|
-
- **Local, private speech.** Speech-to-text uses NVIDIA Parakeet TDT 0.6B v3
|
|
38
|
-
and text-to-speech uses Kokoro v1.0, both running on the CPU through
|
|
39
|
-
[sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx). Audio never leaves
|
|
40
|
-
your machine unless you turn on phone access.
|
|
41
|
-
- **Or your own speech server.** The hub's "Speech" panel can send either
|
|
42
|
-
direction to a speech server you run, such as Breeze TTS 2, vLLM-Omni
|
|
43
|
-
(Fish S2 Pro, Voxtral TTS, Qwen3-TTS, Higgs Audio), Kokoro-FastAPI,
|
|
44
|
-
Chatterbox, Fish Speech, Orpheus, Speaches, vLLM, or whisper.cpp. See
|
|
45
|
-
[Speech servers](#speech-servers).
|
|
46
|
-
- **Phone and tablet access.** `meldivo remote` (or the hub's "Phone access"
|
|
47
|
-
panel) publishes the hub over HTTPS through Tailscale, a Cloudflare
|
|
48
|
-
tunnel, or your own certificate, and shows a QR code to scan.
|
|
49
|
-
- **Runs in the background.** A user service (systemd on Linux, launchd on
|
|
50
|
-
macOS) starts the hub on login and keeps it running. Models download once
|
|
51
|
-
on first use.
|
|
52
|
-
- **Secure by default.** The server listens on `127.0.0.1` only, and the
|
|
53
|
-
whole hub is protected by a random token embedded in its link.
|
|
54
|
-
|
|
55
|
-
## Requirements
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Talk to your coding agents.</strong><br>
|
|
9
|
+
A local voice hub for Pi, Claude Code, and OpenCode, in any browser.
|
|
10
|
+
</p>
|
|
56
11
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/meldivo"><img src="https://img.shields.io/npm/v/meldivo?color=black&label=npm" alt="npm version"></a>
|
|
14
|
+
<a href="https://github.com/amirrouh/meldivo/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/meldivo?color=black" alt="MIT license"></a>
|
|
15
|
+
<a href="https://github.com/amirrouh/meldivo/blob/main/docs/README.md"><img src="https://img.shields.io/badge/docs-read-black" alt="Documentation"></a>
|
|
16
|
+
</p>
|
|
61
17
|
|
|
62
|
-
|
|
18
|
+
<p align="center">
|
|
19
|
+
<img src="https://raw.githubusercontent.com/amirrouh/meldivo/main/assets/meldivo-demo.gif" alt="Install meldivo, start it, and talk to a Claude Code session in the browser" width="720">
|
|
20
|
+
</p>
|
|
63
21
|
|
|
64
|
-
|
|
65
|
-
|---|---|---|---|
|
|
66
|
-
| Pi | Pi's own session files in the home folder | A voice turn continues the session as a fork, leaving the terminal session untouched | No approval step; a denied action is announced |
|
|
67
|
-
| OpenCode | OpenCode's session files | A voice turn continues the session as a fork, leaving the terminal session untouched | Headless runs auto-reject actions that need approval |
|
|
68
|
-
| Claude Code | Claude Code's session files | A voice turn continues the session as a fork, leaving the terminal session untouched | Runs with `--permission-mode acceptEdits` |
|
|
22
|
+
---
|
|
69
23
|
|
|
70
|
-
|
|
71
|
-
project.
|
|
72
|
-
|
|
73
|
-
## Commands
|
|
74
|
-
|
|
75
|
-
| Command | Description |
|
|
76
|
-
|---|---|
|
|
77
|
-
| `meldivo start` | Install (if needed) and start the background service, then print the hub link and QR code. |
|
|
78
|
-
| `meldivo stop` | Stop the background service. |
|
|
79
|
-
| `meldivo status` | Show whether the service is running and its listening address. |
|
|
80
|
-
| `meldivo open [--browser]` | Print the hub link, optionally opening it in a browser. |
|
|
81
|
-
| `meldivo remote` | Set up phone/tablet access over HTTPS. |
|
|
82
|
-
| `meldivo remote tailscale` | Publish the hub over Tailscale. |
|
|
83
|
-
| `meldivo remote cloudflare` | Publish the hub through a Cloudflare quick tunnel. |
|
|
84
|
-
| `meldivo remote certificate` | Publish the hub over HTTPS using your own certificate. |
|
|
85
|
-
| `meldivo remote stop` | Turn remote access off. |
|
|
86
|
-
| `meldivo hub enable` | Make this machine a hub that your other machines join. `meldivo hub disable` turns it off. |
|
|
87
|
-
| `meldivo hub code` | Print a one-time code (10 minutes, single use) and the `meldivo join` command for adding a machine. |
|
|
88
|
-
| `meldivo hub machines` | List the machines that joined this hub. `meldivo hub remove <name>` removes one at once. |
|
|
89
|
-
| `meldivo join <hub-address> <code>` | Join a hub, so this machine's sessions appear there. Options: `--name <name>`, `--allow pi,opencode,claude`, `--speech` (also do speech for the hub). |
|
|
90
|
-
| `meldivo leave` | Leave the hub this machine joined. |
|
|
91
|
-
| `meldivo logs` | Show the service's recent log output. |
|
|
92
|
-
| `meldivo uninstall [--purge]` | Remove the background service. `--purge` also deletes configuration, models, and logs. |
|
|
93
|
-
| `meldivo --version` | Print the installed version. |
|
|
94
|
-
|
|
95
|
-
## Using it from a phone or another computer
|
|
96
|
-
|
|
97
|
-
Browsers only allow microphone access on `localhost` or over HTTPS, so
|
|
98
|
-
another device needs an HTTPS link. Run `meldivo remote`, or open the hub's
|
|
99
|
-
"Phone access" panel, to set one up:
|
|
100
|
-
|
|
101
|
-
1. **Tailscale**: private to your own devices, works from anywhere.
|
|
102
|
-
2. **Cloudflare quick tunnel**: no account needed, produces a public link
|
|
103
|
-
protected by its token.
|
|
104
|
-
3. **Your own certificate**: HTTPS on your local network or VPN, for example
|
|
105
|
-
with `mkcert`.
|
|
106
|
-
|
|
107
|
-
Whichever option you pick, the link is the same hub link — the whole hub
|
|
108
|
-
becomes reachable from that device, protected by its token. See the
|
|
109
|
-
[remote access guide](docs/remote-access.md) for setup details, including
|
|
110
|
-
how to trust a certificate on iPhone and Android.
|
|
111
|
-
|
|
112
|
-
## Several machines, one hub
|
|
113
|
-
|
|
114
|
-
If you use coding agents on more than one computer, make one of them (or a
|
|
115
|
-
small always-on box such as a Raspberry Pi) a **hub**. Every other machine
|
|
116
|
-
joins it and keeps an outbound connection to it, so those machines need no
|
|
117
|
-
open ports. The hub's single link then lists every machine with its sessions
|
|
118
|
-
and new-chat tiles. Only the text of each turn goes to the machine that runs
|
|
119
|
-
it. Speech runs on the hub, or on a faster machine that joins with `--speech`,
|
|
120
|
-
which is recommended when the hub is a small computer.
|
|
121
|
-
|
|
122
|
-
On the hub:
|
|
123
|
-
|
|
124
|
-
```sh
|
|
125
|
-
meldivo hub enable # prints the hub link
|
|
126
|
-
meldivo hub code # prints a one-time code and the join command
|
|
127
|
-
```
|
|
24
|
+
## Get started in three steps
|
|
128
25
|
|
|
129
|
-
|
|
26
|
+
**1. Install**
|
|
130
27
|
|
|
131
|
-
```
|
|
132
|
-
|
|
28
|
+
```bash
|
|
29
|
+
npm i -g meldivo
|
|
133
30
|
```
|
|
134
31
|
|
|
135
|
-
|
|
136
|
-
network such as a VPN: set `MELDIVO_HOST=<vpn-ip>` when starting the hub so it
|
|
137
|
-
also listens there. See the [self-hosting guide](docs/self-hosting-hub.md) for
|
|
138
|
-
a full walkthrough, including running the hub on a Raspberry Pi.
|
|
139
|
-
|
|
140
|
-
How it stays private:
|
|
141
|
-
|
|
142
|
-
- A machine joins only with a one-time code, and gets its own credential; the
|
|
143
|
-
hub stores only a hash of it. `meldivo hub remove <name>` cuts a machine off
|
|
144
|
-
immediately.
|
|
145
|
-
- The hub proves its identity on every connection with a key the machine
|
|
146
|
-
pinned when it joined, so a machine never talks to a look-alike hub.
|
|
147
|
-
- The hub can only ask a machine to run or cancel a text turn in one of its
|
|
148
|
-
own sessions, never a command. `--allow` limits which agents it may use.
|
|
149
|
-
- Session lists are kept in memory on the hub, never written to disk.
|
|
150
|
-
|
|
151
|
-
## How it works
|
|
152
|
-
|
|
153
|
-
`meldivo start` installs a user service that runs the hub server on
|
|
154
|
-
`127.0.0.1:4100`. The hub scans each installed agent's session files to list
|
|
155
|
-
recent sessions and shows a "new chat" tile for each installed agent. When
|
|
156
|
-
you talk to a session, the hub runs that agent headlessly for one turn, using
|
|
157
|
-
its own model and credentials; if the session is open in a terminal, the
|
|
158
|
-
turn runs against a fork instead, so the terminal session is never touched.
|
|
159
|
-
Your speech is transcribed locally, sent to the agent as a normal user
|
|
160
|
-
message, and the reply is read aloud with local text-to-speech.
|
|
32
|
+
**2. Start**
|
|
161
33
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
you hear something within seconds even during long tasks. For reasoning models
|
|
165
|
-
served on your own machine or private network (llama.cpp, vLLM, and similar),
|
|
166
|
-
the first reply of each turn skips the model's thinking so it can start
|
|
167
|
-
speaking right away; later steps, after tool calls, think as usual. Hosted
|
|
168
|
-
APIs are never sent this setting.
|
|
169
|
-
|
|
170
|
-
## Speech servers
|
|
171
|
-
|
|
172
|
-
By default everything runs on the device. To use a bigger, more realistic,
|
|
173
|
-
or faster model, open the hub page, tap **Speech**, pick an engine for text
|
|
174
|
-
to speech or speech to text, and type its server address. meldivo loads that
|
|
175
|
-
server's models and speakers, and ▶ plays a preview of the selected speaker.
|
|
176
|
-
The choice applies to the whole hub: every machine that joined it and every
|
|
177
|
-
browser that opens it.
|
|
178
|
-
|
|
179
|
-
| Engine | Direction | Models it serves | Usual address |
|
|
180
|
-
|---|---|---|---|
|
|
181
|
-
| [Breeze-TTS-2.cpp](https://github.com/HoppouAI/Breeze-TTS-2.cpp) | Text to speech | Breeze TTS 2, with the voices you saved on the server | `http://localhost:8080` |
|
|
182
|
-
| [vLLM-Omni](https://github.com/vllm-project/vllm-omni) | Text to speech | Fish S2 Pro, Voxtral TTS, Qwen3-TTS, Higgs Audio, CosyVoice, IndexTTS2 | `http://localhost:8091` |
|
|
183
|
-
| [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI) | Text to speech | Kokoro | `http://localhost:8880` |
|
|
184
|
-
| [Chatterbox TTS Server](https://github.com/devnen/Chatterbox-TTS-Server) | Text to speech | Chatterbox, Turbo, Multilingual | `http://localhost:8004` |
|
|
185
|
-
| [Fish Speech](https://github.com/fishaudio/fish-speech) | Text to speech | OpenAudio S1, Fish S2 (native API) | `http://localhost:8080` |
|
|
186
|
-
| [Orpheus-FastAPI](https://github.com/Lex-au/Orpheus-FastAPI) | Text to speech | Orpheus | `http://localhost:5005` |
|
|
187
|
-
| [Speaches](https://github.com/speaches-ai/speaches) | Both | Kokoro, Piper; faster-whisper, Parakeet | `http://localhost:8000` |
|
|
188
|
-
| [vLLM](https://docs.vllm.ai) | Speech to text | Whisper, Voxtral, Qwen3-ASR, Granite Speech | `http://localhost:8000` |
|
|
189
|
-
| [whisper.cpp server](https://github.com/ggml-org/whisper.cpp) | Speech to text | Whisper | `http://localhost:8080` |
|
|
190
|
-
| Other OpenAI-compatible | Both | Anything serving `/v1/audio/speech` or `/v1/audio/transcriptions` | |
|
|
191
|
-
|
|
192
|
-
For example, Kokoro on a GPU:
|
|
193
|
-
|
|
194
|
-
```sh
|
|
195
|
-
docker run -d --gpus all -p 127.0.0.1:8880:8880 ghcr.io/remsky/kokoro-fastapi-gpu:latest
|
|
34
|
+
```bash
|
|
35
|
+
meldivo start
|
|
196
36
|
```
|
|
197
37
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
with a `name`, or `breeze-cli --save-voice`); meldivo lists your saved voices as
|
|
201
|
-
speakers, so every reply uses the same voice.
|
|
202
|
-
|
|
203
|
-
An API key is optional; it is stored on the hub only (in
|
|
204
|
-
`~/.config/meldivo/speech.json`, readable by you alone) and never sent to the
|
|
205
|
-
browser. Audio goes to the server you chose, so pick one you run yourself or
|
|
206
|
-
trust. Speaches downloads a model the first time it is used.
|
|
207
|
-
|
|
208
|
-
## Configuration
|
|
38
|
+
meldivo runs in the background from now on and prints your private hub link
|
|
39
|
+
with a QR code.
|
|
209
40
|
|
|
210
|
-
|
|
41
|
+
**3. Talk**
|
|
211
42
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
| `MELDIVO_HTTPS_PORT` | `4443` | HTTPS port used by the "own certificate" remote option. |
|
|
216
|
-
| `MELDIVO_PUBLIC_URL` | unset | Base URL to print when you run your own reverse proxy. |
|
|
217
|
-
| `MELDIVO_HOST` | unset | Extra addresses to listen on besides `127.0.0.1` (comma-separated), e.g. a VPN address that joined machines or your reverse proxy connect to. |
|
|
218
|
-
| `MELDIVO_MODELS_DIR` | `~/.cache/meldivo/models` | Location of the downloaded speech models. |
|
|
43
|
+
Open the link in any browser, or scan the QR code. Every Pi, Claude Code, and
|
|
44
|
+
OpenCode session on your machine is already there. Pick one, or start a new
|
|
45
|
+
chat, and just speak.
|
|
219
46
|
|
|
220
|
-
|
|
47
|
+
That's it. No API keys, no plugins, no cloud.
|
|
221
48
|
|
|
222
|
-
|
|
223
|
-
|---|---|
|
|
224
|
-
| `~/.config/meldivo/secret` | The hub's shared secret, generated automatically. |
|
|
225
|
-
| `~/.config/meldivo/hub.json` | On a machine that joined a hub: the hub's address, this machine's credential, and the hub's pinned key. |
|
|
226
|
-
| `~/.config/meldivo/speech.json` | The speech engines chosen in the hub's "Speech" panel, and their API keys. |
|
|
227
|
-
| `~/.config/meldivo/machines.json`, `hub-key.pem` | On a hub: joined machines (names and credential hashes) and the hub's signing key. |
|
|
228
|
-
| `~/.cache/meldivo/models` | Downloaded speech models. |
|
|
229
|
-
| `~/.local/state/meldivo` | Service logs and runtime state. |
|
|
49
|
+
## Why meldivo
|
|
230
50
|
|
|
231
|
-
|
|
51
|
+
- **Works with the agents you already use.** Pi, Claude Code, and OpenCode
|
|
52
|
+
sessions show up automatically, each running with its own model and
|
|
53
|
+
credentials.
|
|
54
|
+
- **Private by design.** Speech recognition and synthesis run locally on your
|
|
55
|
+
CPU. The hub listens on `127.0.0.1` only and every link is protected by a
|
|
56
|
+
secret token.
|
|
57
|
+
- **Never in your way.** If a session is open in a terminal, voice continues
|
|
58
|
+
it as a fork, so nothing collides with what you're typing.
|
|
59
|
+
- **Anywhere you are.** `meldivo remote` puts the hub on your phone over HTTPS,
|
|
60
|
+
and your other computers can join one hub with a one-time code.
|
|
61
|
+
- **Bring your own voice.** Point it at your own speech server (Breeze TTS 2,
|
|
62
|
+
vLLM-Omni, Kokoro, Whisper, and more) for bigger or more realistic models.
|
|
232
63
|
|
|
233
|
-
|
|
234
|
-
unless you explicitly turn on phone access. Every hub link, local or remote,
|
|
235
|
-
embeds a random token, and requests without it are rejected. Treat the hub
|
|
236
|
-
link like a password: anyone who has it can use every session in the hub,
|
|
237
|
-
including the sessions of every machine that joined it.
|
|
238
|
-
|
|
239
|
-
## Uninstall
|
|
240
|
-
|
|
241
|
-
```bash
|
|
242
|
-
meldivo uninstall --purge
|
|
243
|
-
npm rm -g meldivo
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
`meldivo uninstall` alone removes the background service but keeps your
|
|
247
|
-
configuration, models, and logs; add `--purge` to remove those too.
|
|
64
|
+
## Requirements
|
|
248
65
|
|
|
249
|
-
|
|
66
|
+
Node.js 22+, macOS or Linux, and at least one of Pi, Claude Code, or OpenCode.
|
|
67
|
+
Speech models (about 650 MB) download once on first use.
|
|
250
68
|
|
|
251
|
-
|
|
252
|
-
npm install
|
|
253
|
-
npm test
|
|
254
|
-
```
|
|
69
|
+
## Documentation
|
|
255
70
|
|
|
256
|
-
|
|
257
|
-
|
|
71
|
+
Commands, phone access, multi-machine hubs, speech servers, configuration, and
|
|
72
|
+
security are all covered in the
|
|
73
|
+
**[documentation](https://github.com/amirrouh/meldivo/blob/main/docs/README.md)**.
|
|
258
74
|
|
|
259
75
|
## License
|
|
260
76
|
|
|
261
|
-
MIT. See [LICENSE](LICENSE)
|
|
262
|
-
|
|
77
|
+
MIT. See [LICENSE](https://github.com/amirrouh/meldivo/blob/main/LICENSE) and
|
|
78
|
+
[third-party notices](https://github.com/amirrouh/meldivo/blob/main/THIRD_PARTY_NOTICES.md).
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const IDLE_MS = 10 * 60_000;
|
|
5
|
+
const MAX_PROCESSES = 4;
|
|
6
|
+
const INTERRUPT_GRACE_MS = 3_000;
|
|
7
|
+
function stampOf(file) {
|
|
8
|
+
if (!file)
|
|
9
|
+
return undefined;
|
|
10
|
+
try {
|
|
11
|
+
const stat = statSync(file);
|
|
12
|
+
return { size: stat.size, mtimeMs: stat.mtimeMs };
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** The transcript Claude Code keeps for session `id`, in whichever project folder it lives. */
|
|
19
|
+
function findTranscript(projectsDir, id) {
|
|
20
|
+
let dirs;
|
|
21
|
+
try {
|
|
22
|
+
dirs = readdirSync(projectsDir);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
for (const dir of dirs) {
|
|
28
|
+
const file = path.join(projectsDir, dir, `${id}.jsonl`);
|
|
29
|
+
if (existsSync(file))
|
|
30
|
+
return file;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
export class ClaudeProcess {
|
|
35
|
+
child;
|
|
36
|
+
cwd;
|
|
37
|
+
model;
|
|
38
|
+
sessionId;
|
|
39
|
+
busy = true;
|
|
40
|
+
exited = false;
|
|
41
|
+
exitCode = null;
|
|
42
|
+
lastUsed = Date.now();
|
|
43
|
+
stderrTail = "";
|
|
44
|
+
transcript;
|
|
45
|
+
stamp;
|
|
46
|
+
buffered = "";
|
|
47
|
+
nextRequest = 0;
|
|
48
|
+
listeners = new Set();
|
|
49
|
+
exitListeners = new Set();
|
|
50
|
+
idleTimer;
|
|
51
|
+
constructor(bin, args, cwd, model, onExit) {
|
|
52
|
+
this.cwd = cwd;
|
|
53
|
+
this.model = model;
|
|
54
|
+
this.child = spawn(bin, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
55
|
+
this.child.stdin.on("error", () => undefined);
|
|
56
|
+
this.child.stdout.on("data", (chunk) => this.read(chunk));
|
|
57
|
+
this.child.stderr.on("data", (chunk) => {
|
|
58
|
+
this.stderrTail = (this.stderrTail + chunk.toString("utf8")).slice(-4000);
|
|
59
|
+
});
|
|
60
|
+
const finish = (code) => {
|
|
61
|
+
if (this.exited)
|
|
62
|
+
return;
|
|
63
|
+
this.exited = true;
|
|
64
|
+
this.exitCode = code;
|
|
65
|
+
if (this.idleTimer)
|
|
66
|
+
clearTimeout(this.idleTimer);
|
|
67
|
+
for (const listener of this.exitListeners)
|
|
68
|
+
listener();
|
|
69
|
+
onExit(this);
|
|
70
|
+
};
|
|
71
|
+
this.child.on("close", (code) => finish(code));
|
|
72
|
+
this.child.on("error", (err) => {
|
|
73
|
+
this.stderrTail += `\n${String(err)}`;
|
|
74
|
+
finish(-1);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
read(chunk) {
|
|
78
|
+
this.buffered += chunk.toString("utf8");
|
|
79
|
+
let idx;
|
|
80
|
+
while ((idx = this.buffered.indexOf("\n")) >= 0) {
|
|
81
|
+
const raw = this.buffered.slice(0, idx).trim();
|
|
82
|
+
this.buffered = this.buffered.slice(idx + 1);
|
|
83
|
+
if (!raw)
|
|
84
|
+
continue;
|
|
85
|
+
let record;
|
|
86
|
+
try {
|
|
87
|
+
record = JSON.parse(raw);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
for (const listener of this.listeners)
|
|
93
|
+
listener(record);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
onRecord(listener) {
|
|
97
|
+
this.listeners.add(listener);
|
|
98
|
+
return () => this.listeners.delete(listener);
|
|
99
|
+
}
|
|
100
|
+
onExit(listener) {
|
|
101
|
+
this.exitListeners.add(listener);
|
|
102
|
+
return () => this.exitListeners.delete(listener);
|
|
103
|
+
}
|
|
104
|
+
sendUserMessage(text) {
|
|
105
|
+
this.child.stdin.write(`${JSON.stringify({ type: "user", message: { role: "user", content: text } })}\n`);
|
|
106
|
+
}
|
|
107
|
+
/** Stops the running reply; the process stays usable. Falls back to killing it. */
|
|
108
|
+
interrupt() {
|
|
109
|
+
if (this.exited)
|
|
110
|
+
return Promise.resolve();
|
|
111
|
+
const requestId = `meldivo-${++this.nextRequest}`;
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
const done = () => {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
off();
|
|
116
|
+
offExit();
|
|
117
|
+
resolve();
|
|
118
|
+
};
|
|
119
|
+
const timer = setTimeout(() => {
|
|
120
|
+
this.kill();
|
|
121
|
+
done();
|
|
122
|
+
}, INTERRUPT_GRACE_MS);
|
|
123
|
+
const off = this.onRecord((record) => {
|
|
124
|
+
if (record.type === "result")
|
|
125
|
+
done();
|
|
126
|
+
});
|
|
127
|
+
const offExit = this.onExit(done);
|
|
128
|
+
this.child.stdin.write(`${JSON.stringify({ type: "control_request", request_id: requestId, request: { subtype: "interrupt" } })}\n`);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Remembers where the transcript lives once Claude Code reports the session id. */
|
|
132
|
+
learnSession(id, projectsDir) {
|
|
133
|
+
if (this.sessionId === id && this.transcript)
|
|
134
|
+
return;
|
|
135
|
+
this.sessionId = id;
|
|
136
|
+
this.transcript = findTranscript(projectsDir, id);
|
|
137
|
+
}
|
|
138
|
+
/** Whether something else wrote to this session's transcript since this process last finished a turn. */
|
|
139
|
+
changedOnDisk() {
|
|
140
|
+
const now = stampOf(this.transcript);
|
|
141
|
+
if (!now || !this.stamp)
|
|
142
|
+
return false;
|
|
143
|
+
return now.size !== this.stamp.size || now.mtimeMs !== this.stamp.mtimeMs;
|
|
144
|
+
}
|
|
145
|
+
claim() {
|
|
146
|
+
this.busy = true;
|
|
147
|
+
this.lastUsed = Date.now();
|
|
148
|
+
if (this.idleTimer)
|
|
149
|
+
clearTimeout(this.idleTimer);
|
|
150
|
+
}
|
|
151
|
+
/** Marks the end of a turn: remembers the transcript as this process left it and starts the idle clock. */
|
|
152
|
+
settle(projectsDir) {
|
|
153
|
+
this.busy = false;
|
|
154
|
+
this.lastUsed = Date.now();
|
|
155
|
+
if (!this.transcript && this.sessionId)
|
|
156
|
+
this.transcript = findTranscript(projectsDir, this.sessionId);
|
|
157
|
+
this.stamp = stampOf(this.transcript);
|
|
158
|
+
if (this.idleTimer)
|
|
159
|
+
clearTimeout(this.idleTimer);
|
|
160
|
+
this.idleTimer = setTimeout(() => this.close(), IDLE_MS);
|
|
161
|
+
this.idleTimer.unref();
|
|
162
|
+
}
|
|
163
|
+
/** Orderly shutdown: Claude Code exits when its stdin closes. */
|
|
164
|
+
close() {
|
|
165
|
+
if (this.exited)
|
|
166
|
+
return;
|
|
167
|
+
this.child.stdin.end();
|
|
168
|
+
setTimeout(() => this.kill(), INTERRUPT_GRACE_MS).unref();
|
|
169
|
+
}
|
|
170
|
+
kill() {
|
|
171
|
+
if (!this.exited)
|
|
172
|
+
this.child.kill("SIGTERM");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Keeps at most a few warm Claude Code processes, keyed by the id of the session they have open. */
|
|
176
|
+
export class ClaudePool {
|
|
177
|
+
bin;
|
|
178
|
+
bySession = new Map();
|
|
179
|
+
all = new Set();
|
|
180
|
+
constructor(bin) {
|
|
181
|
+
this.bin = bin;
|
|
182
|
+
}
|
|
183
|
+
/** An idle process that already has session `id` open with the same model and folder, if any. */
|
|
184
|
+
take(id, cwd, model) {
|
|
185
|
+
if (!id)
|
|
186
|
+
return undefined;
|
|
187
|
+
const proc = this.bySession.get(id);
|
|
188
|
+
// A process started without a model runs the session's own model, which is what a turn
|
|
189
|
+
// that names the session's recorded model asks for.
|
|
190
|
+
if (!proc || proc.exited || proc.busy || proc.cwd !== cwd || (proc.model !== undefined && proc.model !== model))
|
|
191
|
+
return undefined;
|
|
192
|
+
proc.claim();
|
|
193
|
+
return proc;
|
|
194
|
+
}
|
|
195
|
+
spawn(args, cwd, model) {
|
|
196
|
+
this.evict();
|
|
197
|
+
const proc = new ClaudeProcess(this.bin, args, cwd, model, (done) => this.forget(done));
|
|
198
|
+
this.all.add(proc);
|
|
199
|
+
return proc;
|
|
200
|
+
}
|
|
201
|
+
register(proc) {
|
|
202
|
+
if (!proc.sessionId || proc.exited)
|
|
203
|
+
return;
|
|
204
|
+
const existing = this.bySession.get(proc.sessionId);
|
|
205
|
+
if (existing && existing !== proc) {
|
|
206
|
+
if (existing.busy)
|
|
207
|
+
return;
|
|
208
|
+
existing.close();
|
|
209
|
+
}
|
|
210
|
+
this.bySession.set(proc.sessionId, proc);
|
|
211
|
+
}
|
|
212
|
+
forget(proc) {
|
|
213
|
+
this.all.delete(proc);
|
|
214
|
+
for (const [id, entry] of this.bySession)
|
|
215
|
+
if (entry === proc)
|
|
216
|
+
this.bySession.delete(id);
|
|
217
|
+
}
|
|
218
|
+
evict() {
|
|
219
|
+
const idle = [...this.all].filter((proc) => !proc.busy && !proc.exited).sort((a, b) => a.lastUsed - b.lastUsed);
|
|
220
|
+
let excess = this.all.size + 1 - MAX_PROCESSES;
|
|
221
|
+
for (const proc of idle) {
|
|
222
|
+
if (excess <= 0)
|
|
223
|
+
break;
|
|
224
|
+
proc.close();
|
|
225
|
+
this.forget(proc);
|
|
226
|
+
excess--;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
closeAll() {
|
|
230
|
+
for (const proc of this.all)
|
|
231
|
+
proc.kill();
|
|
232
|
+
this.all.clear();
|
|
233
|
+
this.bySession.clear();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=claude-warm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-warm.js","sourceRoot":"","sources":["../../../server/src/harnesses/claude-warm.ts"],"names":[],"mappings":"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"}
|