kiku-agent 1.0.0 → 1.0.4

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 (40) hide show
  1. package/README.md +293 -122
  2. package/package.json +60 -61
  3. package/server/agent/agentCore.js +892 -782
  4. package/server/agent/appPaths.js +21 -0
  5. package/server/agent/audioPlayer.js +739 -673
  6. package/server/agent/cli.js +1414 -1111
  7. package/server/agent/commandRegistry.js +74 -58
  8. package/server/agent/commands.js +1694 -788
  9. package/server/agent/config.js +264 -90
  10. package/server/agent/contextCompactor.js +79 -0
  11. package/server/agent/doctor.js +69 -0
  12. package/server/agent/eventBus.js +107 -103
  13. package/server/agent/gpuMetrics.js +189 -0
  14. package/server/agent/llmClient.js +369 -200
  15. package/server/agent/mcpClient.js +364 -0
  16. package/server/agent/memory.js +670 -566
  17. package/server/agent/modelManager.js +77 -0
  18. package/server/agent/research.js +202 -0
  19. package/server/agent/session.js +244 -239
  20. package/server/agent/subagent.js +116 -0
  21. package/server/agent/theme.js +125 -124
  22. package/server/agent/toolRouter.js +201 -0
  23. package/server/agent/tools.js +1679 -1528
  24. package/server/agent/ui.js +760 -469
  25. package/server/agent/youtubeAuth.js +144 -65
  26. package/server/mcp/claude-desktop-config.example.json +1 -1
  27. package/server/mcp/data/discography.js +36 -36
  28. package/server/mcp/dataAdapter.js +102 -0
  29. package/server/mcp/examples/claude-code.mcp.example.json +19 -0
  30. package/server/mcp/examples/claude-desktop.example.json +11 -0
  31. package/server/mcp/examples/cline.example.json +13 -0
  32. package/server/mcp/examples/cursor.example.json +14 -0
  33. package/server/mcp/examples/gemini-cli.example.json +17 -0
  34. package/server/mcp/examples/opencode.example.json +21 -0
  35. package/server/mcp/examples/vscode.example.json +19 -0
  36. package/server/mcp/examples/windsurf.example.json +14 -0
  37. package/server/mcp/handler.js +85 -159
  38. package/server/mcp/security.js +140 -138
  39. package/server/mcp/server.js +129 -100
  40. package/server/mcp/stdio.js +4 -2
@@ -2,41 +2,64 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { spawn, execSync } from 'child_process';
4
4
  import { fileURLToPath } from 'url';
5
+ import { KIKU_DATA_DIR } from './appPaths.js';
5
6
 
6
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
8
  const ENV_PATH = path.resolve(__dirname, '..', '..', '.env');
9
+ const AUTH_CONFIG_PATH = path.join(KIKU_DATA_DIR, 'yt_auth.json');
10
+ const SECURE_COOKIE_PATH = path.join(KIKU_DATA_DIR, 'cookies.txt');
8
11
  const YTDLP_CMD = process.env.KIKU_YTDLP || 'yt-dlp';
9
12
 
10
- const BROWSERS = ['chrome', 'edge', 'brave', 'firefox'];
11
-
12
13
 
14
+ const BROWSERS = ['firefox', 'chrome', 'edge', 'brave'];
13
15
 
14
16
  function findBrowserExe(browser) {
15
17
  const isWin = process.platform === 'win32';
16
18
  const probes = {
17
- chrome: { cmd: isWin ? 'chrome' : 'google-chrome', paths: [
18
- 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
19
- (process.env.LOCALAPPDATA || '') + '\\Google\\Chrome\\Application\\chrome.exe',
20
- 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
21
- ]},
22
- edge: { cmd: isWin ? 'msedge' : 'microsoft-edge', paths: [
23
- 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
24
- 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe'
25
- ]},
26
- brave: { cmd: isWin ? 'brave' : 'brave-browser', paths: [
27
- 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
28
- (process.env.LOCALAPPDATA || '') + '\\BraveSoftware\\Brave-Browser\\Application\\brave.exe'
29
- ]},
30
- firefox: { cmd: isWin ? 'firefox' : 'firefox', paths: [
31
- 'C:\\Program Files\\Mozilla Firefox\\firefox.exe',
32
- (process.env.LOCALAPPDATA || '') + '\\Mozilla Firefox\\firefox.exe'
33
- ]}
19
+ firefox: {
20
+ cmd: isWin ? 'firefox' : 'firefox',
21
+ paths: [
22
+ 'C:\\Program Files\\Mozilla Firefox\\firefox.exe',
23
+ 'C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe',
24
+ (process.env.LOCALAPPDATA || '') + '\\Mozilla Firefox\\firefox.exe'
25
+ ]
26
+ },
27
+ chrome: {
28
+ cmd: isWin ? 'chrome' : 'google-chrome',
29
+ paths: [
30
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
31
+ (process.env.LOCALAPPDATA || '') + '\\Google\\Chrome\\Application\\chrome.exe',
32
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
33
+ ]
34
+ },
35
+ edge: {
36
+ cmd: isWin ? 'msedge' : 'microsoft-edge',
37
+ paths: [
38
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
39
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
40
+ (process.env.LOCALAPPDATA || '') + '\\Microsoft\\Edge\\Application\\msedge.exe'
41
+ ]
42
+ },
43
+ brave: {
44
+ cmd: isWin ? 'brave' : 'brave-browser',
45
+ paths: [
46
+ 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
47
+ (process.env.LOCALAPPDATA || '') + '\\BraveSoftware\\Brave-Browser\\Application\\brave.exe'
48
+ ]
49
+ }
34
50
  };
51
+
35
52
  const p = probes[browser];
36
53
  if (!p) return null;
37
- try { execSync(isWin ? `where ${p.cmd}` : `which ${p.cmd}`, { stdio: 'ignore', windowsHide: true }); return p.cmd; } catch {}
54
+ try {
55
+ execSync(isWin ? `where ${p.cmd}` : `which ${p.cmd}`, { stdio: 'ignore', windowsHide: true });
56
+ return p.cmd;
57
+ } catch {}
58
+
38
59
  if (isWin) {
39
- for (const fp of p.paths) { if (fs.existsSync(fp)) return fp; }
60
+ for (const fp of p.paths) {
61
+ if (fp && fs.existsSync(fp)) return fp;
62
+ }
40
63
  }
41
64
  return null;
42
65
  }
@@ -63,9 +86,35 @@ function openBrowser(url, browser) {
63
86
  }
64
87
  }
65
88
 
89
+ function readAuthConfig() {
90
+ try {
91
+ if (fs.existsSync(AUTH_CONFIG_PATH)) {
92
+ return JSON.parse(fs.readFileSync(AUTH_CONFIG_PATH, 'utf8')) || {};
93
+ }
94
+ } catch {}
95
+ return {};
96
+ }
66
97
 
98
+ function writeAuthConfig(updates) {
99
+ try {
100
+ fs.mkdirSync(KIKU_DATA_DIR, { recursive: true });
101
+ const current = readAuthConfig();
102
+ const merged = { ...current, ...updates };
103
+ fs.writeFileSync(AUTH_CONFIG_PATH, JSON.stringify(merged, null, 2), 'utf8');
104
+ } catch {}
105
+ }
67
106
 
68
107
  function loadEnv() {
108
+
109
+ const userConfig = readAuthConfig();
110
+ if (userConfig.KIKU_YT_BROWSER && process.env.KIKU_YT_BROWSER === undefined) {
111
+ process.env.KIKU_YT_BROWSER = userConfig.KIKU_YT_BROWSER;
112
+ }
113
+ if (userConfig.KIKU_YT_COOKIES && process.env.KIKU_YT_COOKIES === undefined) {
114
+ process.env.KIKU_YT_COOKIES = userConfig.KIKU_YT_COOKIES;
115
+ }
116
+
117
+
69
118
  try {
70
119
  if (!fs.existsSync(ENV_PATH)) return;
71
120
  const text = fs.readFileSync(ENV_PATH, 'utf8');
@@ -73,7 +122,7 @@ function loadEnv() {
73
122
  const m = line.match(/^\s*([\w.-]+)\s*=\s*(.*)\s*$/);
74
123
  if (!m) continue;
75
124
  const key = m[1];
76
- let val = m[2].replace(/^["']|["']$/g, '');
125
+ const val = m[2].replace(/^["']|["']$/g, '');
77
126
  if (process.env[key] === undefined) process.env[key] = val;
78
127
  }
79
128
  } catch {}
@@ -92,24 +141,32 @@ function readEnvMap() {
92
141
  return map;
93
142
  }
94
143
 
95
-
96
144
  function writeEnv(updates) {
97
- const map = readEnvMap();
98
- Object.assign(map, updates);
99
- const out = Object.entries(map).map(([k, v]) => `${k}=${v}`).join('\n') + '\n';
100
- fs.writeFileSync(ENV_PATH, out, 'utf8');
101
- }
102
145
 
146
+ for (const [k, v] of Object.entries(updates)) {
147
+ process.env[k] = v;
148
+ }
149
+ writeAuthConfig(updates);
150
+
151
+
152
+ try {
153
+ const map = readEnvMap();
154
+ Object.assign(map, updates);
155
+ const out = Object.entries(map).map(([k, v]) => `${k}=${v}`).join('\n') + '\n';
156
+ fs.writeFileSync(ENV_PATH, out, 'utf8');
157
+ } catch {}
158
+ }
103
159
 
104
160
  function hasSession(browser) {
105
161
  return new Promise((resolve) => {
106
- const timeoutMs = 12000;
162
+ const timeoutMs = 8000;
107
163
  const args = [
108
164
  '--cookies-from-browser', browser,
109
165
  '--print', '%(title)s',
110
166
  '--no-warnings',
167
+ '--no-playlist',
111
168
  '--no-check-certificates',
112
- 'ytsearch1:Ju Jingyi'
169
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
113
170
  ];
114
171
  const p = spawn(YTDLP_CMD, args, {
115
172
  windowsHide: true,
@@ -128,51 +185,71 @@ function hasSession(browser) {
128
185
  }
129
186
  }, timeoutMs);
130
187
 
131
- p.stdout.on('data', d => { out += d.toString(); });
132
- p.stderr.on('data', d => { err += d.toString(); });
133
- p.on('error', () => { if (!resolved) { resolved = true; clearTimeout(timer); resolve(false); } });
188
+ p.stdout?.on('data', d => { out += d.toString(); });
189
+ p.stderr?.on('data', d => { err += d.toString(); });
190
+ p.on('error', () => {
191
+ if (!resolved) {
192
+ resolved = true;
193
+ clearTimeout(timer);
194
+ resolve(false);
195
+ }
196
+ });
134
197
  p.on('close', (code) => {
135
198
  if (resolved) return;
136
199
  resolved = true;
137
200
  clearTimeout(timer);
138
201
  const title = out.trim();
139
- const blocked = /403|Forbidden|Sign in|confirm you'?re not a bot|Login Required|Private Video/i.test(err);
202
+ const blocked = /403|Forbidden|Sign in|confirm you'?re not a bot|Login Required|Private Video|database is locked/i.test(err);
140
203
  resolve(code === 0 && title.length > 0 && !blocked);
141
204
  });
142
205
  });
143
206
  }
144
207
 
145
-
146
-
147
208
  let _ensured = null;
148
209
  let _lastFailTime = 0;
149
- const RETRY_COOLDOWN_MS = 30000;
150
-
210
+ let _inFlightPromise = null;
211
+ const RETRY_COOLDOWN_MS = 30000;
151
212
 
152
213
  export async function ensureYouTubeCookies(opts = {}) {
153
-
154
214
  if (_ensured && _ensured.ok) return _ensured;
155
215
  if (_ensured && !_ensured.ok && (Date.now() - _lastFailTime) < RETRY_COOLDOWN_MS && !opts.force) {
156
216
  return _ensured;
157
217
  }
158
218
 
219
+
220
+ if (_inFlightPromise) {
221
+ return _inFlightPromise;
222
+ }
223
+
224
+ _inFlightPromise = (async () => {
225
+ try {
226
+ return await _ensureYouTubeCookiesImpl(opts);
227
+ } finally {
228
+ _inFlightPromise = null;
229
+ }
230
+ })();
231
+
232
+ return _inFlightPromise;
233
+ }
234
+
235
+ async function _ensureYouTubeCookiesImpl(opts = {}) {
159
236
  loadEnv();
160
237
 
161
-
162
- if (process.env.KIKU_YT_COOKIES && fs.existsSync(process.env.KIKU_YT_COOKIES)) {
163
- _ensured = { ok: true, source: 'file', cookiesPath: process.env.KIKU_YT_COOKIES };
238
+
239
+ const cookiesCandidate = process.env.KIKU_YT_COOKIES || (fs.existsSync(SECURE_COOKIE_PATH) ? SECURE_COOKIE_PATH : null);
240
+ if (cookiesCandidate && fs.existsSync(cookiesCandidate)) {
241
+ _ensured = { ok: true, source: 'file', cookiesPath: cookiesCandidate };
164
242
  return _ensured;
165
243
  }
166
244
 
167
245
 
168
246
  const available = detectBrowsers();
169
247
  if (!available.length) {
170
- _ensured = { ok: false, reason: 'no-browser', detail: 'No supported browser found (Chrome, Edge, Brave, Firefox)' };
248
+ _ensured = { ok: false, reason: 'no-browser', detail: 'No supported browser found (Firefox, Chrome, Edge, Brave)' };
171
249
  _lastFailTime = Date.now();
172
250
  return _ensured;
173
251
  }
174
252
 
175
-
176
253
  const preferred = process.env.KIKU_YT_BROWSER;
177
254
  const candidates = preferred && available.includes(preferred)
178
255
  ? [preferred, ...available.filter(b => b !== preferred)]
@@ -180,9 +257,7 @@ export async function ensureYouTubeCookies(opts = {}) {
180
257
 
181
258
  for (const b of candidates) {
182
259
  if (await hasSession(b)) {
183
-
184
260
  writeEnv({ KIKU_YT_BROWSER: b });
185
- process.env.KIKU_YT_BROWSER = b;
186
261
  _ensured = { ok: true, source: 'existing-session', browser: b };
187
262
  return _ensured;
188
263
  }
@@ -195,7 +270,7 @@ export async function ensureYouTubeCookies(opts = {}) {
195
270
  return _ensured;
196
271
  }
197
272
 
198
-
273
+
199
274
  const loginBrowser = candidates[0];
200
275
  const loginUrl = 'https://accounts.google.com/ServiceLogin?service=youtube&continue=https://www.youtube.com/';
201
276
  openBrowser(loginUrl, loginBrowser);
@@ -208,21 +283,24 @@ export async function ensureYouTubeCookies(opts = {}) {
208
283
  await new Promise(r => setTimeout(r, 2000));
209
284
 
210
285
  const deadline = Date.now() + (opts.timeout || 120000);
211
- const pollInterval = 3000;
286
+ const pollInterval = 4000;
212
287
  let attempt = 0;
213
288
 
214
289
  while (Date.now() < deadline) {
215
290
  attempt++;
216
291
  const remaining = Math.round((deadline - Date.now()) / 1000);
217
- process.stdout.write(`\r Checking... (attempt ${attempt}, ${remaining}s remaining) `);
292
+ if (!opts.silent && process.stdout.isTTY) {
293
+ process.stdout.write(`\r Checking session... (attempt ${attempt}, ${remaining}s remaining) `);
294
+ }
218
295
 
219
296
  for (const b of available) {
220
297
  if (await hasSession(b)) {
221
298
  writeEnv({ KIKU_YT_BROWSER: b });
222
- process.env.KIKU_YT_BROWSER = b;
223
- process.stdout.write('\r' + ' '.repeat(60) + '\r');
299
+ if (process.stdout.isTTY) {
300
+ process.stdout.write('\r' + ' '.repeat(65) + '\r');
301
+ }
224
302
  console.log(` [OK] YouTube session detected in ${b}.`);
225
- console.log(` Saved KIKU_YT_BROWSER=${b} to .env\n`);
303
+ console.log(` Saved preference (KIKU_YT_BROWSER=${b})\n`);
226
304
  _ensured = { ok: true, source: 'new-session', browser: b };
227
305
  return _ensured;
228
306
  }
@@ -231,14 +309,15 @@ export async function ensureYouTubeCookies(opts = {}) {
231
309
  await new Promise(r => setTimeout(r, pollInterval));
232
310
  }
233
311
 
234
- process.stdout.write('\r' + ' '.repeat(60) + '\r');
312
+ if (process.stdout.isTTY) {
313
+ process.stdout.write('\r' + ' '.repeat(65) + '\r');
314
+ }
235
315
  console.log(` [ERROR] Login timed out. You can retry by running the /login command.\n`);
236
316
  _ensured = { ok: false, reason: 'login-timeout' };
237
317
  _lastFailTime = Date.now();
238
318
  return _ensured;
239
319
  }
240
320
 
241
-
242
321
  export async function exportCookiesToFile() {
243
322
  loadEnv();
244
323
 
@@ -249,22 +328,22 @@ export async function exportCookiesToFile() {
249
328
  : available;
250
329
 
251
330
  for (const b of candidates) {
252
- const cookiesPath = path.resolve(__dirname, '..', '..', '.kiku_yt_cookies.txt');
253
331
  try {
332
+ fs.mkdirSync(KIKU_DATA_DIR, { recursive: true });
254
333
  const args = [
255
334
  '--cookies-from-browser', b,
256
- '--cookies', cookiesPath,
335
+ '--cookies', SECURE_COOKIE_PATH,
257
336
  '--no-warnings',
258
337
  '--skip-download',
259
- 'ytsearch1:__export_test__'
338
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
260
339
  ];
261
340
  await new Promise((resolve, reject) => {
262
341
  const p = spawn(YTDLP_CMD, args, { windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'] });
263
342
  let err = '';
264
- p.stderr.on('data', d => { err += d.toString(); });
343
+ p.stderr?.on('data', d => { err += d.toString(); });
265
344
  p.on('error', reject);
266
345
  p.on('close', code => {
267
- if (code === 0 && fs.existsSync(cookiesPath)) {
346
+ if (code === 0 && fs.existsSync(SECURE_COOKIE_PATH)) {
268
347
  resolve();
269
348
  } else {
270
349
  reject(new Error(err || `yt-dlp exited ${code}`));
@@ -272,9 +351,8 @@ export async function exportCookiesToFile() {
272
351
  });
273
352
  });
274
353
 
275
- writeEnv({ KIKU_YT_COOKIES: cookiesPath });
276
- process.env.KIKU_YT_COOKIES = cookiesPath;
277
- return { ok: true, browser: b, cookiesPath };
354
+ writeEnv({ KIKU_YT_COOKIES: SECURE_COOKIE_PATH });
355
+ return { ok: true, browser: b, cookiesPath: SECURE_COOKIE_PATH };
278
356
  } catch {}
279
357
  }
280
358
 
@@ -284,16 +362,17 @@ export async function exportCookiesToFile() {
284
362
  export function resetCookieCheck() {
285
363
  _ensured = null;
286
364
  _lastFailTime = 0;
365
+ _inFlightPromise = null;
287
366
  }
288
367
 
289
368
  export function getCookieStatus() {
290
369
  return {
291
370
  ensured: _ensured,
292
371
  envBrowser: process.env.KIKU_YT_BROWSER || null,
293
- envCookies: process.env.KIKU_YT_COOKIES || null,
294
- envPath: ENV_PATH
372
+ envCookies: process.env.KIKU_YT_COOKIES || (fs.existsSync(SECURE_COOKIE_PATH) ? SECURE_COOKIE_PATH : null),
373
+ envPath: ENV_PATH,
374
+ authConfigPath: AUTH_CONFIG_PATH
295
375
  };
296
376
  }
297
377
 
298
-
299
378
  loadEnv();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "mcpServers": {
3
- "kiku-mcp": {
3
+ "kiku": {
4
4
  "command": "kiku-mcp",
5
5
  "args": [],
6
6
  "description": "Ju Jingyi (Kiku) Fan Portal MCP Server — Filmography, Discography, Gallery, Timeline, Awards, Brands, and Career Data via MCP protocol."
@@ -1,36 +1,36 @@
1
- export const discographyData = [
2
- { id: "find-the-way", titleEn: "FIND THE WAY", titleCn: "FIND THE WAY", year: 2026, category: "single", duration: "3:30", youtubeId: "9AeErG03SZw" },
3
- { id: "set-fire", titleEn: "Set Fire to the Silent World", titleCn: "着火的沉默世界", year: 2026, category: "ost", duration: "3:55", youtubeId: "7FXGhAmHIAo", work: "Wanhua Sin" },
4
- { id: "not-suffering", titleEn: "Not Suffering", titleCn: "不苦", year: 2026, category: "ost", duration: "3:50", youtubeId: "DafSXZZo9AI", work: "Veil of Shadows" },
5
- { id: "eternal-fragrance-song", titleEn: "The Eternal Fragrance", titleCn: "千香", year: 2026, category: "ost", duration: "3:46", youtubeId: "vIanAvWb_HU", work: "The Eternal Fragrance" },
6
- { id: "rain-love", titleEn: "Yu Ai (Rain Love)", titleCn: "雨爱", year: 2026, category: "cover", duration: "4:10", youtubeId: "OMLpNyp8T7Q" },
7
- { id: "rosa", titleEn: "Rosa", titleCn: "Rosa", year: 2025, category: "single", duration: "3:20", youtubeId: "jZDzOLbd9OU" },
8
- { id: "sleepless-sunset", titleEn: "Sleepless Sunset", titleCn: "落日未眠", year: 2025, category: "single", duration: "3:45", youtubeId: "mYYD2JqZ1tE" },
9
- { id: "fall", titleEn: "Fall", titleCn: "落", year: 2024, category: "single", duration: "3:36", youtubeId: "8Nh4ZPhYKEc" },
10
- { id: "view-from-wall", titleEn: "View From The Wall", titleCn: "壁上观", year: 2024, category: "single", duration: "3:50", youtubeId: "Lge5XhVl9Fc" },
11
- { id: "ru-sha", titleEn: "Ru Sha", titleCn: "如纱", year: 2024, category: "ost", duration: "3:42", youtubeId: "lzDP2MfP3eA", work: "Sword and Fairy 4" },
12
- { id: "in-blossom-ost", titleEn: "In Blossom Main Theme", titleCn: "花间令", year: 2024, category: "ost", youtubeId: null, work: "In Blossom" },
13
- { id: "thoughts-of-joy", titleEn: "Thoughts of Joy", titleCn: "念思凡", year: 2023, category: "ost", duration: "4:05", youtubeId: "D-vEPdasbLo", work: "Beauty of Resilience" },
14
- { id: "dew-before-dawn", titleEn: "Dew Before Dawn", titleCn: "露珠在晨曦前消散", year: 2023, category: "ost", youtubeId: null, work: "Beauty of Resilience" },
15
- { id: "0.2s", titleEn: "0.2s", titleCn: "0.2s", year: 2022, category: "single", duration: "3:20", youtubeId: null },
16
- { id: "mo-li", titleEn: "Mo Li", titleCn: "莫离", year: 2021, category: "ost", duration: "4:12", youtubeId: "7VSAArsR-go", work: "Rebirth for You" },
17
- { id: "qing-cheng-shan-xia", titleEn: "Qing Cheng Shan Xia", titleCn: "青城山下", year: 2021, category: "single", youtubeId: null },
18
- { id: "fu-rong", titleEn: "Fu Rong", titleCn: "芙蓉", year: 2020, category: "ost", duration: "3:58", youtubeId: "9ke3efb9U7A", work: "The Blooms at Ruyi Pavilion" },
19
- { id: "ancient-painting", titleEn: "Ancient Painting", titleCn: "古画", year: 2020, category: "ost", youtubeId: null, work: "The Blooms at Ruyi Pavilion" },
20
- { id: "thousand-years", titleEn: "A Millennium's Wait", titleCn: "千年等一回", year: 2019, category: "ost", duration: "3:44", youtubeId: "-vf6jU96WI4", work: "The Legend of White Snake" },
21
- { id: "du-qing", titleEn: "Du Qing", titleCn: "渡情", year: 2019, category: "ost", youtubeId: null, work: "The Legend of White Snake" },
22
- { id: "wudaoying", titleEn: "Wu Dao Ying", titleCn: "舞导演", year: 2019, category: "single", youtubeId: null },
23
- { id: "gufeng-shanshui", titleEn: "Gufeng Shanshui", titleCn: "古风山水", year: 2019, category: "single", youtubeId: null },
24
- { id: "meng-lu", titleEn: "Meng Lu (Dream Road)", titleCn: "梦路", year: 2018, category: "single", youtubeId: null },
25
- { id: "zui-chi-luo", titleEn: "Zui Chi Luo", titleCn: "最赤裸", year: 2018, category: "single", youtubeId: null },
26
- ];
27
-
28
- export const musicVideos = [
29
- { id: "mv-find-the-way", titleEn: "FIND THE WAY MV", titleCn: "FIND THE WAY", year: 2026, category: "Official MV", youtubeId: "9AeErG03SZw", duration: "3:30" },
30
- { id: "mv-not-suffering", titleEn: "Not Suffering MV", titleCn: "不苦", year: 2026, category: "OST MV", youtubeId: "DafSXZZo9AI", duration: "3:50", work: "Veil of Shadows" },
31
- { id: "mv-rain-love", titleEn: "Yu Ai (Rain Love) Stage", titleCn: "雨爱", year: 2026, category: "Stage Performance", youtubeId: "OMLpNyp8T7Q", duration: "4:10" },
32
- { id: "mv-set-fire", titleEn: "Set Fire to the Silent World MV", titleCn: "着火的沉默世界", year: 2026, category: "OST MV", youtubeId: "7FXGhAmHIAo", duration: "3:55", work: "Wanhua Sin" },
33
- { id: "mv-rosa", titleEn: "Rosa MV", titleCn: "Rosa", year: 2025, category: "Official MV", youtubeId: "jZDzOLbd9OU", duration: "3:20" },
34
- { id: "mv-fall", titleEn: "Fall MV", titleCn: "落", year: 2024, category: "Official MV", youtubeId: "8Nh4ZPhYKEc", duration: "3:36" },
35
- { id: "mv-ru-sha", titleEn: "Ru Sha MV", titleCn: "如纱", year: 2024, category: "OST MV", youtubeId: "lzDP2MfP3eA", duration: "3:42", work: "Sword and Fairy 4" },
36
- ];
1
+ export const discographyData = [
2
+ { id: "find-the-way", titleEn: "FIND THE WAY", titleCn: "FIND THE WAY", year: 2026, category: "single", duration: "3:30", youtubeId: "9AeErG03SZw" },
3
+ { id: "set-fire", titleEn: "Set Fire to the Silent World", titleCn: "着火的沉默世界", year: 2026, category: "ost", duration: "3:55", youtubeId: "7FXGhAmHIAo", work: "Wanhua Sin" },
4
+ { id: "not-suffering", titleEn: "Not Suffering", titleCn: "不苦", year: 2026, category: "ost", duration: "3:50", youtubeId: "DafSXZZo9AI", work: "Veil of Shadows" },
5
+ { id: "eternal-fragrance-song", titleEn: "The Eternal Fragrance", titleCn: "千香", year: 2026, category: "ost", duration: "3:46", youtubeId: "vIanAvWb_HU", work: "The Eternal Fragrance" },
6
+ { id: "rain-love", titleEn: "Yu Ai (Rain Love)", titleCn: "雨爱", year: 2026, category: "cover", duration: "4:10", youtubeId: "OMLpNyp8T7Q" },
7
+ { id: "rosa", titleEn: "Rosa", titleCn: "Rosa", year: 2025, category: "single", duration: "3:20", youtubeId: "jZDzOLbd9OU" },
8
+ { id: "sleepless-sunset", titleEn: "Sleepless Sunset", titleCn: "落日未眠", year: 2025, category: "single", duration: "3:45", youtubeId: "mYYD2JqZ1tE" },
9
+ { id: "fall", titleEn: "Fall", titleCn: "落", year: 2024, category: "single", duration: "3:36", youtubeId: "8Nh4ZPhYKEc" },
10
+ { id: "view-from-wall", titleEn: "View From The Wall", titleCn: "壁上观", year: 2024, category: "single", duration: "3:50", youtubeId: "Lge5XhVl9Fc" },
11
+ { id: "ru-sha", titleEn: "Ru Sha", titleCn: "如纱", year: 2024, category: "ost", duration: "3:42", youtubeId: "lzDP2MfP3eA", work: "Sword and Fairy 4" },
12
+ { id: "in-blossom-ost", titleEn: "In Blossom Main Theme", titleCn: "花间令", year: 2024, category: "ost", youtubeId: null, work: "In Blossom" },
13
+ { id: "thoughts-of-joy", titleEn: "Thoughts of Joy", titleCn: "念思凡", year: 2023, category: "ost", duration: "4:05", youtubeId: "D-vEPdasbLo", work: "Beauty of Resilience" },
14
+ { id: "dew-before-dawn", titleEn: "Dew Before Dawn", titleCn: "露珠在晨曦前消散", year: 2023, category: "ost", youtubeId: null, work: "Beauty of Resilience" },
15
+ { id: "0.2s", titleEn: "0.2s", titleCn: "0.2s", year: 2022, category: "single", duration: "3:20", youtubeId: null },
16
+ { id: "mo-li", titleEn: "Mo Li", titleCn: "莫离", year: 2021, category: "ost", duration: "4:12", youtubeId: "7VSAArsR-go", work: "Rebirth for You" },
17
+ { id: "qing-cheng-shan-xia", titleEn: "Qing Cheng Shan Xia", titleCn: "青城山下", year: 2021, category: "single", youtubeId: null },
18
+ { id: "fu-rong", titleEn: "Fu Rong", titleCn: "芙蓉", year: 2020, category: "ost", duration: "3:58", youtubeId: "9ke3efb9U7A", work: "The Blooms at Ruyi Pavilion" },
19
+ { id: "ancient-painting", titleEn: "Ancient Painting", titleCn: "古画", year: 2020, category: "ost", youtubeId: null, work: "The Blooms at Ruyi Pavilion" },
20
+ { id: "thousand-years", titleEn: "A Millennium's Wait", titleCn: "千年等一回", year: 2019, category: "ost", duration: "3:44", youtubeId: "-vf6jU96WI4", work: "The Legend of White Snake" },
21
+ { id: "du-qing", titleEn: "Du Qing", titleCn: "渡情", year: 2019, category: "ost", youtubeId: null, work: "The Legend of White Snake" },
22
+ { id: "wudaoying", titleEn: "Wu Dao Ying", titleCn: "舞导演", year: 2019, category: "single", youtubeId: null },
23
+ { id: "gufeng-shanshui", titleEn: "Gufeng Shanshui", titleCn: "古风山水", year: 2019, category: "single", youtubeId: null },
24
+ { id: "meng-lu", titleEn: "Meng Lu (Dream Road)", titleCn: "梦路", year: 2018, category: "single", youtubeId: null },
25
+ { id: "zui-chi-luo", titleEn: "Zui Chi Luo", titleCn: "最赤裸", year: 2018, category: "single", youtubeId: null },
26
+ ];
27
+
28
+ export const musicVideos = [
29
+ { id: "mv-find-the-way", titleEn: "FIND THE WAY MV", titleCn: "FIND THE WAY", year: 2026, category: "Official MV", youtubeId: "9AeErG03SZw", duration: "3:30" },
30
+ { id: "mv-not-suffering", titleEn: "Not Suffering MV", titleCn: "不苦", year: 2026, category: "OST MV", youtubeId: "DafSXZZo9AI", duration: "3:50", work: "Veil of Shadows" },
31
+ { id: "mv-rain-love", titleEn: "Yu Ai (Rain Love) Stage", titleCn: "雨爱", year: 2026, category: "Stage Performance", youtubeId: "OMLpNyp8T7Q", duration: "4:10" },
32
+ { id: "mv-set-fire", titleEn: "Set Fire to the Silent World MV", titleCn: "着火的沉默世界", year: 2026, category: "OST MV", youtubeId: "7FXGhAmHIAo", duration: "3:55", work: "Wanhua Sin" },
33
+ { id: "mv-rosa", titleEn: "Rosa MV", titleCn: "Rosa", year: 2025, category: "Official MV", youtubeId: "jZDzOLbd9OU", duration: "3:20" },
34
+ { id: "mv-fall", titleEn: "Fall MV", titleCn: "落", year: 2024, category: "Official MV", youtubeId: "8Nh4ZPhYKEc", duration: "3:36" },
35
+ { id: "mv-ru-sha", titleEn: "Ru Sha MV", titleCn: "如纱", year: 2024, category: "OST MV", youtubeId: "lzDP2MfP3eA", duration: "3:42", work: "Sword and Fairy 4" },
36
+ ];
@@ -0,0 +1,102 @@
1
+ import { profileData } from '../../src/data/profileData.js';
2
+ import { worksData } from '../../src/data/worksData.js';
3
+ import { discographyData } from '../../src/data/discographyData.js';
4
+ import { musicVideoData } from '../../src/data/musicVideoData.js';
5
+ import { brandsData } from '../../src/data/brandsData.js';
6
+ import { socialUpdatesData } from '../../src/data/socialUpdatesData.js';
7
+ import { galleryData } from '../../src/data/galleryData.js';
8
+ import { awardsData } from '../../src/data/awardsData.js';
9
+ import { timelineData } from '../../src/data/timelineData.js';
10
+
11
+ export function getUnifiedProfile() {
12
+ return {
13
+ ...profileData,
14
+ nameEn: profileData.nameEn || 'Ju Jingyi',
15
+ nameCn: profileData.nameCn || '鞠婧祎',
16
+ nameTh: profileData.nameTh || 'จวีจิ้งอี',
17
+ fandomName: profileData.fandomName || 'Miju (蜜橘)',
18
+ slogan: profileData.slogan || '鞠世无双,婧然倾心',
19
+ };
20
+ }
21
+
22
+ export function getUnifiedWorks() {
23
+ return worksData.map(w => ({
24
+ id: w.id,
25
+ titleEn: w.titleEn || w.titleTh || w.titleCn,
26
+ titleCn: w.titleCn,
27
+ titleTh: w.titleTh,
28
+ year: w.year,
29
+ role: w.role,
30
+ characterName: w.role,
31
+ genre: w.genre || w.type || 'Drama',
32
+ featured: Boolean(w.featured),
33
+ synopsis: w.synopsis || '',
34
+ platform: w.platform || '',
35
+ }));
36
+ }
37
+
38
+ export function getUnifiedDiscography() {
39
+ const songs = [
40
+ ...(discographyData.latestReleases || []),
41
+ ...(discographyData.ostList || []),
42
+ ...(discographyData.singles || []),
43
+ ];
44
+
45
+ const map = new Map();
46
+ for (const s of songs) {
47
+ const key = s.titleCn || s.titleTh || s.titleEn || s.id;
48
+ if (!map.has(key)) {
49
+ map.set(key, {
50
+ id: s.id,
51
+ titleEn: s.titleEn || s.titleTh || s.titleCn,
52
+ titleCn: s.titleCn,
53
+ titleTh: s.titleTh,
54
+ year: s.year,
55
+ type: s.type || 'Single',
56
+ category: s.work ? 'ost' : 'single',
57
+ work: s.work || null,
58
+ youtubeId: s.youtubeId || null,
59
+ });
60
+ }
61
+ }
62
+ return Array.from(map.values()).sort((a, b) => (b.year || 0) - (a.year || 0));
63
+ }
64
+
65
+ export function getUnifiedAwards() {
66
+ return awardsData.map(a => ({
67
+ id: a.id,
68
+ title: a.title,
69
+ titleCn: a.titleCn || a.title,
70
+ year: a.year,
71
+ ceremony: a.ceremony || a.organization || '',
72
+ category: a.category || 'General',
73
+ })).sort((a, b) => (b.year || 0) - (a.year || 0));
74
+ }
75
+
76
+ export function getUnifiedBrands() {
77
+ return brandsData.map(b => ({
78
+ id: b.id,
79
+ brandNameEn: b.brand || b.nameEn || '',
80
+ brandNameCn: b.brandCn || b.nameCn || b.brand || '',
81
+ title: b.title || 'Brand Ambassador',
82
+ year: b.year,
83
+ isGlobal: Boolean(b.isGlobal),
84
+ category: b.category || 'Beauty / Fashion',
85
+ })).sort((a, b) => (b.year || 0) - (a.year || 0));
86
+ }
87
+
88
+ export function getUnifiedTimeline() {
89
+ return timelineData.map(m => ({
90
+ id: m.id,
91
+ year: m.year,
92
+ period: m.period || String(m.year),
93
+ title: m.title,
94
+ titleCn: m.titleCn || m.title,
95
+ description: m.description || '',
96
+ era: m.era || 'Solo Career',
97
+ })).sort((a, b) => (b.year || 0) - (a.year || 0));
98
+ }
99
+
100
+ export function getUnifiedGallery() {
101
+ return galleryData;
102
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "mcpServers": {
3
+ "kiku": {
4
+ "type": "stdio",
5
+ "command": "npx",
6
+ "args": ["-y", "-p", "kiku-agent", "kiku-mcp"],
7
+ "env": {
8
+ "KIKU_MCP_ENABLE_WEB_TOOLS": "true"
9
+ }
10
+ },
11
+ "kiku-remote": {
12
+ "type": "http",
13
+ "url": "https://jujingyi-kikuuu.vercel.app/mcp",
14
+ "headers": {
15
+ "X-API-Key": "YOUR_API_KEY"
16
+ }
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "kiku": {
4
+ "command": "npx",
5
+ "args": ["-y", "-p", "kiku-agent", "kiku-mcp"],
6
+ "env": {
7
+ "KIKU_MCP_ENABLE_WEB_TOOLS": "true"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcpServers": {
3
+ "kiku": {
4
+ "command": "npx",
5
+ "args": ["-y", "-p", "kiku-agent", "kiku-mcp"],
6
+ "env": {
7
+ "KIKU_MCP_ENABLE_WEB_TOOLS": "true"
8
+ },
9
+ "disabled": false,
10
+ "alwaysAllow": []
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "mcpServers": {
3
+ "kiku": {
4
+ "command": "npx",
5
+ "args": ["-y", "-p", "kiku-agent", "kiku-mcp"],
6
+ "env": {
7
+ "KIKU_MCP_ENABLE_WEB_TOOLS": "true"
8
+ }
9
+ },
10
+ "kiku-remote": {
11
+ "url": "https://jujingyi-kikuuu.vercel.app/mcp"
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "mcpServers": {
3
+ "kiku": {
4
+ "command": "npx",
5
+ "args": ["-y", "-p", "kiku-agent", "kiku-mcp"],
6
+ "env": {
7
+ "KIKU_MCP_ENABLE_WEB_TOOLS": "true"
8
+ }
9
+ },
10
+ "kiku-remote": {
11
+ "httpUrl": "https://jujingyi-kikuuu.vercel.app/mcp",
12
+ "headers": {
13
+ "X-API-Key": "YOUR_API_KEY"
14
+ }
15
+ }
16
+ }
17
+ }