mobygate 0.7.3 → 0.8.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,186 @@ All notable changes to mobygate are documented here. Format loosely follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version numbers are
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.8.1] — 2026-04-27
8
+
9
+ Diagnostic visibility release. Adds a request/response capture system,
10
+ a session inspector UI, a `mobygate tail` CLI, and fixes the v0.8.0
11
+ connector idempotency bug. Born out of an OpenClaw debugging session
12
+ where it took an hour of forensics to discover OpenClaw was using
13
+ OpenAI shape (no cache_control) instead of `/v1/messages` — a one-line
14
+ config flip — because mobygate had no visibility into what its
15
+ upstream clients were actually sending.
16
+
17
+ ### Added
18
+
19
+ - **Request/response capture system** (`lib/request-capture.js`).
20
+ - Off by default. Three ways to enable:
21
+ 1. Env var: `MOBY_CAPTURE=1 mobygate start`
22
+ 2. Touch file: `touch ~/.mobygate/.capture-enabled` (survives
23
+ restarts, dashboard-toggleable)
24
+ 3. Dashboard inspector toggle button
25
+ - Writes paired files per request to `~/.mobygate/captures/`:
26
+ - `{ts}_{path}_{requestId}.json` — raw inbound body
27
+ - `{ts}_{path}_{requestId}.summary.txt` — human-readable analysis
28
+ (system blocks, cache_control markers, message timeline, tool
29
+ definitions, token breakdown)
30
+ - On response: appends actual usage from the SDK including
31
+ `cache_read_input_tokens`, `cache_creation_input_tokens`,
32
+ `input_tokens`, `output_tokens`, plus computed cache hit % and
33
+ "savings vs uncached" estimate.
34
+ - Auto-rotation: keeps last `MOBY_CAPTURE_KEEP=100` captures (200
35
+ files), prunes oldest in the background.
36
+
37
+ - **Session inspector UI** at `/inspector`. New focused dashboard for
38
+ browsing captures:
39
+ - List view: all recent captures sorted newest-first, one-line stats
40
+ (timestamp, path, model, msgs, tokens, cache hit %), live polling
41
+ every 3s.
42
+ - Detail view: full request body decoded — system blocks (with
43
+ cache_control highlighted in green), messages timeline (color-coded
44
+ by role, tool_use/tool_result expanded with previews), tool
45
+ definitions, response usage stats.
46
+ - Capture toggle in header — flips the touch file live without
47
+ restarting mobygate.
48
+ - Linked from the main dashboard's endpoints row.
49
+
50
+ - **`mobygate tail` CLI** — `tail -f` style live view of captures as
51
+ they land. Shows last 10 historical entries, then watches the
52
+ captures dir for new files. Color-codes `/v1/messages` (green,
53
+ native shape) vs `/v1/chat/completions` (yellow, OpenAI shape) so
54
+ shape mismatches jump off the screen.
55
+
56
+ - **`extractSdkUsage()` helper** in `lib/anthropic.js` — defensive
57
+ extraction of cache + token fields from SDK result messages,
58
+ handles both flat (`message.input_tokens`) and nested
59
+ (`message.usage.input_tokens`) shapes from the Claude Agent SDK.
60
+ Used by all 4 mobygate handlers (the OpenAI streaming handler also
61
+ now tracks usage where it didn't before).
62
+
63
+ - **Connector migration detection** — `openclawConnector.inspect()`
64
+ now reports "shadow providers": entries in OpenClaw's config that
65
+ point at mobygate's base URL but aren't registered under our
66
+ canonical `moby` / `moby-native` names. Catches pre-v0.8.0
67
+ hand-rolled `claude-max-proxy`-style configs (which silently bypass
68
+ the native surface). Returned as `shadowProviders: [{ name, api,
69
+ baseUrl, recommendation }]` in inspect output.
70
+
71
+ - **Cache endpoints for the dashboard:**
72
+ - `GET /dashboard/captures` — list captures with summary stats
73
+ - `GET /dashboard/captures/:filename` — full body + summary for one
74
+ capture
75
+ - `GET /dashboard/captures-state` — current toggle state
76
+ - `POST /dashboard/captures-toggle` — flip toggle live
77
+ - All require local-origin (DNS-rebinding protection).
78
+
79
+ ### Fixed
80
+
81
+ - **Connector idempotency** (v0.8.0 OPEN bug). `mobygate connect <id>`
82
+ run twice with no real change was producing spurious "(changed)"
83
+ diffs and rewriting byte-identical files. Root cause: `diffSummary`
84
+ walks objects structurally (sensitive to key order); the actual
85
+ serialized JSON output was identical. Fix: `writeConfigSafe` now
86
+ byte-compares the rendered output against the existing file content
87
+ before deciding to write. Returns `unchanged: true` if no real
88
+ change. Both `hermesConnector.apply()` and
89
+ `openclawConnector.apply()` updated to surface this flag.
90
+
91
+ - **Tool name display in capture analyzer**. OpenAI-shape tools nest
92
+ the name under `tool.function.name`; the analyzer was only checking
93
+ `tool.name` and showing `(unnamed)` for everything. Now handles
94
+ both shapes via the new `toolName(t)` helper.
95
+
96
+ ### Notes
97
+
98
+ The capture system was the lever that made the entire OpenClaw
99
+ debugging session productive. Without it we had no visibility into
100
+ which wire format clients were using, whether `cache_control` markers
101
+ were being set, where in the request the breakpoints landed, or how
102
+ the conversation was growing turn-over-turn. With it, the diagnosis
103
+ took 60 seconds: "this is `/v1/chat/completions`, no `system` array,
104
+ no cache markers, 33k tokens per turn."
105
+
106
+ The migration detection in `openclawConnector.inspect()` is a small
107
+ prevent-the-next-occurrence add. If `inspect()` had reported
108
+ `claude-max-proxy` as a shadow provider with a "flip api to
109
+ anthropic-messages" recommendation, today's hour-long forensics would
110
+ have been a single CLI command.
111
+
112
+ ## [0.8.0] — 2026-04-25
113
+
114
+ Auto-wire third-party clients to use mobygate. The "find your client's
115
+ config file → paste this JSON → restart" dance is replaced by
116
+ `mobygate connect`.
117
+
118
+ ### Added
119
+
120
+ - **`mobygate connect` CLI** — auto-detects supported clients on the
121
+ system, plans the minimal config edit to register mobygate as a
122
+ provider, and applies it atomically with a backup. Subcommands:
123
+ - `mobygate connect` — interactive, lists detected clients and asks
124
+ which to wire (default: all)
125
+ - `mobygate connect <id>` — wire one specific client
126
+ - `mobygate connect --all` — every detected client, no prompts
127
+ - `mobygate connect --dry-run` — show planned diff, don't write
128
+ - `mobygate connect --yes` — skip the per-client confirm prompt
129
+ - `mobygate connect --no-default` — register the provider but don't
130
+ change the client's active default model
131
+ - `mobygate disconnect <id>` — remove our entries cleanly
132
+ - **Auto-wire prompt at the end of `mobygate init`** — after services
133
+ start, if any supported client is detected, init asks "wire them
134
+ up?" before printing Done. Skipped in `--yes` mode (we don't edit
135
+ third-party configs in non-interactive flows without explicit opt-in).
136
+ - **Hermes connector** (`lib/connectors/hermes.js`) — registers the
137
+ `moby` provider in `~/.hermes/config.yaml`. Hermes only speaks the
138
+ OpenAI-compat wire format, so a single provider entry covers it.
139
+ - **OpenClaw connector** (`lib/connectors/openclaw.js`) — registers
140
+ BOTH `moby` (OpenAI-compat) and `moby-native` (Anthropic-messages)
141
+ in `~/.openclaw/openclaw.json`. With `setDefault`, points OpenClaw's
142
+ main + default at `moby-native/claude-opus-4-7` — the wire format
143
+ that unlocks vision, native tool calls, and reasoning blocks.
144
+
145
+ ### Provider naming
146
+
147
+ All registered entries use the `moby` prefix to be unmistakably from
148
+ mobygate:
149
+ - **`moby`** — OpenAI-compat surface (POST /v1/chat/completions)
150
+ - **`moby-native`** — Anthropic-messages surface (POST /v1/messages)
151
+
152
+ ### Safety
153
+
154
+ Every config write goes through `lib/connectors/safety.js`:
155
+ - Timestamped backup at `<file>.mobygate-backup-<ISO>` before any write
156
+ - Atomic write via temp file + rename
157
+ - Read-back verification that the on-disk content matches what we
158
+ intended to write
159
+ - Idempotent — running `connect` twice doesn't duplicate entries; we
160
+ detect existing moby entries by key and update vs append
161
+
162
+ ### Caveats
163
+
164
+ - **YAML comments are not preserved.** Hermes config uses YAML, and
165
+ `js-yaml` parse-then-emit drops comments and blank lines. The
166
+ auto-backup catches this — the connector also warns on first
167
+ detection. Comment-preserving YAML is a v0.8.x candidate.
168
+ - **Custom config locations not yet supported.** Connectors probe
169
+ `~/.hermes/` and `~/.openclaw/`. If you've installed a client to a
170
+ non-standard path, you'd still need to wire it manually, or set
171
+ `HERMES_HOME` / `OPENCLAW_HOME` env vars (which the connectors
172
+ honor).
173
+ - **No `verify()` step yet.** The connector contract has slots for it,
174
+ but v0.8.0 doesn't fire a real test request through the wired
175
+ client. Verification is on the v0.8.x roadmap.
176
+
177
+ ### v0.8.x backlog (deferred)
178
+
179
+ - More connectors (pi-agent, Goose, Aider, Cursor — anything that
180
+ takes a custom OpenAI-compat baseURL)
181
+ - `verify()` step that exercises the new provider end-to-end
182
+ - Comment-preserving YAML for Hermes
183
+ - Auxiliary-client routing helpers (pin Hermes vision/compression to
184
+ Sonnet/Haiku via mobygate to save Opus quota)
185
+ - `mobygate doctor` extended to check connection health per client
186
+
7
187
  ## [0.7.3] — 2026-04-25
8
188
 
9
189
  Hotfix bundle from a thorough security + bugs + ops audit. Six items.
package/bin/mobygate.js CHANGED
@@ -39,6 +39,13 @@ import {
39
39
  } from '../lib/platform.js';
40
40
  import { getAuthStatus, forceRefresh } from '../scripts/auth-helper.js';
41
41
  import { banner, compactBanner } from '../lib/ascii.js';
42
+ import {
43
+ CONNECTORS,
44
+ detectAll as detectAllConnectors,
45
+ getConnector,
46
+ DEFAULT_BASE_URL as MOBY_BASE_URL,
47
+ DEFAULT_API_KEY as MOBY_API_KEY,
48
+ } from '../lib/connectors/index.js';
42
49
 
43
50
  const __filename = fileURLToPath(import.meta.url);
44
51
  const REPO_ROOT = resolve(dirname(__filename), '..');
@@ -280,6 +287,54 @@ async function cmdInit() {
280
287
  info('Start the server manually (see instructions above) then run `mobygate status`.');
281
288
  }
282
289
 
290
+ // ---- Auto-wire detected clients (Hermes, OpenClaw, etc.) ----
291
+ // Skip in non-interactive mode (--yes) — those flows shouldn't surprise
292
+ // the user by editing third-party config files. They can still run
293
+ // `mobygate connect --all --yes` explicitly afterward.
294
+ if (!nonInteractive) {
295
+ try {
296
+ const detected = await detectAllConnectors();
297
+ const present = detected.filter((d) => d.detection !== null);
298
+ if (present.length > 0) {
299
+ section('Detected clients');
300
+ for (const { connector, detection } of present) {
301
+ print(` ${c.green('✓')} ${c.bold(connector.displayName)} ${c.dim('at ' + detection.configPath)}`);
302
+ }
303
+ print('');
304
+ const wire = await confirm('Auto-wire these to use mobygate?', true);
305
+ if (wire) {
306
+ for (const { connector } of present) {
307
+ try {
308
+ const plan = await connector.plan({
309
+ baseUrl: MOBY_BASE_URL,
310
+ apiKey: MOBY_API_KEY,
311
+ setDefault: true,
312
+ });
313
+ if (plan.skip) { warn(`${connector.displayName}: ${plan.reason}`); continue; }
314
+ for (const w of plan.warnings || []) warn(w);
315
+ const result = await connector.apply(plan);
316
+ if (result.applied) {
317
+ ok(`Wired ${connector.displayName} → ${result.configPath}`);
318
+ if (result.backupPath) print(c.dim(` backup: ${result.backupPath}`));
319
+ } else {
320
+ warn(`${connector.displayName}: ${result.reason}`);
321
+ }
322
+ } catch (e) {
323
+ warn(`${connector.displayName}: ${e.message}`);
324
+ }
325
+ }
326
+ print('');
327
+ info('Restart the wired clients so they pick up the new provider.');
328
+ } else {
329
+ print(c.dim('Skipped — run `mobygate connect` later to wire them on demand.'));
330
+ }
331
+ }
332
+ } catch (e) {
333
+ // Detection failures should never block init.
334
+ warn(`Connector detection failed: ${e.message}`);
335
+ }
336
+ }
337
+
283
338
  section('Done');
284
339
  print(`Dashboard: ${c.cyan(`http://localhost:${port}`)}`);
285
340
  print(`Configure: ${c.cyan(CONFIG_PATH)}`);
@@ -425,6 +480,78 @@ function cmdLogs() {
425
480
  spawnSync(cmd, args, { stdio: 'inherit' });
426
481
  }
427
482
 
483
+ // `mobygate tail` — watch captures live, print a one-line summary as
484
+ // each new request lands. Reads the .summary.txt file paired with each
485
+ // new .json file in ~/.mobygate/captures/.
486
+ async function cmdTail() {
487
+ const { homedir } = await import('os');
488
+ const { watch, existsSync, readFileSync, mkdirSync, statSync } = await import('fs');
489
+ const { readdir } = await import('fs/promises');
490
+ const { join } = await import('path');
491
+
492
+ const dir = process.env.MOBYGATE_CAPTURE_DIR
493
+ || join(process.env.MOBYGATE_HOME || join(homedir(), '.mobygate'), 'captures');
494
+
495
+ if (!existsSync(dir)) {
496
+ print(`Captures directory does not exist yet: ${dir}`);
497
+ print(`Enable capture (touch ~/.mobygate/.capture-enabled OR set MOBY_CAPTURE=1) and send a request.`);
498
+ return;
499
+ }
500
+
501
+ // Print recent history first (last 10), then watch.
502
+ const initial = (await readdir(dir))
503
+ .filter(n => n.endsWith('.summary.txt'))
504
+ .map(n => ({ name: n, mtime: statSync(join(dir, n)).mtimeMs }))
505
+ .sort((a, b) => a.mtime - b.mtime)
506
+ .slice(-10);
507
+
508
+ for (const { name } of initial) {
509
+ printSummaryLine(join(dir, name));
510
+ }
511
+
512
+ print(''); // blank line separator before live tail
513
+ print('--- watching for new captures (Ctrl-C to stop) ---');
514
+
515
+ const seen = new Set(initial.map(x => x.name));
516
+ watch(dir, (_event, name) => {
517
+ if (!name || !name.endsWith('.summary.txt')) return;
518
+ if (seen.has(name)) return;
519
+ seen.add(name);
520
+ // Wait briefly so the file is flushed (fs.watch can fire on the
521
+ // creation rename before content is fully written).
522
+ setTimeout(() => printSummaryLine(join(dir, name)), 50);
523
+ });
524
+ }
525
+
526
+ function printSummaryLine(summaryPath) {
527
+ try {
528
+ const text = readFileSync(summaryPath, 'utf8');
529
+ const grab = (re) => { const m = text.match(re); return m ? m[1].trim() : '?'; };
530
+ const ts = grab(/timestamp:\s+(\S+)/).slice(11, 19);
531
+ const path = grab(/^path:\s+(\S+)/m);
532
+ const model = grab(/^model:\s+(\S+)/m);
533
+ const msgs = grab(/^messages:\s+(\d+)/m);
534
+ const tokens = grab(/grand total:\s+(\d+)/);
535
+ const hit = grab(/cache hit rate:\s+([\d.]+)%/);
536
+ const status = grab(/^status:\s+(\S+)/m);
537
+ const dur = grab(/^duration:\s+(\d+) ms/m);
538
+
539
+ const hitDisplay = hit === '?' ? '─' : `${hit}% hit`;
540
+ const statusDisplay = status === '?' ? '(in flight)' : `${status} ${dur}ms`;
541
+
542
+ // Color-code the path: green for /v1/messages (native), yellow for /v1/chat/completions
543
+ const isNative = path === '/v1/messages';
544
+ const pathColored = isNative ? `\x1b[32m${path}\x1b[0m` : `\x1b[33m${path}\x1b[0m`;
545
+ const hitColored = hit === '?' ? hitDisplay
546
+ : parseFloat(hit) > 50 ? `\x1b[32m${hitDisplay}\x1b[0m`
547
+ : `\x1b[33m${hitDisplay}\x1b[0m`;
548
+
549
+ print(`${ts} ${pathColored.padEnd(35)} ${model.padEnd(22)} msgs=${msgs.padEnd(4)} tok=${tokens.padEnd(6)} ${hitColored.padEnd(20)} ${statusDisplay}`);
550
+ } catch (e) {
551
+ print(`(failed to read ${summaryPath}: ${e.message})`);
552
+ }
553
+ }
554
+
428
555
  async function cmdAuth() {
429
556
  section('mobygate auth');
430
557
  const status = await getAuthStatus();
@@ -659,22 +786,172 @@ async function cmdUpdate() {
659
786
  info(`Tip: if the install-layout changed (new service file, new paths), run \`mobygate init\` to re-install the service definitions.`);
660
787
  }
661
788
 
789
+ // ---------------------------------------------------------------------------
790
+ // `mobygate connect` — auto-wire third-party clients to use mobygate.
791
+ //
792
+ // Detects supported clients (Hermes, OpenClaw) on the system, plans the
793
+ // minimal config edit to register `moby` (OpenAI-compat) and/or
794
+ // `moby-native` (Anthropic-messages) as a provider, and applies it
795
+ // atomically with a backup. Idempotent — running twice doesn't duplicate
796
+ // entries; running with --dry-run shows the planned diff without writing.
797
+ //
798
+ // Per-client adapters live in lib/connectors/<id>.js and follow a
799
+ // uniform contract (detect/inspect/plan/apply/disconnect). Add a new
800
+ // connector by writing the adapter and registering it in
801
+ // lib/connectors/index.js#CONNECTORS.
802
+ // ---------------------------------------------------------------------------
803
+
804
+ function parseConnectArgs() {
805
+ // process.argv is [node, mobygate, connect, ...rest]
806
+ const rest = process.argv.slice(3);
807
+ const flags = new Set(rest.filter((a) => a.startsWith('-')));
808
+ const positional = rest.filter((a) => !a.startsWith('-'));
809
+ return {
810
+ targetId: positional[0] || null,
811
+ dryRun: flags.has('--dry-run'),
812
+ yes: flags.has('--yes') || flags.has('-y'),
813
+ all: flags.has('--all'),
814
+ noDefault: flags.has('--no-default'),
815
+ };
816
+ }
817
+
818
+ async function cmdConnect() {
819
+ const { targetId, dryRun, yes, all, noDefault } = parseConnectArgs();
820
+ section('mobygate connect');
821
+ if (dryRun) info('Dry-run mode — no files will be written.');
822
+
823
+ const detected = await detectAllConnectors();
824
+ const present = detected.filter((d) => d.detection !== null);
825
+
826
+ if (present.length === 0) {
827
+ print(c.dim('No supported clients detected. Currently we recognize:'));
828
+ for (const conn of CONNECTORS) {
829
+ print(c.dim(` - ${conn.displayName}`));
830
+ }
831
+ print(c.dim('Install one and re-run `mobygate connect`.'));
832
+ return;
833
+ }
834
+
835
+ // Decide which clients to act on.
836
+ let targets;
837
+ if (targetId) {
838
+ const match = present.find((p) => p.connector.id === targetId.toLowerCase());
839
+ if (!match) return die(`Client "${targetId}" not detected. Detected: ${present.map((p) => p.connector.id).join(', ')}`);
840
+ targets = [match];
841
+ } else if (all || yes) {
842
+ targets = present;
843
+ } else {
844
+ print('');
845
+ print('Detected clients:');
846
+ for (const { connector, detection } of present) {
847
+ print(` ${c.green('✓')} ${c.bold(connector.displayName)} ${c.dim('at ' + detection.configPath)}`);
848
+ }
849
+ print('');
850
+ const which = await prompt('Wire all? (Y / n / comma-separated ids)', 'Y');
851
+ const trimmed = which.trim().toLowerCase();
852
+ if (trimmed === 'n' || trimmed === 'no') return ok('Aborted — nothing changed.');
853
+ if (!trimmed || trimmed === 'y' || trimmed === 'yes') {
854
+ targets = present;
855
+ } else {
856
+ const ids = trimmed.split(',').map((s) => s.trim()).filter(Boolean);
857
+ targets = present.filter((p) => ids.includes(p.connector.id));
858
+ if (targets.length === 0) return die(`No detected clients matched "${which}".`);
859
+ }
860
+ }
861
+
862
+ // Plan + (maybe) apply per target.
863
+ for (const { connector } of targets) {
864
+ print('');
865
+ section(connector.displayName);
866
+ let plan;
867
+ try {
868
+ plan = await connector.plan({
869
+ baseUrl: MOBY_BASE_URL,
870
+ apiKey: MOBY_API_KEY,
871
+ setDefault: !noDefault,
872
+ });
873
+ } catch (e) {
874
+ warn(`plan failed: ${e.message}`);
875
+ continue;
876
+ }
877
+ if (plan.skip) { warn(plan.reason); continue; }
878
+
879
+ print(c.dim(`config: ${plan.configPath}`));
880
+ print('Changes:');
881
+ for (const line of plan.summary) {
882
+ const colored = line.startsWith('+') ? c.green(line) : line.startsWith('-') ? c.red(line) : c.cyan(line);
883
+ print(' ' + colored);
884
+ }
885
+ for (const w of plan.warnings || []) warn(w);
886
+
887
+ if (dryRun) { info('Dry-run — skipping write.'); continue; }
888
+
889
+ if (!yes) {
890
+ const ok2 = await confirm('Apply this change?', true);
891
+ if (!ok2) { warn('Skipped.'); continue; }
892
+ }
893
+
894
+ try {
895
+ const result = await connector.apply(plan);
896
+ if (result.applied) {
897
+ ok(`Wrote ${result.bytesWritten} bytes → ${result.configPath}`);
898
+ if (result.backupPath) print(c.dim(` backup: ${result.backupPath}`));
899
+ info(`Restart ${connector.displayName} so it picks up the new provider.`);
900
+ } else {
901
+ warn(`Skipped: ${result.reason}`);
902
+ }
903
+ } catch (e) {
904
+ die(`apply failed: ${e.message}`);
905
+ }
906
+ }
907
+ }
908
+
909
+ async function cmdDisconnect() {
910
+ const targetId = process.argv[3];
911
+ if (!targetId) return die('Usage: mobygate disconnect <client-id>\nKnown ids: ' + CONNECTORS.map((c) => c.id).join(', '));
912
+ const conn = getConnector(targetId.toLowerCase());
913
+ if (!conn) return die(`No connector for "${targetId}". Known: ${CONNECTORS.map((c) => c.id).join(', ')}`);
914
+ section(`mobygate disconnect ${conn.displayName}`);
915
+ let result;
916
+ try {
917
+ result = await conn.disconnect();
918
+ } catch (e) {
919
+ return die(`disconnect failed: ${e.message}`);
920
+ }
921
+ if (result.applied) {
922
+ ok(`Removed moby entries from ${conn.displayName}`);
923
+ if (result.backupPath) print(c.dim(` backup: ${result.backupPath}`));
924
+ if (result.note) info(result.note);
925
+ info(`Restart ${conn.displayName} so the change takes effect.`);
926
+ } else {
927
+ warn(result.reason);
928
+ }
929
+ }
930
+
662
931
  function usage() {
663
932
  print(`mobygate — OpenAI → Claude Max local gateway
664
933
 
665
934
  Usage:
666
- mobygate init Interactive setup (add --yes to skip prompts,
667
- --no-browser to not auto-open the dashboard)
668
- mobygate update Upgrade to the latest version + restart service
669
- mobygate doctor Diagnose version mismatches, zombie services, port conflicts
670
- mobygate start Start the proxy service
671
- mobygate stop Stop the proxy service
672
- mobygate restart Stop + start
673
- mobygate status Show service + auth + /health state
674
- mobygate logs Tail the server log
675
- mobygate auth Show auth status + run a refresh probe
676
- mobygate uninstall Remove installed services
677
- mobygate version Print version + install mode + path
935
+ mobygate init Interactive setup (add --yes to skip prompts,
936
+ --no-browser to not auto-open the dashboard)
937
+ mobygate update Upgrade to the latest version + restart service
938
+ mobygate doctor Diagnose version mismatches, zombie services, port conflicts
939
+ mobygate start Start the proxy service
940
+ mobygate stop Stop the proxy service
941
+ mobygate restart Stop + start
942
+ mobygate status Show service + auth + /health state
943
+ mobygate logs Tail the server log
944
+ mobygate tail Watch captures live (request/response inspector)
945
+ mobygate auth Show auth status + run a refresh probe
946
+ mobygate connect Auto-wire detected clients (Hermes, OpenClaw) to
947
+ use mobygate. Add: <id> for one client, --all for
948
+ every detected, --dry-run to preview, --yes to
949
+ skip prompts, --no-default to register without
950
+ changing the active model.
951
+ mobygate disconnect Remove moby entries from a client config.
952
+ Usage: mobygate disconnect <client-id>
953
+ mobygate uninstall Remove installed services
954
+ mobygate version Print version + install mode + path
678
955
 
679
956
  Config: ~/.mobygate/config.yaml (env vars override)
680
957
  Repo: ${REPO_ROOT}
@@ -686,6 +963,8 @@ Repo: ${REPO_ROOT}
686
963
  const cmd = process.argv[2];
687
964
  const COMMANDS = {
688
965
  init: cmdInit,
966
+ connect: cmdConnect,
967
+ disconnect: cmdDisconnect,
689
968
  update: cmdUpdate,
690
969
  upgrade: cmdUpdate,
691
970
  doctor: cmdDoctor,
@@ -694,6 +973,7 @@ const COMMANDS = {
694
973
  restart: cmdRestart,
695
974
  status: cmdStatus,
696
975
  logs: cmdLogs,
976
+ tail: cmdTail,
697
977
  auth: cmdAuth,
698
978
  uninstall: cmdUninstall,
699
979
  version: cmdVersion,
package/index.html CHANGED
@@ -361,6 +361,7 @@
361
361
  <a href="/auth/status?quick=1" class="py-[3px] px-2.5 border border-[#2A2A1F] text-[#C9D9A8] text-[11px] leading-[14px] hover:border-[#5A5F54]">/auth/status</a>
362
362
  <a href="/v1/models" class="py-[3px] px-2.5 border border-[#2A2A1F] text-[#C9D9A8] text-[11px] leading-[14px] hover:border-[#5A5F54]">/v1/models</a>
363
363
  <a href="/events" class="py-[3px] px-2.5 border border-[#2A2A1F] text-[#C9D9A8] text-[11px] leading-[14px] hover:border-[#5A5F54]">/events</a>
364
+ <a href="/inspector" class="py-[3px] px-2.5 border border-[#B7E56D] text-[#B7E56D] text-[11px] leading-[14px] hover:bg-[#1A1F12]">/inspector</a>
364
365
  </div>
365
366
  <div class="flex items-center gap-3">
366
367
  <span class="uppercase text-[#5A5F54] text-[10px] leading-3 tracking-[0.12em]" id="f-stream">stream · connecting</span>