diff --git a/.changeset/web-open-wildcard-host.md b/.changeset/web-open-wildcard-host.md new file mode 100644 index 00000000000..bf08cc40289 --- /dev/null +++ b/.changeset/web-open-wildcard-host.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Open the browser on localhost instead of the wildcard bind address for `kimi web --host 0.0.0.0`. diff --git a/apps/kimi-code/src/cli/sub/web/access-urls.ts b/apps/kimi-code/src/cli/sub/web/access-urls.ts index f0edbf90920..7f147b970d9 100644 --- a/apps/kimi-code/src/cli/sub/web/access-urls.ts +++ b/apps/kimi-code/src/cli/sub/web/access-urls.ts @@ -41,6 +41,18 @@ function isWildcard(host: string): boolean { return host === '' || host === '0.0.0.0' || host === '::'; } +/** + * Rewrite a bound origin for browser auto-open. A wildcard bind host + * (`0.0.0.0` / `::` / empty) is not navigable, so open localhost on the same + * port instead — the same address the ready banner's `Local:` line shows. + */ +export function browserOpenOrigin(origin: string): string { + const separator = origin.lastIndexOf(':'); + const host = origin.slice(origin.indexOf('://') + 3, separator); + if (!isWildcard(host)) return origin; + return `http://localhost${origin.slice(separator)}`; +} + /** True when `host` is a loopback address (this host only). */ export function isLoopbackHost(host: string): boolean { return host === 'localhost' || host === '127.0.0.1' || host === '::1'; diff --git a/apps/kimi-code/src/cli/sub/web/run.ts b/apps/kimi-code/src/cli/sub/web/run.ts index 261623444fa..5293afd63fd 100644 --- a/apps/kimi-code/src/cli/sub/web/run.ts +++ b/apps/kimi-code/src/cli/sub/web/run.ts @@ -31,6 +31,7 @@ import { } from '../../version'; import { accessUrlLines, + browserOpenOrigin, buildOpenableUrl, isLoopbackHost, splitTokenFragment, @@ -260,7 +261,8 @@ export async function handleWebCommand( : formatReadyLine(origin, token, parsed.dangerousBypassAuth), ); if (opts.open === true) { - deps.openUrl(token !== undefined ? buildWebUrl(origin, token) : origin); + const openOrigin = browserOpenOrigin(origin); + deps.openUrl(token !== undefined ? buildWebUrl(openOrigin, token) : openOrigin); } }, onShutdown: async () => { @@ -473,7 +475,7 @@ export function formatReadyBanner( return frag === '' ? url(base) : url(base) + dim(frag); }; - const port = Number(new URL(origin).port); + const port = Number(origin.slice(origin.lastIndexOf(':') + 1)); // Borderless header: the Kimi sprite (the little mascot with eyes) sits next // to the title, keeping the brand without the enclosing box. const logo = ['▐█▛█▛█▌', '▐█████▌'] as const; diff --git a/apps/kimi-code/test/cli/web/web.test.ts b/apps/kimi-code/test/cli/web/web.test.ts index 5f7f2a6f34c..73a944118a8 100644 --- a/apps/kimi-code/test/cli/web/web.test.ts +++ b/apps/kimi-code/test/cli/web/web.test.ts @@ -388,6 +388,46 @@ describe('`kimi web` opens the browser', () => { expect(openUrl).toHaveBeenCalledWith('http://127.0.0.1:58627'); }); + it('opens localhost rather than the wildcard bind address', async () => { + const { handleWebCommand } = await import('#/cli/sub/web/run'); + const { runner } = makeRunner('http://0.0.0.0:58627'); + const { stdout, stderr } = makeIo(); + const openUrl = vi.fn(); + + await handleWebCommand( + { host: '0.0.0.0', open: true }, + { + startServerForeground: runner, + resolveToken: () => 'tok-xyz', + openUrl, + stdout, + stderr, + }, + ); + + expect(openUrl).toHaveBeenCalledWith('http://localhost:58627/#token=tok-xyz'); + }); + + it('opens localhost for a wildcard IPv6 bind', async () => { + const { handleWebCommand } = await import('#/cli/sub/web/run'); + const { runner } = makeRunner('http://:::58627'); + const { stdout, stderr } = makeIo(); + const openUrl = vi.fn(); + + await handleWebCommand( + { host: '::', open: true }, + { + startServerForeground: runner, + resolveToken: () => undefined, + openUrl, + stdout, + stderr, + }, + ); + + expect(openUrl).toHaveBeenCalledWith('http://localhost:58627'); + }); + it('does not open the browser when open is false', async () => { const { handleWebCommand } = await import('#/cli/sub/web/run'); const { runner } = makeRunner('http://127.0.0.1:9000'); @@ -1030,6 +1070,21 @@ describe('accessUrlLines', () => { }); }); +describe('browserOpenOrigin', () => { + it('rewrites wildcard bind hosts to localhost on the same port', async () => { + const { browserOpenOrigin } = await import('#/cli/sub/web/access-urls'); + expect(browserOpenOrigin('http://0.0.0.0:58627')).toBe('http://localhost:58627'); + expect(browserOpenOrigin('http://:::58627')).toBe('http://localhost:58627'); + }); + + it('keeps navigable origins unchanged', async () => { + const { browserOpenOrigin } = await import('#/cli/sub/web/access-urls'); + expect(browserOpenOrigin('http://127.0.0.1:58627')).toBe('http://127.0.0.1:58627'); + expect(browserOpenOrigin('http://192.168.1.5:58627')).toBe('http://192.168.1.5:58627'); + expect(browserOpenOrigin('http://[::1]:58627')).toBe('http://[::1]:58627'); + }); +}); + describe('`kimi web rotate-token`', () => { let dir: string; let prevHome: string | undefined;