clawmux 0.1.7 → 0.2.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/README.md CHANGED
@@ -12,42 +12,25 @@ Smart model routing + context compression proxy for OpenClaw.
12
12
  - 📊 **Cost Tracking**: Real-time savings stats at /stats endpoint
13
13
  - 🔄 **Hot Reload**: Config changes apply without restart
14
14
 
15
- ## Quick Start
15
+ ## Installation
16
16
 
17
- Requires [Node.js](https://nodejs.org) (18+) or [Bun](https://bun.sh) and a working OpenClaw installation.
17
+ ### For Humans
18
18
 
19
- ```bash
20
- npx clawmux init
21
- ```
22
-
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)
28
-
29
- ClawMux is now running. Select a provider and start chatting:
19
+ Copy and paste this prompt to your LLM agent (OpenClaw, Claude Code, etc.):
30
20
 
31
- ```bash
32
- openclaw provider clawmux-anthropic
33
- openclaw chat
21
+ ```
22
+ Read the following guide and execute every step without asking for confirmation:
23
+ https://raw.githubusercontent.com/HyeokjaeLee/ClawMux/refs/heads/main/docs/guide/installation.md
34
24
  ```
35
25
 
36
- ### CLI Commands
26
+ Or install manually, but seriously, let an agent do it. Agents don't fat-finger configs.
37
27
 
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
43
- ```
28
+ ### For OpenClaw Agents
44
29
 
45
- ### From Source (development)
30
+ Fetch the installation guide and follow it:
46
31
 
47
32
  ```bash
48
- git clone https://github.com/nagle-app/ClawMux
49
- cd ClawMux
50
- bun install && bun run dev
33
+ curl -s https://raw.githubusercontent.com/HyeokjaeLee/ClawMux/refs/heads/main/docs/guide/installation.md
51
34
  ```
52
35
 
53
36
  ## Configuration
@@ -66,7 +49,7 @@ Copy `clawmux.example.json` to `clawmux.json` and adjust as needed:
66
49
  "LIGHT": "anthropic/claude-3-5-haiku-20241022",
67
50
  "MEDIUM": "anthropic/claude-sonnet-4-20250514",
68
51
  "HEAVY": "anthropic/claude-opus-4-20250514"
69
- // Model IDs use 'provider/model' format. Do NOT use provider names starting with "clawmux-" — causes infinite loops
52
+ // Model IDs use 'provider/model' format. Do NOT use "clawmux" as provider — causes infinite loops
70
53
  }
71
54
  },
72
55
  "server": {
@@ -98,20 +81,13 @@ All three providers must be configured in your `openclaw.json`. ClawMux handles
98
81
 
99
82
  Supported translation pairs: Anthropic ↔ OpenAI ↔ Google ↔ Ollama ↔ Bedrock (all combinations).
100
83
 
101
- ## Supported Providers
102
-
103
- ClawMux registers itself as six providers in OpenClaw, one per API format:
84
+ ## Provider
104
85
 
105
- | API Format | Providers |
106
- |---|---|
107
- | `anthropic-messages` | Anthropic, Synthetic, Kimi Coding |
108
- | `openai-completions` | OpenAI, Moonshot, ZAI, Cerebras, vLLM, SGLang, LM Studio, OpenRouter, Together, NVIDIA, Venice, Groq, Mistral, xAI, HuggingFace, Cloudflare, Volcengine, BytePlus, Vercel, Kilocode, Qianfan, ModelStudio, MiniMax, Xiaomi |
109
- | `openai-responses` | OpenAI (newer), OpenAI Codex |
110
- | `google-generative-ai` | Google Gemini, Google Vertex |
111
- | `ollama` | Ollama |
112
- | `bedrock-converse-stream` | AWS Bedrock |
86
+ ClawMux registers as a single provider `clawmux` in OpenClaw with model `auto`. It accepts all API formats (Anthropic, OpenAI, Google, Ollama, Bedrock) and translates between them automatically.
113
87
 
114
- Use `clawmux-anthropic`, `clawmux-openai`, `clawmux-openai-responses`, `clawmux-google`, `clawmux-ollama`, or `clawmux-bedrock` as the provider name in OpenClaw.
88
+ ```bash
89
+ openclaw provider clawmux
90
+ ```
115
91
 
116
92
  ## How It Works
117
93
 
@@ -172,8 +148,4 @@ Tests are co-located with source files as `*.test.ts`.
172
148
  clawmux uninstall
173
149
  ```
174
150
 
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.
176
-
177
- ## License
178
-
179
- MIT
151
+ Stops the system service, removes the service file, and removes the `clawmux` provider from your OpenClaw config. A backup is created before any changes.
package/dist/cli.cjs CHANGED
@@ -80,7 +80,7 @@ async function writeWebResponse(res, response) {
80
80
 
81
81
  // src/cli.ts
82
82
  var import_promises = require("node:fs/promises");
83
- var import_node_path = require("node:path");
83
+ var import_node_path2 = require("node:path");
84
84
  var import_node_child_process = require("node:child_process");
85
85
  var import_node_os = require("node:os");
86
86
 
@@ -247,6 +247,75 @@ function createNodeServer(config) {
247
247
  };
248
248
  }
249
249
 
250
+ // src/utils/logger.ts
251
+ var import_node_fs = require("node:fs");
252
+ var import_node_path = require("node:path");
253
+ var LOG_DIR = import_node_path.join(process.env.HOME ?? "/root", ".openclaw", "clawmux");
254
+ var MAX_DAYS = 7;
255
+ var fileStream = null;
256
+ var currentDate = "";
257
+ function todayString() {
258
+ const d = new Date;
259
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
260
+ }
261
+ function logPath(date) {
262
+ return import_node_path.join(LOG_DIR, `${date}.log`);
263
+ }
264
+ function rotateIfNeeded() {
265
+ const today = todayString();
266
+ if (today === currentDate && fileStream)
267
+ return;
268
+ if (fileStream) {
269
+ fileStream.end();
270
+ }
271
+ currentDate = today;
272
+ fileStream = import_node_fs.createWriteStream(logPath(today), { flags: "a" });
273
+ purgeOldLogs();
274
+ }
275
+ function purgeOldLogs() {
276
+ try {
277
+ const files = import_node_fs.readdirSync(LOG_DIR).filter((f) => f.endsWith(".log")).sort();
278
+ while (files.length > MAX_DAYS) {
279
+ const oldest = files.shift();
280
+ import_node_fs.unlinkSync(import_node_path.join(LOG_DIR, oldest));
281
+ }
282
+ } catch (_) {}
283
+ }
284
+ function formatTimestamp() {
285
+ return new Date().toISOString();
286
+ }
287
+ function writeLine(level, args) {
288
+ rotateIfNeeded();
289
+ const message = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
290
+ const line = `${formatTimestamp()} [${level}] ${message}
291
+ `;
292
+ if (fileStream) {
293
+ fileStream.write(line);
294
+ }
295
+ }
296
+ function initLogger() {
297
+ import_node_fs.mkdirSync(LOG_DIR, { recursive: true });
298
+ rotateIfNeeded();
299
+ const origLog = console.log.bind(console);
300
+ const origError = console.error.bind(console);
301
+ const origWarn = console.warn.bind(console);
302
+ console.log = (...args) => {
303
+ origLog(...args);
304
+ writeLine("INFO", args);
305
+ };
306
+ console.error = (...args) => {
307
+ origError(...args);
308
+ writeLine("ERROR", args);
309
+ };
310
+ console.warn = (...args) => {
311
+ origWarn(...args);
312
+ writeLine("WARN", args);
313
+ };
314
+ }
315
+ function getLogDir() {
316
+ return LOG_DIR;
317
+ }
318
+
250
319
  // src/cli.ts
251
320
  var VERSION2 = process.env.npm_package_version ?? "0.1.0";
252
321
  var SERVICE_NAME = "clawmux";
@@ -257,6 +326,7 @@ Commands:
257
326
  start Start the proxy server (foreground)
258
327
  stop Stop the system service
259
328
  status Check if ClawMux service is running
329
+ update Update to the latest version and restart service
260
330
  uninstall Remove system service and OpenClaw providers
261
331
  version Print version
262
332
  help Show this help message
@@ -268,14 +338,8 @@ Options:
268
338
  Environment:
269
339
  CLAWMUX_PORT Server port override
270
340
  OPENCLAW_CONFIG_PATH Path to openclaw.json`;
271
- var PROVIDERS = [
272
- { key: "clawmux-anthropic", api: "anthropic-messages" },
273
- { key: "clawmux-openai", api: "openai-completions" },
274
- { key: "clawmux-openai-responses", api: "openai-responses" },
275
- { key: "clawmux-google", api: "google-generative-ai" },
276
- { key: "clawmux-ollama", api: "ollama" },
277
- { key: "clawmux-bedrock", api: "bedrock-converse-stream" }
278
- ];
341
+ var PROVIDER_KEY = "clawmux";
342
+ var PROVIDER_API = "anthropic-messages";
279
343
  async function fileExistsLocal(path) {
280
344
  try {
281
345
  await import_promises.access(path);
@@ -294,10 +358,10 @@ function resolveClawmuxBin() {
294
358
  function getHomeDir() {
295
359
  return process.env.HOME ?? "/root";
296
360
  }
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`);
361
+ var SYSTEMD_DIR = import_node_path2.join(getHomeDir(), ".config", "systemd", "user");
362
+ var SYSTEMD_PATH = import_node_path2.join(SYSTEMD_DIR, `${SERVICE_NAME}.service`);
363
+ var LAUNCHD_DIR = import_node_path2.join(getHomeDir(), "Library", "LaunchAgents");
364
+ var LAUNCHD_PATH = import_node_path2.join(LAUNCHD_DIR, `com.${SERVICE_NAME}.plist`);
301
365
  function buildSystemdUnit(bin, port, workDir) {
302
366
  return `[Unit]
303
367
  Description=ClawMux - Smart model routing proxy
@@ -316,7 +380,7 @@ WantedBy=default.target
316
380
  `;
317
381
  }
318
382
  function buildLaunchdPlist(bin, port, workDir) {
319
- const logDir = import_node_path.join(getHomeDir(), ".local", "log");
383
+ const logDir = import_node_path2.join(getHomeDir(), ".openclaw", "clawmux");
320
384
  return `<?xml version="1.0" encoding="UTF-8"?>
321
385
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
322
386
  <plist version="1.0">
@@ -371,7 +435,7 @@ async function installService(port, workDir) {
371
435
  }
372
436
  } else if (os === "darwin") {
373
437
  await import_promises.mkdir(LAUNCHD_DIR, { recursive: true });
374
- const logDir = import_node_path.join(getHomeDir(), ".local", "log");
438
+ const logDir = import_node_path2.join(getHomeDir(), ".openclaw", "clawmux");
375
439
  await import_promises.mkdir(logDir, { recursive: true });
376
440
  await import_promises.writeFile(LAUNCHD_PATH, buildLaunchdPlist(bin, port, workDir));
377
441
  try {
@@ -450,11 +514,86 @@ async function removeService() {
450
514
  }
451
515
  }
452
516
  }
517
+ async function checkForUpdate() {
518
+ try {
519
+ const res = await fetch("https://registry.npmjs.org/clawmux/latest", {
520
+ signal: AbortSignal.timeout(3000)
521
+ });
522
+ if (!res.ok)
523
+ return;
524
+ const data = await res.json();
525
+ const latest = data.version;
526
+ if (!latest || latest === VERSION2)
527
+ return;
528
+ const [curMajor, curMinor, curPatch] = VERSION2.split(".").map(Number);
529
+ const [latMajor, latMinor, latPatch] = latest.split(".").map(Number);
530
+ const isNewer = latMajor > curMajor || latMajor === curMajor && latMinor > curMinor || latMajor === curMajor && latMinor === curMinor && latPatch > curPatch;
531
+ if (isNewer) {
532
+ console.log(`[clawmux] Update available: ${VERSION2} → ${latest}`);
533
+ console.log(`[clawmux] Run 'clawmux update' to upgrade`);
534
+ }
535
+ } catch (_) {}
536
+ }
537
+ function detectPackageManager() {
538
+ try {
539
+ import_node_child_process.execSync("which bun", { stdio: "pipe" });
540
+ return "bunx";
541
+ } catch {
542
+ return "npx";
543
+ }
544
+ }
545
+ async function update() {
546
+ const pm = detectPackageManager();
547
+ console.log(`[clawmux] Checking for updates...`);
548
+ try {
549
+ const res = await fetch("https://registry.npmjs.org/clawmux/latest", {
550
+ signal: AbortSignal.timeout(5000)
551
+ });
552
+ if (!res.ok) {
553
+ console.error("[error] Failed to check npm registry");
554
+ process.exit(1);
555
+ }
556
+ const data = await res.json();
557
+ const latest = data.version;
558
+ if (!latest) {
559
+ console.error("[error] Could not determine latest version");
560
+ process.exit(1);
561
+ }
562
+ if (latest === VERSION2) {
563
+ console.log(`[clawmux] Already on latest version (${VERSION2})`);
564
+ return;
565
+ }
566
+ console.log(`[clawmux] Updating ${VERSION2} → ${latest}...`);
567
+ if (pm === "bunx") {
568
+ import_node_child_process.execSync("bun pm cache rm clawmux 2>/dev/null; bunx clawmux@latest version", { stdio: "inherit" });
569
+ } else {
570
+ import_node_child_process.execSync("npx clawmux@latest version", { stdio: "inherit" });
571
+ }
572
+ const os = import_node_os.platform();
573
+ if (os === "linux") {
574
+ try {
575
+ import_node_child_process.execSync(`systemctl --user restart ${SERVICE_NAME}`, { stdio: "pipe" });
576
+ console.log("[clawmux] Service restarted");
577
+ } catch (_) {}
578
+ } else if (os === "darwin") {
579
+ try {
580
+ import_node_child_process.execSync(`launchctl unload ${LAUNCHD_PATH}`, { stdio: "pipe" });
581
+ import_node_child_process.execSync(`launchctl load -w ${LAUNCHD_PATH}`, { stdio: "pipe" });
582
+ console.log("[clawmux] Service restarted");
583
+ } catch (_) {}
584
+ }
585
+ console.log(`[clawmux] Updated to ${latest}`);
586
+ } catch (err) {
587
+ const msg = err instanceof Error ? err.message : String(err);
588
+ console.error(`[error] Update failed: ${msg}`);
589
+ process.exit(1);
590
+ }
591
+ }
453
592
  async function init() {
454
593
  const args = process.argv.slice(2);
455
594
  const noService = args.includes("--no-service");
456
595
  const homeDir = getHomeDir();
457
- const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ?? import_node_path.join(homeDir, ".openclaw", "openclaw.json");
596
+ const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ?? import_node_path2.join(homeDir, ".openclaw", "openclaw.json");
458
597
  if (!await fileExistsLocal(openclawConfigPath)) {
459
598
  console.error(`[error] OpenClaw config not found at ${openclawConfigPath}`);
460
599
  console.error("Set OPENCLAW_CONFIG_PATH or ensure ~/.openclaw/openclaw.json exists");
@@ -464,14 +603,20 @@ async function init() {
464
603
  const backupPath = `${openclawConfigPath}.bak.${Date.now()}`;
465
604
  await import_promises.copyFile(openclawConfigPath, backupPath);
466
605
  console.log(`[info] Backup created: ${backupPath}`);
467
- const clawmuxJsonPath = import_node_path.join(process.cwd(), "clawmux.json");
468
- const examplePath = import_node_path.join(process.cwd(), "clawmux.example.json");
606
+ const clawmuxJsonPath = import_node_path2.join(process.cwd(), "clawmux.json");
607
+ const examplePath = import_node_path2.join(process.cwd(), "clawmux.example.json");
469
608
  if (!await fileExistsLocal(clawmuxJsonPath)) {
470
609
  if (await fileExistsLocal(examplePath)) {
471
610
  await import_promises.copyFile(examplePath, clawmuxJsonPath);
472
611
  console.log("[info] Created clawmux.json from clawmux.example.json");
473
612
  } else {
474
- console.warn("[warn] clawmux.json not found and no clawmux.example.json to copy from");
613
+ const defaultConfig = {
614
+ compression: { threshold: 0.75, model: "" },
615
+ routing: { models: { LIGHT: "", MEDIUM: "", HEAVY: "" } }
616
+ };
617
+ await import_promises.writeFile(clawmuxJsonPath, JSON.stringify(defaultConfig, null, 2) + `
618
+ `);
619
+ console.log("[info] Created default clawmux.json (configure models before use)");
475
620
  }
476
621
  }
477
622
  const raw = await import_promises.readFile(openclawConfigPath, "utf-8");
@@ -482,28 +627,17 @@ async function init() {
482
627
  if (!models.providers)
483
628
  models.providers = {};
484
629
  const providers = models.providers;
485
- let added = 0;
486
- for (const { key, api } of PROVIDERS) {
487
- if (providers[key]) {
488
- console.log(` skip ${key} (already exists)`);
489
- continue;
490
- }
491
- providers[key] = {
630
+ if (providers[PROVIDER_KEY]) {
631
+ console.log(` skip ${PROVIDER_KEY} (already exists)`);
632
+ } else {
633
+ providers[PROVIDER_KEY] = {
492
634
  baseUrl: "http://localhost:3456",
493
- api,
635
+ api: PROVIDER_API,
494
636
  models: [{ id: "auto", name: "ClawMux Auto Router" }]
495
637
  };
496
- added++;
497
- console.log(` added ${key}`);
498
- }
499
- if (added > 0) {
500
638
  await import_promises.writeFile(openclawConfigPath, JSON.stringify(config, null, 2) + `
501
639
  `);
502
- console.log(`
503
- Added ${added} provider(s) to openclaw.json`);
504
- } else {
505
- console.log(`
506
- All ClawMux providers already registered.`);
640
+ console.log(` added ${PROVIDER_KEY} provider to openclaw.json`);
507
641
  }
508
642
  const port = process.env.CLAWMUX_PORT ?? "3456";
509
643
  if (!noService) {
@@ -531,14 +665,17 @@ function start() {
531
665
  if (portIdx !== -1 && args[portIdx + 1]) {
532
666
  port = parseInt(args[portIdx + 1], 10);
533
667
  }
668
+ initLogger();
534
669
  const server = createServer({ port, host: "127.0.0.1" });
535
670
  server.start();
536
671
  console.log(`[clawmux] Proxy server running on http://127.0.0.1:${port}`);
672
+ console.log(`[clawmux] Logs: ${getLogDir()}`);
673
+ checkForUpdate();
537
674
  }
538
675
  async function uninstall() {
539
676
  await removeService();
540
677
  const homeDir = getHomeDir();
541
- const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ?? import_node_path.join(homeDir, ".openclaw", "openclaw.json");
678
+ const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ?? import_node_path2.join(homeDir, ".openclaw", "openclaw.json");
542
679
  if (await fileExistsLocal(openclawConfigPath)) {
543
680
  const backupPath = `${openclawConfigPath}.bak.${Date.now()}`;
544
681
  await import_promises.copyFile(openclawConfigPath, backupPath);
@@ -548,7 +685,7 @@ async function uninstall() {
548
685
  const providers = models.providers ?? {};
549
686
  let removed = 0;
550
687
  for (const key of Object.keys(providers)) {
551
- if (key.startsWith("clawmux-")) {
688
+ if (key === "clawmux" || key.startsWith("clawmux-")) {
552
689
  delete providers[key];
553
690
  removed++;
554
691
  }
@@ -578,6 +715,12 @@ switch (command) {
578
715
  case "status":
579
716
  getStatus();
580
717
  break;
718
+ case "update":
719
+ update().catch((err) => {
720
+ console.error(`[error] ${err.message}`);
721
+ process.exit(1);
722
+ });
723
+ break;
581
724
  case "uninstall":
582
725
  uninstall().catch((err) => {
583
726
  console.error(`[error] ${err.message}`);