kiku-agent 1.0.5 → 1.0.6

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.
@@ -0,0 +1,438 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { KIKU_DATA_DIR } from './appPaths.js';
6
+ import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
7
+
8
+ export const UPDATE_CHANNELS = Object.freeze(['latest', 'next', 'canary']);
9
+ export const UPDATE_STATE_FILE = path.join(KIKU_DATA_DIR, 'update-state.json');
10
+
11
+ const PACKAGE_ROOT = fileURLToPath(new URL('../../', import.meta.url));
12
+ const STRICT_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
13
+ const MAX_NPM_OUTPUT = 2 * 1024 * 1024;
14
+ const DEFAULT_TIMEOUT_MS = 20_000;
15
+ const INSTALL_TIMEOUT_MS = 180_000;
16
+
17
+ export class UpdateError extends Error {
18
+ constructor(message, code = 'UPDATE_ERROR', details = {}) {
19
+ super(message);
20
+ this.name = 'UpdateError';
21
+ this.code = code;
22
+ this.details = details;
23
+ }
24
+ }
25
+
26
+ function cleanOutput(value, maxLength = 400) {
27
+ return String(value || '')
28
+ .replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g, '')
29
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
30
+ .trim()
31
+ .slice(0, maxLength);
32
+ }
33
+
34
+ function parseSemver(value) {
35
+ const normalized = String(value || '').trim().replace(/^v/, '');
36
+ const match = normalized.match(STRICT_SEMVER);
37
+ if (!match) return null;
38
+ return {
39
+ raw: normalized,
40
+ major: Number(match[1]),
41
+ minor: Number(match[2]),
42
+ patch: Number(match[3]),
43
+ prerelease: match[4] ? match[4].split('.') : [],
44
+ };
45
+ }
46
+
47
+ function compareIdentifiers(left, right) {
48
+ const leftNumeric = /^\d+$/.test(left);
49
+ const rightNumeric = /^\d+$/.test(right);
50
+ if (leftNumeric && rightNumeric) return Number(left) - Number(right);
51
+ if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
52
+ return left.localeCompare(right);
53
+ }
54
+
55
+ export function compareVersions(left, right) {
56
+ const a = parseSemver(left);
57
+ const b = parseSemver(right);
58
+ if (!a || !b) throw new UpdateError('The registry returned an invalid semantic version.', 'INVALID_SEMVER');
59
+ for (const key of ['major', 'minor', 'patch']) {
60
+ if (a[key] !== b[key]) return Math.sign(a[key] - b[key]);
61
+ }
62
+ if (a.prerelease.length === 0 && b.prerelease.length === 0) return 0;
63
+ if (a.prerelease.length === 0) return 1;
64
+ if (b.prerelease.length === 0) return -1;
65
+ const max = Math.max(a.prerelease.length, b.prerelease.length);
66
+ for (let index = 0; index < max; index++) {
67
+ if (a.prerelease[index] === undefined) return -1;
68
+ if (b.prerelease[index] === undefined) return 1;
69
+ const compared = compareIdentifiers(a.prerelease[index], b.prerelease[index]);
70
+ if (compared !== 0) return Math.sign(compared);
71
+ }
72
+ return 0;
73
+ }
74
+
75
+ export function isValidUpdateTarget(value) {
76
+ const normalized = String(value || '').trim().toLowerCase();
77
+ return UPDATE_CHANNELS.includes(normalized) || Boolean(parseSemver(normalized));
78
+ }
79
+
80
+ export function normalizeUpdateTarget(value, fallback = 'latest') {
81
+ const normalized = String(value || fallback).trim().toLowerCase().replace(/^v(?=\d)/, '');
82
+ if (!isValidUpdateTarget(normalized)) {
83
+ throw new UpdateError(
84
+ 'Invalid update target. Use latest, next, canary, or an exact version such as 1.2.3.',
85
+ 'INVALID_TARGET'
86
+ );
87
+ }
88
+ return normalized;
89
+ }
90
+
91
+ function satisfiesComparator(version, comparator) {
92
+ const match = comparator.match(/^(>=|<=|>|<|=|\^|~)?\s*(v?\d+(?:\.\d+){0,2})(?:\.x)?$/i);
93
+ if (!match) return false;
94
+ const operator = match[1] || '=';
95
+ const numericParts = match[2].replace(/^v/, '').split('.').map(Number);
96
+ while (numericParts.length < 3) numericParts.push(0);
97
+ const target = numericParts.join('.');
98
+ const compared = compareVersions(version, target);
99
+ if (operator === '>=') return compared >= 0;
100
+ if (operator === '<=') return compared <= 0;
101
+ if (operator === '>') return compared > 0;
102
+ if (operator === '<') return compared < 0;
103
+ if (operator === '~') {
104
+ const parsed = parseSemver(version);
105
+ return compared >= 0 && parsed.major === numericParts[0] && parsed.minor === numericParts[1];
106
+ }
107
+ if (operator === '^') {
108
+ const parsed = parseSemver(version);
109
+ if (numericParts[0] > 0) return compared >= 0 && parsed.major === numericParts[0];
110
+ if (numericParts[1] > 0) return compared >= 0 && parsed.major === 0 && parsed.minor === numericParts[1];
111
+ return compared >= 0 && parsed.major === 0 && parsed.minor === 0 && parsed.patch === numericParts[2];
112
+ }
113
+ return compared === 0;
114
+ }
115
+
116
+ export function satisfiesNodeEngine(nodeVersion, range) {
117
+ if (!range || range === '*') return true;
118
+ const version = String(nodeVersion || '').replace(/^v/, '').split('-')[0];
119
+ if (!parseSemver(version)) return false;
120
+ return String(range).split('||').some(alternative => {
121
+ const comparators = alternative.trim().split(/\s+/).filter(Boolean);
122
+ return comparators.length > 0 && comparators.every(comparator => satisfiesComparator(version, comparator));
123
+ });
124
+ }
125
+
126
+ function defaultNpmInvocation() {
127
+ const nodeDir = path.dirname(process.execPath);
128
+ const candidates = [
129
+ path.join(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
130
+ path.resolve(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
131
+ process.env.npm_execpath,
132
+ ].filter(Boolean);
133
+ const npmCli = candidates.find(candidate => /npm-cli\.(?:c?js|mjs)$/i.test(candidate) && existsSync(candidate));
134
+ if (npmCli) return { command: process.execPath, prefixArgs: [npmCli] };
135
+ return { command: process.platform === 'win32' ? 'npm.cmd' : 'npm', prefixArgs: [] };
136
+ }
137
+
138
+ export function runNpm(args, options = {}) {
139
+ if (!Array.isArray(args) || args.some(arg => typeof arg !== 'string')) {
140
+ return Promise.reject(new UpdateError('Invalid npm arguments.', 'INVALID_NPM_ARGS'));
141
+ }
142
+ const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
143
+ const invocation = options.invocation || defaultNpmInvocation();
144
+ return new Promise((resolve, reject) => {
145
+ let stdout = '';
146
+ let stderr = '';
147
+ let settled = false;
148
+ const child = spawn(invocation.command, [...invocation.prefixArgs, ...args], {
149
+ shell: false,
150
+ windowsHide: true,
151
+ stdio: ['ignore', 'pipe', 'pipe'],
152
+ env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },
153
+ });
154
+ const finish = (error, result) => {
155
+ if (settled) return;
156
+ settled = true;
157
+ clearTimeout(timer);
158
+ if (error) reject(error);
159
+ else resolve(result);
160
+ };
161
+ const append = (stream, chunk) => {
162
+ const next = stream + chunk.toString('utf8');
163
+ if (next.length > MAX_NPM_OUTPUT) {
164
+ try { child.kill('SIGKILL'); } catch {}
165
+ finish(new UpdateError('npm returned more data than expected.', 'NPM_OUTPUT_LIMIT'));
166
+ return stream;
167
+ }
168
+ return next;
169
+ };
170
+ child.stdout.on('data', chunk => { stdout = append(stdout, chunk); });
171
+ child.stderr.on('data', chunk => {
172
+ stderr = append(stderr, chunk);
173
+ if (typeof options.onStderr === 'function') options.onStderr(cleanOutput(chunk, 1000));
174
+ });
175
+ child.once('error', error => finish(new UpdateError(
176
+ `Unable to run npm: ${cleanOutput(error.message)}`,
177
+ 'NPM_UNAVAILABLE',
178
+ { cause: error.code }
179
+ )));
180
+ child.once('close', code => finish(null, { code, stdout: stdout.trim(), stderr: stderr.trim() }));
181
+ const timer = setTimeout(() => {
182
+ try { child.kill('SIGKILL'); } catch {}
183
+ finish(new UpdateError('npm did not respond before the timeout.', 'NPM_TIMEOUT'));
184
+ }, timeoutMs);
185
+ });
186
+ }
187
+
188
+ function safeJsonParse(raw, code) {
189
+ try {
190
+ return JSON.parse(raw);
191
+ } catch {
192
+ throw new UpdateError('npm returned an unreadable response.', code);
193
+ }
194
+ }
195
+
196
+ function isPathInside(candidate, parent) {
197
+ const relative = path.relative(path.resolve(parent), path.resolve(candidate));
198
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
199
+ }
200
+
201
+ function normalizeState(raw = {}) {
202
+ const channel = UPDATE_CHANNELS.includes(raw.channel) ? raw.channel : 'latest';
203
+ const installs = Array.isArray(raw.installs)
204
+ ? raw.installs.filter(item => parseSemver(item?.from) && parseSemver(item?.to)).slice(-20)
205
+ : [];
206
+ return { schemaVersion: 1, channel, installs, lastCheck: raw.lastCheck || null };
207
+ }
208
+
209
+ export class UpdateManager {
210
+ constructor(options = {}) {
211
+ this.packageName = options.packageName || PACKAGE_NAME;
212
+ this.currentVersion = options.currentVersion || PACKAGE_VERSION;
213
+ this.packageRoot = options.packageRoot || PACKAGE_ROOT;
214
+ this.stateFile = options.stateFile || UPDATE_STATE_FILE;
215
+ this.nodeVersion = options.nodeVersion || process.versions.node;
216
+ this.env = options.env || process.env;
217
+ this.runner = options.runner || ((args, runnerOptions) => runNpm(args, runnerOptions));
218
+ }
219
+
220
+ async readState() {
221
+ try {
222
+ return normalizeState(JSON.parse(await fs.readFile(this.stateFile, 'utf8')));
223
+ } catch (error) {
224
+ if (error.code === 'ENOENT' || error instanceof SyntaxError) return normalizeState();
225
+ throw new UpdateError('Unable to read update settings.', 'STATE_READ_FAILED', { cause: error.code });
226
+ }
227
+ }
228
+
229
+ async writeState(state) {
230
+ const normalized = normalizeState(state);
231
+ const directory = path.dirname(this.stateFile);
232
+ const temporary = path.join(directory, `.update-state-${process.pid}-${Date.now()}.tmp`);
233
+ try {
234
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
235
+ await fs.writeFile(temporary, JSON.stringify(normalized, null, 2), { encoding: 'utf8', mode: 0o600 });
236
+ await fs.rename(temporary, this.stateFile);
237
+ } catch (error) {
238
+ try { await fs.unlink(temporary); } catch {}
239
+ throw new UpdateError('Unable to save update settings.', 'STATE_WRITE_FAILED', { cause: error.code });
240
+ }
241
+ return normalized;
242
+ }
243
+
244
+ async getChannel() {
245
+ return (await this.readState()).channel;
246
+ }
247
+
248
+ async setChannel(channel) {
249
+ const normalized = normalizeUpdateTarget(channel);
250
+ if (!UPDATE_CHANNELS.includes(normalized)) {
251
+ throw new UpdateError('Update channels are latest, next, or canary.', 'INVALID_CHANNEL');
252
+ }
253
+ const state = await this.readState();
254
+ state.channel = normalized;
255
+ await this.writeState(state);
256
+ return normalized;
257
+ }
258
+
259
+ async detectInstallMode() {
260
+ const normalizedRoot = path.resolve(this.packageRoot);
261
+ if (/[\\/]_npx[\\/]/i.test(normalizedRoot) || this.env.npm_command === 'exec') {
262
+ return { id: 'npx', label: 'npx / npm exec', automatic: false };
263
+ }
264
+ if (existsSync(path.join(normalizedRoot, '.git'))) {
265
+ return { id: 'source', label: 'Source checkout', automatic: false };
266
+ }
267
+ try {
268
+ const result = await this.runner(['root', '--global'], { timeoutMs: 5_000 });
269
+ if (result.code === 0 && result.stdout && isPathInside(normalizedRoot, result.stdout.trim())) {
270
+ return { id: 'global', label: 'Global npm', automatic: true };
271
+ }
272
+ } catch {}
273
+ if (/[\\/]node_modules[\\/]/i.test(normalizedRoot)) {
274
+ return { id: 'local', label: 'Local project dependency', automatic: false };
275
+ }
276
+ return { id: 'unknown', label: 'Unknown/manual installation', automatic: false };
277
+ }
278
+
279
+ async fetchRelease(target) {
280
+ const normalizedTarget = normalizeUpdateTarget(target);
281
+ const spec = `${this.packageName}@${normalizedTarget}`;
282
+ const result = await this.runner(['view', spec, '--json', '--prefer-online'], { timeoutMs: DEFAULT_TIMEOUT_MS });
283
+ if (result.code !== 0) {
284
+ throw new UpdateError(
285
+ cleanOutput(result.stderr) || 'npm could not find the requested release.',
286
+ 'REGISTRY_LOOKUP_FAILED',
287
+ { exitCode: result.code }
288
+ );
289
+ }
290
+ const manifest = safeJsonParse(result.stdout, 'REGISTRY_RESPONSE_INVALID');
291
+ if (manifest.name !== this.packageName || !parseSemver(manifest.version)) {
292
+ throw new UpdateError('The registry response did not match kiku-agent.', 'REGISTRY_IDENTITY_MISMATCH');
293
+ }
294
+ if (parseSemver(normalizedTarget) && compareVersions(manifest.version, normalizedTarget) !== 0) {
295
+ throw new UpdateError('The registry resolved a different version than requested.', 'REGISTRY_VERSION_MISMATCH');
296
+ }
297
+ const metadata = manifest.kikuUpdate && typeof manifest.kikuUpdate === 'object' ? manifest.kikuUpdate : {};
298
+ const rawNotes = metadata.releaseSummary || metadata.releaseNotes || [];
299
+ const releaseNotes = (Array.isArray(rawNotes) ? rawNotes : [rawNotes])
300
+ .map(note => cleanOutput(note, 240))
301
+ .filter(Boolean)
302
+ .slice(0, 10);
303
+ return {
304
+ target: normalizedTarget,
305
+ version: manifest.version,
306
+ engines: manifest.engines || {},
307
+ deprecated: cleanOutput(manifest.deprecated, 300) || null,
308
+ integrity: cleanOutput(manifest.dist?.integrity, 300) || null,
309
+ releaseNotes,
310
+ releaseUrl: /^https:\/\//i.test(metadata.releaseUrl || '') ? cleanOutput(metadata.releaseUrl, 500) : null,
311
+ };
312
+ }
313
+
314
+ async check(target) {
315
+ const state = await this.readState();
316
+ const requestedTarget = normalizeUpdateTarget(target, state.channel);
317
+ const [release, installMode] = await Promise.all([
318
+ this.fetchRelease(requestedTarget),
319
+ this.detectInstallMode(),
320
+ ]);
321
+ const comparison = compareVersions(release.version, this.currentVersion);
322
+ const nodeRange = release.engines?.node || null;
323
+ const nodeCompatible = satisfiesNodeEngine(this.nodeVersion, nodeRange);
324
+ const current = parseSemver(this.currentVersion);
325
+ const next = parseSemver(release.version);
326
+ const status = {
327
+ packageName: this.packageName,
328
+ currentVersion: this.currentVersion,
329
+ targetVersion: release.version,
330
+ target: requestedTarget,
331
+ channel: UPDATE_CHANNELS.includes(requestedTarget) ? requestedTarget : state.channel,
332
+ comparison,
333
+ change: comparison > 0 ? 'upgrade' : comparison < 0 ? 'downgrade' : 'current',
334
+ updateAvailable: comparison > 0,
335
+ majorChange: current.major !== next.major,
336
+ nodeRange,
337
+ nodeCompatible,
338
+ installMode,
339
+ deprecated: release.deprecated,
340
+ integrity: release.integrity,
341
+ releaseNotes: release.releaseNotes,
342
+ releaseUrl: release.releaseUrl,
343
+ };
344
+ state.lastCheck = {
345
+ checkedAt: new Date().toISOString(),
346
+ currentVersion: status.currentVersion,
347
+ targetVersion: status.targetVersion,
348
+ target: status.target,
349
+ };
350
+ try { await this.writeState(state); } catch {}
351
+ return status;
352
+ }
353
+
354
+ manualCommand(installMode, version) {
355
+ if (installMode.id === 'npx') return `npx --yes --prefer-online ${this.packageName}@${version}`;
356
+ if (installMode.id === 'local') return `npm install ${this.packageName}@${version}`;
357
+ if (installMode.id === 'source') return 'git pull && npm install';
358
+ return `npm install --global ${this.packageName}@${version}`;
359
+ }
360
+
361
+ async install(target, options = {}) {
362
+ const status = await this.check(target);
363
+ if (status.deprecated && !options.allowDeprecated) {
364
+ throw new UpdateError(`This release is deprecated: ${status.deprecated}`, 'DEPRECATED_RELEASE');
365
+ }
366
+ if (!status.nodeCompatible) {
367
+ throw new UpdateError(
368
+ `Node ${this.nodeVersion} does not satisfy ${status.nodeRange}.`,
369
+ 'NODE_INCOMPATIBLE'
370
+ );
371
+ }
372
+ if (status.majorChange && status.change === 'upgrade' && !options.allowMajor) {
373
+ throw new UpdateError(
374
+ `Version ${status.targetVersion} is a major update. Re-run with --allow-major after reviewing its release notes.`,
375
+ 'MAJOR_CONFIRMATION_REQUIRED'
376
+ );
377
+ }
378
+ if (status.change === 'current') return { ...status, outcome: 'current' };
379
+ if (!status.installMode.automatic) {
380
+ return {
381
+ ...status,
382
+ outcome: 'manual',
383
+ command: this.manualCommand(status.installMode, status.targetVersion),
384
+ };
385
+ }
386
+
387
+ const exactSpec = `${this.packageName}@${status.targetVersion}`;
388
+ const result = await this.runner(
389
+ ['install', '--global', exactSpec, '--no-audit', '--no-fund'],
390
+ { timeoutMs: INSTALL_TIMEOUT_MS, onStderr: options.onProgress }
391
+ );
392
+ if (result.code !== 0) {
393
+ throw new UpdateError(
394
+ cleanOutput(result.stderr) || 'npm failed to install the update.',
395
+ 'INSTALL_FAILED',
396
+ { exitCode: result.code, version: status.targetVersion }
397
+ );
398
+ }
399
+
400
+ const verification = await this.runner(
401
+ ['list', '--global', this.packageName, '--depth=0', '--json'],
402
+ { timeoutMs: DEFAULT_TIMEOUT_MS }
403
+ );
404
+ if (verification.code !== 0) {
405
+ throw new UpdateError('The update installed, but its version could not be verified.', 'VERIFY_FAILED');
406
+ }
407
+ const installed = safeJsonParse(verification.stdout, 'VERIFY_RESPONSE_INVALID')
408
+ ?.dependencies?.[this.packageName]?.version;
409
+ if (installed !== status.targetVersion) {
410
+ throw new UpdateError('npm reported a different installed version after updating.', 'VERIFY_VERSION_MISMATCH', {
411
+ expected: status.targetVersion,
412
+ installed,
413
+ });
414
+ }
415
+
416
+ let historyRecorded = true;
417
+ try {
418
+ const state = await this.readState();
419
+ state.installs.push({
420
+ from: this.currentVersion,
421
+ to: status.targetVersion,
422
+ target: status.target,
423
+ installedAt: new Date().toISOString(),
424
+ });
425
+ await this.writeState(state);
426
+ } catch {
427
+ historyRecorded = false;
428
+ }
429
+ return { ...status, outcome: 'installed', installedVersion: installed, historyRecorded };
430
+ }
431
+
432
+ async rollback(options = {}) {
433
+ const state = await this.readState();
434
+ const last = [...state.installs].reverse().find(item => item.to === this.currentVersion) || state.installs.at(-1);
435
+ if (!last) throw new UpdateError('No previous Kiku Agent version is recorded.', 'NO_ROLLBACK_VERSION');
436
+ return this.install(last.from, { ...options, allowMajor: true, rollback: true });
437
+ }
438
+ }
@@ -0,0 +1,10 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ const packageManifest = JSON.parse(
4
+ readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
5
+ );
6
+
7
+ export const PACKAGE_NAME = packageManifest.name;
8
+ export const PACKAGE_VERSION = packageManifest.version;
9
+ export const PACKAGE_ENGINES = Object.freeze({ ...(packageManifest.engines || {}) });
10
+ export const PACKAGE_UPDATE_METADATA = Object.freeze({ ...(packageManifest.kikuUpdate || {}) });
@@ -1,9 +1,14 @@
1
1
  export const brandsData = [
2
2
  { id: "prsr", year: 2026, brandNameEn: "PRSR / Pasha", brandNameCn: "帕莎眼镜", title: "Global Brand Ambassador", category: "Luxury Eyewear", isGlobal: true, highlight: "Milan Fashion Week 2026 debut, viral pink sequin look" },
3
- { id: "fred", year: 2026, brandNameEn: "FRED Jewelry", brandNameCn: "FRED珠宝", title: "Brand Ambassador", category: "Luxury Jewelry", isGlobal: false, highlight: "Force 10 High Jewelry editorial for PORT Magazine" },
4
- { id: "tutucare", year: 2025, brandNameEn: "TuTuCare", brandNameCn: "凸凸棉", title: "Brand Ambassador", category: "Baby Care", isGlobal: false, highlight: "20,000 gift boxes sold out in 1 second, GMV 10.3M CNY in 1 hour" },
5
- { id: "dafuyan", year: 2025, brandNameEn: "DAFUYAN", brandNameCn: "达肤妍", title: "Brand Ambassador", category: "Skincare", isGlobal: false, highlight: "10,000 sets sold out in 8 minutes, 20M CNY in 24 hours" },
6
- { id: "lafang", year: 2025, brandNameEn: "Lafang", brandNameCn: "拉芳", title: "Global Brand Ambassador", category: "Hair Care", isGlobal: true, highlight: "First Global Ambassador in brand's 30-year history" },
3
+ { id: "versace", year: 2025, brandNameEn: "Versace Fragrance", brandNameCn: "范思哲香氛", title: "Global Fragrance Ambassador", category: "Luxury Fragrance", isGlobal: true, highlight: "Global ambassador for Versace Dylan Purple & luxury fragrance line" },
4
+ { id: "fred", year: 2025, brandNameEn: "FRED Jewelry", brandNameCn: "FRED珠宝 (LVMH)", title: "China Brand Ambassador", category: "Luxury Jewelry", isGlobal: false, highlight: "Force 10 High Jewelry editorial for PORT Magazine" },
5
+ { id: "tutucare", year: 2026, brandNameEn: "TuTuCare", brandNameCn: "凸凸棉", title: "Global Brand Ambassador", category: "Personal Care", isGlobal: true, highlight: "20,000 gift boxes sold out in 1 second, GMV 10.3M CNY in 1 hour" },
6
+ { id: "dafuyan", year: 2026, brandNameEn: "DAFUYAN", brandNameCn: "达肤妍", title: "Global Brand Ambassador", category: "Skincare", isGlobal: true, highlight: "10,000 sets sold out in 8 minutes, 20M CNY in 24 hours" },
7
+ { id: "lafang", year: 2026, brandNameEn: "Lafang", brandNameCn: "拉芳", title: "Global Brand Ambassador", category: "Hair Care", isGlobal: true, highlight: "First Global Ambassador in brand's 30-year history" },
8
+ { id: "makeup-forever", year: 2024, brandNameEn: "MAKE UP FOR EVER", brandNameCn: "玫珂菲 (LVMH)", title: "Brand Ambassador", category: "Professional Makeup", isGlobal: false, highlight: "Asia-Pacific brand ambassador for LVMH professional beauty" },
9
+ { id: "mistine", year: 2024, brandNameEn: "Mistine", brandNameCn: "蜜丝婷", title: "Global Sunscreen & Beauty Ambassador", category: "Sunscreen & Cosmetics", isGlobal: true, highlight: "Global ambassador for top-selling sunscreen and beauty products" },
10
+ { id: "maybelline", year: 2023, brandNameEn: "Maybelline New York", brandNameCn: "美宝莲纽约", title: "Brand Ambassador", category: "Makeup", isGlobal: false, highlight: "Campaign ambassador for viral lip and foundation lines" },
11
+ { id: "ysl", year: 2023, brandNameEn: "YSL Beauty", brandNameCn: "圣罗兰美妆", title: "Brand Friend", category: "Luxury Beauty", isGlobal: false, highlight: "Iconic campaign partnership for Libre fragrance and beauty" },
7
12
  { id: "oppo", year: 2024, brandNameEn: "OPPO", brandNameCn: "OPPO", title: "Brand Ambassador", category: "Technology", isGlobal: false, highlight: "Smartphone campaign across Asia" },
8
13
  { id: "loreal", year: 2024, brandNameEn: "L'Oréal Paris", brandNameCn: "巴黎欧莱雅", title: "Brand Ambassador", category: "Beauty", isGlobal: true, highlight: "Global beauty ambassador for L'Oréal Paris" },
9
14
  ];
@@ -1,4 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { PACKAGE_VERSION } from '../agent/version.js';
2
3
  import { z } from 'zod';
3
4
  import { toolDefinitions, handleToolCall } from './tools.js';
4
5
  import { resourceDefinitions, readResource } from './resources.js';
@@ -8,7 +9,7 @@ export function createMcpServer() {
8
9
  const server = new McpServer({
9
10
  name: 'kiku',
10
11
  title: 'kiku',
11
- version: '1.1.0',
12
+ version: PACKAGE_VERSION,
12
13
  description: 'MCP server exposing the Ju Jingyi catalog as tools, resources, and prompts.',
13
14
  websiteUrl: 'https://jujingyi-kikuuu.vercel.app/',
14
15
  icons: [
@@ -50,6 +50,19 @@ export const brandsData = {
50
50
  accentBg: "bg-purple-500/10 text-purple-600 border-purple-200 dark:border-purple-800",
51
51
  tier: "Global Luxury"
52
52
  },
53
+ {
54
+ id: "versace",
55
+ year: 2025,
56
+ brandNameEn: "Versace / Versace Fragrance",
57
+ brandNameCn: "范思哲香氛",
58
+ title: "Global Fragrance Ambassador (แบรนด์แอมบาสเดอร์ระดับโลกฝ่ายน้ำหอม)",
59
+ category: "น้ำหอมและแฟชั่นหรูระดับโลก (Luxury Italian Fashion & Fragrance)",
60
+ highlight: "ตัวแทนภาพลักษณ์ระดับสากลของ Versace Dylan Purple & Luxury Fragrance",
61
+ isGlobal: true,
62
+ logoColor: "from-amber-600 to-yellow-700",
63
+ accentBg: "bg-amber-500/10 text-amber-600 border-amber-200 dark:border-amber-800",
64
+ tier: "Global High Luxury"
65
+ },
53
66
  {
54
67
  id: "fred",
55
68
  year: 2025,
@@ -63,6 +76,58 @@ export const brandsData = {
63
76
  accentBg: "bg-sky-500/10 text-sky-600 border-sky-200 dark:border-sky-800",
64
77
  tier: "LVMH High Jewelry"
65
78
  },
79
+ {
80
+ id: "makeup-forever",
81
+ year: 2024,
82
+ brandNameEn: "MAKE UP FOR EVER",
83
+ brandNameCn: "玫珂菲 (LVMH)",
84
+ title: "Brand Ambassador (ภูมิภาคเอเชียแปซิฟิก)",
85
+ category: "เครื่องสำอางระดับมืออาชีพจากฝรั่งเศส (French Professional Makeup)",
86
+ highlight: "แบรนด์เครื่องสำอางชั้นนำในเครือ LVMH ถ่ายทอดภาพลักษณ์ลุคเมกอัปอันสมบูรณ์แบบ",
87
+ isGlobal: false,
88
+ logoColor: "from-neutral-800 to-black",
89
+ accentBg: "bg-neutral-500/10 text-neutral-700 dark:text-neutral-300 border-neutral-200 dark:border-neutral-700",
90
+ tier: "LVMH Professional Beauty"
91
+ },
92
+ {
93
+ id: "mistine",
94
+ year: 2024,
95
+ brandNameEn: "Mistine",
96
+ brandNameCn: "蜜丝婷",
97
+ title: "Global Sunscreen & Beauty Ambassador (แบรนด์แอมบาสเดอร์ระดับโลก)",
98
+ category: "ผลิตภัณฑ์กันแดดและความงามระดับเอเชีย (Sunscreen & Color Cosmetics)",
99
+ highlight: "พรีเซนเตอร์ไลน์ครีมกันแดดและเมกอัปอันดับ 1 ที่ได้รับความนิยมอย่างล้นหลามทั่วเอเชีย",
100
+ isGlobal: true,
101
+ logoColor: "from-orange-500 to-amber-600",
102
+ accentBg: "bg-orange-500/10 text-orange-600 border-orange-200 dark:border-orange-800",
103
+ tier: "Asian Beauty Titan"
104
+ },
105
+ {
106
+ id: "maybelline",
107
+ year: 2023,
108
+ brandNameEn: "Maybelline New York",
109
+ brandNameCn: "美宝莲纽约 (L'Oréal)",
110
+ title: "Brand Ambassador",
111
+ category: "เครื่องสำอางระดับโลก (Global Makeup)",
112
+ highlight: "แคมเปญลิปสติกและรองพื้นไวรัลระดับอินเตอร์เนชันแนล",
113
+ isGlobal: false,
114
+ logoColor: "from-rose-600 to-pink-700",
115
+ accentBg: "bg-rose-500/10 text-rose-600 border-rose-200 dark:border-rose-800",
116
+ tier: "Global Cosmetics"
117
+ },
118
+ {
119
+ id: "ysl-beauty",
120
+ year: 2023,
121
+ brandNameEn: "YSL Beauty",
122
+ brandNameCn: "圣罗兰美妆 (L'Oréal Luxe)",
123
+ title: "Brand Friend & Campaign Ambassador",
124
+ category: "เครื่องสำอางและน้ำหอมลักชัวรี (French Luxury Beauty)",
125
+ highlight: "ร่วมงานแคมเปญลิปสติกและน้ำหอม Libre สุดไอคอนิก",
126
+ isGlobal: false,
127
+ logoColor: "from-stone-900 to-zinc-950",
128
+ accentBg: "bg-zinc-500/10 text-zinc-700 dark:text-zinc-300 border-zinc-200 dark:border-zinc-700",
129
+ tier: "Luxury Haute Beauty"
130
+ },
66
131
  {
67
132
  id: "mac",
68
133
  year: 2025,