@scorehub/creator-outreach 1.1.11 → 1.2.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/.codebuddy-plugin/plugin.json +1 -1
- package/README.en.md +5 -9
- package/README.md +5 -9
- package/agents/tiktok-creator-outreach.md +12 -13
- package/install.js +320 -75
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tiktok-creator-outreach",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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
|
-
|
|
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
|
-
-
|
|
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 command always remains `npx -y @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
|
-
|
|
44
|
+
Before writing the configuration, Tiky verifies the MCP server with `--self-check --json`. WorkBuddy then starts it through `npx -y @scorehub/mcp-server@latest`. If WorkBuddy 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.
|
|
49
45
|
|
|
50
46
|
## First Use and Authorization
|
|
51
47
|
|
|
52
|
-
At the start of every new conversation, Tiky first
|
|
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
|
|
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 配置始终使用 `npx -y @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
|
|
44
|
+
MCP Server(`@scorehub/mcp-server`)在写入配置前会先通过 `--self-check --json` 验证可执行性,之后由 `npx -y @scorehub/mcp-server@latest` 在 WorkBuddy 启动时自动拉起。WorkBuddy 若已自带可用 Node.js / npx,会优先复用;只有在存在明确缺失证据时,Tiky 才会引导你安装官方 Node.js LTS。
|
|
49
45
|
|
|
50
46
|
## 首次使用与授权
|
|
51
47
|
|
|
52
|
-
每个新对话的第一轮,Tiky
|
|
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
|
|
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
|
-
|
|
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
|
|
5
|
-
const
|
|
6
|
-
const {
|
|
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,6 +43,8 @@ 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
|
|
|
@@ -45,9 +52,9 @@ function resolveWorkBuddyNode(workBuddyDir) {
|
|
|
45
52
|
const versionsDir = path.join(workBuddyDir, 'binaries', 'node', 'versions');
|
|
46
53
|
const npxExecutable = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
47
54
|
try {
|
|
48
|
-
const versions = fs.readdirSync(versionsDir);
|
|
49
|
-
|
|
50
|
-
const versionDir = path.join(versionsDir,
|
|
55
|
+
const versions = fs.readdirSync(versionsDir).sort().reverse();
|
|
56
|
+
for (const version of versions) {
|
|
57
|
+
const versionDir = path.join(versionsDir, version);
|
|
51
58
|
const binDirs = [path.join(versionDir, 'bin'), versionDir];
|
|
52
59
|
for (const binDir of binDirs) {
|
|
53
60
|
const npxPath = path.join(binDir, npxExecutable);
|
|
@@ -60,111 +67,349 @@ function resolveWorkBuddyNode(workBuddyDir) {
|
|
|
60
67
|
return { npx: 'npx', binDir: null };
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
function
|
|
70
|
+
function readJson(filePath) {
|
|
71
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writeJsonAtomic(filePath, value) {
|
|
75
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
76
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
77
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
64
78
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
fs.renameSync(tempPath, filePath);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
try { fs.rmSync(tempPath, { force: true }); } catch { /* preserve the original error */ }
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function snapshotFile(filePath) {
|
|
87
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath) : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function restoreFile(filePath, snapshot) {
|
|
91
|
+
if (snapshot === null) {
|
|
92
|
+
fs.rmSync(filePath, { force: true });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
96
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.restore`;
|
|
97
|
+
fs.writeFileSync(tempPath, snapshot);
|
|
98
|
+
fs.renameSync(tempPath, filePath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function defaultState() {
|
|
102
|
+
return {
|
|
103
|
+
schema_version: STATE_SCHEMA_VERSION,
|
|
104
|
+
consent_granted: false,
|
|
105
|
+
status: 'uninitialized',
|
|
106
|
+
creator_outreach_version: null,
|
|
107
|
+
observed_mcp_server_version: null,
|
|
108
|
+
last_check_at: null,
|
|
109
|
+
last_error: null,
|
|
110
|
+
restart_required: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function loadState(statePath) {
|
|
115
|
+
if (!fs.existsSync(statePath)) return defaultState();
|
|
116
|
+
try {
|
|
117
|
+
const state = readJson(statePath);
|
|
118
|
+
if (state.schema_version !== STATE_SCHEMA_VERSION) throw new Error('unsupported state schema');
|
|
119
|
+
return Object.assign(defaultState(), state);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
122
|
+
return Object.assign(defaultState(), {
|
|
123
|
+
status: 'repair_required',
|
|
124
|
+
last_error: `bootstrap state is invalid: ${reason}`,
|
|
69
125
|
});
|
|
70
|
-
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readInstalledPluginVersion(pluginDir) {
|
|
130
|
+
const packagePath = path.join(pluginDir, 'package.json');
|
|
131
|
+
try {
|
|
132
|
+
const installed = readJson(packagePath);
|
|
133
|
+
return typeof installed.version === 'string' ? installed.version : null;
|
|
71
134
|
} catch {
|
|
72
135
|
return null;
|
|
73
136
|
}
|
|
74
137
|
}
|
|
75
138
|
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
139
|
+
function buildMcpEnvironment(version, binDir) {
|
|
140
|
+
const existingPath = process.env.PATH || process.env.Path || '';
|
|
141
|
+
return Object.assign({}, MCP_CONFIG.env, {
|
|
142
|
+
SCOREHUB_CREATOR_OUTREACH_VERSION: version,
|
|
143
|
+
...(binDir ? { PATH: [binDir, existingPath].filter(Boolean).join(path.delimiter) } : {}),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildMcpConfig(version, npx, binDir) {
|
|
148
|
+
return {
|
|
149
|
+
command: npx,
|
|
150
|
+
args: MCP_CONFIG.args.slice(),
|
|
151
|
+
env: buildMcpEnvironment(version, binDir),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readMcpConfig(configPath) {
|
|
156
|
+
if (!fs.existsSync(configPath)) return { exists: false, config: {} };
|
|
157
|
+
try {
|
|
158
|
+
return { exists: true, config: readJson(configPath) };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
161
|
+
return { exists: true, config: null, error: `mcp.json is invalid: ${reason}` };
|
|
84
162
|
}
|
|
163
|
+
}
|
|
85
164
|
|
|
165
|
+
function isManagedMcpConfig(config) {
|
|
166
|
+
const server = config && config.mcpServers && config.mcpServers.scorehub;
|
|
167
|
+
return !!server
|
|
168
|
+
&& Array.isArray(server.args)
|
|
169
|
+
&& server.args.includes('@scorehub/mcp-server@latest')
|
|
170
|
+
&& server.env
|
|
171
|
+
&& server.env.SCOREHUB_MANAGED_BY === '@scorehub/creator-outreach'
|
|
172
|
+
&& server.env.SCOREHUB_CLIENT_HOST === 'workbuddy';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function mergeMcpConfig(configPath, serverConfig) {
|
|
176
|
+
const current = readMcpConfig(configPath);
|
|
177
|
+
if (current.config === null) throw new Error(current.error);
|
|
178
|
+
const config = current.config || {};
|
|
86
179
|
config.mcpServers = config.mcpServers || {};
|
|
87
|
-
config.mcpServers
|
|
88
|
-
|
|
180
|
+
config.mcpServers.scorehub = serverConfig;
|
|
181
|
+
writeJsonAtomic(configPath, config);
|
|
89
182
|
}
|
|
90
183
|
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
184
|
+
function copyPluginToStage(stageDir) {
|
|
185
|
+
const pluginDirs = ['.codebuddy-plugin', 'agents', 'skills', 'avatars'];
|
|
186
|
+
const pluginFiles = ['package.json', 'README.md', 'README.en.md', 'install.js'];
|
|
187
|
+
fs.mkdirSync(stageDir, { recursive: true });
|
|
188
|
+
for (const dir of pluginDirs) {
|
|
189
|
+
const source = path.join(__dirname, dir);
|
|
190
|
+
if (fs.existsSync(source)) fs.cpSync(source, path.join(stageDir, dir), { recursive: true });
|
|
191
|
+
}
|
|
192
|
+
for (const file of pluginFiles) {
|
|
193
|
+
const source = path.join(__dirname, file);
|
|
194
|
+
if (fs.existsSync(source)) fs.copyFileSync(source, path.join(stageDir, file));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
97
197
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
198
|
+
function replacePluginAtomic(pluginDir) {
|
|
199
|
+
const parentDir = path.dirname(pluginDir);
|
|
200
|
+
const stageDir = path.join(parentDir, `.tiktok-creator-outreach.stage-${process.pid}-${Date.now()}`);
|
|
201
|
+
const backupDir = path.join(parentDir, `.tiktok-creator-outreach.backup-${process.pid}-${Date.now()}`);
|
|
202
|
+
copyPluginToStage(stageDir);
|
|
203
|
+
try {
|
|
204
|
+
if (fs.existsSync(pluginDir)) fs.renameSync(pluginDir, backupDir);
|
|
205
|
+
fs.renameSync(stageDir, pluginDir);
|
|
206
|
+
return backupDir;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (fs.existsSync(pluginDir) && fs.existsSync(backupDir)) fs.rmSync(pluginDir, { recursive: true, force: true });
|
|
209
|
+
if (fs.existsSync(backupDir) && !fs.existsSync(pluginDir)) fs.renameSync(backupDir, pluginDir);
|
|
210
|
+
if (fs.existsSync(stageDir)) fs.rmSync(stageDir, { recursive: true, force: true });
|
|
211
|
+
throw error;
|
|
101
212
|
}
|
|
213
|
+
}
|
|
102
214
|
|
|
103
|
-
|
|
104
|
-
|
|
215
|
+
function restorePlugin(pluginDir, backupDir) {
|
|
216
|
+
if (fs.existsSync(pluginDir)) fs.rmSync(pluginDir, { recursive: true, force: true });
|
|
217
|
+
if (backupDir && fs.existsSync(backupDir)) fs.renameSync(backupDir, pluginDir);
|
|
218
|
+
}
|
|
105
219
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
220
|
+
function parseSelfCheckOutput(output) {
|
|
221
|
+
const lines = String(output).trim().split(/\r?\n/).reverse();
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
try {
|
|
224
|
+
const parsed = JSON.parse(line);
|
|
225
|
+
if (parsed && parsed.schema_version === 1 && typeof parsed.ok === 'boolean') return parsed;
|
|
226
|
+
} catch { /* npx may print non-JSON diagnostics before the payload */ }
|
|
109
227
|
}
|
|
228
|
+
throw new Error('mcp-server self-check did not return a valid JSON payload');
|
|
229
|
+
}
|
|
110
230
|
|
|
111
|
-
|
|
112
|
-
|
|
231
|
+
function runMcpSelfCheck({ npx, env, selfCheck = defaultMcpSelfCheck } = {}) {
|
|
232
|
+
return selfCheck({ npx, env });
|
|
233
|
+
}
|
|
113
234
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
235
|
+
function defaultMcpSelfCheck({ npx, env }) {
|
|
236
|
+
try {
|
|
237
|
+
const output = execFileSync(npx, ['-y', '@scorehub/mcp-server@latest', '--self-check', '--json'], {
|
|
238
|
+
encoding: 'utf8',
|
|
239
|
+
env,
|
|
240
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
241
|
+
timeout: 30000,
|
|
242
|
+
});
|
|
243
|
+
const payload = parseSelfCheckOutput(output);
|
|
244
|
+
if (!payload.ok) throw new Error(`mcp-server self-check failed for Node.js ${payload.node_version}`);
|
|
245
|
+
return payload;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error && typeof error === 'object' && 'stderr' in error && error.stderr) {
|
|
248
|
+
throw new Error(String(error.stderr).trim());
|
|
120
249
|
}
|
|
250
|
+
throw error;
|
|
121
251
|
}
|
|
252
|
+
}
|
|
122
253
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
254
|
+
function detectStatus({ paths, state, nodeVersion }) {
|
|
255
|
+
if (!isSupportedNode(nodeVersion)) {
|
|
256
|
+
return { status: 'repair_required', reason: `Node.js ${MIN_NODE_MAJOR}+ is required. Detected ${nodeVersion}.` };
|
|
257
|
+
}
|
|
258
|
+
if (!fs.existsSync(paths.workBuddyDir)) {
|
|
259
|
+
return { status: 'repair_required', reason: 'WorkBuddy has not been started on this machine.' };
|
|
129
260
|
}
|
|
261
|
+
const mcp = readMcpConfig(paths.workBuddyMcpJson);
|
|
262
|
+
if (mcp.config === null) return { status: 'repair_required', reason: mcp.error };
|
|
263
|
+
if (state.status === 'repair_required') return { status: 'repair_required', reason: state.last_error || 'Bootstrap requires repair.' };
|
|
264
|
+
if (!state.consent_granted || !isManagedMcpConfig(mcp.config)) {
|
|
265
|
+
return { status: 'uninitialized', reason: 'WorkBuddy MCP bootstrap has not been confirmed.' };
|
|
266
|
+
}
|
|
267
|
+
if (state.restart_required) return { status: 'restart_required', reason: 'WorkBuddy must be restarted to load the updated plugin and MCP configuration.' };
|
|
268
|
+
return { status: 'ready', reason: null };
|
|
269
|
+
}
|
|
130
270
|
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
271
|
+
function check({ homeDir = os.homedir(), nodeVersion = process.versions.node, now = Date.now() } = {}) {
|
|
272
|
+
const paths = getPaths(homeDir);
|
|
273
|
+
const state = loadState(paths.bootstrapState);
|
|
274
|
+
const detected = detectStatus({ paths, state, nodeVersion });
|
|
275
|
+
const lastCheckAt = state.last_check_at ? Date.parse(state.last_check_at) : Number.NaN;
|
|
276
|
+
const updateDue = detected.status === 'ready'
|
|
277
|
+
&& (!Number.isFinite(lastCheckAt) || now - lastCheckAt >= UPDATE_INTERVAL_MS);
|
|
278
|
+
return {
|
|
279
|
+
schema_version: STATE_SCHEMA_VERSION,
|
|
280
|
+
status: detected.status,
|
|
281
|
+
reason: detected.reason,
|
|
282
|
+
creator_outreach_version: pkg.version,
|
|
283
|
+
consent_granted: state.consent_granted,
|
|
284
|
+
mcp_configured: detected.status === 'ready' || detected.status === 'restart_required',
|
|
285
|
+
restart_required: state.restart_required,
|
|
286
|
+
update_due: updateDue,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
146
289
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
290
|
+
function assertInstallable({ homeDir, nodeVersion }) {
|
|
291
|
+
const paths = getPaths(homeDir);
|
|
292
|
+
if (!isSupportedNode(nodeVersion)) throw new Error(`Node.js ${MIN_NODE_MAJOR}+ is required. Detected ${nodeVersion}.`);
|
|
293
|
+
if (!fs.existsSync(paths.workBuddyDir)) throw new Error('No supported client was found. Launch WorkBuddy once, then retry.');
|
|
294
|
+
return { paths, node: resolveWorkBuddyNode(paths.workBuddyDir) };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function install({
|
|
298
|
+
homeDir = os.homedir(),
|
|
299
|
+
nodeVersion = process.versions.node,
|
|
300
|
+
selfCheck = defaultMcpSelfCheck,
|
|
301
|
+
markConsent = true,
|
|
302
|
+
replacePlugin = replacePluginAtomic,
|
|
303
|
+
} = {}) {
|
|
304
|
+
const { paths, node } = assertInstallable({ homeDir, nodeVersion });
|
|
305
|
+
const env = buildMcpEnvironment(pkg.version, node.binDir);
|
|
306
|
+
const mcpCheck = runMcpSelfCheck({ npx: node.npx, env, selfCheck });
|
|
307
|
+
const mcpSnapshot = snapshotFile(paths.workBuddyMcpJson);
|
|
308
|
+
const stateSnapshot = snapshotFile(paths.bootstrapState);
|
|
309
|
+
let pluginBackup = null;
|
|
310
|
+
let pluginReplaced = false;
|
|
311
|
+
try {
|
|
312
|
+
mergeMcpConfig(paths.workBuddyMcpJson, buildMcpConfig(pkg.version, node.npx, node.binDir));
|
|
313
|
+
pluginBackup = replacePlugin(paths.workBuddyPluginDir);
|
|
314
|
+
pluginReplaced = true;
|
|
315
|
+
|
|
316
|
+
const state = loadState(paths.bootstrapState);
|
|
317
|
+
state.consent_granted = markConsent || state.consent_granted;
|
|
318
|
+
state.status = 'restart_required';
|
|
319
|
+
state.creator_outreach_version = pkg.version;
|
|
320
|
+
state.observed_mcp_server_version = mcpCheck.server_version || null;
|
|
321
|
+
state.last_check_at = new Date().toISOString();
|
|
322
|
+
state.last_error = null;
|
|
323
|
+
state.restart_required = true;
|
|
324
|
+
writeJsonAtomic(paths.bootstrapState, state);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (pluginReplaced) restorePlugin(paths.workBuddyPluginDir, pluginBackup);
|
|
327
|
+
restoreFile(paths.workBuddyMcpJson, mcpSnapshot);
|
|
328
|
+
restoreFile(paths.bootstrapState, stateSnapshot);
|
|
329
|
+
throw error;
|
|
152
330
|
}
|
|
331
|
+
if (pluginBackup && fs.existsSync(pluginBackup)) fs.rmSync(pluginBackup, { recursive: true, force: true });
|
|
332
|
+
return { status: 'restart_required', mcp_server_version: mcpCheck.server_version || null };
|
|
333
|
+
}
|
|
153
334
|
|
|
154
|
-
|
|
155
|
-
|
|
335
|
+
function markReady({ homeDir = os.homedir(), nodeVersion = process.versions.node } = {}) {
|
|
336
|
+
const paths = getPaths(homeDir);
|
|
337
|
+
const state = loadState(paths.bootstrapState);
|
|
338
|
+
const detected = detectStatus({ paths, state: Object.assign({}, state, { restart_required: false }), nodeVersion });
|
|
339
|
+
if (detected.status !== 'ready') throw new Error(detected.reason || `Cannot mark bootstrap as ready: ${detected.status}`);
|
|
340
|
+
state.status = 'ready';
|
|
341
|
+
state.restart_required = false;
|
|
342
|
+
state.last_error = null;
|
|
343
|
+
writeJsonAtomic(paths.bootstrapState, state);
|
|
344
|
+
return { status: 'ready' };
|
|
156
345
|
}
|
|
157
346
|
|
|
158
|
-
|
|
159
|
-
|
|
347
|
+
function update({
|
|
348
|
+
homeDir = os.homedir(),
|
|
349
|
+
nodeVersion = process.versions.node,
|
|
350
|
+
selfCheck = defaultMcpSelfCheck,
|
|
351
|
+
} = {}) {
|
|
352
|
+
const paths = getPaths(homeDir);
|
|
353
|
+
const state = loadState(paths.bootstrapState);
|
|
354
|
+
if (!state.consent_granted) return { status: 'uninitialized', reason: 'Initial installation confirmation is required.' };
|
|
355
|
+
const detected = detectStatus({ paths, state: Object.assign({}, state, { restart_required: false }), nodeVersion });
|
|
356
|
+
if (detected.status === 'repair_required' || detected.status === 'uninitialized') {
|
|
357
|
+
return { status: detected.status, reason: detected.reason };
|
|
358
|
+
}
|
|
359
|
+
if (state.restart_required) return { status: 'restart_required', reason: 'WorkBuddy must be restarted to load the updated plugin and MCP configuration.' };
|
|
360
|
+
if (readInstalledPluginVersion(paths.workBuddyPluginDir) === pkg.version) {
|
|
361
|
+
state.status = 'ready';
|
|
362
|
+
state.creator_outreach_version = pkg.version;
|
|
363
|
+
state.last_check_at = new Date().toISOString();
|
|
364
|
+
state.last_error = null;
|
|
365
|
+
writeJsonAtomic(paths.bootstrapState, state);
|
|
366
|
+
return { status: 'ready', creator_outreach_version: pkg.version };
|
|
367
|
+
}
|
|
368
|
+
return install({ homeDir, nodeVersion, selfCheck, markConsent: false });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function printJson(value) {
|
|
372
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function runCli(argv = process.argv.slice(2)) {
|
|
376
|
+
const action = argv[0];
|
|
377
|
+
const options = {
|
|
378
|
+
homeDir: process.env.SCOREHUB_BOOTSTRAP_HOME || os.homedir(),
|
|
379
|
+
nodeVersion: process.versions.node,
|
|
380
|
+
};
|
|
381
|
+
try {
|
|
382
|
+
let result;
|
|
383
|
+
if (action === 'bootstrap' && argv.includes('--check')) result = check(options);
|
|
384
|
+
else if (action === 'bootstrap' && argv.includes('--install')) result = install(options);
|
|
385
|
+
else if (action === 'bootstrap' && argv.includes('--update')) result = update(options);
|
|
386
|
+
else if (action === 'bootstrap' && argv.includes('--mark-ready')) result = markReady(options);
|
|
387
|
+
else result = install(options);
|
|
388
|
+
printJson(result);
|
|
389
|
+
return result.status === 'repair_required' ? 1 : 0;
|
|
390
|
+
} catch (error) {
|
|
391
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
392
|
+
printJson({ schema_version: STATE_SCHEMA_VERSION, status: 'repair_required', reason });
|
|
393
|
+
return 1;
|
|
394
|
+
}
|
|
160
395
|
}
|
|
161
396
|
|
|
397
|
+
if (require.main === module) process.exitCode = runCli();
|
|
398
|
+
|
|
162
399
|
module.exports = {
|
|
163
400
|
CREATOR_OUTREACH_SELF_BOOTSTRAP_COMMAND,
|
|
401
|
+
MCP_SERVER_SELF_CHECK_COMMAND,
|
|
164
402
|
MIN_NODE_MAJOR,
|
|
403
|
+
UPDATE_INTERVAL_MS,
|
|
165
404
|
WORKBUDDY_SHARE_URL,
|
|
405
|
+
check,
|
|
406
|
+
detectStatus,
|
|
166
407
|
getNodeMajorVersion,
|
|
167
408
|
getPaths,
|
|
168
409
|
install,
|
|
410
|
+
isManagedMcpConfig,
|
|
169
411
|
isSupportedNode,
|
|
412
|
+
markReady,
|
|
413
|
+
runCli,
|
|
414
|
+
update,
|
|
170
415
|
};
|