kiku-agent 1.0.0 → 1.0.3

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 (39) 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/mcp/claude-desktop-config.example.json +1 -1
  26. package/server/mcp/data/discography.js +36 -36
  27. package/server/mcp/dataAdapter.js +102 -0
  28. package/server/mcp/examples/claude-code.mcp.example.json +19 -0
  29. package/server/mcp/examples/claude-desktop.example.json +11 -0
  30. package/server/mcp/examples/cline.example.json +13 -0
  31. package/server/mcp/examples/cursor.example.json +14 -0
  32. package/server/mcp/examples/gemini-cli.example.json +17 -0
  33. package/server/mcp/examples/opencode.example.json +21 -0
  34. package/server/mcp/examples/vscode.example.json +19 -0
  35. package/server/mcp/examples/windsurf.example.json +14 -0
  36. package/server/mcp/handler.js +85 -159
  37. package/server/mcp/security.js +140 -138
  38. package/server/mcp/server.js +129 -100
  39. package/server/mcp/stdio.js +4 -2
@@ -1,200 +1,369 @@
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
- }
1
+ import { config, CLOUD_PROVIDERS } from './config.js';
2
+
3
+ export class LLMClient {
4
+ constructor(options = {}) {
5
+ const providerId = (options.provider || config.activeProvider || 'lmstudio').toLowerCase();
6
+ const providerDef = CLOUD_PROVIDERS[providerId] || CLOUD_PROVIDERS.lmstudio;
7
+
8
+ this.provider = providerId;
9
+ this.providerName = providerDef.name || providerId;
10
+ this.baseUrl = (options.baseUrl || providerDef.baseUrl || config.lmStudio.baseUrl).replace(/\/+$/, '');
11
+ this.apiKey = options.apiKey !== undefined
12
+ ? options.apiKey
13
+ : (providerDef.apiKey !== undefined ? providerDef.apiKey : (providerDef.isLocal ? (config.lmStudio.apiKey || 'lm-studio') : ''));
14
+ this.model = options.model || providerDef.defaultModel || config.lmStudio.model || 'google/gemma-4-e4b';
15
+ this.timeout = options.timeout || config.lmStudio.requestTimeoutMs || 180000;
16
+ this.extraHeaders = { ...(providerDef.extraHeaders || {}), ...(options.extraHeaders || {}) };
17
+ }
18
+
19
+ switchProvider(providerId, options = {}) {
20
+ const pId = String(providerId || '').toLowerCase().trim();
21
+ const providerDef = CLOUD_PROVIDERS[pId];
22
+ if (!providerDef) {
23
+ throw new Error(`Unknown provider "${providerId}". Supported: ${Object.keys(CLOUD_PROVIDERS).join(', ')}`);
24
+ }
25
+
26
+ this.provider = pId;
27
+ this.providerName = providerDef.name || pId;
28
+ this.baseUrl = (options.baseUrl || providerDef.baseUrl).replace(/\/+$/, '');
29
+ this.apiKey = options.apiKey !== undefined ? options.apiKey : (providerDef.apiKey || '');
30
+ this.model = options.model || providerDef.defaultModel;
31
+ this.extraHeaders = { ...(providerDef.extraHeaders || {}), ...(options.extraHeaders || {}) };
32
+ return this.getProviderInfo();
33
+ }
34
+
35
+ setApiKey(apiKey) {
36
+ this.apiKey = String(apiKey || '').trim();
37
+ return this.getProviderInfo();
38
+ }
39
+
40
+ setModel(model) {
41
+ const trimmed = String(model || '').trim();
42
+ if (!trimmed) throw new Error('A model ID is required.');
43
+ this.model = trimmed;
44
+ return this.model;
45
+ }
46
+
47
+ getProviderInfo() {
48
+ const def = CLOUD_PROVIDERS[this.provider] || {};
49
+ return {
50
+ provider: this.provider,
51
+ providerName: this.providerName,
52
+ baseUrl: this.baseUrl,
53
+ model: this.model,
54
+ hasApiKey: Boolean(this.apiKey && this.apiKey !== 'lm-studio'),
55
+ isLocal: def.isLocal ?? false,
56
+ envKey: def.envKey || null,
57
+ docUrl: def.docUrl || null,
58
+ apiRefUrl: def.apiRefUrl || null,
59
+ pricingSummary: def.pricingSummary || null,
60
+ features: def.features || [],
61
+ recommendedModels: def.recommendedModels || [],
62
+ };
63
+ }
64
+
65
+ _buildHeaders() {
66
+ const headers = {
67
+ 'Content-Type': 'application/json',
68
+ ...this.extraHeaders,
69
+ };
70
+
71
+ if (this.apiKey) {
72
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
73
+ if (this.provider === 'gemini') {
74
+ headers['x-goog-api-key'] = this.apiKey;
75
+ }
76
+ } else if (this.provider === 'lmstudio') {
77
+ headers['Authorization'] = 'Bearer lm-studio';
78
+ }
79
+
80
+ return headers;
81
+ }
82
+
83
+ async checkConnection() {
84
+ const def = CLOUD_PROVIDERS[this.provider] || {};
85
+ const startTime = Date.now();
86
+
87
+ if (!def.isLocal && !this.apiKey) {
88
+ return {
89
+ connected: false,
90
+ provider: this.provider,
91
+ providerName: this.providerName,
92
+ error: `No API key configured for ${this.providerName}. Set ${def.envKey || 'API_KEY'} in .env or run /provider setkey <key>`,
93
+ models: [],
94
+ latencyMs: 0
95
+ };
96
+ }
97
+
98
+ try {
99
+ const controller = new AbortController();
100
+ const timeoutId = setTimeout(() => controller.abort(), 8000);
101
+
102
+ const res = await fetch(`${this.baseUrl}/models`, {
103
+ method: 'GET',
104
+ headers: this._buildHeaders(),
105
+ signal: controller.signal
106
+ });
107
+
108
+ clearTimeout(timeoutId);
109
+ const latencyMs = Date.now() - startTime;
110
+
111
+ if (!res.ok) {
112
+ let errSnippet = res.statusText;
113
+ try {
114
+ const errBody = await res.text();
115
+ if (errBody) {
116
+ try {
117
+ const parsed = JSON.parse(errBody);
118
+ errSnippet = parsed.error?.message || parsed.message || errBody.slice(0, 150);
119
+ } catch {
120
+ errSnippet = errBody.slice(0, 150);
121
+ }
122
+ }
123
+ } catch {}
124
+
125
+ let formattedError = `${this.providerName} returned status ${res.status}: ${errSnippet}`;
126
+ if (res.status === 401) {
127
+ formattedError = `401 Unauthorized: Invalid API key for ${this.providerName}. Check ${def.envKey || 'API key'}.`;
128
+ } else if (res.status === 429) {
129
+ formattedError = `429 Rate Limit / Quota Exceeded: ${errSnippet}`;
130
+ }
131
+
132
+ return {
133
+ connected: false,
134
+ provider: this.provider,
135
+ providerName: this.providerName,
136
+ error: formattedError,
137
+ models: [],
138
+ latencyMs
139
+ };
140
+ }
141
+
142
+ const data = await res.json();
143
+ const rawList = Array.isArray(data) ? data : (data.data || []);
144
+ const models = rawList.map(m => typeof m === 'string' ? m : (m.id || m.name)).filter(Boolean);
145
+
146
+ const matchedGemma = models.find(m =>
147
+ m === 'google/gemma-4-e4b' ||
148
+ m === 'gemma-4-e4b' ||
149
+ m.toLowerCase().includes('gemma-4-e4b') ||
150
+ (m.toLowerCase().includes('gemma') && m.toLowerCase().includes('e4b'))
151
+ );
152
+
153
+ const isAvailable = models.length === 0 ? true : models.includes(this.model);
154
+
155
+ return {
156
+ connected: true,
157
+ provider: this.provider,
158
+ providerName: this.providerName,
159
+ baseUrl: this.baseUrl,
160
+ models,
161
+ activeModel: models.includes(this.model) ? this.model : (matchedGemma || this.model),
162
+ isModelAvailable: isAvailable,
163
+ latencyMs
164
+ };
165
+ } catch (err) {
166
+ const latencyMs = Date.now() - startTime;
167
+ return {
168
+ connected: false,
169
+ provider: this.provider,
170
+ providerName: this.providerName,
171
+ error: err.name === 'AbortError'
172
+ ? `Connection timed out at ${this.baseUrl} (>8s)`
173
+ : `Cannot reach ${this.providerName} at ${this.baseUrl} (${err.message})`,
174
+ models: [],
175
+ latencyMs
176
+ };
177
+ }
178
+ }
179
+
180
+ async chat({
181
+ messages,
182
+ tools = null,
183
+ temperature = 0.3,
184
+ maxTokens = 1536,
185
+ stream = true,
186
+ signal = null,
187
+ onToken = null,
188
+ onThought = null
189
+ }) {
190
+ const isReasoningModel = /^(o1|o3|deepseek-reasoner)/i.test(this.model) || this.model.includes('reasoner');
191
+ const isO1OrO3 = /^(o1|o3)/i.test(this.model);
192
+
193
+ const buildPayload = (includeTools = true, includeTemp = true) => {
194
+ const p = {
195
+ model: this.model,
196
+ messages,
197
+ stream
198
+ };
199
+
200
+ if (!isO1OrO3 && includeTemp && temperature !== undefined && temperature !== null) {
201
+ p.temperature = temperature;
202
+ }
203
+
204
+ if (isO1OrO3) {
205
+ p.max_completion_tokens = maxTokens;
206
+ } else {
207
+ p.max_tokens = maxTokens;
208
+ }
209
+
210
+ if (includeTools && tools && Array.isArray(tools) && tools.length > 0 && !isReasoningModel) {
211
+ p.tools = tools;
212
+ p.tool_choice = "auto";
213
+ }
214
+
215
+ return p;
216
+ };
217
+
218
+ let payload = buildPayload(true, true);
219
+
220
+ const controller = new AbortController();
221
+ let timeoutId = setTimeout(() => controller.abort(), this.timeout);
222
+
223
+ const abortHandler = () => controller.abort();
224
+ if (signal) signal.addEventListener('abort', abortHandler, { once: true });
225
+
226
+ const resetTimeout = () => {
227
+ clearTimeout(timeoutId);
228
+ timeoutId = setTimeout(() => controller.abort(), this.timeout);
229
+ };
230
+
231
+ try {
232
+ let res = await fetch(`${this.baseUrl}/chat/completions`, {
233
+ method: 'POST',
234
+ headers: this._buildHeaders(),
235
+ body: JSON.stringify(payload),
236
+ signal: controller.signal
237
+ });
238
+
239
+ resetTimeout();
240
+
241
+ // Graceful auto-retry if provider rejects tools/temperature/max_tokens with 400
242
+ if (!res.ok && res.status === 400) {
243
+ const errText = await res.text();
244
+ const lowerErr = errText.toLowerCase();
245
+ let retryNeeded = false;
246
+
247
+ if ((lowerErr.includes('tool') || lowerErr.includes('function') || lowerErr.includes('schema')) && payload.tools) {
248
+ payload = buildPayload(false, true);
249
+ retryNeeded = true;
250
+ } else if (lowerErr.includes('temperature') && payload.temperature !== undefined) {
251
+ payload = buildPayload(Boolean(payload.tools), false);
252
+ retryNeeded = true;
253
+ } else if (lowerErr.includes('max_tokens') && payload.max_tokens !== undefined) {
254
+ delete payload.max_tokens;
255
+ payload.max_completion_tokens = maxTokens;
256
+ retryNeeded = true;
257
+ }
258
+
259
+ if (retryNeeded) {
260
+ res = await fetch(`${this.baseUrl}/chat/completions`, {
261
+ method: 'POST',
262
+ headers: this._buildHeaders(),
263
+ body: JSON.stringify(payload),
264
+ signal: controller.signal
265
+ });
266
+ resetTimeout();
267
+ }
268
+
269
+ if (!res.ok) {
270
+ const finalErr = retryNeeded ? await res.text() : errText;
271
+ throw new Error(`${this.providerName} error (${res.status}): ${finalErr}`);
272
+ }
273
+ } else if (!res.ok) {
274
+ const errorText = await res.text();
275
+ throw new Error(`${this.providerName} error (${res.status}): ${errorText}`);
276
+ }
277
+
278
+ if (!stream) {
279
+ const data = await res.json();
280
+ clearTimeout(timeoutId);
281
+ if (signal) signal.removeEventListener('abort', abortHandler);
282
+ const msg = data.choices && data.choices[0] ? data.choices[0].message : null;
283
+ if (msg && !msg.reasoning_content && (msg.reasoning || msg.thinking)) {
284
+ msg.reasoning_content = msg.reasoning || msg.thinking;
285
+ }
286
+ return msg;
287
+ }
288
+
289
+ const reader = res.body.getReader();
290
+ const decoder = new TextDecoder('utf-8');
291
+ let fullContent = '';
292
+ let fullReasoning = '';
293
+ let toolCalls = [];
294
+ let buffer = '';
295
+
296
+ while (true) {
297
+ const { done, value } = await reader.read();
298
+ if (done) break;
299
+
300
+ resetTimeout();
301
+
302
+ buffer += decoder.decode(value, { stream: true });
303
+ const lines = buffer.split('\n');
304
+ buffer = lines.pop() || '';
305
+
306
+ for (const line of lines) {
307
+ const trimmed = line.trim();
308
+ if (!trimmed || !trimmed.startsWith('data:')) continue;
309
+ const jsonStr = trimmed.replace(/^data:\s*/, '');
310
+ if (jsonStr === '[DONE]') continue;
311
+
312
+ try {
313
+ const parsed = JSON.parse(jsonStr);
314
+ const delta = parsed.choices?.[0]?.delta;
315
+ if (!delta) continue;
316
+
317
+ const reasoning = delta.reasoning_content || delta.reasoning || delta.thinking;
318
+ if (reasoning) {
319
+ fullReasoning += reasoning;
320
+ if (onThought) onThought(reasoning);
321
+ }
322
+
323
+ if (delta.content) {
324
+ fullContent += delta.content;
325
+ if (onToken) onToken(delta.content);
326
+ }
327
+
328
+ if (delta.tool_calls) {
329
+ for (const tc of delta.tool_calls) {
330
+ const idx = tc.index ?? 0;
331
+ if (!toolCalls[idx]) {
332
+ toolCalls[idx] = {
333
+ id: tc.id || `call_${Date.now()}_${idx}`,
334
+ type: 'function',
335
+ function: { name: '', arguments: '' }
336
+ };
337
+ }
338
+ if (tc.function?.name) toolCalls[idx].function.name += tc.function.name;
339
+ if (tc.function?.arguments) toolCalls[idx].function.arguments += tc.function.arguments;
340
+ }
341
+ }
342
+ } catch {
343
+ }
344
+ }
345
+ }
346
+
347
+ clearTimeout(timeoutId);
348
+
349
+ const resultMessage = {
350
+ role: 'assistant',
351
+ content: fullContent,
352
+ reasoning_content: fullReasoning
353
+ };
354
+
355
+ if (toolCalls.length > 0) {
356
+ resultMessage.tool_calls = toolCalls.filter(Boolean);
357
+ }
358
+
359
+ if (signal) signal.removeEventListener('abort', abortHandler);
360
+ return resultMessage;
361
+ } catch (err) {
362
+ clearTimeout(timeoutId);
363
+ if (signal) signal.removeEventListener('abort', abortHandler);
364
+ throw err;
365
+ }
366
+ }
367
+ }
368
+
369
+ export { LLMClient as LMStudioClient };