kiku-agent 1.0.0

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 (45) hide show
  1. package/README.md +122 -0
  2. package/package.json +85 -0
  3. package/server/agent/agentCore.js +782 -0
  4. package/server/agent/agentState.js +140 -0
  5. package/server/agent/audioPlayer.js +673 -0
  6. package/server/agent/cli.js +1111 -0
  7. package/server/agent/commandRegistry.js +58 -0
  8. package/server/agent/commands.js +788 -0
  9. package/server/agent/config.js +90 -0
  10. package/server/agent/errors.js +9 -0
  11. package/server/agent/eventBus.js +103 -0
  12. package/server/agent/llmClient.js +200 -0
  13. package/server/agent/memory.js +566 -0
  14. package/server/agent/sanitizer.js +314 -0
  15. package/server/agent/session.js +239 -0
  16. package/server/agent/theme.js +124 -0
  17. package/server/agent/tools.js +1528 -0
  18. package/server/agent/ui.js +469 -0
  19. package/server/agent/webSearch.js +719 -0
  20. package/server/agent/youtubeAuth.js +299 -0
  21. package/server/mcp/claude-desktop-config.example.json +9 -0
  22. package/server/mcp/data/awards.js +14 -0
  23. package/server/mcp/data/brands.js +9 -0
  24. package/server/mcp/data/discography.js +36 -0
  25. package/server/mcp/data/gallery.js +25 -0
  26. package/server/mcp/data/index.js +7 -0
  27. package/server/mcp/data/profile.js +44 -0
  28. package/server/mcp/data/timeline.js +16 -0
  29. package/server/mcp/data/works.js +104 -0
  30. package/server/mcp/handler.js +159 -0
  31. package/server/mcp/prompts.js +143 -0
  32. package/server/mcp/resources.js +54 -0
  33. package/server/mcp/security.js +138 -0
  34. package/server/mcp/server.js +100 -0
  35. package/server/mcp/stdio.js +33 -0
  36. package/server/mcp/tools.js +126 -0
  37. package/src/data/awardsData.js +146 -0
  38. package/src/data/brandsData.js +249 -0
  39. package/src/data/discographyData.js +874 -0
  40. package/src/data/galleryData.js +666 -0
  41. package/src/data/musicVideoData.js +406 -0
  42. package/src/data/profileData.js +156 -0
  43. package/src/data/socialUpdatesData.js +381 -0
  44. package/src/data/timelineData.js +172 -0
  45. package/src/data/worksData.js +516 -0
@@ -0,0 +1,90 @@
1
+ export const config = {
2
+ lmStudio: {
3
+ baseUrl: process.env.LMSTUDIO_BASE_URL || process.env.OPENAI_BASE_URL || 'http://127.0.0.1:1234/v1',
4
+ model: 'google/gemma-4-e4b',
5
+ defaultModel: 'google/gemma-4-e4b',
6
+ temperature: 0.3,
7
+ maxTokens: 1536,
8
+ requestTimeoutMs: 180000,
9
+ },
10
+
11
+ agent: {
12
+ maxIterations: 4,
13
+ slidingWindowSize: 8,
14
+ language: 'auto',
15
+ systemPersona: `You are "Kiku AI" (จวีจิ้งอี AI ผู้ช่วยอัจฉริยะ / 鞠婧祎智能AI助手), an expert AI specialist on the Chinese singer, actress, and artist Ju Jingyi (鞠婧祎 - จวีจิ้งอี).
16
+ You have access to a verified API database of her dramas, songs, character roles, brand endorsements, timeline milestones, awards, and music videos, as well as LIVE Internet Search (web_search, fetch_webpage, web_extract, fetch_json).
17
+
18
+ === CRITICAL INSTRUCTIONS ===
19
+
20
+ 1. PROPORTIONAL RESPONSE & GREETINGS:
21
+ - CASUAL GREETINGS ("Hi", "Hello", "สวัสดี", "你好"): Reply warmly in 1-2 sentences. DO NOT call tools or dump catalogs.
22
+ - SHORT QUESTIONS: Give direct, concise 1-3 sentence answer.
23
+ - BROAD REQUESTS: Provide structured, well-organized list with key details and clickable links.
24
+
25
+ 2. DYNAMIC LANGUAGE MIRRORING:
26
+ - THAI INPUT ➔ Reply in polite Thai (ใช้ ค่ะ/นะคะ เสมอ)
27
+ - CHINESE INPUT ➔ Reply in fluent, polite Chinese (简体/繁体中文)
28
+ - ENGLISH INPUT ➔ Reply in polished, professional English
29
+ - MIXED INPUT ➔ Match dominant language while preserving bilingual titles
30
+
31
+ 3. RESPONSE FORMATTING (CRITICAL — NO SYSTEM LEAK):
32
+ - NEVER include "Thinking Process", "Tool Calling", "Action:", "Action Input:", or any internal reasoning steps in your response text.
33
+ - NEVER include tool call JSON, function names, or parameter schemas in your response.
34
+ - NEVER include advertisements, sponsor mentions, or promotional content in your response.
35
+ - Emoji are optional and must be semantic: use at most one relevant emoji per response, never decorate every heading or bullet.
36
+ - Your response must be CLEAN user-facing content ONLY — formatted with Markdown headers (###), bold (**text**), bullet points (•), and clickable links.
37
+ - Think silently using tools, then present ONLY the final answer to the user.
38
+
39
+ 4. REAL YOUTUBE & MEDIA LINKS (WHEN DISCUSSING MEDIA):
40
+ - Provide REAL, WORKING, CLICKABLE links in Markdown format:
41
+ • Direct Watch Link: [Watch on YouTube](https://www.youtube.com/watch?v=VIDEO_ID)
42
+ • Search Link: https://www.youtube.com/results?search_query=QUERY
43
+ - NEVER use fake placeholder links like "watch?v=xxx".
44
+
45
+ 5. SMART TOOL SELECTION (USE TOOLS TO GATHER FACTS, THEN ANSWER):
46
+ - Music videos & stages ➔ get_music_videos or get_media_links
47
+ - Songs, OSTs, Singles ➔ search_discography or get_media_links
48
+ - Drama works & trailers ➔ search_works or get_work_detail
49
+ - Profile & fandom ➔ get_profile_and_fandom
50
+ - Brand endorsements ➔ get_brand_endorsements
51
+ - Breaking news & external facts ➔ web_search or web_extract (preferred for multi-source research)
52
+ - Deep research on a topic ➔ web_extract (search + auto-fetch top results)
53
+ - Read a specific webpage ➔ fetch_webpage (cheerio-powered extraction)
54
+ - Fetch REST API / JSON data ➔ fetch_json
55
+ - Read local project files ➔ read_local_file
56
+ - Browse directory structure ➔ list_directory
57
+ - Visual screenshot ➔ screenshot_webpage
58
+ - If local tools return insufficient results, ALWAYS fall back to web_search or web_extract.
59
+
60
+ 6. FACT RETRIEVAL & CHRONOLOGICAL ACCURACY:
61
+ - THE CURRENT YEAR IS 2026.
62
+ - Always use tools to verify facts before answering. Do NOT hallucinate dates, titles, or links.
63
+ - Sort chronologically (newest first). Never repeat the same statement.
64
+ - Ju Jingyi's key 2026 projects:
65
+ • "Veil of Shadows (月鳞绮纪)" — Role: Lu Wuyi (露芜衣) on Youku
66
+ • "The Eternal Fragrance (千香)" — Role: Jiang Lifei (姜黎非) with Song Weilong on Youku
67
+ • "Wanhua Sin (万华世界)" — Promo OST: "Set Fire to the Silent World (着火的沉默世界)"
68
+ • 2026 Solo Single: "FIND THE WAY" (YouTube: 9AeErG03SZw)
69
+ • 2026 Special Live Stage: "Yu Ai / 雨爱" (YouTube: OMLpNyp8T7Q)
70
+ - Classic hits: "In Blossom (2024)", "Sword and Fairy 4 (2024)", "Beauty of Resilience (2023)", "Rebirth for You (2021)", "The Blooms at Ruyi Pavilion (2020)", "The Legend of White Snake (2019)", "Legend of Yunxi (2018)".
71
+
72
+ 7. COMPREHENSIVE KNOWLEDGE — LEGAL & CAREER DETAILS:
73
+ When discussing Ju Jingyi's career or controversies, be accurate and specific:
74
+ - CAREER PATH: SNH48 2nd gen (2013) → "4000-year idol" nickname (2014) → General Election winner 2 consecutive years (2016-2017) → Star Hall of Fame → Solo career → Acting breakthrough with "Legend of Yunxi" (2018).
75
+ - CONTRACT DISPUTE (Star48/Siba Media): The supplementary agreement dispute. JJY's side claims main contract ended June 2024, alleges forged signatures. Star48 claims contract extends to 2033. Tax evasion tip-off report was filed by Star48's side; JJY denies all allegations and confirms tax compliance.
76
+ - PORTRAIT/COPYRIGHT INFRINGEMENT CASES: Ju Jingyi has filed lawsuits against beauty clinics and brands for unauthorized use of her image/likeness (Right of Portrait / 肖像权纠纷). This is a separate legal matter from the Star48 dispute.
77
+ - When asked about legal issues, use web_search or web_extract to get the latest verified information.
78
+
79
+ 8. CONCISE & SMART RESPONSE QUALITY:
80
+ - Match the depth of your answer to what the user asked.
81
+ - Do not dump the entire database unless specifically requested.
82
+ - Start directly with the answer — no template greetings like "Hello!" or "I searched...".
83
+ - Keep the interface visually restrained; omit emoji unless one clearly improves recognition of the topic.
84
+
85
+ 9. PROACTIVE MUSIC INVITATION:
86
+ - When discussing a specific song/MV/OST, naturally ask if the user wants to listen.
87
+ - Example: "เพลง FIND THE WAY จังหวะทรงพลังมากเลยค่ะ! อยากให้ Kiku เปิดให้ฟังมั้ยคะ?"
88
+ - If user agrees → call play_music. If user says "Stop" → call stop_music.`,
89
+ }
90
+ };
@@ -0,0 +1,9 @@
1
+ export function createAbortError(message = 'Query cancelled by user.') {
2
+ const error = new Error(message);
3
+ error.name = 'AbortError';
4
+ return error;
5
+ }
6
+
7
+ export function isAbortError(error) {
8
+ return error?.name === 'AbortError';
9
+ }
@@ -0,0 +1,103 @@
1
+ class EventBus {
2
+ constructor() {
3
+ this._listeners = new Map();
4
+ this._history = [];
5
+ this._maxHistory = 50;
6
+ }
7
+
8
+ on(event, fn) {
9
+ if (!this._listeners.has(event)) this._listeners.set(event, new Set());
10
+ this._listeners.get(event).add(fn);
11
+ return () => this.off(event, fn);
12
+ }
13
+
14
+ once(event, fn) {
15
+ const wrapper = (data, name) => {
16
+ this.off(event, wrapper);
17
+ fn(data, name);
18
+ };
19
+ return this.on(event, wrapper);
20
+ }
21
+
22
+ off(event, fn) {
23
+ const set = this._listeners.get(event);
24
+ if (set) set.delete(fn);
25
+ }
26
+
27
+ emit(event, data) {
28
+ this._history.push({ event, data, ts: Date.now() });
29
+ if (this._history.length > this._maxHistory) {
30
+ this._history.shift();
31
+ }
32
+
33
+ const exactSet = this._listeners.get(event);
34
+ if (exactSet) {
35
+ for (const fn of exactSet) {
36
+ try { fn(data, event); } catch { /* swallow */ }
37
+ }
38
+ }
39
+
40
+ const ns = event.split(':')[0];
41
+ const wildcardEvent = ns + ':*';
42
+ const wildcardSet = this._listeners.get(wildcardEvent);
43
+ if (wildcardSet) {
44
+ for (const fn of wildcardSet) {
45
+ try { fn(data, event); } catch { /* swallow */ }
46
+ }
47
+ }
48
+
49
+ const globalSet = this._listeners.get('*');
50
+ if (globalSet) {
51
+ for (const fn of globalSet) {
52
+ try { fn(data, event); } catch { /* swallow */ }
53
+ }
54
+ }
55
+ }
56
+
57
+ getHistory(limit = this._maxHistory) {
58
+ return this._history.slice(-limit);
59
+ }
60
+
61
+ clear() {
62
+ this._listeners.clear();
63
+ this._history.length = 0;
64
+ }
65
+ }
66
+
67
+ export const eventBus = new EventBus();
68
+
69
+ export const Events = Object.freeze({
70
+ AGENT_QUERY_START: 'agent:query:start',
71
+ AGENT_QUERY_END: 'agent:query:end',
72
+ AGENT_QUERY_ERROR: 'agent:query:error',
73
+ AGENT_STATE_CHANGE: 'agent:state:change',
74
+
75
+ AGENT_REASONING_START: 'agent:reasoning:start',
76
+ AGENT_REASONING_DELTA: 'agent:reasoning:delta',
77
+ AGENT_REASONING_END: 'agent:reasoning:end',
78
+ AGENT_TOKEN_START: 'agent:token:start',
79
+ AGENT_TOKEN_DELTA: 'agent:token:delta',
80
+ AGENT_TOKEN_END: 'agent:token:end',
81
+ AGENT_FINAL_ANSWER: 'agent:final:answer',
82
+
83
+ AGENT_TOOL_START: 'agent:tool:start',
84
+ AGENT_TOOL_RESULT: 'agent:tool:result',
85
+ AGENT_TOOL_ERROR: 'agent:tool:error',
86
+
87
+ MUSIC_PLAY: 'music:play',
88
+ MUSIC_STOP: 'music:stop',
89
+ MUSIC_QUEUE_CHANGE: 'music:queue:change',
90
+ MUSIC_STATE_CHANGE: 'music:state:change',
91
+
92
+ SESSION_SAVED: 'session:saved',
93
+ SESSION_ARCHIVED: 'session:archived',
94
+ MEMORY_FACT_ADDED: 'memory:fact:added',
95
+
96
+ UI_RENDER: 'ui:render',
97
+ UI_CLEAR: 'ui:clear',
98
+ UI_MESSAGE: 'ui:message',
99
+
100
+ QUEUE_ITEM_ADDED: 'queue:item:added',
101
+ QUEUE_ITEM_CONSUMED: 'queue:item:consumed',
102
+ QUEUE_CLEARED: 'queue:cleared',
103
+ });
@@ -0,0 +1,200 @@
1
+ import { config } from './config.js';
2
+
3
+ export class LMStudioClient {
4
+ constructor(options = {}) {
5
+ this.baseUrl = (options.baseUrl || config.lmStudio.baseUrl).replace(/\/+$/, '');
6
+ this.model = options.model || config.lmStudio.model || 'google/gemma-4-e4b';
7
+ this.timeout = options.timeout || config.lmStudio.requestTimeoutMs;
8
+ }
9
+
10
+ async checkConnection() {
11
+ try {
12
+ const controller = new AbortController();
13
+ const timeoutId = setTimeout(() => controller.abort(), 4000);
14
+
15
+ const res = await fetch(`${this.baseUrl}/models`, {
16
+ method: 'GET',
17
+ headers: { 'Content-Type': 'application/json' },
18
+ signal: controller.signal
19
+ });
20
+
21
+ clearTimeout(timeoutId);
22
+
23
+ if (!res.ok) {
24
+ return {
25
+ connected: false,
26
+ error: `LM Studio returned status ${res.status}: ${res.statusText}`,
27
+ models: []
28
+ };
29
+ }
30
+
31
+ const data = await res.json();
32
+ const models = (data.data || []).map(m => m.id);
33
+
34
+ const matchedGemma = models.find(m =>
35
+ m === 'google/gemma-4-e4b' ||
36
+ m === 'gemma-4-e4b' ||
37
+ m.toLowerCase().includes('gemma-4-e4b') ||
38
+ (m.toLowerCase().includes('gemma') && m.toLowerCase().includes('e4b'))
39
+ );
40
+
41
+ if (matchedGemma) {
42
+ this.model = matchedGemma;
43
+ }
44
+
45
+ return {
46
+ connected: true,
47
+ baseUrl: this.baseUrl,
48
+ models,
49
+ activeModel: this.model,
50
+ isModelAvailable: !!matchedGemma
51
+ };
52
+ } catch (err) {
53
+ return {
54
+ connected: false,
55
+ error: err.name === 'AbortError'
56
+ ? `Connection timed out at ${this.baseUrl}`
57
+ : `Cannot reach LM Studio at ${this.baseUrl} (${err.message})`,
58
+ models: []
59
+ };
60
+ }
61
+ }
62
+
63
+ async chat({
64
+ messages,
65
+ tools = null,
66
+ temperature = 0.3,
67
+ maxTokens = 1536,
68
+ stream = true,
69
+ signal = null,
70
+ onToken = null,
71
+ onThought = null
72
+ }) {
73
+ const payload = {
74
+ model: this.model,
75
+ messages,
76
+ temperature,
77
+ max_tokens: maxTokens,
78
+ stream
79
+ };
80
+
81
+ if (tools && Array.isArray(tools) && tools.length > 0) {
82
+ payload.tools = tools;
83
+ payload.tool_choice = "auto";
84
+ }
85
+
86
+ const controller = new AbortController();
87
+ let timeoutId = setTimeout(() => controller.abort(), this.timeout);
88
+
89
+ const abortHandler = () => controller.abort();
90
+ if (signal) signal.addEventListener('abort', abortHandler, { once: true });
91
+
92
+ const resetTimeout = () => {
93
+ clearTimeout(timeoutId);
94
+ timeoutId = setTimeout(() => controller.abort(), this.timeout);
95
+ };
96
+
97
+ try {
98
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
99
+ method: 'POST',
100
+ headers: {
101
+ 'Content-Type': 'application/json',
102
+ 'Authorization': 'Bearer lm-studio'
103
+ },
104
+ body: JSON.stringify(payload),
105
+ signal: controller.signal
106
+ });
107
+
108
+ resetTimeout();
109
+
110
+ if (!res.ok) {
111
+ const errorText = await res.text();
112
+ throw new Error(`LM Studio error (${res.status}): ${errorText}`);
113
+ }
114
+
115
+ if (!stream) {
116
+ const data = await res.json();
117
+ clearTimeout(timeoutId);
118
+ if (signal) signal.removeEventListener('abort', abortHandler);
119
+ return data.choices && data.choices[0] ? data.choices[0].message : null;
120
+ }
121
+
122
+
123
+ const reader = res.body.getReader();
124
+ const decoder = new TextDecoder('utf-8');
125
+ let fullContent = '';
126
+ let fullReasoning = '';
127
+ let toolCalls = [];
128
+ let buffer = '';
129
+
130
+ while (true) {
131
+ const { done, value } = await reader.read();
132
+ if (done) break;
133
+
134
+ resetTimeout();
135
+
136
+ buffer += decoder.decode(value, { stream: true });
137
+ const lines = buffer.split('\n');
138
+ buffer = lines.pop() || '';
139
+
140
+ for (const line of lines) {
141
+ const trimmed = line.trim();
142
+ if (!trimmed || !trimmed.startsWith('data:')) continue;
143
+ const jsonStr = trimmed.replace(/^data:\s*/, '');
144
+ if (jsonStr === '[DONE]') continue;
145
+
146
+ try {
147
+ const parsed = JSON.parse(jsonStr);
148
+ const delta = parsed.choices?.[0]?.delta;
149
+ if (!delta) continue;
150
+
151
+ if (delta.reasoning_content) {
152
+ fullReasoning += delta.reasoning_content;
153
+ if (onThought) onThought(delta.reasoning_content);
154
+ }
155
+
156
+ if (delta.content) {
157
+ fullContent += delta.content;
158
+ if (onToken) onToken(delta.content);
159
+ }
160
+
161
+ if (delta.tool_calls) {
162
+ for (const tc of delta.tool_calls) {
163
+ const idx = tc.index ?? 0;
164
+ if (!toolCalls[idx]) {
165
+ toolCalls[idx] = {
166
+ id: tc.id || `call_${Date.now()}_${idx}`,
167
+ type: 'function',
168
+ function: { name: '', arguments: '' }
169
+ };
170
+ }
171
+ if (tc.function?.name) toolCalls[idx].function.name += tc.function.name;
172
+ if (tc.function?.arguments) toolCalls[idx].function.arguments += tc.function.arguments;
173
+ }
174
+ }
175
+ } catch {
176
+ }
177
+ }
178
+ }
179
+
180
+ clearTimeout(timeoutId);
181
+
182
+ const resultMessage = {
183
+ role: 'assistant',
184
+ content: fullContent,
185
+ reasoning_content: fullReasoning
186
+ };
187
+
188
+ if (toolCalls.length > 0) {
189
+ resultMessage.tool_calls = toolCalls.filter(Boolean);
190
+ }
191
+
192
+ if (signal) signal.removeEventListener('abort', abortHandler);
193
+ return resultMessage;
194
+ } catch (err) {
195
+ clearTimeout(timeoutId);
196
+ if (signal) signal.removeEventListener('abort', abortHandler);
197
+ throw err;
198
+ }
199
+ }
200
+ }