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 CHANGED
@@ -1,262 +1,78 @@
1
- # meldivo
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
- **Always-on local voice hub for coding agents.** Install it once, and talk by
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
- ```bash
11
- npm i -g meldivo
12
- meldivo start
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
- - Node.js 22 or later
58
- - macOS or Linux (x64 or arm64)
59
- - At least one of: Pi, OpenCode, Claude Code
60
- - About 650 MB of disk space for the speech models, downloaded on first use
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
- ## Supported agents
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
- | Agent | Sessions found from | "Open in terminal" | Tool approvals |
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
- Pi is listed first on the hub page, since it's the reference agent for this
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
- On each other machine (with meldivo installed and started):
26
+ **1. Install**
130
27
 
131
- ```sh
132
- meldivo join http://<hub-address>:4100 <code>
28
+ ```bash
29
+ npm i -g meldivo
133
30
  ```
134
31
 
135
- The hub address must be reachable from that machine, ideally over a private
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
- Because every reply is spoken, meldivo adds a short note to each message asking
163
- for a brief, plain answer and a one-sentence heads-up before any tool call, so
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
- Breeze TTS 2 designs a voice from a description or clones one from a short
199
- clip. Save the voice you like on the Breeze server once (its `POST /v1/voices`
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
- All settings are optional.
41
+ **3. Talk**
211
42
 
212
- | Variable | Default | Purpose |
213
- |---|---|---|
214
- | `MELDIVO_PORT` | `4100` | Port of the local server (`127.0.0.1` only). |
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
- ## Files and locations
47
+ That's it. No API keys, no plugins, no cloud.
221
48
 
222
- | Path | Contents |
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
- ## Security notes
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
- The server listens on `127.0.0.1` only; it is never exposed on the network
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
- ## Development
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
- ```bash
252
- npm install
253
- npm test
254
- ```
69
+ ## Documentation
255
70
 
256
- See [CONTRIBUTING.md](CONTRIBUTING.md) for the project layout and
257
- conventions.
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). Third-party software and model licenses are
262
- listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
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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"}