@kln-mcp/ctrl-mobile-cn 0.0.6
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/bin/install.js +314 -0
- package/cli/setup.js +304 -0
- package/config/index.d.ts +19 -0
- package/config/index.js +78 -0
- package/config/pro.json +12 -0
- package/core/index.d.ts +57 -0
- package/core/index.js +315 -0
- package/index.js +23 -0
- package/package.json +31 -0
- package/skills/kln-mobile-ctrl/SKILL.md +122 -0
- package/wrapper/index.d.ts +107 -0
- package/wrapper/index.js +2553 -0
- package/wrapper/live-view.html +438 -0
package/bin/install.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require('node:child_process');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
const crypto = require('node:crypto');
|
|
9
|
+
|
|
10
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
11
|
+
const packageJson = require(path.join(packageRoot, 'package.json'));
|
|
12
|
+
const PACKAGE_NAME = packageJson.name;
|
|
13
|
+
const PACKAGE_VERSION = packageJson.version;
|
|
14
|
+
const LANG = PACKAGE_NAME.endsWith('-cn') ? 'cn' : 'en';
|
|
15
|
+
const MCP_SERVER_NAME = 'kln-ctrl-mobile';
|
|
16
|
+
const DEFAULT_API_BASE = readDefaultApiBase();
|
|
17
|
+
|
|
18
|
+
function readDefaultApiBase() {
|
|
19
|
+
const file = path.join(packageRoot, 'config', 'pro.json');
|
|
20
|
+
try {
|
|
21
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
22
|
+
if (data && typeof data.api_url === 'string' && data.api_url.trim()) {
|
|
23
|
+
return data.api_url.trim();
|
|
24
|
+
}
|
|
25
|
+
fail('config/pro.json missing api_url');
|
|
26
|
+
} catch (e) {
|
|
27
|
+
fail(`failed to read config/pro.json: ${e.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const T = LANG === 'cn' ? {
|
|
32
|
+
missingToken: '缺少 --token (也可通过环境变量 KLN_INSTALL_TOKEN 传入)',
|
|
33
|
+
installing: '正在为 OpenClaw 安装 kln-ctrl-mobile MCP...',
|
|
34
|
+
exchangeFail: '安装命令无效或已失效',
|
|
35
|
+
installFail: '安装失败',
|
|
36
|
+
openclawMissing: '未找到 openclaw 命令,请先安装 OpenClaw (npm i -g openclaw)',
|
|
37
|
+
mcpSetFail: '调用 openclaw mcp set 失败',
|
|
38
|
+
done: '安装完成',
|
|
39
|
+
mcpServer: 'MCP 服务',
|
|
40
|
+
installPath: '安装目录',
|
|
41
|
+
skillFile: 'Skill 文件',
|
|
42
|
+
registered: '已通过 openclaw mcp set 注册',
|
|
43
|
+
reloadHint: '请重启或重新加载 OpenClaw 让配置生效。',
|
|
44
|
+
dryRun: '[dry-run] 跳过实际写文件'
|
|
45
|
+
} : {
|
|
46
|
+
missingToken: 'missing --token (or set KLN_INSTALL_TOKEN env var)',
|
|
47
|
+
installing: 'Installing kln-ctrl-mobile MCP for OpenClaw...',
|
|
48
|
+
exchangeFail: 'install token is invalid or has expired',
|
|
49
|
+
installFail: 'installation failed',
|
|
50
|
+
openclawMissing: 'openclaw command not found; install it first (npm i -g openclaw)',
|
|
51
|
+
mcpSetFail: 'openclaw mcp set failed',
|
|
52
|
+
done: 'install complete',
|
|
53
|
+
mcpServer: 'MCP server',
|
|
54
|
+
installPath: 'install dir',
|
|
55
|
+
skillFile: 'skill',
|
|
56
|
+
registered: 'registered via `openclaw mcp set`',
|
|
57
|
+
reloadHint: 'Please restart or reload OpenClaw to pick up the new MCP server.',
|
|
58
|
+
dryRun: '[dry-run] skipping file writes'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
63
|
+
if (!opts.token) fail(T.missingToken);
|
|
64
|
+
|
|
65
|
+
const apiBase = opts.apiBase || process.env.KLN_API_BASE_URL || DEFAULT_API_BASE;
|
|
66
|
+
const installDir = path.resolve(
|
|
67
|
+
opts.installDir
|
|
68
|
+
|| process.env.KLN_INSTALL_DIR
|
|
69
|
+
|| path.join(os.homedir(), '.kln', 'ctrl-mobile', PACKAGE_VERSION)
|
|
70
|
+
);
|
|
71
|
+
const skillsDir = path.resolve(
|
|
72
|
+
opts.skillsDir || process.env.KLN_OPENCLAW_SKILLS_DIR || detectSkillsDir()
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
process.stdout.write(`${T.installing}\n`);
|
|
76
|
+
|
|
77
|
+
let apiKey;
|
|
78
|
+
if (opts.apiKey) {
|
|
79
|
+
apiKey = opts.apiKey;
|
|
80
|
+
} else {
|
|
81
|
+
try {
|
|
82
|
+
apiKey = await exchangeToken({ apiBase, token: opts.token });
|
|
83
|
+
} catch (e) {
|
|
84
|
+
await safePostResult({ apiBase, token: opts.token, status: 'failed', error: `exchange: ${e.message}`, installDir });
|
|
85
|
+
fail(`${T.exchangeFail}: ${e.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (opts.dryRun) {
|
|
90
|
+
process.stdout.write(`${T.dryRun}\n`);
|
|
91
|
+
process.stdout.write(` ${T.installPath}: ${installDir}\n`);
|
|
92
|
+
process.stdout.write(` ${T.mcpServer}: ${MCP_SERVER_NAME} (openclaw mcp set)\n`);
|
|
93
|
+
process.stdout.write(` ${T.skillFile}: ${path.join(skillsDir, 'kln-mobile-ctrl/SKILL.md')}\n`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
copyPackageTo(installDir);
|
|
99
|
+
copyPlatformBinary(installDir);
|
|
100
|
+
verifyNativeLoad(installDir);
|
|
101
|
+
runOpenclawMcpSet({ installDir, apiKey });
|
|
102
|
+
copySkillFile({ installDir, skillsDir });
|
|
103
|
+
} catch (e) {
|
|
104
|
+
await safePostResult({ apiBase, token: opts.token, status: 'failed', error: `install: ${e.message}`, installDir });
|
|
105
|
+
fail(`${T.installFail}: ${e.message}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await safePostResult({ apiBase, token: opts.token, status: 'success', installDir });
|
|
109
|
+
|
|
110
|
+
process.stdout.write(`${T.done}\n`);
|
|
111
|
+
process.stdout.write(` ${T.installPath}: ${installDir}\n`);
|
|
112
|
+
process.stdout.write(` ${T.mcpServer}: ${MCP_SERVER_NAME} (${T.registered})\n`);
|
|
113
|
+
process.stdout.write(` ${T.skillFile}: ${path.join(skillsDir, 'kln-mobile-ctrl/SKILL.md')}\n`);
|
|
114
|
+
process.stdout.write(`${T.reloadHint}\n`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseArgs(argv) {
|
|
118
|
+
const opts = {};
|
|
119
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
120
|
+
const arg = argv[i];
|
|
121
|
+
if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); }
|
|
122
|
+
if (arg === '--dry-run') { opts.dryRun = true; continue; }
|
|
123
|
+
if (!arg.startsWith('--')) fail(`unknown argument: ${arg}`);
|
|
124
|
+
const eq = arg.indexOf('=');
|
|
125
|
+
const key = eq >= 0 ? arg.slice(2, eq) : arg.slice(2);
|
|
126
|
+
const val = eq >= 0 ? arg.slice(eq + 1) : argv[++i];
|
|
127
|
+
if (val === undefined || (typeof val === 'string' && val.startsWith('--'))) {
|
|
128
|
+
fail(`missing value for --${key}`);
|
|
129
|
+
}
|
|
130
|
+
if (key === 'token') opts.token = val;
|
|
131
|
+
else if (key === 'api-base') opts.apiBase = val;
|
|
132
|
+
else if (key === 'api-key') opts.apiKey = val;
|
|
133
|
+
else if (key === 'install-dir') opts.installDir = val;
|
|
134
|
+
else if (key === 'skills-dir') opts.skillsDir = val;
|
|
135
|
+
else fail(`unknown argument: --${key}`);
|
|
136
|
+
}
|
|
137
|
+
if (!opts.token) opts.token = process.env.KLN_INSTALL_TOKEN || '';
|
|
138
|
+
return opts;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function detectSkillsDir() {
|
|
142
|
+
const home = os.homedir();
|
|
143
|
+
if (fs.existsSync(path.join(home, '.openclaw'))) return path.join(home, '.openclaw', 'skills');
|
|
144
|
+
if (fs.existsSync(path.join(home, '.config', 'openclaw'))) return path.join(home, '.config', 'openclaw', 'skills');
|
|
145
|
+
return path.join(home, '.openclaw', 'skills');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function copyPackageTo(installDir) {
|
|
149
|
+
fs.rmSync(installDir, { recursive: true, force: true });
|
|
150
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
151
|
+
fs.cpSync(packageRoot, installDir, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function detectTriple() {
|
|
155
|
+
const { platform, arch } = process;
|
|
156
|
+
if (platform === 'darwin' && arch === 'arm64') return 'darwin-arm64';
|
|
157
|
+
if (platform === 'darwin' && arch === 'x64') return 'darwin-x64';
|
|
158
|
+
if (platform === 'linux' && arch === 'x64') return 'linux-x64-gnu';
|
|
159
|
+
if (platform === 'linux' && arch === 'arm64') return 'linux-arm64-gnu';
|
|
160
|
+
if (platform === 'win32' && arch === 'x64') return 'win32-x64-msvc';
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function copyPlatformBinary(installDir) {
|
|
165
|
+
const triple = detectTriple();
|
|
166
|
+
if (!triple) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`unsupported platform: ${process.platform}-${process.arch}. ` +
|
|
169
|
+
`Supported: darwin-arm64, darwin-x64, linux-x64-gnu, linux-arm64-gnu, win32-x64-msvc.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const subPkg = `@kln-ctrl/mobile-core-${triple}`;
|
|
173
|
+
let src;
|
|
174
|
+
try {
|
|
175
|
+
src = require.resolve(subPkg);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`native binary for ${triple} not installed (missing ${subPkg}). ` +
|
|
179
|
+
`Reinstall with --include=optional, or your platform may not be supported.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const dst = path.join(installDir, 'core', `kln_ctrl_core.${triple}.node`);
|
|
183
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
184
|
+
fs.copyFileSync(src, dst);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function verifyNativeLoad(installDir) {
|
|
188
|
+
const corePath = path.join(installDir, 'core');
|
|
189
|
+
try {
|
|
190
|
+
require(corePath);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
throw new Error(`native binary failed to load: ${e.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function runOpenclawMcpSet({ installDir, apiKey }) {
|
|
197
|
+
const spec = {
|
|
198
|
+
command: 'node',
|
|
199
|
+
args: [path.join(installDir, 'index.js'), 'mcp-server', '--env', 'pro'],
|
|
200
|
+
env: { KLN_API_KEY: apiKey }
|
|
201
|
+
};
|
|
202
|
+
const r = spawnSync('openclaw', ['mcp', 'set', MCP_SERVER_NAME, JSON.stringify(spec)], {
|
|
203
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
204
|
+
});
|
|
205
|
+
if (r.error) {
|
|
206
|
+
if (r.error.code === 'ENOENT') {
|
|
207
|
+
throw new Error(T.openclawMissing);
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`openclaw spawn failed: ${r.error.message}`);
|
|
210
|
+
}
|
|
211
|
+
if (r.status !== 0) {
|
|
212
|
+
const out = ((r.stderr && r.stderr.toString()) || (r.stdout && r.stdout.toString()) || '').trim();
|
|
213
|
+
throw new Error(`${T.mcpSetFail}: ${out || `exit ${r.status}`}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function copySkillFile({ installDir, skillsDir }) {
|
|
218
|
+
const src = path.join(installDir, 'skills', 'kln-mobile-ctrl', 'SKILL.md');
|
|
219
|
+
if (!fs.existsSync(src)) return;
|
|
220
|
+
const dst = path.join(skillsDir, 'kln-mobile-ctrl', 'SKILL.md');
|
|
221
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
222
|
+
fs.copyFileSync(src, dst);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function exchangeToken({ apiBase, token }) {
|
|
226
|
+
const payload = { install_token: token, system_info: collectSystemInfo() };
|
|
227
|
+
const data = await postJson(`${apiBase}/api/v1/user/install-token/exchange`, payload);
|
|
228
|
+
if (!data || data.code !== 0 || !data.data || !data.data.apikey) {
|
|
229
|
+
throw new Error('invalid response');
|
|
230
|
+
}
|
|
231
|
+
return data.data.apikey;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function safePostResult({ apiBase, token, status, error = '', installDir }) {
|
|
235
|
+
const payload = {
|
|
236
|
+
install_token: token,
|
|
237
|
+
status,
|
|
238
|
+
agent: 'openclaw',
|
|
239
|
+
fingerprint: fingerprint(),
|
|
240
|
+
install_dir: installDir || '',
|
|
241
|
+
package_version: PACKAGE_VERSION,
|
|
242
|
+
system_info: collectSystemInfo(),
|
|
243
|
+
error: error || ''
|
|
244
|
+
};
|
|
245
|
+
try {
|
|
246
|
+
await postJson(`${apiBase}/api/v1/user/install-token/install-result`, payload);
|
|
247
|
+
} catch (_) {}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function collectSystemInfo() {
|
|
251
|
+
return {
|
|
252
|
+
agent: 'openclaw',
|
|
253
|
+
platform: os.platform(),
|
|
254
|
+
arch: os.arch(),
|
|
255
|
+
release: os.release(),
|
|
256
|
+
hostname: os.hostname(),
|
|
257
|
+
node: process.version
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function fingerprint() {
|
|
262
|
+
const raw = [os.platform(), os.arch(), os.release(), os.hostname(), os.homedir()].join('|');
|
|
263
|
+
return crypto.createHash('sha256').update(raw).digest('hex');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function postJson(url, payload) {
|
|
267
|
+
const resp = await fetch(url, {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: { 'Content-Type': 'application/json' },
|
|
270
|
+
body: JSON.stringify(payload)
|
|
271
|
+
});
|
|
272
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
273
|
+
return resp.json();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function printHelp() {
|
|
277
|
+
const help = LANG === 'cn' ? `
|
|
278
|
+
用法: npx -y ${PACKAGE_NAME}@latest --token <install-token>
|
|
279
|
+
|
|
280
|
+
选项:
|
|
281
|
+
--token <token> 安装 token (必须;也可通过 KLN_INSTALL_TOKEN 环境变量)
|
|
282
|
+
--api-base <url> 后端 API base URL (默认 ${DEFAULT_API_BASE})
|
|
283
|
+
--install-dir <path> 安装目录 (默认 ~/.kln/ctrl-mobile/<version>)
|
|
284
|
+
--skills-dir <path> openclaw skills 目录 (默认自动检测)
|
|
285
|
+
--dry-run 只打印计划,不实际修改文件
|
|
286
|
+
--help, -h 显示此帮助
|
|
287
|
+
|
|
288
|
+
说明:
|
|
289
|
+
MCP 配置通过 \`openclaw mcp set ${MCP_SERVER_NAME} <spec>\` 注册,不直接编辑 openclaw.json。
|
|
290
|
+
安装后用 \`openclaw mcp list\` 可以验证。
|
|
291
|
+
` : `
|
|
292
|
+
Usage: npx -y ${PACKAGE_NAME}@latest --token <install-token>
|
|
293
|
+
|
|
294
|
+
Options:
|
|
295
|
+
--token <token> install token (required; or set KLN_INSTALL_TOKEN)
|
|
296
|
+
--api-base <url> backend API base URL (default ${DEFAULT_API_BASE})
|
|
297
|
+
--install-dir <path> install destination (default ~/.kln/ctrl-mobile/<version>)
|
|
298
|
+
--skills-dir <path> openclaw skills dir (auto-detected by default)
|
|
299
|
+
--dry-run print plan only; do not modify files
|
|
300
|
+
--help, -h show this help
|
|
301
|
+
|
|
302
|
+
Notes:
|
|
303
|
+
MCP server is registered via \`openclaw mcp set ${MCP_SERVER_NAME} <spec>\`,
|
|
304
|
+
not by editing openclaw.json directly. Verify with \`openclaw mcp list\` afterwards.
|
|
305
|
+
`;
|
|
306
|
+
process.stdout.write(help);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function fail(msg) {
|
|
310
|
+
process.stderr.write(`install: ${msg}\n`);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
main().catch((e) => fail(e && e.message ? e.message : String(e)));
|
package/cli/setup.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createController, createMcpServer, TOOL_DEFINITIONS, checkTaskMemoryFile } = require('../wrapper');
|
|
4
|
+
const { loadConfig } = require('../config');
|
|
5
|
+
|
|
6
|
+
const COMMANDS = new Set(['help', 'mcp-server', 'mcp', 'devices', 'select-device', 'command', 'text', 'send-only', 'device-context', 'memory', 'doctor', 'stream', 'view', 'config', 'tools']);
|
|
7
|
+
|
|
8
|
+
async function run(argv = process.argv.slice(2), io = process) {
|
|
9
|
+
const parsed = parseArgs(argv);
|
|
10
|
+
const command = parsed.command || 'help';
|
|
11
|
+
|
|
12
|
+
if (command === 'help') {
|
|
13
|
+
io.stdout.write(helpText());
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (command === 'tools') {
|
|
18
|
+
io.stdout.write(`${JSON.stringify(TOOL_DEFINITIONS, null, 2)}\n`);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (command === 'config') {
|
|
23
|
+
const config = loadConfig(parsed.options.env);
|
|
24
|
+
writeResult(io, config, parsed.options);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (command === 'doctor') {
|
|
29
|
+
const checks = [
|
|
30
|
+
{
|
|
31
|
+
name: 'task_memory',
|
|
32
|
+
...checkTaskMemoryFile(parsed.options.taskMemoryPath)
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
writeResult(io, {
|
|
36
|
+
ok: checks.every((check) => check.ok),
|
|
37
|
+
checks
|
|
38
|
+
}, parsed.options);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (command === 'mcp' || command === 'mcp-server') {
|
|
43
|
+
const server = createMcpServer(toControllerOptions(parsed.options));
|
|
44
|
+
await server.listenStdio(io.stdin, io.stdout);
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const controller = createController(toControllerOptions(parsed.options));
|
|
49
|
+
try {
|
|
50
|
+
if (command === 'devices') {
|
|
51
|
+
const result = await controller.listServerDevices(parsed.options);
|
|
52
|
+
writeResult(io, result, parsed.options);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (command === 'select-device') {
|
|
57
|
+
const deviceId = parsed.positionals[0] || parsed.options.deviceId;
|
|
58
|
+
const result = await controller.selectDevice(deviceId, parsed.options);
|
|
59
|
+
writeResult(io, result, parsed.options);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (command === 'command') {
|
|
64
|
+
const name = parsed.positionals[0];
|
|
65
|
+
const text = collectText(parsed.positionals.slice(1), parsed.options.text);
|
|
66
|
+
const result = await controller.sendCommand(name, text, parsed.options);
|
|
67
|
+
writeResult(io, result, parsed.options);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (command === 'text') {
|
|
72
|
+
const text = collectText(parsed.positionals, parsed.options.text);
|
|
73
|
+
const result = await controller.sendText(text, parsed.options);
|
|
74
|
+
writeResult(io, result, parsed.options);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (command === 'send-only') {
|
|
79
|
+
const name = parsed.positionals[0];
|
|
80
|
+
const text = collectText(parsed.positionals.slice(1), parsed.options.text);
|
|
81
|
+
const messageId = await controller.sendOnlyCommand(name, text, parsed.options);
|
|
82
|
+
writeResult(io, { messageId }, parsed.options);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (command === 'device-context') {
|
|
87
|
+
const result = await controller.getDeviceContext(parsed.options);
|
|
88
|
+
writeResult(io, result, parsed.options);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (command === 'memory') {
|
|
93
|
+
const result = controller.taskMemory(memoryArgs(parsed.positionals, parsed.options));
|
|
94
|
+
writeResult(io, result, parsed.options);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (command === 'stream') {
|
|
99
|
+
const text = collectText(parsed.positionals, parsed.options.text, true);
|
|
100
|
+
const file = await controller.startScreenStream(text, parsed.options);
|
|
101
|
+
writeResult(io, { file }, parsed.options);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (command === 'view') {
|
|
106
|
+
const text = collectText(parsed.positionals, parsed.options.text, true);
|
|
107
|
+
const server = await controller.startLiveView(text, parsed.options);
|
|
108
|
+
const shutdown = () => {
|
|
109
|
+
server.close().catch((error) => {
|
|
110
|
+
io.stderr.write(`${error.message || error}\n`);
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
process.once('SIGINT', shutdown);
|
|
114
|
+
process.once('SIGTERM', shutdown);
|
|
115
|
+
io.stderr.write(`KLN live view: ${server.url}\n`);
|
|
116
|
+
await server.wait();
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new Error(`unknown command: ${command}`);
|
|
121
|
+
} finally {
|
|
122
|
+
await controller.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseArgs(argv) {
|
|
127
|
+
const options = {};
|
|
128
|
+
const positionals = [];
|
|
129
|
+
let command;
|
|
130
|
+
|
|
131
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
132
|
+
const arg = argv[index];
|
|
133
|
+
|
|
134
|
+
if (arg === '--') {
|
|
135
|
+
positionals.push(...argv.slice(index + 1));
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (arg.startsWith('--')) {
|
|
140
|
+
const eq = arg.indexOf('=');
|
|
141
|
+
const key = normalizeOptionName(eq === -1 ? arg.slice(2) : arg.slice(2, eq));
|
|
142
|
+
const value = eq === -1 ? readOptionValue(argv, index + 1, key) : arg.slice(eq + 1);
|
|
143
|
+
if (eq === -1 && key !== 'json') {
|
|
144
|
+
index += 1;
|
|
145
|
+
}
|
|
146
|
+
setOption(options, key, value);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!command && COMMANDS.has(arg)) {
|
|
151
|
+
command = arg;
|
|
152
|
+
} else {
|
|
153
|
+
positionals.push(arg);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { command, options, positionals };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function readOptionValue(argv, index, key) {
|
|
161
|
+
if (key === 'json') {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const value = argv[index];
|
|
166
|
+
if (value === undefined || value.startsWith('--')) {
|
|
167
|
+
throw new Error(`missing value for --${key}`);
|
|
168
|
+
}
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function setOption(options, key, value) {
|
|
173
|
+
if (key === 'relayUrls') {
|
|
174
|
+
options.relayUrls = options.relayUrls || [];
|
|
175
|
+
options.relayUrls.push(value);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
options[key] = value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function normalizeOptionName(name) {
|
|
182
|
+
switch (name) {
|
|
183
|
+
case 'relay-url':
|
|
184
|
+
case 'relay':
|
|
185
|
+
return 'relayUrls';
|
|
186
|
+
case 'timeout':
|
|
187
|
+
case 'timeout-ms':
|
|
188
|
+
return 'timeoutMs';
|
|
189
|
+
case 'output':
|
|
190
|
+
case 'output-dir':
|
|
191
|
+
return 'outputDir';
|
|
192
|
+
default:
|
|
193
|
+
return name.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function toControllerOptions(options) {
|
|
198
|
+
return {
|
|
199
|
+
ticket: options.ticket,
|
|
200
|
+
apiKey: options.apiKey,
|
|
201
|
+
apiUrl: options.apiUrl,
|
|
202
|
+
deviceId: options.deviceId,
|
|
203
|
+
deviceCachePath: options.deviceCachePath,
|
|
204
|
+
relayUrls: options.relayUrls,
|
|
205
|
+
alpn: options.alpn,
|
|
206
|
+
timeoutMs: options.timeoutMs,
|
|
207
|
+
outputDir: options.outputDir,
|
|
208
|
+
taskMemoryPath: options.taskMemoryPath,
|
|
209
|
+
trace_id: options.traceId,
|
|
210
|
+
env: options.env
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function memoryArgs(positionals, options) {
|
|
215
|
+
const op = positionals[0] || options.op;
|
|
216
|
+
const key = options.key || positionals[1];
|
|
217
|
+
const args = {
|
|
218
|
+
op,
|
|
219
|
+
key,
|
|
220
|
+
value: options.value,
|
|
221
|
+
summary: options.summary,
|
|
222
|
+
progress: options.progress
|
|
223
|
+
};
|
|
224
|
+
if (op === 'put' && args.value === undefined && positionals.length > 2) {
|
|
225
|
+
args.value = positionals.slice(2).join(' ');
|
|
226
|
+
}
|
|
227
|
+
return args;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function collectText(positionals, explicitText, allowEmpty = false) {
|
|
231
|
+
if (explicitText !== undefined) {
|
|
232
|
+
return explicitText;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const text = positionals.join(' ');
|
|
236
|
+
if (!allowEmpty && text.length === 0) {
|
|
237
|
+
throw new Error('text is required');
|
|
238
|
+
}
|
|
239
|
+
return text;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function writeResult(io, result, options) {
|
|
243
|
+
if (options.json) {
|
|
244
|
+
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (result && typeof result === 'object') {
|
|
249
|
+
if (result.summary) {
|
|
250
|
+
io.stdout.write(`${result.summary}\n`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (result.file) {
|
|
254
|
+
io.stdout.write(`${result.file}\n`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function helpText() {
|
|
263
|
+
return `kln-mobile-ctrl MCP CLI
|
|
264
|
+
|
|
265
|
+
Usage:
|
|
266
|
+
node mcp/index.js mcp-server --env dev --ticket <ticket>
|
|
267
|
+
node mcp/index.js mcp-server --env dev --api-key <api-key>
|
|
268
|
+
node mcp/index.js devices --api-key <api-key>
|
|
269
|
+
node mcp/index.js select-device <device-id> --api-key <api-key>
|
|
270
|
+
node mcp/index.js command <name> [text] --ticket <ticket>
|
|
271
|
+
node mcp/index.js text <text> --ticket <ticket>
|
|
272
|
+
node mcp/index.js send-only <name> [text] --ticket <ticket>
|
|
273
|
+
node mcp/index.js device-context --ticket <ticket>
|
|
274
|
+
node mcp/index.js memory list|get|put|delete [key] [value] --summary <text> --progress <text>
|
|
275
|
+
node mcp/index.js doctor
|
|
276
|
+
node mcp/index.js stream [text] --output-dir outputs --ticket <ticket>
|
|
277
|
+
node mcp/index.js view [json-config] --port 8090 --ticket <ticket>
|
|
278
|
+
node mcp/index.js config [--env dev|pro]
|
|
279
|
+
node mcp/index.js tools
|
|
280
|
+
|
|
281
|
+
Options:
|
|
282
|
+
--ticket <ticket> P2P endpoint ticket, or set KLN_TICKET.
|
|
283
|
+
--api-key <key> KLN server API key, or set KLN_API_KEY.
|
|
284
|
+
--api-url <url> KLN server URL, or set KLN_API_URL.
|
|
285
|
+
--device-id <id> Server device ID, or use cached selection.
|
|
286
|
+
--device-cache-path <p> Selected device cache file.
|
|
287
|
+
--relay-url <url> Relay URL. Can be repeated; defaults to mcp/config.
|
|
288
|
+
--timeout-ms <ms> Response timeout. Default: 30000.
|
|
289
|
+
--trace-id <id> Use a caller-provided trace ID. Generated automatically when omitted.
|
|
290
|
+
--task-memory-path <p> Task memory JSON file. Default: ~/.kln/task_memory.json.
|
|
291
|
+
--output-dir <dir> Stream output directory. Default: ./outputs.
|
|
292
|
+
--host <host> Live view host. Default: 127.0.0.1.
|
|
293
|
+
--port <port> Live view port. Default: 8090.
|
|
294
|
+
--env <dev|pro> Config environment. Default: KLN_ENV/NODE_ENV/dev.
|
|
295
|
+
--alpn <value> Override ALPN.
|
|
296
|
+
--json Print JSON output.
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = {
|
|
301
|
+
run,
|
|
302
|
+
parseArgs,
|
|
303
|
+
helpText
|
|
304
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface KlnMcpConfig {
|
|
2
|
+
env: 'dev' | 'pro';
|
|
3
|
+
api_url: string;
|
|
4
|
+
relay_urls: string[];
|
|
5
|
+
openobserve: KlnOpenObserveConfig;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface KlnOpenObserveConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
upload_url: string;
|
|
11
|
+
service_name: string;
|
|
12
|
+
batch_max_records?: number;
|
|
13
|
+
batch_flush_ms?: number;
|
|
14
|
+
disk_buffer_dir?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export declare function loadConfig(runtimeEnv?: string): KlnMcpConfig;
|
|
18
|
+
export declare function normalizeEnv(runtimeEnv?: string): 'dev' | 'pro';
|
|
19
|
+
export declare function resolveConfigPath(runtimeEnv?: string): string;
|