@capclaw/mcp-stdio 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -2
- package/bin/capclaw-mcp-stdio.mjs +274 -0
- package/bin/capclaw-plugin-install.mjs +12 -0
- package/host-plugins/claude/.claude-plugin/plugin.json +19 -0
- package/host-plugins/claude/skills/capclaw-video-editor/SKILL.md +21 -0
- package/host-plugins/codex/.codex-plugin/plugin.json +17 -0
- package/host-plugins/codex/skills/capclaw-video-editor/SKILL.md +31 -0
- package/host-plugins/codex/skills/capclaw-video-editor/agents/openai.yaml +9 -0
- package/host-plugins/dsh/cordis.patch.template.yml +7 -0
- package/host-plugins/dsh/skills/capclaw-video-editor/SKILL.md +23 -0
- package/host-plugins/workbuddy/.codebuddy-plugin/plugin.json +16 -0
- package/host-plugins/workbuddy/.mcp.json +14 -0
- package/host-plugins/workbuddy/agents/capclaw-editor.md +28 -0
- package/host-plugins/workbuddy/skills/capclaw-video-editor/SKILL.md +23 -0
- package/package.json +18 -4
- package/service/hostPluginInstaller.js +432 -0
package/README.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@capclaw/mcp-stdio`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official MCP `stdio` proxy and multi-host plugin installer for **CapClaw (开刻)** AI Video Editor (`https://www.capclaw.cn`).
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### 1. Run as an MCP `stdio` server (WorkBuddy / Claude Code / OpenAI Codex)
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"capclaw-video-editor": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "@capclaw/mcp-stdio"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 2. Install DeepSeek Harness (`dsh`) Plugin & Skill
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx -y @capclaw/mcp-stdio --install-dsh
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 3. Install All 4 Host Plugins Locally (`codex`, `claude`, `workbuddy`, `dsh`)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx -y @capclaw/mcp-stdio --install-all
|
|
30
|
+
```
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CapClaw MCP stdio-to-HTTP Bridge & Host Plugin CLI (Zero external dependencies).
|
|
5
|
+
*
|
|
6
|
+
* Used by stdio-based or auto-waking MCP hosts (Codex, Claude Code, WorkBuddy, dsh):
|
|
7
|
+
* 1. Reads ~/.capclaw/runtime.json to discover the active sidecar port (5600-5639).
|
|
8
|
+
* 2. Probes GET http://127.0.0.1:<port>/mcp/health.
|
|
9
|
+
* 3. If CapClaw is not running, spawns `CapClaw.exe --background` (detached, no window)
|
|
10
|
+
* and polls up to 15 seconds until runtime.json and /mcp/health are ready.
|
|
11
|
+
* 4. Forwards newline-delimited JSON-RPC messages from stdin to POST http://127.0.0.1:<port>/mcp
|
|
12
|
+
* and writes JSON-RPC responses back to stdout.
|
|
13
|
+
*
|
|
14
|
+
* CLI Installer flags:
|
|
15
|
+
* npx -y @capclaw/mcp-stdio --install-dsh
|
|
16
|
+
* npx -y @capclaw/mcp-stdio --install-workbuddy
|
|
17
|
+
* npx -y @capclaw/mcp-stdio --install-claude
|
|
18
|
+
* npx -y @capclaw/mcp-stdio --install-codex
|
|
19
|
+
* npx -y @capclaw/mcp-stdio --install-all
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import os from 'node:os';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import readline from 'node:readline';
|
|
26
|
+
import { spawn } from 'node:child_process';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
|
|
29
|
+
const DEFAULT_PORTS = Array.from({ length: 40 }, (_, i) => 5600 + i);
|
|
30
|
+
|
|
31
|
+
function getRuntimeFilePath() {
|
|
32
|
+
if (process.env.CAPCLAW_RUNTIME_FILE) {
|
|
33
|
+
return path.resolve(process.env.CAPCLAW_RUNTIME_FILE);
|
|
34
|
+
}
|
|
35
|
+
const home = process.env.CAPCLAW_HOME || os.homedir();
|
|
36
|
+
return path.join(home, '.capclaw', 'runtime.json');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readRuntimeFile() {
|
|
40
|
+
const filePath = getRuntimeFilePath();
|
|
41
|
+
try {
|
|
42
|
+
if (!fs.existsSync(filePath)) return null;
|
|
43
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
44
|
+
const data = JSON.parse(raw);
|
|
45
|
+
if (!data || typeof data.port !== 'number') return null;
|
|
46
|
+
return data;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function probeMcpHealth(port, timeoutMs = 1200) {
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(`http://127.0.0.1:${port}/mcp/health`, {
|
|
57
|
+
method: 'GET',
|
|
58
|
+
signal: controller.signal,
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) return false;
|
|
61
|
+
const body = await res.json().catch(() => null);
|
|
62
|
+
return Boolean(body && body.ok);
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
} finally {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function findCandidateExePaths(lastKnownExePath = '') {
|
|
71
|
+
const candidates = [];
|
|
72
|
+
if (process.env.CAPCLAW_EXE_PATH) {
|
|
73
|
+
candidates.push(process.env.CAPCLAW_EXE_PATH);
|
|
74
|
+
}
|
|
75
|
+
if (lastKnownExePath) {
|
|
76
|
+
candidates.push(lastKnownExePath);
|
|
77
|
+
}
|
|
78
|
+
if (process.platform === 'win32') {
|
|
79
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
80
|
+
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
|
81
|
+
candidates.push(
|
|
82
|
+
path.join(localAppData, 'Programs', 'capclaw', '开刻.exe'),
|
|
83
|
+
path.join(localAppData, 'Programs', 'capclaw', 'CapClaw.exe'),
|
|
84
|
+
path.join(localAppData, 'Programs', 'yuanyuan-client', '开刻.exe'),
|
|
85
|
+
path.join(programFiles, '开刻', '开刻.exe'),
|
|
86
|
+
path.join(programFiles, 'CapClaw', 'CapClaw.exe'),
|
|
87
|
+
);
|
|
88
|
+
} else if (process.platform === 'darwin') {
|
|
89
|
+
candidates.push(
|
|
90
|
+
'/Applications/开刻.app/Contents/MacOS/开刻',
|
|
91
|
+
'/Applications/CapClaw.app/Contents/MacOS/CapClaw',
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return candidates.filter((p) => p && fs.existsSync(p));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function discoverOrWakeSidecar() {
|
|
98
|
+
// 1. Check runtime.json first
|
|
99
|
+
const runtime = readRuntimeFile();
|
|
100
|
+
if (runtime && runtime.port && (await probeMcpHealth(runtime.port))) {
|
|
101
|
+
return { port: runtime.port, mcpEndpoint: `http://127.0.0.1:${runtime.port}/mcp` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 2. Scan first few ports in 5600..5639 in case runtime.json was stale
|
|
105
|
+
for (const port of DEFAULT_PORTS.slice(0, 8)) {
|
|
106
|
+
if (await probeMcpHealth(port, 400)) {
|
|
107
|
+
return { port, mcpEndpoint: `http://127.0.0.1:${port}/mcp` };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 3. Auto-spawn CapClaw in background mode if executable is available
|
|
112
|
+
const exeCandidates = findCandidateExePaths(runtime?.exePath || '');
|
|
113
|
+
if (exeCandidates.length > 0 && process.env.CAPCLAW_DISABLE_AUTO_WAKE !== '1') {
|
|
114
|
+
const exePath = exeCandidates[0];
|
|
115
|
+
try {
|
|
116
|
+
const child = spawn(exePath, ['--background'], {
|
|
117
|
+
detached: true,
|
|
118
|
+
stdio: 'ignore',
|
|
119
|
+
windowsHide: true,
|
|
120
|
+
});
|
|
121
|
+
child.unref();
|
|
122
|
+
} catch (err) {
|
|
123
|
+
process.stderr.write(`[capclaw-mcp-stdio] Failed to spawn ${exePath}: ${err.message}\n`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const deadline = Date.now() + 15000;
|
|
127
|
+
while (Date.now() < deadline) {
|
|
128
|
+
await new Promise((r) => setTimeout(r, 350));
|
|
129
|
+
const latest = readRuntimeFile();
|
|
130
|
+
if (latest && latest.port && (await probeMcpHealth(latest.port))) {
|
|
131
|
+
return { port: latest.port, mcpEndpoint: `http://127.0.0.1:${latest.port}/mcp` };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new Error(
|
|
137
|
+
'无法连接到开刻(CapClaw)本地服务。请先启动开刻桌面端(https://www.capclaw.cn),或设置 CAPCLAW_EXE_PATH 指向开刻可执行文件。',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let cachedEndpoint = null;
|
|
142
|
+
let mcpSessionId = '';
|
|
143
|
+
let mcpProtocolVersion = '2025-06-18';
|
|
144
|
+
|
|
145
|
+
async function forwardJsonRpcLine(line) {
|
|
146
|
+
const trimmed = line.trim();
|
|
147
|
+
if (!trimmed) return;
|
|
148
|
+
|
|
149
|
+
let parsed;
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(trimmed);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
process.stdout.write(
|
|
154
|
+
`${JSON.stringify({
|
|
155
|
+
jsonrpc: '2.0',
|
|
156
|
+
id: null,
|
|
157
|
+
error: { code: -32700, message: `Invalid JSON on stdin: ${err.message}` },
|
|
158
|
+
})}\n`,
|
|
159
|
+
);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const isNotification = !Array.isArray(parsed) && parsed && parsed.id === undefined;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
if (!cachedEndpoint) {
|
|
167
|
+
cachedEndpoint = await discoverOrWakeSidecar();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let res;
|
|
171
|
+
try {
|
|
172
|
+
res = await fetch(cachedEndpoint.mcpEndpoint, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: {
|
|
175
|
+
'Content-Type': 'application/json',
|
|
176
|
+
...(mcpSessionId ? { 'Mcp-Session-Id': mcpSessionId } : {}),
|
|
177
|
+
...(mcpProtocolVersion ? { 'MCP-Protocol-Version': mcpProtocolVersion } : {}),
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify(parsed),
|
|
180
|
+
});
|
|
181
|
+
} catch {
|
|
182
|
+
// Sidecar may have restarted on another port; rediscover once
|
|
183
|
+
cachedEndpoint = await discoverOrWakeSidecar();
|
|
184
|
+
res = await fetch(cachedEndpoint.mcpEndpoint, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers: {
|
|
187
|
+
'Content-Type': 'application/json',
|
|
188
|
+
...(mcpSessionId ? { 'Mcp-Session-Id': mcpSessionId } : {}),
|
|
189
|
+
...(mcpProtocolVersion ? { 'MCP-Protocol-Version': mcpProtocolVersion } : {}),
|
|
190
|
+
},
|
|
191
|
+
body: JSON.stringify(parsed),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const respSession = res.headers.get('mcp-session-id');
|
|
196
|
+
if (respSession) mcpSessionId = respSession;
|
|
197
|
+
const respVersion = res.headers.get('mcp-protocol-version');
|
|
198
|
+
if (respVersion) mcpProtocolVersion = respVersion;
|
|
199
|
+
|
|
200
|
+
if (res.status === 202 || isNotification) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const text = await res.text();
|
|
205
|
+
if (text && text.trim()) {
|
|
206
|
+
process.stdout.write(`${text.trim()}\n`);
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (isNotification) return;
|
|
210
|
+
const id = !Array.isArray(parsed) && parsed ? parsed.id ?? null : null;
|
|
211
|
+
process.stdout.write(
|
|
212
|
+
`${JSON.stringify({
|
|
213
|
+
jsonrpc: '2.0',
|
|
214
|
+
id,
|
|
215
|
+
error: {
|
|
216
|
+
code: -32000,
|
|
217
|
+
message: err.message || String(err),
|
|
218
|
+
},
|
|
219
|
+
})}\n`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function startStdioBridge() {
|
|
225
|
+
const rl = readline.createInterface({
|
|
226
|
+
input: process.stdin,
|
|
227
|
+
crlfDelay: Infinity,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
let queue = Promise.resolve();
|
|
231
|
+
rl.on('line', (line) => {
|
|
232
|
+
queue = queue.then(() => forwardJsonRpcLine(line)).catch((err) => {
|
|
233
|
+
process.stderr.write(`[capclaw-mcp-stdio] Unexpected error: ${err.message}\n`);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
rl.on('close', () => {
|
|
238
|
+
queue.finally(() => process.exit(0));
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function handleCliInstallFlags(argv) {
|
|
243
|
+
const flagMap = {
|
|
244
|
+
'--install-dsh': 'dsh',
|
|
245
|
+
'--install-workbuddy': 'workbuddy',
|
|
246
|
+
'--install-claude': 'claude',
|
|
247
|
+
'--install-codex': 'codex',
|
|
248
|
+
'--install-all': 'all',
|
|
249
|
+
};
|
|
250
|
+
const matchedFlag = argv.find((arg) => Object.prototype.hasOwnProperty.call(flagMap, arg));
|
|
251
|
+
if (!matchedFlag) return false;
|
|
252
|
+
|
|
253
|
+
const require = createRequire(import.meta.url);
|
|
254
|
+
const { installHostPlugin, installAllHostPlugins } = require('../service/hostPluginInstaller.js');
|
|
255
|
+
const runtime = readRuntimeFile();
|
|
256
|
+
const port = runtime?.port || 5600;
|
|
257
|
+
const target = flagMap[matchedFlag];
|
|
258
|
+
|
|
259
|
+
const result = target === 'all'
|
|
260
|
+
? installAllHostPlugins({ port })
|
|
261
|
+
: installHostPlugin(target, { port });
|
|
262
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
import.meta.url === `file://${process.argv[1]?.replace(/\\/g, '/')}` ||
|
|
268
|
+
process.argv[1]?.endsWith('capclaw-mcp-stdio.mjs') ||
|
|
269
|
+
process.argv[1]?.endsWith('capclaw-mcp-stdio')
|
|
270
|
+
) {
|
|
271
|
+
if (!handleCliInstallFlags(process.argv.slice(2))) {
|
|
272
|
+
startStdioBridge();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const { installHostPlugin, installAllHostPlugins } = require('../service/hostPluginInstaller.js');
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const hostArg = args.find((a) => !a.startsWith('-')) || 'all';
|
|
10
|
+
|
|
11
|
+
const res = hostArg === 'all' ? installAllHostPlugins() : installHostPlugin(hostArg);
|
|
12
|
+
process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "capclaw-video-editor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "开刻(CapClaw)本地 AI 视频剪辑与画布编排插件(零拷贝素材导入、18 种确定性画布指令、宿主内置浏览器实时编辑、本地 WebCodecs 硬件加速合成导出)",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "CapClaw Team"
|
|
7
|
+
},
|
|
8
|
+
"skills": [
|
|
9
|
+
"./skills/capclaw-video-editor"
|
|
10
|
+
],
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"capclaw-video-editor": {
|
|
13
|
+
"command": "node",
|
|
14
|
+
"args": [
|
|
15
|
+
"{{CAPCLAW_STDIO_PROXY_PATH}}"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: capclaw-video-editor
|
|
3
|
+
description: 使用开刻(CapClaw)本地视频剪辑引擎完成草稿管理、零拷贝本地素材导入、18 种确定性画布与时间线编辑、云端 AI 编排对话、宿主内置浏览器可视化精修及本地硬件加速视频合成导出。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 开刻(CapClaw)Claude Code 视频剪辑技能
|
|
7
|
+
|
|
8
|
+
通过 `capclaw-video-editor` MCP Server 提供的 7 个工具操作本地开刻视频草稿:
|
|
9
|
+
|
|
10
|
+
1. **`manage_project`**:`list` / `get` / `create` / `rename` / `delete` 本地草稿。
|
|
11
|
+
2. **`inspect_timeline`**:读取草稿画布尺寸与组件时间线详情。
|
|
12
|
+
3. **`import_local_media`**:零拷贝导入本机视频/图片/音频文件(支持 `addToTimeline: true`)。
|
|
13
|
+
4. **`apply_canvas_commands`**:执行 18 种公开画布指令(严禁调用内部指令 `ADD_ANNOTATION_RESULT` / `APPLY_WIDGET_VERSION` / `SWITCH_WIDGET_VERSION`)。
|
|
14
|
+
5. **`ai_edit_turn`**:调用开刻云端 AI 画布助手将自然语言转换为画布指令并应用。
|
|
15
|
+
6. **`open_in_editor`**:生成带一次性 `bootToken` Fragment 的 `editorUrl`,在 Claude Desktop 内置浏览器或本地桌面窗口中打开实时画布。
|
|
16
|
+
7. **`export_video`**:触发已挂载前端执行本地 WebCodecs + FFmpeg 合成导出,返回 MP4 路径与 `fileUri`。
|
|
17
|
+
|
|
18
|
+
## 操作守则
|
|
19
|
+
|
|
20
|
+
- **先挂载后写画布/导出**:若 `inspect_timeline` 或 `manage_project` 显示 `mounted: false`,先调用 `open_in_editor` 在内置浏览器打开 `editorUrl`(或传 `autoOpenEditor: true`),待前端挂载就绪后再执行 `apply_canvas_commands` 或 `export_video`。
|
|
21
|
+
- **导出防节流提示**:调用 `export_video` 时提醒用户保持编辑器面板可见,若遇 `CAPCLAW_SYNTHESIS_STALLED` 错误,恢复面板可见后重试即可。
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "capclaw-video-editor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "开刻(CapClaw)本地 AI 视频剪辑与画布编排插件(零拷贝素材导入、18 种确定性画布指令、宿主内置浏览器实时编辑、本地 WebCodecs 硬件加速合成导出)",
|
|
5
|
+
"author": "CapClaw Team",
|
|
6
|
+
"skills": [
|
|
7
|
+
"./skills/capclaw-video-editor"
|
|
8
|
+
],
|
|
9
|
+
"mcpServers": {
|
|
10
|
+
"capclaw-video-editor": {
|
|
11
|
+
"command": "node",
|
|
12
|
+
"args": [
|
|
13
|
+
"{{CAPCLAW_STDIO_PROXY_PATH}}"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: capclaw-video-editor
|
|
3
|
+
description: 使用开刻(CapClaw)本地视频剪辑引擎完成草稿管理、零拷贝本地素材导入、18 种确定性画布与时间线编辑、云端 AI 编排对话、宿主内置浏览器可视化精修及本地硬件加速视频合成导出。当用户要求剪辑视频、配字幕、加转场/滤镜/特效/贴纸/BGM、切分或变速视频片段、或打开开刻编辑器时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 开刻(CapClaw)视频剪辑工作流指南
|
|
7
|
+
|
|
8
|
+
开刻通过本机 Sidecar 提供 7 个正交 MCP 工具(服务名:`capclaw-video-editor`):
|
|
9
|
+
|
|
10
|
+
## 1. 7 个核心 MCP 工具
|
|
11
|
+
|
|
12
|
+
1. **`manage_project`**:查询(`list` / `get`)、创建(`create`)、重命名(`rename`)、删除(`delete`)本地 PGlite 草稿(`Project`)。
|
|
13
|
+
2. **`inspect_timeline`**:查看指定 `projectId` 的画布尺寸、总时长与全部轨道组件(若已在编辑器挂载则返回实时内存态,否则读取 PGlite)。
|
|
14
|
+
3. **`import_local_media`**:通过零拷贝(symlink → hardlink → vlink)将本机视频/图片/音频文件秒级导入草稿,可选 `addToTimeline: true` 自动加入时间线。
|
|
15
|
+
4. **`apply_canvas_commands`**:对已挂载草稿执行 **18 种公开确定性画布指令**(`UPDATE_WIDGET`, `ADD_WIDGET`, `DELETE_WIDGET`, `UPDATE_PAGE`, `REORDER_WIDGET`, `ADD_TRANSITION`, `ADD_GIF`, `ADD_FILTER`, `ADD_EFFECT`, `ADD_PARTICLE`, `ADD_AUDIO`, `BATCH_UPDATE`, `SET_ANIMATION`, `APPLY_EXISTING_ANIMATION`, `SPLIT_WIDGET`, `TRIM_WIDGET`, `SET_CLIP_SPEED`, `FREEZE_FRAME_SPLIT`)。
|
|
16
|
+
5. **`ai_edit_turn`**:将自然语言编辑诉求发送给开刻云端 AI 画布助手,生成并自动应用画布指令。
|
|
17
|
+
6. **`open_in_editor`**:签发一次性 60s 有效期的 `bootToken`(置于 URL Fragment `#capclawBridge=...&bootToken=...`),返回 `editorUrl` 与 `browserHandoff`。
|
|
18
|
+
7. **`export_video`**:由已挂载的前端编辑器执行 WebCodecs + Pixi + FFmpeg 本地硬件加速合成导出,返回本地 MP4 路径与 `fileUri`。
|
|
19
|
+
|
|
20
|
+
## 2. 「前端挂载即真源」黄金工作流
|
|
21
|
+
|
|
22
|
+
开刻的画布状态更新(如 `SPLIT_WIDGET`、`TRIM_WIDGET`、`SET_CLIP_SPEED` 级联重排)与 WebCodecs 视频合成均由真实前端执行:
|
|
23
|
+
|
|
24
|
+
1. **新建或选择草稿**:先调用 `manage_project(action="create" | "list")`。
|
|
25
|
+
2. **挂载编辑器**:
|
|
26
|
+
- 在执行 `apply_canvas_commands`、`import_local_media(addToTimeline=true)`、`ai_edit_turn` 或 `export_video` 前,若目标 `projectId` 尚未挂载(`mounted: false`),请先调用 `open_in_editor` 并在宿主内置浏览器打开返回的 `editorUrl`(或在工具参数中传 `autoOpenEditor: true` 自动唤起桌面端窗口)。
|
|
27
|
+
3. **导入素材与编辑**:
|
|
28
|
+
- 使用 `import_local_media` 导入本地素材;
|
|
29
|
+
- 使用 `apply_canvas_commands` 或 `ai_edit_turn` 精确调整组件、字幕、转场、动画与时间线切分。
|
|
30
|
+
4. **导出成片**:
|
|
31
|
+
- 调用 `export_video({ projectId })`;导出期间请提醒用户**保持开刻编辑器面板处于可见状态**(避免 Chromium 后台节流 `requestAnimationFrame`)。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "开刻 CapClaw 视频剪辑"
|
|
3
|
+
short_description: "使用开刻(CapClaw)在本地创建视频草稿、零拷贝导入素材、执行 18 种画布与时间线编辑指令,并在内置浏览器预览与导出 MP4。"
|
|
4
|
+
default_prompt: "使用开刻(CapClaw)帮我创建并剪辑一段视频草稿"
|
|
5
|
+
dependencies:
|
|
6
|
+
tools:
|
|
7
|
+
- type: "mcp"
|
|
8
|
+
value: "capclaw-video-editor"
|
|
9
|
+
description: "开刻本地 Sidecar MCP Server(7 个正交视频剪辑工具)"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# DeepSeek Harness (dsh) Cordis MCP patch template for CapClaw
|
|
2
|
+
# Target path: ~/.dsh/profiles/<profile>/cordis.patch.yml
|
|
3
|
+
"@deepseek-ai/dsh-mcp-client":
|
|
4
|
+
servers:
|
|
5
|
+
capclaw-video-editor:
|
|
6
|
+
transport: streamable-http
|
|
7
|
+
url: "http://127.0.0.1:{{CAPCLAW_MCP_PORT}}/mcp"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: capclaw-video-editor
|
|
3
|
+
description: 开刻(CapClaw)DeepSeek Harness (dsh) 本地视频剪辑技能。支持本地 PGlite 草稿管理、零拷贝素材导入、18 种确定性画布指令、云端 AI 编排对话以及唤起开刻桌面端窗口进行实时编辑与 WebCodecs 视频导出。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 开刻(CapClaw)DeepSeek Harness (`dsh`) 视频剪辑指南
|
|
7
|
+
|
|
8
|
+
## 1. 宿主特性说明(R1:桌面窗口挂载模式 E1)
|
|
9
|
+
|
|
10
|
+
DeepSeek Harness (`dsh`) 为纯终端/Cordis 宿主,**不包含内置 WebView 浏览器**。因此:
|
|
11
|
+
- 在需要执行画布指令(`apply_canvas_commands`、`import_local_media(addToTimeline=true)`、`ai_edit_turn`)或导出视频(`export_video`)时:
|
|
12
|
+
- 若目标 `projectId` 尚未挂载(`mounted: false`),请优先调用 `open_in_editor({ projectId, openDesktopWindow: true })`,或在调用编辑/导出工具时传入 `autoOpenEditor: true`,由开刻后台守护进程自动拉起本机桌面编辑器窗口(E1 模式)并等待挂载完成。
|
|
13
|
+
- 若本机未运行桌面窗口且未传 `autoOpenEditor: true`,工具将返回明确错误码 `CAPCLAW_EDITOR_NOT_MOUNTED`(不会静默降级到被 Chromium 节流的隐藏窗口)。
|
|
14
|
+
|
|
15
|
+
## 2. 7 个正交 MCP 工具
|
|
16
|
+
|
|
17
|
+
1. `manage_project`:`list` / `get` / `create` / `rename` / `delete` 本地草稿。
|
|
18
|
+
2. `inspect_timeline`:查看草稿时间线与组件状态(未挂载时直接从 PGlite 读取,无需打开窗口)。
|
|
19
|
+
3. `import_local_media`:零拷贝导入本地视频/图片/音频(仅导入素材库无需挂载窗口;若 `addToTimeline: true` 则需挂载窗口)。
|
|
20
|
+
4. `apply_canvas_commands`:执行 18 种公开画布指令(可传 `autoOpenEditor: true`)。
|
|
21
|
+
5. `ai_edit_turn`:调用云端 AI 画布助手生成并执行编辑指令(可传 `autoOpenEditor: true`)。
|
|
22
|
+
6. `open_in_editor`:传 `openDesktopWindow: true` 直接唤起本机开刻桌面端窗口加载该草稿。
|
|
23
|
+
7. `export_video`:触发已挂载的开刻桌面窗口执行本地 WebCodecs 合成导出(导出期间请保持开刻窗口未最小化)。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "capclaw-video-editor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "开刻(CapClaw)本地 AI 视频剪辑专家插件:支持零拷贝素材导入、18 种确定性画布与时间线指令、WorkBuddy 内置浏览器实时可视化编辑及本地 WebCodecs 硬件加速视频合成导出。",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "CapClaw Team"
|
|
7
|
+
},
|
|
8
|
+
"categoryId": "06-ContentCreative",
|
|
9
|
+
"skills": [
|
|
10
|
+
"./skills/capclaw-video-editor"
|
|
11
|
+
],
|
|
12
|
+
"agents": [
|
|
13
|
+
"./agents"
|
|
14
|
+
],
|
|
15
|
+
"mcpServers": "./.mcp.json"
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"capclaw-video-editor": {
|
|
4
|
+
"command": "node",
|
|
5
|
+
"args": [
|
|
6
|
+
"{{CAPCLAW_STDIO_PROXY_PATH}}"
|
|
7
|
+
],
|
|
8
|
+
"x-workbuddy": {
|
|
9
|
+
"displayName": "开刻 CapClaw 视频剪辑引擎",
|
|
10
|
+
"description": "本地 PGlite 草稿库 + 零拷贝素材 + 18 种画布指令 + 内置浏览器协同编辑"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: capclaw-editor
|
|
3
|
+
description: 开刻(CapClaw)AI 视频剪辑专家,负责本地视频草稿编排、素材零拷贝导入、时间线精细剪辑、WorkBuddy 内置浏览器协同预览与本地 MP4 导出。
|
|
4
|
+
tools:
|
|
5
|
+
- mcp__capclaw-video-editor__manage_project
|
|
6
|
+
- mcp__capclaw-video-editor__inspect_timeline
|
|
7
|
+
- mcp__capclaw-video-editor__import_local_media
|
|
8
|
+
- mcp__capclaw-video-editor__apply_canvas_commands
|
|
9
|
+
- mcp__capclaw-video-editor__ai_edit_turn
|
|
10
|
+
- mcp__capclaw-video-editor__open_in_editor
|
|
11
|
+
- mcp__capclaw-video-editor__export_video
|
|
12
|
+
- present_files
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
你是**开刻(CapClaw)视频剪辑专家**。你可以直接调用 `capclaw-video-editor` MCP 服务管理本地视频草稿、零拷贝导入本机音视频与图片素材、执行 18 种确定性画布指令,并利用 WorkBuddy 的 `present_files` 工具在右侧内置浏览器中打开开刻可视化编辑器或预览导出的 MP4 成片。
|
|
16
|
+
|
|
17
|
+
## 核心工作流
|
|
18
|
+
|
|
19
|
+
1. **创建或查看草稿**:调用 `manage_project`(`action: "create"` 或 `"list"`)。
|
|
20
|
+
2. **在 WorkBuddy 内置浏览器挂载编辑器**:
|
|
21
|
+
- 调用 `open_in_editor({ projectId })` 获取 `browserHandoff.url`(或 `editorUrl`);
|
|
22
|
+
- 立即调用 `present_files` 打开该 `browserHandoff.url`,让编辑器在 WorkBuddy 内置浏览器中完成回环桥握手与草稿挂载。
|
|
23
|
+
3. **导入素材与画布编辑**:
|
|
24
|
+
- 使用 `import_local_media` 导入本地文件(可传 `addToTimeline: true`);
|
|
25
|
+
- 使用 `apply_canvas_commands` 或 `ai_edit_turn` 修改字幕、贴纸、转场、滤镜、动画或切分/变速视频片段。
|
|
26
|
+
4. **本地合成导出与预览**:
|
|
27
|
+
- 调用 `export_video({ projectId })`(提醒用户保持右侧编辑器面板可见,避免浏览器后台节流);
|
|
28
|
+
- 导出完成后,将返回的 `fileUri`(`file:///...`)或 `exportedFilePath` 传给 `present_files` 供用户直接预览成片。
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: capclaw-video-editor
|
|
3
|
+
description: 开刻(CapClaw)WorkBuddy 视频剪辑与内置浏览器实时精修技能。用于创建本地视频草稿、零拷贝导入素材、执行 18 种画布指令、在 WorkBuddy 内置浏览器打开编辑器以及导出本地 MP4。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 开刻(CapClaw)WorkBuddy 视频剪辑技能指南
|
|
7
|
+
|
|
8
|
+
## 1. WorkBuddy 内置浏览器协同(`present_files`)
|
|
9
|
+
|
|
10
|
+
- 当调用 `manage_project(action="create")` 或 `open_in_editor({ projectId })` 获得 `browserHandoff.url` / `editorUrl` 后:
|
|
11
|
+
- 使用 WorkBuddy 内置的 `present_files` 工具传入该 URL,即可在 WorkBuddy 右侧内置浏览器直接打开开刻画布编辑器。
|
|
12
|
+
- URL Fragment 中的 `#capclawBridge=http://127.0.0.1:<port>&bootToken=<token>` 会在前端初始化时自动完成一次性握手并剥离,随后上报 `project-mounted`。
|
|
13
|
+
- 当调用 `export_video({ projectId })` 获得 `fileUri`(`file:///...mp4`)后:
|
|
14
|
+
- 使用 `present_files` 传入该 `fileUri` 即可唤起预览或系统播放器。
|
|
15
|
+
|
|
16
|
+
## 2. 18 种公开画布指令(`apply_canvas_commands`)
|
|
17
|
+
|
|
18
|
+
仅使用以下 18 种公开指令:
|
|
19
|
+
`UPDATE_WIDGET`, `ADD_WIDGET`, `DELETE_WIDGET`, `UPDATE_PAGE`, `REORDER_WIDGET`, `ADD_TRANSITION`, `ADD_GIF`, `ADD_FILTER`, `ADD_EFFECT`, `ADD_PARTICLE`, `ADD_AUDIO`, `BATCH_UPDATE`, `SET_ANIMATION`, `APPLY_EXISTING_ANIMATION`, `SPLIT_WIDGET`, `TRIM_WIDGET`, `SET_CLIP_SPEED`, `FREEZE_FRAME_SPLIT`。
|
|
20
|
+
|
|
21
|
+
## 3. 导出注意事项
|
|
22
|
+
|
|
23
|
+
- `export_video` 依赖前端可见 DOM 驱动 `requestAnimationFrame` + WebCodecs 合成;导出期间请提醒用户不要最小化或切走右侧编辑器面板。
|
package/package.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capclaw/mcp-stdio",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official MCP stdio proxy and multi-host plugin installer for CapClaw (开刻) AI Video Editor",
|
|
5
|
+
"bin": {
|
|
6
|
+
"capclaw-mcp-stdio": "./bin/capclaw-mcp-stdio.mjs",
|
|
7
|
+
"capclaw-plugin-install": "./bin/capclaw-plugin-install.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"service/",
|
|
12
|
+
"host-plugins/",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"license": "PROPRIETARY",
|
|
19
|
+
"homepage": "https://www.capclaw.cn"
|
|
20
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 开刻(CapClaw)四宿主插件一键安装与自愈器(service/hostPluginInstaller.js)
|
|
3
|
+
*
|
|
4
|
+
* 支持的四大宿主:
|
|
5
|
+
* 1. OpenAI Codex (`codex`):
|
|
6
|
+
* - 技能目录:`~/.codex/skills/capclaw-video-editor/`
|
|
7
|
+
* - 插件目录:`~/.codex/plugins/capclaw-video-editor/`
|
|
8
|
+
* - MCP 配置:`~/.codex/config.toml` -> `[mcp_servers.capclaw-video-editor]` (stdio)
|
|
9
|
+
* 2. Claude Code / Claude Desktop (`claude`):
|
|
10
|
+
* - 技能目录:`~/.claude/skills/capclaw-video-editor/`
|
|
11
|
+
* - MCP 配置:`~/.claude.json` -> `mcpServers["capclaw-video-editor"]` (stdio)
|
|
12
|
+
* 3. 腾讯 WorkBuddy (`workbuddy`):
|
|
13
|
+
* - 专家插件包:`~/.workbuddy/plugins/capclaw-video-editor/`(含 `.codebuddy-plugin/plugin.json`、包内 `.mcp.json` 含 `x-workbuddy`、`agents/`、`skills/`,遵循 C-4)
|
|
14
|
+
* - 用户级 MCP 配置:`~/.workbuddy/mcp.json`(严格剥离 `x-workbuddy`,遵循 C-4)
|
|
15
|
+
* 4. DeepSeek Harness (`dsh`):
|
|
16
|
+
* - 技能目录:`~/.dsh/skills/capclaw-video-editor/SKILL.md`(遵循 R1,无内置浏览器回落 E1 桌面窗口)
|
|
17
|
+
* - Cordis 补丁:`~/.dsh/profiles/<profile>/cordis.patch.yml` -> `@deepseek-ai/dsh-mcp-client` (`transport: streamable-http`,遵循 P0-3)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
|
|
24
|
+
const SERVER_KEY = 'capclaw-video-editor';
|
|
25
|
+
const SUPPORTED_HOSTS = ['codex', 'claude', 'workbuddy', 'dsh'];
|
|
26
|
+
|
|
27
|
+
function unpackAsarPathIfNeeded(targetPath) {
|
|
28
|
+
if (targetPath.includes('app.asar') && !targetPath.includes('app.asar.unpacked')) {
|
|
29
|
+
const unpacked = targetPath.replace('app.asar', 'app.asar.unpacked');
|
|
30
|
+
if (fs.existsSync(unpacked)) return unpacked;
|
|
31
|
+
}
|
|
32
|
+
return targetPath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveDefaultStdioProxyPath() {
|
|
36
|
+
return unpackAsarPathIfNeeded(path.resolve(__dirname, '..', 'bin', 'capclaw-mcp-stdio.mjs'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveHostPluginsRoot() {
|
|
40
|
+
return unpackAsarPathIfNeeded(path.resolve(__dirname, '..', 'host-plugins'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function copyDirTemplateRecursive(srcDir, destDir, replacements = {}) {
|
|
44
|
+
if (!fs.existsSync(srcDir)) return;
|
|
45
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
46
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
49
|
+
const destPath = path.join(destDir, entry.name);
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
copyDirTemplateRecursive(srcPath, destPath, replacements);
|
|
52
|
+
} else {
|
|
53
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
54
|
+
for (const [placeholder, val] of Object.entries(replacements)) {
|
|
55
|
+
content = content.split(placeholder).join(String(val));
|
|
56
|
+
}
|
|
57
|
+
fs.writeFileSync(destPath, content, 'utf8');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readJsonSafe(filePath, fallback = {}) {
|
|
63
|
+
try {
|
|
64
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
65
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
66
|
+
const parsed = JSON.parse(raw);
|
|
67
|
+
return parsed && typeof parsed === 'object' ? parsed : fallback;
|
|
68
|
+
} catch (_e) {
|
|
69
|
+
return fallback;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeJsonAtomic(filePath, data) {
|
|
74
|
+
const dir = path.dirname(filePath);
|
|
75
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
76
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
77
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
78
|
+
fs.renameSync(tmpPath, filePath);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function escapeTomlString(str) {
|
|
82
|
+
return String(str || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function upsertCodexTomlMcpSection(existingToml, { stdioProxyPath, exePath = '' }) {
|
|
86
|
+
const blockLines = [
|
|
87
|
+
`[mcp_servers.${SERVER_KEY}]`,
|
|
88
|
+
'command = "node"',
|
|
89
|
+
`args = ["${escapeTomlString(stdioProxyPath)}"]`,
|
|
90
|
+
];
|
|
91
|
+
if (exePath) {
|
|
92
|
+
blockLines.push(`[mcp_servers.${SERVER_KEY}.env]`);
|
|
93
|
+
blockLines.push(`CAPCLAW_EXE_PATH = "${escapeTomlString(exePath)}"`);
|
|
94
|
+
}
|
|
95
|
+
const newBlock = blockLines.join('\n');
|
|
96
|
+
|
|
97
|
+
const raw = String(existingToml || '');
|
|
98
|
+
// 移除已有的 [mcp_servers.capclaw-video-editor] 及紧随其后的 [mcp_servers.capclaw-video-editor.env]
|
|
99
|
+
const sectionPattern = new RegExp(
|
|
100
|
+
`\\[mcp_servers\\.${SERVER_KEY}(?:\\.env)?\\][\\s\\S]*?(?=\\n\\[|$)`,
|
|
101
|
+
'g',
|
|
102
|
+
);
|
|
103
|
+
const cleaned = raw.replace(sectionPattern, '').trim();
|
|
104
|
+
return cleaned ? `${cleaned}\n\n${newBlock}\n` : `${newBlock}\n`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function removeCodexTomlMcpSection(existingToml) {
|
|
108
|
+
const raw = String(existingToml || '');
|
|
109
|
+
const sectionPattern = new RegExp(
|
|
110
|
+
`\\[mcp_servers\\.${SERVER_KEY}(?:\\.env)?\\][\\s\\S]*?(?=\\n\\[|$)`,
|
|
111
|
+
'g',
|
|
112
|
+
);
|
|
113
|
+
return `${raw.replace(sectionPattern, '').trim()}\n`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function upsertDshCordisPatchYaml(existingYaml, { port = 5600 }) {
|
|
117
|
+
const targetUrl = `http://127.0.0.1:${Number(port) || 5600}/mcp`;
|
|
118
|
+
const raw = String(existingYaml || '');
|
|
119
|
+
|
|
120
|
+
if (!raw.trim()) {
|
|
121
|
+
return [
|
|
122
|
+
'# Managed by CapClaw Host Plugin Installer',
|
|
123
|
+
'"@deepseek-ai/dsh-mcp-client":',
|
|
124
|
+
' servers:',
|
|
125
|
+
` ${SERVER_KEY}:`,
|
|
126
|
+
' transport: streamable-http',
|
|
127
|
+
` url: "${targetUrl}"`,
|
|
128
|
+
'',
|
|
129
|
+
].join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 若已存在 capclaw-video-editor 条目,原地更新其 url 与 transport
|
|
133
|
+
if (raw.includes(`${SERVER_KEY}:`)) {
|
|
134
|
+
const updatedUrl = raw.replace(
|
|
135
|
+
/(capclaw-video-editor:\s*(?:\r?\n\s+[^\r\n]+)*?\r?\n\s+url:\s*["']?https?:\/\/(?:127\.0\.0\.1|localhost):)\d+(\/mcp["']?)/i,
|
|
136
|
+
`$1${Number(port) || 5600}$2`,
|
|
137
|
+
);
|
|
138
|
+
return updatedUrl;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 若已有 "@deepseek-ai/dsh-mcp-client": 且含 servers:
|
|
142
|
+
if (raw.includes('dsh-mcp-client') && /\bservers:\s*\r?\n/.test(raw)) {
|
|
143
|
+
return raw.replace(
|
|
144
|
+
/(\bservers:\s*\r?\n)/,
|
|
145
|
+
`$1 ${SERVER_KEY}:\n transport: streamable-http\n url: "${targetUrl}"\n`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return `${raw.trimEnd()}\n\n"@deepseek-ai/dsh-mcp-client":\n servers:\n ${SERVER_KEY}:\n transport: streamable-http\n url: "${targetUrl}"\n`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function installCodexPlugin({
|
|
153
|
+
homeDir = os.homedir(),
|
|
154
|
+
stdioProxyPath = resolveDefaultStdioProxyPath(),
|
|
155
|
+
exePath = process.env.CAPCLAW_EXE_PATH || '',
|
|
156
|
+
templatesRoot = resolveHostPluginsRoot(),
|
|
157
|
+
} = {}) {
|
|
158
|
+
const codexDir = path.join(homeDir, '.codex');
|
|
159
|
+
const pluginDest = path.join(codexDir, 'plugins', SERVER_KEY);
|
|
160
|
+
const skillDest = path.join(codexDir, 'skills', SERVER_KEY);
|
|
161
|
+
const configTomlPath = path.join(codexDir, 'config.toml');
|
|
162
|
+
|
|
163
|
+
const srcDir = path.join(templatesRoot, 'codex');
|
|
164
|
+
copyDirTemplateRecursive(srcDir, pluginDest, {
|
|
165
|
+
'{{CAPCLAW_STDIO_PROXY_PATH}}': stdioProxyPath.replace(/\\/g, '/'),
|
|
166
|
+
});
|
|
167
|
+
copyDirTemplateRecursive(path.join(srcDir, 'skills', SERVER_KEY), skillDest, {
|
|
168
|
+
'{{CAPCLAW_STDIO_PROXY_PATH}}': stdioProxyPath.replace(/\\/g, '/'),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const existingToml = fs.existsSync(configTomlPath) ? fs.readFileSync(configTomlPath, 'utf8') : '';
|
|
172
|
+
const updatedToml = upsertCodexTomlMcpSection(existingToml, { stdioProxyPath, exePath });
|
|
173
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
174
|
+
fs.writeFileSync(configTomlPath, updatedToml, 'utf8');
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
host: 'codex',
|
|
179
|
+
pluginDir: pluginDest,
|
|
180
|
+
skillDir: skillDest,
|
|
181
|
+
configPath: configTomlPath,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function installClaudePlugin({
|
|
186
|
+
homeDir = os.homedir(),
|
|
187
|
+
stdioProxyPath = resolveDefaultStdioProxyPath(),
|
|
188
|
+
exePath = process.env.CAPCLAW_EXE_PATH || '',
|
|
189
|
+
templatesRoot = resolveHostPluginsRoot(),
|
|
190
|
+
} = {}) {
|
|
191
|
+
const claudeDir = path.join(homeDir, '.claude');
|
|
192
|
+
const pluginDest = path.join(claudeDir, 'plugins', SERVER_KEY);
|
|
193
|
+
const skillDest = path.join(claudeDir, 'skills', SERVER_KEY);
|
|
194
|
+
const claudeJsonPath = path.join(homeDir, '.claude.json');
|
|
195
|
+
|
|
196
|
+
const srcDir = path.join(templatesRoot, 'claude');
|
|
197
|
+
copyDirTemplateRecursive(srcDir, pluginDest, {
|
|
198
|
+
'{{CAPCLAW_STDIO_PROXY_PATH}}': stdioProxyPath.replace(/\\/g, '/'),
|
|
199
|
+
});
|
|
200
|
+
copyDirTemplateRecursive(path.join(srcDir, 'skills', SERVER_KEY), skillDest, {
|
|
201
|
+
'{{CAPCLAW_STDIO_PROXY_PATH}}': stdioProxyPath.replace(/\\/g, '/'),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const claudeConfig = readJsonSafe(claudeJsonPath, {});
|
|
205
|
+
if (!claudeConfig.mcpServers || typeof claudeConfig.mcpServers !== 'object') {
|
|
206
|
+
claudeConfig.mcpServers = {};
|
|
207
|
+
}
|
|
208
|
+
const serverEntry = {
|
|
209
|
+
type: 'stdio',
|
|
210
|
+
command: 'node',
|
|
211
|
+
args: [stdioProxyPath],
|
|
212
|
+
};
|
|
213
|
+
if (exePath) {
|
|
214
|
+
serverEntry.env = { CAPCLAW_EXE_PATH: exePath };
|
|
215
|
+
}
|
|
216
|
+
claudeConfig.mcpServers[SERVER_KEY] = serverEntry;
|
|
217
|
+
writeJsonAtomic(claudeJsonPath, claudeConfig);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
host: 'claude',
|
|
222
|
+
pluginDir: pluginDest,
|
|
223
|
+
skillDir: skillDest,
|
|
224
|
+
configPath: claudeJsonPath,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function installWorkBuddyPlugin({
|
|
229
|
+
homeDir = os.homedir(),
|
|
230
|
+
stdioProxyPath = resolveDefaultStdioProxyPath(),
|
|
231
|
+
exePath = process.env.CAPCLAW_EXE_PATH || '',
|
|
232
|
+
port = 5600,
|
|
233
|
+
useStreamableHttpInUserConfig = false,
|
|
234
|
+
templatesRoot = resolveHostPluginsRoot(),
|
|
235
|
+
} = {}) {
|
|
236
|
+
const wbDir = path.join(homeDir, '.workbuddy');
|
|
237
|
+
const pluginDest = path.join(wbDir, 'plugins', SERVER_KEY);
|
|
238
|
+
const userMcpJsonPath = path.join(wbDir, 'mcp.json');
|
|
239
|
+
|
|
240
|
+
const srcDir = path.join(templatesRoot, 'workbuddy');
|
|
241
|
+
copyDirTemplateRecursive(srcDir, pluginDest, {
|
|
242
|
+
'{{CAPCLAW_STDIO_PROXY_PATH}}': stdioProxyPath.replace(/\\/g, '/'),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// 用户级 ~/.workbuddy/mcp.json:严格不写 x-workbuddy(C-4:x-workbuddy 仅在插件包内 .mcp.json 生效,写入用户配置时剥离)
|
|
246
|
+
const userMcpConfig = readJsonSafe(userMcpJsonPath, {});
|
|
247
|
+
if (!userMcpConfig.mcpServers || typeof userMcpConfig.mcpServers !== 'object') {
|
|
248
|
+
userMcpConfig.mcpServers = {};
|
|
249
|
+
}
|
|
250
|
+
if (useStreamableHttpInUserConfig) {
|
|
251
|
+
userMcpConfig.mcpServers[SERVER_KEY] = {
|
|
252
|
+
type: 'streamable-http',
|
|
253
|
+
url: `http://127.0.0.1:${Number(port) || 5600}/mcp`,
|
|
254
|
+
};
|
|
255
|
+
} else {
|
|
256
|
+
const entry = {
|
|
257
|
+
command: 'node',
|
|
258
|
+
args: [stdioProxyPath],
|
|
259
|
+
};
|
|
260
|
+
if (exePath) {
|
|
261
|
+
entry.env = { CAPCLAW_EXE_PATH: exePath };
|
|
262
|
+
}
|
|
263
|
+
userMcpConfig.mcpServers[SERVER_KEY] = entry;
|
|
264
|
+
}
|
|
265
|
+
// 确保剥离任何残留的 x-workbuddy
|
|
266
|
+
if (userMcpConfig.mcpServers[SERVER_KEY] && 'x-workbuddy' in userMcpConfig.mcpServers[SERVER_KEY]) {
|
|
267
|
+
delete userMcpConfig.mcpServers[SERVER_KEY]['x-workbuddy'];
|
|
268
|
+
}
|
|
269
|
+
writeJsonAtomic(userMcpJsonPath, userMcpConfig);
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
ok: true,
|
|
273
|
+
host: 'workbuddy',
|
|
274
|
+
pluginDir: pluginDest,
|
|
275
|
+
configPath: userMcpJsonPath,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function installDshPlugin({
|
|
280
|
+
homeDir = os.homedir(),
|
|
281
|
+
port = 5600,
|
|
282
|
+
profile = 'default',
|
|
283
|
+
templatesRoot = resolveHostPluginsRoot(),
|
|
284
|
+
} = {}) {
|
|
285
|
+
const dshDir = path.join(homeDir, '.dsh');
|
|
286
|
+
const skillDest = path.join(dshDir, 'skills', SERVER_KEY);
|
|
287
|
+
const profileDir = path.join(dshDir, 'profiles', profile);
|
|
288
|
+
const patchPath = path.join(profileDir, 'cordis.patch.yml');
|
|
289
|
+
|
|
290
|
+
const srcDir = path.join(templatesRoot, 'dsh');
|
|
291
|
+
copyDirTemplateRecursive(path.join(srcDir, 'skills', SERVER_KEY), skillDest, {
|
|
292
|
+
'{{CAPCLAW_MCP_PORT}}': String(Number(port) || 5600),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
296
|
+
const existingYaml = fs.existsSync(patchPath) ? fs.readFileSync(patchPath, 'utf8') : '';
|
|
297
|
+
const updatedYaml = upsertDshCordisPatchYaml(existingYaml, { port });
|
|
298
|
+
fs.writeFileSync(patchPath, updatedYaml, 'utf8');
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
ok: true,
|
|
302
|
+
host: 'dsh',
|
|
303
|
+
skillDir: skillDest,
|
|
304
|
+
configPath: patchPath,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function installHostPlugin(host, options = {}) {
|
|
309
|
+
const cleanHost = String(host || '').trim().toLowerCase();
|
|
310
|
+
switch (cleanHost) {
|
|
311
|
+
case 'codex':
|
|
312
|
+
return installCodexPlugin(options);
|
|
313
|
+
case 'claude':
|
|
314
|
+
return installClaudePlugin(options);
|
|
315
|
+
case 'workbuddy':
|
|
316
|
+
return installWorkBuddyPlugin(options);
|
|
317
|
+
case 'dsh':
|
|
318
|
+
return installDshPlugin(options);
|
|
319
|
+
default:
|
|
320
|
+
throw new Error(`Unsupported host "${host}". Expected one of: ${SUPPORTED_HOSTS.join(', ')}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function installAllHostPlugins(options = {}) {
|
|
325
|
+
const results = {};
|
|
326
|
+
for (const host of SUPPORTED_HOSTS) {
|
|
327
|
+
results[host] = installHostPlugin(host, options);
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
ok: true,
|
|
331
|
+
hosts: results,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function uninstallHostPlugin(host, { homeDir = os.homedir(), profile = 'default' } = {}) {
|
|
336
|
+
const cleanHost = String(host || '').trim().toLowerCase();
|
|
337
|
+
if (cleanHost === 'codex') {
|
|
338
|
+
const codexDir = path.join(homeDir, '.codex');
|
|
339
|
+
const configTomlPath = path.join(codexDir, 'config.toml');
|
|
340
|
+
if (fs.existsSync(configTomlPath)) {
|
|
341
|
+
const updated = removeCodexTomlMcpSection(fs.readFileSync(configTomlPath, 'utf8'));
|
|
342
|
+
fs.writeFileSync(configTomlPath, updated, 'utf8');
|
|
343
|
+
}
|
|
344
|
+
fs.rmSync(path.join(codexDir, 'plugins', SERVER_KEY), { recursive: true, force: true });
|
|
345
|
+
fs.rmSync(path.join(codexDir, 'skills', SERVER_KEY), { recursive: true, force: true });
|
|
346
|
+
return { ok: true, host: 'codex' };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (cleanHost === 'claude') {
|
|
350
|
+
const claudeJsonPath = path.join(homeDir, '.claude.json');
|
|
351
|
+
if (fs.existsSync(claudeJsonPath)) {
|
|
352
|
+
const cfg = readJsonSafe(claudeJsonPath, {});
|
|
353
|
+
if (cfg.mcpServers && cfg.mcpServers[SERVER_KEY]) {
|
|
354
|
+
delete cfg.mcpServers[SERVER_KEY];
|
|
355
|
+
writeJsonAtomic(claudeJsonPath, cfg);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
fs.rmSync(path.join(homeDir, '.claude', 'plugins', SERVER_KEY), { recursive: true, force: true });
|
|
359
|
+
fs.rmSync(path.join(homeDir, '.claude', 'skills', SERVER_KEY), { recursive: true, force: true });
|
|
360
|
+
return { ok: true, host: 'claude' };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (cleanHost === 'workbuddy') {
|
|
364
|
+
const wbMcpPath = path.join(homeDir, '.workbuddy', 'mcp.json');
|
|
365
|
+
if (fs.existsSync(wbMcpPath)) {
|
|
366
|
+
const cfg = readJsonSafe(wbMcpPath, {});
|
|
367
|
+
if (cfg.mcpServers && cfg.mcpServers[SERVER_KEY]) {
|
|
368
|
+
delete cfg.mcpServers[SERVER_KEY];
|
|
369
|
+
writeJsonAtomic(wbMcpPath, cfg);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
fs.rmSync(path.join(homeDir, '.workbuddy', 'plugins', SERVER_KEY), { recursive: true, force: true });
|
|
373
|
+
return { ok: true, host: 'workbuddy' };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (cleanHost === 'dsh') {
|
|
377
|
+
const patchPath = path.join(homeDir, '.dsh', 'profiles', profile, 'cordis.patch.yml');
|
|
378
|
+
if (fs.existsSync(patchPath)) {
|
|
379
|
+
const raw = fs.readFileSync(patchPath, 'utf8');
|
|
380
|
+
const cleaned = raw.replace(
|
|
381
|
+
new RegExp(`\\s*${SERVER_KEY}:\\s*\\r?\\n\\s*transport:\\s*[^\\r\\n]+\\r?\\n\\s*url:\\s*[^\\r\\n]+`, 'g'),
|
|
382
|
+
'',
|
|
383
|
+
);
|
|
384
|
+
fs.writeFileSync(patchPath, cleaned, 'utf8');
|
|
385
|
+
}
|
|
386
|
+
fs.rmSync(path.join(homeDir, '.dsh', 'skills', SERVER_KEY), { recursive: true, force: true });
|
|
387
|
+
return { ok: true, host: 'dsh' };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
throw new Error(`Unsupported host "${host}"`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function getHostPluginStatus({ homeDir = os.homedir(), profile = 'default' } = {}) {
|
|
394
|
+
const codexToml = path.join(homeDir, '.codex', 'config.toml');
|
|
395
|
+
const claudeJson = path.join(homeDir, '.claude.json');
|
|
396
|
+
const wbPluginJson = path.join(homeDir, '.workbuddy', 'plugins', SERVER_KEY, '.codebuddy-plugin', 'plugin.json');
|
|
397
|
+
const dshPatch = path.join(homeDir, '.dsh', 'profiles', profile, 'cordis.patch.yml');
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
codex: {
|
|
401
|
+
installed: fs.existsSync(codexToml) && fs.readFileSync(codexToml, 'utf8').includes(`[mcp_servers.${SERVER_KEY}]`),
|
|
402
|
+
configPath: codexToml,
|
|
403
|
+
},
|
|
404
|
+
claude: {
|
|
405
|
+
installed: Boolean(readJsonSafe(claudeJson, {})?.mcpServers?.[SERVER_KEY]),
|
|
406
|
+
configPath: claudeJson,
|
|
407
|
+
},
|
|
408
|
+
workbuddy: {
|
|
409
|
+
installed: fs.existsSync(wbPluginJson),
|
|
410
|
+
configPath: wbPluginJson,
|
|
411
|
+
},
|
|
412
|
+
dsh: {
|
|
413
|
+
installed: fs.existsSync(dshPatch) && fs.readFileSync(dshPatch, 'utf8').includes(SERVER_KEY),
|
|
414
|
+
configPath: dshPatch,
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
module.exports = {
|
|
420
|
+
SERVER_KEY,
|
|
421
|
+
SUPPORTED_HOSTS,
|
|
422
|
+
installCodexPlugin,
|
|
423
|
+
installClaudePlugin,
|
|
424
|
+
installWorkBuddyPlugin,
|
|
425
|
+
installDshPlugin,
|
|
426
|
+
installHostPlugin,
|
|
427
|
+
installAllHostPlugins,
|
|
428
|
+
uninstallHostPlugin,
|
|
429
|
+
getHostPluginStatus,
|
|
430
|
+
upsertCodexTomlMcpSection,
|
|
431
|
+
upsertDshCordisPatchYaml,
|
|
432
|
+
};
|