|
|
@@ -5,56 +5,83 @@ const path = require('path');
|
|
|
const { spawnSync } = require('child_process');
|
|
|
|
|
|
const SOURCE_ROOT = path.resolve(__dirname, '..');
|
|
|
+const WORKSPACE_ROOT = process.cwd();
|
|
|
const DEFAULT_TARGET = path.join(os.homedir(), '.claude', 'plugins', 'voc-intelligence');
|
|
|
+const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, '.claude', 'plugins', 'voc-intelligence');
|
|
|
+const WORKSPACE_PLUGINS_ROOT = path.join(WORKSPACE_ROOT, '.claude', 'plugins');
|
|
|
const MIN_NODE_MAJOR = 18;
|
|
|
+
|
|
|
const EXCLUDED_DIRS = new Set([
|
|
|
'node_modules',
|
|
|
'memory',
|
|
|
'outputs',
|
|
|
'.tmp',
|
|
|
- '.git'
|
|
|
+ '.git',
|
|
|
+ '.claude'
|
|
|
]);
|
|
|
+
|
|
|
const EXCLUDED_FILES = new Set([
|
|
|
'.env',
|
|
|
+ '.env.local',
|
|
|
'.npmrc'
|
|
|
]);
|
|
|
|
|
|
function usage() {
|
|
|
return [
|
|
|
- 'Claude VOC 技能包安装工具',
|
|
|
+ 'Claude VOC skill package installer',
|
|
|
'',
|
|
|
- '用法:',
|
|
|
+ 'Usage:',
|
|
|
' claude-voc install',
|
|
|
- ' claude-voc install --smoke',
|
|
|
+ ' claude-voc workspace',
|
|
|
+ ' claude-voc install --workspace --smoke',
|
|
|
' claude-voc check',
|
|
|
' claude-voc smoke',
|
|
|
' claude-voc path',
|
|
|
'',
|
|
|
- '也可以免全局安装:',
|
|
|
+ 'npx:',
|
|
|
' npx @gangvy/claude-code-voc-intelligence install',
|
|
|
+ ' npx @gangvy/claude-code-voc-intelligence workspace --smoke',
|
|
|
'',
|
|
|
- '参数:',
|
|
|
- ' --target <dir> 指定安装目录,默认 ~/.claude/plugins/voc-intelligence',
|
|
|
- ' --skip-install 复制后跳过 npm install',
|
|
|
- ' --smoke 安装后跑 sample / 偏好记忆 / MCP smoke',
|
|
|
- ' --help, -h 显示帮助'
|
|
|
+ 'Options:',
|
|
|
+ ' --workspace Install into ./.claude/plugins/voc-intelligence',
|
|
|
+ ' --target <dir> Install into a custom directory',
|
|
|
+ ' --force Allow overwriting an existing custom target',
|
|
|
+ ' --skip-install Skip npm install after copying files',
|
|
|
+ ' --smoke Run sample/preference/MCP smoke checks after install',
|
|
|
+ ' --help, -h Show help'
|
|
|
].join('\n');
|
|
|
}
|
|
|
|
|
|
+function expandHome(value) {
|
|
|
+ return value.replace(/^~(?=$|[\\/])/, os.homedir());
|
|
|
+}
|
|
|
+
|
|
|
function parseArgs(argv) {
|
|
|
+ const first = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'install';
|
|
|
const opts = {
|
|
|
- command: argv[0] && !argv[0].startsWith('--') ? argv[0] : 'install',
|
|
|
+ command: first,
|
|
|
target: DEFAULT_TARGET,
|
|
|
skipInstall: false,
|
|
|
smoke: false,
|
|
|
+ force: false,
|
|
|
help: false
|
|
|
};
|
|
|
- for (let i = opts.command === argv[0] ? 1 : 0; i < argv.length; i++) {
|
|
|
+
|
|
|
+ if (first === 'workspace' || first === 'install-workspace') {
|
|
|
+ opts.command = 'install';
|
|
|
+ opts.target = WORKSPACE_TARGET;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (let i = first === argv[0] ? 1 : 0; i < argv.length; i++) {
|
|
|
const token = argv[i];
|
|
|
if (token === '--target') {
|
|
|
opts.target = argv[++i];
|
|
|
} else if (token.startsWith('--target=')) {
|
|
|
opts.target = token.slice('--target='.length);
|
|
|
+ } else if (token === '--workspace') {
|
|
|
+ opts.target = WORKSPACE_TARGET;
|
|
|
+ } else if (token === '--force') {
|
|
|
+ opts.force = true;
|
|
|
} else if (token === '--skip-install') {
|
|
|
opts.skipInstall = true;
|
|
|
} else if (token === '--smoke') {
|
|
|
@@ -63,7 +90,8 @@ function parseArgs(argv) {
|
|
|
opts.help = true;
|
|
|
}
|
|
|
}
|
|
|
- opts.target = path.resolve(opts.target.replace(/^~(?=$|[\\/])/, os.homedir()));
|
|
|
+
|
|
|
+ opts.target = path.resolve(expandHome(opts.target));
|
|
|
return opts;
|
|
|
}
|
|
|
|
|
|
@@ -86,7 +114,7 @@ function run(command, args, cwd) {
|
|
|
function checkNodeVersion() {
|
|
|
const major = Number(process.versions.node.split('.')[0]);
|
|
|
if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
|
|
|
- throw new Error(`Node.js 版本过低:当前 ${process.version},需要 Node.js ${MIN_NODE_MAJOR}+`);
|
|
|
+ throw new Error(`Node.js ${MIN_NODE_MAJOR}+ is required. Current version: ${process.version}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -94,10 +122,23 @@ function ensureDir(dirPath) {
|
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
|
}
|
|
|
|
|
|
+function isInside(parentDir, childDir) {
|
|
|
+ const relative = path.relative(path.resolve(parentDir), path.resolve(childDir));
|
|
|
+ return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
|
+}
|
|
|
+
|
|
|
function isSafeDefaultTarget(targetDir) {
|
|
|
return path.resolve(targetDir) === path.resolve(DEFAULT_TARGET);
|
|
|
}
|
|
|
|
|
|
+function isSafeWorkspaceTarget(targetDir) {
|
|
|
+ return isInside(WORKSPACE_PLUGINS_ROOT, targetDir);
|
|
|
+}
|
|
|
+
|
|
|
+function canOverwriteTarget(targetDir, opts) {
|
|
|
+ return opts.force || isSafeDefaultTarget(targetDir) || isSafeWorkspaceTarget(targetDir);
|
|
|
+}
|
|
|
+
|
|
|
function shouldCopyEntry(entry) {
|
|
|
if (entry.isDirectory()) return !EXCLUDED_DIRS.has(entry.name);
|
|
|
if (entry.isFile()) {
|
|
|
@@ -133,51 +174,573 @@ function assertInstalled(targetDir) {
|
|
|
'.claude-plugin/plugin.json',
|
|
|
'.mcp.json',
|
|
|
'skills/xiaohongshu-trend-intelligence/SKILL.md',
|
|
|
+ 'skills/douyin-trend-intelligence/SKILL.md',
|
|
|
+ 'skills/voc-issue-pool/SKILL.md',
|
|
|
+ 'skills/voc-problem-deep-dive/SKILL.md',
|
|
|
+ 'skills/voc-content-plan/SKILL.md',
|
|
|
+ 'skills/voc-speaking-script/SKILL.md',
|
|
|
+ 'skills/voc-competitor-map/SKILL.md',
|
|
|
+ 'skills/voc-business-workflow/SKILL.md',
|
|
|
'mcp/src/server.js'
|
|
|
];
|
|
|
const missing = required.filter(rel => !fs.existsSync(path.join(targetDir, rel)));
|
|
|
if (missing.length) {
|
|
|
- throw new Error(`安装目录缺少必要文件:${missing.join(', ')}`);
|
|
|
+ throw new Error(`Missing required files in install target: ${missing.join(', ')}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function writeMcpConfig(targetDir) {
|
|
|
+ const serverPath = path.join(targetDir, 'mcp', 'src', 'server.js');
|
|
|
+ const mcpConfig = {
|
|
|
+ mcpServers: {
|
|
|
+ voc: {
|
|
|
+ command: 'node',
|
|
|
+ args: [serverPath],
|
|
|
+ cwd: targetDir
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ fs.writeFileSync(path.join(targetDir, '.mcp.json'), `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
|
|
|
+}
|
|
|
+
|
|
|
+function readJsonIfExists(filePath) {
|
|
|
+ if (!fs.existsSync(filePath)) return {};
|
|
|
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
|
+}
|
|
|
+
|
|
|
+function writeWorkspaceMcpConfig(targetDir) {
|
|
|
+ const mcpConfigPath = path.join(WORKSPACE_ROOT, '.mcp.json');
|
|
|
+ const existing = readJsonIfExists(mcpConfigPath);
|
|
|
+ const mcpConfig = {
|
|
|
+ ...existing,
|
|
|
+ mcpServers: {
|
|
|
+ ...(existing.mcpServers || {}),
|
|
|
+ voc: {
|
|
|
+ command: 'node',
|
|
|
+ args: [path.join(targetDir, 'mcp', 'src', 'server.js')],
|
|
|
+ cwd: targetDir
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ fs.writeFileSync(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
|
|
|
+}
|
|
|
+
|
|
|
+function asForwardSlash(filePath) {
|
|
|
+ return path.resolve(filePath).replace(/\\/g, '/');
|
|
|
+}
|
|
|
+
|
|
|
+function quotedPath(filePath) {
|
|
|
+ return `"${asForwardSlash(filePath)}"`;
|
|
|
+}
|
|
|
+
|
|
|
+function workspaceTrendSkillText(options) {
|
|
|
+ const trendRunner = path.join(options.targetDir, 'mcp', 'src', 'tools', options.trendTool);
|
|
|
+ const preferenceUpdater = path.join(options.targetDir, 'mcp', 'src', 'tools', options.preferenceTool);
|
|
|
+ const profilePath = path.join(options.targetDir, 'memory-templates', options.profileFile);
|
|
|
+ const defaultMemoryPath = path.join(WORKSPACE_ROOT, 'outputs', options.memoryDir, options.memoryFile);
|
|
|
+ const liveLimits = options.platform === '抖音'
|
|
|
+ ? '--keyword-limit 1 --videos-per-keyword 1 --max-comment-pages 1'
|
|
|
+ : '--keyword-limit 1 --notes-per-keyword 1 --max-comment-pages 1';
|
|
|
+
|
|
|
+ return [
|
|
|
+ '---',
|
|
|
+ `name: ${options.skillName}`,
|
|
|
+ `description: ${options.description}`,
|
|
|
+ 'allowed-tools: Read Write Bash(node *)',
|
|
|
+ '---',
|
|
|
+ '',
|
|
|
+ `# ${options.platform}趋势情报官`,
|
|
|
+ '',
|
|
|
+ `你是面向业务用户的“${options.platform}趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。`,
|
|
|
+ '',
|
|
|
+ '## 必须执行',
|
|
|
+ '',
|
|
|
+ `- 用户要求${options.platform}趋势情报、内容趋势、评论分析、真实采集、live采集或社媒 VOC 时,必须运行本技能包的工具或 MCP,不要只凭经验回答。`,
|
|
|
+ '- 用户说“live / 真实采集 / 进入 live / 实时采集 / 抓真实数据”时,必须运行 live 模式工具。不要回复“我不能实时抓取外部数据”。本技能包已经提供本地 VOC 采集入口。',
|
|
|
+ '- live 模式返回 `needs_token`、`needs_recharge` 或 `needs_valid_token` 时,必须把工具输出里的 `assistantMessage` 原样转述给用户,尤其不能省略充值/开通链接。',
|
|
|
+ '- 如果工具输出里有 `nextActions`,至少转述其中带 `apig-pay` 的充值链接。',
|
|
|
+ '- 第一轮只说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。',
|
|
|
+ '- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。',
|
|
|
+ '',
|
|
|
+ '## MANDATORY Live Rule',
|
|
|
+ '',
|
|
|
+ '- If the user asks for live collection, real collection, or entering live mode, run the local VOC tool in live mode.',
|
|
|
+ '- Do not answer that you cannot fetch external data. The local VOC tool is the approved collection entry.',
|
|
|
+ '- If the tool returns `needs_token`, `needs_recharge`, or `needs_valid_token`, paste its `assistantMessage` to the user and keep the payment URL visible.',
|
|
|
+ '- The payment URL must be shown when there is no token or insufficient balance: `https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF`',
|
|
|
+ '',
|
|
|
+ '## sample 命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(trendRunner)} --collection-mode sample --profile ${quotedPath(profilePath)} --output ${JSON.stringify(options.sampleOutput)} --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '## live 命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(trendRunner)} --collection-mode live --profile ${quotedPath(profilePath)} --output ${JSON.stringify(options.liveOutput)} ${liveLimits} --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
|
|
|
+ '',
|
|
|
+ '## 偏好更新命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(preferenceUpdater)} --message "<用户反馈原文>" --memory ${quotedPath(defaultMemoryPath)} --result-prefix ${options.resultPrefix}`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '## 充值链接口径',
|
|
|
+ '',
|
|
|
+ '没有 token 或余额不足时,用户必须看到:',
|
|
|
+ '',
|
|
|
+ '```text',
|
|
|
+ 'https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF',
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '套餐入口可作为补充:',
|
|
|
+ '',
|
|
|
+ '```text',
|
|
|
+ 'https://app.fmode.cn/dev/apig-pay/#/workshop/99',
|
|
|
+ '```',
|
|
|
+ ''
|
|
|
+ ].join('\n');
|
|
|
+}
|
|
|
+
|
|
|
+function workspaceSkillText(targetDir) {
|
|
|
+ return workspaceTrendSkillText({
|
|
|
+ targetDir,
|
|
|
+ skillName: 'xiaohongshu-trend-intelligence',
|
|
|
+ platform: '小红书',
|
|
|
+ description: 'Build Xiaohongshu trend intelligence reports. Use when the user asks for 小红书趋势情报、行业趋势、内容选题、销售话术、真实采集、live采集、用户顾虑 or social VOC analysis.',
|
|
|
+ trendTool: 'xiaohongshu-trend-run.js',
|
|
|
+ preferenceTool: 'xiaohongshu-preference-update.js',
|
|
|
+ profileFile: 'xiaohongshu-trend-profile.json',
|
|
|
+ memoryDir: 'claude-code-xhs-memory',
|
|
|
+ memoryFile: 'xiaohongshu-trend-memory.json',
|
|
|
+ sampleOutput: 'outputs/claude-code-xhs-sample',
|
|
|
+ liveOutput: 'outputs/claude-code-xhs-live',
|
|
|
+ resultPrefix: 'XHS_PREF_RESULT'
|
|
|
+ });
|
|
|
+ const trendRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'xiaohongshu-trend-run.js');
|
|
|
+ const preferenceUpdater = path.join(targetDir, 'mcp', 'src', 'tools', 'xiaohongshu-preference-update.js');
|
|
|
+ const profilePath = path.join(targetDir, 'memory-templates', 'xiaohongshu-trend-profile.json');
|
|
|
+ const defaultMemoryPath = path.join(WORKSPACE_ROOT, 'outputs', 'claude-code-xhs-memory', 'xiaohongshu-trend-memory.json');
|
|
|
+
|
|
|
+ return [
|
|
|
+ '---',
|
|
|
+ 'name: xiaohongshu-trend-intelligence',
|
|
|
+ 'description: Build Xiaohongshu trend intelligence reports from industry direction, audience, keywords, notes, and comments. Use when the user asks for 小红书趋势情报、行业趋势、女性客户消费决策、设计元素洞察、家装全屋定制趋势、内容选题 or social VOC analysis.',
|
|
|
+ 'allowed-tools: Read Write Bash(node *)',
|
|
|
+ '---',
|
|
|
+ '',
|
|
|
+ '# 小红书趋势情报官',
|
|
|
+ '',
|
|
|
+ '你是面向业务用户的“小红书趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。',
|
|
|
+ '',
|
|
|
+ '## 必须执行',
|
|
|
+ '',
|
|
|
+ '- 当用户要求做小红书趋势情报、行业趋势、女性客户消费决策、家装全屋定制趋势、设计元素洞察或内容选题时,优先运行下面的 sample 工具命令。',
|
|
|
+ '- 不要只凭经验回答,也不要只返回文件路径。',
|
|
|
+ '- 工具输出里会有 `assistantMessage`,请把它的正文直接发给用户。',
|
|
|
+ '- 第一轮只能说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。',
|
|
|
+ '- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。',
|
|
|
+ '',
|
|
|
+ '## sample 趋势情报命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(trendRunner)} --collection-mode sample --profile ${quotedPath(profilePath)} --output "outputs/claude-code-xhs-sample" --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
|
|
|
+ '',
|
|
|
+ '## 偏好更新命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(preferenceUpdater)} --message "<用户反馈原文>" --memory ${quotedPath(defaultMemoryPath)} --result-prefix XHS_PREF_RESULT`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '## 推荐用户启动话术',
|
|
|
+ '',
|
|
|
+ '```text',
|
|
|
+ '帮我做一份家装全屋定制行业的小红书趋势情报。',
|
|
|
+ '先用演示样例跑通流程,不要真实采集。',
|
|
|
+ '在聊天里给我第一轮样本观察和待确认问题。',
|
|
|
+ '```',
|
|
|
+ ''
|
|
|
+ ].join('\n');
|
|
|
+}
|
|
|
+
|
|
|
+function workspaceDouyinSkillText(targetDir) {
|
|
|
+ return workspaceTrendSkillText({
|
|
|
+ targetDir,
|
|
|
+ skillName: 'douyin-trend-intelligence',
|
|
|
+ platform: '抖音',
|
|
|
+ description: 'Build Douyin trend intelligence reports. Use when the user asks for 抖音趋势情报、视频评论、口播选题、短视频开头、内容趋势、真实采集、live采集、用户顾虑 or social VOC analysis.',
|
|
|
+ trendTool: 'douyin-trend-run.js',
|
|
|
+ preferenceTool: 'douyin-preference-update.js',
|
|
|
+ profileFile: 'douyin-trend-profile.json',
|
|
|
+ memoryDir: 'claude-code-douyin-memory',
|
|
|
+ memoryFile: 'douyin-trend-memory.json',
|
|
|
+ sampleOutput: 'outputs/claude-code-douyin-sample',
|
|
|
+ liveOutput: 'outputs/claude-code-douyin-live',
|
|
|
+ resultPrefix: 'DOUYIN_PREF_RESULT'
|
|
|
+ });
|
|
|
+ const trendRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'douyin-trend-run.js');
|
|
|
+ const preferenceUpdater = path.join(targetDir, 'mcp', 'src', 'tools', 'douyin-preference-update.js');
|
|
|
+ const profilePath = path.join(targetDir, 'memory-templates', 'douyin-trend-profile.json');
|
|
|
+ const defaultMemoryPath = path.join(WORKSPACE_ROOT, 'outputs', 'claude-code-douyin-memory', 'douyin-trend-memory.json');
|
|
|
+
|
|
|
+ return [
|
|
|
+ '---',
|
|
|
+ 'name: douyin-trend-intelligence',
|
|
|
+ 'description: Build Douyin trend intelligence reports from industry direction, audience, keywords, videos, and comments. Use when the user asks for 抖音趋势情报、视频评论、口播选题、短视频开头、内容趋势 or social VOC analysis.',
|
|
|
+ 'allowed-tools: Read Write Bash(node *)',
|
|
|
+ '---',
|
|
|
+ '',
|
|
|
+ '# 抖音趋势情报官',
|
|
|
+ '',
|
|
|
+ '你是面向业务用户的“抖音趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。',
|
|
|
+ '',
|
|
|
+ '## 必须执行',
|
|
|
+ '',
|
|
|
+ '- 当用户要求做抖音趋势情报、视频评论、口播选题、短视频开头或内容趋势时,优先运行下面的 sample 工具命令。',
|
|
|
+ '- 不要只凭经验回答,也不要只返回文件路径。',
|
|
|
+ '- 工具输出里会有 `assistantMessage`,请把它的正文直接发给用户。',
|
|
|
+ '- 第一轮只能说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。',
|
|
|
+ '- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。',
|
|
|
+ '',
|
|
|
+ '## sample 趋势情报命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(trendRunner)} --collection-mode sample --profile ${quotedPath(profilePath)} --output "outputs/claude-code-douyin-sample" --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
|
|
|
+ '',
|
|
|
+ '## 偏好更新命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(preferenceUpdater)} --message "<用户反馈原文>" --memory ${quotedPath(defaultMemoryPath)} --result-prefix DOUYIN_PREF_RESULT`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '## 推荐用户启动话术',
|
|
|
+ '',
|
|
|
+ '```text',
|
|
|
+ '帮我做一份家装全屋定制行业的抖音趋势情报。',
|
|
|
+ '先用演示样例跑通流程,不要真实采集。',
|
|
|
+ '在聊天里给我第一轮样本观察和待确认问题。',
|
|
|
+ '```',
|
|
|
+ ''
|
|
|
+ ].join('\n');
|
|
|
+}
|
|
|
+
|
|
|
+function workspaceProblemDeepDiveSkillText(targetDir) {
|
|
|
+ const deepDiveRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-problem-deep-dive-run.js');
|
|
|
+ return [
|
|
|
+ '---',
|
|
|
+ 'name: voc-problem-deep-dive',
|
|
|
+ 'description: Deep-dive a single VOC problem into root cause, boss/operator impact, concrete actions, content ideas, and validation metrics. Use when the user says 继续挖、深入分析、老板怎么解决、这个问题怎么办、怎么改门店、怎么转成内容、单点VOC or VOC深挖.',
|
|
|
+ 'allowed-tools: Read Write Bash(node *)',
|
|
|
+ '---',
|
|
|
+ '',
|
|
|
+ '# 单点 VOC 深挖官',
|
|
|
+ '',
|
|
|
+ '你是面向老板和门店经营者的“单点 VOC 深挖官”。用户不需要学习提示词。用户只要说“这个问题继续挖”“老板怎么解决”“这个差评背后是什么”,你就要把真实用户声音转成可执行经营动作。',
|
|
|
+ '',
|
|
|
+ '## 必须执行',
|
|
|
+ '',
|
|
|
+ '- 当用户说“继续挖、深入分析、老板怎么解决、这个问题怎么办、怎么改、怎么转成内容、单点 VOC”时,必须运行单点深挖工具。',
|
|
|
+ '- 不要输出提示词教学,不要让用户自己设计分析维度。',
|
|
|
+ '- 输出必须站在老板视角:这件事影响新客、复购、客单、口碑,还是现场效率。',
|
|
|
+ '- 每次只深挖一个问题点。如果用户给了多个问题,先选择最影响转化的一个。',
|
|
|
+ '- 如果上一轮趋势报告里有证据样本或高赞评论,要把相关评论作为 evidence 传入工具。',
|
|
|
+ '- 要使用记忆能力:用户说“这个动作不适合”“我们已经试过”“这个有效”“下次更偏内容/门店动作”时,把原话作为 `feedback` 传入;如果能明确识别,也同步传 `blockedActions`、`validatedActions`、`rejectedActions` 或 `preferredActions`。',
|
|
|
+ '- 后续多轮迭代时要带上 `project`/`brand`/`store`、`industry`、`scenario`、`audience`。未显式传 `memory` 时,工具会按这些字段自动隔离记忆,避免不同行业、不同客户串味。',
|
|
|
+ '- 如果用户没有指定记忆文件,不要强行要求用户理解路径;让工具使用默认分桶记忆即可。只有在同一个客户需要固定沉淀时,才显式传同一个 `memory` 路径。',
|
|
|
+ '',
|
|
|
+ '## 命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(deepDiveRunner)} --issue "排队" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '## 带记忆的多轮迭代',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(deepDiveRunner)} --issue "排队" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --memory "outputs/voc-problem-memory.json" --feedback "老板不想做等位饮品,更想先改评论区回复和预点单" --blocked-actions "等位饮品" --preferred-actions "评论区回复,预点单" --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '自然语言反馈也可以直接传入 `feedback`,工具会尝试自动提取偏好和屏蔽动作:',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(deepDiveRunner)} --issue "排队" --project "样板门店A" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --feedback "这个店不想做等位饮品,更想先改评论区回复和预点单" --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
|
|
|
+ ''
|
|
|
+ ].join('\n');
|
|
|
+}
|
|
|
+
|
|
|
+function workspaceIssuePoolSkillText(targetDir) {
|
|
|
+ const issuePoolRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-issue-pool-run.js');
|
|
|
+ return [
|
|
|
+ '---',
|
|
|
+ 'name: voc-issue-pool',
|
|
|
+ 'description: Turn VOC comments, report evidence, or user feedback into a prioritized issue pool with status, evidence, business impact, next actions, and follow-up deep-dive suggestions. Use when the user asks 哪些问题最影响生意、问题池、问题清单、用户都在吐槽什么、先改哪个、VOC问题管理、问题优先级、把评论整理成问题.',
|
|
|
+ 'allowed-tools: Read Write Bash(node *)',
|
|
|
+ '---',
|
|
|
+ '',
|
|
|
+ '# VOC 问题池官',
|
|
|
+ '',
|
|
|
+ '你是面向老板、门店经营者和营销负责人的 VOC 问题池官。用户不需要知道分类法,也不需要学习提示词。用户只要说“这些评论里哪些问题最重要”“先改哪个”“整理成问题池”,你就把真实用户声音整理成可排序、可跟进、可继续深挖的问题列表。',
|
|
|
+ '',
|
|
|
+ '## 必须执行',
|
|
|
+ '',
|
|
|
+ '- 当用户说“问题池、问题清单、用户吐槽什么、哪些问题最影响生意、先改哪个、VOC 问题管理、把评论整理成问题”时,优先运行 `voc_issue_pool_run`。',
|
|
|
+ '- 如果上一轮小红书/抖音报告里有证据样本或高赞评论,把相关评论作为 `evidence`、`evidenceText` 或 `reportPath` 传入。',
|
|
|
+ '- 输出必须站在业务视角:这个问题影响新客、复购、客单、口碑、内容信任,还是现场效率。',
|
|
|
+ '- 不要只做词频统计。每个问题都要包含证据样本、影响环节、建议动作和下一步。',
|
|
|
+ '- 如果用户说某个问题已解决、验证中、暂缓,要通过 `resolvedIssues`、`validatingIssues`、`blockedIssues` 或 `statusUpdates` 更新问题状态。',
|
|
|
+ '- 后续深挖时,引导用户直接说“继续深挖「问题名」”,然后转入 `voc-problem-deep-dive`。',
|
|
|
+ '',
|
|
|
+ '## 命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(issuePoolRunner)} --project "样板门店A" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --evidence "排队久,但是不知道值不值得等。,人均有点高,第一次来不知道怎么点不踩雷。,服务忙起来没人理。" --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '带报告文件:',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(issuePoolRunner)} --project "样板门店A" --industry "本地生活餐饮" --report "outputs/douyin-trend-report.md" --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
|
|
|
+ ''
|
|
|
+ ].join('\n');
|
|
|
+}
|
|
|
+
|
|
|
+function workspaceContentPlanSkillText(targetDir) {
|
|
|
+ const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-content-plan-run.js');
|
|
|
+ return [
|
|
|
+ '---',
|
|
|
+ 'name: voc-content-plan',
|
|
|
+ 'description: Turn VOC issues into a 7-day content plan with short-video topics and speaking scripts. Use when the user asks 下周账号发什么、口播脚本、短视频选题、把用户问题变成内容、7天内容计划.',
|
|
|
+ 'allowed-tools: Read Write Bash(node *)',
|
|
|
+ '---',
|
|
|
+ '',
|
|
|
+ '# VOC 内容选题官',
|
|
|
+ '',
|
|
|
+ '当用户说“下周发什么、7 天选题、口播脚本、短视频脚本、账号内容、把问题变成内容”时,运行本工具。',
|
|
|
+ '',
|
|
|
+ '必须把内容绑定到 VOC 用户问题,不要输出空泛爆款标题。不要覆盖创始人故事。',
|
|
|
+ '',
|
|
|
+ '## 命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(runner)} --brand "样板门店A" --industry "本地生活餐饮" --issues "第一次来怕踩雷;觉得人均贵;怕排队久" --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '运行后把正文直接发给用户,不要展示 JSON。',
|
|
|
+ ''
|
|
|
+ ].join('\n');
|
|
|
+}
|
|
|
+
|
|
|
+function workspaceSpeakingScriptSkillText(targetDir) {
|
|
|
+ const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-speaking-script-run.js');
|
|
|
+ return [
|
|
|
+ '---',
|
|
|
+ 'name: voc-speaking-script',
|
|
|
+ 'description: Turn one VOC-backed topic into a co-created speaking script with 60-second draft, 30-second compressed version, teleprompter text, revision feedback, finalization, and script memory. Use when the user says 选题2进入脚本共创、改稿、定稿、口播稿、提词器、开头更狠、老板视角、少讲概念.',
|
|
|
+ 'allowed-tools: Read Write Bash(node *)',
|
|
|
+ '---',
|
|
|
+ '',
|
|
|
+ '# VOC 口播脚本共创',
|
|
|
+ '',
|
|
|
+ '当用户从 7 天内容计划里选择某条选题,或说“进入脚本共创、改稿、定稿、提词器版、开头更狠、老板视角”时,运行本工具。',
|
|
|
+ '',
|
|
|
+ '口播必须来自 VOC 用户问题、趋势证据、问题池 Top 问题、单点深挖动作或竞品评论差异。没有真实证据时要标注低证据风险。',
|
|
|
+ '',
|
|
|
+ '## 命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(runner)} --brand "样板门店A" --industry "本地生活餐饮" --topic "第一次来怎么点不踩雷" --user-issue "第一次来怕点错" --evidence "第一次来不知道怎么点,怕踩雷" --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '改稿:',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(runner)} --brand "样板门店A" --topic "第一次来怎么点不踩雷" --user-issue "第一次来怕点错" --evidence "第一次来不知道怎么点,怕踩雷" --feedback "开头更狠一点,老板视角,少讲概念,多给具体场景" --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '定稿时传 `--finalize true`,工具会写入脚本记忆。运行后把正文直接发给用户,不要展示 JSON。',
|
|
|
+ ''
|
|
|
+ ].join('\n');
|
|
|
+}
|
|
|
+
|
|
|
+function workspaceCompetitorMapSkillText(targetDir) {
|
|
|
+ const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-competitor-map-run.js');
|
|
|
+ return [
|
|
|
+ '---',
|
|
|
+ 'name: voc-competitor-map',
|
|
|
+ 'description: Build a competitor map and differentiated opportunity report. Use when the user asks 看竞品、竞品分析、错位竞争、别人为什么吸引顾客、我不知道竞品是谁.',
|
|
|
+ 'allowed-tools: Read Write Bash(node *)',
|
|
|
+ '---',
|
|
|
+ '',
|
|
|
+ '# VOC 竞品图谱官',
|
|
|
+ '',
|
|
|
+ '当用户说“看竞品、竞品分析、错位竞争、别人怎么做、我不知道竞品是谁”时,运行本工具。',
|
|
|
+ '',
|
|
|
+ '如果用户没有明确竞品,先按同城、同品类、同价格带、同消费场景、平台声量生成候选竞品类型。不要覆盖创始人故事。',
|
|
|
+ '',
|
|
|
+ '## 命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(runner)} --brand "样板门店A" --city "南昌" --category "餐饮" --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '运行后把正文直接发给用户,不要展示 JSON。',
|
|
|
+ ''
|
|
|
+ ].join('\n');
|
|
|
+}
|
|
|
+
|
|
|
+function workspaceBusinessWorkflowSkillText(targetDir) {
|
|
|
+ const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-business-workflow-run.js');
|
|
|
+ return [
|
|
|
+ '---',
|
|
|
+ 'name: voc-business-workflow',
|
|
|
+ 'description: Run the full VOC business workflow from market voices to issue pool, top issue action, 7-day content plan, and one speaking script. Use when the user asks 看顾客关心什么、先改哪里、下周发什么、完整VOC闭环、经营诊断.',
|
|
|
+ 'allowed-tools: Read Write Bash(node *)',
|
|
|
+ '---',
|
|
|
+ '',
|
|
|
+ '# VOC经营闭环',
|
|
|
+ '',
|
|
|
+ '当用户想一次拿到“看市场、看问题、找动作、做内容”的结果时,运行本工具。不要让用户理解 MCP、schema 或多个工具名。',
|
|
|
+ '',
|
|
|
+ '## 命令',
|
|
|
+ '',
|
|
|
+ '```bash',
|
|
|
+ `node ${quotedPath(runner)} --brand "样板门店A" --industry "本地生活餐饮" --platform douyin --collection-mode sample --keywords "南昌菜,第一次来怎么点,排队" --assistant-message-only`,
|
|
|
+ '```',
|
|
|
+ '',
|
|
|
+ '真实采集时把 `--collection-mode sample` 换成 `--collection-mode live`。如果工具提示没有 token、余额不足或没有样本,直接把工具正文发给用户,并保留充值/降级建议。',
|
|
|
+ '',
|
|
|
+ '## 交付物',
|
|
|
+ '',
|
|
|
+ '必须包含五段:市场和用户声音、先改哪个问题、Top问题怎么解决、下周账号发什么、第一条口播稿。',
|
|
|
+ ''
|
|
|
+ ].join('\n');
|
|
|
+}
|
|
|
+
|
|
|
+function writeWorkspaceSkillEntries(targetDir) {
|
|
|
+ const xhsSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'xiaohongshu-trend-intelligence');
|
|
|
+ const douyinSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'douyin-trend-intelligence');
|
|
|
+ const issuePoolSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-issue-pool');
|
|
|
+ const deepDiveSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-problem-deep-dive');
|
|
|
+ const contentPlanSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-content-plan');
|
|
|
+ const speakingScriptSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-speaking-script');
|
|
|
+ const competitorMapSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-competitor-map');
|
|
|
+ const businessWorkflowSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-business-workflow');
|
|
|
+ ensureDir(xhsSkillDir);
|
|
|
+ ensureDir(douyinSkillDir);
|
|
|
+ ensureDir(issuePoolSkillDir);
|
|
|
+ ensureDir(deepDiveSkillDir);
|
|
|
+ ensureDir(contentPlanSkillDir);
|
|
|
+ ensureDir(speakingScriptSkillDir);
|
|
|
+ ensureDir(competitorMapSkillDir);
|
|
|
+ ensureDir(businessWorkflowSkillDir);
|
|
|
+ fs.writeFileSync(path.join(xhsSkillDir, 'SKILL.md'), workspaceSkillText(targetDir), 'utf8');
|
|
|
+ fs.writeFileSync(path.join(douyinSkillDir, 'SKILL.md'), workspaceDouyinSkillText(targetDir), 'utf8');
|
|
|
+ fs.writeFileSync(path.join(issuePoolSkillDir, 'SKILL.md'), workspaceIssuePoolSkillText(targetDir), 'utf8');
|
|
|
+ fs.writeFileSync(path.join(deepDiveSkillDir, 'SKILL.md'), workspaceProblemDeepDiveSkillText(targetDir), 'utf8');
|
|
|
+ fs.writeFileSync(path.join(contentPlanSkillDir, 'SKILL.md'), workspaceContentPlanSkillText(targetDir), 'utf8');
|
|
|
+ fs.writeFileSync(path.join(speakingScriptSkillDir, 'SKILL.md'), workspaceSpeakingScriptSkillText(targetDir), 'utf8');
|
|
|
+ fs.writeFileSync(path.join(competitorMapSkillDir, 'SKILL.md'), workspaceCompetitorMapSkillText(targetDir), 'utf8');
|
|
|
+ fs.writeFileSync(path.join(businessWorkflowSkillDir, 'SKILL.md'), workspaceBusinessWorkflowSkillText(targetDir), 'utf8');
|
|
|
+}
|
|
|
+
|
|
|
+function writeWorkspaceActivation(targetDir) {
|
|
|
+ if (!isSafeWorkspaceTarget(targetDir)) return;
|
|
|
+ writeWorkspaceMcpConfig(targetDir);
|
|
|
+ writeWorkspaceSkillEntries(targetDir);
|
|
|
+ console.log('Wrote workspace MCP config: .\\.mcp.json');
|
|
|
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\xiaohongshu-trend-intelligence\\SKILL.md');
|
|
|
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\douyin-trend-intelligence\\SKILL.md');
|
|
|
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-issue-pool\\SKILL.md');
|
|
|
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-problem-deep-dive\\SKILL.md');
|
|
|
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-content-plan\\SKILL.md');
|
|
|
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-speaking-script\\SKILL.md');
|
|
|
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-competitor-map\\SKILL.md');
|
|
|
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-business-workflow\\SKILL.md');
|
|
|
+}
|
|
|
+
|
|
|
function printNextSteps(targetDir) {
|
|
|
+ const workspaceMode = isSafeWorkspaceTarget(targetDir);
|
|
|
+ console.log('');
|
|
|
+ console.log('Install complete.');
|
|
|
+ console.log('');
|
|
|
+ if (workspaceMode) {
|
|
|
+ console.log('VSCode workspace mode is ready.');
|
|
|
+ console.log('');
|
|
|
+ console.log('Generated project files:');
|
|
|
+ console.log(' .\\.mcp.json');
|
|
|
+ console.log(' .\\.claude\\skills\\xiaohongshu-trend-intelligence\\SKILL.md');
|
|
|
+ console.log(' .\\.claude\\skills\\douyin-trend-intelligence\\SKILL.md');
|
|
|
+ console.log(' .\\.claude\\skills\\voc-issue-pool\\SKILL.md');
|
|
|
+ console.log(' .\\.claude\\skills\\voc-problem-deep-dive\\SKILL.md');
|
|
|
+ console.log(' .\\.claude\\skills\\voc-content-plan\\SKILL.md');
|
|
|
+ console.log(' .\\.claude\\skills\\voc-speaking-script\\SKILL.md');
|
|
|
+ console.log(' .\\.claude\\skills\\voc-competitor-map\\SKILL.md');
|
|
|
+ console.log(' .\\.claude\\skills\\voc-business-workflow\\SKILL.md');
|
|
|
+ console.log(' .\\.claude\\plugins\\voc-intelligence');
|
|
|
+ console.log('');
|
|
|
+ console.log('Restart the VSCode Claude Code session if it was already open.');
|
|
|
+ } else {
|
|
|
+ console.log('Claude Code CLI launch command:');
|
|
|
+ console.log(` claude --plugin-dir "${targetDir}"`);
|
|
|
+ }
|
|
|
+ console.log('');
|
|
|
+ console.log('Try this prompt in Claude Code:');
|
|
|
+ console.log(' 帮我看一下南昌餐饮最近顾客在关心什么,并告诉我先改哪里、下周发什么。');
|
|
|
console.log('');
|
|
|
- console.log('安装完成。');
|
|
|
+ console.log('Or try:');
|
|
|
+ console.log(' 帮我做一份家装全屋定制行业的小红书趋势情报。');
|
|
|
+ console.log(' 先用演示样例跑通流程,不要真实采集。');
|
|
|
+ console.log(' 在聊天里给我第一轮样本观察和待确认问题。');
|
|
|
console.log('');
|
|
|
- console.log('下一步:');
|
|
|
- console.log(` 1. 启动 Claude Code:claude --plugin-dir "${targetDir}"`);
|
|
|
- console.log(' 2. 在 Claude Code 里说:');
|
|
|
- console.log(' 帮我做一份家装全屋定制行业的小红书趋势情报。');
|
|
|
- console.log(' 重点看女性客户下季度可能关注的设计元素、风格和消费顾虑。');
|
|
|
- console.log(' 先在聊天里给我第一轮样本观察和待确认问题,不要只给文件路径。');
|
|
|
+ console.log('Or try:');
|
|
|
+ console.log(' 帮我做一份家装全屋定制行业的抖音趋势情报。');
|
|
|
+ console.log(' 先用演示样例跑通流程,不要真实采集。');
|
|
|
+ console.log(' 在聊天里给我第一轮样本观察和待确认问题。');
|
|
|
console.log('');
|
|
|
- console.log(`安装目录:${targetDir}`);
|
|
|
+ console.log(`Install target: ${targetDir}`);
|
|
|
}
|
|
|
|
|
|
function install(opts) {
|
|
|
checkNodeVersion();
|
|
|
if (path.resolve(SOURCE_ROOT) !== path.resolve(opts.target)) {
|
|
|
if (fs.existsSync(opts.target)) {
|
|
|
- if (!isSafeDefaultTarget(opts.target)) {
|
|
|
- throw new Error(`自定义安装目录已存在,为避免覆盖,请先手动清理:${opts.target}`);
|
|
|
+ if (!canOverwriteTarget(opts.target, opts)) {
|
|
|
+ throw new Error(`Target already exists. Use --force to overwrite custom target: ${opts.target}`);
|
|
|
}
|
|
|
fs.rmSync(opts.target, { recursive: true, force: true });
|
|
|
}
|
|
|
ensureDir(opts.target);
|
|
|
const copied = copyDirRecursive(SOURCE_ROOT, opts.target);
|
|
|
- console.log(`已复制技能包文件:${copied} 个`);
|
|
|
+ console.log(`Copied skill package files: ${copied}`);
|
|
|
}
|
|
|
|
|
|
assertInstalled(opts.target);
|
|
|
+ writeMcpConfig(opts.target);
|
|
|
+ writeWorkspaceActivation(opts.target);
|
|
|
|
|
|
if (!opts.skipInstall) {
|
|
|
- console.log('正在安装运行依赖...');
|
|
|
+ console.log('Installing runtime dependencies...');
|
|
|
run('npm', ['install', '--omit=dev', '--ignore-scripts'], opts.target);
|
|
|
}
|
|
|
|
|
|
if (opts.smoke) {
|
|
|
- console.log('正在运行安装自检...');
|
|
|
+ console.log('Running install smoke checks...');
|
|
|
run(process.execPath, ['install.js', '--smoke', '--skip-install', '--no-next'], opts.target);
|
|
|
} else {
|
|
|
run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
|
|
|
@@ -198,7 +761,11 @@ function smoke(opts) {
|
|
|
|
|
|
function printPath(opts) {
|
|
|
console.log(opts.target);
|
|
|
- console.log(`claude --plugin-dir "${opts.target}"`);
|
|
|
+ if (isSafeWorkspaceTarget(opts.target)) {
|
|
|
+ console.log('.\\.claude\\plugins\\voc-intelligence');
|
|
|
+ } else {
|
|
|
+ console.log(`claude --plugin-dir "${opts.target}"`);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function main() {
|
|
|
@@ -211,14 +778,13 @@ function main() {
|
|
|
if (opts.command === 'check') return check(opts);
|
|
|
if (opts.command === 'smoke') return smoke(opts);
|
|
|
if (opts.command === 'path') return printPath(opts);
|
|
|
- throw new Error(`未知命令:${opts.command}\n\n${usage()}`);
|
|
|
+ throw new Error(`Unknown command: ${opts.command}\n\n${usage()}`);
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
main();
|
|
|
} catch (error) {
|
|
|
console.error('');
|
|
|
- console.error(`claude-voc 执行失败:${error.message}`);
|
|
|
+ console.error(`claude-voc failed: ${error.message}`);
|
|
|
process.exit(1);
|
|
|
}
|
|
|
-
|