kiku-agent 1.0.5 → 1.0.6

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/README.md CHANGED
@@ -48,7 +48,24 @@ The same package also installs the stdio MCP server command:
48
48
  kiku-mcp
49
49
  ```
50
50
 
51
- For a local checkout before the package is published, use `npm pack` followed by `npm install -g ./kiku-agent-1.0.5.tgz`.
51
+ ### Updating the CLI and MCP server
52
+
53
+ Kiku uses the published `kiku-agent` package on npm as its update source. Inside an interactive session:
54
+
55
+ ```text
56
+ /update # Check the configured release channel
57
+ /update install # Install the stable `latest` release
58
+ /update install next # Install a preview release
59
+ /update install 1.2.3 # Install an exact published version
60
+ /update channel next # Persist the preferred channel
61
+ /update rollback # Restore the previously recorded version
62
+ ```
63
+
64
+ Automatic installation is available for global npm installs. Source checkouts, local dependencies, and `npx` executions receive a safe manual command instead of having their files replaced. Major updates require `--allow-major`; deprecated releases require `--allow-deprecated`. npm commands are executed without a shell and only allow known channel names or strict semantic versions.
65
+
66
+ After an update or rollback, restart `kiku-agent` and any application hosting `kiku-mcp`. Update settings and rollback history are stored in the normal Kiku user data directory as `update-state.json`; sessions, memory, `.env`, and YouTube cookies are not modified.
67
+
68
+ For a local checkout before the package is published, use `npm pack` followed by `npm install -g ./kiku-agent-1.0.6.tgz`.
52
69
 
53
70
  ### ✨ Features
54
71
  - **LM Studio Integration**: Connects seamlessly to LM Studio's OpenAI-compatible server at `http://127.0.0.1:1234/v1` (default model `google/gemma-4-e4b`, with auto-detection of loaded models).
@@ -104,6 +121,7 @@ npm run kiku-agent -- --model google/gemma-4-e4b-Q4_K_M --url http://127.0.0.1:1
104
121
  | Flag | Description |
105
122
  |---|---|
106
123
  | `-h`, `--help` | Show usage help |
124
+ | `-V`, `--version` | Print the installed package version and exit |
107
125
  | `-P`, `--provider <name>` | Select active provider (`lmstudio`, `openai`, `gemini`, `groq`, `deepseek`, `openrouter`, `custom`) |
108
126
  | `-k`, `--api-key <key>` | Specify API key for cloud provider |
109
127
  | `-m`, `--model <id>` | Override model ID (e.g. `gpt-4o-mini`, `google/gemma-4-e4b-Q4_K_M`) |
@@ -131,8 +149,11 @@ Run `/help` inside a session for the full list. Commonly used:
131
149
  - `/model use <model-id>` — Switch runtime model without restarting the CLI
132
150
  - `/model reconnect` — Recheck server and leave catalog-only mode after a chat model is loaded
133
151
  - `/doctor` — Check Node, writable app data, LM Studio, model availability, FFmpeg, speaker, and yt-dlp
152
+ - `/update [check|install|channel|rollback]` — Manage npm-backed CLI and MCP package updates
134
153
  - `/tools` — View all tool names and parameter descriptions
135
- - `/details [Qn]` — Expand recorded thinking and tool results for a question
154
+ - `/details [Qn]` — Open a compact diagnostic overview for the latest or selected question (F2 opens latest)
155
+ - `/details [Qn] answer|thinking|tools` — Inspect one diagnostic section without dumping every raw result
156
+ - `/details [Qn] tool <n>` — Inspect bounded raw arguments and result for one tool call
136
157
  - `/stats` — Print summary counts of dramas, songs, brands, and awards
137
158
  - `/thinking [show|hide]` — Toggle reasoning process visibility
138
159
  - `/save` — Export session transcript to Markdown file
@@ -252,6 +273,8 @@ The production deployment exposes a stateless Streamable HTTP endpoint at `https
252
273
  /mcp tools
253
274
  ```
254
275
 
276
+ `/mcp` shows the connection summary plus every available MCP tool command in a compact list. `/mcp tools` expands the same list with parameters and descriptions. Long command panels open at their first page in the classic TUI; use Page Down or End to continue through the result.
277
+
255
278
  ---
256
279
 
257
280
  ## ⚙️ Environment Variables
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiku-agent",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Kiku AI terminal agent and MCP server for Ju Jingyi knowledge, media, and career data.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,16 @@
23
23
  "engines": {
24
24
  "node": ">=20.0.0"
25
25
  },
26
+ "kikuUpdate": {
27
+ "channel": "stable",
28
+ "releaseSummary": [
29
+ "AI research suggestion engine and batch subagent execution",
30
+ "Enhanced web search fallbacks and missing catalog tool hints",
31
+ "Updated brand endorsements (Versace) and dual catalog sync",
32
+ "Secure npm-backed updates and terminal markdown renderer"
33
+ ],
34
+ "releaseUrl": "https://www.npmjs.com/package/kiku-agent"
35
+ },
26
36
  "keywords": [
27
37
  "kiku",
28
38
  "ju-jingyi",
@@ -9,6 +9,7 @@ import { sanitizeQuery, isLikelySafe } from './sanitizer.js';
9
9
  import { createAbortError, isAbortError } from './errors.js';
10
10
  import { ResearchSubagent } from './subagent.js';
11
11
  import { ResearchOrchestrator } from './research.js';
12
+ import { SuggestionEngine } from './suggestions.js';
12
13
  import { McpClientManager } from './mcpClient.js';
13
14
  import { DynamicToolRouter } from './toolRouter.js';
14
15
  import { contextCompactor } from './contextCompactor.js';
@@ -62,6 +63,7 @@ export class KikuAgent {
62
63
  this.mcpManager = options.mcpManager || new McpClientManager();
63
64
  this.router = new DynamicToolRouter(this.getAllToolsSchema());
64
65
  this.research = new ResearchOrchestrator({ client: this.client });
66
+ this.suggestions = new SuggestionEngine({ client: this.client, memory: this.memory });
65
67
  this.systemPrompt = this.buildSystemPrompt();
66
68
  this.initHistory();
67
69
  this.restoredMessageCount = 0;
@@ -88,7 +90,7 @@ export class KikuAgent {
88
90
  await this.mcpManager.loadPersistedServers();
89
91
  this.router.setTools(this.getAllToolsSchema());
90
92
  this.systemPrompt = this.buildSystemPrompt();
91
- } catch {}
93
+ } catch { }
92
94
  }
93
95
  if (!this.conversationHistory || this.conversationHistory.length === 0) {
94
96
  this.initHistory();
@@ -128,6 +130,20 @@ export class KikuAgent {
128
130
  return this.research.cancel();
129
131
  }
130
132
 
133
+ async generateSuggestions(options = {}) {
134
+ await this._ready;
135
+ return this.suggestions.generateSuggestions(options);
136
+ }
137
+
138
+ async runSuggestionBatch(targets, options = {}) {
139
+ await this._ready;
140
+ return this.suggestions.runBatchSubagents(targets, options);
141
+ }
142
+
143
+ getLastSuggestions() {
144
+ return this.suggestions.getLastSuggestions();
145
+ }
146
+
131
147
  initHistory() {
132
148
  this.conversationHistory = [
133
149
  {
@@ -139,12 +155,12 @@ export class KikuAgent {
139
155
 
140
156
  clearMemory() {
141
157
  if (this.session.messages.length > 0) {
142
- this.memory.archiveSession(this.session.toSnapshot()).catch(() => {});
143
- this.memory.consolidateSession(this.session.toSnapshot()).catch(() => {});
158
+ this.memory.archiveSession(this.session.toSnapshot()).catch(() => { });
159
+ this.memory.consolidateSession(this.session.toSnapshot()).catch(() => { });
144
160
  }
145
161
  this.initHistory();
146
162
  this.session.reset();
147
- this.session.clearDisk().catch(() => {});
163
+ this.session.clearDisk().catch(() => { });
148
164
  }
149
165
 
150
166
  async loadSession() {
@@ -156,7 +172,7 @@ export class KikuAgent {
156
172
  content: this.systemPrompt
157
173
  }
158
174
  ];
159
-
175
+
160
176
  for (const msg of this.session.messages) {
161
177
  if (msg.role === 'user') {
162
178
  const textPart = msg.parts.find(p => p.type === 'text');
@@ -224,6 +240,47 @@ export class KikuAgent {
224
240
  return result;
225
241
  }
226
242
 
243
+ async removeMcp(name) {
244
+ const result = await this.mcpManager.remove(name);
245
+ this.router.setTools(this.getAllToolsSchema());
246
+ this.systemPrompt = this.buildSystemPrompt();
247
+ if (this.conversationHistory && this.conversationHistory[0]?.role === 'system') {
248
+ this.conversationHistory[0].content = this.getEnrichedSystemPrompt();
249
+ }
250
+ return result;
251
+ }
252
+
253
+ async reconnectMcp(name) {
254
+ const result = name && name !== 'all'
255
+ ? await this.mcpManager.reconnect(name)
256
+ : await this.mcpManager.reconnectAll();
257
+ this.router.setTools(this.getAllToolsSchema());
258
+ this.systemPrompt = this.buildSystemPrompt();
259
+ if (this.conversationHistory && this.conversationHistory[0]?.role === 'system') {
260
+ this.conversationHistory[0].content = this.getEnrichedSystemPrompt();
261
+ }
262
+ return result;
263
+ }
264
+
265
+ async saveMcpConfig(target = 'global') {
266
+ if (target === 'workspace' || target === 'local') {
267
+ return this.mcpManager.saveToWorkspace();
268
+ }
269
+ return this.mcpManager.savePersistedConfig();
270
+ }
271
+
272
+ async loadMcpConfig(customPath) {
273
+ const results = customPath
274
+ ? await this.mcpManager.loadConfigFile(customPath)
275
+ : await this.mcpManager.loadPersistedServers();
276
+ this.router.setTools(this.getAllToolsSchema());
277
+ this.systemPrompt = this.buildSystemPrompt();
278
+ if (this.conversationHistory && this.conversationHistory[0]?.role === 'system') {
279
+ this.conversationHistory[0].content = this.getEnrichedSystemPrompt();
280
+ }
281
+ return results;
282
+ }
283
+
227
284
  buildSystemPrompt() {
228
285
  const allTools = this.getAllToolsSchema();
229
286
  const toolsSummary = allTools.map(t => {
@@ -280,9 +337,9 @@ ${toolsSummary}
280
337
  - Directory listing ➔ list_directory
281
338
  - Screenshot ➔ screenshot_webpage
282
339
 
283
- 6. AGGRESSIVE WEB SEARCH FALLBACK:
284
- - If local tools return insufficient results, IMMEDIATELY call web_search or web_extract.
285
- - Combine local data WITH web search for complete, accurate answers.
340
+ 6. AGGRESSIVE WEB SEARCH FALLBACK (CRITICAL):
341
+ - If a local catalog tool (e.g. search_works, search_discography, get_brand_endorsements, get_awards) returns not_found, empty, or 0 results, DO NOT give up or guess. You MUST IMMEDIATELY call web_search or web_extract in the next step to look up live external facts online.
342
+ - Combine local catalog data WITH live web search for complete, accurate, and up-to-date answers.
286
343
  - Use web_extract for comprehensive research on any topic.
287
344
 
288
345
  7. LEGAL & CAREER KNOWLEDGE:
@@ -367,18 +424,18 @@ ${toolsSummary}
367
424
  for (const topic of topics) {
368
425
  this.memory.trackTopicInterest(topic);
369
426
  }
370
-
427
+
371
428
  const {
372
- onStepStart = () => {},
373
- onReasoningStart = () => {},
374
- onReasoningDelta = () => {},
375
- onReasoningEnd = () => {},
376
- onToolStart = () => {},
377
- onToolResult = () => {},
378
- onTokenStart = () => {},
379
- onTokenDelta = () => {},
380
- onFinalAnswer = () => {},
381
- onError = () => {},
429
+ onStepStart = () => { },
430
+ onReasoningStart = () => { },
431
+ onReasoningDelta = () => { },
432
+ onReasoningEnd = () => { },
433
+ onToolStart = () => { },
434
+ onToolResult = () => { },
435
+ onTokenStart = () => { },
436
+ onTokenDelta = () => { },
437
+ onFinalAnswer = () => { },
438
+ onError = () => { },
382
439
  signal = null
383
440
  } = callbacks;
384
441
 
@@ -509,6 +566,15 @@ ${toolsSummary}
509
566
  let toolCalls = assistantMessage.tool_calls || [];
510
567
  if (toolCalls.length === 0 && assistantMessage.content) {
511
568
  toolCalls = this.parseTextToolCalls(assistantMessage.content);
569
+
570
+ if (toolCalls.length > 0) {
571
+ let cleaned = assistantMessage.content;
572
+
573
+ cleaned = cleaned.replace(/```(?:json)?\s*\{[\s\S]*?\}\s*```/g, '');
574
+
575
+ cleaned = cleaned.replace(/Action:\s*[a-zA-Z0-9_]+\s*\nAction Input:\s*(?:\{[\s\S]*?\}|[^\n]+)/g, '');
576
+ assistantMessage.content = cleaned.trim();
577
+ }
512
578
  }
513
579
 
514
580
  this.conversationHistory.push(assistantMessage);
@@ -519,8 +585,8 @@ ${toolsSummary}
519
585
 
520
586
  const finalAnswer = assistantMessage.content || (
521
587
  dominantLang === 'zh' ? "抱歉,无法处理您的回答。" :
522
- dominantLang === 'en' ? "Sorry, unable to process a response." :
523
- "ขออภัยค่ะ ไม่สามารถประมวลผลคำตอบได้"
588
+ dominantLang === 'en' ? "Sorry, unable to process a response." :
589
+ "ขออภัยค่ะ ไม่สามารถประมวลผลคำตอบได้"
524
590
  );
525
591
  onFinalAnswer(finalAnswer, { durationMs: totalDurationMs, language: dominantLang });
526
592
  this._extractFactsFromFinalAnswer(finalAnswer);
@@ -532,11 +598,8 @@ ${toolsSummary}
532
598
  return finalAnswer;
533
599
  }
534
600
 
535
- for (const [toolCallIndex, tc] of toolCalls.entries()) {
536
- if (signal?.aborted) {
537
- this.stateMachine.reset();
538
- throw createAbortError();
539
- }
601
+ const executionPromises = toolCalls.map(async (tc, toolCallIndex) => {
602
+ if (signal?.aborted) throw createAbortError();
540
603
 
541
604
  const toolName = tc?.function?.name || '';
542
605
  const toolCallId = tc?.id || `call_${queryId}_${currentIteration}_${toolCallIndex}`;
@@ -560,20 +623,21 @@ ${toolsSummary}
560
623
  validationWarnings: prepared.warnings,
561
624
  };
562
625
  eventBus.emit(Events.AGENT_TOOL_ERROR, {
563
- queryId, toolName, args: tc.function.arguments,
626
+ queryId, toolName, args: tc?.function?.arguments,
564
627
  error: prepared.error,
565
628
  });
566
629
  onToolStart(toolName, {});
567
630
  onToolResult(toolName, rejectedResult, 0);
568
- const toolMessage = {
569
- role: 'tool',
570
- tool_call_id: toolCallId,
571
- name: toolName,
572
- content: JSON.stringify(rejectedResult)
631
+ return {
632
+ toolMessage: {
633
+ role: 'tool',
634
+ tool_call_id: toolCallId,
635
+ name: toolName,
636
+ content: JSON.stringify(rejectedResult)
637
+ }
573
638
  };
574
- this.conversationHistory.push(toolMessage);
575
- continue;
576
639
  }
640
+
577
641
  if (prepared.warnings.length > 0) {
578
642
  eventBus.emit(Events.AGENT_TOOL_ERROR, {
579
643
  queryId, toolName, warnings: prepared.warnings,
@@ -581,7 +645,6 @@ ${toolsSummary}
581
645
  }
582
646
 
583
647
  const finalArgs = prepared.args;
584
-
585
648
  this.stateMachine.transition(AgentStates.FETCHING_TOOL, { toolName });
586
649
 
587
650
  const toolPart = this.session.addToolPart(currentAssistantSessionMsg, toolName, finalArgs);
@@ -610,30 +673,40 @@ ${toolsSummary}
610
673
 
611
674
  this._extractFactsFromToolResult(toolName, finalArgs, result);
612
675
 
613
- const toolMessage = {
614
- role: 'tool',
615
- tool_call_id: toolCallId,
616
- name: toolName,
617
- content: JSON.stringify({
618
- authoritative: true,
619
- result,
620
- instruction: result.status === 'not_found'
621
- ? 'No matching data was found. Do not invent records; say that the catalog has no match or use an appropriate fallback tool.'
622
- : result.status === 'error'
623
- ? 'The tool failed. Do not claim that the requested facts were verified.'
624
- : 'Use only facts present in this result when answering.'
625
- })
676
+
677
+ const cappedResult = this._capToolResultForHistory(result);
678
+
679
+ return {
680
+ toolMessage: {
681
+ role: 'tool',
682
+ tool_call_id: toolCallId,
683
+ name: toolName,
684
+ content: JSON.stringify({
685
+ authoritative: true,
686
+ result: cappedResult,
687
+ instruction: result.status === 'not_found'
688
+ ? 'No matching data was found. Do not invent records; say that the catalog has no match or use an appropriate fallback tool.'
689
+ : result.status === 'error'
690
+ ? 'The tool failed. Do not claim that the requested facts were verified.'
691
+ : 'Use only facts present in this result when answering. Do not echo raw JSON data — summarize in natural language.'
692
+ })
693
+ }
626
694
  };
695
+ });
627
696
 
628
- this.conversationHistory.push(toolMessage);
697
+ const toolResults = await Promise.all(executionPromises);
698
+ for (const item of toolResults) {
699
+ if (item?.toolMessage) {
700
+ this.conversationHistory.push(item.toolMessage);
701
+ }
629
702
  }
630
703
  }
631
704
 
632
- const langPrompt = dominantLang === 'zh'
705
+ const langPrompt = dominantLang === 'zh'
633
706
  ? "请根据上述工具查询结果,直接回答(不要开头问候语),用流畅优雅的中文,先1-2句概述,再列要点。包含具体的 YouTube 真实链接(如 Direct Watch: https://www.youtube.com/watch?v=... หรือ Search: https://www.youtube.com/results?search_query=...)及事实数据。"
634
707
  : dominantLang === 'en'
635
- ? "Based on the tool results above, answer directly (no opening greeting or template phrase). Give a 1-2 sentence summary, then concise bullet points in polished English. Include verified YouTube direct and search links (https://www.youtube.com/results?search_query=...)."
636
- : "สรุปคำตอบจากผลลัพธ์เครื่องมือข้างต้น โดยไม่ต้องทักทาย ตอบตรงประเด็น 1-2 ประโยค แล้วตามด้วย bullet points สั้นๆ เป็นภาษาไทย พร้อมแสดงลิงก์ YouTube จริง (Direct Watch Link และ https://www.youtube.com/results?search_query=...) และข้อมูลผลงานที่ถูกต้องแม่นยำ";
708
+ ? "Based on the tool results above, answer directly (no opening greeting or template phrase). Give a 1-2 sentence summary, then concise bullet points in polished English. Include verified YouTube direct and search links (https://www.youtube.com/results?search_query=...)."
709
+ : "สรุปคำตอบจากผลลัพธ์เครื่องมือข้างต้น โดยไม่ต้องทักทาย ตอบตรงประเด็น 1-2 ประโยค แล้วตามด้วย bullet points สั้นๆ เป็นภาษาไทย พร้อมแสดงลิงก์ YouTube จริง (Direct Watch Link และ https://www.youtube.com/results?search_query=...) และข้อมูลผลงานที่ถูกต้องแม่นยำ";
637
710
 
638
711
  this.conversationHistory.push({ role: 'user', content: langPrompt });
639
712
 
@@ -677,19 +750,55 @@ ${toolsSummary}
677
750
  const defaultDone = dominantLang === 'zh' ? "查询完毕" : dominantLang === 'en' ? "Search complete." : "เสร็จสิ้นการค้นหาข้อมูล";
678
751
  const output = finalRes?.content || defaultDone;
679
752
  onFinalAnswer(output, { durationMs: totalDurationMs, language: dominantLang });
680
-
753
+
681
754
  this._extractFactsFromFinalAnswer(output);
682
755
 
683
756
  this.stateMachine.transition(AgentStates.COMPLETE);
684
757
  eventBus.emit(Events.AGENT_FINAL_ANSWER, { queryId, answer: output, durationMs: totalDurationMs });
685
758
  eventBus.emit(Events.AGENT_QUERY_END, { queryId, durationMs: totalDurationMs });
686
759
  this.stateMachine.reset();
687
-
760
+
688
761
  this.saveSession().catch(err => console.error('Session save failed:', err));
689
-
762
+
690
763
  return output;
691
764
  }
692
765
 
766
+ _capToolResultForHistory(result) {
767
+ if (!result || typeof result !== 'object') return result;
768
+
769
+ const MAX_RESULT_BYTES = 4096;
770
+ const serialized = JSON.stringify(result);
771
+ if (serialized.length <= MAX_RESULT_BYTES) return result;
772
+
773
+
774
+ const truncate = (obj) => {
775
+ if (Array.isArray(obj)) {
776
+ if (obj.length > 8) {
777
+ const kept = obj.slice(0, 8).map(truncate);
778
+ kept.push({ _truncated: true, totalItems: obj.length, showing: 8 });
779
+ return kept;
780
+ }
781
+ return obj.map(truncate);
782
+ }
783
+ if (obj && typeof obj === 'object') {
784
+ const out = {};
785
+ for (const [key, val] of Object.entries(obj)) {
786
+ out[key] = truncate(val);
787
+ }
788
+ return out;
789
+ }
790
+
791
+ if (typeof obj === 'string' && obj.length > 500) {
792
+ return obj.slice(0, 500) + '…[truncated]';
793
+ }
794
+ return obj;
795
+ };
796
+
797
+ const capped = truncate(result);
798
+ capped._cappedForHistory = true;
799
+ return capped;
800
+ }
801
+
693
802
  _extractFactsFromToolResult(toolName, toolArgs, result) {
694
803
  if (!result || result.status === 'error' || result.status === 'not_found') return;
695
804