clawmux 0.1.0 → 0.1.5

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HyeokjaeLee
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 CHANGED
@@ -14,36 +14,40 @@ Smart model routing + context compression proxy for OpenClaw.
14
14
 
15
15
  ## Quick Start
16
16
 
17
- Requires [Bun](https://bun.sh) or [Node.js](https://nodejs.org) (18+) and a working OpenClaw installation.
17
+ Requires [Node.js](https://nodejs.org) (18+) or [Bun](https://bun.sh) and a working OpenClaw installation.
18
18
 
19
19
  ```bash
20
- # Clone and install
21
- git clone https://github.com/your-org/ClawMux
22
- cd ClawMux
23
- bash scripts/install.sh
20
+ npx clawmux init
24
21
  ```
25
22
 
26
- The install script:
27
- 1. Detects your OpenClaw config at `~/.openclaw/openclaw.json` (override with `OPENCLAW_CONFIG_PATH`)
28
- 2. Creates `clawmux.json` from the example if it doesn't exist
29
- 3. Registers ClawMux as a provider in your OpenClaw config
23
+ This will:
24
+ 1. Detect your OpenClaw config at `~/.openclaw/openclaw.json`
25
+ 2. Create `clawmux.json` from the default template
26
+ 3. Register 6 ClawMux providers in your OpenClaw config
27
+ 4. Install a system service for auto-start on boot (systemd on Linux, launchd on macOS)
30
28
 
31
- Then start the proxy:
29
+ ClawMux is now running. Select a provider and start chatting:
32
30
 
33
31
  ```bash
34
- # Bun (recommended — faster startup & runtime)
35
- bun run dev # watch mode (development)
36
- bun run start # production
32
+ openclaw provider clawmux-anthropic
33
+ openclaw chat
34
+ ```
35
+
36
+ ### CLI Commands
37
37
 
38
- # Node.js
39
- npm run start:node # requires tsx: npm i -D tsx
38
+ ```bash
39
+ clawmux status # check if running
40
+ clawmux stop # stop the service
41
+ clawmux start # manual foreground start
42
+ clawmux uninstall # remove service + OpenClaw providers
40
43
  ```
41
44
 
42
- Select a provider in OpenClaw and start chatting:
45
+ ### From Source (development)
43
46
 
44
47
  ```bash
45
- openclaw provider clawmux-anthropic
46
- openclaw chat
48
+ git clone https://github.com/nagle-app/ClawMux
49
+ cd ClawMux
50
+ bun install && bun run dev
47
51
  ```
48
52
 
49
53
  ## Configuration
@@ -63,9 +67,6 @@ Copy `clawmux.example.json` to `clawmux.json` and adjust as needed:
63
67
  "MEDIUM": "anthropic/claude-sonnet-4-20250514",
64
68
  "HEAVY": "anthropic/claude-opus-4-20250514"
65
69
  // Model IDs use 'provider/model' format. Do NOT use provider names starting with "clawmux-" — causes infinite loops
66
- },
67
- "scoring": {
68
- "confidenceThreshold": 0.7 // classification confidence below this → fallback to MEDIUM tier
69
70
  }
70
71
  },
71
72
  "server": {
@@ -125,9 +126,9 @@ OpenClaw → ClawMux Proxy (localhost:3456) → Upstream Provider(s)
125
126
  └── 6. Translate response back to original format
126
127
  ```
127
128
 
128
- **Routing tiers** map to model IDs you configure. A local embedding model (`Xenova/paraphrase-multilingual-MiniLM-L12-v2`) classifies the semantic complexity of each request using nearest-centroid classification (~4ms first run, <1ms cached), supporting both Korean and English. Short queries are detected by a lightweight heuristic and routed to LIGHT tier directly. No external API calls are needed for classification.
129
+ **Routing tiers** map to model IDs you configure. A local embedding model (`Xenova/multilingual-e5-small`) classifies the semantic complexity of each request using nearest-centroid classification (~8ms p50), supporting both Korean and English. Short queries are detected by a lightweight heuristic and routed to LIGHT tier directly. No external API calls are needed for classification.
129
130
 
130
- **Low confidence fallback**: When the classifier's confidence falls below `confidenceThreshold` (default 0.7), the request is routed to MEDIUM tier regardless of the computed score. This prevents unreliable classifications from sending requests to an inappropriate tier — MEDIUM provides a safe cost/quality balance compared to risking unnecessary cost (HEAVY) or degraded quality (LIGHT).
131
+ **Low confidence fallback**: When the classifier's confidence is low, the request is routed to MEDIUM tier. This prevents unreliable classifications from sending requests to an inappropriate tier — MEDIUM provides a safe cost/quality balance.
131
132
 
132
133
  **Context compression** runs in the background after each response. When the conversation approaches the configured threshold, ClawMux summarizes older messages before the next request goes out. This keeps costs down on long conversations without interrupting the flow.
133
134
 
@@ -168,10 +169,10 @@ Tests are co-located with source files as `*.test.ts`.
168
169
  ## Uninstall
169
170
 
170
171
  ```bash
171
- bash scripts/uninstall.sh
172
+ clawmux uninstall
172
173
  ```
173
174
 
174
- Removes all `clawmux-*` providers from your OpenClaw config. Your original config is backed up before any changes.
175
+ Stops the system service, removes the service file, and removes all `clawmux-*` providers from your OpenClaw config. A backup is created before any changes.
175
176
 
176
177
  ## License
177
178
 
package/dist/cli.cjs CHANGED
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
3
7
  var __export = (target, all) => {
4
8
  for (var name in all)
5
9
  __defProp(target, name, {
6
10
  get: all[name],
7
11
  enumerable: true,
8
12
  configurable: true,
9
- set: (newValue) => all[name] = () => newValue
13
+ set: __exportSetter.bind(all, name)
10
14
  });
11
15
  };
12
16
 
@@ -77,6 +81,8 @@ async function writeWebResponse(res, response) {
77
81
  // src/cli.ts
78
82
  var import_promises = require("node:fs/promises");
79
83
  var import_node_path = require("node:path");
84
+ var import_node_child_process = require("node:child_process");
85
+ var import_node_os = require("node:os");
80
86
 
81
87
  // src/proxy/router.ts
82
88
  var VERSION = "0.1.0";
@@ -243,16 +249,21 @@ function createNodeServer(config) {
243
249
 
244
250
  // src/cli.ts
245
251
  var VERSION2 = process.env.npm_package_version ?? "0.1.0";
252
+ var SERVICE_NAME = "clawmux";
246
253
  var HELP = `Usage: clawmux <command>
247
254
 
248
255
  Commands:
249
- init Detect OpenClaw config, create clawmux.json, register providers
250
- start Start the proxy server (foreground)
251
- version Print version
252
- help Show this help message
256
+ init Detect OpenClaw config, register providers, install system service
257
+ start Start the proxy server (foreground)
258
+ stop Stop the system service
259
+ status Check if ClawMux service is running
260
+ uninstall Remove system service and OpenClaw providers
261
+ version Print version
262
+ help Show this help message
253
263
 
254
264
  Options:
255
265
  --port, -p <port> Override server port (default: 3456)
266
+ --no-service Skip system service installation during init
256
267
 
257
268
  Environment:
258
269
  CLAWMUX_PORT Server port override
@@ -273,8 +284,176 @@ async function fileExistsLocal(path) {
273
284
  return false;
274
285
  }
275
286
  }
287
+ function resolveClawmuxBin() {
288
+ try {
289
+ return import_node_child_process.execSync("which clawmux", { encoding: "utf-8" }).trim();
290
+ } catch {
291
+ return "npx clawmux";
292
+ }
293
+ }
294
+ function getHomeDir() {
295
+ return process.env.HOME ?? "/root";
296
+ }
297
+ var SYSTEMD_DIR = import_node_path.join(getHomeDir(), ".config", "systemd", "user");
298
+ var SYSTEMD_PATH = import_node_path.join(SYSTEMD_DIR, `${SERVICE_NAME}.service`);
299
+ var LAUNCHD_DIR = import_node_path.join(getHomeDir(), "Library", "LaunchAgents");
300
+ var LAUNCHD_PATH = import_node_path.join(LAUNCHD_DIR, `com.${SERVICE_NAME}.plist`);
301
+ function buildSystemdUnit(bin, port, workDir) {
302
+ return `[Unit]
303
+ Description=ClawMux - Smart model routing proxy
304
+ After=network.target
305
+
306
+ [Service]
307
+ Type=simple
308
+ ExecStart=${bin} start --port ${port}
309
+ WorkingDirectory=${workDir}
310
+ Restart=on-failure
311
+ RestartSec=5
312
+ Environment=CLAWMUX_PORT=${port}
313
+
314
+ [Install]
315
+ WantedBy=default.target
316
+ `;
317
+ }
318
+ function buildLaunchdPlist(bin, port, workDir) {
319
+ const logDir = import_node_path.join(getHomeDir(), ".local", "log");
320
+ return `<?xml version="1.0" encoding="UTF-8"?>
321
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
322
+ <plist version="1.0">
323
+ <dict>
324
+ <key>Label</key>
325
+ <string>com.${SERVICE_NAME}</string>
326
+ <key>ProgramArguments</key>
327
+ <array>
328
+ <string>${bin}</string>
329
+ <string>start</string>
330
+ <string>--port</string>
331
+ <string>${port}</string>
332
+ </array>
333
+ <key>WorkingDirectory</key>
334
+ <string>${workDir}</string>
335
+ <key>EnvironmentVariables</key>
336
+ <dict>
337
+ <key>CLAWMUX_PORT</key>
338
+ <string>${port}</string>
339
+ <key>PATH</key>
340
+ <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
341
+ </dict>
342
+ <key>RunAtLoad</key>
343
+ <true/>
344
+ <key>KeepAlive</key>
345
+ <true/>
346
+ <key>StandardOutPath</key>
347
+ <string>${logDir}/clawmux.log</string>
348
+ <key>StandardErrorPath</key>
349
+ <string>${logDir}/clawmux.err</string>
350
+ </dict>
351
+ </plist>
352
+ `;
353
+ }
354
+ async function installService(port, workDir) {
355
+ const bin = resolveClawmuxBin();
356
+ const os = import_node_os.platform();
357
+ if (os === "linux") {
358
+ await import_promises.mkdir(SYSTEMD_DIR, { recursive: true });
359
+ await import_promises.writeFile(SYSTEMD_PATH, buildSystemdUnit(bin, port, workDir));
360
+ try {
361
+ import_node_child_process.execSync("systemctl --user daemon-reload", { stdio: "pipe" });
362
+ import_node_child_process.execSync(`systemctl --user enable ${SERVICE_NAME}`, { stdio: "pipe" });
363
+ import_node_child_process.execSync(`systemctl --user start ${SERVICE_NAME}`, { stdio: "pipe" });
364
+ import_node_child_process.execSync("loginctl enable-linger $(whoami)", { stdio: "pipe" });
365
+ console.log("[info] systemd user service installed and started");
366
+ console.log(` Service file: ${SYSTEMD_PATH}`);
367
+ } catch (err) {
368
+ const msg = err instanceof Error ? err.message : String(err);
369
+ console.warn(`[warn] systemd setup failed: ${msg}`);
370
+ console.warn(" You can start manually: clawmux start");
371
+ }
372
+ } else if (os === "darwin") {
373
+ await import_promises.mkdir(LAUNCHD_DIR, { recursive: true });
374
+ const logDir = import_node_path.join(getHomeDir(), ".local", "log");
375
+ await import_promises.mkdir(logDir, { recursive: true });
376
+ await import_promises.writeFile(LAUNCHD_PATH, buildLaunchdPlist(bin, port, workDir));
377
+ try {
378
+ import_node_child_process.execSync(`launchctl load -w ${LAUNCHD_PATH}`, { stdio: "pipe" });
379
+ console.log("[info] launchd service installed and started");
380
+ console.log(` Plist file: ${LAUNCHD_PATH}`);
381
+ } catch (err) {
382
+ const msg = err instanceof Error ? err.message : String(err);
383
+ console.warn(`[warn] launchd setup failed: ${msg}`);
384
+ console.warn(" You can start manually: clawmux start");
385
+ }
386
+ } else {
387
+ console.warn(`[warn] Auto-start not supported on ${os}. Start manually: clawmux start`);
388
+ }
389
+ }
390
+ function stopService() {
391
+ const os = import_node_os.platform();
392
+ if (os === "linux") {
393
+ try {
394
+ import_node_child_process.execSync(`systemctl --user stop ${SERVICE_NAME}`, { stdio: "pipe" });
395
+ console.log("[info] Service stopped");
396
+ } catch {
397
+ console.log("[info] Service is not running");
398
+ }
399
+ } else if (os === "darwin") {
400
+ try {
401
+ import_node_child_process.execSync(`launchctl unload ${LAUNCHD_PATH}`, { stdio: "pipe" });
402
+ console.log("[info] Service stopped");
403
+ } catch {
404
+ console.log("[info] Service is not running");
405
+ }
406
+ } else {
407
+ console.error(`[error] Auto-start not supported on ${os}`);
408
+ }
409
+ }
410
+ function getStatus() {
411
+ const os = import_node_os.platform();
412
+ if (os === "linux") {
413
+ try {
414
+ const output = import_node_child_process.execSync(`systemctl --user is-active ${SERVICE_NAME}`, { encoding: "utf-8" }).trim();
415
+ console.log(`ClawMux service: ${output}`);
416
+ } catch {
417
+ console.log("ClawMux service: inactive");
418
+ }
419
+ } else if (os === "darwin") {
420
+ try {
421
+ const output = import_node_child_process.execSync(`launchctl list | grep com.${SERVICE_NAME}`, { encoding: "utf-8" }).trim();
422
+ console.log(output ? `ClawMux service: running
423
+ ${output}` : "ClawMux service: not loaded");
424
+ } catch {
425
+ console.log("ClawMux service: not loaded");
426
+ }
427
+ } else {
428
+ console.log(`Auto-start not supported on ${os}`);
429
+ }
430
+ }
431
+ async function removeService() {
432
+ const os = import_node_os.platform();
433
+ if (os === "linux") {
434
+ try {
435
+ import_node_child_process.execSync(`systemctl --user stop ${SERVICE_NAME}`, { stdio: "pipe" });
436
+ import_node_child_process.execSync(`systemctl --user disable ${SERVICE_NAME}`, { stdio: "pipe" });
437
+ } catch (_) {}
438
+ if (await fileExistsLocal(SYSTEMD_PATH)) {
439
+ await import_promises.unlink(SYSTEMD_PATH);
440
+ import_node_child_process.execSync("systemctl --user daemon-reload", { stdio: "pipe" });
441
+ console.log("[info] systemd service removed");
442
+ }
443
+ } else if (os === "darwin") {
444
+ try {
445
+ import_node_child_process.execSync(`launchctl unload ${LAUNCHD_PATH}`, { stdio: "pipe" });
446
+ } catch (_) {}
447
+ if (await fileExistsLocal(LAUNCHD_PATH)) {
448
+ await import_promises.unlink(LAUNCHD_PATH);
449
+ console.log("[info] launchd plist removed");
450
+ }
451
+ }
452
+ }
276
453
  async function init() {
277
- const homeDir = process.env.HOME ?? "/root";
454
+ const args = process.argv.slice(2);
455
+ const noService = args.includes("--no-service");
456
+ const homeDir = getHomeDir();
278
457
  const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ?? import_node_path.join(homeDir, ".openclaw", "openclaw.json");
279
458
  if (!await fileExistsLocal(openclawConfigPath)) {
280
459
  console.error(`[error] OpenClaw config not found at ${openclawConfigPath}`);
@@ -326,12 +505,22 @@ Added ${added} provider(s) to openclaw.json`);
326
505
  console.log(`
327
506
  All ClawMux providers already registered.`);
328
507
  }
508
+ const port = process.env.CLAWMUX_PORT ?? "3456";
509
+ if (!noService) {
510
+ console.log("");
511
+ await installService(port, process.cwd());
512
+ }
329
513
  console.log(`
330
- [info] ClawMux provider registration complete!`);
514
+ [info] ClawMux setup complete!`);
331
515
  console.log(`
332
516
  Next steps:`);
333
517
  console.log(" 1. Edit clawmux.json to configure your models");
334
- console.log(" 2. Run: clawmux start");
518
+ if (noService) {
519
+ console.log(" 2. Run: clawmux start");
520
+ } else {
521
+ console.log(" 2. ClawMux is running and will auto-start on boot");
522
+ console.log(" Check status: clawmux status");
523
+ }
335
524
  console.log(" 3. Select a provider: openclaw provider clawmux-openai");
336
525
  console.log(" 4. Start chatting: openclaw chat");
337
526
  }
@@ -346,6 +535,32 @@ function start() {
346
535
  server.start();
347
536
  console.log(`[clawmux] Proxy server running on http://127.0.0.1:${port}`);
348
537
  }
538
+ async function uninstall() {
539
+ await removeService();
540
+ const homeDir = getHomeDir();
541
+ const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ?? import_node_path.join(homeDir, ".openclaw", "openclaw.json");
542
+ if (await fileExistsLocal(openclawConfigPath)) {
543
+ const backupPath = `${openclawConfigPath}.bak.${Date.now()}`;
544
+ await import_promises.copyFile(openclawConfigPath, backupPath);
545
+ const raw = await import_promises.readFile(openclawConfigPath, "utf-8");
546
+ const config = JSON.parse(raw);
547
+ const models = config.models ?? {};
548
+ const providers = models.providers ?? {};
549
+ let removed = 0;
550
+ for (const key of Object.keys(providers)) {
551
+ if (key.startsWith("clawmux-")) {
552
+ delete providers[key];
553
+ removed++;
554
+ }
555
+ }
556
+ if (removed > 0) {
557
+ await import_promises.writeFile(openclawConfigPath, JSON.stringify(config, null, 2) + `
558
+ `);
559
+ console.log(`[info] Removed ${removed} ClawMux provider(s) from openclaw.json`);
560
+ }
561
+ }
562
+ console.log("[info] ClawMux uninstalled");
563
+ }
349
564
  var command = process.argv[2];
350
565
  switch (command) {
351
566
  case "init":
@@ -357,6 +572,18 @@ switch (command) {
357
572
  case "start":
358
573
  start();
359
574
  break;
575
+ case "stop":
576
+ stopService();
577
+ break;
578
+ case "status":
579
+ getStatus();
580
+ break;
581
+ case "uninstall":
582
+ uninstall().catch((err) => {
583
+ console.error(`[error] ${err.message}`);
584
+ process.exit(1);
585
+ });
586
+ break;
360
587
  case "version":
361
588
  case "--version":
362
589
  case "-v":
package/dist/index.cjs CHANGED
@@ -1,11 +1,15 @@
1
1
  var __defProp = Object.defineProperty;
2
+ var __returnValue = (v) => v;
3
+ function __exportSetter(name, newValue) {
4
+ this[name] = __returnValue.bind(null, newValue);
5
+ }
2
6
  var __export = (target, all) => {
3
7
  for (var name in all)
4
8
  __defProp(target, name, {
5
9
  get: all[name],
6
10
  enumerable: true,
7
11
  configurable: true,
8
- set: (newValue) => all[name] = () => newValue
12
+ set: __exportSetter.bind(all, name)
9
13
  });
10
14
  };
11
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmux",
3
- "version": "0.1.0",
3
+ "version": "0.1.5",
4
4
  "description": "Smart model routing + context compression proxy for OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "repository": {
36
36
  "type": "git",
37
- "url": "git+https://github.com/nagle-app/ClawMux.git"
37
+ "url": "git+https://github.com/HyeokjaeLee/ClawMux.git"
38
38
  },
39
39
  "keywords": [
40
40
  "llm",