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.
@@ -1,4 +1,31 @@
1
- export const CLOUD_PROVIDERS = {
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-4o-mini',
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://platform.openai.com/docs',
30
- apiRefUrl: 'https://platform.openai.com/docs/api-reference',
31
- pricingSummary: 'Pay-per-token (gpt-4o-mini: ~$0.15/1M in, $0.60/1M out)',
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-4o-mini', name: 'GPT-4o Mini', tier: 'ultra-cheap', cost: '$0.15 / 1M in', context: '128k', hasTools: true, description: 'Recommended default. Fast, intelligent, and highly cost-effective with tool use.' },
35
- { id: 'gpt-4o', name: 'GPT-4o', tier: 'flagship', cost: '$2.50 / 1M in', context: '128k', hasTools: true, description: 'OpenAI flagship multimodal model for complex multimodal & reasoning tasks.' },
36
- { id: 'o3-mini', name: 'o3 Mini', tier: 'reasoning', cost: '$1.10 / 1M in', context: '200k', hasTools: true, description: 'Fast, cost-efficient reasoning model with function calling capabilities.' },
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 || 'google/gemini-2.5-flash',
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/quick-start',
53
- apiRefUrl: 'https://openrouter.ai/docs/api_reference/overview',
54
- pricingSummary: 'Universal aggregator with free tiers & flexible pay-as-you-go',
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: 'google/gemini-2.5-flash', name: 'Gemini 2.5 Flash', tier: 'ultra-cheap', cost: '$0.10 / 1M in', context: '1M', hasTools: true, description: 'Super fast with huge 1M context window and native function calling.' },
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 || 'llama-3.3-70b-versatile',
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 (llama-3.1-8b: $0.05/1M, llama-3.3-70b: $0.59/1M)',
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: 'llama-3.3-70b-versatile', name: 'Llama 3.3 70B Versatile', tier: 'balanced', cost: '$0.59 / 1M in', context: '128k', hasTools: true, description: 'Recommended default. 70B flagship quality with fast LPU speed and tool use.' },
80
- { id: 'llama-3.1-8b-instant', name: 'Llama 3.1 8B Instant', tier: 'ultra-cheap', cost: '$0.05 / 1M in', context: '128k', hasTools: true, description: 'Blazing fast (~800 tokens/s) and ultra-low cost for quick inquiries.' },
81
- { id: 'gpt-oss-120b', name: 'GPT-OSS 120B', tier: 'balanced', cost: '$0.59 / 1M in', context: '128k', hasTools: true, description: 'Open-weights 120B flagship model running with ultra-fast Groq LPU inference.' },
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/v1',
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-chat',
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: 'Ultra-low cost (deepseek-chat: $0.14 cached / $0.27 uncached / 1M)',
97
- features: ['deepseek-v3', 'deepseek-r1-reasoning', 'tools', 'streaming', 'context-cache'],
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-chat', name: 'DeepSeek Chat (V3)', tier: 'ultra-cheap', cost: '$0.14-$0.27 / 1M in', context: '64k', hasTools: true, description: 'Recommended default. DeepSeek-V3 671B MoE, top-tier reasoning, tool calling, fast & ultra-cheap.' },
100
- { id: 'deepseek-reasoner', name: 'DeepSeek Reasoner (R1)', tier: 'reasoning', cost: '$0.55 / 1M in', context: '64k', hasTools: false, description: 'DeepSeek-R1 full Chain-of-Thought reasoning with native thinking output.' },
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-2.5-flash',
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: 'Ultra-cheap (gemini-2.5-flash: $0.10/1M in, $0.40/1M out)',
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-2.5-flash', name: 'Gemini 2.5 Flash', tier: 'ultra-cheap', cost: '$0.10 / 1M in', context: '1M', hasTools: true, description: 'Recommended default. Advanced reasoning and thinking capabilities with 1M context.' },
117
- { id: 'gemini-2.5-flash-lite', name: 'Gemini 2.5 Flash Lite', tier: 'lowest-cost', cost: '$0.075 / 1M in', context: '1M', hasTools: true, description: 'Cost-optimized 2.5 lightweight model for high volume queries.' },
118
- { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', tier: 'flagship', cost: '$1.25 / 1M in', context: '2M', hasTools: true, description: 'Deep reasoning and 2M context for complex multimodal analysis.' },
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] || CLOUD_PROVIDERS.lmstudio;
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 = options.model || providerDef.defaultModel || config.lmStudio.model || 'google/gemma-4-e4b';
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 isReasoningModel = /^(o1|o3|deepseek-reasoner)/i.test(this.model) || this.model.includes('reasoner');
191
- const isO1OrO3 = /^(o1|o3)/i.test(this.model);
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 (!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) {
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
- await this.savePersistedConfig().catch(() => {});
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
- return hadPersisted;
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 || path.join(KIKU_DATA_DIR, 'mcp_servers.json');
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 candidates) {
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);
@@ -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
- topicsOfInterest: (loadedPrefs.topicsOfInterest || []).map(t => ({
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 = [];