@vocmarket/voc-skill 0.3.15

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.
Files changed (82) hide show
  1. package/.claude-plugin/plugin.json +8 -0
  2. package/.mcp.json +11 -0
  3. package/README.md +334 -0
  4. package/bin/claude-voc.js +790 -0
  5. package/docs/boss-prompt-templates.md +682 -0
  6. package/docs/capability-map.md +195 -0
  7. package/docs/claude-code-troubleshooting-playbook.md +136 -0
  8. package/docs/context-budget-implementation-plan.md +491 -0
  9. package/docs/customer-quickstart.md +43 -0
  10. package/docs/deliverables/competitor-map-report.md +43 -0
  11. package/docs/deliverables/seven-day-content-plan.md +45 -0
  12. package/docs/deliverables/single-problem-action-plan.md +49 -0
  13. package/docs/deliverables/trend-insight-report.md +41 -0
  14. package/docs/deliverables/voc-issue-pool-report.md +42 -0
  15. package/docs/demo-runbook.md +31 -0
  16. package/docs/live-manual-acceptance-checklist.md +63 -0
  17. package/docs/npm-packaging-notes.md +93 -0
  18. package/docs/payment-package-links.md +86 -0
  19. package/install.js +208 -0
  20. package/mcp/src/core/credentials.js +113 -0
  21. package/mcp/src/core/evidence-quality.js +106 -0
  22. package/mcp/src/core/memory-store.js +31 -0
  23. package/mcp/src/core/payment-links.js +117 -0
  24. package/mcp/src/core/result-envelope.js +31 -0
  25. package/mcp/src/features/douyin-trend/live-collector.js +396 -0
  26. package/mcp/src/features/douyin-trend/preference-memory.js +130 -0
  27. package/mcp/src/features/douyin-trend/report.js +318 -0
  28. package/mcp/src/features/douyin-trend/sample-data.js +103 -0
  29. package/mcp/src/features/douyin-trend/signal-extractor.js +79 -0
  30. package/mcp/src/features/douyin-trend/topic-builder.js +1 -0
  31. package/mcp/src/features/voc-business-workflow/business-workflow.js +214 -0
  32. package/mcp/src/features/voc-competitor-map/competitor-map.js +198 -0
  33. package/mcp/src/features/voc-content-plan/content-plan.js +359 -0
  34. package/mcp/src/features/voc-issue-pool/issue-pool.js +488 -0
  35. package/mcp/src/features/voc-problem-deep-dive/deep-dive.js +662 -0
  36. package/mcp/src/features/voc-speaking-script/speaking-script.js +293 -0
  37. package/mcp/src/features/xiaohongshu-trend/assets.js +158 -0
  38. package/mcp/src/features/xiaohongshu-trend/industry-presets.js +335 -0
  39. package/mcp/src/features/xiaohongshu-trend/live-collector.js +337 -0
  40. package/mcp/src/features/xiaohongshu-trend/preference-memory.js +130 -0
  41. package/mcp/src/features/xiaohongshu-trend/report.js +319 -0
  42. package/mcp/src/features/xiaohongshu-trend/sample-data.js +79 -0
  43. package/mcp/src/features/xiaohongshu-trend/signal-extractor.js +77 -0
  44. package/mcp/src/features/xiaohongshu-trend/topic-builder.js +84 -0
  45. package/mcp/src/providers/douyin-api.js +170 -0
  46. package/mcp/src/providers/xiaohongshu-api.js +102 -0
  47. package/mcp/src/server.js +621 -0
  48. package/mcp/src/tools/douyin-preference-update.js +70 -0
  49. package/mcp/src/tools/douyin-trend-run.js +464 -0
  50. package/mcp/src/tools/voc-business-workflow-run.js +52 -0
  51. package/mcp/src/tools/voc-competitor-map-run.js +59 -0
  52. package/mcp/src/tools/voc-content-plan-run.js +59 -0
  53. package/mcp/src/tools/voc-issue-pool-run.js +59 -0
  54. package/mcp/src/tools/voc-problem-deep-dive-run.js +59 -0
  55. package/mcp/src/tools/voc-speaking-script-run.js +59 -0
  56. package/mcp/src/tools/xiaohongshu-preference-update.js +85 -0
  57. package/mcp/src/tools/xiaohongshu-trend-run.js +404 -0
  58. package/memory-templates/douyin-trend-profile.json +33 -0
  59. package/memory-templates/xiaohongshu-trend-profile.json +33 -0
  60. package/package.json +65 -0
  61. package/scripts/smoke-cross-industry.js +126 -0
  62. package/scripts/smoke-mcp.js +302 -0
  63. package/scripts/smoke-package.js +1005 -0
  64. package/skill-package-manifest.json +62 -0
  65. package/skills/douyin-trend-intelligence/SKILL.md +78 -0
  66. package/skills/douyin-trend-intelligence/references/live-mode.md +52 -0
  67. package/skills/douyin-trend-intelligence/references/output-format.md +32 -0
  68. package/skills/douyin-trend-intelligence/references/user-workflow.md +24 -0
  69. package/skills/voc-business-workflow/SKILL.md +61 -0
  70. package/skills/voc-competitor-map/SKILL.md +60 -0
  71. package/skills/voc-competitor-map/references/restaurant-competitor-framework.md +25 -0
  72. package/skills/voc-competitor-map/references/restaurant-differentiation-patterns.md +44 -0
  73. package/skills/voc-content-plan/SKILL.md +63 -0
  74. package/skills/voc-content-plan/references/restaurant-content-framework.md +41 -0
  75. package/skills/voc-content-plan/references/restaurant-speaking-script-patterns.md +34 -0
  76. package/skills/voc-issue-pool/SKILL.md +81 -0
  77. package/skills/voc-problem-deep-dive/SKILL.md +86 -0
  78. package/skills/voc-speaking-script/SKILL.md +99 -0
  79. package/skills/xiaohongshu-trend-intelligence/SKILL.md +61 -0
  80. package/skills/xiaohongshu-trend-intelligence/references/live-mode.md +65 -0
  81. package/skills/xiaohongshu-trend-intelligence/references/output-format.md +71 -0
  82. package/skills/xiaohongshu-trend-intelligence/references/user-workflow.md +60 -0
@@ -0,0 +1,790 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ const SOURCE_ROOT = path.resolve(__dirname, '..');
8
+ const WORKSPACE_ROOT = process.cwd();
9
+ const DEFAULT_TARGET = path.join(os.homedir(), '.claude', 'plugins', 'voc-intelligence');
10
+ const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, '.claude', 'plugins', 'voc-intelligence');
11
+ const WORKSPACE_PLUGINS_ROOT = path.join(WORKSPACE_ROOT, '.claude', 'plugins');
12
+ const MIN_NODE_MAJOR = 18;
13
+
14
+ const EXCLUDED_DIRS = new Set([
15
+ 'node_modules',
16
+ 'memory',
17
+ 'outputs',
18
+ '.tmp',
19
+ '.git',
20
+ '.claude'
21
+ ]);
22
+
23
+ const EXCLUDED_FILES = new Set([
24
+ '.env',
25
+ '.env.local',
26
+ '.npmrc'
27
+ ]);
28
+
29
+ function usage() {
30
+ return [
31
+ 'Claude VOC skill package installer',
32
+ '',
33
+ 'Usage:',
34
+ ' claude-voc install',
35
+ ' claude-voc workspace',
36
+ ' claude-voc install --workspace --smoke',
37
+ ' claude-voc check',
38
+ ' claude-voc smoke',
39
+ ' claude-voc path',
40
+ '',
41
+ 'npx:',
42
+ ' npx @vocmarket/voc-skill install',
43
+ ' npx @vocmarket/voc-skill workspace --smoke',
44
+ '',
45
+ 'Options:',
46
+ ' --workspace Install into ./.claude/plugins/voc-intelligence',
47
+ ' --target <dir> Install into a custom directory',
48
+ ' --force Allow overwriting an existing custom target',
49
+ ' --skip-install Skip npm install after copying files',
50
+ ' --smoke Run sample/preference/MCP smoke checks after install',
51
+ ' --help, -h Show help'
52
+ ].join('\n');
53
+ }
54
+
55
+ function expandHome(value) {
56
+ return value.replace(/^~(?=$|[\\/])/, os.homedir());
57
+ }
58
+
59
+ function parseArgs(argv) {
60
+ const first = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'install';
61
+ const opts = {
62
+ command: first,
63
+ target: DEFAULT_TARGET,
64
+ skipInstall: false,
65
+ smoke: false,
66
+ force: false,
67
+ help: false
68
+ };
69
+
70
+ if (first === 'workspace' || first === 'install-workspace') {
71
+ opts.command = 'install';
72
+ opts.target = WORKSPACE_TARGET;
73
+ }
74
+
75
+ for (let i = first === argv[0] ? 1 : 0; i < argv.length; i++) {
76
+ const token = argv[i];
77
+ if (token === '--target') {
78
+ opts.target = argv[++i];
79
+ } else if (token.startsWith('--target=')) {
80
+ opts.target = token.slice('--target='.length);
81
+ } else if (token === '--workspace') {
82
+ opts.target = WORKSPACE_TARGET;
83
+ } else if (token === '--force') {
84
+ opts.force = true;
85
+ } else if (token === '--skip-install') {
86
+ opts.skipInstall = true;
87
+ } else if (token === '--smoke') {
88
+ opts.smoke = true;
89
+ } else if (token === '--help' || token === '-h') {
90
+ opts.help = true;
91
+ }
92
+ }
93
+
94
+ opts.target = path.resolve(expandHome(opts.target));
95
+ return opts;
96
+ }
97
+
98
+ function run(command, args, cwd) {
99
+ const useCmd = process.platform === 'win32' && command === 'npm';
100
+ const executable = useCmd ? 'cmd.exe' : command;
101
+ const finalArgs = useCmd ? ['/d', '/s', '/c', 'npm', ...args] : args;
102
+ const child = spawnSync(executable, finalArgs, {
103
+ cwd,
104
+ encoding: 'utf8',
105
+ stdio: 'inherit',
106
+ maxBuffer: 1024 * 1024 * 100
107
+ });
108
+ if (child.status !== 0) {
109
+ const detail = child.error ? `: ${child.error.message}` : '';
110
+ throw new Error(`${command} ${args.join(' ')} failed with exit ${child.status}${detail}`);
111
+ }
112
+ }
113
+
114
+ function checkNodeVersion() {
115
+ const major = Number(process.versions.node.split('.')[0]);
116
+ if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
117
+ throw new Error(`Node.js ${MIN_NODE_MAJOR}+ is required. Current version: ${process.version}`);
118
+ }
119
+ }
120
+
121
+ function ensureDir(dirPath) {
122
+ fs.mkdirSync(dirPath, { recursive: true });
123
+ }
124
+
125
+ function isInside(parentDir, childDir) {
126
+ const relative = path.relative(path.resolve(parentDir), path.resolve(childDir));
127
+ return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
128
+ }
129
+
130
+ function isSafeDefaultTarget(targetDir) {
131
+ return path.resolve(targetDir) === path.resolve(DEFAULT_TARGET);
132
+ }
133
+
134
+ function isSafeWorkspaceTarget(targetDir) {
135
+ return isInside(WORKSPACE_PLUGINS_ROOT, targetDir);
136
+ }
137
+
138
+ function canOverwriteTarget(targetDir, opts) {
139
+ return opts.force || isSafeDefaultTarget(targetDir) || isSafeWorkspaceTarget(targetDir);
140
+ }
141
+
142
+ function shouldCopyEntry(entry) {
143
+ if (entry.isDirectory()) return !EXCLUDED_DIRS.has(entry.name);
144
+ if (entry.isFile()) {
145
+ if (EXCLUDED_FILES.has(entry.name)) return false;
146
+ if (entry.name.endsWith('.tgz')) return false;
147
+ return true;
148
+ }
149
+ return false;
150
+ }
151
+
152
+ function copyDirRecursive(srcDir, destDir) {
153
+ ensureDir(destDir);
154
+ let count = 0;
155
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
156
+ if (!shouldCopyEntry(entry)) continue;
157
+ const src = path.join(srcDir, entry.name);
158
+ const dest = path.join(destDir, entry.name);
159
+ if (entry.isDirectory()) {
160
+ count += copyDirRecursive(src, dest);
161
+ } else if (entry.isFile()) {
162
+ ensureDir(path.dirname(dest));
163
+ fs.copyFileSync(src, dest);
164
+ count++;
165
+ }
166
+ }
167
+ return count;
168
+ }
169
+
170
+ function assertInstalled(targetDir) {
171
+ const required = [
172
+ 'package.json',
173
+ 'install.js',
174
+ '.claude-plugin/plugin.json',
175
+ '.mcp.json',
176
+ 'skills/xiaohongshu-trend-intelligence/SKILL.md',
177
+ 'skills/douyin-trend-intelligence/SKILL.md',
178
+ 'skills/voc-issue-pool/SKILL.md',
179
+ 'skills/voc-problem-deep-dive/SKILL.md',
180
+ 'skills/voc-content-plan/SKILL.md',
181
+ 'skills/voc-speaking-script/SKILL.md',
182
+ 'skills/voc-competitor-map/SKILL.md',
183
+ 'skills/voc-business-workflow/SKILL.md',
184
+ 'mcp/src/server.js'
185
+ ];
186
+ const missing = required.filter(rel => !fs.existsSync(path.join(targetDir, rel)));
187
+ if (missing.length) {
188
+ throw new Error(`Missing required files in install target: ${missing.join(', ')}`);
189
+ }
190
+ }
191
+
192
+ function writeMcpConfig(targetDir) {
193
+ const serverPath = path.join(targetDir, 'mcp', 'src', 'server.js');
194
+ const mcpConfig = {
195
+ mcpServers: {
196
+ voc: {
197
+ command: 'node',
198
+ args: [serverPath],
199
+ cwd: targetDir
200
+ }
201
+ }
202
+ };
203
+ fs.writeFileSync(path.join(targetDir, '.mcp.json'), `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
204
+ }
205
+
206
+ function readJsonIfExists(filePath) {
207
+ if (!fs.existsSync(filePath)) return {};
208
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
209
+ }
210
+
211
+ function writeWorkspaceMcpConfig(targetDir) {
212
+ const mcpConfigPath = path.join(WORKSPACE_ROOT, '.mcp.json');
213
+ const existing = readJsonIfExists(mcpConfigPath);
214
+ const mcpConfig = {
215
+ ...existing,
216
+ mcpServers: {
217
+ ...(existing.mcpServers || {}),
218
+ voc: {
219
+ command: 'node',
220
+ args: [path.join(targetDir, 'mcp', 'src', 'server.js')],
221
+ cwd: targetDir
222
+ }
223
+ }
224
+ };
225
+ fs.writeFileSync(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
226
+ }
227
+
228
+ function asForwardSlash(filePath) {
229
+ return path.resolve(filePath).replace(/\\/g, '/');
230
+ }
231
+
232
+ function quotedPath(filePath) {
233
+ return `"${asForwardSlash(filePath)}"`;
234
+ }
235
+
236
+ function workspaceTrendSkillText(options) {
237
+ const trendRunner = path.join(options.targetDir, 'mcp', 'src', 'tools', options.trendTool);
238
+ const preferenceUpdater = path.join(options.targetDir, 'mcp', 'src', 'tools', options.preferenceTool);
239
+ const profilePath = path.join(options.targetDir, 'memory-templates', options.profileFile);
240
+ const defaultMemoryPath = path.join(WORKSPACE_ROOT, 'outputs', options.memoryDir, options.memoryFile);
241
+ const liveLimits = options.platform === '抖音'
242
+ ? '--keyword-limit 1 --videos-per-keyword 1 --max-comment-pages 1'
243
+ : '--keyword-limit 1 --notes-per-keyword 1 --max-comment-pages 1';
244
+
245
+ return [
246
+ '---',
247
+ `name: ${options.skillName}`,
248
+ `description: ${options.description}`,
249
+ 'allowed-tools: Read Write Bash(node *)',
250
+ '---',
251
+ '',
252
+ `# ${options.platform}趋势情报官`,
253
+ '',
254
+ `你是面向业务用户的“${options.platform}趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。`,
255
+ '',
256
+ '## 必须执行',
257
+ '',
258
+ `- 用户要求${options.platform}趋势情报、内容趋势、评论分析、真实采集、live采集或社媒 VOC 时,必须运行本技能包的工具或 MCP,不要只凭经验回答。`,
259
+ '- 用户说“live / 真实采集 / 进入 live / 实时采集 / 抓真实数据”时,必须运行 live 模式工具。不要回复“我不能实时抓取外部数据”。本技能包已经提供本地 VOC 采集入口。',
260
+ '- live 模式返回 `needs_token`、`needs_recharge` 或 `needs_valid_token` 时,必须把工具输出里的 `assistantMessage` 原样转述给用户,尤其不能省略充值/开通链接。',
261
+ '- 如果工具输出里有 `nextActions`,至少转述其中带 `apig-pay` 的充值链接。',
262
+ '- 第一轮只说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。',
263
+ '- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。',
264
+ '',
265
+ '## MANDATORY Live Rule',
266
+ '',
267
+ '- If the user asks for live collection, real collection, or entering live mode, run the local VOC tool in live mode.',
268
+ '- Do not answer that you cannot fetch external data. The local VOC tool is the approved collection entry.',
269
+ '- If the tool returns `needs_token`, `needs_recharge`, or `needs_valid_token`, paste its `assistantMessage` to the user and keep the payment URL visible.',
270
+ '- 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`',
271
+ '',
272
+ '## sample 命令',
273
+ '',
274
+ '```bash',
275
+ `node ${quotedPath(trendRunner)} --collection-mode sample --profile ${quotedPath(profilePath)} --output ${JSON.stringify(options.sampleOutput)} --assistant-message-only`,
276
+ '```',
277
+ '',
278
+ '## live 命令',
279
+ '',
280
+ '```bash',
281
+ `node ${quotedPath(trendRunner)} --collection-mode live --profile ${quotedPath(profilePath)} --output ${JSON.stringify(options.liveOutput)} ${liveLimits} --assistant-message-only`,
282
+ '```',
283
+ '',
284
+ '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
285
+ '',
286
+ '## 偏好更新命令',
287
+ '',
288
+ '```bash',
289
+ `node ${quotedPath(preferenceUpdater)} --message "<用户反馈原文>" --memory ${quotedPath(defaultMemoryPath)} --result-prefix ${options.resultPrefix}`,
290
+ '```',
291
+ '',
292
+ '## 充值链接口径',
293
+ '',
294
+ '没有 token 或余额不足时,用户必须看到:',
295
+ '',
296
+ '```text',
297
+ 'https://app.fmode.cn/dev/apig-pay/?apigid=Vo3ROWEvDy&fun_id=HOkkX72PMF',
298
+ '```',
299
+ '',
300
+ '套餐入口可作为补充:',
301
+ '',
302
+ '```text',
303
+ 'https://app.fmode.cn/dev/apig-pay/#/workshop/99',
304
+ '```',
305
+ ''
306
+ ].join('\n');
307
+ }
308
+
309
+ function workspaceSkillText(targetDir) {
310
+ return workspaceTrendSkillText({
311
+ targetDir,
312
+ skillName: 'xiaohongshu-trend-intelligence',
313
+ platform: '小红书',
314
+ description: 'Build Xiaohongshu trend intelligence reports. Use when the user asks for 小红书趋势情报、行业趋势、内容选题、销售话术、真实采集、live采集、用户顾虑 or social VOC analysis.',
315
+ trendTool: 'xiaohongshu-trend-run.js',
316
+ preferenceTool: 'xiaohongshu-preference-update.js',
317
+ profileFile: 'xiaohongshu-trend-profile.json',
318
+ memoryDir: 'claude-code-xhs-memory',
319
+ memoryFile: 'xiaohongshu-trend-memory.json',
320
+ sampleOutput: 'outputs/claude-code-xhs-sample',
321
+ liveOutput: 'outputs/claude-code-xhs-live',
322
+ resultPrefix: 'XHS_PREF_RESULT'
323
+ });
324
+ const trendRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'xiaohongshu-trend-run.js');
325
+ const preferenceUpdater = path.join(targetDir, 'mcp', 'src', 'tools', 'xiaohongshu-preference-update.js');
326
+ const profilePath = path.join(targetDir, 'memory-templates', 'xiaohongshu-trend-profile.json');
327
+ const defaultMemoryPath = path.join(WORKSPACE_ROOT, 'outputs', 'claude-code-xhs-memory', 'xiaohongshu-trend-memory.json');
328
+
329
+ return [
330
+ '---',
331
+ 'name: xiaohongshu-trend-intelligence',
332
+ 'description: Build Xiaohongshu trend intelligence reports from industry direction, audience, keywords, notes, and comments. Use when the user asks for 小红书趋势情报、行业趋势、女性客户消费决策、设计元素洞察、家装全屋定制趋势、内容选题 or social VOC analysis.',
333
+ 'allowed-tools: Read Write Bash(node *)',
334
+ '---',
335
+ '',
336
+ '# 小红书趋势情报官',
337
+ '',
338
+ '你是面向业务用户的“小红书趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。',
339
+ '',
340
+ '## 必须执行',
341
+ '',
342
+ '- 当用户要求做小红书趋势情报、行业趋势、女性客户消费决策、家装全屋定制趋势、设计元素洞察或内容选题时,优先运行下面的 sample 工具命令。',
343
+ '- 不要只凭经验回答,也不要只返回文件路径。',
344
+ '- 工具输出里会有 `assistantMessage`,请把它的正文直接发给用户。',
345
+ '- 第一轮只能说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。',
346
+ '- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。',
347
+ '',
348
+ '## sample 趋势情报命令',
349
+ '',
350
+ '```bash',
351
+ `node ${quotedPath(trendRunner)} --collection-mode sample --profile ${quotedPath(profilePath)} --output "outputs/claude-code-xhs-sample" --assistant-message-only`,
352
+ '```',
353
+ '',
354
+ '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
355
+ '',
356
+ '## 偏好更新命令',
357
+ '',
358
+ '```bash',
359
+ `node ${quotedPath(preferenceUpdater)} --message "<用户反馈原文>" --memory ${quotedPath(defaultMemoryPath)} --result-prefix XHS_PREF_RESULT`,
360
+ '```',
361
+ '',
362
+ '## 推荐用户启动话术',
363
+ '',
364
+ '```text',
365
+ '帮我做一份家装全屋定制行业的小红书趋势情报。',
366
+ '先用演示样例跑通流程,不要真实采集。',
367
+ '在聊天里给我第一轮样本观察和待确认问题。',
368
+ '```',
369
+ ''
370
+ ].join('\n');
371
+ }
372
+
373
+ function workspaceDouyinSkillText(targetDir) {
374
+ return workspaceTrendSkillText({
375
+ targetDir,
376
+ skillName: 'douyin-trend-intelligence',
377
+ platform: '抖音',
378
+ description: 'Build Douyin trend intelligence reports. Use when the user asks for 抖音趋势情报、视频评论、口播选题、短视频开头、内容趋势、真实采集、live采集、用户顾虑 or social VOC analysis.',
379
+ trendTool: 'douyin-trend-run.js',
380
+ preferenceTool: 'douyin-preference-update.js',
381
+ profileFile: 'douyin-trend-profile.json',
382
+ memoryDir: 'claude-code-douyin-memory',
383
+ memoryFile: 'douyin-trend-memory.json',
384
+ sampleOutput: 'outputs/claude-code-douyin-sample',
385
+ liveOutput: 'outputs/claude-code-douyin-live',
386
+ resultPrefix: 'DOUYIN_PREF_RESULT'
387
+ });
388
+ const trendRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'douyin-trend-run.js');
389
+ const preferenceUpdater = path.join(targetDir, 'mcp', 'src', 'tools', 'douyin-preference-update.js');
390
+ const profilePath = path.join(targetDir, 'memory-templates', 'douyin-trend-profile.json');
391
+ const defaultMemoryPath = path.join(WORKSPACE_ROOT, 'outputs', 'claude-code-douyin-memory', 'douyin-trend-memory.json');
392
+
393
+ return [
394
+ '---',
395
+ 'name: douyin-trend-intelligence',
396
+ 'description: Build Douyin trend intelligence reports from industry direction, audience, keywords, videos, and comments. Use when the user asks for 抖音趋势情报、视频评论、口播选题、短视频开头、内容趋势 or social VOC analysis.',
397
+ 'allowed-tools: Read Write Bash(node *)',
398
+ '---',
399
+ '',
400
+ '# 抖音趋势情报官',
401
+ '',
402
+ '你是面向业务用户的“抖音趋势情报官”。用户要的是第一轮样本观察、机会假设和待确认问题,不是技术日志。',
403
+ '',
404
+ '## 必须执行',
405
+ '',
406
+ '- 当用户要求做抖音趋势情报、视频评论、口播选题、短视频开头或内容趋势时,优先运行下面的 sample 工具命令。',
407
+ '- 不要只凭经验回答,也不要只返回文件路径。',
408
+ '- 工具输出里会有 `assistantMessage`,请把它的正文直接发给用户。',
409
+ '- 第一轮只能说“初步判断 / 机会假设 / 待校准”,不要说成最终结论。',
410
+ '- 后续用户说“保留、不要、更偏、这个不准”等反馈时,运行偏好更新命令沉淀记忆。',
411
+ '',
412
+ '## sample 趋势情报命令',
413
+ '',
414
+ '```bash',
415
+ `node ${quotedPath(trendRunner)} --collection-mode sample --profile ${quotedPath(profilePath)} --output "outputs/claude-code-douyin-sample" --assistant-message-only`,
416
+ '```',
417
+ '',
418
+ '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
419
+ '',
420
+ '## 偏好更新命令',
421
+ '',
422
+ '```bash',
423
+ `node ${quotedPath(preferenceUpdater)} --message "<用户反馈原文>" --memory ${quotedPath(defaultMemoryPath)} --result-prefix DOUYIN_PREF_RESULT`,
424
+ '```',
425
+ '',
426
+ '## 推荐用户启动话术',
427
+ '',
428
+ '```text',
429
+ '帮我做一份家装全屋定制行业的抖音趋势情报。',
430
+ '先用演示样例跑通流程,不要真实采集。',
431
+ '在聊天里给我第一轮样本观察和待确认问题。',
432
+ '```',
433
+ ''
434
+ ].join('\n');
435
+ }
436
+
437
+ function workspaceProblemDeepDiveSkillText(targetDir) {
438
+ const deepDiveRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-problem-deep-dive-run.js');
439
+ return [
440
+ '---',
441
+ 'name: voc-problem-deep-dive',
442
+ '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深挖.',
443
+ 'allowed-tools: Read Write Bash(node *)',
444
+ '---',
445
+ '',
446
+ '# 单点 VOC 深挖官',
447
+ '',
448
+ '你是面向老板和门店经营者的“单点 VOC 深挖官”。用户不需要学习提示词。用户只要说“这个问题继续挖”“老板怎么解决”“这个差评背后是什么”,你就要把真实用户声音转成可执行经营动作。',
449
+ '',
450
+ '## 必须执行',
451
+ '',
452
+ '- 当用户说“继续挖、深入分析、老板怎么解决、这个问题怎么办、怎么改、怎么转成内容、单点 VOC”时,必须运行单点深挖工具。',
453
+ '- 不要输出提示词教学,不要让用户自己设计分析维度。',
454
+ '- 输出必须站在老板视角:这件事影响新客、复购、客单、口碑,还是现场效率。',
455
+ '- 每次只深挖一个问题点。如果用户给了多个问题,先选择最影响转化的一个。',
456
+ '- 如果上一轮趋势报告里有证据样本或高赞评论,要把相关评论作为 evidence 传入工具。',
457
+ '- 要使用记忆能力:用户说“这个动作不适合”“我们已经试过”“这个有效”“下次更偏内容/门店动作”时,把原话作为 `feedback` 传入;如果能明确识别,也同步传 `blockedActions`、`validatedActions`、`rejectedActions` 或 `preferredActions`。',
458
+ '- 后续多轮迭代时要带上 `project`/`brand`/`store`、`industry`、`scenario`、`audience`。未显式传 `memory` 时,工具会按这些字段自动隔离记忆,避免不同行业、不同客户串味。',
459
+ '- 如果用户没有指定记忆文件,不要强行要求用户理解路径;让工具使用默认分桶记忆即可。只有在同一个客户需要固定沉淀时,才显式传同一个 `memory` 路径。',
460
+ '',
461
+ '## 命令',
462
+ '',
463
+ '```bash',
464
+ `node ${quotedPath(deepDiveRunner)} --issue "排队" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --assistant-message-only`,
465
+ '```',
466
+ '',
467
+ '## 带记忆的多轮迭代',
468
+ '',
469
+ '```bash',
470
+ `node ${quotedPath(deepDiveRunner)} --issue "排队" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --memory "outputs/voc-problem-memory.json" --feedback "老板不想做等位饮品,更想先改评论区回复和预点单" --blocked-actions "等位饮品" --preferred-actions "评论区回复,预点单" --assistant-message-only`,
471
+ '```',
472
+ '',
473
+ '自然语言反馈也可以直接传入 `feedback`,工具会尝试自动提取偏好和屏蔽动作:',
474
+ '',
475
+ '```bash',
476
+ `node ${quotedPath(deepDiveRunner)} --issue "排队" --project "样板门店A" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --feedback "这个店不想做等位饮品,更想先改评论区回复和预点单" --assistant-message-only`,
477
+ '```',
478
+ '',
479
+ '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
480
+ ''
481
+ ].join('\n');
482
+ }
483
+
484
+ function workspaceIssuePoolSkillText(targetDir) {
485
+ const issuePoolRunner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-issue-pool-run.js');
486
+ return [
487
+ '---',
488
+ 'name: voc-issue-pool',
489
+ '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问题管理、问题优先级、把评论整理成问题.',
490
+ 'allowed-tools: Read Write Bash(node *)',
491
+ '---',
492
+ '',
493
+ '# VOC 问题池官',
494
+ '',
495
+ '你是面向老板、门店经营者和营销负责人的 VOC 问题池官。用户不需要知道分类法,也不需要学习提示词。用户只要说“这些评论里哪些问题最重要”“先改哪个”“整理成问题池”,你就把真实用户声音整理成可排序、可跟进、可继续深挖的问题列表。',
496
+ '',
497
+ '## 必须执行',
498
+ '',
499
+ '- 当用户说“问题池、问题清单、用户吐槽什么、哪些问题最影响生意、先改哪个、VOC 问题管理、把评论整理成问题”时,优先运行 `voc_issue_pool_run`。',
500
+ '- 如果上一轮小红书/抖音报告里有证据样本或高赞评论,把相关评论作为 `evidence`、`evidenceText` 或 `reportPath` 传入。',
501
+ '- 输出必须站在业务视角:这个问题影响新客、复购、客单、口碑、内容信任,还是现场效率。',
502
+ '- 不要只做词频统计。每个问题都要包含证据样本、影响环节、建议动作和下一步。',
503
+ '- 如果用户说某个问题已解决、验证中、暂缓,要通过 `resolvedIssues`、`validatingIssues`、`blockedIssues` 或 `statusUpdates` 更新问题状态。',
504
+ '- 后续深挖时,引导用户直接说“继续深挖「问题名」”,然后转入 `voc-problem-deep-dive`。',
505
+ '',
506
+ '## 命令',
507
+ '',
508
+ '```bash',
509
+ `node ${quotedPath(issuePoolRunner)} --project "样板门店A" --industry "本地生活餐饮" --scenario "门店经营" --audience "到店顾客" --evidence "排队久,但是不知道值不值得等。,人均有点高,第一次来不知道怎么点不踩雷。,服务忙起来没人理。" --assistant-message-only`,
510
+ '```',
511
+ '',
512
+ '带报告文件:',
513
+ '',
514
+ '```bash',
515
+ `node ${quotedPath(issuePoolRunner)} --project "样板门店A" --industry "本地生活餐饮" --report "outputs/douyin-trend-report.md" --assistant-message-only`,
516
+ '```',
517
+ '',
518
+ '运行后把命令输出正文直接发给用户。不要展示 JSON、summary、data、files 或调试日志。',
519
+ ''
520
+ ].join('\n');
521
+ }
522
+
523
+ function workspaceContentPlanSkillText(targetDir) {
524
+ const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-content-plan-run.js');
525
+ return [
526
+ '---',
527
+ 'name: voc-content-plan',
528
+ 'description: Turn VOC issues into a 7-day content plan with short-video topics and speaking scripts. Use when the user asks 下周账号发什么、口播脚本、短视频选题、把用户问题变成内容、7天内容计划.',
529
+ 'allowed-tools: Read Write Bash(node *)',
530
+ '---',
531
+ '',
532
+ '# VOC 内容选题官',
533
+ '',
534
+ '当用户说“下周发什么、7 天选题、口播脚本、短视频脚本、账号内容、把问题变成内容”时,运行本工具。',
535
+ '',
536
+ '必须把内容绑定到 VOC 用户问题,不要输出空泛爆款标题。不要覆盖创始人故事。',
537
+ '',
538
+ '## 命令',
539
+ '',
540
+ '```bash',
541
+ `node ${quotedPath(runner)} --brand "样板门店A" --industry "本地生活餐饮" --issues "第一次来怕踩雷;觉得人均贵;怕排队久" --assistant-message-only`,
542
+ '```',
543
+ '',
544
+ '运行后把正文直接发给用户,不要展示 JSON。',
545
+ ''
546
+ ].join('\n');
547
+ }
548
+
549
+ function workspaceSpeakingScriptSkillText(targetDir) {
550
+ const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-speaking-script-run.js');
551
+ return [
552
+ '---',
553
+ 'name: voc-speaking-script',
554
+ '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进入脚本共创、改稿、定稿、口播稿、提词器、开头更狠、老板视角、少讲概念.',
555
+ 'allowed-tools: Read Write Bash(node *)',
556
+ '---',
557
+ '',
558
+ '# VOC 口播脚本共创',
559
+ '',
560
+ '当用户从 7 天内容计划里选择某条选题,或说“进入脚本共创、改稿、定稿、提词器版、开头更狠、老板视角”时,运行本工具。',
561
+ '',
562
+ '口播必须来自 VOC 用户问题、趋势证据、问题池 Top 问题、单点深挖动作或竞品评论差异。没有真实证据时要标注低证据风险。',
563
+ '',
564
+ '## 命令',
565
+ '',
566
+ '```bash',
567
+ `node ${quotedPath(runner)} --brand "样板门店A" --industry "本地生活餐饮" --topic "第一次来怎么点不踩雷" --user-issue "第一次来怕点错" --evidence "第一次来不知道怎么点,怕踩雷" --assistant-message-only`,
568
+ '```',
569
+ '',
570
+ '改稿:',
571
+ '',
572
+ '```bash',
573
+ `node ${quotedPath(runner)} --brand "样板门店A" --topic "第一次来怎么点不踩雷" --user-issue "第一次来怕点错" --evidence "第一次来不知道怎么点,怕踩雷" --feedback "开头更狠一点,老板视角,少讲概念,多给具体场景" --assistant-message-only`,
574
+ '```',
575
+ '',
576
+ '定稿时传 `--finalize true`,工具会写入脚本记忆。运行后把正文直接发给用户,不要展示 JSON。',
577
+ ''
578
+ ].join('\n');
579
+ }
580
+
581
+ function workspaceCompetitorMapSkillText(targetDir) {
582
+ const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-competitor-map-run.js');
583
+ return [
584
+ '---',
585
+ 'name: voc-competitor-map',
586
+ 'description: Build a competitor map and differentiated opportunity report. Use when the user asks 看竞品、竞品分析、错位竞争、别人为什么吸引顾客、我不知道竞品是谁.',
587
+ 'allowed-tools: Read Write Bash(node *)',
588
+ '---',
589
+ '',
590
+ '# VOC 竞品图谱官',
591
+ '',
592
+ '当用户说“看竞品、竞品分析、错位竞争、别人怎么做、我不知道竞品是谁”时,运行本工具。',
593
+ '',
594
+ '如果用户没有明确竞品,先按同城、同品类、同价格带、同消费场景、平台声量生成候选竞品类型。不要覆盖创始人故事。',
595
+ '',
596
+ '## 命令',
597
+ '',
598
+ '```bash',
599
+ `node ${quotedPath(runner)} --brand "样板门店A" --city "南昌" --category "餐饮" --assistant-message-only`,
600
+ '```',
601
+ '',
602
+ '运行后把正文直接发给用户,不要展示 JSON。',
603
+ ''
604
+ ].join('\n');
605
+ }
606
+
607
+ function workspaceBusinessWorkflowSkillText(targetDir) {
608
+ const runner = path.join(targetDir, 'mcp', 'src', 'tools', 'voc-business-workflow-run.js');
609
+ return [
610
+ '---',
611
+ 'name: voc-business-workflow',
612
+ '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闭环、经营诊断.',
613
+ 'allowed-tools: Read Write Bash(node *)',
614
+ '---',
615
+ '',
616
+ '# VOC经营闭环',
617
+ '',
618
+ '当用户想一次拿到“看市场、看问题、找动作、做内容”的结果时,运行本工具。不要让用户理解 MCP、schema 或多个工具名。',
619
+ '',
620
+ '## 命令',
621
+ '',
622
+ '```bash',
623
+ `node ${quotedPath(runner)} --brand "样板门店A" --industry "本地生活餐饮" --platform douyin --collection-mode sample --keywords "南昌菜,第一次来怎么点,排队" --assistant-message-only`,
624
+ '```',
625
+ '',
626
+ '真实采集时把 `--collection-mode sample` 换成 `--collection-mode live`。如果工具提示没有 token、余额不足或没有样本,直接把工具正文发给用户,并保留充值/降级建议。',
627
+ '',
628
+ '## 交付物',
629
+ '',
630
+ '必须包含五段:市场和用户声音、先改哪个问题、Top问题怎么解决、下周账号发什么、第一条口播稿。',
631
+ ''
632
+ ].join('\n');
633
+ }
634
+
635
+ function writeWorkspaceSkillEntries(targetDir) {
636
+ const xhsSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'xiaohongshu-trend-intelligence');
637
+ const douyinSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'douyin-trend-intelligence');
638
+ const issuePoolSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-issue-pool');
639
+ const deepDiveSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-problem-deep-dive');
640
+ const contentPlanSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-content-plan');
641
+ const speakingScriptSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-speaking-script');
642
+ const competitorMapSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-competitor-map');
643
+ const businessWorkflowSkillDir = path.join(WORKSPACE_ROOT, '.claude', 'skills', 'voc-business-workflow');
644
+ ensureDir(xhsSkillDir);
645
+ ensureDir(douyinSkillDir);
646
+ ensureDir(issuePoolSkillDir);
647
+ ensureDir(deepDiveSkillDir);
648
+ ensureDir(contentPlanSkillDir);
649
+ ensureDir(speakingScriptSkillDir);
650
+ ensureDir(competitorMapSkillDir);
651
+ ensureDir(businessWorkflowSkillDir);
652
+ fs.writeFileSync(path.join(xhsSkillDir, 'SKILL.md'), workspaceSkillText(targetDir), 'utf8');
653
+ fs.writeFileSync(path.join(douyinSkillDir, 'SKILL.md'), workspaceDouyinSkillText(targetDir), 'utf8');
654
+ fs.writeFileSync(path.join(issuePoolSkillDir, 'SKILL.md'), workspaceIssuePoolSkillText(targetDir), 'utf8');
655
+ fs.writeFileSync(path.join(deepDiveSkillDir, 'SKILL.md'), workspaceProblemDeepDiveSkillText(targetDir), 'utf8');
656
+ fs.writeFileSync(path.join(contentPlanSkillDir, 'SKILL.md'), workspaceContentPlanSkillText(targetDir), 'utf8');
657
+ fs.writeFileSync(path.join(speakingScriptSkillDir, 'SKILL.md'), workspaceSpeakingScriptSkillText(targetDir), 'utf8');
658
+ fs.writeFileSync(path.join(competitorMapSkillDir, 'SKILL.md'), workspaceCompetitorMapSkillText(targetDir), 'utf8');
659
+ fs.writeFileSync(path.join(businessWorkflowSkillDir, 'SKILL.md'), workspaceBusinessWorkflowSkillText(targetDir), 'utf8');
660
+ }
661
+
662
+ function writeWorkspaceActivation(targetDir) {
663
+ if (!isSafeWorkspaceTarget(targetDir)) return;
664
+ writeWorkspaceMcpConfig(targetDir);
665
+ writeWorkspaceSkillEntries(targetDir);
666
+ console.log('Wrote workspace MCP config: .\\.mcp.json');
667
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\xiaohongshu-trend-intelligence\\SKILL.md');
668
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\douyin-trend-intelligence\\SKILL.md');
669
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-issue-pool\\SKILL.md');
670
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-problem-deep-dive\\SKILL.md');
671
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-content-plan\\SKILL.md');
672
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-speaking-script\\SKILL.md');
673
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-competitor-map\\SKILL.md');
674
+ console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-business-workflow\\SKILL.md');
675
+ }
676
+
677
+ function printNextSteps(targetDir) {
678
+ const workspaceMode = isSafeWorkspaceTarget(targetDir);
679
+ console.log('');
680
+ console.log('Install complete.');
681
+ console.log('');
682
+ if (workspaceMode) {
683
+ console.log('VSCode workspace mode is ready.');
684
+ console.log('');
685
+ console.log('Generated project files:');
686
+ console.log(' .\\.mcp.json');
687
+ console.log(' .\\.claude\\skills\\xiaohongshu-trend-intelligence\\SKILL.md');
688
+ console.log(' .\\.claude\\skills\\douyin-trend-intelligence\\SKILL.md');
689
+ console.log(' .\\.claude\\skills\\voc-issue-pool\\SKILL.md');
690
+ console.log(' .\\.claude\\skills\\voc-problem-deep-dive\\SKILL.md');
691
+ console.log(' .\\.claude\\skills\\voc-content-plan\\SKILL.md');
692
+ console.log(' .\\.claude\\skills\\voc-speaking-script\\SKILL.md');
693
+ console.log(' .\\.claude\\skills\\voc-competitor-map\\SKILL.md');
694
+ console.log(' .\\.claude\\skills\\voc-business-workflow\\SKILL.md');
695
+ console.log(' .\\.claude\\plugins\\voc-intelligence');
696
+ console.log('');
697
+ console.log('Restart the VSCode Claude Code session if it was already open.');
698
+ } else {
699
+ console.log('Claude Code CLI launch command:');
700
+ console.log(` claude --plugin-dir "${targetDir}"`);
701
+ }
702
+ console.log('');
703
+ console.log('Try this prompt in Claude Code:');
704
+ console.log(' 帮我看一下南昌餐饮最近顾客在关心什么,并告诉我先改哪里、下周发什么。');
705
+ console.log('');
706
+ console.log('Or try:');
707
+ console.log(' 帮我做一份家装全屋定制行业的小红书趋势情报。');
708
+ console.log(' 先用演示样例跑通流程,不要真实采集。');
709
+ console.log(' 在聊天里给我第一轮样本观察和待确认问题。');
710
+ console.log('');
711
+ console.log('Or try:');
712
+ console.log(' 帮我做一份家装全屋定制行业的抖音趋势情报。');
713
+ console.log(' 先用演示样例跑通流程,不要真实采集。');
714
+ console.log(' 在聊天里给我第一轮样本观察和待确认问题。');
715
+ console.log('');
716
+ console.log(`Install target: ${targetDir}`);
717
+ }
718
+
719
+ function install(opts) {
720
+ checkNodeVersion();
721
+ if (path.resolve(SOURCE_ROOT) !== path.resolve(opts.target)) {
722
+ if (fs.existsSync(opts.target)) {
723
+ if (!canOverwriteTarget(opts.target, opts)) {
724
+ throw new Error(`Target already exists. Use --force to overwrite custom target: ${opts.target}`);
725
+ }
726
+ fs.rmSync(opts.target, { recursive: true, force: true });
727
+ }
728
+ ensureDir(opts.target);
729
+ const copied = copyDirRecursive(SOURCE_ROOT, opts.target);
730
+ console.log(`Copied skill package files: ${copied}`);
731
+ }
732
+
733
+ assertInstalled(opts.target);
734
+ writeMcpConfig(opts.target);
735
+ writeWorkspaceActivation(opts.target);
736
+
737
+ if (!opts.skipInstall) {
738
+ console.log('Installing runtime dependencies...');
739
+ run('npm', ['install', '--omit=dev', '--ignore-scripts'], opts.target);
740
+ }
741
+
742
+ if (opts.smoke) {
743
+ console.log('Running install smoke checks...');
744
+ run(process.execPath, ['install.js', '--smoke', '--skip-install', '--no-next'], opts.target);
745
+ } else {
746
+ run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
747
+ }
748
+
749
+ printNextSteps(opts.target);
750
+ }
751
+
752
+ function check(opts) {
753
+ assertInstalled(opts.target);
754
+ run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
755
+ }
756
+
757
+ function smoke(opts) {
758
+ assertInstalled(opts.target);
759
+ run(process.execPath, ['install.js', '--smoke', '--skip-install', '--no-next'], opts.target);
760
+ }
761
+
762
+ function printPath(opts) {
763
+ console.log(opts.target);
764
+ if (isSafeWorkspaceTarget(opts.target)) {
765
+ console.log('.\\.claude\\plugins\\voc-intelligence');
766
+ } else {
767
+ console.log(`claude --plugin-dir "${opts.target}"`);
768
+ }
769
+ }
770
+
771
+ function main() {
772
+ const opts = parseArgs(process.argv.slice(2));
773
+ if (opts.help || opts.command === 'help') {
774
+ console.log(usage());
775
+ return;
776
+ }
777
+ if (opts.command === 'install') return install(opts);
778
+ if (opts.command === 'check') return check(opts);
779
+ if (opts.command === 'smoke') return smoke(opts);
780
+ if (opts.command === 'path') return printPath(opts);
781
+ throw new Error(`Unknown command: ${opts.command}\n\n${usage()}`);
782
+ }
783
+
784
+ try {
785
+ main();
786
+ } catch (error) {
787
+ console.error('');
788
+ console.error(`claude-voc failed: ${error.message}`);
789
+ process.exit(1);
790
+ }