@reconcrap/boss-recruit-mcp 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 ADDED
@@ -0,0 +1,82 @@
1
+ # @reconcrap/boss-recruit-mcp
2
+
3
+ 统一招聘流水线 MCP(stdio)服务。将 `boss-search-cli` 与 `boss-screen-cli` 串联为单工具:
4
+
5
+ - 工具名:`run_recruit_pipeline`
6
+ - 状态:`NEED_INPUT` / `NEED_CONFIRMATION` / `COMPLETED` / `FAILED`
7
+
8
+ ## 通过 npm / npx 安装
9
+
10
+ 全局安装:
11
+
12
+ ```bash
13
+ npm install -g @reconcrap/boss-recruit-mcp
14
+ boss-recruit-mcp install
15
+ ```
16
+
17
+ 一次性执行安装:
18
+
19
+ ```bash
20
+ npx @reconcrap/boss-recruit-mcp install
21
+ ```
22
+
23
+ 安装命令会:
24
+
25
+ - 安装 Codex skill 到 `$CODEX_HOME/skills/boss-recruit-pipeline`
26
+ - 初始化用户配置到 `$CODEX_HOME/boss-recruit-mcp/screening-config.json`
27
+ - 包内自带 `boss-search-cli` 与 `boss-screen-cli` 运行时文件,无需额外目录结构
28
+
29
+ ## 准备配置
30
+
31
+ 1. 初始化后编辑用户配置文件:
32
+
33
+ ```bash
34
+ $CODEX_HOME/boss-recruit-mcp/screening-config.json
35
+ ```
36
+
37
+ 2. 填写以下字段:
38
+
39
+ - `baseUrl` / `apiKey` / `model` 必填
40
+ - `calibrationFile` 可选,默认走 `boss-screen-cli/favorite-calibration.json`
41
+ - `outputDir` 可选,不填则输出到 `boss-screen-cli` 目录
42
+
43
+ ## 运行
44
+
45
+ ```bash
46
+ boss-recruit-mcp start
47
+ ```
48
+
49
+ 该服务通过 stdio 与 MCP client 通信。
50
+
51
+ ## 工具输入
52
+
53
+ ```json
54
+ {
55
+ "instruction": "自然语言招聘指令",
56
+ "confirmation": {
57
+ "keyword_confirmed": true,
58
+ "keyword_value": "ai infra"
59
+ },
60
+ "overrides": {
61
+ "target_count": 500
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## 行为说明
67
+
68
+ - 若缺 `city/degree/schools/keyword/target_count`,返回 `NEED_INPUT`
69
+ - 若 keyword 由语义自动抽取(非显式给出),返回 `NEED_CONFIRMATION`
70
+ - 确认后自动执行:搜索 CLI -> 筛选 CLI
71
+ - 返回摘要:目标数、已处理、通过数、耗时、输出 CSV
72
+ - 执行前会先做本地依赖预检查,若目录 / 入口 / 配置文件缺失则返回 `PIPELINE_PREFLIGHT_FAILED`
73
+ - 若当前运行环境不允许启动子进程,会返回更明确的权限错误码而不是笼统失败
74
+ - 配置文件查找顺序:`BOSS_RECRUIT_SCREEN_CONFIG` > 工作区 `boss-recruit-mcp/config/screening-config.json` > 用户目录 `$CODEX_HOME/boss-recruit-mcp/screening-config.json` > 包内示例配置
75
+
76
+ ## 发布
77
+
78
+ ```bash
79
+ npm publish
80
+ ```
81
+
82
+ 该包已设置 `publishConfig.access=public`。
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../src/cli.js";
@@ -0,0 +1,7 @@
1
+ {
2
+ "baseUrl": "https://your-llm-endpoint.example.com/v1",
3
+ "apiKey": "replace-with-your-api-key",
4
+ "model": "replace-with-your-model-name",
5
+ "calibrationFile": "../boss-screen-cli/favorite-calibration.json",
6
+ "outputDir": "../boss-screen-cli/output"
7
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@reconcrap/boss-recruit-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Unified MCP pipeline for boss-search-cli and boss-screen-cli",
5
+ "keywords": [
6
+ "boss",
7
+ "mcp",
8
+ "codex",
9
+ "recruiting",
10
+ "boss-zhipin"
11
+ ],
12
+ "type": "module",
13
+ "main": "src/index.js",
14
+ "bin": {
15
+ "boss-recruit-mcp": "bin/boss-recruit-mcp.js"
16
+ },
17
+ "scripts": {
18
+ "start": "node src/index.js",
19
+ "cli": "node src/cli.js",
20
+ "install:local": "node src/cli.js install",
21
+ "test:parser": "node src/test-parser.js"
22
+ },
23
+ "files": [
24
+ "bin",
25
+ "config/screening-config.example.json",
26
+ "skills",
27
+ "src",
28
+ "vendor",
29
+ "README.md"
30
+ ],
31
+ "dependencies": {
32
+ "chrome-remote-interface": "^0.33.3",
33
+ "ws": "^8.19.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "license": "MIT"
42
+ }
@@ -0,0 +1,20 @@
1
+ # boss-recruit-pipeline skill
2
+
3
+ 随 `boss-recruit-mcp` npm 包一起分发的 Codex skill。
4
+
5
+ ## 安装
6
+
7
+ ```powershell
8
+ npx boss-recruit-mcp install
9
+ ```
10
+
11
+ 这条命令会:
12
+
13
+ - 把 skill 安装到 `$CODEX_HOME/skills/boss-recruit-pipeline`
14
+ - 在 `$CODEX_HOME/boss-recruit-mcp/screening-config.json` 创建配置模板
15
+
16
+ ## 前置要求
17
+
18
+ - Chrome 已使用 `--remote-debugging-port=9222` 启动
19
+ - Boss 页面已登录
20
+ - 已在用户配置中填写有效的 `baseUrl`、`apiKey`、`model`
@@ -0,0 +1,83 @@
1
+ # Boss Recruit Pipeline Skill
2
+
3
+ ## Purpose
4
+
5
+ 当用户给出招聘需求时,优先调用 MCP 工具 `run_recruit_pipeline` 完成端到端任务:
6
+
7
+ 1. 解析指令
8
+ 2. 校验必填项
9
+ 3. 关键词自动提取与确认
10
+ 4. 调用搜索与筛选工具
11
+ 5. 返回简洁摘要结果
12
+
13
+ 适用场景:
14
+
15
+ - 在 Boss 上按城市 / 学历 / 学校标签搜索候选人
16
+ - 再按自然语言 criteria 做二次 LLM 筛选
17
+ - 适合“做过某方向 + 额外论文/项目/经历要求”的招聘请求
18
+
19
+ ## Required MCP Tool
20
+
21
+ - Tool name: `run_recruit_pipeline`
22
+ - Input:
23
+ - `instruction` (string, required)
24
+ - `confirmation` (object, optional)
25
+ - `overrides` (object, optional)
26
+
27
+ ## Execution Policy
28
+
29
+ 1. 收到招聘指令后,先调用一次 `run_recruit_pipeline`(只传 `instruction`)。
30
+ 2. 若返回 `NEED_INPUT`:
31
+ - 一次性向用户列出 `missing_fields` 所有缺失项;
32
+ - 缺失项常见含义:
33
+ - `city`: 城市,如“杭州”
34
+ - `degree`: 学历,如“本科”“硕士及以上”
35
+ - `schools`: 学校标签,如“985、211、qs100”
36
+ - `target_count`: 目标筛选人数,如“10”
37
+ - 用户补充后再次调用工具。
38
+ 3. 若返回 `NEED_CONFIRMATION`:
39
+ - 询问用户是否确认 `proposed_keyword`;
40
+ - 若确认,带 `confirmation.keyword_confirmed=true` 和 `keyword_value` 再次调用;
41
+ - 若用户修改关键词,传用户给的新词作为 `keyword_value` 再次调用。
42
+ 4. 若返回 `COMPLETED`:
43
+ - 向用户返回摘要:目标数、已处理、通过数、耗时、输出文件路径。
44
+ 5. 若返回 `FAILED`:
45
+ - 先提炼 `error.code`、`error.message`、`diagnostics`;
46
+ - 如果是 `PIPELINE_PREFLIGHT_FAILED`,明确指出缺失的本地目录 / 文件;
47
+ - 如果是 `SEARCH_PROCESS_PERMISSION_DENIED` 或 `SCREEN_PROCESS_PERMISSION_DENIED`,明确说明“当前环境拒绝创建子进程”,建议用户在本地终端直接运行 MCP;
48
+ - 如果是 `SEARCH_CLI_MISSING` / `SCREEN_CLI_MISSING` / `SCREEN_CONFIG_ERROR`,直接告诉用户缺什么,不要只说“重试”;
49
+ - 若是可修复输入问题,提示用户修正条件后重试。
50
+
51
+ ## Input Guidance
52
+
53
+ - 优先鼓励用户一次性给全这些字段:城市、学历、学校标签、目标人数、核心方向关键词。
54
+ - 当用户提到“做过 AI infra / 推荐系统 / 搜索 / 广告 / 多模态”等经历,但没有显式写“关键词”,默认允许流水线先自动抽取,再走确认分支。
55
+ - 当用户附带筛选要求(如“必须发表过 CCF-A 区论文”“有开源项目”“带过团队”),这些要求应该保留在 `criteria` 中,不应被误当作搜索过滤条件。
56
+ - 回答时不要暴露 `screening-config.json` 中的 `apiKey`、`baseUrl` 等敏感值。
57
+
58
+ ## Failure Handling
59
+
60
+ - 不要把底层 stderr 原样大段贴给用户,只提炼关键错误和下一步。
61
+ - 如果失败原因明显是环境问题,要直接说明不是用户输入有误。
62
+ - 如果工具已经返回 `diagnostics.checks`,优先基于这些检查项生成排障建议。
63
+ - 如果工具返回 `output_csv`,在摘要里给出路径,避免重复解释内部流程。
64
+
65
+ ## Example
66
+
67
+ 用户:
68
+
69
+ - “在 Boss 上找做过 AI infra 的候选人,必须发过 CCF-A 区论文,城市杭州,本科,学校 985/211/QS100,目标 10 人”
70
+
71
+ 期望行为:
72
+
73
+ 1. 首次调用流水线。
74
+ 2. 若 keyword 被自动提取为 `AI infra`,先让用户确认。
75
+ 3. 确认后再次调用。
76
+ 4. 成功则返回通过人数与 CSV 路径;失败则按错误类型给出下一步。
77
+
78
+ ## Response Style
79
+
80
+ - 优先结构化、简洁中文输出。
81
+ - 不展示密钥和底层敏感配置。
82
+ - 不跳过 `NEED_CONFIRMATION` 分支。
83
+ - 若运行失败,优先给用户“现在卡在哪一步 + 怎么继续”。
@@ -0,0 +1,352 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ const currentFilePath = fileURLToPath(import.meta.url);
7
+ const packagedMcpDir = path.resolve(path.dirname(currentFilePath), "..");
8
+
9
+ function getCodexHome() {
10
+ return process.env.CODEX_HOME
11
+ ? path.resolve(process.env.CODEX_HOME)
12
+ : path.join(os.homedir(), ".codex");
13
+ }
14
+
15
+ function getUserConfigPath() {
16
+ return path.join(getCodexHome(), "boss-recruit-mcp", "screening-config.json");
17
+ }
18
+
19
+ function resolveScreenConfigPath(workspaceRoot) {
20
+ const envConfigPath = process.env.BOSS_RECRUIT_SCREEN_CONFIG
21
+ ? path.resolve(process.env.BOSS_RECRUIT_SCREEN_CONFIG)
22
+ : null;
23
+ const workspaceConfigPath = path.join(workspaceRoot, "boss-recruit-mcp", "config", "screening-config.json");
24
+ const userConfigPath = getUserConfigPath();
25
+ const packagedConfigPath = path.join(packagedMcpDir, "config", "screening-config.json");
26
+ const candidates = [
27
+ envConfigPath,
28
+ workspaceConfigPath,
29
+ userConfigPath,
30
+ packagedConfigPath
31
+ ].filter(Boolean);
32
+
33
+ return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
34
+ }
35
+
36
+ function pathExists(targetPath) {
37
+ try {
38
+ return fs.existsSync(targetPath);
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ function resolveSearchCliDir(workspaceRoot) {
45
+ const localDir = path.join(workspaceRoot, "boss-search-cli");
46
+ if (pathExists(localDir)) {
47
+ return localDir;
48
+ }
49
+ const vendoredDir = path.join(packagedMcpDir, "vendor", "boss-search-cli");
50
+ if (pathExists(vendoredDir)) {
51
+ return vendoredDir;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ function resolveScreenCliDir(workspaceRoot) {
57
+ const localDir = path.join(workspaceRoot, "boss-screen-cli");
58
+ if (pathExists(localDir)) {
59
+ return localDir;
60
+ }
61
+ const vendoredDir = path.join(packagedMcpDir, "vendor", "boss-screen-cli");
62
+ if (pathExists(vendoredDir)) {
63
+ return vendoredDir;
64
+ }
65
+ return null;
66
+ }
67
+
68
+ function resolveScreenCliEntry(screenDir) {
69
+ const candidates = [
70
+ path.join(screenDir, "boss-screen-cli.js"),
71
+ path.join(screenDir, "boss-screen-cli.cjs")
72
+ ];
73
+ return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
74
+ }
75
+
76
+ function runProcess({ command, args, cwd, timeoutMs }) {
77
+ return new Promise((resolve) => {
78
+ let stdout = "";
79
+ let stderr = "";
80
+ let settled = false;
81
+ let timer = null;
82
+
83
+ function finish(payload) {
84
+ if (settled) return;
85
+ settled = true;
86
+ if (timer) clearTimeout(timer);
87
+ resolve(payload);
88
+ }
89
+
90
+ let child;
91
+ try {
92
+ child = spawn(command, args, {
93
+ cwd,
94
+ windowsHide: true,
95
+ shell: false,
96
+ env: process.env
97
+ });
98
+ } catch (error) {
99
+ finish({
100
+ code: -1,
101
+ stdout,
102
+ stderr: error.message,
103
+ error_code: error.code || "SPAWN_FAILED"
104
+ });
105
+ return;
106
+ }
107
+
108
+ if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) {
109
+ timer = setTimeout(() => {
110
+ try {
111
+ child.kill();
112
+ } catch {}
113
+ finish({
114
+ code: -1,
115
+ stdout,
116
+ stderr: `${stderr}\nProcess timed out after ${timeoutMs}ms`.trim(),
117
+ error_code: "TIMEOUT"
118
+ });
119
+ }, timeoutMs);
120
+ }
121
+
122
+ child.stdout.on("data", (chunk) => {
123
+ stdout += chunk.toString();
124
+ });
125
+ child.stderr.on("data", (chunk) => {
126
+ stderr += chunk.toString();
127
+ });
128
+
129
+ child.on("close", (code) => {
130
+ finish({ code, stdout, stderr });
131
+ });
132
+ child.on("error", (error) => {
133
+ finish({
134
+ code: -1,
135
+ stdout,
136
+ stderr: `${stderr}\n${error.message}`.trim(),
137
+ error_code: error.code || "SPAWN_FAILED"
138
+ });
139
+ });
140
+ });
141
+ }
142
+
143
+ function parseSearchCount(output) {
144
+ const m = output.match(/找到\s*(\d+)\s*个候选人/);
145
+ if (!m) return null;
146
+ return Number.parseInt(m[1], 10);
147
+ }
148
+
149
+ function parseScreenSummary(output) {
150
+ const processed = output.match(/已处理:\s*(\d+)\s*人/);
151
+ const passed = output.match(/通过筛选:\s*(\d+)\s*人/);
152
+ const target = output.match(/目标人数:\s*(\d+)\s*人/);
153
+ const csv = output.match(/结果已导出到:\s*(.+)/);
154
+
155
+ return {
156
+ processed_count: processed ? Number.parseInt(processed[1], 10) : null,
157
+ passed_count: passed ? Number.parseInt(passed[1], 10) : null,
158
+ target_count: target ? Number.parseInt(target[1], 10) : null,
159
+ output_csv: csv ? csv[1].trim() : null
160
+ };
161
+ }
162
+
163
+ function loadScreenConfig(configPath) {
164
+ if (!fs.existsSync(configPath)) {
165
+ return {
166
+ ok: false,
167
+ error: `Screen config file not found: ${configPath}`
168
+ };
169
+ }
170
+ try {
171
+ const content = fs.readFileSync(configPath, "utf8");
172
+ const parsed = JSON.parse(content);
173
+ if (!parsed.baseUrl || !parsed.apiKey || !parsed.model) {
174
+ return {
175
+ ok: false,
176
+ error: "Invalid screen config: baseUrl/apiKey/model are required"
177
+ };
178
+ }
179
+ return { ok: true, config: parsed };
180
+ } catch (error) {
181
+ return { ok: false, error: `Failed to read screen config: ${error.message}` };
182
+ }
183
+ }
184
+
185
+ export function runPipelinePreflight(workspaceRoot) {
186
+ const searchDir = resolveSearchCliDir(workspaceRoot);
187
+ const screenDir = resolveScreenCliDir(workspaceRoot);
188
+ const screenConfigPath = resolveScreenConfigPath(workspaceRoot);
189
+ const checks = [
190
+ {
191
+ key: "search_cli_dir",
192
+ ok: Boolean(searchDir && pathExists(searchDir)),
193
+ path: searchDir || localDirHint(workspaceRoot, "boss-search-cli"),
194
+ message: "boss-search-cli 目录不存在"
195
+ },
196
+ {
197
+ key: "search_cli_entry",
198
+ ok: Boolean(searchDir && pathExists(path.join(searchDir, "src", "cli.js"))),
199
+ path: searchDir ? path.join(searchDir, "src", "cli.js") : path.join(localDirHint(workspaceRoot, "boss-search-cli"), "src", "cli.js"),
200
+ message: "boss-search-cli 入口文件缺失"
201
+ },
202
+ {
203
+ key: "screen_cli_dir",
204
+ ok: Boolean(screenDir && pathExists(screenDir)),
205
+ path: screenDir || localDirHint(workspaceRoot, "boss-screen-cli"),
206
+ message: "boss-screen-cli 目录不存在"
207
+ },
208
+ {
209
+ key: "screen_cli_entry",
210
+ ok: Boolean(screenDir && pathExists(resolveScreenCliEntry(screenDir))),
211
+ path: screenDir ? resolveScreenCliEntry(screenDir) : path.join(localDirHint(workspaceRoot, "boss-screen-cli"), "boss-screen-cli.js"),
212
+ message: "boss-screen-cli 入口文件缺失"
213
+ },
214
+ {
215
+ key: "screen_config",
216
+ ok: pathExists(screenConfigPath),
217
+ path: screenConfigPath,
218
+ message: "screening-config.json 不存在"
219
+ }
220
+ ];
221
+
222
+ return {
223
+ ok: checks.every((item) => item.ok),
224
+ checks
225
+ };
226
+ }
227
+
228
+ function localDirHint(workspaceRoot, dirName) {
229
+ return path.join(workspaceRoot, dirName);
230
+ }
231
+
232
+ export async function runSearchCli({ workspaceRoot, searchParams }) {
233
+ const searchDir = resolveSearchCliDir(workspaceRoot);
234
+ if (!searchDir) {
235
+ return {
236
+ ok: false,
237
+ exit_code: -1,
238
+ candidate_count: null,
239
+ stdout: "",
240
+ stderr: "boss-search-cli package not found",
241
+ error_code: "ENOENT"
242
+ };
243
+ }
244
+ const cliPath = path.join(searchDir, "src", "cli.js");
245
+ const args = [
246
+ cliPath,
247
+ "--keywords",
248
+ searchParams.keyword,
249
+ "--degree",
250
+ searchParams.degree,
251
+ "--schools",
252
+ searchParams.schools.join(","),
253
+ "--city",
254
+ searchParams.city
255
+ ];
256
+
257
+ const result = await runProcess({
258
+ command: "node",
259
+ args,
260
+ cwd: searchDir,
261
+ timeoutMs: 180000
262
+ });
263
+
264
+ const combined = `${result.stdout}\n${result.stderr}`;
265
+ const candidateCount = parseSearchCount(combined);
266
+
267
+ return {
268
+ ok: result.code === 0,
269
+ exit_code: result.code,
270
+ candidate_count: candidateCount,
271
+ stdout: result.stdout,
272
+ stderr: result.stderr,
273
+ error_code: result.error_code || null
274
+ };
275
+ }
276
+
277
+ export async function runScreenCli({ workspaceRoot, screenParams }) {
278
+ const screenDir = resolveScreenCliDir(workspaceRoot);
279
+ if (!screenDir) {
280
+ return {
281
+ ok: false,
282
+ exit_code: -1,
283
+ summary: null,
284
+ stdout: "",
285
+ stderr: "boss-screen-cli package not found",
286
+ error_code: "ENOENT"
287
+ };
288
+ }
289
+ const cliPath = resolveScreenCliEntry(screenDir);
290
+
291
+ const configPath = resolveScreenConfigPath(workspaceRoot);
292
+ const configBaseDir = path.dirname(configPath);
293
+ const loaded = loadScreenConfig(configPath);
294
+ if (!loaded.ok) {
295
+ return {
296
+ ok: false,
297
+ config_error: true,
298
+ exit_code: -1,
299
+ stdout: "",
300
+ stderr: loaded.error,
301
+ error_code: "INVALID_SCREEN_CONFIG"
302
+ };
303
+ }
304
+
305
+ const calibration = loaded.config.calibrationFile
306
+ ? path.resolve(configBaseDir, loaded.config.calibrationFile)
307
+ : path.join(screenDir, "favorite-calibration.json");
308
+
309
+ const outputName = `筛选结果_${Date.now()}.csv`;
310
+ let outputPath = outputName;
311
+ if (loaded.config.outputDir) {
312
+ const resolvedOutputDir = path.resolve(configBaseDir, loaded.config.outputDir);
313
+ fs.mkdirSync(resolvedOutputDir, { recursive: true });
314
+ outputPath = path.join(resolvedOutputDir, outputName);
315
+ }
316
+
317
+ const args = [
318
+ cliPath,
319
+ "--baseurl",
320
+ loaded.config.baseUrl,
321
+ "--apikey",
322
+ loaded.config.apiKey,
323
+ "--model",
324
+ loaded.config.model,
325
+ "--criteria",
326
+ screenParams.criteria,
327
+ "--targetCount",
328
+ String(screenParams.target_count),
329
+ "--config",
330
+ calibration,
331
+ "--output",
332
+ outputPath
333
+ ];
334
+
335
+ const result = await runProcess({
336
+ command: "node",
337
+ args,
338
+ cwd: screenDir
339
+ });
340
+
341
+ const combined = `${result.stdout}\n${result.stderr}`;
342
+ const summary = parseScreenSummary(combined);
343
+
344
+ return {
345
+ ok: result.code === 0,
346
+ exit_code: result.code,
347
+ summary,
348
+ stdout: result.stdout,
349
+ stderr: result.stderr,
350
+ error_code: result.error_code || null
351
+ };
352
+ }
package/src/cli.js ADDED
@@ -0,0 +1,112 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { startServer } from "./index.js";
7
+
8
+ const currentFilePath = fileURLToPath(import.meta.url);
9
+ const packageRoot = path.resolve(path.dirname(currentFilePath), "..");
10
+ const skillSourceDir = path.join(packageRoot, "skills", "boss-recruit-pipeline");
11
+ const exampleConfigPath = path.join(packageRoot, "config", "screening-config.example.json");
12
+
13
+ function getCodexHome() {
14
+ return process.env.CODEX_HOME
15
+ ? path.resolve(process.env.CODEX_HOME)
16
+ : path.join(os.homedir(), ".codex");
17
+ }
18
+
19
+ function ensureDir(targetPath) {
20
+ fs.mkdirSync(targetPath, { recursive: true });
21
+ }
22
+
23
+ function installSkill() {
24
+ const codexHome = getCodexHome();
25
+ const targetDir = path.join(codexHome, "skills", "boss-recruit-pipeline");
26
+ ensureDir(path.dirname(targetDir));
27
+ fs.cpSync(skillSourceDir, targetDir, { recursive: true, force: true });
28
+ return targetDir;
29
+ }
30
+
31
+ function ensureUserConfig() {
32
+ const targetDir = path.join(getCodexHome(), "boss-recruit-mcp");
33
+ const targetPath = path.join(targetDir, "screening-config.json");
34
+ ensureDir(targetDir);
35
+ if (!fs.existsSync(targetPath)) {
36
+ fs.copyFileSync(exampleConfigPath, targetPath);
37
+ return { path: targetPath, created: true };
38
+ }
39
+ return { path: targetPath, created: false };
40
+ }
41
+
42
+ function printHelp() {
43
+ console.log("boss-recruit-mcp");
44
+ console.log("");
45
+ console.log("Usage:");
46
+ console.log(" boss-recruit-mcp Start the MCP server");
47
+ console.log(" boss-recruit-mcp start Start the MCP server");
48
+ console.log(" boss-recruit-mcp install Install Codex skill and initialize user config");
49
+ console.log(" boss-recruit-mcp install-skill Install only the Codex skill");
50
+ console.log(" boss-recruit-mcp init-config Create ~/.codex/boss-recruit-mcp/screening-config.json if missing");
51
+ console.log(" boss-recruit-mcp where Print installed package, skill, and config paths");
52
+ }
53
+
54
+ function printPaths() {
55
+ const codexHome = getCodexHome();
56
+ console.log(`package_root=${packageRoot}`);
57
+ console.log(`skill_source=${skillSourceDir}`);
58
+ console.log(`codex_home=${codexHome}`);
59
+ console.log(`skill_target=${path.join(codexHome, "skills", "boss-recruit-pipeline")}`);
60
+ console.log(`config_target=${path.join(codexHome, "boss-recruit-mcp", "screening-config.json")}`);
61
+ }
62
+
63
+ function installAll() {
64
+ const skillTarget = installSkill();
65
+ const configResult = ensureUserConfig();
66
+ console.log(`Skill installed to: ${skillTarget}`);
67
+ if (configResult.created) {
68
+ console.log(`Config template created at: ${configResult.path}`);
69
+ } else {
70
+ console.log(`Config already exists at: ${configResult.path}`);
71
+ }
72
+ console.log("");
73
+ console.log("Next steps:");
74
+ console.log("1. Fill in baseUrl/apiKey/model in the config file above.");
75
+ console.log("2. Start Chrome with --remote-debugging-port=9222 and log in to Boss.");
76
+ console.log("3. Run `boss-recruit-mcp start` or configure your MCP client to launch `boss-recruit-mcp`.");
77
+ }
78
+
79
+ const command = process.argv[2] || "start";
80
+
81
+ switch (command) {
82
+ case "start":
83
+ startServer();
84
+ break;
85
+ case "install":
86
+ installAll();
87
+ break;
88
+ case "install-skill":
89
+ console.log(`Skill installed to: ${installSkill()}`);
90
+ break;
91
+ case "init-config": {
92
+ const result = ensureUserConfig();
93
+ console.log(
94
+ result.created
95
+ ? `Config template created at: ${result.path}`
96
+ : `Config already exists at: ${result.path}`
97
+ );
98
+ break;
99
+ }
100
+ case "where":
101
+ printPaths();
102
+ break;
103
+ case "help":
104
+ case "--help":
105
+ case "-h":
106
+ printHelp();
107
+ break;
108
+ default:
109
+ console.error(`Unknown command: ${command}`);
110
+ console.error("Run `boss-recruit-mcp --help` for usage.");
111
+ process.exitCode = 1;
112
+ }