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 +180 -0
- package/bin/mobygate.js +292 -12
- package/index.html +1 -0
- package/inspector.html +422 -0
- package/lib/anthropic.js +23 -0
- package/lib/connectors/hermes.js +188 -0
- package/lib/connectors/index.js +80 -0
- package/lib/connectors/openclaw.js +290 -0
- package/lib/connectors/safety.js +141 -0
- package/lib/request-capture.js +394 -0
- package/package.json +2 -1
- package/server.js +248 -6
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
|
|
667
|
-
|
|
668
|
-
mobygate update
|
|
669
|
-
mobygate doctor
|
|
670
|
-
mobygate start
|
|
671
|
-
mobygate stop
|
|
672
|
-
mobygate restart
|
|
673
|
-
mobygate status
|
|
674
|
-
mobygate logs
|
|
675
|
-
mobygate
|
|
676
|
-
mobygate
|
|
677
|
-
mobygate
|
|
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>
|