@scorehub/creator-outreach 1.1.11 → 1.2.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiktok-creator-outreach",
3
- "version": "1.1.11",
3
+ "version": "1.2.1",
4
4
  "description": "ScoreHub AI's TikTok creator marketing expert for building clear creator profiles and validating focused collaboration shortlists across Southeast Asia.",
5
5
  "author": {
6
6
  "name": "Ben",
package/README.en.md CHANGED
@@ -21,13 +21,9 @@ For the public release, end users only need to:
21
21
 
22
22
  3. Return to WorkBuddy and open the “Tiky · TikTok Creator Marketing Expert” conversation
23
23
 
24
- At the start of a new conversation, Tiky first helps check:
24
+ Tiky uses `npx -y @scorehub/creator-outreach@latest` to run a bootstrap check before every new conversation. When the machine is uninitialized, Tiky explains that the ScoreHub local component is required and waits for explicit confirmation. It then prefetches and self-checks `@scorehub/mcp-server@latest` before updating Tiky's files and the WorkBuddy `scorehub` MCP configuration. WorkBuddy must be fully restarted after the initial installation.
25
25
 
26
- - whether the plugin should be upgraded
27
- - whether the current WorkBuddy environment can run `npx -y @scorehub/creator-outreach@latest`
28
- - whether the local `scorehub` MCP configuration or `mcp-server` bootstrap is missing, outdated, or broken
29
-
30
- When needed, Tiky first explains the action and waits for confirmation, then runs `npx -y @scorehub/creator-outreach@latest` to complete or repair the local bootstrap. That command refreshes Tiky's local installation and rewrites the WorkBuddy `scorehub` MCP configuration. After that, `@scorehub/mcp-server` still starts through the configured `@latest` command on first use.
26
+ That first confirmation also authorizes future silent creator-outreach updates, so Tiky does not ask again for every release. The MCP launcher always resolves `@scorehub/mcp-server@latest`; it is never pinned to a specific version.
31
27
 
32
28
  The share link only installs Tiky into WorkBuddy. In the normal public path, users do not need to install `@scorehub/mcp-server` manually or edit `mcp.json` themselves.
33
29
 
@@ -45,11 +41,11 @@ The installer configures WorkBuddy only. If it does not find WorkBuddy, it exits
45
41
  |---|---|---|---|
46
42
  | WorkBuddy | `~/.workbuddy/` exists | Plugin files in `~/.workbuddy/plugins/.../`; MCP configuration merged into `~/.workbuddy/mcp.json` | Restart, then select “ScoreHub AI TikTok Creator Marketing Expert” from the Agent list |
47
43
 
48
- After Tiky completes the local bootstrap, the MCP server (`@scorehub/mcp-server`) is started automatically through `npx -y @scorehub/mcp-server@latest`. If WorkBuddy already provides a working bundled Node.js / npx, Tiky reuses it. Tiky only guides users to the official Node.js LTS installer when there is explicit evidence that the runtime is missing or too old.
44
+ Before writing the configuration, Tiky verifies the MCP server with `--self-check --json`, then WorkBuddy starts it automatically. macOS and Linux use `npx -y @scorehub/mcp-server@latest`. Windows reuses WorkBuddy's managed `node.exe` to load the matching npm `npx-cli.js` directly, avoiding `.cmd` / `.bat` process-launch compatibility problems. Tiky only guides users to the official Node.js LTS installer when the managed runtime is explicitly missing or too old.
49
45
 
50
46
  ## First Use and Authorization
51
47
 
52
- At the start of every new conversation, Tiky first looks for public-onboarding signals such as an outdated plugin version, an incomplete local bootstrap, a missing or broken `mcp-server` configuration, or host-provided evidence that Node.js / npm / npx is unavailable. Only when those signals are absent does it proceed to the standard introduction of creator discovery, profile evaluation, and focused outreach validation. If bootstrap or self-upgrade is needed, Tiky explains that it will run `npx -y @scorehub/creator-outreach@latest` and waits for confirmation first. Explicit account or shop switch requests are an exception: a shop switch opens the current account's shop picker, while an account switch opens ScoreHub login. After you choose an operation, its first TikTok call opens a browser for ScoreHub login. Once authorization, reauthorization, or a shop switch succeeds, Tiky confirms the current login phone number, authorized shop, shop country, and bound brands in that order. Tokens are then cached under `~/.scorehub/`, and ScoreHub clients on the same machine can reuse them without another authorization.
48
+ At the start of every new conversation, Tiky first checks one of four bootstrap states: `uninitialized`, `restart_required`, `ready`, or `repair_required`. Only `ready` permits the capability introduction, account or shop switching, and business operations. The first TikTok tool call then opens ScoreHub login when authorization is needed. After authorization, reauthorization, or a shop switch, Tiky confirms the login phone number, authorized shop, country, and bound brands. Tokens remain under `~/.scorehub/` and are not removed by bootstrap or updates.
53
49
 
54
50
  If Tiky says that the current shop's TikTok authorization is invalid or unavailable, your local ScoreHub login is usually still valid. Rebind the shop in ScoreHub and try again; do not repeat browser login.
55
51
 
@@ -71,7 +67,7 @@ Install and launch WorkBuddy once, then use the share link to launch Tiky's inst
71
67
 
72
68
  ### MCP tools are unavailable, the tool list is empty, or the connection closes
73
69
 
74
- Tiky first distinguishes between an outdated plugin, an incomplete `npx -y @scorehub/creator-outreach@latest` bootstrap, a broken local `mcp-server` setup, and a temporary ScoreHub outage. Follow Tiky's upgrade or repair guidance first. If there is no more specific diagnosis, fully quit and reopen WorkBuddy, then try again.
70
+ Tiky runs the bootstrap check first. It does not attempt OAuth or business calls while installation, restart, or repair is required. If bootstrap reports `ready` but tools remain unavailable, fully quit and reopen WorkBuddy, then try again.
75
71
 
76
72
  If an error explicitly says that Node.js, npm, or npx is missing, or that Node.js is below version 18, Tiky explains the diagnosis first. After your confirmation, it guides recovery through the official Node.js LTS installation path, verifies the result, and asks you to restart WorkBuddy. This does not reauthorize or change TikTok credentials.
77
73
 
package/README.md CHANGED
@@ -21,13 +21,9 @@ English version: [README.en.md](./README.en.md)
21
21
 
22
22
  3. 回到 WorkBuddy,打开 “Tiky · TikTok达人营销专家” 对话
23
23
 
24
- Tiky 会在新会话中优先帮助你检查:
24
+ Tiky 每个新会话都会通过 `npx -y @scorehub/creator-outreach@latest` 执行 bootstrap 检查。未初始化时,她会说明需要安装 ScoreHub 本地组件,并等待你明确确认;确认后预拉取并自检 `@scorehub/mcp-server@latest`,再更新 Tiky 本地文件和 WorkBuddy 的 `scorehub` MCP 配置。安装完成后需要完全重启 WorkBuddy。
25
25
 
26
- - 当前插件是否需要升级
27
- - WorkBuddy 当前环境是否支持执行 `npx -y @scorehub/creator-outreach@latest`
28
- - 本地 `scorehub` MCP 配置或 `mcp-server` 引导是否缺失、过旧或损坏
29
-
30
- 在需要时,Tiky 会先告知并征得你的确认,再执行 `npx -y @scorehub/creator-outreach@latest` 来完成或修复本地引导。该命令会刷新 Tiky 本地安装,并重写 WorkBuddy 中的 `scorehub` MCP 配置;之后 `@scorehub/mcp-server` 仍由配置中的 `@latest` 在首次启动时自动拉起。
26
+ 首次确认同时授权后续静默更新 creator-outreach,不会对每个新版本重复询问。MCP 始终运行 `@scorehub/mcp-server@latest`,不固定具体版本。
31
27
 
32
28
  分享链接只负责把 Tiky 放进 WorkBuddy。默认情况下,你不需要手工安装 `@scorehub/mcp-server`,也不需要自己编辑 `mcp.json`。
33
29
 
@@ -45,11 +41,11 @@ npx -y @scorehub/creator-outreach
45
41
  |---|---|---|---|
46
42
  | WorkBuddy | `~/.workbuddy/` 存在 | 插件文件写入 `~/.workbuddy/plugins/.../`;MCP 配置合并到 `~/.workbuddy/mcp.json` | 重启后在 Agent 列表选择“ScoreHub AI TikTok达人营销专家” |
47
43
 
48
- MCP Server(`@scorehub/mcp-server`)由 Tiky 完成本地引导后,通过配置中的 `npx -y @scorehub/mcp-server@latest` 在首次启动时自动拉起。WorkBuddy 若已自带可用 Node.js / npx,会优先复用;只有在存在明确缺失证据时,Tiky 才会引导你安装官方 Node.js LTS。
44
+ MCP Server(`@scorehub/mcp-server`)在写入配置前会先通过 `--self-check --json` 验证可执行性,之后由 WorkBuddy 自动拉起。macOS / Linux 使用 `npx -y @scorehub/mcp-server@latest`;Windows 复用 WorkBuddy 托管的 `node.exe` 直接加载同版本 npm 的 `npx-cli.js`,避免 `.cmd` / `.bat` 的进程启动兼容问题。只有在托管运行时明确缺失或版本过低时,Tiky 才会引导你安装官方 Node.js LTS。
49
45
 
50
46
  ## 首次使用与授权
51
47
 
52
- 每个新对话的第一轮,Tiky 会先识别公开版初始化信号:例如当前插件版本过旧、尚未完成本地引导、`mcp-server` 配置缺失或损坏、或宿主已明确报告 Node.js / npm / npx 缺失。只有在这些信号不存在时,她才进入达人搜索、画像评估和小范围建联验证的标准能力介绍。若需要本地引导或升级,她会先说明将执行 `npx -y @scorehub/creator-outreach@latest`,在你确认后再继续。明确要求切换账号或店铺时除外:切换店铺会直接打开当前账号的店铺确认页,切换账号才会打开 ScoreHub 登录页。选择需要的操作后,它才会在首次调用 TikTok 功能时打开浏览器引导你登录 ScoreHub。授权、重新授权或店铺切换成功后,Tiky 会依次确认当前登录账号、当前授权店铺、店铺所属国家和店铺关联品牌。登录后 Token 缓存在 `~/.scorehub/`,同一台机器上的 ScoreHub 客户端会复用这些授权信息,无需重复授权。
48
+ 每个新对话的第一轮,Tiky 都先检查 `uninitialized` / `restart_required` / `ready` / `repair_required` bootstrap 状态。只有 `ready` 才进入能力介绍、账号/店铺切换或业务操作。首次 TikTok 工具调用时会打开浏览器引导你登录 ScoreHub。授权、重新授权或店铺切换成功后,Tiky 会依次确认当前登录账号、当前授权店铺、店铺所属国家和店铺关联品牌。Token 缓存在 `~/.scorehub/`,不会被 bootstrap 或升级清除。
53
49
 
54
50
  如果提示当前店铺的 TikTok 授权已失效或异常,本地 ScoreHub 登录通常仍然有效。请先到 ScoreHub 重新绑定店铺,再重试;不要反复进行浏览器登录。
55
51
 
@@ -71,7 +67,7 @@ MCP Server(`@scorehub/mcp-server`)由 Tiky 完成本地引导后,通过配
71
67
 
72
68
  ### MCP 工具不可用、工具列表为空或连接已关闭
73
69
 
74
- Tiky 会优先判断是当前版本需要升级、尚未完成 `npx -y @scorehub/creator-outreach@latest` 本地引导、本地 `mcp-server` 配置异常,还是 ScoreHub 服务暂时未连接。请先按她的引导完成升级或修复;如果没有更具体的诊断结果,请完全退出并重新打开 WorkBuddy 后重试。
70
+ Tiky 会优先运行 bootstrap 检查。未安装、需重启或需修复时,她不会尝试 OAuth 或业务调用;已显示 `ready` 但工具仍不可用时,请完全退出并重新打开 WorkBuddy 后重试。
75
71
 
76
72
  如果错误明确提示找不到 Node.js、npm 或 npx,或 Node.js 版本低于 18,Tiky 会先说明诊断结论。经你确认后,它会使用 Node.js 官方 LTS 安装路径协助恢复,复检通过后提示你重启 WorkBuddy。该流程不会重新授权或修改 TikTok 凭证。
77
73
 
@@ -26,9 +26,18 @@ skills:
26
26
 
27
27
  ## 新会话首轮引导(最高优先级)
28
28
 
29
- 每个新对话的第一条回复,除“账号与店铺切换”这一会话准备操作外,**无论用户输入问候、快捷指令还是具体需求**,我都必须先完成能力介绍,不执行搜索、分析、建联或授权。不要展示工具名、技术配置、错误码或内部流程。
29
+ 每个新对话的第一条回复,**无论用户输入问候、快捷指令、账号/店铺切换还是具体业务需求**,都必须先完成 WorkBuddy bootstrap 门禁。bootstrap 未完成时不执行搜索、分析、建联、`authorize` 或 `status`,也不输出标准欢迎语。
30
30
 
31
- 在输出欢迎语前,先识别以下明确意图:
31
+ 首先执行 `npx -y @scorehub/creator-outreach@latest bootstrap --check --json`,并严格按结构化状态处理:
32
+
33
+ - `uninitialized`:只告知用户 Tiky 需要安装 ScoreHub 本地组件才能使用达人能力,且不会清除现有授权数据;**必须等待用户明确确认**。确认后执行 `npx -y @scorehub/creator-outreach@latest bootstrap --install --json`。返回 `restart_required` 后提示完全退出并重启 WorkBuddy,本轮结束。这次确认同时代表用户允许后续静默更新,不再对每个 creator-outreach 新版本重复询问。
34
+ - `restart_required`:若当前 `status` 已可用,且返回 `config_source.managed_by = "@scorehub/creator-outreach"` 与 `config_source.client_host = "workbuddy"`,执行 `npx -y @scorehub/creator-outreach@latest bootstrap --mark-ready --json` 清除重启标记并继续;否则只提示完全退出并重启 WorkBuddy,不进入业务流程。
35
+ - `repair_required`:说明结果中的可恢复原因。若明确是 Node.js / npm / npx 问题,进入“本地运行环境恢复”;其他情况经用户确认后重试 `bootstrap --install --json`。修复前不进入业务流程。
36
+ - `ready`:仅当结果同时返回 `update_due = true` 时,执行 `npx -y @scorehub/creator-outreach@latest bootstrap --update --silent --json`;否则直接继续。更新返回 `ready` 时继续;返回 `restart_required` 时可继续使用当前已加载版本,但需在首轮末尾简短提示重启后使用新版本。网络检查失败但现有 MCP 仍可用时,保留当前版本并继续,不误判为 OAuth 问题。
37
+
38
+ 宿主不支持执行本地命令时,回退为提示用户重新打开分享链接 `https://www.workbuddy.cn/work/launch/?sharecode=lNM8H05BRKoV-dw2gac3ZwQ-p01o3C3KVz1gXJ_CwvtX02-mSRqBB3xepE4V0gr7&expertname=Tiky+%C2%B7+TikTok%E8%BE%BE%E4%BA%BA%E8%90%A5%E9%94%80%E4%B8%93%E5%AE%B6&buddy_type=workbuddy` 进行覆盖安装;不要让终端用户手工编辑 `mcp.json`。
39
+
40
+ bootstrap 返回 `ready` 后,再识别以下明确意图:
32
41
 
33
42
  - 用户要求“切换店铺”“换店”“使用另一个店铺”或同义表达时,立即调用 `authorize`,参数为 `{ "switch_shop": true }`。这会直接打开当前账号的店铺确认页,**不得**传 `force`、不得要求用户重新登录、不得先输出欢迎语。
34
43
  - 用户要求“切换账号”“换号”“登录另一个账号”或同义表达时,立即调用 `authorize`,参数为 `{ "force": true }`。这会重新打开 ScoreHub 登录页,**不得**传 `switch_shop`、不得先输出欢迎语。
@@ -43,14 +52,6 @@ skills:
43
52
 
44
53
  字段值必须原样使用工具结果;“未绑定”与“未获取”也必须如实告知,不得猜测或补充。仅复用本地缓存、且工具结果未包含上述字段时,不输出这段确认信息。
45
54
 
46
- 在 WorkBuddy 公开发布场景中,若宿主、版本检测或本地诊断结果已**明确**提供以下初始化信号,也要在欢迎语前优先处理;没有明确信号时,不猜测、不阻塞欢迎语:
47
-
48
- - **Tiky 版本落后**:优先说明将执行本地命令 `npx -y @scorehub/creator-outreach@latest` 来升级 Tiky,并明确升级不会清除本机 `~/.scorehub/` 授权缓存;**必须等待用户明确确认** 后再执行。若宿主不支持执行本地命令,或命令执行失败,再回退为提示用户重新打开分享链接 `https://www.workbuddy.cn/work/launch/?sharecode=lNM8H05BRKoV-dw2gac3ZwQ-p01o3C3KVz1gXJ_CwvtX02-mSRqBB3xepE4V0gr7&expertname=Tiky+%C2%B7+TikTok%E8%BE%BE%E4%BA%BA%E8%90%A5%E9%94%80%E4%B8%93%E5%AE%B6&buddy_type=workbuddy` 进行覆盖安装。
49
- - **尚未完成本地引导,或 `mcp-server` 配置缺失、过旧或损坏**:优先说明将执行本地命令 `npx -y @scorehub/creator-outreach@latest`,用它完成 Tiky 本地引导,并重写 WorkBuddy 中名为 `scorehub` 的 MCP 配置;**必须等待用户明确确认** 后再执行。只有在宿主不支持命令执行、或该命令执行失败时,才回退为提示用户重启 WorkBuddy 或重新打开分享链接。
50
- - **`status` 已明确返回 `local_state = "running_authorized_remote_unavailable"`**:说明本地 `mcp-server` 已启动并持有授权,但远端工具发现暂不可用。此时不要建议重新授权或安装 Node.js,只提示稍后重试、完全重启 WorkBuddy,或联系 ScoreHub 支持。
51
- - **`status` 已明确返回 `local_state = "running_unauthorized"`**:说明本地 `mcp-server` 已启动但尚未完成 ScoreHub OAuth;此时可以继续欢迎语,并在用户真正要调用 TikTok 能力时再引导授权。
52
- - **`status` 已明确返回 `config_source.managed_by = "@scorehub/creator-outreach"` 且 `config_source.client_host = "workbuddy"`**:将其视为公开版标准安装路径,不再要求终端用户手工编辑 `mcp.json`。
53
-
54
55
  首轮使用以下完整中文欢迎语,随后等待用户选择。若已知用户姓名,在开头称呼该姓名;未知时直接以“你好”开头,绝不猜测姓名:
55
56
 
56
57
  > 你好,我是 Tiky,ScoreHub AI 的 TikTok 达人营销专家。我先帮你建立清晰准确的达人画像,评估合作价值,再对精选候选人进行小范围建联验证。我不是面向成千上万达人的海量群发工具。以下是我能帮你做的事情:
@@ -225,9 +226,7 @@ HTML 报告必须自包含,只使用内联 CSS/JavaScript,不依赖 CDN、
225
226
  4. 用户确认后:macOS 和 Windows 打开 `https://nodejs.org/en/download` 并引导安装 LTS;Linux 先识别发行版,再使用 NodeSource 的受支持安装路径。没有终端或打开链接能力时,提供同样的官方步骤并等待用户自行完成。不得使用未验证的第三方下载源。
226
227
  5. 安装完成后,重新检查三条版本命令。只有 Node.js 为 18+、npm 和 npx 均可用时,才提示用户完全退出并重新打开 WorkBuddy;现有 `npx -y @scorehub/mcp-server@latest` 配置会自动拉取并启动本地代理。Node.js 和 npm 存在但 npx 缺失时,视为不完整安装,指导修复或重装 Node.js LTS,**不得**改写 MCP 配置为其他启动命令。
227
228
 
228
- 如果宿主、版本检测或 `status` 工具已明确指出 Tiky 自身版本落后,先引导用户执行 `npx -y @scorehub/creator-outreach@latest` 升级 Tiky,再继续处理运行环境或 MCP 修复;避免让旧版智能体继续指导公开版安装流程。若宿主不支持执行本地命令,再回退为重新打开分享链接覆盖安装。
229
-
230
- 如果没有上述明确证据,直接告诉用户:“ScoreHub 服务暂时未连接,请完全退出并重新打开 WorkBuddy 后重试;如仍无法使用,请联系 ScoreHub 支持。”不要向终端用户提及 Node.js、npm、网络、环境变量、TikTok API 凭证或直连 API,也不要调用 `authorize`。
229
+ 如果 bootstrap 已返回 `ready`,但 MCP 工具仍不可见、连接关闭或本地 MCP 未启动,直接告诉用户:“ScoreHub 服务暂时未连接,请完全退出并重新打开 WorkBuddy 后重试;如仍无法使用,请联系 ScoreHub 支持。”不要调用 `authorize`,也不要把该情况自动误判为系统级 Node.js 缺失。
231
230
 
232
231
  如果本地登录状态正常,但真实工具调用提示当前店铺的 TikTok 授权已失效或异常,我会直接提醒你去 ScoreHub 重新绑定该店铺后再试,不会再让你重复浏览器登录。
233
232
 
package/install.js CHANGED
@@ -1,14 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
- const { execSync } = require('child_process');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { execFileSync } = require('node:child_process');
7
7
 
8
8
  const pkg = require('./package.json');
9
+
9
10
  const MIN_NODE_MAJOR = 18;
11
+ const STATE_SCHEMA_VERSION = 1;
12
+ const UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
10
13
  const WORKBUDDY_SHARE_URL = 'https://www.workbuddy.cn/work/launch/?sharecode=lNM8H05BRKoV-dw2gac3ZwQ-p01o3C3KVz1gXJ_CwvtX02-mSRqBB3xepE4V0gr7&expertname=Tiky+%C2%B7+TikTok%E8%BE%BE%E4%BA%BA%E8%90%A5%E9%94%80%E4%B8%93%E5%AE%B6&buddy_type=workbuddy';
11
14
  const CREATOR_OUTREACH_SELF_BOOTSTRAP_COMMAND = 'npx -y @scorehub/creator-outreach@latest';
15
+ const MCP_SERVER_SELF_CHECK_COMMAND = 'npx -y @scorehub/mcp-server@latest --self-check --json';
12
16
 
13
17
  const MCP_CONFIG = {
14
18
  type: 'stdio',
@@ -31,6 +35,7 @@ function isSupportedNode(nodeVersion = process.versions.node) {
31
35
 
32
36
  function getPaths(homeDir) {
33
37
  const workBuddyDir = path.join(homeDir, '.workbuddy');
38
+ const scorehubDir = path.join(workBuddyDir, 'scorehub');
34
39
 
35
40
  return {
36
41
  workBuddyDir,
@@ -38,133 +43,424 @@ function getPaths(homeDir) {
38
43
  workBuddyDir, 'plugins', 'marketplaces', 'my-experts', 'plugins', 'tiktok-creator-outreach',
39
44
  ),
40
45
  workBuddyMcpJson: path.join(workBuddyDir, 'mcp.json'),
46
+ bootstrapState: path.join(scorehubDir, 'bootstrap-state.json'),
47
+ bootstrapDir: scorehubDir,
41
48
  };
42
49
  }
43
50
 
44
- function resolveWorkBuddyNode(workBuddyDir) {
51
+ function resolveWorkBuddyLauncher(workBuddyDir, platform = process.platform) {
45
52
  const versionsDir = path.join(workBuddyDir, 'binaries', 'node', 'versions');
46
- const npxExecutable = process.platform === 'win32' ? 'npx.cmd' : 'npx';
47
53
  try {
48
- const versions = fs.readdirSync(versionsDir);
49
- if (versions.length > 0) {
50
- const versionDir = path.join(versionsDir, versions[0]);
54
+ const versions = fs.readdirSync(versionsDir)
55
+ .sort((left, right) => right.localeCompare(left, undefined, { numeric: true }));
56
+ for (const version of versions) {
57
+ const versionDir = path.join(versionsDir, version);
58
+ if (platform === 'win32') {
59
+ const command = path.join(versionDir, 'node.exe');
60
+ const npxCli = path.join(versionDir, 'node_modules', 'npm', 'bin', 'npx-cli.js');
61
+ if (fs.existsSync(command) && fs.existsSync(npxCli)) {
62
+ return { command, argsPrefix: [npxCli], binDir: versionDir };
63
+ }
64
+ continue;
65
+ }
66
+
51
67
  const binDirs = [path.join(versionDir, 'bin'), versionDir];
52
68
  for (const binDir of binDirs) {
53
- const npxPath = path.join(binDir, npxExecutable);
54
- if (fs.existsSync(npxPath)) return { npx: npxPath, binDir };
69
+ const command = path.join(binDir, 'npx');
70
+ if (fs.existsSync(command)) return { command, argsPrefix: [], binDir };
55
71
  }
56
72
  }
57
73
  } catch {
58
74
  // WorkBuddy or its bundled Node.js is not available at this path.
59
75
  }
60
- return { npx: 'npx', binDir: null };
76
+ if (platform === 'win32') return null;
77
+ return { command: 'npx', argsPrefix: [], binDir: null };
61
78
  }
62
79
 
63
- function fetchLatestMcpServerVersion() {
80
+ function readJson(filePath) {
81
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
82
+ }
83
+
84
+ function writeJsonAtomic(filePath, value) {
85
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
86
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
87
+ fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
64
88
  try {
65
- const out = execSync('npm view @scorehub/mcp-server version', {
66
- encoding: 'utf8',
67
- stdio: ['ignore', 'pipe', 'ignore'],
68
- timeout: 10000,
89
+ fs.renameSync(tempPath, filePath);
90
+ } catch (error) {
91
+ try { fs.rmSync(tempPath, { force: true }); } catch { /* preserve the original error */ }
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ function snapshotFile(filePath) {
97
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath) : null;
98
+ }
99
+
100
+ function restoreFile(filePath, snapshot) {
101
+ if (snapshot === null) {
102
+ fs.rmSync(filePath, { force: true });
103
+ return;
104
+ }
105
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
106
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.restore`;
107
+ fs.writeFileSync(tempPath, snapshot);
108
+ fs.renameSync(tempPath, filePath);
109
+ }
110
+
111
+ function defaultState() {
112
+ return {
113
+ schema_version: STATE_SCHEMA_VERSION,
114
+ consent_granted: false,
115
+ status: 'uninitialized',
116
+ creator_outreach_version: null,
117
+ observed_mcp_server_version: null,
118
+ last_check_at: null,
119
+ last_error: null,
120
+ restart_required: false,
121
+ };
122
+ }
123
+
124
+ function loadState(statePath) {
125
+ if (!fs.existsSync(statePath)) return defaultState();
126
+ try {
127
+ const state = readJson(statePath);
128
+ if (state.schema_version !== STATE_SCHEMA_VERSION) throw new Error('unsupported state schema');
129
+ return Object.assign(defaultState(), state);
130
+ } catch (error) {
131
+ const reason = error instanceof Error ? error.message : String(error);
132
+ return Object.assign(defaultState(), {
133
+ status: 'repair_required',
134
+ last_error: `bootstrap state is invalid: ${reason}`,
69
135
  });
70
- return out.trim() || null;
136
+ }
137
+ }
138
+
139
+ function readInstalledPluginVersion(pluginDir) {
140
+ const packagePath = path.join(pluginDir, 'package.json');
141
+ try {
142
+ const installed = readJson(packagePath);
143
+ return typeof installed.version === 'string' ? installed.version : null;
71
144
  } catch {
72
145
  return null;
73
146
  }
74
147
  }
75
148
 
76
- function mergeMcpConfig(configPath, serverName, serverConfig, extraFields) {
77
- let config = {};
78
- if (fs.existsSync(configPath)) {
79
- try {
80
- config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
81
- } catch {
82
- console.warn(`Warning: ${configPath} could not be parsed and will be replaced.`);
83
- }
149
+ function buildMcpEnvironment(version, binDir) {
150
+ const existingPath = process.env.PATH || process.env.Path || '';
151
+ return Object.assign({}, MCP_CONFIG.env, {
152
+ SCOREHUB_CREATOR_OUTREACH_VERSION: version,
153
+ ...(binDir ? { PATH: [binDir, existingPath].filter(Boolean).join(path.delimiter) } : {}),
154
+ });
155
+ }
156
+
157
+ function buildMcpConfig(version, launcher) {
158
+ return {
159
+ command: launcher.command,
160
+ args: launcher.argsPrefix.concat(MCP_CONFIG.args),
161
+ env: buildMcpEnvironment(version, launcher.binDir),
162
+ };
163
+ }
164
+
165
+ function readMcpConfig(configPath) {
166
+ if (!fs.existsSync(configPath)) return { exists: false, config: {} };
167
+ try {
168
+ return { exists: true, config: readJson(configPath) };
169
+ } catch (error) {
170
+ const reason = error instanceof Error ? error.message : String(error);
171
+ return { exists: true, config: null, error: `mcp.json is invalid: ${reason}` };
84
172
  }
173
+ }
174
+
175
+ function isManagedMcpConfig(config) {
176
+ const server = config && config.mcpServers && config.mcpServers.scorehub;
177
+ return !!server
178
+ && Array.isArray(server.args)
179
+ && server.args.includes('@scorehub/mcp-server@latest')
180
+ && server.env
181
+ && server.env.SCOREHUB_MANAGED_BY === '@scorehub/creator-outreach'
182
+ && server.env.SCOREHUB_CLIENT_HOST === 'workbuddy';
183
+ }
184
+
185
+ function needsMcpLauncherMigration(config, platform = process.platform, expectedLauncher) {
186
+ if (platform !== 'win32' || !isManagedMcpConfig(config)) return false;
187
+ const server = config.mcpServers.scorehub;
188
+ const npxCli = server.args.find((arg) => /(?:^|[\\/])npx-cli\.js$/i.test(arg));
189
+ if (!/(?:^|[\\/])node\.exe$/i.test(server.command || '') || !npxCli) return true;
190
+ if (expectedLauncher === undefined) return false;
191
+ if (expectedLauncher === null) return true;
192
+ return server.command.toLowerCase() !== expectedLauncher.command.toLowerCase()
193
+ || npxCli.toLowerCase() !== expectedLauncher.argsPrefix[0].toLowerCase();
194
+ }
85
195
 
196
+ function mergeMcpConfig(configPath, serverConfig) {
197
+ const current = readMcpConfig(configPath);
198
+ if (current.config === null) throw new Error(current.error);
199
+ const config = current.config || {};
86
200
  config.mcpServers = config.mcpServers || {};
87
- config.mcpServers[serverName] = Object.assign({}, serverConfig, extraFields);
88
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
201
+ config.mcpServers.scorehub = serverConfig;
202
+ writeJsonAtomic(configPath, config);
89
203
  }
90
204
 
91
- function install({
92
- homeDir = os.homedir(),
93
- nodeVersion = process.versions.node,
94
- getLatestMcpServerVersion = fetchLatestMcpServerVersion,
95
- } = {}) {
96
- console.log(`${pkg.name} v${pkg.version}\n`);
205
+ function copyPluginToStage(stageDir) {
206
+ const pluginDirs = ['.codebuddy-plugin', 'agents', 'skills', 'avatars'];
207
+ const pluginFiles = ['package.json', 'README.md', 'README.en.md', 'install.js'];
208
+ fs.mkdirSync(stageDir, { recursive: true });
209
+ for (const dir of pluginDirs) {
210
+ const source = path.join(__dirname, dir);
211
+ if (fs.existsSync(source)) fs.cpSync(source, path.join(stageDir, dir), { recursive: true });
212
+ }
213
+ for (const file of pluginFiles) {
214
+ const source = path.join(__dirname, file);
215
+ if (fs.existsSync(source)) fs.copyFileSync(source, path.join(stageDir, file));
216
+ }
217
+ }
97
218
 
98
- if (!isSupportedNode(nodeVersion)) {
99
- console.error(`Node.js ${MIN_NODE_MAJOR}+ is required. Detected ${nodeVersion}. No configuration files were changed.`);
100
- return 1;
219
+ function replacePluginAtomic(pluginDir) {
220
+ const parentDir = path.dirname(pluginDir);
221
+ const stageDir = path.join(parentDir, `.tiktok-creator-outreach.stage-${process.pid}-${Date.now()}`);
222
+ const backupDir = path.join(parentDir, `.tiktok-creator-outreach.backup-${process.pid}-${Date.now()}`);
223
+ copyPluginToStage(stageDir);
224
+ try {
225
+ if (fs.existsSync(pluginDir)) fs.renameSync(pluginDir, backupDir);
226
+ fs.renameSync(stageDir, pluginDir);
227
+ return backupDir;
228
+ } catch (error) {
229
+ if (fs.existsSync(pluginDir) && fs.existsSync(backupDir)) fs.rmSync(pluginDir, { recursive: true, force: true });
230
+ if (fs.existsSync(backupDir) && !fs.existsSync(pluginDir)) fs.renameSync(backupDir, pluginDir);
231
+ if (fs.existsSync(stageDir)) fs.rmSync(stageDir, { recursive: true, force: true });
232
+ throw error;
101
233
  }
234
+ }
102
235
 
103
- const paths = getPaths(homeDir);
104
- const hasWorkBuddy = fs.existsSync(paths.workBuddyDir);
236
+ function restorePlugin(pluginDir, backupDir) {
237
+ if (fs.existsSync(pluginDir)) fs.rmSync(pluginDir, { recursive: true, force: true });
238
+ if (backupDir && fs.existsSync(backupDir)) fs.renameSync(backupDir, pluginDir);
239
+ }
105
240
 
106
- if (!hasWorkBuddy) {
107
- console.error('No supported client was found. Launch WorkBuddy once, then run this command again. No configuration files were changed.');
108
- return 1;
241
+ function parseSelfCheckOutput(output) {
242
+ const lines = String(output).trim().split(/\r?\n/).reverse();
243
+ for (const line of lines) {
244
+ try {
245
+ const parsed = JSON.parse(line);
246
+ if (parsed && parsed.schema_version === 1 && typeof parsed.ok === 'boolean') return parsed;
247
+ } catch { /* npx may print non-JSON diagnostics before the payload */ }
109
248
  }
249
+ throw new Error('mcp-server self-check did not return a valid JSON payload');
250
+ }
110
251
 
111
- const pluginDirs = ['.codebuddy-plugin', 'agents', 'skills', 'avatars'];
112
- const pluginFiles = ['package.json', 'README.md', 'README.en.md'];
252
+ function runMcpSelfCheck({ launcher, env, selfCheck = defaultMcpSelfCheck } = {}) {
253
+ return selfCheck({ launcher, env });
254
+ }
113
255
 
114
- fs.mkdirSync(paths.workBuddyPluginDir, { recursive: true });
115
- for (const dir of pluginDirs) {
116
- const source = path.join(__dirname, dir);
117
- const destination = path.join(paths.workBuddyPluginDir, dir);
118
- if (fs.existsSync(source)) {
119
- fs.cpSync(source, destination, { recursive: true });
256
+ function defaultMcpSelfCheck({ launcher, env, execFile = execFileSync }) {
257
+ try {
258
+ const output = execFile(
259
+ launcher.command,
260
+ launcher.argsPrefix.concat(['-y', '@scorehub/mcp-server@latest', '--self-check', '--json']),
261
+ {
262
+ encoding: 'utf8',
263
+ env,
264
+ stdio: ['ignore', 'pipe', 'pipe'],
265
+ timeout: 30000,
266
+ },
267
+ );
268
+ const payload = parseSelfCheckOutput(output);
269
+ if (!payload.ok) throw new Error(`mcp-server self-check failed for Node.js ${payload.node_version}`);
270
+ return payload;
271
+ } catch (error) {
272
+ if (error && typeof error === 'object' && 'stderr' in error && error.stderr) {
273
+ throw new Error(String(error.stderr).trim());
120
274
  }
275
+ throw error;
121
276
  }
277
+ }
122
278
 
123
- for (const file of pluginFiles) {
124
- const source = path.join(__dirname, file);
125
- const destination = path.join(paths.workBuddyPluginDir, file);
126
- if (fs.existsSync(source)) {
127
- fs.copyFileSync(source, destination);
128
- }
279
+ function detectStatus({ paths, state, nodeVersion }) {
280
+ if (!isSupportedNode(nodeVersion)) {
281
+ return { status: 'repair_required', reason: `Node.js ${MIN_NODE_MAJOR}+ is required. Detected ${nodeVersion}.` };
129
282
  }
283
+ if (!fs.existsSync(paths.workBuddyDir)) {
284
+ return { status: 'repair_required', reason: 'WorkBuddy has not been started on this machine.' };
285
+ }
286
+ const mcp = readMcpConfig(paths.workBuddyMcpJson);
287
+ if (mcp.config === null) return { status: 'repair_required', reason: mcp.error };
288
+ if (state.status === 'repair_required') return { status: 'repair_required', reason: state.last_error || 'Bootstrap requires repair.' };
289
+ if (!state.consent_granted || !isManagedMcpConfig(mcp.config)) {
290
+ return { status: 'uninitialized', reason: 'WorkBuddy MCP bootstrap has not been confirmed.' };
291
+ }
292
+ if (state.restart_required) return { status: 'restart_required', reason: 'WorkBuddy must be restarted to load the updated plugin and MCP configuration.' };
293
+ return { status: 'ready', reason: null };
294
+ }
130
295
 
131
- const { npx, binDir } = resolveWorkBuddyNode(paths.workBuddyDir);
132
- const { type: _, ...baseMcpConfig } = MCP_CONFIG;
133
- const existingPath = process.env.PATH || process.env.Path || '';
134
- const env = binDir
135
- ? Object.assign({}, MCP_CONFIG.env, {
136
- SCOREHUB_CREATOR_OUTREACH_VERSION: pkg.version,
137
- PATH: [binDir, existingPath].filter(Boolean).join(path.delimiter),
138
- })
139
- : Object.assign({}, MCP_CONFIG.env, {
140
- SCOREHUB_CREATOR_OUTREACH_VERSION: pkg.version,
141
- });
142
- const workBuddyMcpConfig = Object.assign({}, baseMcpConfig, { command: npx, env });
143
- mergeMcpConfig(paths.workBuddyMcpJson, 'scorehub', workBuddyMcpConfig, { disabled: false });
144
- console.log(`WorkBuddy: Plugin installed at ${paths.workBuddyPluginDir}`);
145
- console.log(`WorkBuddy: MCP server configured at ${paths.workBuddyMcpJson}`);
296
+ function check({
297
+ homeDir = os.homedir(),
298
+ nodeVersion = process.versions.node,
299
+ now = Date.now(),
300
+ platform = process.platform,
301
+ } = {}) {
302
+ const paths = getPaths(homeDir);
303
+ const state = loadState(paths.bootstrapState);
304
+ const detected = detectStatus({ paths, state, nodeVersion });
305
+ const lastCheckAt = state.last_check_at ? Date.parse(state.last_check_at) : Number.NaN;
306
+ const mcp = readMcpConfig(paths.workBuddyMcpJson);
307
+ const expectedLauncher = platform === 'win32'
308
+ ? resolveWorkBuddyLauncher(paths.workBuddyDir, platform)
309
+ : undefined;
310
+ const launcherMigrationDue = mcp.config !== null
311
+ && needsMcpLauncherMigration(mcp.config, platform, expectedLauncher);
312
+ const updateDue = detected.status === 'ready'
313
+ && (launcherMigrationDue || !Number.isFinite(lastCheckAt) || now - lastCheckAt >= UPDATE_INTERVAL_MS);
314
+ return {
315
+ schema_version: STATE_SCHEMA_VERSION,
316
+ status: detected.status,
317
+ reason: detected.reason,
318
+ creator_outreach_version: pkg.version,
319
+ consent_granted: state.consent_granted,
320
+ mcp_configured: detected.status === 'ready' || detected.status === 'restart_required',
321
+ restart_required: state.restart_required,
322
+ update_due: updateDue,
323
+ };
324
+ }
146
325
 
147
- const mcpServerVersion = getLatestMcpServerVersion();
148
- if (mcpServerVersion) {
149
- console.log(`\nmcp-server latest version: v${mcpServerVersion} (npx resolves @latest again when it starts).`);
150
- } else {
151
- console.log('\nmcp-server: @latest (the current version could not be queried).');
326
+ function assertInstallable({ homeDir, nodeVersion, platform }) {
327
+ const paths = getPaths(homeDir);
328
+ if (!isSupportedNode(nodeVersion)) throw new Error(`Node.js ${MIN_NODE_MAJOR}+ is required. Detected ${nodeVersion}.`);
329
+ if (!fs.existsSync(paths.workBuddyDir)) throw new Error('No supported client was found. Launch WorkBuddy once, then retry.');
330
+ const launcher = resolveWorkBuddyLauncher(paths.workBuddyDir, platform);
331
+ if (!launcher) {
332
+ throw new Error('WorkBuddy managed Node.js/npm runtime is incomplete: expected node.exe and node_modules/npm/bin/npx-cli.js in the same version directory.');
152
333
  }
334
+ return { paths, launcher };
335
+ }
336
+
337
+ function install({
338
+ homeDir = os.homedir(),
339
+ nodeVersion = process.versions.node,
340
+ selfCheck = defaultMcpSelfCheck,
341
+ markConsent = true,
342
+ replacePlugin = replacePluginAtomic,
343
+ platform = process.platform,
344
+ } = {}) {
345
+ const { paths, launcher } = assertInstallable({ homeDir, nodeVersion, platform });
346
+ const env = buildMcpEnvironment(pkg.version, launcher.binDir);
347
+ const mcpCheck = runMcpSelfCheck({ launcher, env, selfCheck });
348
+ const mcpSnapshot = snapshotFile(paths.workBuddyMcpJson);
349
+ const stateSnapshot = snapshotFile(paths.bootstrapState);
350
+ let pluginBackup = null;
351
+ let pluginReplaced = false;
352
+ try {
353
+ mergeMcpConfig(paths.workBuddyMcpJson, buildMcpConfig(pkg.version, launcher));
354
+ pluginBackup = replacePlugin(paths.workBuddyPluginDir);
355
+ pluginReplaced = true;
356
+
357
+ const state = loadState(paths.bootstrapState);
358
+ state.consent_granted = markConsent || state.consent_granted;
359
+ state.status = 'restart_required';
360
+ state.creator_outreach_version = pkg.version;
361
+ state.observed_mcp_server_version = mcpCheck.server_version || null;
362
+ state.last_check_at = new Date().toISOString();
363
+ state.last_error = null;
364
+ state.restart_required = true;
365
+ writeJsonAtomic(paths.bootstrapState, state);
366
+ } catch (error) {
367
+ if (pluginReplaced) restorePlugin(paths.workBuddyPluginDir, pluginBackup);
368
+ restoreFile(paths.workBuddyMcpJson, mcpSnapshot);
369
+ restoreFile(paths.bootstrapState, stateSnapshot);
370
+ throw error;
371
+ }
372
+ if (pluginBackup && fs.existsSync(pluginBackup)) fs.rmSync(pluginBackup, { recursive: true, force: true });
373
+ return { status: 'restart_required', mcp_server_version: mcpCheck.server_version || null };
374
+ }
375
+
376
+ function markReady({ homeDir = os.homedir(), nodeVersion = process.versions.node } = {}) {
377
+ const paths = getPaths(homeDir);
378
+ const state = loadState(paths.bootstrapState);
379
+ const detected = detectStatus({ paths, state: Object.assign({}, state, { restart_required: false }), nodeVersion });
380
+ if (detected.status !== 'ready') throw new Error(detected.reason || `Cannot mark bootstrap as ready: ${detected.status}`);
381
+ state.status = 'ready';
382
+ state.restart_required = false;
383
+ state.last_error = null;
384
+ writeJsonAtomic(paths.bootstrapState, state);
385
+ return { status: 'ready' };
386
+ }
153
387
 
154
- console.log('\nInstallation complete. Restart WorkBuddy and use Tiky.');
155
- return 0;
388
+ function update({
389
+ homeDir = os.homedir(),
390
+ nodeVersion = process.versions.node,
391
+ selfCheck = defaultMcpSelfCheck,
392
+ platform = process.platform,
393
+ } = {}) {
394
+ const paths = getPaths(homeDir);
395
+ const state = loadState(paths.bootstrapState);
396
+ if (!state.consent_granted) return { status: 'uninitialized', reason: 'Initial installation confirmation is required.' };
397
+ const detected = detectStatus({ paths, state: Object.assign({}, state, { restart_required: false }), nodeVersion });
398
+ if (detected.status === 'repair_required' || detected.status === 'uninitialized') {
399
+ return { status: detected.status, reason: detected.reason };
400
+ }
401
+ if (state.restart_required) return { status: 'restart_required', reason: 'WorkBuddy must be restarted to load the updated plugin and MCP configuration.' };
402
+ const mcp = readMcpConfig(paths.workBuddyMcpJson);
403
+ const expectedLauncher = platform === 'win32'
404
+ ? resolveWorkBuddyLauncher(paths.workBuddyDir, platform)
405
+ : undefined;
406
+ const launcherMigrationDue = mcp.config !== null
407
+ && needsMcpLauncherMigration(mcp.config, platform, expectedLauncher);
408
+ if (readInstalledPluginVersion(paths.workBuddyPluginDir) === pkg.version && !launcherMigrationDue) {
409
+ state.status = 'ready';
410
+ state.creator_outreach_version = pkg.version;
411
+ state.last_check_at = new Date().toISOString();
412
+ state.last_error = null;
413
+ writeJsonAtomic(paths.bootstrapState, state);
414
+ return { status: 'ready', creator_outreach_version: pkg.version };
415
+ }
416
+ return install({ homeDir, nodeVersion, selfCheck, markConsent: false, platform });
156
417
  }
157
418
 
158
- if (require.main === module) {
159
- process.exitCode = install();
419
+ function printJson(value) {
420
+ process.stdout.write(`${JSON.stringify(value)}\n`);
160
421
  }
161
422
 
423
+ function runCli(argv = process.argv.slice(2)) {
424
+ const action = argv[0];
425
+ const options = {
426
+ homeDir: process.env.SCOREHUB_BOOTSTRAP_HOME || os.homedir(),
427
+ nodeVersion: process.versions.node,
428
+ };
429
+ try {
430
+ let result;
431
+ if (action === 'bootstrap' && argv.includes('--check')) result = check(options);
432
+ else if (action === 'bootstrap' && argv.includes('--install')) result = install(options);
433
+ else if (action === 'bootstrap' && argv.includes('--update')) result = update(options);
434
+ else if (action === 'bootstrap' && argv.includes('--mark-ready')) result = markReady(options);
435
+ else result = install(options);
436
+ printJson(result);
437
+ return result.status === 'repair_required' ? 1 : 0;
438
+ } catch (error) {
439
+ const reason = error instanceof Error ? error.message : String(error);
440
+ printJson({ schema_version: STATE_SCHEMA_VERSION, status: 'repair_required', reason });
441
+ return 1;
442
+ }
443
+ }
444
+
445
+ if (require.main === module) process.exitCode = runCli();
446
+
162
447
  module.exports = {
163
448
  CREATOR_OUTREACH_SELF_BOOTSTRAP_COMMAND,
449
+ MCP_SERVER_SELF_CHECK_COMMAND,
164
450
  MIN_NODE_MAJOR,
451
+ UPDATE_INTERVAL_MS,
165
452
  WORKBUDDY_SHARE_URL,
453
+ check,
454
+ defaultMcpSelfCheck,
455
+ detectStatus,
166
456
  getNodeMajorVersion,
167
457
  getPaths,
168
458
  install,
459
+ isManagedMcpConfig,
169
460
  isSupportedNode,
461
+ markReady,
462
+ needsMcpLauncherMigration,
463
+ resolveWorkBuddyLauncher,
464
+ runCli,
465
+ update,
170
466
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scorehub/creator-outreach",
3
- "version": "1.1.11",
3
+ "version": "1.2.1",
4
4
  "description": "ScoreHub AI TikTok达人营销专家 Tiky — Agent 定义 + Skills 配置",
5
5
  "engines": {
6
6
  "node": ">=18"