diff --git a/.changeset/lan-remote-control.md b/.changeset/lan-remote-control.md new file mode 100644 index 0000000000..83ff9d633e --- /dev/null +++ b/.changeset/lan-remote-control.md @@ -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. diff --git a/apps/kimi-code/src/cli/sub/vis.ts b/apps/kimi-code/src/cli/sub/vis.ts index 7e6f2e1f25..6fea80b985 100644 --- a/apps/kimi-code/src/cli/sub/vis.ts +++ b/apps/kimi-code/src/cli/sub/vis.ts @@ -21,6 +21,7 @@ export interface StartedVisServer { readonly port: number; readonly host: string; readonly url: string; + readonly lanUrls?: string[]; readonly close: () => Promise; } @@ -86,6 +87,12 @@ export async function handleVis(deps: VisDeps, opts: VisOptions): Promise : `${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) { diff --git a/apps/vis/server/src/config.ts b/apps/vis/server/src/config.ts index 0af5b9f32d..ed258c2195 100644 --- a/apps/vis/server/src/config.ts +++ b/apps/vis/server/src/config.ts @@ -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 { diff --git a/apps/vis/server/src/index.ts b/apps/vis/server/src/index.ts index 0047337367..80ef2b47b5 100644 --- a/apps/vis/server/src/index.ts +++ b/apps/vis/server/src/index.ts @@ -5,9 +5,9 @@ import { formatStartupBanner } from './startup-banner'; async function main(): Promise { 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 }), ); } diff --git a/apps/vis/server/src/start.ts b/apps/vis/server/src/start.ts index 1a33cf2ae0..f305f02b81 100644 --- a/apps/vis/server/src/start.ts +++ b/apps/vis/server/src/start.ts @@ -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 { @@ -18,6 +18,7 @@ export interface StartedVisServer { readonly port: number; readonly host: string; readonly url: string; + readonly lanUrls?: string[]; readonly close: () => Promise; } @@ -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((done, fail) => { server.close((err?: Error) => (err ? fail(err) : done())); diff --git a/apps/vis/server/src/startup-banner.ts b/apps/vis/server/src/startup-banner.ts index a32589e89b..d2c2c457f2 100644 --- a/apps/vis/server/src/startup-banner.ts +++ b/apps/vis/server/src/startup-banner.ts @@ -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; } diff --git a/apps/vis/server/test/lib/config.test.ts b/apps/vis/server/test/lib/config.test.ts index e0f064fe59..c938c82053 100644 --- a/apps/vis/server/test/lib/config.test.ts +++ b/apps/vis/server/test/lib/config.test.ts @@ -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', () => { @@ -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/'); + } + }); +}); diff --git a/packages/agent-core/src/profile/default/system.md b/packages/agent-core/src/profile/default/system.md index 0671cd1084..e5b9b34187 100644 --- a/packages/agent-core/src/profile/default/system.md +++ b/packages/agent-core/src/profile/default/system.md @@ -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.