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
11 changes: 11 additions & 0 deletions .changeset/lan-remote-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@moonshot-ai/kimi-code": patch
"@moonshot-ai/vis-server": patch
---

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

When vis-server binds to 0.0.0.0 or :: (all interfaces), the startup
banner and CLI output now display the actual LAN IP addresses that
other devices on the same network can use to connect. This enables
lan-range remote control from phones, tablets, or other machines.
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}/`);
Comment on lines +13 to +14

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 Support IPv6 LAN URLs for :: binds

For VIS_HOST=:: or --host :: on an IPv6-only LAN, startVisServer considers the host an all-interfaces bind but this helper drops every IPv6 interface, so the new LAN access block is empty or only shows IPv4 URLs that may not connect. Include non-internal IPv6 addresses as bracketed URLs, e.g. http://[addr]:port/, when advertising an IPv6 all-interface bind.

Useful? React with 👍 / 👎.

}
}
}
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,

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 access URLs

When binding to 0.0.0.0 or ::, resolveVisAuthToken requires VIS_AUTH_TOKEN for non-loopback hosts, and the vis web client only sends that token after reading token/vis_token from the URL or existing localStorage. The lanUrls returned here are bare URLs, so a fresh phone/tablet opening one will load the shell but every /api request gets 401; append the token to these auth-required URLs or provide a login flow.

Useful? React with 👍 / 👎.

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') +
'\n';
}
return banner;
}
38 changes: 37 additions & 1 deletion apps/vis/server/test/lib/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { hostForUrl } from '../../src/config';
import { hostForUrl, isAllInterfaces, getLocalNetworkAddresses } from '../../src/config';

describe('hostForUrl', () => {
it('brackets a bare IPv6 literal for use in a URL', () => {
Expand All @@ -18,3 +18,39 @@ describe('hostForUrl', () => {
expect(hostForUrl('[::1]')).toBe('[::1]');
});
});

describe('isAllInterfaces', () => {
it('returns true for 0.0.0.0', () => {
expect(isAllInterfaces('0.0.0.0')).toBe(true);
});

it('returns true for ::', () => {
expect(isAllInterfaces('::')).toBe(true);
});

it('returns false for loopback hosts', () => {
expect(isAllInterfaces('127.0.0.1')).toBe(false);
expect(isAllInterfaces('localhost')).toBe(false);
expect(isAllInterfaces('::1')).toBe(false);
});
});

describe('getLocalNetworkAddresses', () => {
it('returns non-empty array of IPv4 URLs for a valid port', () => {
const addresses = getLocalNetworkAddresses(3001);
expect(addresses.length).toBeGreaterThan(0);
for (const addr of addresses) {
expect(addr).toMatch(/^http:\/\/\d+\.\d+\.\d+\.\d+:3001\/$/);
}
});

it('returns different URLs for different ports', () => {
const addrs3001 = getLocalNetworkAddresses(3001);
const addrs8080 = getLocalNetworkAddresses(8080);
expect(addrs3001.length).toBe(addrs8080.length);
for (let i = 0; i < addrs3001.length; i++) {
expect(addrs3001[i]).toContain(':3001/');
expect(addrs8080[i]).toContain(':8080/');
}
});
});
2 changes: 1 addition & 1 deletion packages/agent-core/src/profile/default/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ The user may ask you to research on certain topics, process or generate certain
- Understand the user's requirements thoroughly, ask for clarification before you start if needed.
- Make plans before doing deep or wide research, to ensure you are always on track.
- Search on the Internet if possible, with carefully-designed search queries to improve efficiency and accuracy.
- Use proper tools or shell commands or Python packages to process or generate images, videos, PDFs, docs, spreadsheets, presentations, or other multimedia files. Detect if there are already such tools in the environment. If you have to install third-party tools/packages, you MUST ensure that they are installed in a virtual/isolated environment.
- Use proper tools or shell commands or Python packages to process or generate images, PDFs, docs, spreadsheets, presentations, or other multimedia files. **For video files, prefer using the `ReadMediaFile` tool to read them directly rather than writing Python scripts or ffmpeg commands to extract frames manually.** Detect if there are already such tools in the environment. If you have to install third-party tools/packages, you MUST ensure that they are installed in a virtual/isolated environment.
- Once you generate or edit any images, videos or other media files, try to read it again before proceed, to ensure that the content is as expected.
- Avoid installing or deleting anything to/from outside of the current working directory. If you have to do so, ask the user for confirmation.

Expand Down
Loading