kiku-agent 1.0.9 → 1.0.11

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.
@@ -771,10 +771,14 @@ export const handlers = {
771
771
  },
772
772
 
773
773
  '/session': async (ctx) => {
774
- const { agent, sub } = ctx;
774
+ const { agent, sub, extra } = ctx;
775
775
  const sess = agent.session;
776
776
  const action = (sub || 'info').toLowerCase();
777
777
 
778
+ if (/^\d+$/.test(action) || action.startsWith('sess_')) {
779
+ return handlers['/session']({ ...ctx, sub: 'load', extra: sub });
780
+ }
781
+
778
782
  if (action === 'info' || !action) {
779
783
  const msgCount = sess.messages.length;
780
784
  const userMsgs = sess.messages.filter(m => m.role === 'user').length;
@@ -783,17 +787,31 @@ export const handlers = {
783
787
  const totalDur = ((sess.stats.totalDurationMs || 0) / 1000).toFixed(1);
784
788
  const createdAt = sess.createdAt ? sess.createdAt.toLocaleString() : 'unknown';
785
789
 
790
+ const lines = [
791
+ `${C.gold}Session ID:${C.reset} ${C.white}${sess.id}${C.reset}`,
792
+ `${C.gold}Created At:${C.reset} ${C.white}${createdAt}${C.reset}`,
793
+ `${C.gold}Total Messages:${C.reset} ${C.cyberCyan}${msgCount}${C.reset} total ${C.dim}(${userMsgs} user, ${assistantMsgs} assistant)${C.reset}`,
794
+ `${C.gold}Total Queries:${C.reset} ${C.cyberCyan}${sess.stats.totalQueries}${C.reset}`,
795
+ `${C.gold}Tool Calls:${C.reset} ${C.cyberCyan}${toolCalls}${C.reset}`,
796
+ `${C.gold}Total Duration:${C.reset} ${C.cyberCyan}${totalDur}s${C.reset}`,
797
+ `${C.gold}Thinking Mode:${C.reset} ${C.bold}${C.gold}${sess.thinkingMode.toUpperCase()}${C.reset}`
798
+ ];
799
+
800
+ const toolStats = typeof sess.getToolStats === 'function' ? sess.getToolStats() : {};
801
+ const toolEntries = Object.entries(toolStats);
802
+ if (toolEntries.length > 0) {
803
+ lines.push('', `${C.bold}${C.lavender}Tool Executions Breakdown:${C.reset}`);
804
+ for (const [name, s] of toolEntries) {
805
+ const dur = (s.totalDurationMs / 1000).toFixed(2);
806
+ lines.push(` ${C.cyberCyan}• ${name.padEnd(24)}${C.reset} ${C.white}${s.count} call(s)${C.reset} ${C.dim}(${s.successCount} ok, ${s.notFoundCount} empty, ${s.errorCount} err • ${dur}s)${C.reset}`);
807
+ }
808
+ }
809
+
810
+ lines.push('', `${C.dim}Commands: /session summary | /session export [markdown|json|text] | /session copy | /session clear${C.reset}`);
811
+
786
812
  printSolidPanel({
787
- title: `Active Session Details`,
788
- lines: [
789
- `${C.gold}Session ID:${C.reset} ${C.white}${sess.id}${C.reset}`,
790
- `${C.gold}Created At:${C.reset} ${C.white}${createdAt}${C.reset}`,
791
- `${C.gold}Total Messages:${C.reset} ${C.cyberCyan}${msgCount}${C.reset} total ${C.dim}(${userMsgs} user, ${assistantMsgs} assistant)${C.reset}`,
792
- `${C.gold}Total Queries:${C.reset} ${C.cyberCyan}${sess.stats.totalQueries}${C.reset}`,
793
- `${C.gold}Tool Calls:${C.reset} ${C.cyberCyan}${toolCalls}${C.reset}`,
794
- `${C.gold}Total Duration:${C.reset} ${C.cyberCyan}${totalDur}s${C.reset}`,
795
- `${C.gold}Thinking Mode:${C.reset} ${C.bold}${C.gold}${sess.thinkingMode.toUpperCase()}${C.reset}`
796
- ],
813
+ title: `Active Session Details • ${sess.id}`,
814
+ lines,
797
815
  headerBg: '\x1b[48;2;48;54;70m',
798
816
  bg: '\x1b[48;2;36;40;52m'
799
817
  });
@@ -839,10 +857,10 @@ export const handlers = {
839
857
  title: `Archived Sessions (Total: ${sessions.length})`,
840
858
  lines: sessions.length === 0
841
859
  ? [`${C.dim}No archived sessions found.${C.reset}`]
842
- : sessions.map(s => {
860
+ : sessions.map((s, i) => {
843
861
  const date = new Date(s.createdAt).toLocaleString();
844
862
  const dur = ((s.stats?.totalDurationMs || 0) / 1000).toFixed(1);
845
- return `${C.gold}${s.id}${C.reset} ${C.dim}(${date})${C.reset} — ${s.messageCount} msgs, ${dur}s`;
863
+ return `${C.gold}[${i + 1}] ${s.id}${C.reset} ${C.dim}(${date})${C.reset} — ${s.messageCount} msgs, ${dur}s`;
846
864
  }),
847
865
  headerBg: '\x1b[48;2;48;54;70m',
848
866
  bg: '\x1b[48;2;36;40;52m'
@@ -850,20 +868,98 @@ export const handlers = {
850
868
  return 'HANDLED';
851
869
  }
852
870
 
871
+ if (action === 'load' || action === 'resume' || action === 'restore') {
872
+ const target = (extra || '').trim();
873
+ if (!target) {
874
+ const sessions = agent.memory.getArchivedSessions(10);
875
+ if (sessions.length === 0) {
876
+ console.log(`\n ${C.amber}No archived sessions available to load.${C.reset}\n`);
877
+ return 'HANDLED';
878
+ }
879
+ printSolidPanel({
880
+ title: 'Load Session Usage',
881
+ lines: [
882
+ `${C.dim}Specify a session ID or list number to resume:${C.reset}`,
883
+ '',
884
+ ...sessions.map((s, i) => {
885
+ const date = new Date(s.createdAt).toLocaleString();
886
+ return ` ${C.gold}[${i + 1}]${C.reset} ${C.bold}${C.white}${s.id}${C.reset} ${C.dim}(${date} — ${s.messageCount} msgs)${C.reset}`;
887
+ }),
888
+ '',
889
+ `${C.dim}Example: /session load 1 OR /session load ${sessions[0].id}${C.reset}`,
890
+ ],
891
+ headerBg: '\x1b[48;2;48;54;70m',
892
+ bg: '\x1b[48;2;36;40;52m',
893
+ });
894
+ return 'HANDLED';
895
+ }
896
+
897
+ const restored = await agent.restoreSession(target);
898
+ if (!restored) {
899
+ console.log(`\n ${C.amber}Session "${target}" not found. Use /session list to view available archives.${C.reset}\n`);
900
+ return 'HANDLED';
901
+ }
902
+
903
+ const date = restored.createdAt ? new Date(restored.createdAt).toLocaleString() : 'unknown';
904
+ const msgCount = restored.messages?.length || 0;
905
+ const totalDur = (((restored.stats?.totalDurationMs) || 0) / 1000).toFixed(1);
906
+
907
+ printSolidPanel({
908
+ title: 'Session Restored Successfully',
909
+ lines: [
910
+ `${C.gold}Session ID:${C.reset} ${C.bold}${C.white}${restored.id}${C.reset}`,
911
+ `${C.gold}Created At:${C.reset} ${C.white}${date}${C.reset}`,
912
+ `${C.gold}Restored Messages:${C.reset} ${C.cyberCyan}${msgCount}${C.reset} exchanges loaded into memory`,
913
+ `${C.gold}Total Queries:${C.reset} ${C.cyberCyan}${restored.stats?.totalQueries || 0}${C.reset}`,
914
+ `${C.gold}Tool Invocations:${C.reset} ${C.cyberCyan}${restored.stats?.totalToolCalls || 0}${C.reset}`,
915
+ `${C.gold}Total Duration:${C.reset} ${C.cyberCyan}${totalDur}s${C.reset}`,
916
+ '',
917
+ `${C.emerald}✓ Session conversation context reloaded. You can now continue chatting!${C.reset}`,
918
+ ],
919
+ headerBg: '\x1b[48;2;48;54;70m',
920
+ bg: '\x1b[48;2;36;40;52m',
921
+ });
922
+ return 'HANDLED';
923
+ }
924
+
853
925
  if (action === 'export') {
854
- const exported = await agent.session.exportToMarkdown();
855
- console.log(`\n ${C.emerald}[OK] Session transcript exported.${C.reset}`);
856
- console.log(` ${C.slate}• File:${C.reset} ${C.white}${exported.filePath}${C.reset}\n`);
926
+ const format = (extra || '').toLowerCase().trim();
927
+ let exported;
928
+ if (format === 'json') {
929
+ exported = await agent.session.exportToJson();
930
+ } else if (format === 'text' || format === 'txt') {
931
+ exported = await agent.session.exportToText();
932
+ } else {
933
+ exported = await agent.session.exportToMarkdown();
934
+ }
935
+
936
+ printRichCard({
937
+ title: 'Session Exported Successfully',
938
+ status: 'ok',
939
+ lines: [
940
+ `• Format: ${exported.format.toUpperCase()}`,
941
+ `• File: ${exported.filePath}`,
942
+ `• Size: ${(exported.size || 0).toLocaleString()} bytes`,
943
+ ],
944
+ });
857
945
  return 'HANDLED';
858
946
  }
859
947
 
948
+ if (action === 'copy') {
949
+ if (typeof ctx.copyQueryDetails === 'function') {
950
+ ctx.copyQueryDetails(`session ${extra || ''}`.trim());
951
+ return 'HANDLED';
952
+ }
953
+ return handlers['/copy']({ ...ctx, sub: 'session', extra });
954
+ }
955
+
860
956
  if (action === 'clear' || action === 'new' || action === 'reset') {
861
957
  await agent.endSession();
862
958
  console.log(`\n ${C.emerald}[OK] Previous session archived. Started fresh.${C.reset}\n`);
863
959
  return 'HANDLED';
864
960
  }
865
961
 
866
- console.log(`\n ${C.amber}Unknown session action: "${sub}". Use /session [info|list|export|clear]${C.reset}\n`);
962
+ console.log(`\n ${C.amber}Unknown session action: "${sub}". Use /session [info|summary|list|load|export|copy|clear]${C.reset}\n`);
867
963
  return 'HANDLED';
868
964
  },
869
965
 
@@ -930,14 +1026,28 @@ export const handlers = {
930
1026
  return 'HANDLED';
931
1027
  }
932
1028
 
933
- const requested = action === 'reset' ? 'sakura' : action;
1029
+ let requested = action === 'reset' ? 'sakura' : action;
1030
+ if (action === 'next' || action === 'cycle') {
1031
+ const currentIndex = THEME_NAMES.indexOf(current);
1032
+ const nextIndex = (currentIndex + 1) % THEME_NAMES.length;
1033
+ requested = THEME_NAMES[nextIndex];
1034
+ } else if (action === 'prev') {
1035
+ const currentIndex = THEME_NAMES.indexOf(current);
1036
+ const prevIndex = (currentIndex - 1 + THEME_NAMES.length) % THEME_NAMES.length;
1037
+ requested = THEME_NAMES[prevIndex];
1038
+ }
1039
+
934
1040
  const theme = setTheme(requested);
935
1041
  if (!theme) {
936
1042
  console.log(`\n ${C.amber}Unknown theme: "${action}".${C.reset}`);
937
- console.log(` ${C.dim}Available: ${THEME_NAMES.join(', ')}${C.reset}\n`);
1043
+ console.log(` ${C.dim}Available: ${THEME_NAMES.join(', ')}, next, prev, reset${C.reset}\n`);
938
1044
  return 'HANDLED';
939
1045
  }
940
1046
 
1047
+ if (ctx.tuiPrompt) {
1048
+ ctx.tuiPrompt.render();
1049
+ }
1050
+
941
1051
  printSolidPanel({
942
1052
  title: 'Theme Updated',
943
1053
  lines: [
@@ -1168,7 +1278,16 @@ export const handlers = {
1168
1278
 
1169
1279
  '/stop': async () => { await audioPlayer.stop(); console.log(`\n ${C.emerald}[OK] Stopped${C.reset}\n`); return 'HANDLED'; },
1170
1280
  '/pause': async () => { const r = await audioPlayer.pause(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
1171
- '/resume': async () => { const r = await audioPlayer.resume(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
1281
+ '/resume': async (ctx) => {
1282
+ const { sub, extra } = ctx || {};
1283
+ if (sub) {
1284
+ const target = extra ? `${sub} ${extra}`.trim() : sub;
1285
+ return handlers['/session']({ ...ctx, sub: 'load', extra: target });
1286
+ }
1287
+ const r = await audioPlayer.resume();
1288
+ console.log(`\n ${C.emerald}${r}${C.reset}\n`);
1289
+ return 'HANDLED';
1290
+ },
1172
1291
  '/next': async () => { const r = await audioPlayer.next(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
1173
1292
  '/prev': async () => { const r = await audioPlayer.prev(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
1174
1293
  '/mute': async () => { const r = await audioPlayer.mute(); console.log(`\n ${C.emerald}${r}${C.reset}\n`); return 'HANDLED'; },
@@ -1302,9 +1421,24 @@ export const handlers = {
1302
1421
  ctx.modelManager.client = ctx.client;
1303
1422
  ctx.modelManager.requestedModel = info.model;
1304
1423
  const resolution = await ctx.modelManager.resolve({ requestedModel: info.model });
1305
- if (resolution.connected && resolution.usedFallback) {
1306
- console.log(` ${C.amber}• Requested model unavailable; using ${resolution.selectedModel}${C.reset}`);
1424
+ if (resolution.connected && resolution.selectedModel) {
1425
+ ctx.setCatalogOnlyMode?.(false);
1426
+ if (ctx.tuiPrompt) {
1427
+ ctx.tuiPrompt.isConnected = true;
1428
+ ctx.tuiPrompt.modelName = resolution.selectedModel;
1429
+ ctx.tuiPrompt.provider = info.providerName;
1430
+ ctx.tuiPrompt.render();
1431
+ }
1432
+ if (resolution.usedFallback) {
1433
+ console.log(` ${C.amber}• Requested model unavailable; using ${resolution.selectedModel}${C.reset}`);
1434
+ }
1307
1435
  } else if (!resolution.connected) {
1436
+ ctx.setCatalogOnlyMode?.(true);
1437
+ if (ctx.tuiPrompt) {
1438
+ ctx.tuiPrompt.isConnected = false;
1439
+ ctx.tuiPrompt.provider = info.providerName;
1440
+ ctx.tuiPrompt.render();
1441
+ }
1308
1442
  console.log(` ${C.amber}• Provider is not ready: ${resolution.error}${C.reset}`);
1309
1443
  }
1310
1444
  }
@@ -1428,9 +1562,11 @@ export const handlers = {
1428
1562
  return 'HANDLED';
1429
1563
  }
1430
1564
 
1431
- if (targetProvider === ctx.client.provider) {
1432
- ctx.client.setApiKey(newKey);
1433
- if (ctx.agent?.client) ctx.agent.client.setApiKey(newKey);
1565
+ if (ctx.client?.setApiKey) {
1566
+ ctx.client.setApiKey(newKey, targetProvider);
1567
+ }
1568
+ if (ctx.agent?.client?.setApiKey) {
1569
+ ctx.agent.client.setApiKey(newKey, targetProvider);
1434
1570
  }
1435
1571
  const pDef = getProvider(targetProvider);
1436
1572
  if (pDef) pDef.apiKey = newKey;
@@ -1438,7 +1574,7 @@ export const handlers = {
1438
1574
  const masked = newKey.length > 8
1439
1575
  ? `${newKey.slice(0, 4)}...${newKey.slice(-4)}`
1440
1576
  : '****';
1441
- console.log(`\n ${C.emerald}[OK] API key updated for ${targetProvider}${C.reset} (${masked})\n`);
1577
+ console.log(`\n ${C.emerald}[OK] API key saved & persisted for ${targetProvider}${C.reset} (${masked})\n`);
1442
1578
  return 'HANDLED';
1443
1579
  }
1444
1580
 
@@ -1468,9 +1604,10 @@ export const handlers = {
1468
1604
  },
1469
1605
 
1470
1606
  '/model': async (ctx) => {
1471
- const action = (ctx.sub || 'status').toLowerCase();
1607
+ const rawSub = (ctx.sub || '').trim();
1608
+ const action = (rawSub || 'status').toLowerCase();
1472
1609
  const providerName = ctx.client.providerName || 'Model';
1473
- const { getRecommendedModels } = await import('./config.js');
1610
+ const { getRecommendedModels, CLOUD_PROVIDERS } = await import('./config.js');
1474
1611
 
1475
1612
  if (action === 'reconnect') {
1476
1613
  if (!ctx.modelManager) {
@@ -1480,8 +1617,18 @@ export const handlers = {
1480
1617
  const resolution = await ctx.modelManager.resolve();
1481
1618
  if (resolution.connected && resolution.selectedModel) {
1482
1619
  ctx.setCatalogOnlyMode?.(false);
1620
+ if (ctx.tuiPrompt) {
1621
+ ctx.tuiPrompt.isConnected = true;
1622
+ ctx.tuiPrompt.modelName = resolution.selectedModel;
1623
+ ctx.tuiPrompt.render();
1624
+ }
1483
1625
  console.log(`\n ${C.emerald}[OK] Connected using ${resolution.selectedModel}.${C.reset}\n`);
1484
1626
  } else {
1627
+ ctx.setCatalogOnlyMode?.(true);
1628
+ if (ctx.tuiPrompt) {
1629
+ ctx.tuiPrompt.isConnected = false;
1630
+ ctx.tuiPrompt.render();
1631
+ }
1485
1632
  console.log(`\n ${C.red}[MODEL ERROR] ${resolution.error || 'No chat model is ready.'}${C.reset}\n`);
1486
1633
  }
1487
1634
  return 'HANDLED';
@@ -1523,19 +1670,42 @@ export const handlers = {
1523
1670
  `${C.bold}${C.lavender}Reported by Server:${C.reset}`,
1524
1671
  ...(serverModels.length ? serverModels.map(s => ` ${s}`) : [` ${C.dim}No live model list reported by ${providerName}.${C.reset}`]),
1525
1672
  '',
1526
- `${C.dim}Select model: /model use <model_id>${C.reset}`
1673
+ `${C.dim}Select model: /model use <model_id> or /model <provider_or_model>${C.reset}`
1527
1674
  ],
1528
1675
  });
1529
1676
  return 'HANDLED';
1530
1677
  }
1531
1678
 
1532
- if (action === 'use') {
1679
+ const isDirectUse = action === 'use';
1680
+ const targetModel = isDirectUse ? (ctx.extra?.trim() || '') : (!['status', 'diag', 'diagnostics'].includes(action) ? rawSub : '');
1681
+
1682
+ if (targetModel) {
1533
1683
  try {
1534
- const targetModel = ctx.extra?.trim() || '';
1535
- if (!targetModel) {
1536
- console.log(`\n ${C.amber}Usage: /model use <model_id>${C.reset}\n`);
1537
- return 'HANDLED';
1684
+ const lowerTarget = targetModel.toLowerCase();
1685
+
1686
+
1687
+ if (CLOUD_PROVIDERS[lowerTarget]) {
1688
+ return await handlers['/provider']({ ...ctx, sub: 'use', extra: lowerTarget, parts: ['/provider', 'use', lowerTarget] });
1538
1689
  }
1690
+
1691
+
1692
+ let matchedProvider = null;
1693
+ if (lowerTarget.includes('gemma') || lowerTarget === 'google/gemma-4-e4b') {
1694
+ matchedProvider = 'lmstudio';
1695
+ } else {
1696
+ for (const [pId, pDef] of Object.entries(CLOUD_PROVIDERS)) {
1697
+ if (pDef.recommendedModels?.some(rm => rm.id.toLowerCase() === lowerTarget || rm.id.toLowerCase().includes(lowerTarget))) {
1698
+ matchedProvider = pId;
1699
+ break;
1700
+ }
1701
+ }
1702
+ }
1703
+
1704
+ if (matchedProvider && matchedProvider !== ctx.client.provider) {
1705
+ console.log(` ${C.cyberCyan}• Switching provider to ${CLOUD_PROVIDERS[matchedProvider].name} for model ${targetModel}...${C.reset}`);
1706
+ return await handlers['/provider']({ ...ctx, sub: 'use', extra: `${matchedProvider} ${targetModel}`, parts: ['/provider', 'use', matchedProvider, targetModel] });
1707
+ }
1708
+
1539
1709
  if (ctx.modelManager) {
1540
1710
  const selected = ctx.modelManager.select(targetModel, liveCheck.models || []);
1541
1711
  console.log(`\n ${C.emerald}[OK] Active model changed to ${selected}.${C.reset}\n`);
@@ -2282,20 +2452,12 @@ export const handlers = {
2282
2452
  const reference = [ctx.sub, ctx.extra].filter(Boolean).join(' ');
2283
2453
  const request = parseDetailsRequest(reference);
2284
2454
  const details = ctx.agent?.lastQueryDetails || null;
2285
- if (!details) {
2286
- printRichCard({
2287
- title: 'No Diagnostics Recorded',
2288
- status: 'warning',
2289
- lines: ['No completed model queries are available to copy.'],
2290
- });
2291
- return 'HANDLED';
2292
- }
2293
- const copyable = getCopyableContent(details, request);
2455
+ const copyable = getCopyableContent(details, request, ctx.agent?.session);
2294
2456
  if (!copyable?.text) {
2295
2457
  printRichCard({
2296
2458
  title: 'Nothing to Copy',
2297
2459
  status: 'warning',
2298
- lines: ['The requested query section is empty.'],
2460
+ lines: ['The requested query or session section is empty.'],
2299
2461
  });
2300
2462
  return 'HANDLED';
2301
2463
  }
@@ -2318,7 +2480,8 @@ export const handlers = {
2318
2480
  },
2319
2481
 
2320
2482
  '/gallery': async (ctx) => {
2321
- const displayed = typeof ctx.showTerminalImage === 'function' && ctx.showTerminalImage();
2483
+ const count = Number(ctx.sub) || 2;
2484
+ const displayed = typeof ctx.showTerminalImage === 'function' && ctx.showTerminalImage(count);
2322
2485
  if (!displayed) {
2323
2486
  printRichCard({
2324
2487
  title: 'Terminal Gallery Unavailable',
@@ -2571,6 +2734,11 @@ export const handlers = {
2571
2734
  }
2572
2735
  return 'HANDLED';
2573
2736
  },
2737
+
2738
+ '/exit': async (ctx) => {
2739
+ try { await ctx.agent?.endSession(); } catch {}
2740
+ return 'EXIT';
2741
+ },
2574
2742
  };
2575
2743
 
2576
2744
  const ALIASES = {
@@ -2580,6 +2748,7 @@ const ALIASES = {
2580
2748
  '/?': '/help', '/commands': '/help', '/menu': '/help',
2581
2749
  '/hist': '/history', '/h': '/history',
2582
2750
  '/sess': '/session', '/sessions': '/session',
2751
+ '/load-session': '/session', '/load': '/session', '/restore': '/session',
2583
2752
  '/think': '/thinking', '/th': '/thinking',
2584
2753
  '/detail': '/details', '/d': '/details',
2585
2754
  '/language': '/lang',
@@ -2608,6 +2777,9 @@ const ALIASES = {
2608
2777
  '/export': '/save',
2609
2778
  '/r': '/retry',
2610
2779
  '/kvip': '/key',
2780
+ '/switch': '/provider',
2781
+ '/providers': '/provider',
2782
+ '/fallback': '/provider',
2611
2783
  };
2612
2784
 
2613
2785
  export async function routeCommand(rawInput, ctx) {