@vocmarket/voc-skill 0.3.28 → 0.3.29

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.28",
4
+ "version": "0.3.29",
5
5
  "author": {
6
6
  "name": "nkkj-BrainHack"
7
7
  }
@@ -0,0 +1,211 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Self-update detection for the locally-installed VOC skill package.
5
+ *
6
+ * Surfaces a one-line "new version available" hint inside tool results so that
7
+ * a user running the skill in Claude Code is told, while they work, that an
8
+ * update exists. Network access is best-effort: it is throttled (24h cache),
9
+ * fail-silent (never throws, never blocks a tool call), and can be disabled.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const https = require('https');
16
+
17
+ const PKG = '@vocmarket/voc-skill';
18
+ const REGISTRY_URL = `https://registry.npmjs.org/${PKG.replace('/', '%2F')}/latest`;
19
+ const TTL_MS = 24 * 60 * 60 * 1000; // check npm at most once per day
20
+ const FETCH_TIMEOUT_MS = 1500;
21
+ const CACHE_FILE = path.join(os.homedir(), '.claude', 'voc-update-check.json');
22
+
23
+ let sessionNotified = false; // notify at most once per server process (session)
24
+ let latestInMemory = null; // populated by primeUpdateCheck()
25
+
26
+ function isDisabled() {
27
+ return process.env.VOC_SKILL_NO_UPDATE_CHECK === '1';
28
+ }
29
+
30
+ function getInstalledVersion() {
31
+ // version-check.js lives at <pkg>/mcp/src/core/, package.json at <pkg>/.
32
+ const candidates = [
33
+ path.join(__dirname, '..', '..', '..', 'package.json'),
34
+ path.join(__dirname, '..', '..', '..', 'skill-package-manifest.json')
35
+ ];
36
+ for (const file of candidates) {
37
+ try {
38
+ const json = JSON.parse(fs.readFileSync(file, 'utf8'));
39
+ if (json && typeof json.version === 'string') return json.version;
40
+ } catch (_) {
41
+ /* ignore and try next */
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+
47
+ // Compare two semver strings. Returns 1 if a>b, -1 if a<b, 0 if equal.
48
+ // Handles `major.minor.patch[-prerelease]`; a release outranks its prerelease.
49
+ function compareSemver(a, b) {
50
+ const parse = v => {
51
+ const [core, pre] = String(v).trim().replace(/^v/, '').split('-');
52
+ const nums = core.split('.').map(n => parseInt(n, 10) || 0);
53
+ return { nums: [nums[0] || 0, nums[1] || 0, nums[2] || 0], pre: pre || '' };
54
+ };
55
+ const pa = parse(a);
56
+ const pb = parse(b);
57
+ for (let i = 0; i < 3; i += 1) {
58
+ if (pa.nums[i] > pb.nums[i]) return 1;
59
+ if (pa.nums[i] < pb.nums[i]) return -1;
60
+ }
61
+ if (pa.pre === pb.pre) return 0;
62
+ if (!pa.pre) return 1; // release > prerelease
63
+ if (!pb.pre) return -1;
64
+ return pa.pre > pb.pre ? 1 : -1;
65
+ }
66
+
67
+ function readCache() {
68
+ try {
69
+ const cache = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
70
+ if (cache && typeof cache === 'object') return cache;
71
+ } catch (_) {
72
+ /* no cache yet / unreadable */
73
+ }
74
+ return null;
75
+ }
76
+
77
+ function writeCache(next) {
78
+ try {
79
+ fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
80
+ const tmp = `${CACHE_FILE}.${process.pid}.tmp`;
81
+ fs.writeFileSync(tmp, JSON.stringify(next));
82
+ fs.renameSync(tmp, CACHE_FILE); // atomic swap, survives concurrent writers
83
+ } catch (_) {
84
+ /* caching is best-effort */
85
+ }
86
+ }
87
+
88
+ function fetchLatest() {
89
+ return new Promise(resolve => {
90
+ let done = false;
91
+ const finish = value => {
92
+ if (done) return;
93
+ done = true;
94
+ resolve(value);
95
+ };
96
+ try {
97
+ const req = https.get(REGISTRY_URL, { timeout: FETCH_TIMEOUT_MS }, res => {
98
+ if (res.statusCode !== 200) {
99
+ res.resume();
100
+ return finish(null);
101
+ }
102
+ let body = '';
103
+ res.on('data', chunk => {
104
+ body += chunk;
105
+ if (body.length > 1e6) req.destroy(); // guard against pathological size
106
+ });
107
+ res.on('end', () => {
108
+ try {
109
+ const v = JSON.parse(body).version;
110
+ finish(typeof v === 'string' ? v : null);
111
+ } catch (_) {
112
+ finish(null);
113
+ }
114
+ });
115
+ });
116
+ req.on('timeout', () => {
117
+ req.destroy();
118
+ finish(null);
119
+ });
120
+ req.on('error', () => finish(null));
121
+ } catch (_) {
122
+ finish(null);
123
+ }
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Fire-and-forget at server startup. Refreshes `latestInMemory` from the 24h
129
+ * cache, or from npm when the cache is stale. Always resolves, never throws.
130
+ */
131
+ async function primeUpdateCheck() {
132
+ if (isDisabled()) return;
133
+ try {
134
+ const cache = readCache();
135
+ if (cache && cache.latest && typeof cache.fetchedAt === 'number' && Date.now() - cache.fetchedAt < TTL_MS) {
136
+ latestInMemory = cache.latest;
137
+ return;
138
+ }
139
+ const latest = await fetchLatest();
140
+ if (latest) {
141
+ latestInMemory = latest;
142
+ writeCache({ ...(cache || {}), latest, fetchedAt: Date.now() });
143
+ } else if (cache && cache.latest) {
144
+ latestInMemory = cache.latest; // fall back to a stale-but-known value
145
+ }
146
+ } catch (_) {
147
+ /* never let update detection affect the server */
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Synchronous. Returns a one-line update notice the first time it is called in
153
+ * a session where a newer version is known, otherwise null. Safe to call from
154
+ * the hot path of every tool result.
155
+ */
156
+ function consumeUpdateNotice() {
157
+ try {
158
+ if (isDisabled() || sessionNotified) return null;
159
+
160
+ let latest = latestInMemory;
161
+ if (!latest) {
162
+ const cache = readCache(); // prime may not have finished yet
163
+ if (cache && cache.latest) latest = cache.latest;
164
+ }
165
+ if (!latest) return null;
166
+
167
+ const installed = getInstalledVersion();
168
+ if (!installed) return null;
169
+ if (compareSemver(latest, installed) <= 0) return null;
170
+
171
+ // Don't re-nag across sessions for a version we already announced.
172
+ const cache = readCache() || {};
173
+ if (cache.notifiedVersion === latest) {
174
+ sessionNotified = true;
175
+ return null;
176
+ }
177
+
178
+ sessionNotified = true;
179
+ writeCache({ ...cache, notifiedVersion: latest });
180
+ return (
181
+ `ℹ️ VOC 技能包有新版本 ${latest}(当前 ${installed})。新版多平台电商/海外选品等接口能力更全。\n` +
182
+ '更新命令:npx @vocmarket/voc-skill@latest install(或 workspace 模式同理)。'
183
+ );
184
+ } catch (_) {
185
+ return null;
186
+ }
187
+ }
188
+
189
+ /** Prepend the update notice (once) to a normalized tool result in place. */
190
+ function applyUpdateNotice(result) {
191
+ try {
192
+ if (!result || typeof result.assistantMessage !== 'string') return result;
193
+ const notice = consumeUpdateNotice();
194
+ if (notice) {
195
+ result.assistantMessage = result.assistantMessage
196
+ ? `${notice}\n\n${result.assistantMessage}`
197
+ : notice;
198
+ }
199
+ } catch (_) {
200
+ /* never block a tool result */
201
+ }
202
+ return result;
203
+ }
204
+
205
+ module.exports = {
206
+ primeUpdateCheck,
207
+ consumeUpdateNotice,
208
+ applyUpdateNotice,
209
+ getInstalledVersion,
210
+ compareSemver
211
+ };
package/mcp/src/server.js CHANGED
@@ -16,6 +16,7 @@ const { runBusinessWorkflow } = require('./tools/voc-business-workflow-run');
16
16
  const { analyzeFmodeImage } = require('./tools/fmode-image-analysis');
17
17
  const { searchVocApis, getVocApiDoc, callVocApi } = require('./tools/voc-api-catalog-run');
18
18
  const { buildVocRechargeInfo, buildMissingTokenMessage, buildMissingNewApiTokenMessage } = require('./core/payment-links');
19
+ const { primeUpdateCheck, applyUpdateNotice } = require('./core/version-check');
19
20
 
20
21
  function asToolResult(result) {
21
22
  const normalized = normalizeToolResult(result);
@@ -55,6 +56,7 @@ function normalizeToolResult(result = {}) {
55
56
  if (Object.prototype.hasOwnProperty.call(result, 'errors')) {
56
57
  normalized.errors = Array.isArray(result.errors) ? result.errors : [];
57
58
  }
59
+ applyUpdateNotice(normalized);
58
60
  return normalized;
59
61
  }
60
62
 
@@ -774,6 +776,7 @@ function createServer() {
774
776
 
775
777
  async function main() {
776
778
  const server = createServer();
779
+ primeUpdateCheck(); // fire-and-forget: refresh latest-version knowledge for update hints
777
780
  const transport = new StdioServerTransport();
778
781
  await server.connect(transport);
779
782
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vocmarket/voc-skill",
3
- "version": "0.3.28",
3
+ "version": "0.3.29",
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": {
@@ -1,7 +1,63 @@
1
1
  {
2
2
  "name": "claude-code-voc-intelligence",
3
- "version": "0.3.28",
3
+ "version": "0.3.29",
4
+ "capabilities": {
5
+ "summary": "三类情报源同源接入:①社媒内容 ②国内电商+创作者情报 ③海外选品。统一 search→doc→call、鉴权计费一致。",
6
+ "channels": [
7
+ {
8
+ "key": "social",
9
+ "label": "社媒内容",
10
+ "platforms": 18,
11
+ "apis": 1018,
12
+ "examples": [
13
+ "抖音",
14
+ "小红书",
15
+ "TikTok",
16
+ "YouTube",
17
+ "微博",
18
+ "哔哩哔哩",
19
+ "快手",
20
+ "知乎",
21
+ "Instagram",
22
+ "Reddit",
23
+ "Twitter/X",
24
+ "微信/视频号"
25
+ ]
26
+ },
27
+ {
28
+ "key": "ecommerce",
29
+ "label": "国内电商 + 创作者情报",
30
+ "platforms": 32,
31
+ "apis": 285,
32
+ "examples": [
33
+ "京东",
34
+ "淘宝天猫",
35
+ "1688",
36
+ "抖音电商",
37
+ "TikTok Shop",
38
+ "闲鱼",
39
+ "得物",
40
+ "抖音星图",
41
+ "小红书蒲公英"
42
+ ]
43
+ },
44
+ {
45
+ "key": "overseas",
46
+ "label": "海外选品",
47
+ "platforms": 1,
48
+ "apis": 37,
49
+ "examples": [
50
+ "亚马逊"
51
+ ]
52
+ }
53
+ ],
54
+ "totals": {
55
+ "platforms": 51,
56
+ "apis": 1340
57
+ }
58
+ },
4
59
  "description": "Claude Code native VOC intelligence package for cross-industry social VOC workflows. Includes Xiaohongshu and Douyin trend intelligence, live token checks, evidence-card reports, single-issue VOC deep dives, boss/operator action plans, and scoped multi-turn memory.",
60
+ "changelogUrl": "https://www.npmjs.com/package/@vocmarket/voc-skill?activeTab=versions",
5
61
  "plugin": "voc-intelligence",
6
62
  "skills": [
7
63
  "xiaohongshu-trend-intelligence",
@@ -67,4 +123,4 @@
67
123
  "workspaceInstallCommand": "npx @vocmarket/voc-skill workspace --smoke",
68
124
  "workspaceInstallTarget": ".claude/plugins/voc-intelligence",
69
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."
70
- }
126
+ }