kiku-agent 1.0.6 → 1.0.8
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.
- package/LICENSE +621 -0
- package/README.md +317 -31
- package/package.json +96 -93
- package/server/agent/agentCore.js +58 -31
- package/server/agent/cli.js +101 -56
- package/server/agent/commandFeatures.js +517 -0
- package/server/agent/commandInput.js +113 -0
- package/server/agent/commandRegistry.js +124 -10
- package/server/agent/commands.js +383 -10
- package/server/agent/config.js +64 -48
- package/server/agent/jobManager.js +96 -0
- package/server/agent/llmClient.js +26 -19
- package/server/agent/mcpClient.js +68 -11
- package/server/agent/memory.js +87 -14
- package/server/agent/modelManager.js +30 -13
- package/server/agent/quizData.js +384 -0
- package/server/agent/terminalImage.js +248 -0
- package/server/agent/tools.js +153 -43
- package/server/agent/ui.js +67 -21
- package/server/agent/youtubeAuth.js +1 -2
- package/src/assets/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/src/assets/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
package/server/agent/config.js
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
const PROVIDER_MODEL_ALIASES = {
|
|
2
|
+
openai: {
|
|
3
|
+
'o1-mini': 'gpt-5.6-luna',
|
|
4
|
+
'o3-mini': 'gpt-5.6-luna',
|
|
5
|
+
},
|
|
6
|
+
openrouter: {
|
|
7
|
+
auto: 'openrouter/auto',
|
|
8
|
+
},
|
|
9
|
+
groq: {
|
|
10
|
+
'gpt-oss-120b': 'openai/gpt-oss-120b',
|
|
11
|
+
'gpt-oss-20b': 'openai/gpt-oss-20b',
|
|
12
|
+
},
|
|
13
|
+
deepseek: {
|
|
14
|
+
'deepseek-chat': 'deepseek-v4-flash',
|
|
15
|
+
'deepseek-reasoner': 'deepseek-v4-flash',
|
|
16
|
+
},
|
|
17
|
+
gemini: {
|
|
18
|
+
'gemini-3.1-pro': 'gemini-3.1-pro-preview',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function normalizeProviderModel(providerId, modelId) {
|
|
23
|
+
const normalizedProvider = String(providerId || '').toLowerCase().trim();
|
|
24
|
+
const normalizedModel = String(modelId || '').trim();
|
|
25
|
+
return PROVIDER_MODEL_ALIASES[normalizedProvider]?.[normalizedModel] || normalizedModel;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const CLOUD_PROVIDERS = {
|
|
2
29
|
lmstudio: {
|
|
3
30
|
id: 'lmstudio',
|
|
4
31
|
name: 'LM Studio (Local)',
|
|
@@ -23,18 +50,17 @@ export const CLOUD_PROVIDERS = {
|
|
|
23
50
|
name: 'OpenAI Cloud',
|
|
24
51
|
baseUrl: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
|
|
25
52
|
apiKey: process.env.OPENAI_API_KEY || '',
|
|
26
|
-
defaultModel: process.env.OPENAI_MODEL || 'gpt-
|
|
53
|
+
defaultModel: normalizeProviderModel('openai', process.env.OPENAI_MODEL || 'gpt-5.6-luna'),
|
|
27
54
|
isLocal: false,
|
|
28
55
|
envKey: 'OPENAI_API_KEY',
|
|
29
|
-
docUrl: 'https://
|
|
30
|
-
apiRefUrl: 'https://
|
|
31
|
-
pricingSummary: '
|
|
56
|
+
docUrl: 'https://developers.openai.com/api/docs/models',
|
|
57
|
+
apiRefUrl: 'https://developers.openai.com/api/reference',
|
|
58
|
+
pricingSummary: 'GPT-5.6 Luna: $0.20/1M in, $1.20/1M out; Terra/Sol available for higher capability',
|
|
32
59
|
features: ['tools', 'streaming', 'vision', 'reasoning', 'system-prompt'],
|
|
33
60
|
recommendedModels: [
|
|
34
|
-
{ id: 'gpt-
|
|
35
|
-
{ id: 'gpt-
|
|
36
|
-
{ id: '
|
|
37
|
-
{ id: 'o1-mini', name: 'o1 Mini', tier: 'reasoning', cost: '$1.10 / 1M in', context: '128k', hasTools: true, description: 'Reasoning model specialized for complex STEM logic and coding.' },
|
|
61
|
+
{ id: 'gpt-5.6-luna', name: 'GPT-5.6 Luna', tier: 'cost-sensitive', cost: '$0.20 / 1M in', context: '1.05M', hasTools: true, description: 'Recommended default for efficient, high-volume multilingual agent workloads.' },
|
|
62
|
+
{ id: 'gpt-5.6-terra', name: 'GPT-5.6 Terra', tier: 'balanced', cost: '$2.00 / 1M in', context: '1.05M', hasTools: true, description: 'Balances intelligence and cost for production reasoning and tool use.' },
|
|
63
|
+
{ id: 'gpt-5.6-sol', name: 'GPT-5.6 Sol', tier: 'flagship', cost: '$4.00 / 1M in', context: '1.05M', hasTools: true, description: 'Flagship model for complex professional reasoning and coding.' },
|
|
38
64
|
],
|
|
39
65
|
},
|
|
40
66
|
openrouter: {
|
|
@@ -42,62 +68,53 @@ export const CLOUD_PROVIDERS = {
|
|
|
42
68
|
name: 'OpenRouter Cloud',
|
|
43
69
|
baseUrl: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1',
|
|
44
70
|
apiKey: process.env.OPENROUTER_API_KEY || '',
|
|
45
|
-
defaultModel: process.env.OPENROUTER_MODEL || '
|
|
46
|
-
extraHeaders: {
|
|
47
|
-
'HTTP-Referer': 'https://jujingyi-kikuuu.vercel.app/',
|
|
48
|
-
'X-Title': 'Kiku Agent',
|
|
49
|
-
},
|
|
71
|
+
defaultModel: normalizeProviderModel('openrouter', process.env.OPENROUTER_MODEL || 'openrouter/auto'),
|
|
72
|
+
extraHeaders: {
|
|
73
|
+
'HTTP-Referer': 'https://jujingyi-kikuuu.vercel.app/',
|
|
74
|
+
'X-OpenRouter-Title': 'Kiku Agent',
|
|
75
|
+
},
|
|
50
76
|
isLocal: false,
|
|
51
77
|
envKey: 'OPENROUTER_API_KEY',
|
|
52
|
-
docUrl: 'https://openrouter.ai/docs/
|
|
53
|
-
apiRefUrl: 'https://openrouter.ai/docs/
|
|
54
|
-
pricingSummary: '
|
|
78
|
+
docUrl: 'https://openrouter.ai/docs/guides/routing/routers/auto-router',
|
|
79
|
+
apiRefUrl: 'https://openrouter.ai/docs/api/reference/overview',
|
|
80
|
+
pricingSummary: 'Auto Router selects a current model dynamically; usage is billed at the routed model rate',
|
|
55
81
|
features: ['universal-routing', 'tools', 'streaming', 'free-tier', 'fallback'],
|
|
56
82
|
recommendedModels: [
|
|
57
|
-
{ id: '
|
|
58
|
-
{ id: 'deepseek/deepseek-chat', name: 'DeepSeek Chat (V3)', tier: 'ultra-cheap', cost: '$0.14 / 1M in', context: '64k', hasTools: true, description: 'DeepSeek-V3 671B MoE via OpenRouter at ultra-low price.' },
|
|
59
|
-
{ id: 'deepseek/deepseek-r1', name: 'DeepSeek Reasoner (R1)', tier: 'reasoning', cost: '$0.55 / 1M in', context: '64k', hasTools: false, description: 'Full DeepSeek-R1 reasoning with Chain-of-Thought output.' },
|
|
60
|
-
{ id: 'meta-llama/llama-3.3-70b-instruct', name: 'Llama 3.3 70B Instruct', tier: 'balanced', cost: '$0.40 / 1M in', context: '128k', hasTools: true, description: 'State-of-the-art open-weights flagship model.' },
|
|
61
|
-
{ id: 'openai/gpt-4o-mini', name: 'GPT-4o Mini', tier: 'ultra-cheap', cost: '$0.15 / 1M in', context: '128k', hasTools: true, description: 'OpenAI mini model routed via OpenRouter.' },
|
|
62
|
-
{ id: 'qwen/qwen-2.5-72b-instruct', name: 'Qwen 2.5 72B Instruct', tier: 'balanced', cost: '$0.35 / 1M in', context: '128k', hasTools: true, description: 'Top-tier open-weights multilingual and coding model.' },
|
|
63
|
-
{ id: 'meta-llama/llama-3.1-8b-instruct:free', name: 'Llama 3.1 8B (Free)', tier: 'free', cost: '$0.00', context: '128k', hasTools: true, description: 'Free tier Llama 3.1 8B on OpenRouter.' },
|
|
83
|
+
{ id: 'openrouter/auto', name: 'OpenRouter Auto Router', tier: 'dynamic', cost: 'Routed model rate', context: 'Model-dependent', hasTools: true, description: 'Recommended default. Chooses from the current catalog so a retired third-party model does not break startup.' },
|
|
64
84
|
],
|
|
65
85
|
},
|
|
66
|
-
groq: {
|
|
86
|
+
groq: {
|
|
67
87
|
id: 'groq',
|
|
68
88
|
name: 'Groq Cloud',
|
|
69
89
|
baseUrl: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1',
|
|
70
90
|
apiKey: process.env.GROQ_API_KEY || '',
|
|
71
|
-
defaultModel: process.env.GROQ_MODEL || '
|
|
91
|
+
defaultModel: normalizeProviderModel('groq', process.env.GROQ_MODEL || 'openai/gpt-oss-120b'),
|
|
72
92
|
isLocal: false,
|
|
73
93
|
envKey: 'GROQ_API_KEY',
|
|
74
94
|
docUrl: 'https://console.groq.com/docs/quickstart',
|
|
75
95
|
apiRefUrl: 'https://console.groq.com/docs/api-reference',
|
|
76
|
-
pricingSummary: 'Ultra-fast LPU inference (
|
|
96
|
+
pricingSummary: 'Ultra-fast LPU inference (GPT-OSS 20B: $0.075/1M in, GPT-OSS 120B: $0.15/1M in)',
|
|
77
97
|
features: ['lpu-speed', 'tools', 'streaming', 'openai-compatible'],
|
|
78
|
-
recommendedModels: [
|
|
79
|
-
{ id: '
|
|
80
|
-
{ id: '
|
|
81
|
-
|
|
82
|
-
{ id: 'mixtral-8x7b-32768', name: 'Mixtral 8x7B', tier: 'cheap', cost: '$0.24 / 1M in', context: '32k', hasTools: true, description: 'Mistral MoE architecture with 32k context.' },
|
|
83
|
-
{ id: 'gemma2-9b-it', name: 'Gemma 2 9B IT', tier: 'cheap', cost: '$0.20 / 1M in', context: '8k', hasTools: true, description: 'Google Gemma 2 on Groq LPU hardware.' },
|
|
84
|
-
],
|
|
98
|
+
recommendedModels: [
|
|
99
|
+
{ id: 'openai/gpt-oss-120b', name: 'GPT-OSS 120B', tier: 'flagship', cost: '$0.15 / 1M in', context: '131k', hasTools: true, description: 'Recommended default. Production-grade 120B reasoning model with tool use on Groq.' },
|
|
100
|
+
{ id: 'openai/gpt-oss-20b', name: 'GPT-OSS 20B', tier: 'ultra-cheap', cost: '$0.075 / 1M in', context: '131k', hasTools: true, description: 'Fast production model for lower-cost, high-volume requests.' },
|
|
101
|
+
],
|
|
85
102
|
},
|
|
86
103
|
deepseek: {
|
|
87
104
|
id: 'deepseek',
|
|
88
105
|
name: 'DeepSeek Cloud',
|
|
89
|
-
baseUrl: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com
|
|
106
|
+
baseUrl: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com',
|
|
90
107
|
apiKey: process.env.DEEPSEEK_API_KEY || '',
|
|
91
|
-
defaultModel: process.env.DEEPSEEK_MODEL || 'deepseek-
|
|
108
|
+
defaultModel: normalizeProviderModel('deepseek', process.env.DEEPSEEK_MODEL || 'deepseek-v4-flash'),
|
|
92
109
|
isLocal: false,
|
|
93
110
|
envKey: 'DEEPSEEK_API_KEY',
|
|
94
111
|
docUrl: 'https://api-docs.deepseek.com',
|
|
95
112
|
apiRefUrl: 'https://api-docs.deepseek.com/api/create-chat-completion',
|
|
96
|
-
pricingSummary: '
|
|
97
|
-
features: ['deepseek-
|
|
113
|
+
pricingSummary: 'V4 Flash: $0.22-$0.44/1M uncached input; V4 Pro: $0.66-$1.32/1M (off-peak/peak)',
|
|
114
|
+
features: ['deepseek-v4', 'thinking', 'tools', 'streaming', 'context-cache', '1m-context'],
|
|
98
115
|
recommendedModels: [
|
|
99
|
-
{ id: 'deepseek-
|
|
100
|
-
{ id: 'deepseek-
|
|
116
|
+
{ id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash', tier: 'balanced', cost: '$0.22-$0.44 / 1M uncached in', context: '1M', hasTools: true, description: 'Recommended default. Current V4 model with tool calls and thinking/non-thinking modes.' },
|
|
117
|
+
{ id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro', tier: 'flagship', cost: '$0.66-$1.32 / 1M uncached in', context: '1M', hasTools: true, description: 'Higher-capability V4 model with tool calls and thinking mode.' },
|
|
101
118
|
],
|
|
102
119
|
},
|
|
103
120
|
gemini: {
|
|
@@ -105,18 +122,17 @@ export const CLOUD_PROVIDERS = {
|
|
|
105
122
|
name: 'Google Gemini Cloud',
|
|
106
123
|
baseUrl: process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
107
124
|
apiKey: process.env.GEMINI_API_KEY || '',
|
|
108
|
-
defaultModel: process.env.GEMINI_MODEL || 'gemini-
|
|
125
|
+
defaultModel: normalizeProviderModel('gemini', process.env.GEMINI_MODEL || 'gemini-3.7-flash'),
|
|
109
126
|
isLocal: false,
|
|
110
127
|
envKey: 'GEMINI_API_KEY',
|
|
111
|
-
docUrl: 'https://ai.google.dev/docs',
|
|
112
|
-
apiRefUrl: 'https://ai.google.dev/api',
|
|
113
|
-
pricingSummary: '
|
|
128
|
+
docUrl: 'https://ai.google.dev/gemini-api/docs',
|
|
129
|
+
apiRefUrl: 'https://ai.google.dev/api/generate-content',
|
|
130
|
+
pricingSummary: 'Gemini 3.7 Flash: free tier available; paid standard $0.75/1M input through 2026',
|
|
114
131
|
features: ['1m-context', 'multimodal', 'tools', 'streaming', 'thinking'],
|
|
115
132
|
recommendedModels: [
|
|
116
|
-
{ id: 'gemini-
|
|
117
|
-
{ id: 'gemini-
|
|
118
|
-
{ id: 'gemini-
|
|
119
|
-
{ id: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro', tier: 'flagship', cost: '$1.50 / 1M in', context: '2M', hasTools: true, description: 'Next-gen Gemini 3.1 flagship with state-of-the-art coding and multimodal reasoning.' },
|
|
133
|
+
{ id: 'gemini-3.7-flash', name: 'Gemini 3.7 Flash', tier: 'latest-stable', cost: '$0.75 / 1M in (2026)', context: '1M', hasTools: true, description: 'Recommended default. Latest stable Flash model for agentic workflows and multimodal reasoning.' },
|
|
134
|
+
{ id: 'gemini-3.5-flash-lite', name: 'Gemini 3.5 Flash-Lite', tier: 'lowest-cost', cost: '$0.30 / 1M in', context: '1M', hasTools: true, description: 'Stable, cost-efficient model for high-volume translation and simple processing.' },
|
|
135
|
+
{ id: 'gemini-3.6-flash', name: 'Gemini 3.6 Flash', tier: 'stable', cost: '$0.75 / 1M in (2026)', context: '1M', hasTools: true, description: 'Stable previous-generation Flash fallback with function calling and thinking.' },
|
|
120
136
|
],
|
|
121
137
|
},
|
|
122
138
|
custom: {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
function errorMessage(error) {
|
|
2
|
+
return error?.message || String(error || 'Unknown error');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class AgentJobManager {
|
|
6
|
+
constructor(limit = 50) {
|
|
7
|
+
this.limit = limit;
|
|
8
|
+
this.jobs = [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
create(type, label) {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const job = {
|
|
14
|
+
id: `job_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
15
|
+
type: String(type || 'task'),
|
|
16
|
+
label: String(label || type || 'Task').slice(0, 200),
|
|
17
|
+
state: 'running',
|
|
18
|
+
createdAt: new Date().toISOString(),
|
|
19
|
+
startedAt: new Date().toISOString(),
|
|
20
|
+
completedAt: null,
|
|
21
|
+
error: null,
|
|
22
|
+
result: null,
|
|
23
|
+
controller,
|
|
24
|
+
};
|
|
25
|
+
this.jobs.unshift(job);
|
|
26
|
+
this.jobs = this.jobs.slice(0, this.limit);
|
|
27
|
+
return job;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async run(type, label, runner, options = {}) {
|
|
31
|
+
const job = this.create(type, label);
|
|
32
|
+
const abortParent = () => job.controller.abort(options.signal?.reason);
|
|
33
|
+
options.signal?.addEventListener('abort', abortParent, { once: true });
|
|
34
|
+
options.onJob?.(this.publicJob(job));
|
|
35
|
+
try {
|
|
36
|
+
const result = await runner({ signal: job.controller.signal, job: this.publicJob(job) });
|
|
37
|
+
job.state = job.controller.signal.aborted ? 'cancelled' : 'completed';
|
|
38
|
+
job.result = result;
|
|
39
|
+
job.completedAt = new Date().toISOString();
|
|
40
|
+
return result;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
job.state = job.controller.signal.aborted || error?.name === 'AbortError' ? 'cancelled' : 'failed';
|
|
43
|
+
job.error = errorMessage(error);
|
|
44
|
+
job.completedAt = new Date().toISOString();
|
|
45
|
+
throw error;
|
|
46
|
+
} finally {
|
|
47
|
+
options.signal?.removeEventListener('abort', abortParent);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
list() {
|
|
52
|
+
return this.jobs.map(job => this.publicJob(job));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get(id) {
|
|
56
|
+
const job = this.jobs.find(item => item.id === id);
|
|
57
|
+
return job ? this.publicJob(job) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
cancel(id) {
|
|
61
|
+
const job = this.jobs.find(item => item.id === id);
|
|
62
|
+
if (!job || job.state !== 'running') return false;
|
|
63
|
+
job.state = 'cancelled';
|
|
64
|
+
job.completedAt = new Date().toISOString();
|
|
65
|
+
job.controller.abort(new DOMException('Cancelled by user', 'AbortError'));
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
cancelType(type) {
|
|
70
|
+
let cancelled = false;
|
|
71
|
+
for (const job of this.jobs) {
|
|
72
|
+
if (job.type === type && job.state === 'running') cancelled = this.cancel(job.id) || cancelled;
|
|
73
|
+
}
|
|
74
|
+
return cancelled;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
clear() {
|
|
78
|
+
const before = this.jobs.length;
|
|
79
|
+
this.jobs = this.jobs.filter(job => job.state === 'running');
|
|
80
|
+
return before - this.jobs.length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
publicJob(job) {
|
|
84
|
+
return {
|
|
85
|
+
id: job.id,
|
|
86
|
+
type: job.type,
|
|
87
|
+
label: job.label,
|
|
88
|
+
state: job.state,
|
|
89
|
+
createdAt: job.createdAt,
|
|
90
|
+
startedAt: job.startedAt,
|
|
91
|
+
completedAt: job.completedAt,
|
|
92
|
+
error: job.error,
|
|
93
|
+
result: job.result,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { config, CLOUD_PROVIDERS } from './config.js';
|
|
1
|
+
import { config, CLOUD_PROVIDERS, normalizeProviderModel } from './config.js';
|
|
2
2
|
|
|
3
3
|
export class LLMClient {
|
|
4
4
|
constructor(options = {}) {
|
|
5
|
-
const providerId = (options.provider || config.activeProvider || 'lmstudio').toLowerCase();
|
|
6
|
-
const providerDef = CLOUD_PROVIDERS[providerId]
|
|
5
|
+
const providerId = (options.provider || config.activeProvider || 'lmstudio').toLowerCase();
|
|
6
|
+
const providerDef = CLOUD_PROVIDERS[providerId];
|
|
7
|
+
if (!providerDef) {
|
|
8
|
+
throw new Error(`Unknown provider "${providerId}". Supported: ${Object.keys(CLOUD_PROVIDERS).join(', ')}`);
|
|
9
|
+
}
|
|
7
10
|
|
|
8
11
|
this.provider = providerId;
|
|
9
12
|
this.providerName = providerDef.name || providerId;
|
|
@@ -11,7 +14,10 @@ export class LLMClient {
|
|
|
11
14
|
this.apiKey = options.apiKey !== undefined
|
|
12
15
|
? options.apiKey
|
|
13
16
|
: (providerDef.apiKey !== undefined ? providerDef.apiKey : (providerDef.isLocal ? (config.lmStudio.apiKey || 'lm-studio') : ''));
|
|
14
|
-
this.model =
|
|
17
|
+
this.model = normalizeProviderModel(
|
|
18
|
+
providerId,
|
|
19
|
+
options.model || providerDef.defaultModel || config.lmStudio.model || 'google/gemma-4-e4b'
|
|
20
|
+
);
|
|
15
21
|
this.timeout = options.timeout || config.lmStudio.requestTimeoutMs || 180000;
|
|
16
22
|
this.extraHeaders = { ...(providerDef.extraHeaders || {}), ...(options.extraHeaders || {}) };
|
|
17
23
|
}
|
|
@@ -27,7 +33,7 @@ export class LLMClient {
|
|
|
27
33
|
this.providerName = providerDef.name || pId;
|
|
28
34
|
this.baseUrl = (options.baseUrl || providerDef.baseUrl).replace(/\/+$/, '');
|
|
29
35
|
this.apiKey = options.apiKey !== undefined ? options.apiKey : (providerDef.apiKey || '');
|
|
30
|
-
this.model = options.model || providerDef.defaultModel;
|
|
36
|
+
this.model = normalizeProviderModel(pId, options.model || providerDef.defaultModel);
|
|
31
37
|
this.extraHeaders = { ...(providerDef.extraHeaders || {}), ...(options.extraHeaders || {}) };
|
|
32
38
|
return this.getProviderInfo();
|
|
33
39
|
}
|
|
@@ -40,7 +46,7 @@ export class LLMClient {
|
|
|
40
46
|
setModel(model) {
|
|
41
47
|
const trimmed = String(model || '').trim();
|
|
42
48
|
if (!trimmed) throw new Error('A model ID is required.');
|
|
43
|
-
this.model = trimmed;
|
|
49
|
+
this.model = normalizeProviderModel(this.provider, trimmed);
|
|
44
50
|
return this.model;
|
|
45
51
|
}
|
|
46
52
|
|
|
@@ -187,8 +193,9 @@ export class LLMClient {
|
|
|
187
193
|
onToken = null,
|
|
188
194
|
onThought = null
|
|
189
195
|
}) {
|
|
190
|
-
const
|
|
191
|
-
const
|
|
196
|
+
const isLegacyReasoningModel = /^(o1|o3|deepseek-reasoner)/i.test(this.model) || this.model.includes('reasoner');
|
|
197
|
+
const usesCompletionTokenLimit = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
|
|
198
|
+
const rejectsTemperature = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
|
|
192
199
|
|
|
193
200
|
const buildPayload = (includeTools = true, includeTemp = true) => {
|
|
194
201
|
const p = {
|
|
@@ -197,17 +204,17 @@ export class LLMClient {
|
|
|
197
204
|
stream
|
|
198
205
|
};
|
|
199
206
|
|
|
200
|
-
if (!
|
|
201
|
-
p.temperature = temperature;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (
|
|
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 && !
|
|
207
|
+
if (!rejectsTemperature && includeTemp && temperature !== undefined && temperature !== null) {
|
|
208
|
+
p.temperature = temperature;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (usesCompletionTokenLimit) {
|
|
212
|
+
p.max_completion_tokens = maxTokens;
|
|
213
|
+
} else {
|
|
214
|
+
p.max_tokens = maxTokens;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (includeTools && tools && Array.isArray(tools) && tools.length > 0 && !isLegacyReasoningModel) {
|
|
211
218
|
p.tools = tools;
|
|
212
219
|
p.tool_choice = "auto";
|
|
213
220
|
}
|
|
@@ -11,11 +11,53 @@ export class McpClientManager {
|
|
|
11
11
|
constructor(options = {}) {
|
|
12
12
|
this.clientInfo = options.clientInfo || { name: 'kiku-agent', version: PACKAGE_VERSION };
|
|
13
13
|
this.autoLoad = options.autoLoad !== false;
|
|
14
|
+
this.globalConfigPath = options.globalConfigPath || path.join(KIKU_DATA_DIR, 'mcp_servers.json');
|
|
15
|
+
this.configPaths = options.configPaths || [
|
|
16
|
+
this.globalConfigPath,
|
|
17
|
+
path.join(process.cwd(), 'opencode.json'),
|
|
18
|
+
path.join(process.cwd(), '.mcp.json'),
|
|
19
|
+
path.join(process.cwd(), 'mcp.json'),
|
|
20
|
+
];
|
|
14
21
|
this.servers = new Map();
|
|
15
22
|
this.persistedConfigs = new Map();
|
|
23
|
+
this.configSources = new Map();
|
|
16
24
|
this.toolRegistry = new Map();
|
|
17
25
|
}
|
|
18
26
|
|
|
27
|
+
_trackConfigSource(serverName, filePath) {
|
|
28
|
+
const name = String(serverName || '').trim();
|
|
29
|
+
if (!name || !filePath) return;
|
|
30
|
+
const resolvedPath = path.resolve(filePath);
|
|
31
|
+
const sources = this.configSources.get(name) || new Set();
|
|
32
|
+
sources.add(resolvedPath);
|
|
33
|
+
this.configSources.set(name, sources);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async _removeFromConfigFile(filePath, serverName) {
|
|
37
|
+
let content;
|
|
38
|
+
try {
|
|
39
|
+
content = await fs.readFile(filePath, 'utf8');
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error?.code === 'ENOENT') return false;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parsed = JSON.parse(content);
|
|
46
|
+
let changed = false;
|
|
47
|
+
for (const key of ['mcpServers', 'mcp']) {
|
|
48
|
+
const servers = parsed[key];
|
|
49
|
+
if (servers && typeof servers === 'object' && Object.hasOwn(servers, serverName)) {
|
|
50
|
+
delete servers[serverName];
|
|
51
|
+
changed = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (changed) {
|
|
56
|
+
await fs.writeFile(filePath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
return changed;
|
|
59
|
+
}
|
|
60
|
+
|
|
19
61
|
async connect(serverName, serverConfig = {}, { persist = true } = {}) {
|
|
20
62
|
const name = String(serverName || '').trim();
|
|
21
63
|
if (!name) throw new Error('Server name is required to connect to MCP.');
|
|
@@ -107,7 +149,8 @@ export class McpClientManager {
|
|
|
107
149
|
this._rebuildToolRegistry();
|
|
108
150
|
|
|
109
151
|
if (persist) {
|
|
110
|
-
await this.savePersistedConfig().catch(() =>
|
|
152
|
+
const saveResult = await this.savePersistedConfig().catch(() => null);
|
|
153
|
+
if (saveResult?.success) this._trackConfigSource(name, saveResult.filePath);
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
return {
|
|
@@ -153,12 +196,25 @@ export class McpClientManager {
|
|
|
153
196
|
}
|
|
154
197
|
|
|
155
198
|
async remove(serverName, { persist = true } = {}) {
|
|
199
|
+
const wasConnected = this.servers.has(serverName);
|
|
156
200
|
await this.disconnect(serverName, { persist: false });
|
|
157
201
|
const hadPersisted = this.persistedConfigs.delete(serverName);
|
|
202
|
+
const sources = [...(this.configSources.get(serverName) || [])];
|
|
203
|
+
let removedFromSource = false;
|
|
204
|
+
|
|
158
205
|
if (persist) {
|
|
159
|
-
|
|
206
|
+
for (const sourcePath of sources) {
|
|
207
|
+
const removed = await this._removeFromConfigFile(sourcePath, serverName);
|
|
208
|
+
removedFromSource = removedFromSource || removed;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const saveResult = await this.savePersistedConfig();
|
|
212
|
+
if (!saveResult.success) {
|
|
213
|
+
throw new Error(`Failed to update global MCP configuration: ${saveResult.error}`);
|
|
214
|
+
}
|
|
160
215
|
}
|
|
161
|
-
|
|
216
|
+
this.configSources.delete(serverName);
|
|
217
|
+
return wasConnected || hadPersisted || removedFromSource;
|
|
162
218
|
}
|
|
163
219
|
|
|
164
220
|
async disconnectAll() {
|
|
@@ -325,6 +381,7 @@ export class McpClientManager {
|
|
|
325
381
|
resourcesCount: server.resources.length,
|
|
326
382
|
promptsCount: server.prompts.length,
|
|
327
383
|
connectedAt: server.connectedAt,
|
|
384
|
+
sources: [...(this.configSources.get(name) || [])],
|
|
328
385
|
config: server.config,
|
|
329
386
|
});
|
|
330
387
|
}
|
|
@@ -339,6 +396,7 @@ export class McpClientManager {
|
|
|
339
396
|
resourcesCount: 0,
|
|
340
397
|
promptsCount: 0,
|
|
341
398
|
error: cfg.lastError || null,
|
|
399
|
+
sources: [...(this.configSources.get(name) || [])],
|
|
342
400
|
config: cfg,
|
|
343
401
|
});
|
|
344
402
|
}
|
|
@@ -353,7 +411,7 @@ export class McpClientManager {
|
|
|
353
411
|
|
|
354
412
|
async savePersistedConfig(customPath) {
|
|
355
413
|
try {
|
|
356
|
-
const filePath = customPath ||
|
|
414
|
+
const filePath = customPath || this.globalConfigPath;
|
|
357
415
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
358
416
|
|
|
359
417
|
const serversObj = {};
|
|
@@ -369,6 +427,9 @@ export class McpClientManager {
|
|
|
369
427
|
}
|
|
370
428
|
|
|
371
429
|
await fs.writeFile(filePath, JSON.stringify({ mcpServers: serversObj }, null, 2), 'utf8');
|
|
430
|
+
for (const name of Object.keys(serversObj)) {
|
|
431
|
+
this._trackConfigSource(name, filePath);
|
|
432
|
+
}
|
|
372
433
|
return { success: true, filePath, count: Object.keys(serversObj).length };
|
|
373
434
|
} catch (err) {
|
|
374
435
|
return { success: false, error: err.message };
|
|
@@ -383,20 +444,15 @@ export class McpClientManager {
|
|
|
383
444
|
async loadPersistedServers() {
|
|
384
445
|
if (this.autoLoad === false) return [];
|
|
385
446
|
const results = [];
|
|
386
|
-
const candidates = [
|
|
387
|
-
path.join(KIKU_DATA_DIR, 'mcp_servers.json'),
|
|
388
|
-
path.join(process.cwd(), 'opencode.json'),
|
|
389
|
-
path.join(process.cwd(), '.mcp.json'),
|
|
390
|
-
path.join(process.cwd(), 'mcp.json'),
|
|
391
|
-
];
|
|
392
447
|
|
|
393
|
-
for (const filePath of
|
|
448
|
+
for (const filePath of this.configPaths) {
|
|
394
449
|
try {
|
|
395
450
|
const content = await fs.readFile(filePath, 'utf8');
|
|
396
451
|
const parsed = JSON.parse(content);
|
|
397
452
|
const serversObj = parsed.mcpServers || parsed.mcp || {};
|
|
398
453
|
for (const [name, cfg] of Object.entries(serversObj)) {
|
|
399
454
|
this.persistedConfigs.set(name, cfg);
|
|
455
|
+
this._trackConfigSource(name, filePath);
|
|
400
456
|
if (cfg.enabled === false || cfg.disabled === true) continue;
|
|
401
457
|
if (this.servers.has(name)) continue;
|
|
402
458
|
try {
|
|
@@ -422,6 +478,7 @@ export class McpClientManager {
|
|
|
422
478
|
|
|
423
479
|
for (const [name, cfg] of Object.entries(serversObj)) {
|
|
424
480
|
this.persistedConfigs.set(name, cfg);
|
|
481
|
+
this._trackConfigSource(name, resolvedPath);
|
|
425
482
|
if (cfg.enabled === false || cfg.disabled === true) continue;
|
|
426
483
|
try {
|
|
427
484
|
const res = await this.connect(name, cfg);
|
package/server/agent/memory.js
CHANGED
|
@@ -13,9 +13,10 @@ export class AgentMemory {
|
|
|
13
13
|
this.preferences = {
|
|
14
14
|
language: null,
|
|
15
15
|
topicsOfInterest: [],
|
|
16
|
-
responseStyle: 'default',
|
|
17
|
-
userSkills: [],
|
|
18
|
-
skillsEnabled: true,
|
|
16
|
+
responseStyle: 'default',
|
|
17
|
+
userSkills: [],
|
|
18
|
+
skillsEnabled: true,
|
|
19
|
+
favorites: [],
|
|
19
20
|
};
|
|
20
21
|
this.sessions = [];
|
|
21
22
|
this.summaries = [];
|
|
@@ -68,9 +69,10 @@ export class AgentMemory {
|
|
|
68
69
|
const loadedPrefs = parsed.preferences || {};
|
|
69
70
|
this.preferences = {
|
|
70
71
|
...this.preferences,
|
|
71
|
-
...loadedPrefs,
|
|
72
|
-
userSkills: Array.isArray(loadedPrefs.userSkills) ? loadedPrefs.userSkills : [],
|
|
73
|
-
|
|
72
|
+
...loadedPrefs,
|
|
73
|
+
userSkills: Array.isArray(loadedPrefs.userSkills) ? loadedPrefs.userSkills : [],
|
|
74
|
+
favorites: Array.isArray(loadedPrefs.favorites) ? loadedPrefs.favorites : [],
|
|
75
|
+
topicsOfInterest: (loadedPrefs.topicsOfInterest || []).map(t => ({
|
|
74
76
|
...t,
|
|
75
77
|
lastSeen: t.lastSeen ? new Date(t.lastSeen) : new Date()
|
|
76
78
|
}))
|
|
@@ -238,10 +240,78 @@ export class AgentMemory {
|
|
|
238
240
|
return grouped;
|
|
239
241
|
}
|
|
240
242
|
|
|
241
|
-
setPreference(key, value) {
|
|
242
|
-
this.preferences[key] = value;
|
|
243
|
-
this.save().catch(() => {});
|
|
244
|
-
}
|
|
243
|
+
setPreference(key, value) {
|
|
244
|
+
this.preferences[key] = value;
|
|
245
|
+
this.save().catch(() => {});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
addFavorite(entity = {}) {
|
|
249
|
+
const allowedTypes = ['work', 'song', 'role', 'brand', 'award', 'photo', 'stage'];
|
|
250
|
+
const type = String(entity.type || '').toLowerCase();
|
|
251
|
+
const title = String(entity.title || '').replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
252
|
+
const entityId = String(entity.entityId || title).replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 200);
|
|
253
|
+
if (!allowedTypes.includes(type)) throw new Error(`Favorite type must be one of: ${allowedTypes.join(', ')}.`);
|
|
254
|
+
if (!title || !entityId) throw new Error('Favorite title and entity ID are required.');
|
|
255
|
+
if (!Array.isArray(this.preferences.favorites)) this.preferences.favorites = [];
|
|
256
|
+
const duplicate = this.preferences.favorites.find(item => item.type === type && item.entityId.toLowerCase() === entityId.toLowerCase());
|
|
257
|
+
if (duplicate) return { favorite: duplicate, created: false };
|
|
258
|
+
const favorite = {
|
|
259
|
+
id: `fav_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
260
|
+
type,
|
|
261
|
+
entityId,
|
|
262
|
+
title,
|
|
263
|
+
year: Number.isInteger(Number(entity.year)) ? Number(entity.year) : null,
|
|
264
|
+
detail: String(entity.detail || '').replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 300),
|
|
265
|
+
url: /^https?:\/\//i.test(String(entity.url || '')) ? String(entity.url).slice(0, 1000) : null,
|
|
266
|
+
createdAt: new Date().toISOString(),
|
|
267
|
+
};
|
|
268
|
+
this.preferences.favorites.unshift(favorite);
|
|
269
|
+
this.preferences.favorites = this.preferences.favorites.slice(0, 200);
|
|
270
|
+
this.save().catch(() => {});
|
|
271
|
+
return { favorite, created: true };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
getFavorites(type) {
|
|
275
|
+
const favorites = Array.isArray(this.preferences.favorites) ? this.preferences.favorites : [];
|
|
276
|
+
if (!type || type === 'all') return [...favorites];
|
|
277
|
+
return favorites.filter(item => item.type === String(type).toLowerCase());
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
removeFavorite(id) {
|
|
281
|
+
if (!/^fav_[a-zA-Z0-9_]+$/.test(String(id || ''))) return false;
|
|
282
|
+
const favorites = Array.isArray(this.preferences.favorites) ? this.preferences.favorites : [];
|
|
283
|
+
const before = favorites.length;
|
|
284
|
+
this.preferences.favorites = favorites.filter(item => item.id !== id);
|
|
285
|
+
if (this.preferences.favorites.length === before) return false;
|
|
286
|
+
this.save().catch(() => {});
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async exportFavorites(format = 'json') {
|
|
291
|
+
const normalized = String(format || 'json').toLowerCase();
|
|
292
|
+
if (!['json', 'markdown'].includes(normalized)) throw new Error('Favorite export format must be json or markdown.');
|
|
293
|
+
await fs.mkdir(EXPORT_DIR, { recursive: true });
|
|
294
|
+
const now = new Date();
|
|
295
|
+
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
296
|
+
const extension = normalized === 'markdown' ? 'md' : 'json';
|
|
297
|
+
const filename = `kiku-favorites-${timestamp}.${extension}`;
|
|
298
|
+
const filePath = path.join(EXPORT_DIR, filename);
|
|
299
|
+
const favorites = this.getFavorites();
|
|
300
|
+
const content = normalized === 'json'
|
|
301
|
+
? JSON.stringify({ exportedAt: now.toISOString(), count: favorites.length, favorites }, null, 2)
|
|
302
|
+
: [
|
|
303
|
+
'# Kiku Agent Favorites',
|
|
304
|
+
'',
|
|
305
|
+
`Exported: ${now.toISOString()}`,
|
|
306
|
+
'',
|
|
307
|
+
'| ID | Type | Title | Year | Detail |',
|
|
308
|
+
'|---|---|---|---:|---|',
|
|
309
|
+
...favorites.map(item => `| ${item.id} | ${item.type} | ${String(item.title).replace(/\|/g, '\\|')} | ${item.year || ''} | ${String(item.detail || '').replace(/\|/g, '\\|')} |`),
|
|
310
|
+
'',
|
|
311
|
+
].join('\n');
|
|
312
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
313
|
+
return { filePath, filename, count: favorites.length, size: (await fs.stat(filePath)).size };
|
|
314
|
+
}
|
|
245
315
|
|
|
246
316
|
addUserSkill(instruction) {
|
|
247
317
|
const value = String(instruction || '').replace(/\s+/g, ' ').trim();
|
|
@@ -633,10 +703,13 @@ export class AgentMemory {
|
|
|
633
703
|
|
|
634
704
|
async clearAll() {
|
|
635
705
|
this.facts = [];
|
|
636
|
-
this.preferences = {
|
|
637
|
-
language: null,
|
|
638
|
-
topicsOfInterest: [],
|
|
639
|
-
responseStyle: 'default'
|
|
706
|
+
this.preferences = {
|
|
707
|
+
language: null,
|
|
708
|
+
topicsOfInterest: [],
|
|
709
|
+
responseStyle: 'default',
|
|
710
|
+
userSkills: [],
|
|
711
|
+
skillsEnabled: true,
|
|
712
|
+
favorites: [],
|
|
640
713
|
};
|
|
641
714
|
this.sessions = [];
|
|
642
715
|
this.summaries = [];
|