build(cli): bundle the capability wiring plugins into client releases

Vendor the official kimi-cu plugin (v0.5.4, from the CU team's plugin zip)
next to kimi-webbridge under plugins/official, copy both into
apps/kimi-code/bundled-plugins at build time, and ship them in the npm
package (files) and the native SEA blob (a new bundled-plugins asset set
extracted into the native cache at startup, published to the engine via
KIMI_CODE_BUNDLED_PLUGINS_DIR). Desktop points the same variable at its
extraResources copy. The .gitignore build-output entries are anchored so
sources under src/native and test/native stop being silently ignored.
This commit is contained in:
qer 2026-08-03 14:35:56 +08:00
parent d78c73329d
commit 8240dd32cc
14 changed files with 620 additions and 5 deletions

View file

@ -1,5 +1,5 @@
# Copied from packages/kimi-core at build time
agents/
/agents/
# Generated at build time by scripts/build-vis-asset.mjs.
# Only the ~150KB base64 VALUE file is ignored; the committed `.d.ts` stub
@ -8,4 +8,8 @@ agents/
src/generated/vis-web-asset.ts
# Copied from packages/pi-tui/native at build time by scripts/copy-native-assets.mjs
native/
# (anchored: src/native and test/native hold tracked sources)
/native/
# Copied from plugins/official at build time by scripts/copy-bundled-plugins.mjs
/bundled-plugins/

View file

@ -29,6 +29,7 @@
"dist",
"dist-web",
"native",
"bundled-plugins",
"scripts/postinstall.mjs",
"scripts/postinstall",
"README.md"
@ -50,7 +51,7 @@
"provenance": true
},
"scripts": {
"build": "pnpm -C ../kimi-web run build && tsdown && node scripts/copy-native-assets.mjs && node scripts/copy-web-assets.mjs",
"build": "pnpm -C ../kimi-web run build && tsdown && node scripts/copy-native-assets.mjs && node scripts/copy-web-assets.mjs && node scripts/copy-bundled-plugins.mjs",
"prebuild": "node scripts/build-vis-asset.mjs",
"catalog:update": "node scripts/update-catalog.mjs --out dist/built-in-catalog.json",
"smoke": "node scripts/smoke.mjs",

View file

@ -0,0 +1,38 @@
import { cp, mkdir, rm, stat } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const repoRoot = resolve(appRoot, '../..');
const source = resolve(repoRoot, 'plugins', 'official');
const target = resolve(appRoot, 'bundled-plugins');
// The built-in capability wiring plugins ship inside the client release (see
// packages/agent-core-v2/src/app/capability/bundledPlugins.ts). Only the two
// capability entries are bundled — other official plugins stay marketplace
// catalog entries.
const PLUGIN_IDS = ['kimi-cu', 'kimi-webbridge'];
async function assertPluginDir(id) {
const dir = resolve(source, id);
const manifest = resolve(dir, 'kimi.plugin.json');
try {
const info = await stat(manifest);
if (!info.isFile()) {
throw new Error('not a file');
}
} catch {
throw new Error(`Bundled plugin manifest was not found at ${manifest}.`);
}
return dir;
}
await rm(target, { recursive: true, force: true });
await mkdir(target, { recursive: true });
for (const id of PLUGIN_IDS) {
const srcDir = await assertPluginDir(id);
await cp(srcDir, resolve(target, id), { recursive: true });
}
console.log(`Copied bundled capability plugins (${PLUGIN_IDS.join(', ')}) to ${target}`);

View file

@ -6,6 +6,10 @@ import {
nativeAssetManifestKey,
nativeAssetSummary,
} from './assets.mjs';
import {
bundledPluginsManifestKey,
collectBundledPlugins,
} from './bundled-plugins.mjs';
import { fail, run } from './exec.mjs';
import {
appRoot,
@ -33,18 +37,29 @@ async function writeSeaConfig(target) {
target,
});
const web = await collectWebAssets({ appRoot, target });
const bundledPlugins = await collectBundledPlugins({ appRoot, target });
const manifestPath = resolve(nativeManifestDir(target), 'manifest.json');
const webManifestPath = resolve(nativeIntermediatesDir(), 'web-assets', target, 'manifest.json');
const bundledPluginsManifestPath = resolve(
nativeIntermediatesDir(),
'bundled-plugins',
target,
'manifest.json',
);
await mkdir(dirname(manifestPath), { recursive: true });
await mkdir(dirname(webManifestPath), { recursive: true });
await mkdir(dirname(bundledPluginsManifestPath), { recursive: true });
await writeFile(manifestPath, manifestJson);
await writeFile(webManifestPath, web.manifestJson);
await writeFile(bundledPluginsManifestPath, bundledPlugins.manifestJson);
const seaAssets = {
[nativeAssetManifestKey(target)]: manifestPath,
[webAssetManifestKey(target)]: webManifestPath,
[bundledPluginsManifestKey(target)]: bundledPluginsManifestPath,
...assets,
...web.assets,
...bundledPlugins.assets,
};
const config = {
main: nativeJsBundlePath(),
@ -65,6 +80,9 @@ async function writeSeaConfig(target) {
console.log(
`Collected web assets for ${web.manifest.target}: ${web.manifest.files.length} files`,
);
console.log(
`Collected bundled plugins for ${bundledPlugins.manifest.target}: ${bundledPlugins.manifest.files.length} files`,
);
}
export async function runSeaBlobStep() {

View file

@ -0,0 +1,33 @@
import { resolve } from 'node:path';
import {
BUNDLED_PLUGINS_MANIFEST_VERSION,
buildBundledPluginsAssetKey,
buildBundledPluginsManifestKey,
} from './manifest.mjs';
import { collectAssetRoot } from './web-assets.mjs';
export { BUNDLED_PLUGINS_MANIFEST_VERSION };
const BUNDLED_PLUGINS_DIR = 'bundled-plugins';
export function bundledPluginsManifestKey(target) {
return buildBundledPluginsManifestKey(target);
}
export function bundledPluginsAssetKey(target, relativePath) {
return buildBundledPluginsAssetKey(target, relativePath);
}
export async function collectBundledPlugins({ appRoot, target }) {
const buildCommand = 'pnpm --filter @moonshot-ai/kimi-code run build';
return collectAssetRoot({
appRoot,
target,
root: BUNDLED_PLUGINS_DIR,
requiredFile: 'kimi-webbridge/kimi.plugin.json',
missingMessage: `Bundled capability plugins were not found at ${resolve(appRoot, BUNDLED_PLUGINS_DIR)}. Run \`${buildCommand}\` (or scripts/copy-bundled-plugins.mjs) before building native SEA assets. App root: ${appRoot}`,
assetKey: bundledPluginsAssetKey,
version: BUNDLED_PLUGINS_MANIFEST_VERSION,
});
}

View file

@ -1,5 +1,6 @@
export const NATIVE_ASSET_MANIFEST_VERSION = 1;
export const WEB_ASSET_MANIFEST_VERSION = 1;
export const BUNDLED_PLUGINS_MANIFEST_VERSION = 1;
export function buildManifestKey(target) {
return `native/${target}/manifest.json`;
@ -20,3 +21,11 @@ export function buildWebManifestKey(target) {
export function buildWebAssetKey(target, relativePath) {
return `web/${target}/dist-web/${relativePath}`;
}
export function buildBundledPluginsManifestKey(target) {
return `bundled-plugins/${target}/manifest.json`;
}
export function buildBundledPluginsAssetKey(target, relativePath) {
return `bundled-plugins/${target}/files/${relativePath}`;
}

View file

@ -62,13 +62,14 @@ export function webAssetKey(target, relativePath) {
return buildWebAssetKey(target, relativePath);
}
async function collectAssetRoot({
export async function collectAssetRoot({
appRoot,
target,
root,
requiredFile,
missingMessage,
assetKey,
version = WEB_ASSET_MANIFEST_VERSION,
}) {
const assetRoot = resolve(appRoot, ...root.split('/'));
await assertBuiltAssetRoot({ assetRoot, requiredFile, message: missingMessage });
@ -91,7 +92,7 @@ async function collectAssetRoot({
}
const manifest = {
version: WEB_ASSET_MANIFEST_VERSION,
version,
target,
root,
files: manifestFiles,

View file

@ -36,6 +36,7 @@ import { runUpdatePreflight } from './cli/update/preflight';
import { createKimiCodeHostIdentity, getVersion } from './cli/version';
import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE, PROCESS_NAME } from './constant/app';
import { cleanupStaleNativeCacheForCurrent } from './native/native-assets';
import { publishNativeBundledPluginsDir } from './native/bundled-plugins';
import { installNativeModuleHook } from './native/module-hook';
import { runNativeAssetSmokeIfRequested } from './native/smoke';
@ -142,6 +143,16 @@ export function main(): void {
// invalid proxy URL is reported and ignored rather than aborting startup.
installGlobalProxyDispatcher();
installNativeModuleHook();
// SEA builds embed the capability wiring plugins (kimi-cu / kimi-webbridge)
// as blob assets; extract them into the native cache and point the engine's
// capability domain at them. A failure here must not abort startup — the
// engine's own probes still cover npm/dev, and a missing bundle surfaces as
// a clear capability-install error.
try {
publishNativeBundledPluginsDir();
} catch (error) {
log.warn('failed to extract bundled capability plugins', { error });
}
if (runNativeAssetSmokeIfRequested()) return;
// Start the background cleanup of stale native cache. Fire-and-forget; must not block startup or throw.

View file

@ -0,0 +1,214 @@
import { createHash } from 'node:crypto';
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { KIMI_BUILD_INFO } from '#/cli/build-info';
import {
getNativeCacheBase,
getSeaAssetSource,
type NativeAssetSource,
} from './native-assets';
import {
BUNDLED_PLUGINS_MANIFEST_VERSION as MANIFEST_VERSION,
buildBundledPluginsManifestKey,
} from '../../scripts/native/manifest.mjs';
export const BUNDLED_PLUGINS_MANIFEST_VERSION = MANIFEST_VERSION;
/**
* The environment variable the engine's capability domain reads to locate the
* bundled wiring plugins. Kept in sync with `BUNDLED_PLUGINS_DIR_ENV` in
* `packages/agent-core-v2/src/app/capability/bundledPlugins.ts` the app
* cannot import the engine constant (layering), so both sides pin the string.
*/
export const BUNDLED_PLUGINS_DIR_ENV = 'KIMI_CODE_BUNDLED_PLUGINS_DIR';
export interface BundledPluginsFile {
readonly assetKey: string;
readonly relativePath: string;
readonly sha256: string;
}
export interface BundledPluginsManifest {
readonly version: typeof BUNDLED_PLUGINS_MANIFEST_VERSION;
readonly target: string;
readonly root: 'bundled-plugins';
readonly files: readonly BundledPluginsFile[];
}
export type BundledPluginsSource = NativeAssetSource;
export interface BundledPluginsOptions {
readonly source?: BundledPluginsSource | null;
readonly manifest?: BundledPluginsManifest | null;
readonly cacheBase?: string;
readonly env?: NodeJS.ProcessEnv;
readonly platform?: NodeJS.Platform;
readonly homeDir?: string;
readonly version?: string;
}
type RawBundledPluginsManifest = Omit<BundledPluginsManifest, 'version' | 'root'> & {
readonly version: number;
readonly root: string;
};
function currentTarget(): string {
return KIMI_BUILD_INFO.buildTarget ?? `${process.platform}-${process.arch}`;
}
function toBuffer(value: ArrayBuffer | ArrayBufferView | Buffer | string): Buffer {
if (Buffer.isBuffer(value)) return value;
if (typeof value === 'string') return Buffer.from(value);
if (ArrayBuffer.isView(value)) {
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
}
return Buffer.from(value);
}
function sha256(bytes: Buffer | Uint8Array | string): string {
return createHash('sha256').update(bytes).digest('hex');
}
function sanitizeSegment(value: string): string {
const sanitized = value.replaceAll(/[^a-zA-Z0-9._-]/g, '_');
return sanitized.length > 0 ? sanitized : 'unknown';
}
function readFileSha256(path: string): string | null {
try {
return sha256(readFileSync(path));
} catch {
return null;
}
}
function ensureFile(path: string, bytes: Buffer, expectedSha256: string): void {
if (readFileSha256(path) === expectedSha256) return;
mkdirSync(dirname(path), { recursive: true });
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
writeFileSync(tempPath, bytes, { mode: 0o644 });
try {
renameSync(tempPath, path);
return;
} catch {
if (readFileSha256(path) === expectedSha256) {
rmSync(tempPath, { force: true });
return;
}
}
try {
rmSync(path, { force: true });
renameSync(tempPath, path);
} catch (error) {
rmSync(tempPath, { force: true });
if (readFileSha256(path) === expectedSha256) return;
throw error;
}
}
function assertSafeRelativePath(relativePath: string): void {
if (
relativePath.length === 0 ||
relativePath.startsWith('/') ||
relativePath.includes('\\') ||
relativePath.split('/').includes('..') ||
/^[A-Za-z]:/.test(relativePath)
) {
throw new Error(`Invalid bundled plugin relative path: ${relativePath}`);
}
}
export function bundledPluginsManifestKey(target: string = currentTarget()): string {
return buildBundledPluginsManifestKey(target);
}
export function getEmbeddedBundledPluginsManifest(
source: BundledPluginsSource | null = getSeaAssetSource(),
target = currentTarget(),
): BundledPluginsManifest | null {
if (source === null) return null;
const key = bundledPluginsManifestKey(target);
if (!source.getAssetKeys().includes(key)) return null;
const raw = source.getRawAsset(key);
const manifest = JSON.parse(toBuffer(raw).toString('utf-8')) as RawBundledPluginsManifest;
if (manifest.version !== BUNDLED_PLUGINS_MANIFEST_VERSION) {
throw new Error(`Unsupported bundled plugins manifest version: ${manifest.version}`);
}
if (manifest.target !== target) {
throw new Error(`Bundled plugins manifest target mismatch: ${manifest.target} !== ${target}`);
}
if (manifest.root !== 'bundled-plugins') {
throw new Error(`Unsupported bundled plugins root: ${manifest.root}`);
}
return manifest as BundledPluginsManifest;
}
export function getBundledPluginsCacheRoot(
manifest: BundledPluginsManifest,
options: BundledPluginsOptions = {},
): string {
const version = sanitizeSegment(options.version ?? KIMI_BUILD_INFO.version ?? 'dev');
const manifestHash = sha256(JSON.stringify(manifest));
return join(
getNativeCacheBase({
cacheBase: options.cacheBase,
env: options.env,
platform: options.platform,
homeDir: options.homeDir,
}),
'bundled-plugins',
version,
sanitizeSegment(manifest.target),
manifestHash,
manifest.root,
);
}
/**
* Extract the embedded bundled capability plugins (kimi-cu / kimi-webbridge
* wiring) into the native asset cache and return the directory that holds one
* `<plugin-id>/` subdirectory per plugin. Returns null when this is not a SEA
* build or the blob predates bundled plugins callers then leave the
* engine's own npm/dev probes to find the plugins.
*/
export function getNativeBundledPluginsDir(options: BundledPluginsOptions = {}): string | null {
const source = options.source ?? getSeaAssetSource();
if (source === null) return null;
const manifest = options.manifest ?? getEmbeddedBundledPluginsManifest(source, currentTarget());
if (manifest === null) return null;
const cacheRoot = getBundledPluginsCacheRoot(manifest, options);
for (const file of manifest.files) {
assertSafeRelativePath(file.relativePath);
const bytes = toBuffer(source.getRawAsset(file.assetKey));
const actualSha256 = sha256(bytes);
if (actualSha256 !== file.sha256) {
throw new Error(
`Bundled plugin checksum mismatch for ${file.assetKey}: ${actualSha256} !== ${file.sha256}`,
);
}
ensureFile(join(cacheRoot, file.relativePath), bytes, file.sha256);
}
return cacheRoot;
}
/**
* Publish the extracted bundled-plugins directory to the engine's capability
* domain (which reads `KIMI_CODE_BUNDLED_PLUGINS_DIR`). No-op outside SEA
* builds, where the npm layout / source checkout probes already resolve.
*/
export function publishNativeBundledPluginsDir(
options: BundledPluginsOptions = {},
env: NodeJS.ProcessEnv = process.env,
): void {
if (env[BUNDLED_PLUGINS_DIR_ENV] !== undefined) return;
const dir = getNativeBundledPluginsDir(options);
if (dir !== null) {
env[BUNDLED_PLUGINS_DIR_ENV] = dir;
}
}

View file

@ -0,0 +1,149 @@
import { createHash } from 'node:crypto';
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import {
BUNDLED_PLUGINS_DIR_ENV,
BUNDLED_PLUGINS_MANIFEST_VERSION,
getBundledPluginsCacheRoot,
getNativeBundledPluginsDir,
publishNativeBundledPluginsDir,
type BundledPluginsManifest,
type BundledPluginsSource,
} from '#/native/bundled-plugins';
function sha256(bytes: Buffer | string): string {
return createHash('sha256').update(bytes).digest('hex');
}
function fakeBundledPlugins(files: Record<string, string>): {
manifest: BundledPluginsManifest;
source: BundledPluginsSource;
} {
const manifest: BundledPluginsManifest = {
version: BUNDLED_PLUGINS_MANIFEST_VERSION,
target: 'test-target',
root: 'bundled-plugins',
files: Object.entries(files).map(([relativePath, content]) => ({
assetKey: `bundled-plugins/test-target/files/${relativePath}`,
relativePath,
sha256: sha256(content),
})),
};
const assets = new Map<string, Buffer>([
['bundled-plugins/test-target/manifest.json', Buffer.from(JSON.stringify(manifest))],
...Object.entries(files).map(([relativePath, content]) => [
`bundled-plugins/test-target/files/${relativePath}`,
Buffer.from(content),
] as const),
]);
return {
manifest,
source: {
getAssetKeys: () => [...assets.keys()],
getRawAsset: (assetKey) => {
const asset = assets.get(assetKey);
if (asset === undefined) throw new Error(`missing test asset: ${assetKey}`);
return asset;
},
},
};
}
const PLUGIN_FILES: Record<string, string> = {
'kimi-cu/kimi.plugin.json': '{"name":"kimi-cu"}\n',
'kimi-cu/bin/kimi-cu-mcp': '#!/bin/sh\n',
'kimi-webbridge/kimi.plugin.json': '{"name":"kimi-webbridge"}\n',
'kimi-webbridge/skills/kimi-webbridge/SKILL.md': '# skill\n',
};
describe('bundled plugins (native SEA assets)', () => {
it('extracts embedded plugin files into a bundled-plugins cache directory', () => {
const dir = mkdtempSync(join(tmpdir(), 'kimi-bundled-plugins-runtime-'));
try {
const { manifest, source } = fakeBundledPlugins(PLUGIN_FILES);
const pluginsDir = getNativeBundledPluginsDir({
cacheBase: dir,
manifest,
source,
version: 'test',
});
expect(pluginsDir).toBe(
getBundledPluginsCacheRoot(manifest, { cacheBase: dir, version: 'test' }),
);
for (const [relativePath, content] of Object.entries(PLUGIN_FILES)) {
expect(readFileSync(join(pluginsDir ?? '', ...relativePath.split('/')), 'utf-8')).toBe(
content,
);
}
expect(existsSync(join(dir, 'bundled-plugins', 'test', 'test-target'))).toBe(true);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it('repairs corrupted extracted files on the next lookup', () => {
const dir = mkdtempSync(join(tmpdir(), 'kimi-bundled-plugins-repair-'));
try {
const { manifest, source } = fakeBundledPlugins(PLUGIN_FILES);
const pluginsDir = getNativeBundledPluginsDir({
cacheBase: dir,
manifest,
source,
version: 'test',
});
const manifestPath = join(pluginsDir ?? '', 'kimi-cu', 'kimi.plugin.json');
writeFileSync(manifestPath, 'broken');
const repairedDir = getNativeBundledPluginsDir({
cacheBase: dir,
manifest,
source,
version: 'test',
});
expect(repairedDir).toBe(pluginsDir);
expect(readFileSync(manifestPath, 'utf-8')).toBe('{"name":"kimi-cu"}\n');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it('returns null when no SEA bundled-plugins source is available', () => {
expect(getNativeBundledPluginsDir({ source: null })).toBeNull();
});
it('publishes the extracted directory through the engine env contract', () => {
const dir = mkdtempSync(join(tmpdir(), 'kimi-bundled-plugins-publish-'));
try {
const { manifest, source } = fakeBundledPlugins(PLUGIN_FILES);
const env: NodeJS.ProcessEnv = {};
publishNativeBundledPluginsDir({ cacheBase: dir, manifest, source, version: 'test' }, env);
const published = env[BUNDLED_PLUGINS_DIR_ENV];
expect(published).toBe(
getBundledPluginsCacheRoot(manifest, { cacheBase: dir, version: 'test' }),
);
// An explicit override (desktop extraResources, tests) always wins.
const overrideEnv: NodeJS.ProcessEnv = { [BUNDLED_PLUGINS_DIR_ENV]: '/opt/custom' };
publishNativeBundledPluginsDir(
{ cacheBase: dir, manifest, source, version: 'test' },
overrideEnv,
);
expect(overrideEnv[BUNDLED_PLUGINS_DIR_ENV]).toBe('/opt/custom');
// No SEA source → nothing published.
const emptyEnv: NodeJS.ProcessEnv = {};
publishNativeBundledPluginsDir({ source: null }, emptyEnv);
expect(emptyEnv[BUNDLED_PLUGINS_DIR_ENV]).toBeUndefined();
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});

View file

@ -0,0 +1,17 @@
# Kimi Computer Use
让 AI agent 在 macOS 上安静地操作图形界面:读取任意 app 的界面状态(无障碍树 + 截图),并在后台完成点击、输入、滚动、拖拽 —— 全程不移动你的鼠标、不把目标 app 切到前台,你可以继续正常使用电脑。
## 前置:安装 KimiCU.app
本插件依赖本机已安装的 KimiCU.app。一键安装
```bash
curl -fsSL https://cdn.kimi.com/kimi-computer-use/latest/setup_macos.sh | bash
```
安装后,在 **系统设置 → 隐私与安全性** 中为 KimiCU 开启 **辅助功能****屏幕录制**
## 工具
`list_apps` · `get_app_state` · `click` · `type_text` · `press_key` · `scroll` · `set_value` · `perform_secondary_action` · `select_text` · `drag`

View file

@ -0,0 +1,9 @@
#!/bin/sh
# kimi-code 只允许 stdio 插件的 command 是 PATH 命令或 ./ 相对路径,
# 故经由本 wrapper 转发到系统安装的 KimiCU.app。
APP="/Applications/KimiCU.app/Contents/MacOS/kimi-cu"
if [ ! -x "$APP" ]; then
echo "KimiCU.app 未安装,安装命令: curl -fsSL https://cdn.kimi.com/kimi-computer-use/latest/setup_macos.sh | bash" >&2
exit 1
fi
exec "$APP" mcp

View file

@ -0,0 +1,52 @@
{
"$schema": "https://kimi.com/schemas/kimi.plugin.schema.json",
"name": "kimi-cu",
"version": "0.5.4",
"description": "macOS Computer Use后台读取无障碍树 + 截图,不抢鼠标、不切前台地完成点击 / 输入 / 滚动 / 拖拽。",
"keywords": [
"computer-use",
"macos",
"accessibility",
"automation",
"screenshot",
"gui"
],
"author": "Moonshot AI",
"license": "Proprietary",
"skills": "./skills/",
"interface": {
"displayName": "Kimi Computer Use",
"shortDescription": "不抢鼠标、不切前台的 macOS 后台 GUI 操作",
"longDescription": "Kimi Computer Use 让 AI agent 在 macOS 上安静地操作图形界面通过无障碍AX树 + 智能截图读取任意 app 的界面状态,并在后台完成点击、输入、滚动、拖拽、调用元素自带动作等操作 —— 全程不移动用户真实鼠标、不把目标 app 切到前台,用户可以继续正常使用电脑。\n\n前置要求本插件依赖本机已安装的 KimiCU.app并需在系统设置中授予「辅助功能」与「屏幕录制」权限。一键安装\n\ncurl -fsSL https://cdn.kimi.com/kimi-computer-use/latest/setup_macos.sh | bash",
"developerName": "Moonshot AI",
"iconUrl": "https://cdn.kimi.com/kimi-computer-use/assets/icon.png",
"category": "PRODUCTIVITY",
"hostKind": "local",
"platforms": ["macos"],
"mcpOverrides": {
"mac": {
"displayName": "Kimi Computer Use",
"iconUrl": "https://cdn.kimi.com/kimi-computer-use/assets/icon.png"
}
}
},
"mcpServers": {
"mac": {
"command": "sh",
"args": ["./bin/kimi-cu-mcp"],
"cwd": "./",
"enabledTools": [
"list_apps",
"get_app_state",
"click",
"type_text",
"press_key",
"scroll",
"set_value",
"perform_secondary_action",
"select_text",
"drag"
]
}
}
}

View file

@ -0,0 +1,59 @@
---
name: kimi-cu
description: |
macOS Computer Use操作本机桌面 app。当用户要求点击、输入、滚动、拖拽、读取某个 app 的界面内容或截图,说「帮我点一下 X」「在 Y 里输入」「看看 Z app 什么状态」「列一下打开的 app」或任何需要在 macOS 图形界面上代替用户动手的请求时,使用本 skill。所有操作后台执行不抢用户鼠标、不切换前台。
---
# kimi-cu — macOS 后台 GUI 操作
后台读取无障碍AX树 + 截图感知界面,后台注入事件完成操作,用户的鼠标、键盘、前台焦点全程不受影响。各工具的参数细节以工具自身的 schema 为准,本文只讲跨工具的工作流和守则。
## 工作流
1. `list_apps` 找到目标 app。
2. `get_app_state` 获取无障碍树 + 截图,这是后续操作的状态来源:树节点的 **index**`click` / `set_value` / `perform_secondary_action` / `select_text` 直接引用;**截图像素坐标**供 `click` / `scroll` / `drag` 使用(工具自动换算到真实窗口)。需要低上下文观察时用 `mode:"image"` 只取截图、`mode:"ax"` 只取 AX 文本;只关心部分元素时加 `ax_filter`
3. 执行操作。
4. 重要操作后再次 `get_app_state` 验证界面达到预期。界面状态以工具返回为准,不要凭空断言成功。
读取界面、拿截图、取坐标都必须使用 MCP 的 `get_app_state`。不要通过 shell 跑 `kimi-cu shot` / `app-state` / `windows` 来替代 MCP这些 CLI 不会刷新 MCP 快照缓存,用它们得到的坐标/index 不能作为后续 MCP 操作依据。
界面一旦变化,旧快照的 index 和坐标即失效,必须重新 `get_app_state`;找不到目标元素时,先 `scroll` 让它进入可视区域再重新获取。
## 用法偏好
- 输入普通表单优先 `set_value`原子、写后校验Electron/Web 文本框走无前台后台替换路径:先清空再写入,目标窗口被全覆盖时自动改用 AX 聚焦 + 编辑命令清空 + AX 直写兜底,无需露出窗口、绝不把目标 app 拉前台聊天框、富文本、Electron/Web 输入区仍优先使用 `type_text`,因为它能显式控制聚焦、清空和发送。`type_text` 可传可选 `index` 或截图坐标 `x/y` 先聚焦目标再输入:内部走真实后台鼠标点击建立渲染层焦点——这是 Electron/Web 输入区能收到键盘输入的前提。**聚焦输入框请用 `type_text``index`,不要用 `click(index)`**(后者走 AXPress只设 AX 焦点,渲染层未聚焦,打字会落空)。都不传 `index/x/y` 时,`type_text` 只向当前焦点注入。
- 列表重排、删除项等优先 `perform_secondary_action` 调用元素自带动作,`drag` 是最后手段。
- **右键菜单 / 上下文菜单**:优先 `perform_secondary_action(index, action:"AXShowMenu")`——原生 app 与 Electron/Web 通常都可靠。`click(mouse_button:"right")` 是合成真实右键,**只对原生 app 有效Electron/Web 会忽略**,这类应用别用右键 `click` 弹菜单。
- **滚动**:原生滚动区/列表优先 `scroll(index, dy)`(按元素 AX 滚动整页粒度dy>0 上);普通页面用 `scroll(x, y, dy)` 截图坐标。
## 安全守则
1. 删除、发送、提交、付款等不可逆操作,执行前向用户复述将做的事并获得确认。
2. 不要用 AppleScript / cliclick 等手段绕过设计不变量(永不移动真实鼠标、永不切换前台)。
3. 截图中出现密码、银行等敏感内容时,仅完成用户明确要求的操作,不读取或复述无关信息。
## 排障(工具调用失败时)
KimiCU 的权限由 launchd 后台服务持有并在服务内执行操作agent 进程不需要也不会有这些权限。因此**不要用 `kimi-cu doctor` 判断权限**——它检查的是调用者进程,从你这里跑必然显示 ❌,不代表服务故障。
按顺序排查Bash
```bash
ls /Applications/KimiCU.app/Contents/MacOS/kimi-cu # 1) app 是否存在
/Applications/KimiCU.app/Contents/MacOS/kimi-cu service-status # 2) 服务是否注册运行
/Applications/KimiCU.app/Contents/MacOS/kimi-cu xpc-ping # 3) 权限判定以这条为准
# 正常输出permissionStatus: accessibility=true screenRecording=true
```
- **app 不存在** → 告知用户KimiCU.app 未安装,请运行以下命令安装(或经用户同意后代为执行):
```bash
curl -fsSL https://cdn.kimi.com/kimi-computer-use/latest/setup_macos.sh | bash
```
- **服务未运行 / xpc-ping 不通**`/Applications/KimiCU.app/Contents/MacOS/kimi-cu install`
- **权限为 false或截图全黑/树为空**`/Applications/KimiCU.app/Contents/MacOS/kimi-cu request-permissions --ax --screen`,并告知用户去「系统设置 → 隐私与安全性」打开 KimiCU 的「辅助功能」「屏幕录制」开关(系统要求必须用户手点),完成后直接重试即可。
CLI 仅用于以上排障,正常操作一律走 MCP 工具;尤其不要用 `kimi-cu shot` 做日常截图观察。
## 版本更新
工具结果末尾若出现「KimiCU 有新版本」提示,请转告用户:在终端运行 `kimi-cu upgrade` 即可更新到最新版(经用户同意后也可代为执行)。