@vocmarket/voc-skill 0.3.30 → 0.3.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "voc-intelligence",
3
3
  "description": "VOC intelligence skills for Claude Code: end-to-end VOC business workflows, Xiaohongshu and Douyin trend reports, live social VOC collection, Fmode Doubao image analysis, token/recharge checks, evidence-card outputs, prioritized issue pools, single-problem deep dives, competitor maps, 7-day content plans, VOC-backed speaking script co-creation, boss/operator action plans, and multi-turn memory.",
4
- "version": "0.3.30",
4
+ "version": "0.3.31",
5
5
  "author": {
6
6
  "name": "nkkj-BrainHack"
7
7
  }
package/bin/claude-voc.js CHANGED
@@ -4,12 +4,17 @@ const os = require('os');
4
4
  const path = require('path');
5
5
  const { spawnSync } = require('child_process');
6
6
  const { ensureActivated } = require('../mcp/src/core/activation');
7
+ const { readNewApiToken } = require('../mcp/src/core/credentials');
7
8
 
8
9
  const SOURCE_ROOT = path.resolve(__dirname, '..');
9
10
  const WORKSPACE_ROOT = process.cwd();
10
11
  const DEFAULT_TARGET = path.join(os.homedir(), '.claude', 'plugins', 'voc-intelligence');
11
12
  const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, '.claude', 'plugins', 'voc-intelligence');
12
13
  const WORKSPACE_PLUGINS_ROOT = path.join(WORKSPACE_ROOT, '.claude', 'plugins');
14
+ const CODEX_PROJECT_TARGET = path.join(WORKSPACE_ROOT, '.codex', 'plugins', 'voc-intelligence');
15
+ const CODEX_GLOBAL_TARGET = path.join(os.homedir(), '.codex', 'plugins', 'voc-intelligence');
16
+ const PORTABLE_WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, '.vocmarket', 'plugins', 'voc-intelligence');
17
+ const VOCMARKET_CREDENTIALS = path.join(os.homedir(), '.vocmarket', 'credentials.json');
13
18
  const MIN_NODE_MAJOR = 18;
14
19
 
15
20
  const EXCLUDED_DIRS = new Set([
@@ -35,7 +40,7 @@ function usage() {
35
40
  'Usage:',
36
41
  ' claude-voc install',
37
42
  ' claude-voc workspace',
38
- ' claude-voc workspace --activate',
43
+ ' claude-voc workspace --activate --channel <name>',
39
44
  ' claude-voc install --workspace --smoke',
40
45
  ' claude-voc check',
41
46
  ' claude-voc smoke',
@@ -54,7 +59,8 @@ function usage() {
54
59
  ' --skip-install Skip npm install after copying files',
55
60
  ' --smoke Run sample/preference/MCP smoke checks after install',
56
61
  ' --activate 安装后按 token 状态激活:无 token 弹付费/登录页,有 token 直接放行(外部 AI IDE 专用,不影响 --smoke)',
57
- ' --channel <name> 付费归因渠道名(claude-code/codex/workbuddy)',
62
+ ' --channel <name> 安装渠道(claude-code/codex/workbuddy/fmode-studio)',
63
+ ' --scope <name> Codex 安装范围(project/global,默认 project)',
58
64
  ' --help, -h Show help'
59
65
  ].join('\n');
60
66
  }
@@ -72,6 +78,9 @@ function parseArgs(argv) {
72
78
  smoke: false,
73
79
  activate: false,
74
80
  channel: 'external',
81
+ scope: 'project',
82
+ targetExplicit: false,
83
+ workspaceRequested: first === 'workspace' || first === 'install-workspace',
75
84
  force: false,
76
85
  help: false
77
86
  };
@@ -85,10 +94,13 @@ function parseArgs(argv) {
85
94
  const token = argv[i];
86
95
  if (token === '--target') {
87
96
  opts.target = argv[++i];
97
+ opts.targetExplicit = true;
88
98
  } else if (token.startsWith('--target=')) {
89
99
  opts.target = token.slice('--target='.length);
100
+ opts.targetExplicit = true;
90
101
  } else if (token === '--workspace') {
91
102
  opts.target = WORKSPACE_TARGET;
103
+ opts.workspaceRequested = true;
92
104
  } else if (token === '--force') {
93
105
  opts.force = true;
94
106
  } else if (token === '--skip-install') {
@@ -101,15 +113,48 @@ function parseArgs(argv) {
101
113
  opts.channel = argv[++i];
102
114
  } else if (token.startsWith('--channel=')) {
103
115
  opts.channel = token.slice('--channel='.length);
116
+ } else if (token === '--scope') {
117
+ opts.scope = argv[++i];
118
+ } else if (token.startsWith('--scope=')) {
119
+ opts.scope = token.slice('--scope='.length);
104
120
  } else if (token === '--help' || token === '-h') {
105
121
  opts.help = true;
106
122
  }
107
123
  }
108
124
 
125
+ opts.channel = normalizeInstallChannel(opts.channel);
126
+ opts.scope = normalizeInstallScope(opts.channel, opts.scope);
127
+ if (opts.workspaceRequested && !opts.targetExplicit) {
128
+ opts.target = defaultTargetFor(opts.channel, opts.scope);
129
+ }
109
130
  opts.target = path.resolve(expandHome(opts.target));
110
131
  return opts;
111
132
  }
112
133
 
134
+ function normalizeInstallChannel(value) {
135
+ const channel = String(value || 'claude-code').trim().toLowerCase();
136
+ if (channel === 'external') return 'claude-code';
137
+ if (!['claude-code', 'codex', 'workbuddy', 'fmode-studio'].includes(channel)) {
138
+ throw new Error(`Unsupported install channel: ${channel}`);
139
+ }
140
+ return channel;
141
+ }
142
+
143
+ function normalizeInstallScope(channel, value) {
144
+ if (channel !== 'codex') return 'project';
145
+ const scope = String(value || 'project').trim().toLowerCase();
146
+ if (!['project', 'global'].includes(scope)) {
147
+ throw new Error(`Unsupported Codex install scope: ${scope}`);
148
+ }
149
+ return scope;
150
+ }
151
+
152
+ function defaultTargetFor(channel, scope) {
153
+ if (channel === 'codex') return scope === 'global' ? CODEX_GLOBAL_TARGET : CODEX_PROJECT_TARGET;
154
+ if (channel === 'workbuddy') return PORTABLE_WORKSPACE_TARGET;
155
+ return WORKSPACE_TARGET;
156
+ }
157
+
113
158
  function run(command, args, cwd) {
114
159
  const useCmd = process.platform === 'win32' && command === 'npm';
115
160
  const executable = useCmd ? 'cmd.exe' : command;
@@ -184,8 +229,13 @@ function isSafeWorkspaceTarget(targetDir) {
184
229
  return isInside(WORKSPACE_PLUGINS_ROOT, targetDir);
185
230
  }
186
231
 
232
+ function isSafeManagedTarget(targetDir) {
233
+ return [WORKSPACE_TARGET, CODEX_PROJECT_TARGET, CODEX_GLOBAL_TARGET, PORTABLE_WORKSPACE_TARGET]
234
+ .some(candidate => path.resolve(candidate) === path.resolve(targetDir));
235
+ }
236
+
187
237
  function canOverwriteTarget(targetDir, opts) {
188
- return opts.force || isSafeDefaultTarget(targetDir) || isSafeWorkspaceTarget(targetDir);
238
+ return opts.force || isSafeDefaultTarget(targetDir) || isSafeWorkspaceTarget(targetDir) || isSafeManagedTarget(targetDir);
189
239
  }
190
240
 
191
241
  function shouldCopyEntry(entry) {
@@ -262,9 +312,8 @@ function readJsonIfExists(filePath) {
262
312
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
263
313
  }
264
314
 
265
- function writeWorkspaceMcpConfig(targetDir) {
266
- const mcpConfigPath = path.join(WORKSPACE_ROOT, '.mcp.json');
267
- const existing = readJsonIfExists(mcpConfigPath);
315
+ function writeJsonMcpConfig(configPath, targetDir) {
316
+ const existing = readJsonIfExists(configPath);
268
317
  const mcpConfig = {
269
318
  ...existing,
270
319
  mcpServers: {
@@ -276,7 +325,99 @@ function writeWorkspaceMcpConfig(targetDir) {
276
325
  }
277
326
  }
278
327
  };
279
- fs.writeFileSync(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
328
+ ensureDir(path.dirname(configPath));
329
+ fs.writeFileSync(configPath, `${JSON.stringify(mcpConfig, null, 2)}\n`, 'utf8');
330
+ }
331
+
332
+ function writeWorkspaceMcpConfig(targetDir) {
333
+ const mcpConfigPath = path.join(WORKSPACE_ROOT, '.mcp.json');
334
+ writeJsonMcpConfig(mcpConfigPath, targetDir);
335
+ }
336
+
337
+ function tomlString(value) {
338
+ return JSON.stringify(String(value));
339
+ }
340
+
341
+ function upsertCodexProjectMcp(targetDir, workspaceRoot = WORKSPACE_ROOT) {
342
+ const configPath = path.join(workspaceRoot, '.codex', 'config.toml');
343
+ const original = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
344
+ const lines = original.replace(/\r\n/g, '\n').split('\n');
345
+ const kept = [];
346
+ let skipping = false;
347
+ for (const line of lines) {
348
+ const table = line.match(/^\s*\[([^\]]+)]\s*(?:#.*)?$/);
349
+ if (table) {
350
+ skipping = table[1] === 'mcp_servers.voc' || table[1].startsWith('mcp_servers.voc.');
351
+ }
352
+ if (!skipping) kept.push(line);
353
+ }
354
+ while (kept.length && !kept[kept.length - 1].trim()) kept.pop();
355
+ const serverPath = path.join(targetDir, 'mcp', 'src', 'server.js');
356
+ const block = [
357
+ '[mcp_servers.voc]',
358
+ 'command = "node"',
359
+ `args = [${tomlString(serverPath)}]`,
360
+ `cwd = ${tomlString(targetDir)}`
361
+ ];
362
+ ensureDir(path.dirname(configPath));
363
+ fs.writeFileSync(configPath, `${kept.concat(kept.length ? [''] : [], block).join('\n')}\n`, 'utf8');
364
+ return configPath;
365
+ }
366
+
367
+ function runCapture(command, args, cwd = WORKSPACE_ROOT) {
368
+ const useCmd = process.platform === 'win32';
369
+ const executable = useCmd ? 'cmd.exe' : command;
370
+ const finalArgs = useCmd ? ['/d', '/s', '/c', command, ...args] : args;
371
+ return spawnSync(executable, finalArgs, {
372
+ cwd,
373
+ encoding: 'utf8',
374
+ stdio: ['ignore', 'pipe', 'pipe'],
375
+ maxBuffer: 1024 * 1024 * 10
376
+ });
377
+ }
378
+
379
+ function registerCodexGlobalMcp(targetDir) {
380
+ const getResult = runCapture('codex', ['mcp', 'get', 'voc']);
381
+ if (!getResult.error && getResult.status === 0) {
382
+ const removeResult = runCapture('codex', ['mcp', 'remove', 'voc']);
383
+ if (removeResult.error || removeResult.status !== 0) {
384
+ throw new Error('Unable to update the existing Codex VOC MCP registration');
385
+ }
386
+ }
387
+ const serverPath = path.join(targetDir, 'mcp', 'src', 'server.js');
388
+ const addResult = runCapture('codex', ['mcp', 'add', 'voc', '--', 'node', serverPath]);
389
+ if (addResult.error || addResult.status !== 0) {
390
+ const detail = String(addResult.stderr || addResult.error?.message || '').trim();
391
+ throw new Error(`Codex MCP registration failed${detail ? `: ${detail}` : ''}`);
392
+ }
393
+ }
394
+
395
+ function copyPackagedSkills(targetDir, destinationRoot) {
396
+ const sourceRoot = path.join(targetDir, 'skills');
397
+ ensureDir(destinationRoot);
398
+ let copied = 0;
399
+ for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
400
+ if (!entry.isDirectory()) continue;
401
+ const destination = path.join(destinationRoot, entry.name);
402
+ removeDirResilient(destination);
403
+ copied += copyDirRecursive(path.join(sourceRoot, entry.name), destination);
404
+ }
405
+ return copied;
406
+ }
407
+
408
+ function persistNeutralCredential(credentialsPath = VOCMARKET_CREDENTIALS) {
409
+ const token = readNewApiToken();
410
+ if (!token) return false;
411
+ const existing = readJsonIfExists(credentialsPath);
412
+ ensureDir(path.dirname(credentialsPath));
413
+ fs.writeFileSync(credentialsPath, `${JSON.stringify({ ...existing, fmodeApiKey: token }, null, 2)}\n`, {
414
+ encoding: 'utf8',
415
+ mode: 0o600
416
+ });
417
+ try {
418
+ fs.chmodSync(credentialsPath, 0o600);
419
+ } catch {}
420
+ return true;
280
421
  }
281
422
 
282
423
  function asForwardSlash(filePath) {
@@ -617,11 +758,10 @@ function writeWorkspaceSkillEntries(targetDir) {
617
758
  fs.copyFileSync(path.join(targetDir, 'skills', 'voc-api-catalog', 'SKILL.md'), path.join(apiCatalogSkillDir, 'SKILL.md'));
618
759
  }
619
760
 
620
- function writeWorkspaceActivation(targetDir) {
621
- if (!isSafeWorkspaceTarget(targetDir)) return;
761
+ function writeClaudeWorkspaceActivation(targetDir) {
622
762
  writeWorkspaceMcpConfig(targetDir);
623
763
  writeWorkspaceSkillEntries(targetDir);
624
- console.log('Wrote workspace MCP config: .\\.mcp.json');
764
+ console.log('Wrote Claude/Fmode workspace MCP config: .\\.mcp.json');
625
765
  console.log('Wrote workspace skill entry: .\\.claude\\skills\\xiaohongshu-trend-intelligence\\SKILL.md');
626
766
  console.log('Wrote workspace skill entry: .\\.claude\\skills\\douyin-trend-intelligence\\SKILL.md');
627
767
  console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-issue-pool\\SKILL.md');
@@ -634,26 +774,71 @@ function writeWorkspaceActivation(targetDir) {
634
774
  console.log('Wrote workspace skill entry: .\\.claude\\skills\\voc-api-catalog\\SKILL.md');
635
775
  }
636
776
 
637
- function printNextSteps(targetDir) {
638
- const workspaceMode = isSafeWorkspaceTarget(targetDir);
777
+ function writeWorkspaceActivation(targetDir, opts) {
778
+ if (!opts.workspaceRequested) return;
779
+ if (opts.channel === 'codex') {
780
+ const skillRoot = opts.scope === 'global'
781
+ ? path.join(os.homedir(), '.codex', 'skills')
782
+ : path.join(WORKSPACE_ROOT, '.codex', 'skills');
783
+ copyPackagedSkills(targetDir, skillRoot);
784
+ if (opts.scope === 'global') {
785
+ registerCodexGlobalMcp(targetDir);
786
+ console.log('Registered VOC MCP with Codex user config.');
787
+ console.log(`Installed Codex user skills: ${skillRoot}`);
788
+ } else {
789
+ const configPath = upsertCodexProjectMcp(targetDir);
790
+ console.log(`Registered VOC MCP with Codex project config: ${configPath}`);
791
+ console.log(`Installed Codex project skills: ${skillRoot}`);
792
+ }
793
+ return;
794
+ }
795
+ if (opts.channel === 'workbuddy') {
796
+ writeWorkspaceMcpConfig(targetDir);
797
+ const skillRoot = path.join(WORKSPACE_ROOT, '.workbuddy', 'skills');
798
+ copyPackagedSkills(targetDir, skillRoot);
799
+ console.log('Wrote portable WorkBuddy MCP registration: .\\.mcp.json');
800
+ console.log('Installed WorkBuddy workspace skills: .\\.workbuddy\\skills');
801
+ return;
802
+ }
803
+ writeClaudeWorkspaceActivation(targetDir);
804
+ }
805
+
806
+ async function verifyVocToolDiscovery(targetDir) {
807
+ const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
808
+ const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
809
+ const { version } = require('../package.json');
810
+ const client = new Client({ name: 'voc-installer-verification', version });
811
+ const transport = new StdioClientTransport({
812
+ command: process.execPath,
813
+ args: [path.join(targetDir, 'mcp', 'src', 'server.js')],
814
+ cwd: targetDir,
815
+ stderr: 'pipe'
816
+ });
817
+ await client.connect(transport);
818
+ try {
819
+ const result = await client.listTools();
820
+ const names = (result.tools || []).map(tool => tool.name);
821
+ const discovered = names.filter(name => name.startsWith('voc_api_'));
822
+ if (!discovered.length) {
823
+ throw new Error('MCP started, but no voc_api_* tool was discovered');
824
+ }
825
+ console.log(`Verified MCP tool discovery (no data request): ${discovered.join(', ')}`);
826
+ return discovered;
827
+ } finally {
828
+ await client.close();
829
+ }
830
+ }
831
+
832
+ function printNextSteps(targetDir, opts) {
833
+ const workspaceMode = opts.workspaceRequested;
639
834
  console.log('');
640
835
  console.log('Install complete.');
641
836
  console.log('');
642
837
  if (workspaceMode) {
643
- console.log('VSCode workspace mode is ready.');
838
+ console.log(`${opts.channel} ${opts.scope} mode is ready.`);
644
839
  console.log('');
645
840
  console.log('Generated project files:');
646
- console.log(' .\\.mcp.json');
647
- console.log(' .\\.claude\\skills\\xiaohongshu-trend-intelligence\\SKILL.md');
648
- console.log(' .\\.claude\\skills\\douyin-trend-intelligence\\SKILL.md');
649
- console.log(' .\\.claude\\skills\\voc-issue-pool\\SKILL.md');
650
- console.log(' .\\.claude\\skills\\voc-problem-deep-dive\\SKILL.md');
651
- console.log(' .\\.claude\\skills\\voc-content-plan\\SKILL.md');
652
- console.log(' .\\.claude\\skills\\voc-speaking-script\\SKILL.md');
653
- console.log(' .\\.claude\\skills\\voc-competitor-map\\SKILL.md');
654
- console.log(' .\\.claude\\skills\\voc-business-workflow\\SKILL.md');
655
- console.log(' .\\.claude\\skills\\fmode-image-analysis\\SKILL.md');
656
- console.log(' .\\.claude\\plugins\\voc-intelligence');
841
+ console.log(` ${targetDir}`);
657
842
  console.log('');
658
843
  console.log('Restart the VSCode Claude Code session if it was already open.');
659
844
  } else {
@@ -677,7 +862,7 @@ function printNextSteps(targetDir) {
677
862
  console.log(`Install target: ${targetDir}`);
678
863
  }
679
864
 
680
- function install(opts) {
865
+ async function install(opts) {
681
866
  checkNodeVersion();
682
867
  if (path.resolve(SOURCE_ROOT) !== path.resolve(opts.target)) {
683
868
  if (fs.existsSync(opts.target)) {
@@ -693,7 +878,7 @@ function install(opts) {
693
878
 
694
879
  assertInstalled(opts.target);
695
880
  writeMcpConfig(opts.target);
696
- writeWorkspaceActivation(opts.target);
881
+ writeWorkspaceActivation(opts.target, opts);
697
882
 
698
883
  if (!opts.skipInstall) {
699
884
  console.log('Installing runtime dependencies...');
@@ -707,7 +892,11 @@ function install(opts) {
707
892
  run(process.execPath, ['install.js', '--check', '--no-next'], opts.target);
708
893
  }
709
894
 
710
- printNextSteps(opts.target);
895
+ if (opts.activate) {
896
+ persistNeutralCredential();
897
+ await verifyVocToolDiscovery(opts.target);
898
+ }
899
+ printNextSteps(opts.target, opts);
711
900
  }
712
901
 
713
902
  function check(opts) {
@@ -742,7 +931,7 @@ async function main() {
742
931
  return;
743
932
  }
744
933
  if (opts.command === 'install') {
745
- install(opts);
934
+ await install(opts);
746
935
  if (opts.activate) {
747
936
  await ensureActivated({ channel: opts.channel });
748
937
  }
@@ -754,10 +943,20 @@ async function main() {
754
943
  throw new Error(`Unknown command: ${opts.command}\n\n${usage()}`);
755
944
  }
756
945
 
757
- main().catch((error) => {
758
- console.error('');
759
- console.error(`claude-voc failed: ${error.message}`);
760
- process.exit(1);
761
- });
762
-
946
+ if (require.main === module) {
947
+ main().catch((error) => {
948
+ console.error('');
949
+ console.error(`claude-voc failed: ${error.message}`);
950
+ process.exit(1);
951
+ });
952
+ }
763
953
 
954
+ module.exports = {
955
+ parseArgs,
956
+ normalizeInstallChannel,
957
+ normalizeInstallScope,
958
+ defaultTargetFor,
959
+ upsertCodexProjectMcp,
960
+ verifyVocToolDiscovery,
961
+ persistNeutralCredential
962
+ };
@@ -149,6 +149,7 @@ function readXiaohongshuToken(input = {}) {
149
149
  // → Claude Code env(进程注入 + ~/.claude/settings.json)的 ANTHROPIC_AUTH_TOKEN(sk-、且 base 指向 fmode)。
150
150
  function readNewApiToken(input = {}) {
151
151
  const workspaceEnv = readEnvLocalFiles();
152
+ const vocMarketCreds = readJsonMaybe(path.join(os.homedir(), '.vocmarket', 'credentials.json'));
152
153
  const claudeCreds = readJsonMaybe(path.join(os.homedir(), '.claude', 'voc-credentials.json'));
153
154
  const fmodeConfig = readFmodeConfig();
154
155
  const claudeEnv = readClaudeSettingsEnv();
@@ -165,6 +166,9 @@ function readNewApiToken(input = {}) {
165
166
  process.env.NEW_API_TOKEN,
166
167
  process.env.FMODE_API_KEY,
167
168
  process.env.FMODE_API_TOKEN,
169
+ vocMarketCreds.fmodeApiKey,
170
+ vocMarketCreds.fmodeApiToken,
171
+ vocMarketCreds.newApiToken,
168
172
  claudeCreds.fmodeApiKey,
169
173
  claudeCreds.fmodeApiToken,
170
174
  fmodeConfig.newapiToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vocmarket/voc-skill",
3
- "version": "0.3.30",
3
+ "version": "0.3.31",
4
4
  "description": "Claude Code VOC intelligence skills for Xiaohongshu and Douyin trend reports, live social VOC collection, Fmode image analysis, single-issue deep dives, boss/operator action plans, and multi-turn memory.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -29,7 +29,8 @@
29
29
  "voc:business-workflow": "node mcp/src/tools/voc-business-workflow-run.js --brand 示例品牌A --industry 家居定制 --platform douyin --collection-mode sample --keywords \"家居定制怎么选,第一次买怎么对比,售后\" --assistant-message-only",
30
30
  "mcp:start": "node mcp/src/server.js",
31
31
  "mcp:smoke": "node scripts/smoke-mcp.js",
32
- "acceptance": "node scripts/acceptance.js"
32
+ "acceptance": "node scripts/acceptance.js",
33
+ "acceptance:installer": "node scripts/installer-acceptance.js"
33
34
  },
34
35
  "files": [
35
36
  ".claude-plugin/",
@@ -13,6 +13,7 @@ function main() {
13
13
  run('npm', ['run', 'fmode:image:smoke'], ROOT);
14
14
  run('npm', ['run', 'smoke:package'], ROOT);
15
15
  run('npm', ['run', 'mcp:smoke'], ROOT);
16
+ run('npm', ['run', 'acceptance:installer'], ROOT);
16
17
  run('npm', ['pack', '--dry-run'], ROOT);
17
18
 
18
19
  const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'voc-workspace-install-'));
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const assert = require('assert');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const {
9
+ parseArgs,
10
+ normalizeInstallChannel,
11
+ normalizeInstallScope,
12
+ upsertCodexProjectMcp,
13
+ verifyVocToolDiscovery,
14
+ persistNeutralCredential
15
+ } = require('../bin/claude-voc');
16
+
17
+ async function main() {
18
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'voc-installer-acceptance-'));
19
+ const targetDir = path.join(tempRoot, '.codex', 'plugins', 'voc-intelligence');
20
+ const configDir = path.join(tempRoot, '.codex');
21
+ const configPath = path.join(configDir, 'config.toml');
22
+ const credentialsPath = path.join(tempRoot, '.vocmarket', 'credentials.json');
23
+ const previousToken = process.env.FMODE_API_KEY;
24
+
25
+ try {
26
+ assert.strictEqual(normalizeInstallChannel('CODEX'), 'codex');
27
+ assert.strictEqual(normalizeInstallScope('codex', 'global'), 'global');
28
+ assert.strictEqual(normalizeInstallScope('workbuddy', 'global'), 'project');
29
+
30
+ const options = parseArgs(['workspace', '--activate', '--channel=codex', '--scope=global']);
31
+ assert.strictEqual(options.activate, true);
32
+ assert.strictEqual(options.smoke, false);
33
+ assert.strictEqual(options.channel, 'codex');
34
+ assert.strictEqual(options.scope, 'global');
35
+
36
+ fs.mkdirSync(configDir, { recursive: true });
37
+ fs.writeFileSync(configPath, [
38
+ 'model = "gpt-test"',
39
+ '',
40
+ '[mcp_servers.existing]',
41
+ 'command = "existing-command"',
42
+ '',
43
+ '[mcp_servers.voc]',
44
+ 'command = "old-command"',
45
+ ''
46
+ ].join('\n'));
47
+ upsertCodexProjectMcp(targetDir, tempRoot);
48
+ const config = fs.readFileSync(configPath, 'utf8');
49
+ assert.match(config, /model = "gpt-test"/);
50
+ assert.match(config, /\[mcp_servers\.existing]/);
51
+ assert.strictEqual((config.match(/\[mcp_servers\.voc]/g) || []).length, 1);
52
+ assert.ok(config.includes('mcp\\\\src\\\\server.js') || config.includes('mcp/src/server.js'));
53
+
54
+ process.env.FMODE_API_KEY = 'sk-installer_acceptance-123456789';
55
+ assert.strictEqual(persistNeutralCredential(credentialsPath), true);
56
+ const credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'));
57
+ assert.strictEqual(credentials.fmodeApiKey, process.env.FMODE_API_KEY);
58
+
59
+ const tools = await verifyVocToolDiscovery(path.resolve(__dirname, '..'));
60
+ assert.ok(tools.includes('voc_api_search'));
61
+ assert.ok(tools.includes('voc_api_doc'));
62
+ assert.ok(tools.includes('voc_api_call'));
63
+
64
+ console.log(JSON.stringify({
65
+ status: 'ok',
66
+ checks: ['channels', 'no-smoke activation', 'Codex TOML merge', 'neutral credentials', 'MCP ListTools'],
67
+ discoveredTools: tools
68
+ }, null, 2));
69
+ } finally {
70
+ if (previousToken === undefined) delete process.env.FMODE_API_KEY;
71
+ else process.env.FMODE_API_KEY = previousToken;
72
+ fs.rmSync(tempRoot, { recursive: true, force: true });
73
+ }
74
+ }
75
+
76
+ main().catch(error => {
77
+ console.error(error.message);
78
+ process.exit(1);
79
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-voc-intelligence",
3
- "version": "0.3.30",
3
+ "version": "0.3.31",
4
4
  "capabilities": {
5
5
  "summary": "三类情报源同源接入:①社媒内容 ②国内电商+创作者情报 ③海外选品。统一 search→doc→call、鉴权计费一致。",
6
6
  "channels": [
@@ -123,4 +123,4 @@
123
123
  "workspaceInstallCommand": "npx @vocmarket/voc-skill workspace --smoke",
124
124
  "workspaceInstallTarget": ".claude/plugins/voc-intelligence",
125
125
  "installHint": "Supports three delivery modes: unzip and run node install.js, install globally into ~/.claude/plugins/voc-intelligence, or install into the current workspace. Workspace mode writes .mcp.json plus .claude/skills/* entries for trend intelligence, issue pool, deep dive, competitor map, content plan, speaking script, VOC business workflow, and Fmode image analysis so VSCode Claude Code can discover the workflows without relying only on --plugin-dir."
126
- }
126
+ }