genshin-persona 3.7.0 → 3.9.0

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.
Files changed (136) hide show
  1. package/hooks/hooks.json +1 -1
  2. package/package.json +8 -4
  3. package/scripts/genshin.mjs +5 -0
  4. package/scripts/genshin.ts +11 -420
  5. package/scripts/pick.mjs +5 -0
  6. package/scripts/speak.mjs +5 -0
  7. package/scripts/spinner.mjs +5 -0
  8. package/scripts/spinner.ts +19 -4
  9. package/scripts/status.mjs +5 -0
  10. package/scripts/voice.mjs +5 -0
  11. package/skills/genshin/SKILL.md +1 -1
  12. package/skills/genshin-author/SKILL.md +4 -4
  13. package/skills/language/SKILL.md +1 -1
  14. package/skills/mute/SKILL.md +1 -1
  15. package/skills/pin/SKILL.md +1 -1
  16. package/skills/reply/SKILL.md +1 -1
  17. package/skills/roster/SKILL.md +1 -1
  18. package/skills/setup/SKILL.md +1 -1
  19. package/skills/status/SKILL.md +1 -1
  20. package/skills/teardown/SKILL.md +1 -1
  21. package/skills/today/SKILL.md +1 -1
  22. package/skills/unmute/SKILL.md +1 -1
  23. package/skills/unpin/SKILL.md +1 -1
  24. package/skills/use/SKILL.md +1 -1
  25. package/skills/voice/SKILL.md +1 -1
  26. package/skills/volume/SKILL.md +1 -1
  27. package/src/generated/CharacterLinesLoaderMap.ts +39 -0
  28. package/src/generated/characterLines/ChineseSimplified.json +4000 -0
  29. package/src/generated/characterLines/ChineseTraditional.json +4000 -0
  30. package/src/generated/characterLines/English.json +4000 -0
  31. package/src/generated/characterLines/French.json +4000 -0
  32. package/src/generated/characterLines/German.json +4000 -0
  33. package/src/generated/characterLines/Indonesian.json +4000 -0
  34. package/src/generated/characterLines/Italian.json +4000 -0
  35. package/src/generated/characterLines/Japanese.json +4000 -0
  36. package/src/generated/characterLines/Korean.json +4000 -0
  37. package/src/generated/characterLines/Portuguese.json +4000 -0
  38. package/src/generated/characterLines/Russian.json +4000 -0
  39. package/src/generated/characterLines/Spanish.json +4000 -0
  40. package/src/generated/characterLines/Thai.json +4000 -0
  41. package/src/generated/characterLines/Turkish.json +4000 -0
  42. package/src/generated/characterLines/Vietnamese.json +4000 -0
  43. package/src/generated/genshinText/generated/GameTextLoaderMap.ts +41 -0
  44. package/src/generated/genshinText/generated/text/ChineseSimplified.json +18 -0
  45. package/src/generated/genshinText/generated/text/ChineseTraditional.json +18 -0
  46. package/src/generated/genshinText/generated/text/English.json +18 -0
  47. package/src/generated/genshinText/generated/text/French.json +18 -0
  48. package/src/generated/genshinText/generated/text/German.json +18 -0
  49. package/src/generated/genshinText/generated/text/Indonesian.json +18 -0
  50. package/src/generated/genshinText/generated/text/Italian.json +18 -0
  51. package/src/generated/genshinText/generated/text/Japanese.json +18 -0
  52. package/src/generated/genshinText/generated/text/Korean.json +18 -0
  53. package/src/generated/genshinText/generated/text/Portuguese.json +18 -0
  54. package/src/generated/genshinText/generated/text/Russian.json +18 -0
  55. package/src/generated/genshinText/generated/text/Spanish.json +18 -0
  56. package/src/generated/genshinText/generated/text/Thai.json +18 -0
  57. package/src/generated/genshinText/generated/text/Turkish.json +18 -0
  58. package/src/generated/genshinText/generated/text/Vietnamese.json +18 -0
  59. package/src/generated/genshinText/models/GameLanguage.ts +22 -0
  60. package/src/generated/genshinText/models/GameText.ts +5 -0
  61. package/src/generated/genshinText/models/GameTextKey.ts +35 -0
  62. package/src/generated/genshinText/models/TravelerGender.ts +7 -0
  63. package/src/generated/genshinText/services/GameLanguageTagMap.ts +22 -0
  64. package/src/generated/genshinText/services/checkIsGameLanguage.ts +9 -0
  65. package/src/{services → generated/genshinText/services}/fillLinePlaceholders.ts +3 -2
  66. package/src/generated/genshinText/services/getCanonicalLanguage.ts +14 -0
  67. package/src/generated/genshinText/services/getLanguageDisplayName.ts +14 -0
  68. package/src/localizations/chineseSimplified.ts +269 -0
  69. package/src/localizations/chineseTraditional.ts +270 -0
  70. package/src/localizations/english.ts +5 -2
  71. package/src/localizations/french.ts +595 -0
  72. package/src/localizations/german.ts +595 -0
  73. package/src/localizations/indonesian.ts +598 -0
  74. package/src/localizations/italian.ts +630 -0
  75. package/src/localizations/japanese.ts +2 -2
  76. package/src/localizations/korean.ts +422 -0
  77. package/src/localizations/portuguese.ts +618 -0
  78. package/src/localizations/russian.ts +557 -0
  79. package/src/localizations/spanish.ts +621 -0
  80. package/src/localizations/thai.ts +455 -0
  81. package/src/localizations/turkish.ts +616 -0
  82. package/src/localizations/vietnamese.ts +627 -0
  83. package/src/models/GenshinContext.ts +15 -0
  84. package/src/models/GenshinVerb.ts +20 -24
  85. package/src/models/LocalizationStrings.ts +1 -1
  86. package/src/models/ResolvedLocalization.ts +5 -2
  87. package/src/models/SpinnerVerbsMode.ts +4 -4
  88. package/src/models/TravelerTwin.ts +1 -1
  89. package/src/models/VoiceLanguage.ts +8 -5
  90. package/src/models/VoiceRequestType.ts +6 -9
  91. package/src/models/VoiceStatus.ts +6 -9
  92. package/src/services/cli/commands/genshinCommand.ts +48 -0
  93. package/src/services/cli/commands/languageCommand.ts +77 -0
  94. package/src/services/cli/commands/linesCommand.ts +25 -0
  95. package/src/services/cli/commands/muteCommand.ts +15 -0
  96. package/src/services/cli/commands/pinCommand.ts +34 -0
  97. package/src/services/cli/commands/replyCommand.ts +30 -0
  98. package/src/services/cli/commands/rosterCommand.ts +14 -0
  99. package/src/services/cli/commands/setupCommand.ts +30 -0
  100. package/src/services/cli/commands/statusCommand.ts +15 -0
  101. package/src/services/cli/commands/teardownCommand.ts +21 -0
  102. package/src/services/cli/commands/todayCommand.ts +16 -0
  103. package/src/services/cli/commands/uncardedCommand.ts +14 -0
  104. package/src/services/cli/commands/unmuteCommand.ts +15 -0
  105. package/src/services/cli/commands/unpinCommand.ts +26 -0
  106. package/src/services/cli/commands/untranslatedCommand.ts +26 -0
  107. package/src/services/cli/commands/unverbedCommand.ts +14 -0
  108. package/src/services/cli/commands/useCommand.ts +27 -0
  109. package/src/services/cli/commands/voiceCommand.ts +115 -0
  110. package/src/services/cli/commands/volumeCommand.ts +24 -0
  111. package/src/services/cli/getCurrentCharacter.ts +23 -0
  112. package/src/services/cli/getRosterLine.ts +6 -0
  113. package/src/services/cli/getStatusReport.ts +46 -0
  114. package/src/services/cli/printCard.ts +16 -0
  115. package/src/services/cli/printQueue.ts +20 -0
  116. package/src/services/cli/readGenshinContext.ts +19 -0
  117. package/src/services/cli/readNamedCharacter.ts +16 -0
  118. package/src/services/cli/switchSessionCharacter.ts +13 -0
  119. package/src/services/constants.ts +13 -44
  120. package/src/services/getBirthdayNote.ts +5 -4
  121. package/src/services/getPersonaCardName.ts +6 -3
  122. package/src/services/parseWikiStoryLines.ts +6 -9
  123. package/src/services/parseWikiTemplateLines.ts +6 -14
  124. package/src/services/parseWikiTravelerLines.ts +10 -13
  125. package/src/services/readInterfaceLanguage.ts +9 -7
  126. package/src/services/readLocalization.ts +12 -5
  127. package/src/services/readReferenceClip.ts +2 -2
  128. package/src/services/readRoster.ts +3 -4
  129. package/src/services/readVoiceLines.ts +28 -28
  130. package/src/services/readWikiStoryLines.ts +6 -10
  131. package/src/models/TravelerGender.ts +0 -5
  132. package/src/models/WikiVoiceOversPage.ts +0 -6
  133. package/src/services/checkIsLanguageName.ts +0 -8
  134. package/src/services/getCanonicalLanguage.ts +0 -11
  135. package/src/services/getLanguageDisplayName.ts +0 -12
  136. package/src/services/readLanguageNames.ts +0 -8
package/hooks/hooks.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "hooks": {
3
- "SessionStart": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pick.ts\"" }] }]
3
+ "SessionStart": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pick.mjs\"" }] }]
4
4
  }
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genshin-persona",
3
- "version": "3.7.0",
3
+ "version": "3.9.0",
4
4
  "description": "A Claude Code plugin that speaks as the Genshin Impact character whose birthday is nearest: an answer opens with one spoken line in the character's voice and the rest is plain, a joke or a hello is spoken whole, and every line is read aloud in the character's own cloned voice as the reply is written.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -34,7 +34,9 @@
34
34
  ],
35
35
  "type": "module",
36
36
  "imports": {
37
- "#src/*": "./src/*.ts"
37
+ "#src/*": "./src/*.ts",
38
+ "#src/*.json": "./src/*.json",
39
+ "#scripts/*": "./scripts/*.ts"
38
40
  },
39
41
  "publishConfig": {
40
42
  "access": "public"
@@ -50,10 +52,12 @@
50
52
  "dependencies": {
51
53
  "@typesafe-ai/sdk": "^0.6.0",
52
54
  "@wasm-audio-decoders/ogg-vorbis": "^0.1.20",
53
- "genshin-db": "^5.2.14"
55
+ "citty": "^0.2.2",
56
+ "genshin-db": "^5.2.14",
57
+ "tsx": "^4.23.15"
54
58
  },
55
59
  "engines": {
56
60
  "node": ">=26.10.0"
57
61
  },
58
- "gitHead": "7fd06688d06adf6dafff4bd869cd7a33d8028688"
62
+ "gitHead": "f9f6208c35e6cff4ed33910623c798cd88f09301"
59
63
  }
@@ -0,0 +1,5 @@
1
+ // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
+ import { register } from "tsx/esm/api";
3
+
4
+ register();
5
+ await import("#scripts/genshin");
@@ -1,424 +1,15 @@
1
- import type { CardedCharacter } from "#src/models/CardedCharacter";
2
- import type { Character } from "#src/models/Character";
3
-
4
- import { GenshinVerb, GenshinVerbs } from "#src/models/GenshinVerb";
5
- import { VoiceRequestType } from "#src/models/VoiceRequestType";
6
- import { VoiceStatus } from "#src/models/VoiceStatus";
7
- import { checkHasWeights } from "#src/services/checkHasWeights";
8
- import { checkIsMuted } from "#src/services/checkIsMuted";
9
- import { checkIsOwnVoiceLine } from "#src/services/checkIsOwnVoiceLine";
10
- import { checkIsPluginHookEntry } from "#src/services/checkIsPluginHookEntry";
11
- import { checkIsPluginSpinner } from "#src/services/checkIsPluginSpinner";
12
- import { checkIsPluginStatusLine } from "#src/services/checkIsPluginStatusLine";
13
- import { checkIsRuntimeInstalled } from "#src/services/checkIsRuntimeInstalled";
14
- import { checkIsVoiceLanguage } from "#src/services/checkIsVoiceLanguage";
15
- import { checkIsVolume } from "#src/services/checkIsVolume";
16
- import { connectVoiceServer } from "#src/services/connectVoiceServer";
17
- import {
18
- CARD_DETAIL_SEPARATOR,
19
- DEFAULT_LANGUAGE,
20
- MAX_VOLUME,
21
- MODELS_DIRECTORY,
22
- RUNTIME_MANIFEST_PATH,
23
- SESSION_ID_ENVIRONMENT_VARIABLE,
24
- STATUS_LAUNCHER_PATH,
25
- STATUS_SCRIPT_PATH,
26
- VOICE_CPU_DEVICE,
27
- VOICE_LOG_PATH,
28
- VOICE_PROOF_TEXT,
29
- VOICE_STATUS_SEPARATOR,
30
- VoiceLanguageNameMap,
31
- } from "#src/services/constants";
32
- import { createVoiceProgressPrinter } from "#src/services/createVoiceProgressPrinter";
33
- import { createVoiceSynthesizer } from "#src/services/createVoiceSynthesizer";
34
- import { deletePin } from "#src/services/deletePin";
35
- import { deleteVoiceDevice } from "#src/services/deleteVoiceDevice";
36
- import { deleteVoiceState } from "#src/services/deleteVoiceState";
37
- import { findCharacterByName } from "#src/services/findCharacterByName";
38
- import { formatCard } from "#src/services/formatCard";
39
- import { getCanonicalLanguage } from "#src/services/getCanonicalLanguage";
40
- import { getCard } from "#src/services/getCard";
41
- import { getLanguageDisplayName } from "#src/services/getLanguageDisplayName";
42
- import { getSettingsWithoutPluginEntries } from "#src/services/getSettingsWithoutPluginEntries";
43
- import { getSettingsWithStatusLine } from "#src/services/getSettingsWithStatusLine";
44
- import { getSpeechRequest } from "#src/services/getSpeechRequest";
45
- import { getWarmRequest } from "#src/services/getWarmRequest";
46
- import { installVoiceRuntime } from "#src/services/installVoiceRuntime";
47
- import { pickCurrentCharacter } from "#src/services/pickCurrentCharacter";
48
- import { readCardedRoster } from "#src/services/readCardedRoster";
49
- import { readCharacterReference } from "#src/services/readCharacterReference";
1
+ import { GenshinVerbs } from "#src/models/GenshinVerb";
2
+ import { genshinCommand } from "#src/services/cli/commands/genshinCommand";
50
3
  import { readInterfaceLanguage } from "#src/services/readInterfaceLanguage";
51
- import { readLanguageNames } from "#src/services/readLanguageNames";
52
4
  import { readLocalization } from "#src/services/readLocalization";
53
- import { readPersonaCard } from "#src/services/readPersonaCard";
54
- import { readPin } from "#src/services/readPin";
55
- import { readReplyLanguage } from "#src/services/readReplyLanguage";
56
- import { readRoster } from "#src/services/readRoster";
57
- import { readSpinner } from "#src/services/readSpinner";
58
- import { readUserSettings } from "#src/services/readUserSettings";
59
- import { readVoiceDevice } from "#src/services/readVoiceDevice";
60
- import { readVoiceLanguage } from "#src/services/readVoiceLanguage";
61
- import { readVoiceLines } from "#src/services/readVoiceLines";
62
- import { readVoiceRuntime } from "#src/services/readVoiceRuntime";
63
- import { readVolume } from "#src/services/readVolume";
64
- import { recordSessionCharacter } from "#src/services/recordSessionCharacter";
65
- import { resolveSessionCharacter } from "#src/services/resolveSessionCharacter";
66
- import { sendVoiceRequest } from "#src/services/sendVoiceRequest";
67
- import { setMuted } from "#src/services/setMuted";
68
- import { writeInterfaceLanguage } from "#src/services/writeInterfaceLanguage";
69
- import { writeLauncher } from "#src/services/writeLauncher";
70
- import { writePin } from "#src/services/writePin";
71
- import { writeReplyLanguage } from "#src/services/writeReplyLanguage";
72
- import { writeSessionSpinner } from "#src/services/writeSessionSpinner";
73
- import { writeSpeakHook } from "#src/services/writeSpeakHook";
74
- import { writeSpinner } from "#src/services/writeSpinner";
75
- import { writeUserSettings } from "#src/services/writeUserSettings";
76
- import { writeVoiceLanguage } from "#src/services/writeVoiceLanguage";
77
- import { writeVolume } from "#src/services/writeVolume";
78
-
79
- const [verb, ...nameParts] = process.argv.slice(2);
80
- const name = nameParts.join(" ");
81
- const language = readInterfaceLanguage();
82
- const roster = readRoster(language);
83
- const { locale, strings } = await readLocalization(language);
84
- // A choice a verb offers is a list in the interface language's own punctuation, which the runtime knows
85
- const listFormat = new Intl.ListFormat(locale, { type: "disjunction" });
86
- const today = Temporal.Now.plainDateISO();
87
- const getRosterLine = ({ birthday, displayElement, displayName, region, title, version }: Character) =>
88
- [displayName, title, displayElement, region, birthday, `v${version}`].filter(Boolean).join(CARD_DETAIL_SEPARATOR);
89
- // An authoring queue prints the characters that meet it newest first, so a patch's arrivals are at the top
90
- const printQueue = async (checkIsQueued: (cardedCharacter: CardedCharacter) => boolean) => {
91
- const cardedRoster = await readCardedRoster(roster);
92
- for (const { character } of cardedRoster
93
- .filter((cardedCharacter) => checkIsQueued(cardedCharacter))
94
- .toSorted(
95
- ({ character: a }, { character: b }) =>
96
- b.version.localeCompare(a.version, undefined, { numeric: true }) || a.name.localeCompare(b.name),
97
- ))
98
- console.log(getRosterLine(character));
99
- };
100
- // The character a verb names by its argument, or nothing once the argument names none — which every such verb answers
101
- // The same way, so the refusal and its exit code are said here rather than at each
102
- const readNamedCharacter = () => {
103
- const character = findCharacterByName(roster, name);
104
- if (!character) {
105
- console.error(strings.noCharacterNamed(name));
106
- process.exitCode = 1;
107
- }
108
-
109
- return character;
110
- };
111
- // Empty from a shell, where the tool set nothing
112
- const sessionId = process.env[SESSION_ID_ENVIRONMENT_VARIABLE] ?? "";
113
- // The language is read again rather than closed over, because the `language` verb changes it and then prints the
114
- // Card: the card a verb prints is always in the language in force at the end of that verb
115
- const printCard = async (character: Character) => {
116
- const localization = await readLocalization(readInterfaceLanguage());
117
- const card = getCard(character, today, localization, await readPersonaCard(character.name));
118
- console.log(formatCard(card));
119
- };
120
- // This session's character when run inside one, else the pin, else a fresh pick: the resolution the start hook runs,
121
- // So `today` answers who is speaking now. Under the lore pick a fresh ask may answer differently, which is the point
122
- // Of it, and a fresh ask the tier did not answer says so beside the birthday pick that stood in
123
- const getCurrentCharacter = async () => {
124
- const pin = readPin();
125
- if (pin && !findCharacterByName(roster, pin.name)) console.log(strings.pinIgnored(pin.name));
126
-
127
- const pick = await resolveSessionCharacter(roster, sessionId, today);
128
- if (pick?.loreFailure) console.log(strings.lorePickUnanswered(pick.loreFailure));
129
- return pick?.character;
130
- };
131
- // The session speaks as the character from the reply that relays the card: its record is rewritten so every later
132
- // Start, the status line and the speech hook agree. The spinner is not rewritten here: the tool read its keys when
133
- // This session started, so a write now reaches only some other session, and a switch scoped to this one must not
134
- const switchSessionCharacter = async (character: Character) => {
135
- recordSessionCharacter(character, sessionId, today.toString());
136
- await printCard(character);
137
- };
138
- // Every knob in one read, for the verbs that report rather than change: the status verb, and either language verb
139
- // Given no argument
140
- const getStatusReport = async () => {
141
- const pin = readPin();
142
- const pick = sessionId ? await resolveSessionCharacter(roster, sessionId, today) : undefined;
143
- const replyLanguage = readReplyLanguage();
144
- const settings = readUserSettings();
145
- return {
146
- displayName: pick?.character.displayName ?? pin?.displayName ?? "",
147
- interfaceLanguage: getLanguageDisplayName(language, language),
148
- isFromSessionRecord: Boolean(pick),
149
- isMuted: checkIsMuted(),
150
- isPluginSpeakHook: settings.hooks?.MessageDisplay?.some((entry) => checkIsPluginHookEntry(entry)) ?? false,
151
- isPluginSpinner: checkIsPluginSpinner(settings),
152
- isPluginStatusLine: checkIsPluginStatusLine(settings.statusLine),
153
- isReplyLanguageCascaded: !replyLanguage,
154
- isRuntimeInstalled: checkIsRuntimeInstalled(),
155
- pinnedName: pin?.name ?? "",
156
- replyLanguage: getLanguageDisplayName(replyLanguage ?? language, language),
157
- voiceDevice: readVoiceDevice(),
158
- voiceLanguage: readVoiceLanguage(),
159
- volume: readVolume(),
160
- };
161
- };
162
-
163
- switch (verb) {
164
- case GenshinVerb.Language: {
165
- const languageNames = readLanguageNames();
166
- // Each language is offered in its own words beside the word a person types for it, and either resolves
167
- const offered = listFormat.format(
168
- languageNames.map((languageName) => {
169
- const ownName = getLanguageDisplayName(languageName, languageName);
170
- return ownName === languageName ? languageName : `${languageName} (${ownName})`;
171
- }),
172
- );
173
- if (!name) {
174
- console.log(strings.status(await getStatusReport()));
175
- console.log(strings.languageMustBeOneOf(offered));
176
- break;
177
- }
178
-
179
- const canonicalLanguage = getCanonicalLanguage(languageNames, name);
180
- if (!canonicalLanguage) {
181
- console.error(strings.languageMustBeOneOf(offered));
182
- process.exitCode = 1;
183
- break;
184
- }
185
-
186
- writeInterfaceLanguage(canonicalLanguage);
187
- // Everything downstream reads the language afresh: the roster in it, which builds that language's cache, the
188
- // Words that are ours in it, and the session's record and spinner, whose name is drawn from it
189
- const localizedRoster = readRoster(canonicalLanguage);
190
- const { strings: localizedStrings } = await readLocalization(canonicalLanguage);
191
- const pick = await resolveSessionCharacter(localizedRoster, sessionId, today);
192
- const character = pick?.character;
193
- if (character && sessionId) {
194
- recordSessionCharacter(character, sessionId, today.toString());
195
- await writeSessionSpinner(character, await readPersonaCard(character.name), canonicalLanguage);
196
- }
197
-
198
- const pin = readPin();
199
- const pinnedCharacter = findCharacterByName(localizedRoster, pin?.name ?? "");
200
- if (pinnedCharacter) writePin(pinnedCharacter);
201
- console.log(localizedStrings.interfaceLanguageSet(getLanguageDisplayName(canonicalLanguage, canonicalLanguage)));
202
- // A multi-gigabyte download is never a side effect of a labels setting, so the dub is reported on and left
203
- // Alone. Three states and three answers: no dub of this language, one that is not installed, and one that
204
- // Already is — which is the quiet case, since there is nothing for the person to do about it
205
- const [matchingDub] =
206
- Object.entries(VoiceLanguageNameMap).find(([, languageName]) => languageName === canonicalLanguage) ?? [];
207
- if (canonicalLanguage !== DEFAULT_LANGUAGE)
208
- if (!matchingDub) console.log(localizedStrings.voiceLanguageUnavailable);
209
- else if (matchingDub !== readVoiceLanguage()) console.log(localizedStrings.voiceLanguageAvailable(matchingDub));
210
-
211
- if (character) await printCard(character);
212
- break;
213
- }
214
- case GenshinVerb.Lines: {
215
- const character = readNamedCharacter();
216
- if (!character) break;
217
-
218
- console.log(`${getRosterLine(character)}\n${character.description}`);
219
- const voiceLines = await readVoiceLines(character.name, language);
220
- for (const { text, title } of voiceLines.filter((line) => checkIsOwnVoiceLine(line)))
221
- console.log(`- ${title}: ${text}`);
222
- break;
223
- }
224
- case GenshinVerb.Mute:
225
- setMuted(true);
226
- console.log(strings.muted);
227
- break;
228
- case GenshinVerb.Pin: {
229
- const pinnedCharacter = readNamedCharacter();
230
- if (!pinnedCharacter) break;
231
-
232
- writePin(pinnedCharacter);
233
- // The pinned character is what every later session speaks as, so the spinner may follow where `setup` opted it in
234
- await writeSessionSpinner(pinnedCharacter, await readPersonaCard(pinnedCharacter.name), language);
235
- if (sessionId) {
236
- await switchSessionCharacter(pinnedCharacter);
237
- console.log(strings.pinnedInSession);
238
- break;
239
- }
240
-
241
- await printCard(pinnedCharacter);
242
- console.log(strings.pinned);
243
- break;
244
- }
245
- case GenshinVerb.Reply: {
246
- if (!name) {
247
- console.log(strings.status(await getStatusReport()));
248
- break;
249
- }
250
- // Anything the model can write is a legal reply language, so this is not held to the data package's fifteen;
251
- // The canonical spelling is taken where it names one of them, so the common case reads as the language verb's
252
- const replyLanguage = getCanonicalLanguage(readLanguageNames(), name) ?? name;
253
- writeReplyLanguage(replyLanguage);
254
- console.log(strings.replyLanguageSet(getLanguageDisplayName(replyLanguage, language)));
255
- if (replyLanguage !== DEFAULT_LANGUAGE && readVoiceLanguage()) console.log(strings.replyLanguageSilencesVoice);
256
- break;
257
- }
258
- case GenshinVerb.Roster:
259
- for (const character of roster) console.log(getRosterLine(character));
260
- break;
261
- case GenshinVerb.Setup: {
262
- writeLauncher(STATUS_LAUNCHER_PATH, STATUS_SCRIPT_PATH);
263
- const userSettings = readUserSettings();
264
- const settings = getSettingsWithStatusLine(userSettings);
265
- writeUserSettings(settings);
266
- const character = await getCurrentCharacter();
267
- if (character) writeSpinner(await readSpinner(character, await readPersonaCard(character.name), language));
268
- console.log(checkIsPluginStatusLine(settings.statusLine) ? strings.setupDone : strings.setupStatusLineKept);
269
- break;
270
- }
271
- case GenshinVerb.Status:
272
- console.log(strings.status(await getStatusReport()));
273
- break;
274
- case GenshinVerb.Teardown: {
275
- const userSettings = readUserSettings();
276
- const settings = getSettingsWithoutPluginEntries(userSettings);
277
- writeUserSettings(settings);
278
- await deleteVoiceState();
279
- console.log(strings.teardownDone);
280
- break;
281
- }
282
- case GenshinVerb.Today: {
283
- const character = await getCurrentCharacter();
284
- if (character) await printCard(character);
285
- break;
286
- }
287
- case GenshinVerb.Uncarded:
288
- await printQueue(({ personaCard }) => !personaCard);
289
- break;
290
- case GenshinVerb.Unmute:
291
- setMuted(false);
292
- console.log(strings.unmuted);
293
- break;
294
- case GenshinVerb.Unpin: {
295
- deletePin();
296
- const pick = sessionId ? await pickCurrentCharacter(roster, today) : undefined;
297
- if (!pick) {
298
- console.log(strings.pinRemoved);
299
- break;
300
- }
301
-
302
- if (pick.loreFailure) console.log(strings.lorePickUnanswered(pick.loreFailure));
303
- await switchSessionCharacter(pick.character);
304
- console.log(strings.pinRemovedInSession);
305
- break;
306
- }
307
- case GenshinVerb.Untranslated: {
308
- // The queue the language modules are filled from, the way `unverbed` is the queue the cards' verbs are: who has
309
- // No gerunds in this language, or a card whose greeting it has not written. English reads both off the cards,
310
- // So it is never behind
311
- if (language === DEFAULT_LANGUAGE) break;
312
-
313
- const { characters } = await readLocalization(language);
314
- await printQueue(({ character: { name: characterName }, personaCard }) => {
315
- const localizedPersonaCard = characters[characterName];
316
- return !localizedPersonaCard?.verbs || (Boolean(personaCard) && !localizedPersonaCard.greeting);
317
- });
318
- break;
319
- }
320
- case GenshinVerb.Unverbed:
321
- await printQueue(({ personaCard }) => personaCard?.verbs.length === 0);
322
- break;
323
- case GenshinVerb.Use: {
324
- const character = readNamedCharacter();
325
- if (!character) break;
326
-
327
- if (!sessionId) {
328
- console.error(strings.noSession);
329
- process.exitCode = 1;
330
- break;
331
- }
332
-
333
- await switchSessionCharacter(character);
334
- console.log(strings.usingInSession);
335
- break;
336
- }
337
- case GenshinVerb.Voice: {
338
- if (!name) {
339
- const voiceLanguage = readVoiceLanguage();
340
- console.log(
341
- voiceLanguage
342
- ? strings.voiceStatus(checkIsRuntimeInstalled(), voiceLanguage, readVoiceDevice(), VOICE_LOG_PATH)
343
- : strings.voiceUnset,
344
- );
345
- break;
346
- }
347
-
348
- if (!checkIsVoiceLanguage(name)) {
349
- console.error(strings.voiceLanguageMustBeOneOf(listFormat.format(Object.keys(VoiceLanguageNameMap))));
350
- process.exitCode = 1;
351
- break;
352
- }
353
- // The proof walks the device ladder from the top, so a rung this machine once demoted is tried again here and
354
- // Nowhere else: the rung on file is cleared, and a synthesizer still running — on that rung, or on the runtime
355
- // Being replaced — is stopped, so the one the warm spawns loads afresh
356
- deleteVoiceDevice();
357
- await connectVoiceServer({ type: VoiceRequestType.Stop });
358
- if (checkIsRuntimeInstalled()) console.log(strings.runtimeInstalled);
359
- else {
360
- console.log(strings.runtimeInstalling);
361
- if (!installVoiceRuntime()) {
362
- console.error(strings.runtimeInstallFailed);
363
- process.exitCode = 1;
364
- break;
365
- }
366
- }
367
- // The runtime's own loader fetches what it is asked to load into the models directory, so the weights are
368
- // Downloaded by loading the engine once here — with progress, which the detached synthesizer cannot print —
369
- // And the synthesizer then loads them from the cache inside a hook's budget. A cache already holding them skips
370
- // The load, since the rung the engine speaks on is the proof's to report
371
- if (checkHasWeights(MODELS_DIRECTORY)) console.log(strings.weightsPresent);
372
- else {
373
- const runtime = readVoiceRuntime(RUNTIME_MANIFEST_PATH);
374
- const synthesizer = await createVoiceSynthesizer(runtime, MODELS_DIRECTORY, {
375
- onFallback: console.log,
376
- onProgress: createVoiceProgressPrinter(),
377
- });
378
- console.log(
379
- synthesizer.device === VOICE_CPU_DEVICE ? strings.weightsOnCpu : strings.weightsOnDevice(synthesizer.device),
380
- );
381
- }
382
- // The dub on disk is the gate every spoken reply passes, so it is written only where this run proved the
383
- // Voice — a setup that failed after it leaves the replies silent rather than broken in the hooks' silence
384
- const character = await getCurrentCharacter();
385
- if (!character) {
386
- writeVoiceLanguage(name);
387
- writeSpeakHook();
388
- console.log(strings.voiceLanguageWritten(name));
389
- break;
390
- }
391
-
392
- const personaCard = await readPersonaCard(character.name);
393
- if (!readCharacterReference(character.name, personaCard)) console.log(strings.noReference(character.displayName));
394
-
395
- const warmed = await sendVoiceRequest(getWarmRequest(character.name, personaCard, name));
396
- const [status, device] = warmed.split(VOICE_STATUS_SEPARATOR);
397
- if (status !== VoiceStatus.Ok) {
398
- console.error(strings.warmRequestUnanswered(status || "unreachable", VOICE_LOG_PATH));
399
- process.exitCode = 1;
400
- break;
401
- }
402
-
403
- writeVoiceLanguage(name);
404
- writeSpeakHook();
405
- await sendVoiceRequest(
406
- getSpeechRequest(VoiceRequestType.Speak, character.name, personaCard, name, [VOICE_PROOF_TEXT]),
407
- );
408
- console.log(strings.spoke(character.displayName, device ?? ""));
409
- break;
410
- }
411
- case GenshinVerb.Volume:
412
- if (!checkIsVolume(name)) {
413
- console.error(strings.volumeMustBeWholeNumber(MAX_VOLUME));
414
- process.exitCode = 1;
415
- break;
416
- }
5
+ import { runMain } from "citty";
417
6
 
418
- writeVolume(name);
419
- console.log(strings.volumeSet(name));
420
- break;
421
- default:
7
+ // Every line the plugin prints is in the interface language, so a verb it does not know, or none, or a request for help,
8
+ // Is answered with its own usage line, and exits before citty would add an English one of its own
9
+ await runMain(genshinCommand, {
10
+ showUsage: async () => {
11
+ const { strings } = await readLocalization(readInterfaceLanguage());
422
12
  console.error(strings.usage(GenshinVerbs.join(" | ")));
423
- process.exitCode = 1;
424
- }
13
+ process.exit(1);
14
+ },
15
+ });
@@ -0,0 +1,5 @@
1
+ // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
+ import { register } from "tsx/esm/api";
3
+
4
+ register();
5
+ await import("#scripts/pick");
@@ -0,0 +1,5 @@
1
+ // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
+ import { register } from "tsx/esm/api";
3
+
4
+ register();
5
+ await import("#scripts/speak");
@@ -0,0 +1,5 @@
1
+ // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
+ import { register } from "tsx/esm/api";
3
+
4
+ register();
5
+ await import("#scripts/spinner");
@@ -4,13 +4,28 @@ import { readPersonaCard } from "#src/services/readPersonaCard";
4
4
  import { readRoster } from "#src/services/readRoster";
5
5
  import { registerQuietExit } from "#src/services/registerQuietExit";
6
6
  import { writeSessionSpinner } from "#src/services/writeSessionSpinner";
7
+ import { defineCommand, runMain } from "citty";
7
8
 
8
9
  // Spawned detached by the session-start hook, which must not wait on it: this session's character's lines are read
9
10
  // And the spinner rewritten while the person reads the card, where `setup` opted the settings in. The character
10
11
  // Arrives as the English name, which is the identity, and the roster it is looked up in is the interface
11
12
  // Language's — so the label written is the name that language spells them by
12
13
  registerQuietExit();
13
- const [name = ""] = process.argv.slice(2);
14
- const language = readInterfaceLanguage();
15
- const character = name ? findCharacterByName(readRoster(language), name) : undefined;
16
- if (character) await writeSessionSpinner(character, await readPersonaCard(character.name), language);
14
+ await runMain(
15
+ defineCommand({
16
+ args: {
17
+ name: {
18
+ default: "",
19
+ description: "The session's character, by English name",
20
+ required: false,
21
+ type: "positional",
22
+ },
23
+ },
24
+ meta: { description: "Rewrite the spinner under the session's character", name: "spinner" },
25
+ run: async ({ args }) => {
26
+ const language = readInterfaceLanguage();
27
+ const character = args.name ? findCharacterByName(readRoster(language), args.name) : undefined;
28
+ if (character) await writeSessionSpinner(character, await readPersonaCard(character.name), language);
29
+ },
30
+ }),
31
+ );
@@ -0,0 +1,5 @@
1
+ // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
+ import { register } from "tsx/esm/api";
3
+
4
+ register();
5
+ await import("#scripts/status");
@@ -0,0 +1,5 @@
1
+ // Node strips types and takes no `enum`, so the entry registers tsx before it loads its TypeScript
2
+ import { register } from "tsx/esm/api";
3
+
4
+ register();
5
+ await import("#scripts/voice");
@@ -9,7 +9,7 @@ user-invocable: false
9
9
  The plugin picks a character at every session start — by lore through a typed decision when a TypeSafe key is configured, by the nearest birthday otherwise; the verbs below are how the user reads and overrides that. Each verb is its own slash command, `/genshin-persona:<verb>`, for the user to type; this skill is how a request put in words reaches one. Every verb runs the plugin's script and relays its output — the roster is game data the script reads, and nothing about it is known without running it.
10
10
 
11
11
  ```bash
12
- node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" <verb> [name]
12
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" <verb> [name]
13
13
  ```
14
14
 
15
15
  | Asked for | Verb |
@@ -12,9 +12,9 @@ A character with no card is fully usable: the session-start hook prints the name
12
12
  Newest first, so the queue starts with whoever a player has heard most recently:
13
13
 
14
14
  ```bash
15
- node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" uncarded # no card at all
16
- node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" unverbed # a card with no verbs
17
- node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" untranslated # no gerunds or greeting in the interface language
15
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" uncarded # no card at all
16
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" unverbed # a card with no verbs
17
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" untranslated # no gerunds or greeting in the interface language
18
18
  ```
19
19
 
20
20
  The third queue is a language's rather than a card's, and it is empty under English, where the gerunds and the greeting are read off the cards themselves. Under any other language they live in one module per language at `src/localizations/<language>.ts`, named the way a card is named for its character: a `characters` map keyed by the character's English name, each entry the person's half of that card — `verbs` and `greeting`, either present once written — beside that language's base Teyvat verbs. A character with no verbs in a module that exists shows the base verbs alone rather than the card's English ones behind localized ones; a language with no module at all inherits English base verbs, so the cards' gerunds show there too. A character with no greeting is greeted in the card's own words, since one line in another script is a queue item where a spinner mixing two is broken. Either way the queue draining is what fills a spinner and a welcome out and never what stops them working.
@@ -83,7 +83,7 @@ The full reasoning — how a reference is chosen and what its likeness number is
83
83
  A line is drawn from the character's own in-game lines and story, and one command prints them — the description, then every line the game-data dependency carries, and when it carries none yet, the same lines read off the community wiki's voice-over page for the interface language, which the wiki keeps for the four dubs:
84
84
 
85
85
  ```bash
86
- node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" lines <name>
86
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" lines <name>
87
87
  ```
88
88
 
89
89
  A card is described **in our words** — never a quoted line, never a catchphrase copied verbatim, never a lyric, and never a line reworded closely enough to be recognised. The repository holds no text, image or audio lifted from the game, and a card is the one place that rule is tested by hand. An invented mannerism is not a habit: if the character's lines do not show it, it does not go in, however well it would read.
@@ -8,7 +8,7 @@ disable-model-invocation: true
8
8
  # Language
9
9
 
10
10
  ```bash
11
- node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" language $ARGUMENTS
11
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" language $ARGUMENTS
12
12
  ```
13
13
 
14
14
  Relay its lines as written. The card it prints is the session's card from this reply on.
@@ -7,7 +7,7 @@ disable-model-invocation: true
7
7
  # Mute
8
8
 
9
9
  ```bash
10
- node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" mute
10
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" mute
11
11
  ```
12
12
 
13
13
  Relay its lines as written.
@@ -8,7 +8,7 @@ disable-model-invocation: true
8
8
  # Pin
9
9
 
10
10
  ```bash
11
- node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" pin $ARGUMENTS
11
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" pin $ARGUMENTS
12
12
  ```
13
13
 
14
14
  Relay its lines as written, and answer as the card it printed from this reply on.
@@ -8,7 +8,7 @@ disable-model-invocation: true
8
8
  # Reply
9
9
 
10
10
  ```bash
11
- node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" reply $ARGUMENTS
11
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" reply $ARGUMENTS
12
12
  ```
13
13
 
14
14
  Relay its lines as written, and write every reply in that language from this one on.
@@ -7,7 +7,7 @@ disable-model-invocation: true
7
7
  # Roster
8
8
 
9
9
  ```bash
10
- node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" roster
10
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" roster
11
11
  ```
12
12
 
13
13
  Relay its lines as written.
@@ -7,7 +7,7 @@ disable-model-invocation: true
7
7
  # Setup
8
8
 
9
9
  ```bash
10
- node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" setup
10
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" setup
11
11
  ```
12
12
 
13
13
  Relay its lines as written.
@@ -7,7 +7,7 @@ disable-model-invocation: true
7
7
  # Status
8
8
 
9
9
  ```bash
10
- node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.ts" status
10
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/genshin.mjs" status
11
11
  ```
12
12
 
13
13
  Relay its lines as written.