kiku-agent 1.0.7 → 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.
- package/LICENSE +621 -0
- package/README.md +296 -14
- package/package.json +92 -89
- package/server/agent/agentCore.js +58 -31
- package/server/agent/cli.js +79 -43
- package/server/agent/commandFeatures.js +517 -0
- package/server/agent/commandInput.js +113 -0
- package/server/agent/commandRegistry.js +122 -8
- package/server/agent/commands.js +387 -20
- package/server/agent/jobManager.js +96 -0
- package/server/agent/mcpClient.js +68 -11
- package/server/agent/memory.js +87 -14
- package/server/agent/quizData.js +384 -0
- package/server/agent/terminalImage.js +248 -0
- package/server/agent/tools.js +153 -43
- package/server/agent/ui.js +67 -21
- package/src/assets/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/src/assets/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
package/server/agent/tools.js
CHANGED
|
@@ -30,9 +30,16 @@ export function buildQqMusicSearchUrl(query) {
|
|
|
30
30
|
return `https://y.qq.com/n/ryqq/search?w=${encodeURIComponent(query)}`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function buildYoukuSearchUrl(query) {
|
|
34
|
-
return `https://so.youku.com/search_video/q_${encodeURIComponent(query)}`;
|
|
35
|
-
}
|
|
33
|
+
export function buildYoukuSearchUrl(query) {
|
|
34
|
+
return `https://so.youku.com/search_video/q_${encodeURIComponent(query)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function formatCatalogTitle(primary, secondary) {
|
|
38
|
+
const first = String(primary || secondary || '').trim();
|
|
39
|
+
const second = String(secondary || '').trim();
|
|
40
|
+
if (!second || first.toLocaleLowerCase().includes(second.toLocaleLowerCase())) return first;
|
|
41
|
+
return `${first} (${second})`;
|
|
42
|
+
}
|
|
36
43
|
|
|
37
44
|
export class ToolCache {
|
|
38
45
|
constructor(maxSize = 300, defaultTtlMs = 600_000) {
|
|
@@ -302,9 +309,11 @@ export const toolsSchema = [
|
|
|
302
309
|
description: "Query awards and honors received by Ju Jingyi across music, television, and cultural influence.",
|
|
303
310
|
parameters: {
|
|
304
311
|
type: "object",
|
|
305
|
-
properties: {
|
|
306
|
-
category: { type: "string", description: "Category (Acting, Music, Honor)" },
|
|
307
|
-
year: { type: "number", description: "Year filter" }
|
|
312
|
+
properties: {
|
|
313
|
+
category: { type: "string", description: "Category (Acting, Music, Honor)" },
|
|
314
|
+
year: { type: "number", description: "Year filter" },
|
|
315
|
+
query: { type: "string", description: "Award title, ceremony, or category keyword" },
|
|
316
|
+
limit: { type: "number", description: "Maximum results from 1 to 20" }
|
|
308
317
|
},
|
|
309
318
|
required: []
|
|
310
319
|
}
|
|
@@ -332,9 +341,11 @@ export const toolsSchema = [
|
|
|
332
341
|
description: "Search visual photo gallery of Ju Jingyi (red carpet, photoshoot, drama stills).",
|
|
333
342
|
parameters: {
|
|
334
343
|
type: "object",
|
|
335
|
-
properties: {
|
|
336
|
-
category: { type: "string", description: "Category (fashion-magazine, red-carpet, drama-stills)" },
|
|
337
|
-
|
|
344
|
+
properties: {
|
|
345
|
+
category: { type: "string", description: "Category (fashion-magazine, red-carpet, drama-stills)" },
|
|
346
|
+
query: { type: "string", description: "Photo title or category keyword" },
|
|
347
|
+
year: { type: "number", description: "Year filter" },
|
|
348
|
+
limit: { type: "number", description: "Number of photos from 1 to 20" }
|
|
338
349
|
},
|
|
339
350
|
required: []
|
|
340
351
|
}
|
|
@@ -726,6 +737,30 @@ export const toolsSchema = [
|
|
|
726
737
|
required: ["action"]
|
|
727
738
|
}
|
|
728
739
|
}
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
type: "function",
|
|
743
|
+
function: {
|
|
744
|
+
name: "run_quiz",
|
|
745
|
+
description: "Fetch the Ju Jingyi quiz challenge session and return all 15 questions with shuffled options for the user to answer interactively via /quiz command.",
|
|
746
|
+
parameters: {
|
|
747
|
+
type: "object",
|
|
748
|
+
properties: {
|
|
749
|
+
lang: { type: "string", enum: ["th", "en", "zh"], description: "Language for quiz questions (default: auto-detect from user preference)." }
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
type: "function",
|
|
756
|
+
function: {
|
|
757
|
+
name: "get_api_key",
|
|
758
|
+
description: "Retrieve the stored Developer VIP API key for authenticated requests to the Ju Jingyi API.",
|
|
759
|
+
parameters: {
|
|
760
|
+
type: "object",
|
|
761
|
+
properties: {}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
729
764
|
}
|
|
730
765
|
];
|
|
731
766
|
|
|
@@ -829,7 +864,7 @@ export const toolHandlers = {
|
|
|
829
864
|
youtubeTrailerSearch: buildYouTubeSearchUrl(`${titleSearch} Trailer Official`),
|
|
830
865
|
youkuSearchUrl: buildYoukuSearchUrl(w.titleCn || w.titleEn),
|
|
831
866
|
themeSong: w.themeSong ? {
|
|
832
|
-
title:
|
|
867
|
+
title: formatCatalogTitle(w.themeSong.titleTh, w.themeSong.titleCn),
|
|
833
868
|
youtubeUrl: themeSongYtId ? buildYouTubeWatchUrl(themeSongYtId) : null,
|
|
834
869
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.themeSong.titleCn || w.themeSong.titleTh} OST`)
|
|
835
870
|
} : null
|
|
@@ -875,7 +910,7 @@ export const toolHandlers = {
|
|
|
875
910
|
synopsis: work.synopsis?.slice(0, 250) + (work.synopsis?.length > 250 ? '...' : ''),
|
|
876
911
|
highlights: (work.highlights || []).slice(0, 3),
|
|
877
912
|
themeSong: work.themeSong ? {
|
|
878
|
-
title:
|
|
913
|
+
title: formatCatalogTitle(work.themeSong.titleTh, work.themeSong.titleCn),
|
|
879
914
|
youtubeUrl: themeSongYtId ? buildYouTubeWatchUrl(themeSongYtId) : null,
|
|
880
915
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${work.themeSong.titleCn || work.themeSong.titleTh} OST`)
|
|
881
916
|
} : null
|
|
@@ -885,7 +920,7 @@ export const toolHandlers = {
|
|
|
885
920
|
|
|
886
921
|
get_character_roles: async ({ query } = {}) => {
|
|
887
922
|
const roles = worksData.map(w => ({
|
|
888
|
-
workTitle:
|
|
923
|
+
workTitle: formatCatalogTitle(w.titleTh, w.titleCn),
|
|
889
924
|
year: w.year,
|
|
890
925
|
role: w.role,
|
|
891
926
|
roleCn: w.roleCn,
|
|
@@ -940,7 +975,7 @@ export const toolHandlers = {
|
|
|
940
975
|
count: list.length,
|
|
941
976
|
songs: list.slice(0, 8).map(s => ({
|
|
942
977
|
id: s.id,
|
|
943
|
-
title:
|
|
978
|
+
title: formatCatalogTitle(s.titleTh, s.titleCn),
|
|
944
979
|
titleEn: s.titleEn,
|
|
945
980
|
year: s.year,
|
|
946
981
|
type: s.work,
|
|
@@ -991,7 +1026,7 @@ export const toolHandlers = {
|
|
|
991
1026
|
const searchTitle = `Ju Jingyi 鞠婧祎 ${m.titleCn || m.titleTh} ${m.category === 'Stage-Performance' ? 'Stage Live' : 'Official MV'}`;
|
|
992
1027
|
return {
|
|
993
1028
|
id: m.id,
|
|
994
|
-
title:
|
|
1029
|
+
title: formatCatalogTitle(m.titleTh, m.titleCn),
|
|
995
1030
|
category: m.category,
|
|
996
1031
|
year: m.releaseYear || m.year,
|
|
997
1032
|
drama: m.dramaName || null,
|
|
@@ -1021,7 +1056,7 @@ export const toolHandlers = {
|
|
|
1021
1056
|
(m.dramaName && m.dramaName.toLowerCase().includes(q))
|
|
1022
1057
|
).map(m => ({
|
|
1023
1058
|
type: m.category || 'MV',
|
|
1024
|
-
title:
|
|
1059
|
+
title: formatCatalogTitle(m.titleTh, m.titleCn),
|
|
1025
1060
|
year: m.releaseYear || m.year,
|
|
1026
1061
|
duration: m.duration,
|
|
1027
1062
|
youtubeUrl: m.youtubeId ? buildYouTubeWatchUrl(m.youtubeId) : null,
|
|
@@ -1036,7 +1071,7 @@ export const toolHandlers = {
|
|
|
1036
1071
|
(s.work && s.work.toLowerCase().includes(q))
|
|
1037
1072
|
).map(s => ({
|
|
1038
1073
|
type: s.work || 'Song',
|
|
1039
|
-
title:
|
|
1074
|
+
title: formatCatalogTitle(s.titleTh, s.titleCn),
|
|
1040
1075
|
year: s.year,
|
|
1041
1076
|
duration: s.duration,
|
|
1042
1077
|
youtubeUrl: s.youtubeUrl || (s.youtubeId ? buildYouTubeWatchUrl(s.youtubeId) : null),
|
|
@@ -1051,14 +1086,14 @@ export const toolHandlers = {
|
|
|
1051
1086
|
(w.titleEn && w.titleEn.toLowerCase().includes(q))
|
|
1052
1087
|
).map(w => ({
|
|
1053
1088
|
type: 'Drama / Series',
|
|
1054
|
-
title:
|
|
1089
|
+
title: formatCatalogTitle(w.titleTh, w.titleCn),
|
|
1055
1090
|
year: w.year,
|
|
1056
1091
|
platform: w.streamPlatform,
|
|
1057
1092
|
streamUrl: w.streamUrl || null,
|
|
1058
1093
|
youtubeTrailerSearch: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.titleCn || w.titleEn} Trailer Official`),
|
|
1059
1094
|
youkuSearchUrl: buildYoukuSearchUrl(w.titleCn || w.titleEn),
|
|
1060
1095
|
themeSong: w.themeSong ? {
|
|
1061
|
-
title:
|
|
1096
|
+
title: formatCatalogTitle(w.themeSong.titleTh, w.themeSong.titleCn),
|
|
1062
1097
|
youtubeUrl: w.themeSong.youtubeId ? buildYouTubeWatchUrl(w.themeSong.youtubeId) : null,
|
|
1063
1098
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${w.themeSong.titleCn || w.themeSong.titleTh} OST`)
|
|
1064
1099
|
} : null
|
|
@@ -1135,26 +1170,32 @@ export const toolHandlers = {
|
|
|
1135
1170
|
count: eras.length,
|
|
1136
1171
|
milestones: eras.slice(0, 6).map(e => ({
|
|
1137
1172
|
period: e.period || e.year,
|
|
1138
|
-
title:
|
|
1173
|
+
title: formatCatalogTitle(e.title, e.titleCn),
|
|
1139
1174
|
summary: e.summary?.slice(0, 150),
|
|
1140
1175
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${e.titleCn || e.title}`)
|
|
1141
1176
|
}))
|
|
1142
1177
|
};
|
|
1143
1178
|
},
|
|
1144
1179
|
|
|
1145
|
-
get_awards: async ({ category, year } = {}) => {
|
|
1146
|
-
let list = Array.isArray(awardsData) ? awardsData : (awardsData.awards || []);
|
|
1147
|
-
if (year) list = list.filter(a => a.year === Number(year));
|
|
1148
|
-
if (category && category !== 'all') list = list.filter(a => a.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1180
|
+
get_awards: async ({ category, year, query, limit = 8 } = {}) => {
|
|
1181
|
+
let list = Array.isArray(awardsData) ? awardsData : (awardsData.awards || []);
|
|
1182
|
+
if (year) list = list.filter(a => a.year === Number(year));
|
|
1183
|
+
if (category && category !== 'all') list = list.filter(a => a.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1184
|
+
if (query) {
|
|
1185
|
+
const q = String(query).toLowerCase().trim();
|
|
1186
|
+
list = list.filter(a => [a.titleTh, a.titleCn, a.titleEn, a.ceremony, a.category].filter(Boolean).join(' ').toLowerCase().includes(q));
|
|
1187
|
+
}
|
|
1188
|
+
const safeLimit = Math.min(Math.max(Number(limit) || 8, 1), 20);
|
|
1189
|
+
|
|
1190
|
+
return {
|
|
1191
|
+
status: "success",
|
|
1192
|
+
count: list.length,
|
|
1193
|
+
awards: list.slice(0, safeLimit).map((a, index) => ({
|
|
1194
|
+
id: a.id || `award-${a.year || 'unknown'}-${index + 1}`,
|
|
1195
|
+
year: a.year,
|
|
1196
|
+
title: `${a.titleTh || a.titleEn || a.titleCn || 'Award'}${a.titleCn && a.titleCn !== a.titleTh ? ` (${a.titleCn})` : ''}`,
|
|
1197
|
+
ceremony: a.ceremony,
|
|
1198
|
+
category: a.category,
|
|
1158
1199
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${a.titleCn || a.ceremony}`)
|
|
1159
1200
|
}))
|
|
1160
1201
|
};
|
|
@@ -1176,15 +1217,22 @@ export const toolHandlers = {
|
|
|
1176
1217
|
};
|
|
1177
1218
|
},
|
|
1178
1219
|
|
|
1179
|
-
get_gallery_photos: async ({ category, limit = 4 } = {}) => {
|
|
1180
|
-
let photos = Array.isArray(galleryData) ? galleryData : (galleryData.photos || []);
|
|
1181
|
-
if (category && category !== 'all') photos = photos.filter(p => p.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1220
|
+
get_gallery_photos: async ({ category, query, year, limit = 4 } = {}) => {
|
|
1221
|
+
let photos = Array.isArray(galleryData) ? galleryData : (galleryData.photos || []);
|
|
1222
|
+
if (category && category !== 'all') photos = photos.filter(p => p.category?.toLowerCase().includes(category.toLowerCase()));
|
|
1223
|
+
if (query) {
|
|
1224
|
+
const q = String(query).toLowerCase().trim();
|
|
1225
|
+
photos = photos.filter(p => [p.titleTh, p.titleCn, p.titleEn, p.category, p.description].filter(Boolean).join(' ').toLowerCase().includes(q));
|
|
1226
|
+
}
|
|
1227
|
+
if (year) photos = photos.filter(p => Number(p.era || p.year) === Number(year));
|
|
1228
|
+
const safeLimit = Math.min(Math.max(Number(limit) || 4, 1), 20);
|
|
1229
|
+
|
|
1230
|
+
return {
|
|
1231
|
+
status: "success",
|
|
1232
|
+
count: Math.min(photos.length, safeLimit),
|
|
1233
|
+
photos: photos.slice(0, safeLimit).map((p, index) => ({
|
|
1234
|
+
id: p.id || `photo-${p.era || p.year || 'unknown'}-${index + 1}`,
|
|
1235
|
+
title: p.titleTh || p.titleCn,
|
|
1188
1236
|
category: p.category,
|
|
1189
1237
|
year: p.era || p.year
|
|
1190
1238
|
}))
|
|
@@ -1220,7 +1268,7 @@ export const toolHandlers = {
|
|
|
1220
1268
|
const mvs = getAllMusicVideos().filter(m =>
|
|
1221
1269
|
matchesQuery(m.titleTh, m.titleCn, m.titlePinyin, m.dramaName, m.description)
|
|
1222
1270
|
).slice(0, 4).map(m => ({
|
|
1223
|
-
title: `${m.titleTh
|
|
1271
|
+
title: `${formatCatalogTitle(m.titleTh, m.titleCn)} [${m.category}]`,
|
|
1224
1272
|
youtubeUrl: m.youtubeId ? buildYouTubeWatchUrl(m.youtubeId) : null,
|
|
1225
1273
|
youtubeSearchUrl: buildYouTubeSearchUrl(`Ju Jingyi 鞠婧祎 ${m.titleCn || m.titleTh}`)
|
|
1226
1274
|
}));
|
|
@@ -1616,7 +1664,69 @@ export const toolHandlers = {
|
|
|
1616
1664
|
};
|
|
1617
1665
|
}
|
|
1618
1666
|
}
|
|
1619
|
-
}
|
|
1667
|
+
},
|
|
1668
|
+
|
|
1669
|
+
run_quiz: async ({ lang } = {}, options = {}) => {
|
|
1670
|
+
let memory = options?.agent?.memory || options?.memory;
|
|
1671
|
+
if (!memory) {
|
|
1672
|
+
const { AgentMemory } = await import('./memory.js');
|
|
1673
|
+
memory = new AgentMemory();
|
|
1674
|
+
await memory.load();
|
|
1675
|
+
}
|
|
1676
|
+
const quizLang = ['th', 'en', 'zh'].includes(lang) ? lang : (memory.preferences?.language || 'en');
|
|
1677
|
+
const { shuffleQuizQuestions } = await import('./quizData.js');
|
|
1678
|
+
const { QUIZ_QUESTIONS, QUIZ_I18N, LETTER_LABELS } = await import('./quizData.js');
|
|
1679
|
+
const i18n = QUIZ_I18N[quizLang] || QUIZ_I18N.en;
|
|
1680
|
+
const shuffled = shuffleQuizQuestions(QUIZ_QUESTIONS);
|
|
1681
|
+
|
|
1682
|
+
let challengeToken = null;
|
|
1683
|
+
try {
|
|
1684
|
+
const baseUrl = options?.baseUrl || 'http://localhost:3000';
|
|
1685
|
+
const res = await fetch(`${baseUrl}/api/v1/quiz/challenge`);
|
|
1686
|
+
if (res.ok) {
|
|
1687
|
+
const json = await res.json();
|
|
1688
|
+
challengeToken = json.data?.challengeToken;
|
|
1689
|
+
}
|
|
1690
|
+
} catch {}
|
|
1691
|
+
|
|
1692
|
+
return {
|
|
1693
|
+
status: 'success',
|
|
1694
|
+
message: i18n.subtitle,
|
|
1695
|
+
lang: quizLang,
|
|
1696
|
+
challengeToken,
|
|
1697
|
+
instructions: 'Present each question one by one. User answers with A/B/C/D or 1-4. After all 15, use POST /api/v1/keys/claim with challengeToken and answers array [{qIndex, answerIndex}].',
|
|
1698
|
+
questions: shuffled.map((q, i) => ({
|
|
1699
|
+
index: i,
|
|
1700
|
+
question: q.q[quizLang] || q.q.en,
|
|
1701
|
+
options: (q.options[quizLang] || q.options.en).map((opt, idx) => `${LETTER_LABELS[idx]}) ${opt}`),
|
|
1702
|
+
hint: q.hint[quizLang] || q.hint.en,
|
|
1703
|
+
}))
|
|
1704
|
+
};
|
|
1705
|
+
},
|
|
1706
|
+
|
|
1707
|
+
get_api_key: async (_args = {}, options = {}) => {
|
|
1708
|
+
let memory = options?.agent?.memory || options?.memory;
|
|
1709
|
+
if (!memory) {
|
|
1710
|
+
const { AgentMemory } = await import('./memory.js');
|
|
1711
|
+
memory = new AgentMemory();
|
|
1712
|
+
await memory.load();
|
|
1713
|
+
}
|
|
1714
|
+
const key = memory.preferences?.vipKey;
|
|
1715
|
+
if (key) {
|
|
1716
|
+
return {
|
|
1717
|
+
status: 'success',
|
|
1718
|
+
apiKey: key,
|
|
1719
|
+
tier: 'Developer VIP',
|
|
1720
|
+
rateLimit: '1,000 requests/minute',
|
|
1721
|
+
usage: `Header: X-API-Key: ${key} | Query: ?apiKey=${key}`
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
return {
|
|
1725
|
+
status: 'info',
|
|
1726
|
+
message: 'No VIP key stored. Run /quiz in the CLI to complete the challenge and claim one.',
|
|
1727
|
+
apiKey: null
|
|
1728
|
+
};
|
|
1729
|
+
},
|
|
1620
1730
|
};
|
|
1621
1731
|
|
|
1622
1732
|
const toolSchemaByName = new Map(
|
package/server/agent/ui.js
CHANGED
|
@@ -228,13 +228,28 @@ export function wrapText(text, maxWidth = 72) {
|
|
|
228
228
|
const lines = [];
|
|
229
229
|
const rawParagraphs = text.split('\n');
|
|
230
230
|
|
|
231
|
+
const tokenize = para => {
|
|
232
|
+
const raw = para.split(/\s+/).filter(Boolean);
|
|
233
|
+
const tokens = [];
|
|
234
|
+
for (let i = 0; i < raw.length; i++) {
|
|
235
|
+
if (raw[i] === '•' && i + 1 < raw.length) {
|
|
236
|
+
tokens.push('• ' + raw[i + 1]);
|
|
237
|
+
i++;
|
|
238
|
+
} else {
|
|
239
|
+
tokens.push(raw[i]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return tokens;
|
|
243
|
+
};
|
|
244
|
+
|
|
231
245
|
for (const para of rawParagraphs) {
|
|
232
246
|
if (!para.trim()) {
|
|
233
247
|
lines.push('');
|
|
234
248
|
continue;
|
|
235
249
|
}
|
|
236
250
|
|
|
237
|
-
const words = para
|
|
251
|
+
const words = tokenize(para);
|
|
252
|
+
const paraLines = [];
|
|
238
253
|
let currentLine = '';
|
|
239
254
|
|
|
240
255
|
for (const word of words) {
|
|
@@ -246,7 +261,7 @@ export function wrapText(text, maxWidth = 72) {
|
|
|
246
261
|
currentLine = testLine;
|
|
247
262
|
} else {
|
|
248
263
|
if (currentLine) {
|
|
249
|
-
|
|
264
|
+
paraLines.push(currentLine);
|
|
250
265
|
currentLine = word;
|
|
251
266
|
} else {
|
|
252
267
|
let chunk = '';
|
|
@@ -254,7 +269,7 @@ export function wrapText(text, maxWidth = 72) {
|
|
|
254
269
|
if (getStringDisplayWidth(chunk + ch) <= maxWidth) {
|
|
255
270
|
chunk += ch;
|
|
256
271
|
} else {
|
|
257
|
-
|
|
272
|
+
paraLines.push(chunk);
|
|
258
273
|
chunk = ch;
|
|
259
274
|
}
|
|
260
275
|
}
|
|
@@ -262,9 +277,19 @@ export function wrapText(text, maxWidth = 72) {
|
|
|
262
277
|
}
|
|
263
278
|
}
|
|
264
279
|
}
|
|
265
|
-
if (currentLine)
|
|
266
|
-
|
|
280
|
+
if (currentLine) paraLines.push(currentLine);
|
|
281
|
+
|
|
282
|
+
if (paraLines.length >= 2 && !/\s/.test(paraLines[paraLines.length - 1].trim())) {
|
|
283
|
+
const last = paraLines[paraLines.length - 1];
|
|
284
|
+
const prev = paraLines[paraLines.length - 2];
|
|
285
|
+
const candidate = prev + ' ' + last;
|
|
286
|
+
if (getStringDisplayWidth(candidate) <= maxWidth) {
|
|
287
|
+
paraLines[paraLines.length - 2] = candidate;
|
|
288
|
+
paraLines.pop();
|
|
289
|
+
}
|
|
267
290
|
}
|
|
291
|
+
|
|
292
|
+
lines.push(...paraLines);
|
|
268
293
|
}
|
|
269
294
|
return lines;
|
|
270
295
|
}
|
|
@@ -829,7 +854,7 @@ export function formatMarkdownLines(text, maxWidth = getFullWidth() - 8) {
|
|
|
829
854
|
return result;
|
|
830
855
|
}
|
|
831
856
|
|
|
832
|
-
export function getWelcomeHeroLines() {
|
|
857
|
+
export function getWelcomeHeroLines() {
|
|
833
858
|
const p = C.sakura;
|
|
834
859
|
const r = C.roseGlow;
|
|
835
860
|
const g = C.gold;
|
|
@@ -841,7 +866,8 @@ export function getWelcomeHeroLines() {
|
|
|
841
866
|
const b = C.bold;
|
|
842
867
|
const d = C.dim;
|
|
843
868
|
const rst = C.reset;
|
|
844
|
-
const boxWidth = Math.min(84, Math.max(50, getFullWidth() - 4));
|
|
869
|
+
const boxWidth = Math.min(84, Math.max(50, getFullWidth() - 4));
|
|
870
|
+
const compact = getFullWidth() < 120;
|
|
845
871
|
const headerTitle = 'Suggested Topics & Queries';
|
|
846
872
|
const topDashCount = Math.max(2, boxWidth - getStringDisplayWidth(headerTitle) - 6);
|
|
847
873
|
const innerWidth = boxWidth - 2;
|
|
@@ -854,14 +880,28 @@ export function getWelcomeHeroLines() {
|
|
|
854
880
|
return ` ${s}│${rst} ${icon} ${tag}${rst}${' '.repeat(Math.max(0, tagWidth - getStringDisplayWidth(tag)))} ${w}${text}${rst}${' '.repeat(pad)}${s}│${rst}`;
|
|
855
881
|
};
|
|
856
882
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
` ${p}██╗ ██╗██╗██╗ ██╗██╗ ██╗ █████╗ ██╗${rst}
|
|
860
|
-
` ${
|
|
861
|
-
|
|
862
|
-
` ${
|
|
863
|
-
` ${p}██║
|
|
864
|
-
` ${
|
|
883
|
+
const heroLines = compact ? [
|
|
884
|
+
` ${b}${p}◆ Ju Jingyi • 鞠婧祎 • Mìjú Terminal Portal ◆${rst}`,
|
|
885
|
+
` ${p}██╗ ██╗██╗██╗ ██╗██╗ ██╗ █████╗ ██╗${rst}`,
|
|
886
|
+
` ${r}█████═╝ ██║█████═╝ ██║ ██║ ███████║██║${rst}`,
|
|
887
|
+
] : [
|
|
888
|
+
` ${p}██╗ ██╗██╗██╗ ██╗██╗ ██╗ █████╗ ██╗${rst} ${b}${p}◆ Ju Jingyi • 鞠婧祎 • Mìjú Terminal Portal ◆${rst}`,
|
|
889
|
+
` ${p}██║ ██╔╝██║██║ ██╔╝██║ ██║ ██╔══██╗██║${rst} ${d}${s}Interactive Fan Knowledge Base & Autonomous Agent${rst}`,
|
|
890
|
+
` ${r}█████═╝ ██║█████═╝ ██║ ██║ ███████║██║${rst} ${d}${s}Filmography • Discography • Fashion & Awards${rst}`,
|
|
891
|
+
` ${r}██╔═██╗ ██║██╔═██╗ ██║ ██║ ██╔══██╗██║${rst} ${c}● Live Ready${rst} ${s}• Multilingual (CN / EN / TH)${rst}`,
|
|
892
|
+
` ${p}██║ ╚██╗██║██║ ╚██╗╚██████╔╝ ██║ ██║██║${rst}`,
|
|
893
|
+
` ${p}╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝${rst}`,
|
|
894
|
+
];
|
|
895
|
+
const shortcutLines = compact ? [
|
|
896
|
+
` ${s}Shortcuts: ${b}${w}[Tab]${rst} ${s}Commands • ${b}${w}[↑/↓]${rst} ${s}History • ${b}${w}[F2]${rst} ${s}Details${rst}`,
|
|
897
|
+
` ${b}${w}[PgUp/PgDn]${rst} ${s}Scroll • ${b}${w}[Ctrl+C]${rst} ${s}Exit${rst}`,
|
|
898
|
+
] : [
|
|
899
|
+
` ${s}Shortcuts: ${b}${w}[Tab]${rst} ${s}Autocomplete • ${b}${w}[↑/↓]${rst} ${s}History • ${b}${w}[PgUp/PgDn]${rst} ${s}Scroll • ${b}${w}[F2]${rst} ${s}Details • ${b}${w}[Ctrl+C]${rst} ${s}Exit${rst}`,
|
|
900
|
+
];
|
|
901
|
+
|
|
902
|
+
return [
|
|
903
|
+
...(compact ? [] : ['']),
|
|
904
|
+
...heroLines,
|
|
865
905
|
'',
|
|
866
906
|
` ${s}╭──${rst} ${b}${g}${headerTitle}${rst} ${s}${'─'.repeat(topDashCount)}╮${rst}`,
|
|
867
907
|
padItem(p + '◆', '2026 Works', '"What are Ju Jingyi\'s 2026 upcoming dramas?"'),
|
|
@@ -870,7 +910,7 @@ export function getWelcomeHeroLines() {
|
|
|
870
910
|
padItem(l + '◆', 'Tools / MCP', '"@search_works 2026, /mcp list, /stats, /help"'),
|
|
871
911
|
` ${s}╰${'─'.repeat(boxWidth)}╯${rst}`,
|
|
872
912
|
'',
|
|
873
|
-
|
|
913
|
+
...shortcutLines,
|
|
874
914
|
'',
|
|
875
915
|
];
|
|
876
916
|
}
|
|
@@ -1040,7 +1080,7 @@ export function exitAlternateScreen() {
|
|
|
1040
1080
|
}
|
|
1041
1081
|
}
|
|
1042
1082
|
|
|
1043
|
-
export function getTuiViewportLayout(termRows = 24) {
|
|
1083
|
+
export function getTuiViewportLayout(termRows = 24) {
|
|
1044
1084
|
const parsedRows = Number(termRows);
|
|
1045
1085
|
const rows = Math.max(10, Number.isFinite(parsedRows) ? Math.floor(parsedRows) : 24);
|
|
1046
1086
|
const viewportStartRow = 1;
|
|
@@ -1055,10 +1095,16 @@ export function getTuiViewportLayout(termRows = 24) {
|
|
|
1055
1095
|
statusBarRow,
|
|
1056
1096
|
promptRow,
|
|
1057
1097
|
separatorRow,
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
export
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
export function getNewOutputScrollOffset(addedLineCount, termRows = 24, currentOffset = 0) {
|
|
1102
|
+
const added = Math.max(0, Number.isFinite(Number(addedLineCount)) ? Math.floor(Number(addedLineCount)) : 0);
|
|
1103
|
+
if (currentOffset > 0) return currentOffset + added;
|
|
1104
|
+
return Math.max(0, added - getTuiViewportLayout(termRows).viewportHeight);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
export class VirtualScreenBuffer {
|
|
1062
1108
|
constructor(width = 80, height = 24, writer = null) {
|
|
1063
1109
|
this.width = Math.max(10, width);
|
|
1064
1110
|
this.height = Math.max(5, height);
|
|
Binary file
|
|
Binary file
|