Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
224fa51
feat(kimi-code): add remote control web tunnel
sailist Aug 17, 2026
3bb6ded
fix(kimi-code): prevent remote control websocket crash
sailist Aug 18, 2026
18540a6
fix(kimi-code): align websocket dependency versions
sailist Aug 18, 2026
9bf08da
fix(kimi-code): harden remote control connection setup
sailist Aug 18, 2026
369f512
fix(kimi-code): fix remote control rewriting, caching, and WS frame loss
sailist Aug 19, 2026
d70ad81
feat(kimi-code): add remote control QR output
sailist Aug 21, 2026
3aaf1b0
build: update pnpm dependencies hash
sailist Aug 21, 2026
118c484
refactor(kimi-code): remove the --allow-remote-terminals flag
sailist Aug 21, 2026
cb4aed4
Merge remote-tracking branch 'origin/main' into feat-000-08-06-rc
liruifengv Aug 21, 2026
d594222
Merge branch 'main' into feat-000-08-06-rc
liruifengv Aug 21, 2026
c2ef5d2
feat(kimi-code): add remote control lock, rc command, and QR fixes
sailist Aug 23, 2026
e8d4eee
fix(kap-server): broadcast user prompts to all session clients on submit
sailist Aug 23, 2026
7a2c323
fix(node-sdk): drop v2-only prompt.started from SDK event stream
sailist Aug 23, 2026
cafe039
ci(pkg-pr-new): post custom install comment for npm 12 compatibility
sailist Aug 25, 2026
d689e13
feat(kimi-code): render remote control QR as inline image on capable …
sailist Aug 25, 2026
c6ef37d
feat(kimi-code): improve remote control terminal output
sailist Aug 25, 2026
fbae2eb
test(agent-core-v2): update tool event snapshot
sailist Aug 25, 2026
4fb8f8d
revert(ci): keep preview workflow unchanged in rc pr
sailist Aug 25, 2026
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
5 changes: 5 additions & 0 deletions .changeset/add-remote-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Add Remote Control as an experimental feature for accessing a local web session remotely. Enable it with `KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL=1`, then run `kimi rc`, `kimi web --remote-control`, or `/remote-control` to start it.
5 changes: 5 additions & 0 deletions .changeset/broadcast-user-prompts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix messages sent from one web client not appearing on other clients connected to the same session.
5 changes: 5 additions & 0 deletions .changeset/drop-allow-remote-terminals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Remove the `--allow-remote-terminals` flag from `kimi web`; PTY terminal routes now stay available on loopback binds only.
6 changes: 6 additions & 0 deletions apps/kimi-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@
"@moonshot-ai/pi-tui": "workspace:^",
"@moonshot-ai/vis-server": "workspace:^",
"@moonshot-ai/vis-web": "workspace:*",
"@types/qrcode": "^1.5.6",
"@types/semver": "^7.7.0",
"@types/ws": "^8.18.0",
"@types/yazl": "^2.4.6",
"chalk": "^5.4.1",
"cli-highlight": "^2.1.11",
Expand All @@ -110,5 +112,9 @@
},
"engines": {
"node": ">=22.19.0"
},
"dependencies": {
"qrcode": "^1.5.4",
"ws": "^8.18.0"
}
}
8 changes: 8 additions & 0 deletions apps/kimi-code/src/cli/sub/web/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import type { Command } from 'commander';

import { registerDeprecatedServerCommand } from './deprecated-server';
import { isRemoteControlEnabled } from './remote-control';
import { registerRotateTokenCommand } from './rotate-token';
import { buildWebCommand } from './run';

Expand All @@ -24,4 +25,11 @@ export function registerWebCommand(program: Command): void {
);
registerRotateTokenCommand(web);
registerDeprecatedServerCommand(program);
buildWebCommand(
program
.command('rc', { hidden: !isRemoteControlEnabled() })
.alias('remote')
.description('Run the local Kimi server and open the web UI through Remote Control (experimental).'),
{ forceRemoteControl: true },
);
}
170 changes: 170 additions & 0 deletions apps/kimi-code/src/cli/sub/web/remote-control-lock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { randomBytes } from 'node:crypto';
import { mkdir, open, readFile, unlink } from 'node:fs/promises';
import { dirname, join } from 'node:path';

export interface RemoteControlLockInfo {
readonly pid: number;
readonly nonce: string;
readonly localOrigin: string;
readonly deviceId: string;
readonly url: string;
readonly startedAt: number;
}

interface RemoteControlLockDisk {
readonly pid: number;
readonly nonce: string;
readonly local_origin: string;
readonly device_id: string;
readonly url: string;
readonly started_at: number;
}

export class RemoteControlAlreadyRunningError extends Error {
readonly holder: RemoteControlLockInfo;

constructor(holder: RemoteControlLockInfo) {
super(formatRemoteControlAlreadyRunning(holder));
this.name = 'RemoteControlAlreadyRunningError';
this.holder = holder;
}
}

export function formatRemoteControlAlreadyRunning(holder: RemoteControlLockInfo): string {
return [
`Remote Control is already running on this machine (pid ${holder.pid}, ${holder.localOrigin}, since ${new Date(holder.startedAt).toLocaleString()}).`,
`Use the existing link: ${holder.url}`,
'To start a new one here, stop the other `kimi web --remote-control` process first.',
].join('\n');
}

export function remoteControlLockPath(homeDir: string): string {
return join(homeDir, 'server', 'rc.json');
}

export interface RemoteControlLock {
release(): Promise<void>;
}

const MAX_ACQUIRE_ATTEMPTS = 3;

export async function acquireRemoteControlLock(
homeDir: string,
details: { localOrigin: string; deviceId: string; url: string },
): Promise<RemoteControlLock> {
const lockPath = remoteControlLockPath(homeDir);
await mkdir(dirname(lockPath), { recursive: true });
const info: RemoteControlLockInfo = {
pid: process.pid,
nonce: randomBytes(8).toString('hex'),
localOrigin: details.localOrigin,
deviceId: details.deviceId,
url: details.url,
startedAt: Date.now(),
};
for (let attempt = 0; ; attempt += 1) {
try {
const handle = await open(lockPath, 'wx');
try {
await handle.writeFile(encodeLock(info));
} finally {
await handle.close();
}
return { release: () => releaseRemoteControlLock(lockPath, info.nonce) };
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST' || attempt >= MAX_ACQUIRE_ATTEMPTS) {
throw error;
}
const holder = await readRemoteControlLock(lockPath);
if (holder !== undefined && pidAlive(holder.pid)) {
throw new RemoteControlAlreadyRunningError(holder);
}
await removeFile(lockPath);
}
}
}

export async function inspectRemoteControlLock(
homeDir: string,
): Promise<RemoteControlLockInfo | undefined> {
const lockPath = remoteControlLockPath(homeDir);
const info = await readRemoteControlLock(lockPath);
if (info === undefined) return undefined;
if (!pidAlive(info.pid)) {
await removeFile(lockPath);
return undefined;
}
return info;
}

async function releaseRemoteControlLock(lockPath: string, nonce: string): Promise<void> {
const info = await readRemoteControlLock(lockPath);
if (info === undefined || info.nonce !== nonce) return;
await removeFile(lockPath);
}

async function readRemoteControlLock(lockPath: string): Promise<RemoteControlLockInfo | undefined> {
let raw: string;
try {
raw = await readFile(lockPath, 'utf8');
} catch {
return undefined;
}
return decodeLock(raw);
}

function encodeLock(info: RemoteControlLockInfo): string {
const disk: RemoteControlLockDisk = {
pid: info.pid,
nonce: info.nonce,
local_origin: info.localOrigin,
device_id: info.deviceId,
url: info.url,
started_at: info.startedAt,
};
return JSON.stringify(disk);
}

function decodeLock(raw: string): RemoteControlLockInfo | undefined {
try {
const parsed = JSON.parse(raw) as Partial<RemoteControlLockDisk>;
if (
typeof parsed.pid === 'number' &&
typeof parsed.nonce === 'string' &&
typeof parsed.local_origin === 'string' &&
typeof parsed.device_id === 'string' &&
typeof parsed.url === 'string' &&
typeof parsed.started_at === 'number'
) {
return {
pid: parsed.pid,
nonce: parsed.nonce,
localOrigin: parsed.local_origin,
deviceId: parsed.device_id,
url: parsed.url,
startedAt: parsed.started_at,
};
}
return undefined;
} catch {
return undefined;
}
}

async function removeFile(lockPath: string): Promise<void> {
try {
await unlink(lockPath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
}
}

function pidAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ESRCH') return false;
return true;
}
}
Loading
Loading