Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/lan-remote-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@moonshot-ai/kimi-server": patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Point the changeset at an existing package

This changeset names @moonshot-ai/kimi-server, but there is no workspace package with that name in the repo; the changed CLI package is @moonshot-ai/kimi-code, while @moonshot-ai/vis-server is private and ignored by changesets. As written, the release workflow cannot apply the user-visible kimi vis changelog/bump to the published CLI package and may fail when processing the unknown package name.

Useful? React with 👍 / 👎.

---

feat(vis): show LAN URLs when binding to 0.0.0.0 for remote control

Display accessible LAN URLs in the terminal when the server binds to 0.0.0.0,
enabling remote control from other devices on the local network.
7 changes: 7 additions & 0 deletions apps/kimi-code/src/cli/sub/vis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface StartedVisServer {
readonly port: number;
readonly host: string;
readonly url: string;
readonly lanUrls?: string[];
readonly close: () => Promise<void>;
}

Expand Down Expand Up @@ -86,6 +87,12 @@ export async function handleVis(deps: VisDeps, opts: VisOptions): Promise<void>
: `${server.url}sessions/${encodeURIComponent(opts.sessionId)}`;

deps.stdout.write(`kimi vis is running at ${server.url}\n`);
if (server.lanUrls !== undefined && server.lanUrls.length > 0) {
deps.stdout.write(`LAN access:\n`);
for (const lanUrl of server.lanUrls) {
deps.stdout.write(` ${lanUrl}\n`);
}
}
deps.stdout.write('Press Ctrl-C to stop.\n');

if (opts.open) {
Expand Down
17 changes: 17 additions & 0 deletions apps/vis/server/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import { homedir } from 'node:os';
import { join } from 'node:path';
import os from 'node:os';

export function isAllInterfaces(host: string): boolean {
return host === '0.0.0.0' || host === '::';
}

export function getLocalNetworkAddresses(port: number): string[] {
const addresses: string[] = [];
for (const [, ifaceList] of Object.entries(os.networkInterfaces())) {
for (const iface of ifaceList ?? []) {
if (!iface.internal && iface.family === 'IPv4') {
addresses.push(`http://${iface.address}:${port}/`);
}
}
}
return addresses;
}

/** Resolve KIMI_CODE_HOME (env > ~/.kimi-code). */
export function resolveKimiCodeHome(): string {
Expand Down
4 changes: 2 additions & 2 deletions apps/vis/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { formatStartupBanner } from './startup-banner';
async function main(): Promise<void> {
const host = resolveHost();
const authToken = resolveVisAuthToken(host);
const { port } = await startVisServer({ host, authToken });
const { port, lanUrls } = await startVisServer({ host, authToken });
process.stdout.write(
formatStartupBanner({ authToken, host, kimiCodeHome: KIMI_CODE_HOME, port }),
formatStartupBanner({ authToken, host, kimiCodeHome: KIMI_CODE_HOME, port, lanUrls }),
);
}

Expand Down
4 changes: 3 additions & 1 deletion apps/vis/server/src/start.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { serve } from '@hono/node-server';

import { createApp } from './app';
import { hostForUrl, resolveHost, resolveKimiCodeHome, resolvePort, resolveVisAuthToken } from './config';
import { getLocalNetworkAddresses, hostForUrl, isAllInterfaces, resolveHost, resolveKimiCodeHome, resolvePort, resolveVisAuthToken } from './config';
import type { WebAsset } from './lib/web-asset';

export interface StartVisServerOptions {
Expand All @@ -18,6 +18,7 @@ export interface StartedVisServer {
readonly port: number;
readonly host: string;
readonly url: string;
readonly lanUrls?: string[];
readonly close: () => Promise<void>;
}

Expand All @@ -36,6 +37,7 @@ export async function startVisServer(
port: info.port,
host,
url: `http://${hostForUrl(host)}:${info.port}/`,
lanUrls: isAllInterfaces(host) ? getLocalNetworkAddresses(info.port) : undefined,
close: () =>
new Promise<void>((done, fail) => {
server.close((err?: Error) => (err ? fail(err) : done()));
Expand Down
13 changes: 10 additions & 3 deletions apps/vis/server/src/startup-banner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ export interface StartupBannerOptions {
readonly host: string;
readonly kimiCodeHome: string;
readonly port: number;
readonly lanUrls?: string[];
}

export function formatStartupBanner(options: StartupBannerOptions): string {
const authStatus = options.authToken === undefined ? 'auth=disabled' : 'auth=required';
return (
let banner =
`[vis-server] listening on http://${hostForUrl(options.host)}:${String(options.port)} ` +
`(${authStatus}, KIMI_CODE_HOME=${options.kimiCodeHome})\n`
);
`(${authStatus}, KIMI_CODE_HOME=${options.kimiCodeHome})\n`;
if (options.lanUrls !== undefined && options.lanUrls.length > 0) {
banner +=
`[vis-server] LAN access:\n` +
options.lanUrls.map((url) => ` - ${url}`).join('\n') +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the auth token in LAN links

When vis is bound to 0.0.0.0 or ::, the server normally has VIS_AUTH_TOKEN set because non-loopback binds are rejected without it; however these new LAN URLs are printed as bare origins. A browser on another device has no kimi-vis-auth-token in localStorage and the SPA only sends Authorization after it reads token/vis_token from the URL, so opening the advertised LAN link loads the shell but every /api/* request returns 401. Add the token fragment/query to these printed links, or otherwise print a usable authenticated URL.

Useful? React with 👍 / 👎.

'\n';
}
return banner;
}
Loading