mobygate 0.4.0 → 0.5.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/CHANGELOG.md +64 -0
- package/bin/mobygate.js +144 -11
- package/lib/platform.js +104 -0
- package/package.json +4 -3
- package/scripts/postinstall.js +84 -0
- package/server.js +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,70 @@ 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.5.0] — 2026-04-19
|
|
8
|
+
|
|
9
|
+
The "upgrade should just work" release. Closes an entire class of
|
|
10
|
+
user-reported friction where a fresh `npm install -g mobygate@latest`
|
|
11
|
+
would seemingly succeed but leave the dashboard showing the old
|
|
12
|
+
version, the wrong server on :3456, or the browser serving stale HTML.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`Cache-Control: no-cache` headers on `/`** — browsers now always
|
|
17
|
+
re-fetch `index.html`, so a dashboard hard-reload is no longer
|
|
18
|
+
required after an upgrade. Fixes the most common "I see the old
|
|
19
|
+
dashboard after upgrade" report.
|
|
20
|
+
- **`postinstall` npm hook** — on `npm install -g mobygate[@…]`, if a
|
|
21
|
+
mobygate service (launchd / systemd / Task Scheduler) is already
|
|
22
|
+
installed on the box, it gets auto-restarted so the new code is
|
|
23
|
+
the one running on :3456. Users no longer need to remember
|
|
24
|
+
`mobygate restart` or `mobygate update` after an `npm install`.
|
|
25
|
+
Silent on first install and in dev-clone `npm install` (doesn't
|
|
26
|
+
bounce your live server while you're editing).
|
|
27
|
+
- **`mobygate doctor`** — diagnostic that reports:
|
|
28
|
+
- service-reported version vs CLI version (mismatch warning)
|
|
29
|
+
- port 3456 owner (flags non-mobygate processes holding the port)
|
|
30
|
+
- legacy launchd / systemd / Task Scheduler entries from prior
|
|
31
|
+
project names (claude-gate, claude-max-sdk-proxy) still present
|
|
32
|
+
- CLI-install path vs service-install path mismatch
|
|
33
|
+
(multi-node-version / fnm / nvm splits)
|
|
34
|
+
- auth state (logged-in, plan)
|
|
35
|
+
Prints a Recommended fix section with copy-pasteable commands.
|
|
36
|
+
- **`mobygate init --yes` / `-y`** — non-interactive mode. Accepts
|
|
37
|
+
all prompts using existing config values or built-in defaults.
|
|
38
|
+
Makes the tool scriptable: you can now write a one-liner recovery
|
|
39
|
+
command for anyone stuck on an old install.
|
|
40
|
+
- **Legacy service auto-cleanup in `mobygate init`** — before
|
|
41
|
+
installing new services, detects and removes launchd plists /
|
|
42
|
+
systemd units / Task Scheduler tasks under prior project names
|
|
43
|
+
(`ai.claude-gate.*`, `ai.claude-max-sdk-proxy.*`, etc.). Fixes the
|
|
44
|
+
"zombie old proxy still holding :3456" case that had friends
|
|
45
|
+
seeing the pre-v0.1.0 stub dashboard long after upgrading.
|
|
46
|
+
- **Port auto-kill in `mobygate init`** — if anything is bound to
|
|
47
|
+
the target port when init runs, it's killed (LISTEN sockets
|
|
48
|
+
only — won't kill active clients). Guarantees the new service
|
|
49
|
+
can bind :3456 cleanly.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- `doctor` / `killPort` filter `lsof -sTCP:LISTEN` so a browser tab
|
|
54
|
+
or `curl` connection *to* the port is no longer misidentified as
|
|
55
|
+
the owner of the port.
|
|
56
|
+
|
|
57
|
+
### Recovery flow
|
|
58
|
+
|
|
59
|
+
A user on any prior generation (including the pre-rename
|
|
60
|
+
claude-max-sdk-proxy) can now catch up with a single command:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
npm install -g mobygate@latest && mobygate init --yes
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The postinstall hook restarts any existing mobygate service, and
|
|
67
|
+
`init --yes` cleans up legacy services, frees the port, re-writes
|
|
68
|
+
the service definition for this machine's paths, and restarts.
|
|
69
|
+
No manual `lsof | xargs kill`, no browser hard-reload required.
|
|
70
|
+
|
|
7
71
|
## [0.4.0] — 2026-04-19
|
|
8
72
|
|
|
9
73
|
### Added
|
package/bin/mobygate.js
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
installLinuxServices, uninstallLinuxServices,
|
|
36
36
|
queryLinuxUnit, startLinuxUnit, stopLinuxUnit, LINUX_UNITS,
|
|
37
37
|
nonMacInstallInstructions,
|
|
38
|
+
cleanupLegacyServices, killPort,
|
|
38
39
|
} from '../lib/platform.js';
|
|
39
40
|
import { getAuthStatus, forceRefresh } from '../scripts/auth-helper.js';
|
|
40
41
|
import { banner, compactBanner } from '../lib/ascii.js';
|
|
@@ -96,8 +97,12 @@ async function confirm(q, def = true) {
|
|
|
96
97
|
|
|
97
98
|
async function cmdInit() {
|
|
98
99
|
const pkg = readPackage();
|
|
100
|
+
// --yes / -y skips prompts (uses defaults or existing config values),
|
|
101
|
+
// makes `mobygate init` scriptable for unattended upgrades.
|
|
102
|
+
const nonInteractive = process.argv.includes('--yes') || process.argv.includes('-y');
|
|
99
103
|
print(banner({ version: pkg.version }));
|
|
100
104
|
section('mobygate init');
|
|
105
|
+
if (nonInteractive) print(c.dim('(non-interactive — using existing config and defaults)'));
|
|
101
106
|
print(c.dim(`Platform: ${PLATFORM} Install path: ${REPO_ROOT}`));
|
|
102
107
|
print(c.dim(`Config: ${CONFIG_PATH}`));
|
|
103
108
|
print('');
|
|
@@ -113,8 +118,10 @@ async function cmdInit() {
|
|
|
113
118
|
warn('`claude` CLI not found on PATH.');
|
|
114
119
|
print(c.dim(' The proxy uses Claude Code\'s CLI to resolve OAuth credentials.'));
|
|
115
120
|
print(c.dim(' Install: https://docs.claude.com/en/docs/claude-code'));
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
if (!nonInteractive) {
|
|
122
|
+
const proceed = await confirm('Continue anyway? (you can set CLAUDE_BIN in config.yaml later)', false);
|
|
123
|
+
if (!proceed) die('Aborted.');
|
|
124
|
+
}
|
|
118
125
|
} else {
|
|
119
126
|
ok(`claude CLI at ${claudeProbe.stdout.trim().split('\n')[0]}`);
|
|
120
127
|
}
|
|
@@ -127,22 +134,47 @@ async function cmdInit() {
|
|
|
127
134
|
} else {
|
|
128
135
|
warn('Not logged into Claude Max.');
|
|
129
136
|
print(c.dim(' Run `claude auth login` in another terminal, then re-run `mobygate init`.'));
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
if (!nonInteractive) {
|
|
138
|
+
const proceed = await confirm('Continue without login?', false);
|
|
139
|
+
if (!proceed) die('Aborted.');
|
|
140
|
+
}
|
|
132
141
|
}
|
|
133
142
|
} catch (e) {
|
|
134
143
|
warn(`Couldn't check auth status: ${e.message}`);
|
|
135
144
|
}
|
|
136
145
|
|
|
146
|
+
// ---- Clean up any legacy service / zombie process on our port ----
|
|
147
|
+
// Idempotent — runs every init. Removes launchd/systemd/Task Scheduler
|
|
148
|
+
// entries left behind by claude-max-sdk-proxy / claude-gate / any prior
|
|
149
|
+
// generation, and kills anything currently bound to :3456 so the new
|
|
150
|
+
// service can claim it without collision.
|
|
151
|
+
section('Cleanup');
|
|
152
|
+
const cleaned = cleanupLegacyServices();
|
|
153
|
+
const removed = cleaned.filter((r) => r.removed);
|
|
154
|
+
if (removed.length) {
|
|
155
|
+
for (const r of removed) ok(`Removed legacy ${r.label}`);
|
|
156
|
+
} else {
|
|
157
|
+
print(c.dim(' No legacy services found.'));
|
|
158
|
+
}
|
|
159
|
+
const targetPort = parseInt(process.env.PORT || String(loadConfig().port), 10) || 3456;
|
|
160
|
+
const killed = killPort(targetPort);
|
|
161
|
+
if (killed.length) ok(`Freed port ${targetPort} (killed pid${killed.length > 1 ? 's' : ''} ${killed.join(', ')})`);
|
|
162
|
+
else print(c.dim(` Port ${targetPort} is free.`));
|
|
163
|
+
|
|
137
164
|
// ---- Config ----
|
|
138
165
|
section('Configuration');
|
|
139
166
|
const existing = loadConfig();
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
// With --yes, inherit existing values / defaults without prompting
|
|
168
|
+
const port = nonInteractive ? String(existing.port)
|
|
169
|
+
: await prompt('HTTP port', String(existing.port));
|
|
170
|
+
const defaultModel = nonInteractive ? existing.default_model
|
|
171
|
+
: await prompt('Default model', existing.default_model);
|
|
172
|
+
const sessionTtl = nonInteractive ? String(existing.session_ttl_minutes)
|
|
173
|
+
: await prompt('Session TTL (minutes)', String(existing.session_ttl_minutes));
|
|
174
|
+
const claudeBin = nonInteractive ? (existing.claude_bin || '')
|
|
175
|
+
: (claudeProbe.status === 0
|
|
176
|
+
? (await prompt('CLAUDE_BIN override (blank = PATH)', existing.claude_bin || ''))
|
|
177
|
+
: (await prompt('CLAUDE_BIN (absolute path to claude binary)', existing.claude_bin || '')));
|
|
146
178
|
|
|
147
179
|
const configPath = writeConfig({
|
|
148
180
|
port: parseInt(port, 10) || 3456,
|
|
@@ -408,6 +440,105 @@ function cmdVersion() {
|
|
|
408
440
|
print(`mobygate v${pkg.version} · ${detectInstallMode()} install at ${REPO_ROOT}`);
|
|
409
441
|
}
|
|
410
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Diagnostic command — identifies the classes of problem users have hit
|
|
445
|
+
* during upgrades (CLI vs service version mismatch, zombie old services,
|
|
446
|
+
* port-3456 owned by something else, node-version split). Prints one
|
|
447
|
+
* line per finding + a recommended fix at the end.
|
|
448
|
+
*/
|
|
449
|
+
async function cmdDoctor() {
|
|
450
|
+
const pkg = readPackage();
|
|
451
|
+
const mode = detectInstallMode();
|
|
452
|
+
section('mobygate doctor');
|
|
453
|
+
print(c.dim(`CLI: v${pkg.version} · ${mode} install at ${REPO_ROOT}`));
|
|
454
|
+
const issues = [];
|
|
455
|
+
const fixes = [];
|
|
456
|
+
|
|
457
|
+
// 1. Version reported by the running service (via /dashboard/recent)
|
|
458
|
+
let serviceVersion = null;
|
|
459
|
+
try {
|
|
460
|
+
const port = loadConfig().port;
|
|
461
|
+
const r = await fetch(`http://localhost:${port}/dashboard/recent`, { signal: AbortSignal.timeout(2000) });
|
|
462
|
+
const j = await r.json();
|
|
463
|
+
serviceVersion = j.build?.version || null;
|
|
464
|
+
} catch (e) { /* server down or pre-v0.2.0 */ }
|
|
465
|
+
if (!serviceVersion) {
|
|
466
|
+
warn('Service on :3456 is not responding / older than v0.2.0');
|
|
467
|
+
issues.push('service-down-or-ancient');
|
|
468
|
+
} else {
|
|
469
|
+
const match = serviceVersion === pkg.version;
|
|
470
|
+
(match ? ok : warn)(`Service on :3456 reports v${serviceVersion}${match ? '' : ` (CLI is v${pkg.version} — mismatch)`}`);
|
|
471
|
+
if (!match) {
|
|
472
|
+
issues.push('version-mismatch');
|
|
473
|
+
fixes.push('mobygate restart');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 2. Port owner — filter to LISTEN state so we don't flag clients that
|
|
478
|
+
// happen to be *connected to* :3456 (browser tabs, curl) as owners.
|
|
479
|
+
if (IS_MAC || IS_LINUX) {
|
|
480
|
+
const r = spawnSync('sh', ['-c', `lsof -ti :${loadConfig().port} -sTCP:LISTEN 2>/dev/null`], { encoding: 'utf8' });
|
|
481
|
+
const pid = (r.stdout || '').trim().split('\n').filter(Boolean)[0];
|
|
482
|
+
if (pid) {
|
|
483
|
+
const ps = spawnSync('ps', ['-p', pid, '-o', 'command='], { encoding: 'utf8' });
|
|
484
|
+
const cmdline = (ps.stdout || '').trim();
|
|
485
|
+
if (cmdline && !cmdline.includes('mobygate')) {
|
|
486
|
+
warn(`:${loadConfig().port} is owned by NON-mobygate pid ${pid}: ${cmdline.slice(0, 80)}`);
|
|
487
|
+
issues.push('port-owned-by-foreign-process');
|
|
488
|
+
fixes.push(`lsof -ti :${loadConfig().port} -sTCP:LISTEN | xargs kill && mobygate init --yes`);
|
|
489
|
+
} else {
|
|
490
|
+
print(c.dim(` port ${loadConfig().port} held by mobygate pid ${pid}.`));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 3. Legacy service entries we haven't cleaned yet
|
|
496
|
+
if (IS_MAC) {
|
|
497
|
+
const legacyFound = [];
|
|
498
|
+
for (const lbl of ['ai.claude-gate.server','ai.claude-gate.auth-refresh','ai.claude-max-sdk-proxy.server','ai.claude-max-sdk-proxy.auth-refresh']) {
|
|
499
|
+
const p = join(process.env.HOME, 'Library', 'LaunchAgents', `${lbl}.plist`);
|
|
500
|
+
if (existsSync(p)) legacyFound.push(lbl);
|
|
501
|
+
}
|
|
502
|
+
if (legacyFound.length) {
|
|
503
|
+
warn(`Legacy launchd plists found: ${legacyFound.join(', ')}`);
|
|
504
|
+
issues.push('legacy-services');
|
|
505
|
+
fixes.push('mobygate init --yes # cleans up legacy entries');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// 4. CLI mobygate vs service install path (multi-node-version detection)
|
|
510
|
+
const state = readState();
|
|
511
|
+
if (state && state.install_path && state.install_path !== REPO_ROOT) {
|
|
512
|
+
warn(`CLI install (${REPO_ROOT}) differs from service install (${state.install_path})`);
|
|
513
|
+
issues.push('install-path-split');
|
|
514
|
+
fixes.push('mobygate init --yes # rebinds service to the current CLI location');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 5. /auth/status
|
|
518
|
+
try {
|
|
519
|
+
const r = await fetch(`http://localhost:${loadConfig().port}/auth/status?quick=1`, { signal: AbortSignal.timeout(2000) });
|
|
520
|
+
const j = await r.json();
|
|
521
|
+
if (!j.loggedIn) {
|
|
522
|
+
warn('Not logged in to Claude — run `claude auth login`');
|
|
523
|
+
issues.push('not-logged-in');
|
|
524
|
+
fixes.push('claude auth login');
|
|
525
|
+
} else {
|
|
526
|
+
ok(`Auth: ${j.email || 'logged in'} (${j.subscriptionType || j.authMethod})`);
|
|
527
|
+
}
|
|
528
|
+
} catch { /* already reported via service-down */ }
|
|
529
|
+
|
|
530
|
+
// Summary
|
|
531
|
+
print('');
|
|
532
|
+
if (!issues.length) {
|
|
533
|
+
ok('No problems detected. Everything looks healthy.');
|
|
534
|
+
} else {
|
|
535
|
+
section('Recommended fix');
|
|
536
|
+
for (const f of fixes) print(` ${c.cyan(f)}`);
|
|
537
|
+
print('');
|
|
538
|
+
print(c.dim(`${issues.length} issue${issues.length > 1 ? 's' : ''} found: ${issues.join(', ')}`));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
411
542
|
/**
|
|
412
543
|
* Upgrade mobygate to the latest version from its source of truth:
|
|
413
544
|
* - `npm` mode → `npm install -g mobygate@latest`
|
|
@@ -474,8 +605,9 @@ function usage() {
|
|
|
474
605
|
print(`mobygate — OpenAI → Claude Max local gateway
|
|
475
606
|
|
|
476
607
|
Usage:
|
|
477
|
-
mobygate init Interactive setup
|
|
608
|
+
mobygate init Interactive setup (add --yes to skip prompts)
|
|
478
609
|
mobygate update Upgrade to the latest version + restart service
|
|
610
|
+
mobygate doctor Diagnose version mismatches, zombie services, port conflicts
|
|
479
611
|
mobygate start Start the proxy service
|
|
480
612
|
mobygate stop Stop the proxy service
|
|
481
613
|
mobygate restart Stop + start
|
|
@@ -497,6 +629,7 @@ const COMMANDS = {
|
|
|
497
629
|
init: cmdInit,
|
|
498
630
|
update: cmdUpdate,
|
|
499
631
|
upgrade: cmdUpdate,
|
|
632
|
+
doctor: cmdDoctor,
|
|
500
633
|
start: cmdStart,
|
|
501
634
|
stop: cmdStop,
|
|
502
635
|
restart: cmdRestart,
|
package/lib/platform.js
CHANGED
|
@@ -328,6 +328,110 @@ export const WIN_LABELS = {
|
|
|
328
328
|
auth: WIN_AUTH_TASK,
|
|
329
329
|
};
|
|
330
330
|
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Legacy / zombie service cleanup — invoked by `mobygate init` so upgrades
|
|
333
|
+
// from any prior generation (claude-max-sdk-proxy / claude-gate / brother's
|
|
334
|
+
// forks) never leave an old process competing with the new one on :3456.
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
const LEGACY_MAC_LABELS = [
|
|
338
|
+
'ai.claude-max-sdk-proxy.server',
|
|
339
|
+
'ai.claude-max-sdk-proxy.auth-refresh',
|
|
340
|
+
'ai.claude-gate.server',
|
|
341
|
+
'ai.claude-gate.auth-refresh',
|
|
342
|
+
];
|
|
343
|
+
const LEGACY_LINUX_UNITS = [
|
|
344
|
+
'claude-max-sdk-proxy.service',
|
|
345
|
+
'claude-gate-server.service',
|
|
346
|
+
'claude-gate-auth.service',
|
|
347
|
+
'claude-gate-auth.timer',
|
|
348
|
+
];
|
|
349
|
+
const LEGACY_WIN_TASKS = [
|
|
350
|
+
'claude-max-sdk-proxy-server',
|
|
351
|
+
'claude-max-sdk-proxy-auth-refresh',
|
|
352
|
+
'claude-gate-server',
|
|
353
|
+
'claude-gate-auth-refresh',
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Remove all known legacy service entries. Returns a list of
|
|
358
|
+
* { platform, label, removed } for each attempt so callers can log.
|
|
359
|
+
* Safe to run every `mobygate init` — no-op if nothing matches.
|
|
360
|
+
*/
|
|
361
|
+
export function cleanupLegacyServices() {
|
|
362
|
+
const results = [];
|
|
363
|
+
if (IS_MAC) {
|
|
364
|
+
for (const label of LEGACY_MAC_LABELS) {
|
|
365
|
+
const p = join(LAUNCH_AGENTS_DIR, `${label}.plist`);
|
|
366
|
+
if (!existsSync(p)) { results.push({ label, removed: false, reason: 'absent' }); continue; }
|
|
367
|
+
try { execSync(`launchctl unload "${p}" 2>/dev/null`, { stdio: 'ignore' }); } catch {}
|
|
368
|
+
try { unlinkSync(p); results.push({ label, removed: true }); }
|
|
369
|
+
catch (e) { results.push({ label, removed: false, error: e.message }); }
|
|
370
|
+
}
|
|
371
|
+
} else if (IS_LINUX) {
|
|
372
|
+
for (const unit of LEGACY_LINUX_UNITS) {
|
|
373
|
+
const r = systemctlUser(['disable', '--now', unit]);
|
|
374
|
+
const p = join(SYSTEMD_USER_DIR, unit);
|
|
375
|
+
if (existsSync(p)) {
|
|
376
|
+
try { unlinkSync(p); results.push({ label: unit, removed: true }); }
|
|
377
|
+
catch (e) { results.push({ label: unit, removed: false, error: e.message }); }
|
|
378
|
+
} else {
|
|
379
|
+
results.push({ label: unit, removed: false, reason: 'absent' });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
systemctlUser(['daemon-reload']);
|
|
383
|
+
} else if (IS_WIN) {
|
|
384
|
+
for (const task of LEGACY_WIN_TASKS) {
|
|
385
|
+
const r = runPowershell(`
|
|
386
|
+
$t = Get-ScheduledTask -TaskName '${task}' -ErrorAction SilentlyContinue
|
|
387
|
+
if ($t) {
|
|
388
|
+
Stop-ScheduledTask -TaskName '${task}' -ErrorAction SilentlyContinue
|
|
389
|
+
Unregister-ScheduledTask -TaskName '${task}' -Confirm:$false
|
|
390
|
+
Write-Output 'removed'
|
|
391
|
+
} else { Write-Output 'absent' }
|
|
392
|
+
`);
|
|
393
|
+
if (r.ok) results.push({ label: task, removed: r.stdout === 'removed', reason: r.stdout });
|
|
394
|
+
else results.push({ label: task, removed: false, error: r.stderr });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return results;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Kill whatever currently owns the given port. Best-effort across platforms.
|
|
402
|
+
* Returns the list of PIDs killed (empty if nothing was bound).
|
|
403
|
+
*/
|
|
404
|
+
export function killPort(port) {
|
|
405
|
+
const killed = [];
|
|
406
|
+
if (IS_MAC || IS_LINUX) {
|
|
407
|
+
try {
|
|
408
|
+
// Filter to LISTEN state so we don't kill clients connected to :port
|
|
409
|
+
const r = spawnSync('sh', ['-c', `lsof -ti :${port} -sTCP:LISTEN 2>/dev/null || true`], { encoding: 'utf8' });
|
|
410
|
+
const pids = (r.stdout || '').trim().split('\n').filter(Boolean);
|
|
411
|
+
for (const pid of pids) {
|
|
412
|
+
try { process.kill(parseInt(pid, 10), 'SIGTERM'); killed.push(pid); } catch {}
|
|
413
|
+
}
|
|
414
|
+
if (pids.length) {
|
|
415
|
+
// Give graceful shutdown a moment, then force
|
|
416
|
+
setTimeout(() => {
|
|
417
|
+
for (const pid of pids) { try { process.kill(parseInt(pid, 10), 'SIGKILL'); } catch {} }
|
|
418
|
+
}, 500);
|
|
419
|
+
}
|
|
420
|
+
} catch {}
|
|
421
|
+
} else if (IS_WIN) {
|
|
422
|
+
const r = runPowershell(`
|
|
423
|
+
$c = Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue
|
|
424
|
+
if ($c) {
|
|
425
|
+
$c | ForEach-Object {
|
|
426
|
+
try { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue; Write-Output $_.OwningProcess } catch {}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
`);
|
|
430
|
+
if (r.ok && r.stdout) killed.push(...r.stdout.split(/\r?\n/).filter(Boolean));
|
|
431
|
+
}
|
|
432
|
+
return killed;
|
|
433
|
+
}
|
|
434
|
+
|
|
331
435
|
// ---------------------------------------------------------------------------
|
|
332
436
|
// Linux — systemd user units
|
|
333
437
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobygate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "OpenAI-compatible local proxy for Claude Max. The Möbius-strip gateway: OpenAI shape in, Claude Max out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"mobygate": "
|
|
8
|
+
"mobygate": "bin/mobygate.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node server.js",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"up": "npm install && node server.js",
|
|
14
14
|
"auth:status": "node scripts/auth-status.js",
|
|
15
15
|
"auth:status:quick": "node scripts/auth-status.js --quick",
|
|
16
|
-
"auth:refresh": "node scripts/auth-refresh.js"
|
|
16
|
+
"auth:refresh": "node scripts/auth-refresh.js",
|
|
17
|
+
"postinstall": "node scripts/postinstall.js || true"
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|
|
19
20
|
"@anthropic-ai/claude-agent-sdk": "^0.2.112",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mobygate postinstall hook.
|
|
4
|
+
*
|
|
5
|
+
* Runs automatically after `npm install -g mobygate` (and any fresh
|
|
6
|
+
* `npm install` in a dev clone). Its job: if a previous mobygate
|
|
7
|
+
* service is already registered on this box (launchd / systemd /
|
|
8
|
+
* Task Scheduler), restart it so the newly-updated code is what's
|
|
9
|
+
* actually running on :3456.
|
|
10
|
+
*
|
|
11
|
+
* Without this, `npm install -g mobygate@latest` updates files on
|
|
12
|
+
* disk but leaves the old process running — users have to remember
|
|
13
|
+
* to also run `mobygate restart` or `mobygate update`. This hook
|
|
14
|
+
* makes upgrades one-command.
|
|
15
|
+
*
|
|
16
|
+
* Silent on first install (no service exists yet → nothing to
|
|
17
|
+
* restart). Silent on CI / npm install in odd contexts (no
|
|
18
|
+
* permission, no launchctl available, etc.) — never fails the
|
|
19
|
+
* install. Worst case: prints a line saying it couldn't auto-
|
|
20
|
+
* restart, user runs `mobygate restart` themselves.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync } from 'fs';
|
|
24
|
+
import { homedir, platform } from 'os';
|
|
25
|
+
import { join } from 'path';
|
|
26
|
+
import { execSync, spawnSync } from 'child_process';
|
|
27
|
+
|
|
28
|
+
// Skip during `npm install` inside this package's own dev clone —
|
|
29
|
+
// you don't want `npm install` as a developer to restart your live
|
|
30
|
+
// service every save. Skip when npm's lifecycle event is "install"
|
|
31
|
+
// and the cwd isn't a global install.
|
|
32
|
+
const isGlobalInstall = (process.env.npm_config_global === 'true')
|
|
33
|
+
|| !!process.env.npm_lifecycle_event && process.env.npm_config_prefix;
|
|
34
|
+
if (!isGlobalInstall && process.env.npm_package_name === 'mobygate') {
|
|
35
|
+
// Developer install — skip, don't surprise them by bouncing their server
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const plat = platform();
|
|
40
|
+
const log = (...args) => process.stderr.write('[mobygate postinstall] ' + args.join(' ') + '\n');
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
if (plat === 'darwin') {
|
|
44
|
+
const plist = join(homedir(), 'Library', 'LaunchAgents', 'ai.mobygate.server.plist');
|
|
45
|
+
if (!existsSync(plist)) process.exit(0); // first install, no service yet
|
|
46
|
+
try { execSync(`launchctl unload "${plist}"`, { stdio: 'ignore' }); } catch {}
|
|
47
|
+
try { execSync(`launchctl load "${plist}"`, { stdio: 'ignore' }); } catch (e) {
|
|
48
|
+
log(`couldn't reload ai.mobygate.server — run \`mobygate restart\` manually`);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
log('auto-restarted ai.mobygate.server (launchd)');
|
|
52
|
+
} else if (plat === 'linux') {
|
|
53
|
+
const r = spawnSync('systemctl', ['--user', 'is-active', 'mobygate-server.service'], { encoding: 'utf8' });
|
|
54
|
+
if (r.stdout?.trim() !== 'active') process.exit(0); // not running → first install or disabled
|
|
55
|
+
const restart = spawnSync('systemctl', ['--user', 'restart', 'mobygate-server.service'], { encoding: 'utf8' });
|
|
56
|
+
if (restart.status !== 0) {
|
|
57
|
+
log('couldn\'t restart mobygate-server.service — run `mobygate restart` manually');
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
log('auto-restarted mobygate-server (systemd user)');
|
|
61
|
+
} else if (plat === 'win32') {
|
|
62
|
+
// Detect task presence, then restart via PowerShell
|
|
63
|
+
const ps = spawnSync('powershell', [
|
|
64
|
+
'-NoProfile', '-Command',
|
|
65
|
+
`$t = Get-ScheduledTask -TaskName 'mobygate-server' -ErrorAction SilentlyContinue
|
|
66
|
+
if (-not $t) { exit 0 }
|
|
67
|
+
Stop-ScheduledTask -TaskName 'mobygate-server' -ErrorAction SilentlyContinue
|
|
68
|
+
Start-Sleep -Milliseconds 500
|
|
69
|
+
Start-ScheduledTask -TaskName 'mobygate-server'
|
|
70
|
+
Write-Output 'ok'`,
|
|
71
|
+
], { encoding: 'utf8', windowsHide: true });
|
|
72
|
+
if (ps.status === 0 && ps.stdout?.includes('ok')) {
|
|
73
|
+
log('auto-restarted mobygate-server (Task Scheduler)');
|
|
74
|
+
} else if (ps.status === 0) {
|
|
75
|
+
// Task didn't exist — first install, silent
|
|
76
|
+
} else {
|
|
77
|
+
log("couldn't restart mobygate-server — run `mobygate restart` manually");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// Never fail the npm install for any reason
|
|
82
|
+
log(`postinstall skipped: ${e.message}`);
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
package/server.js
CHANGED
|
@@ -785,8 +785,13 @@ async function handleNonStreaming(res, body, requestId, sessionKey) {
|
|
|
785
785
|
const app = express();
|
|
786
786
|
app.use(express.json({ limit: '10mb' }));
|
|
787
787
|
|
|
788
|
-
// GET / — serve dashboard
|
|
788
|
+
// GET / — serve dashboard. No-cache headers so browsers always re-fetch
|
|
789
|
+
// after a mobygate upgrade; otherwise they keep serving the old index.html
|
|
790
|
+
// from cache and users see a stale dashboard long after the service updated.
|
|
789
791
|
app.get('/', (_req, res) => {
|
|
792
|
+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
793
|
+
res.setHeader('Pragma', 'no-cache');
|
|
794
|
+
res.setHeader('Expires', '0');
|
|
790
795
|
res.sendFile(join(__dirname, 'index.html'));
|
|
791
796
|
});
|
|
792
797
|
|