kiku-agent 1.0.7 → 1.0.9

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.
@@ -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 = [];