kiku-agent 1.0.3 → 1.0.5

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,7 @@ 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.3.tgz`.
51
+ For a local checkout before the package is published, use `npm pack` followed by `npm install -g ./kiku-agent-1.0.5.tgz`.
52
52
 
53
53
  ### ✨ Features
54
54
  - **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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiku-agent",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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": {
@@ -51,8 +51,7 @@
51
51
  "kiku-agent": "node server/agent/cli.js",
52
52
  "kiku-mcp": "node server/mcp/stdio.js",
53
53
  "kiku-ink": "npx tsx server/agent/ink/main.jsx",
54
- "test": "node --test server/agent/tests/*.test.js server/mcp/tests/*.test.js",
55
- "prepublishOnly": "npm test"
54
+ "test": "node --test server/agent/tests/*.test.js server/mcp/tests/*.test.js"
56
55
  },
57
56
  "dependencies": {
58
57
  "@modelcontextprotocol/sdk": "^1.30.0",
@@ -467,7 +467,7 @@ class InteractiveTuiPrompt {
467
467
  const mcpCount = this.agent?.mcpManager?.servers?.size || 0;
468
468
  const mcpLabel = mcpCount > 0 ? `${C.emerald}● ${mcpCount} MCP${C.reset}` : `${C.slate}○ 0 MCP${C.reset}`;
469
469
 
470
- const headerLeft = ` ${C.bold}${C.sakura}◆ KIKU AGENT${C.reset} ${C.slate}v1.0.3${C.reset}`;
470
+ const headerLeft = ` ${C.bold}${C.sakura}◆ KIKU AGENT${C.reset} ${C.slate}v1.0.5${C.reset}`;
471
471
  const headerRight = `${modelLabel} ${C.slate}│${C.reset} ${mcpLabel} ${C.slate}│${C.reset} ${C.roseGlow}◆ Sakura${C.reset} `;
472
472
  const safeCols = Math.max(1, termCols - 1);
473
473
  const headerPad = Math.max(1, safeCols - getStringDisplayWidth(headerLeft) - getStringDisplayWidth(headerRight));
@@ -1460,7 +1460,7 @@ export const handlers = {
1460
1460
  printSolidPanel({
1461
1461
  title: 'Version',
1462
1462
  lines: [
1463
- `${C.gold}Version:${C.reset} ${C.white}1.0.3${C.reset}`,
1463
+ `${C.gold}Version:${C.reset} ${C.white}1.0.5${C.reset}`,
1464
1464
  `${C.gold}Node:${C.reset} ${C.white}${process.version}${C.reset}`,
1465
1465
  `${C.gold}Platform:${C.reset} ${C.white}${process.platform} ${process.arch}${C.reset}`,
1466
1466
  ],
@@ -8,7 +8,7 @@ import { KIKU_DATA_DIR } from './appPaths.js';
8
8
 
9
9
  export class McpClientManager {
10
10
  constructor(options = {}) {
11
- this.clientInfo = options.clientInfo || { name: 'kiku-agent', version: '1.0.3' };
11
+ this.clientInfo = options.clientInfo || { name: 'kiku-agent', version: '1.0.5' };
12
12
  this.autoLoad = options.autoLoad !== false;
13
13
  this.servers = new Map();
14
14
  this.toolRegistry = new Map();
@@ -476,7 +476,7 @@ export function getWelcomeHeroLines() {
476
476
  return [
477
477
  '',
478
478
  ` ${C.sakura}██╗ ██╗██╗██╗ ██╗██╗ ██╗ █████╗ ██╗${C.reset} ${C.bold}${C.white}Kiku AI Terminal Agent${C.reset}`,
479
- ` ${C.sakura}██║ ██╔╝██║██║ ██╔╝██║ ██║ ██╔══██╗██║${C.reset} ${C.gold}v1.0.3${C.reset} ${C.slate}• Intelligent Fan Portal${C.reset}`,
479
+ ` ${C.sakura}██║ ██╔╝██║██║ ██╔╝██║ ██║ ██╔══██╗██║${C.reset} ${C.gold}v1.0.5${C.reset} ${C.slate}• Intelligent Fan Portal${C.reset}`,
480
480
  ` ${C.roseGlow}█████═╝ ██║█████═╝ ██║ ██║ ███████║██║${C.reset} ${C.cyberCyan}SSOT • DAG • Dynamic Tools${C.reset}`,
481
481
  ` ${C.roseGlow}██╔═██╗ ██║██╔═██╗ ██║ ██║ ██╔══██╗██║${C.reset} ${C.emerald}LM Studio / Cloud Providers${C.reset}`,
482
482
  ` ${C.sakura}██║ ╚██╗██║██║ ╚██╗╚██████╔╝ ██║ ██║██║${C.reset}`,
@@ -2,41 +2,64 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { spawn, execSync } from 'child_process';
4
4
  import { fileURLToPath } from 'url';
5
+ import { KIKU_DATA_DIR } from './appPaths.js';
5
6
 
6
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
8
  const ENV_PATH = path.resolve(__dirname, '..', '..', '.env');
9
+ const AUTH_CONFIG_PATH = path.join(KIKU_DATA_DIR, 'yt_auth.json');
10
+ const SECURE_COOKIE_PATH = path.join(KIKU_DATA_DIR, 'cookies.txt');
8
11
  const YTDLP_CMD = process.env.KIKU_YTDLP || 'yt-dlp';
9
12
 
10
- const BROWSERS = ['chrome', 'edge', 'brave', 'firefox'];
11
-
12
13
 
14
+ const BROWSERS = ['firefox', 'chrome', 'edge', 'brave'];
13
15
 
14
16
  function findBrowserExe(browser) {
15
17
  const isWin = process.platform === 'win32';
16
18
  const probes = {
17
- chrome: { cmd: isWin ? 'chrome' : 'google-chrome', paths: [
18
- 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
19
- (process.env.LOCALAPPDATA || '') + '\\Google\\Chrome\\Application\\chrome.exe',
20
- 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
21
- ]},
22
- edge: { cmd: isWin ? 'msedge' : 'microsoft-edge', paths: [
23
- 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
24
- 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe'
25
- ]},
26
- brave: { cmd: isWin ? 'brave' : 'brave-browser', paths: [
27
- 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
28
- (process.env.LOCALAPPDATA || '') + '\\BraveSoftware\\Brave-Browser\\Application\\brave.exe'
29
- ]},
30
- firefox: { cmd: isWin ? 'firefox' : 'firefox', paths: [
31
- 'C:\\Program Files\\Mozilla Firefox\\firefox.exe',
32
- (process.env.LOCALAPPDATA || '') + '\\Mozilla Firefox\\firefox.exe'
33
- ]}
19
+ firefox: {
20
+ cmd: isWin ? 'firefox' : 'firefox',
21
+ paths: [
22
+ 'C:\\Program Files\\Mozilla Firefox\\firefox.exe',
23
+ 'C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe',
24
+ (process.env.LOCALAPPDATA || '') + '\\Mozilla Firefox\\firefox.exe'
25
+ ]
26
+ },
27
+ chrome: {
28
+ cmd: isWin ? 'chrome' : 'google-chrome',
29
+ paths: [
30
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
31
+ (process.env.LOCALAPPDATA || '') + '\\Google\\Chrome\\Application\\chrome.exe',
32
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
33
+ ]
34
+ },
35
+ edge: {
36
+ cmd: isWin ? 'msedge' : 'microsoft-edge',
37
+ paths: [
38
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
39
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
40
+ (process.env.LOCALAPPDATA || '') + '\\Microsoft\\Edge\\Application\\msedge.exe'
41
+ ]
42
+ },
43
+ brave: {
44
+ cmd: isWin ? 'brave' : 'brave-browser',
45
+ paths: [
46
+ 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
47
+ (process.env.LOCALAPPDATA || '') + '\\BraveSoftware\\Brave-Browser\\Application\\brave.exe'
48
+ ]
49
+ }
34
50
  };
51
+
35
52
  const p = probes[browser];
36
53
  if (!p) return null;
37
- try { execSync(isWin ? `where ${p.cmd}` : `which ${p.cmd}`, { stdio: 'ignore', windowsHide: true }); return p.cmd; } catch {}
54
+ try {
55
+ execSync(isWin ? `where ${p.cmd}` : `which ${p.cmd}`, { stdio: 'ignore', windowsHide: true });
56
+ return p.cmd;
57
+ } catch {}
58
+
38
59
  if (isWin) {
39
- for (const fp of p.paths) { if (fs.existsSync(fp)) return fp; }
60
+ for (const fp of p.paths) {
61
+ if (fp && fs.existsSync(fp)) return fp;
62
+ }
40
63
  }
41
64
  return null;
42
65
  }
@@ -63,9 +86,35 @@ function openBrowser(url, browser) {
63
86
  }
64
87
  }
65
88
 
89
+ function readAuthConfig() {
90
+ try {
91
+ if (fs.existsSync(AUTH_CONFIG_PATH)) {
92
+ return JSON.parse(fs.readFileSync(AUTH_CONFIG_PATH, 'utf8')) || {};
93
+ }
94
+ } catch {}
95
+ return {};
96
+ }
66
97
 
98
+ function writeAuthConfig(updates) {
99
+ try {
100
+ fs.mkdirSync(KIKU_DATA_DIR, { recursive: true });
101
+ const current = readAuthConfig();
102
+ const merged = { ...current, ...updates };
103
+ fs.writeFileSync(AUTH_CONFIG_PATH, JSON.stringify(merged, null, 2), 'utf8');
104
+ } catch {}
105
+ }
67
106
 
68
107
  function loadEnv() {
108
+
109
+ const userConfig = readAuthConfig();
110
+ if (userConfig.KIKU_YT_BROWSER && process.env.KIKU_YT_BROWSER === undefined) {
111
+ process.env.KIKU_YT_BROWSER = userConfig.KIKU_YT_BROWSER;
112
+ }
113
+ if (userConfig.KIKU_YT_COOKIES && process.env.KIKU_YT_COOKIES === undefined) {
114
+ process.env.KIKU_YT_COOKIES = userConfig.KIKU_YT_COOKIES;
115
+ }
116
+
117
+
69
118
  try {
70
119
  if (!fs.existsSync(ENV_PATH)) return;
71
120
  const text = fs.readFileSync(ENV_PATH, 'utf8');
@@ -73,7 +122,7 @@ function loadEnv() {
73
122
  const m = line.match(/^\s*([\w.-]+)\s*=\s*(.*)\s*$/);
74
123
  if (!m) continue;
75
124
  const key = m[1];
76
- let val = m[2].replace(/^["']|["']$/g, '');
125
+ const val = m[2].replace(/^["']|["']$/g, '');
77
126
  if (process.env[key] === undefined) process.env[key] = val;
78
127
  }
79
128
  } catch {}
@@ -92,24 +141,32 @@ function readEnvMap() {
92
141
  return map;
93
142
  }
94
143
 
95
-
96
144
  function writeEnv(updates) {
97
- const map = readEnvMap();
98
- Object.assign(map, updates);
99
- const out = Object.entries(map).map(([k, v]) => `${k}=${v}`).join('\n') + '\n';
100
- fs.writeFileSync(ENV_PATH, out, 'utf8');
101
- }
102
145
 
146
+ for (const [k, v] of Object.entries(updates)) {
147
+ process.env[k] = v;
148
+ }
149
+ writeAuthConfig(updates);
150
+
151
+
152
+ try {
153
+ const map = readEnvMap();
154
+ Object.assign(map, updates);
155
+ const out = Object.entries(map).map(([k, v]) => `${k}=${v}`).join('\n') + '\n';
156
+ fs.writeFileSync(ENV_PATH, out, 'utf8');
157
+ } catch {}
158
+ }
103
159
 
104
160
  function hasSession(browser) {
105
161
  return new Promise((resolve) => {
106
- const timeoutMs = 12000;
162
+ const timeoutMs = 8000;
107
163
  const args = [
108
164
  '--cookies-from-browser', browser,
109
165
  '--print', '%(title)s',
110
166
  '--no-warnings',
167
+ '--no-playlist',
111
168
  '--no-check-certificates',
112
- 'ytsearch1:Ju Jingyi'
169
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
113
170
  ];
114
171
  const p = spawn(YTDLP_CMD, args, {
115
172
  windowsHide: true,
@@ -128,51 +185,71 @@ function hasSession(browser) {
128
185
  }
129
186
  }, timeoutMs);
130
187
 
131
- p.stdout.on('data', d => { out += d.toString(); });
132
- p.stderr.on('data', d => { err += d.toString(); });
133
- p.on('error', () => { if (!resolved) { resolved = true; clearTimeout(timer); resolve(false); } });
188
+ p.stdout?.on('data', d => { out += d.toString(); });
189
+ p.stderr?.on('data', d => { err += d.toString(); });
190
+ p.on('error', () => {
191
+ if (!resolved) {
192
+ resolved = true;
193
+ clearTimeout(timer);
194
+ resolve(false);
195
+ }
196
+ });
134
197
  p.on('close', (code) => {
135
198
  if (resolved) return;
136
199
  resolved = true;
137
200
  clearTimeout(timer);
138
201
  const title = out.trim();
139
- const blocked = /403|Forbidden|Sign in|confirm you'?re not a bot|Login Required|Private Video/i.test(err);
202
+ const blocked = /403|Forbidden|Sign in|confirm you'?re not a bot|Login Required|Private Video|database is locked/i.test(err);
140
203
  resolve(code === 0 && title.length > 0 && !blocked);
141
204
  });
142
205
  });
143
206
  }
144
207
 
145
-
146
-
147
208
  let _ensured = null;
148
209
  let _lastFailTime = 0;
149
- const RETRY_COOLDOWN_MS = 30000;
150
-
210
+ let _inFlightPromise = null;
211
+ const RETRY_COOLDOWN_MS = 30000;
151
212
 
152
213
  export async function ensureYouTubeCookies(opts = {}) {
153
-
154
214
  if (_ensured && _ensured.ok) return _ensured;
155
215
  if (_ensured && !_ensured.ok && (Date.now() - _lastFailTime) < RETRY_COOLDOWN_MS && !opts.force) {
156
216
  return _ensured;
157
217
  }
158
218
 
219
+
220
+ if (_inFlightPromise) {
221
+ return _inFlightPromise;
222
+ }
223
+
224
+ _inFlightPromise = (async () => {
225
+ try {
226
+ return await _ensureYouTubeCookiesImpl(opts);
227
+ } finally {
228
+ _inFlightPromise = null;
229
+ }
230
+ })();
231
+
232
+ return _inFlightPromise;
233
+ }
234
+
235
+ async function _ensureYouTubeCookiesImpl(opts = {}) {
159
236
  loadEnv();
160
237
 
161
-
162
- if (process.env.KIKU_YT_COOKIES && fs.existsSync(process.env.KIKU_YT_COOKIES)) {
163
- _ensured = { ok: true, source: 'file', cookiesPath: process.env.KIKU_YT_COOKIES };
238
+
239
+ const cookiesCandidate = process.env.KIKU_YT_COOKIES || (fs.existsSync(SECURE_COOKIE_PATH) ? SECURE_COOKIE_PATH : null);
240
+ if (cookiesCandidate && fs.existsSync(cookiesCandidate)) {
241
+ _ensured = { ok: true, source: 'file', cookiesPath: cookiesCandidate };
164
242
  return _ensured;
165
243
  }
166
244
 
167
245
 
168
246
  const available = detectBrowsers();
169
247
  if (!available.length) {
170
- _ensured = { ok: false, reason: 'no-browser', detail: 'No supported browser found (Chrome, Edge, Brave, Firefox)' };
248
+ _ensured = { ok: false, reason: 'no-browser', detail: 'No supported browser found (Firefox, Chrome, Edge, Brave)' };
171
249
  _lastFailTime = Date.now();
172
250
  return _ensured;
173
251
  }
174
252
 
175
-
176
253
  const preferred = process.env.KIKU_YT_BROWSER;
177
254
  const candidates = preferred && available.includes(preferred)
178
255
  ? [preferred, ...available.filter(b => b !== preferred)]
@@ -180,9 +257,7 @@ export async function ensureYouTubeCookies(opts = {}) {
180
257
 
181
258
  for (const b of candidates) {
182
259
  if (await hasSession(b)) {
183
-
184
260
  writeEnv({ KIKU_YT_BROWSER: b });
185
- process.env.KIKU_YT_BROWSER = b;
186
261
  _ensured = { ok: true, source: 'existing-session', browser: b };
187
262
  return _ensured;
188
263
  }
@@ -195,7 +270,7 @@ export async function ensureYouTubeCookies(opts = {}) {
195
270
  return _ensured;
196
271
  }
197
272
 
198
-
273
+
199
274
  const loginBrowser = candidates[0];
200
275
  const loginUrl = 'https://accounts.google.com/ServiceLogin?service=youtube&continue=https://www.youtube.com/';
201
276
  openBrowser(loginUrl, loginBrowser);
@@ -208,21 +283,24 @@ export async function ensureYouTubeCookies(opts = {}) {
208
283
  await new Promise(r => setTimeout(r, 2000));
209
284
 
210
285
  const deadline = Date.now() + (opts.timeout || 120000);
211
- const pollInterval = 3000;
286
+ const pollInterval = 4000;
212
287
  let attempt = 0;
213
288
 
214
289
  while (Date.now() < deadline) {
215
290
  attempt++;
216
291
  const remaining = Math.round((deadline - Date.now()) / 1000);
217
- process.stdout.write(`\r Checking... (attempt ${attempt}, ${remaining}s remaining) `);
292
+ if (!opts.silent && process.stdout.isTTY) {
293
+ process.stdout.write(`\r Checking session... (attempt ${attempt}, ${remaining}s remaining) `);
294
+ }
218
295
 
219
296
  for (const b of available) {
220
297
  if (await hasSession(b)) {
221
298
  writeEnv({ KIKU_YT_BROWSER: b });
222
- process.env.KIKU_YT_BROWSER = b;
223
- process.stdout.write('\r' + ' '.repeat(60) + '\r');
299
+ if (process.stdout.isTTY) {
300
+ process.stdout.write('\r' + ' '.repeat(65) + '\r');
301
+ }
224
302
  console.log(` [OK] YouTube session detected in ${b}.`);
225
- console.log(` Saved KIKU_YT_BROWSER=${b} to .env\n`);
303
+ console.log(` Saved preference (KIKU_YT_BROWSER=${b})\n`);
226
304
  _ensured = { ok: true, source: 'new-session', browser: b };
227
305
  return _ensured;
228
306
  }
@@ -231,14 +309,15 @@ export async function ensureYouTubeCookies(opts = {}) {
231
309
  await new Promise(r => setTimeout(r, pollInterval));
232
310
  }
233
311
 
234
- process.stdout.write('\r' + ' '.repeat(60) + '\r');
312
+ if (process.stdout.isTTY) {
313
+ process.stdout.write('\r' + ' '.repeat(65) + '\r');
314
+ }
235
315
  console.log(` [ERROR] Login timed out. You can retry by running the /login command.\n`);
236
316
  _ensured = { ok: false, reason: 'login-timeout' };
237
317
  _lastFailTime = Date.now();
238
318
  return _ensured;
239
319
  }
240
320
 
241
-
242
321
  export async function exportCookiesToFile() {
243
322
  loadEnv();
244
323
 
@@ -249,22 +328,22 @@ export async function exportCookiesToFile() {
249
328
  : available;
250
329
 
251
330
  for (const b of candidates) {
252
- const cookiesPath = path.resolve(__dirname, '..', '..', '.kiku_yt_cookies.txt');
253
331
  try {
332
+ fs.mkdirSync(KIKU_DATA_DIR, { recursive: true });
254
333
  const args = [
255
334
  '--cookies-from-browser', b,
256
- '--cookies', cookiesPath,
335
+ '--cookies', SECURE_COOKIE_PATH,
257
336
  '--no-warnings',
258
337
  '--skip-download',
259
- 'ytsearch1:__export_test__'
338
+ 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
260
339
  ];
261
340
  await new Promise((resolve, reject) => {
262
341
  const p = spawn(YTDLP_CMD, args, { windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'] });
263
342
  let err = '';
264
- p.stderr.on('data', d => { err += d.toString(); });
343
+ p.stderr?.on('data', d => { err += d.toString(); });
265
344
  p.on('error', reject);
266
345
  p.on('close', code => {
267
- if (code === 0 && fs.existsSync(cookiesPath)) {
346
+ if (code === 0 && fs.existsSync(SECURE_COOKIE_PATH)) {
268
347
  resolve();
269
348
  } else {
270
349
  reject(new Error(err || `yt-dlp exited ${code}`));
@@ -272,9 +351,8 @@ export async function exportCookiesToFile() {
272
351
  });
273
352
  });
274
353
 
275
- writeEnv({ KIKU_YT_COOKIES: cookiesPath });
276
- process.env.KIKU_YT_COOKIES = cookiesPath;
277
- return { ok: true, browser: b, cookiesPath };
354
+ writeEnv({ KIKU_YT_COOKIES: SECURE_COOKIE_PATH });
355
+ return { ok: true, browser: b, cookiesPath: SECURE_COOKIE_PATH };
278
356
  } catch {}
279
357
  }
280
358
 
@@ -284,16 +362,17 @@ export async function exportCookiesToFile() {
284
362
  export function resetCookieCheck() {
285
363
  _ensured = null;
286
364
  _lastFailTime = 0;
365
+ _inFlightPromise = null;
287
366
  }
288
367
 
289
368
  export function getCookieStatus() {
290
369
  return {
291
370
  ensured: _ensured,
292
371
  envBrowser: process.env.KIKU_YT_BROWSER || null,
293
- envCookies: process.env.KIKU_YT_COOKIES || null,
294
- envPath: ENV_PATH
372
+ envCookies: process.env.KIKU_YT_COOKIES || (fs.existsSync(SECURE_COOKIE_PATH) ? SECURE_COOKIE_PATH : null),
373
+ envPath: ENV_PATH,
374
+ authConfigPath: AUTH_CONFIG_PATH
295
375
  };
296
376
  }
297
377
 
298
-
299
378
  loadEnv();