genshin-persona 3.9.0 → 4.1.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 (166) hide show
  1. package/.claude-plugin/plugin.json +1 -0
  2. package/.mcp.json +1 -0
  3. package/README.md +48 -49
  4. package/hooks/hooks.json +2 -1
  5. package/mod/CardVerbs.ts +9 -0
  6. package/mod/VerbDescriptionMap.ts +22 -0
  7. package/mod/register.ts +140 -0
  8. package/package.json +11 -5
  9. package/runtime/package-lock.json +7 -7
  10. package/runtime/package.json +1 -1
  11. package/scripts/channel.mjs +5 -0
  12. package/scripts/channel.ts +98 -0
  13. package/scripts/genshin.ts +2 -2
  14. package/scripts/pick.ts +3 -18
  15. package/scripts/spinner.ts +8 -7
  16. package/scripts/status.ts +13 -18
  17. package/scripts/statusLine.mjs +5 -0
  18. package/scripts/statusLine.ts +11 -0
  19. package/scripts/voice.ts +22 -16
  20. package/skills/genshin/SKILL.md +19 -19
  21. package/skills/genshin-author/SKILL.md +2 -2
  22. package/src/generated/CharacterLinesLoaderMap.ts +2 -3
  23. package/src/generated/genshinText/generated/GameTextLoaderMap.ts +2 -3
  24. package/src/generated/genshinText/generated/text/ChineseSimplified.json +402 -0
  25. package/src/generated/genshinText/generated/text/ChineseTraditional.json +402 -0
  26. package/src/generated/genshinText/generated/text/English.json +402 -0
  27. package/src/generated/genshinText/generated/text/French.json +402 -0
  28. package/src/generated/genshinText/generated/text/German.json +402 -0
  29. package/src/generated/genshinText/generated/text/Indonesian.json +402 -0
  30. package/src/generated/genshinText/generated/text/Italian.json +402 -0
  31. package/src/generated/genshinText/generated/text/Japanese.json +402 -0
  32. package/src/generated/genshinText/generated/text/Korean.json +402 -0
  33. package/src/generated/genshinText/generated/text/Portuguese.json +402 -0
  34. package/src/generated/genshinText/generated/text/Russian.json +402 -0
  35. package/src/generated/genshinText/generated/text/Spanish.json +402 -0
  36. package/src/generated/genshinText/generated/text/Thai.json +402 -0
  37. package/src/generated/genshinText/generated/text/Turkish.json +402 -0
  38. package/src/generated/genshinText/generated/text/Vietnamese.json +402 -0
  39. package/src/generated/genshinText/models/GameTextKey.ts +487 -3
  40. package/src/generated/genshinText/services/checkIsGameLanguage.ts +2 -2
  41. package/src/localizations/chineseSimplified.ts +5 -16
  42. package/src/localizations/chineseTraditional.ts +5 -17
  43. package/src/localizations/english.ts +5 -20
  44. package/src/localizations/french.ts +5 -21
  45. package/src/localizations/german.ts +5 -21
  46. package/src/localizations/indonesian.ts +5 -21
  47. package/src/localizations/italian.ts +5 -21
  48. package/src/localizations/japanese.ts +5 -18
  49. package/src/localizations/korean.ts +5 -19
  50. package/src/localizations/portuguese.ts +5 -21
  51. package/src/localizations/russian.ts +5 -22
  52. package/src/localizations/spanish.ts +5 -21
  53. package/src/localizations/thai.ts +5 -16
  54. package/src/localizations/turkish.ts +5 -20
  55. package/src/localizations/vietnamese.ts +5 -20
  56. package/src/models/Character.ts +1 -1
  57. package/src/models/GenshinVerb.ts +0 -1
  58. package/src/models/LocalizationStrings.ts +0 -2
  59. package/src/models/Nameplate.ts +1 -1
  60. package/src/models/StatusReport.ts +2 -5
  61. package/src/models/VoiceDeviceLadder.ts +4 -0
  62. package/src/models/VoiceDeviceRung.ts +1 -1
  63. package/src/models/VoiceModel.ts +5 -1
  64. package/src/models/VoiceRuntime.ts +1 -1
  65. package/src/models/VoiceSynthesizer.ts +11 -1
  66. package/src/models/VoiceTensor.ts +3 -4
  67. package/src/services/CharacterColorMap.ts +5 -6
  68. package/src/services/ElementColorMap.ts +12 -0
  69. package/src/services/LanguageDubPrefixMap.ts +9 -0
  70. package/src/services/TravelerTwinMap.ts +10 -0
  71. package/src/services/VoiceLanguageNameMap.ts +11 -0
  72. package/src/services/checkIsChannelRequestAllowed.ts +17 -0
  73. package/src/services/checkIsVoiceLanguage.ts +1 -2
  74. package/src/services/cli/commands/genshinCommand.ts +0 -2
  75. package/src/services/cli/commands/languageCommand.ts +4 -8
  76. package/src/services/cli/commands/linesCommand.ts +2 -2
  77. package/src/services/cli/commands/pinCommand.ts +3 -7
  78. package/src/services/cli/commands/teardownCommand.ts +0 -6
  79. package/src/services/cli/commands/useCommand.ts +2 -2
  80. package/src/services/cli/commands/voiceCommand.ts +14 -15
  81. package/src/services/cli/commands/volumeCommand.ts +4 -4
  82. package/src/services/cli/{readNamedCharacter.ts → findNamedCharacter.ts} +1 -1
  83. package/src/services/cli/getStatusReport.ts +0 -8
  84. package/src/services/cli/printQueue.ts +3 -2
  85. package/src/services/cli/switchSessionCharacter.ts +1 -2
  86. package/src/services/constants.ts +58 -64
  87. package/src/services/createAudioPlayer.ts +6 -5
  88. package/src/services/createChannelPage.ts +58 -0
  89. package/src/services/createChunkSplicer.ts +47 -0
  90. package/src/services/createVoiceSynthesizer.ts +152 -4
  91. package/src/services/cutReferenceClip.ts +1 -1
  92. package/src/services/findCharacterByName.ts +1 -1
  93. package/src/services/formatLoreChart.ts +1 -1
  94. package/src/services/formatNameplate.ts +10 -13
  95. package/src/services/{readCharacterReference.ts → getCharacterReference.ts} +1 -1
  96. package/src/services/getLorePickRequest.ts +5 -1
  97. package/src/services/getNameplateColor.ts +10 -0
  98. package/src/services/getPersonaCharacter.ts +13 -0
  99. package/src/services/getReplyLanguageInstruction.ts +4 -0
  100. package/src/services/getSessionNameplate.ts +3 -3
  101. package/src/services/getSessionStartOutput.ts +3 -7
  102. package/src/services/getSpeechRequest.ts +2 -2
  103. package/src/services/getSpinner.ts +7 -22
  104. package/src/services/getSpokenLines.ts +4 -4
  105. package/src/services/getVoiceDeviceLadder.ts +7 -0
  106. package/src/services/getWikiFileTitle.ts +2 -1
  107. package/src/services/installVoiceRuntime.ts +1 -0
  108. package/src/services/parseBirthday.ts +7 -3
  109. package/src/services/parseWikiTemplateLines.ts +9 -8
  110. package/src/services/pickCharacterByLore.ts +8 -4
  111. package/src/services/readChannelLine.ts +19 -0
  112. package/src/services/readPersonaModule.ts +1 -0
  113. package/src/services/readSessionNameplate.ts +24 -0
  114. package/src/services/readSpinner.ts +2 -2
  115. package/src/services/readStdinSessionId.ts +7 -0
  116. package/src/services/readVoiceDevice.ts +3 -2
  117. package/src/services/readVoiceLines.ts +2 -1
  118. package/src/services/readWikiFileUrls.ts +2 -2
  119. package/src/services/readWikiStoryLines.ts +1 -1
  120. package/src/services/recordSessionCharacter.ts +3 -3
  121. package/src/services/registerQuietExit.ts +6 -2
  122. package/src/services/vocodeSpeechTokens.ts +26 -0
  123. package/src/util/getContrastRatio.ts +3 -4
  124. package/types/index.d.ts +37 -0
  125. package/skills/language/SKILL.md +0 -14
  126. package/skills/mute/SKILL.md +0 -13
  127. package/skills/pin/SKILL.md +0 -14
  128. package/skills/reply/SKILL.md +0 -14
  129. package/skills/roster/SKILL.md +0 -13
  130. package/skills/setup/SKILL.md +0 -13
  131. package/skills/status/SKILL.md +0 -13
  132. package/skills/teardown/SKILL.md +0 -13
  133. package/skills/today/SKILL.md +0 -13
  134. package/skills/unmute/SKILL.md +0 -13
  135. package/skills/unpin/SKILL.md +0 -13
  136. package/skills/use/SKILL.md +0 -14
  137. package/skills/voice/SKILL.md +0 -14
  138. package/skills/volume/SKILL.md +0 -14
  139. package/src/models/HookCommand.ts +0 -6
  140. package/src/models/HookEntry.ts +0 -7
  141. package/src/models/Spinner.ts +0 -11
  142. package/src/models/SpinnerTip.ts +0 -5
  143. package/src/models/SpinnerTipsOverride.ts +0 -12
  144. package/src/models/SpinnerVerbs.ts +0 -8
  145. package/src/models/SpinnerVerbsMode.ts +0 -4
  146. package/src/models/StatusLine.ts +0 -6
  147. package/src/models/UserHooks.ts +0 -7
  148. package/src/models/UserSettings.ts +0 -14
  149. package/src/services/checkIsPluginHookEntry.ts +0 -8
  150. package/src/services/checkIsPluginSpinner.ts +0 -12
  151. package/src/services/checkIsPluginStatusLine.ts +0 -7
  152. package/src/services/cli/commands/setupCommand.ts +0 -30
  153. package/src/services/getPluginSpeakHookEntry.ts +0 -10
  154. package/src/services/getPluginStatusLine.ts +0 -9
  155. package/src/services/getSettingsWithSpeakHook.ts +0 -13
  156. package/src/services/getSettingsWithSpinner.ts +0 -11
  157. package/src/services/getSettingsWithStatusLine.ts +0 -10
  158. package/src/services/getSettingsWithoutPluginEntries.ts +0 -25
  159. package/src/services/readUserSettings.ts +0 -29
  160. package/src/services/spawnDetachedScript.ts +0 -7
  161. package/src/services/writeLauncher.ts +0 -12
  162. package/src/services/writeSessionSpinner.ts +0 -32
  163. package/src/services/writeSpeakHook.ts +0 -13
  164. package/src/services/writeSpinner.ts +0 -14
  165. package/src/services/writeUserSettings.ts +0 -9
  166. package/src/util/toForwardSlashes.ts +0 -2
@@ -8,6 +8,7 @@
8
8
  "repository": "https://github.com/Esposter/Esposter",
9
9
  "license": "Apache-2.0",
10
10
  "keywords": ["genshin", "genshin-impact", "output-style", "persona", "text-to-speech", "voice-cloning"],
11
+ "types": "./types/index.d.ts",
11
12
  "userConfig": {
12
13
  "typesafe_key": {
13
14
  "type": "string",
package/.mcp.json ADDED
@@ -0,0 +1 @@
1
+ { "mcpServers": { "genshin-persona": { "command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/channel.mjs"] } } }
package/README.md CHANGED
@@ -10,7 +10,7 @@ A Claude Code plugin that gives every session a Genshin Impact character: a repl
10
10
  - **A character per session** — picked by the nearest birthday, or by lore through one typed decision when you give it a TypeSafe key; `use` and `pin` override the pick.
11
11
  - **A spoken channel, not a persona in the prose** — the character speaks in blockquote lines and nowhere else: one line opening an answer a reader will use, with everything else — the answer, the code, the commit message — a neutral assistant's, and the whole reply when the ask was the character's.
12
12
  - **Read aloud as it is written** — a hook hands each spoken line to a resident synthesizer the moment its line lands, and the line is read in the character's cloned voice while the reply is still streaming; the engine is warmed at the session start so the first reply pays no load.
13
- - **Localized** — the card, the spinner, the status line and every line the plugin prints in the language you set, and replies in the one you choose.
13
+ - **Localized** — the card, the spinner, the prompt hint and every line the plugin prints in the language you set, and replies in the one you choose.
14
14
 
15
15
  ## Table of Contents
16
16
 
@@ -22,7 +22,7 @@ A Claude Code plugin that gives every session a Genshin Impact character: a repl
22
22
 
23
23
  ## <a name="getting-started">🚀 Getting Started</a>
24
24
 
25
- This repository is a Claude Code plugin marketplace named `esposter`, and this package is its one plugin:
25
+ This repository is a Claude Code plugin marketplace named `esposter`:
26
26
 
27
27
  ```bash
28
28
  claude plugin marketplace add Esposter/Esposter
@@ -44,15 +44,15 @@ claude plugin disable genshin-persona@esposter
44
44
  claude --plugin-dir packages/genshin-persona
45
45
  ```
46
46
 
47
- Nobody switches that on: `.agents/settings.json` declares `autoUpdate` on the marketplace it declares, and Claude Code leaves it off for every marketplace but Anthropic's own. A checkout therefore re-pins its own plugin to its own current commit on startup, and `/plugin`'s **Enable auto-update** reports the value as settings' rather than offering to change it. An update still moves a pin to a commit, so work that is not committed is not loaded, whatever the toggle says. Nothing below is repeated for an update: the state directory's launchers re-aim at the running copy at every session start, and the voice's runtime and weights are kept until the copy carries a newer lockfile or engine, which `voice` with no argument reports and `voice <dub>` run again picks up, installing only what changed.
47
+ Nobody switches that on: `.agents/settings.json` declares `autoUpdate` on the marketplace it declares, and Claude Code leaves it off for every marketplace but Anthropic's own. A checkout therefore re-pins its own plugin to its own current commit on startup, and `/plugin`'s **Enable auto-update** reports the value as settings' rather than offering to change it. An update still moves a pin to a commit, so work that is not committed is not loaded, whatever the toggle says. Nothing below is repeated for an update: the plugin writes nothing into your settings, so every surface it draws comes from the running copy, and the voice's runtime and weights are kept until the copy carries a newer lockfile or engine, which `voice` with no argument reports and `voice <dub>` run again picks up, installing only what changed.
48
48
 
49
49
  Spoken lines stay silent until the `voice` verb has set the engine up, once, with the dub the reference lines are taken from — `en`, `ja`, `ko` or `zh`:
50
50
 
51
51
  ```bash
52
- node "<plugin root>/scripts/genshin.ts" voice ja # or: /genshin-persona:voice ja
52
+ node "<plugin root>/scripts/genshin.mjs" voice ja # or: /genshin-voice ja
53
53
  ```
54
54
 
55
- It installs the engine's runtime and weights — about a gigabyte, once — into the plugin's own state directory under `~/.claude/genshin-persona`, ends by speaking one sentence as the session's character, and writes the hook that reads each reply's spoken lines into your user settings, which Claude Code reads once per process, so replies are read from the next session. Each character is read in a clone of their own voice, conditioned on one line of their performance fetched from the community wiki the first time they are picked; nothing lifted from the game ships with the plugin. How the engine runs, which device it settles on and why, and what it does when it cannot speak is the [spoken replies](https://esposter.com/docs/infra/claude-interface/spoken-replies) page.
55
+ It installs the engine's runtime and weights — about a gigabyte, once — into the plugin's own state directory under `~/.claude/genshin-persona`, and ends by speaking one sentence as the session's character; replies are read aloud from the next reply on, by the plugin's own hooks module, with nothing written into your settings. Each character is read in a clone of their own voice, conditioned on one line of their performance fetched from the community wiki the first time they are picked; nothing lifted from the game ships with the plugin. How the engine runs, which device it settles on and why, and what it does when it cannot speak is the [spoken replies](https://esposter.com/docs/infra/claude-interface/spoken-replies) page.
56
56
 
57
57
  A [TypeSafe](https://typesafe.ai) API key, given as an option, turns the pick over to lore — one typed decision over the whole roster, weighing the date, the season's festivals and your moment — instead of the nearest birthday. The key is sensitive, and the variable the SDK itself reads, `TYPESAFE_API_KEY`, is honoured when the option is empty:
58
58
 
@@ -66,31 +66,30 @@ We highly recommend you take a look at the [documentation](https://esposter.com/
66
66
 
67
67
  ### Command reference
68
68
 
69
- Every command is a slash command in Claude Code and the same verb of the plugin's script from any shell; the script form is what a hook, a terminal or another tool runs, and the two never differ:
69
+ Every command is a slash command in Claude Code, `/genshin-` and the verb, which the plugin's hooks module registers at session start, and the same verb of the plugin's script from any shell; the script form is what a hook, a terminal or another tool runs, and the two never differ:
70
70
 
71
71
  ```bash
72
- /genshin-persona:<verb> [argument]
73
- node "<plugin root>/scripts/genshin.ts" <verb> [argument]
72
+ /genshin-<verb> [argument]
73
+ node "<plugin root>/scripts/genshin.mjs" <verb> [argument]
74
74
  ```
75
75
 
76
76
  A `<name>` is a character's, matched whole and ignoring case, in English or as the interface language spells it. A `[language]` is one the `language` verb lists, typed in English or in its own words. A verb given a name the roster has no character by, or an argument outside its set, prints why and exits 1; a verb with no argument where one is optional reports instead of changing anything.
77
77
 
78
- | Verb | Argument | What it does |
79
- | :--------- | :----------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
80
- | `today` | — | Prints the card of this session's character; from a shell, of the pinned character, else of a fresh pick. A pin naming nobody in the roster is reported and ignored. |
81
- | `roster` | — | Every playable character, one line each: name, title, element, region, birthday and the patch that introduced them. |
82
- | `use` | `<name>` | Speaks as one character for this session alone, from that reply on; other sessions and the next start are untouched. From a shell, where no session is running, exits 1. |
83
- | `pin` | `<name>` | Fixes one character for every session until unpinned, and for this session from that reply on. |
84
- | `unpin` | — | Removes the pin; the pick decides again from the next session, and for this session from that reply on. |
85
- | `voice` | `[en \| ja \| ko \| zh]` | Sets up spoken lines in that dub — installing the engine's runtime and weights on the first run, about a gigabyte — ends by speaking one sentence, and writes the hook that reads each reply into user settings, on from the next session. With no dub, reports what is set up. |
86
- | `mute` | — | Stops replies' spoken lines being read aloud. The pick and the card are unaffected. |
87
- | `unmute` | — | Lets replies' spoken lines be read aloud again. |
88
- | `volume` | `<0–100>` | Sets how loud replies are spoken, as a whole number, from the next reply on; `0` is silence the engine is not woken for. |
89
- | `language` | `[language]` | Sets the interface language — every word the plugin writes — and carries the reply language with it. With none, reports what is set and lists the languages on offer. |
90
- | `reply` | `[language]` | Sets the language replies are written in, on its own. With none, reports what is set and whether it was set or cascaded from the interface language. |
91
- | `status` | — | Reports every setting at once and where each value came from. Changes nothing. |
92
- | `setup` | — | Writes the status line and the spinner into user settings, the spinner under this session's character. |
93
- | `teardown` | — | Removes exactly what `setup` and `voice` wrote: the three settings, and the voice's runtime, weights, cached clips and dub. |
78
+ | Verb | Argument | What it does |
79
+ | :--------- | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
+ | `today` | — | Prints the card of this session's character; from a shell, of the pinned character, else of a fresh pick. A pin naming nobody in the roster is reported and ignored. |
81
+ | `roster` | — | Every playable character, one line each: name, title, element, region, birthday and the patch that introduced them. |
82
+ | `use` | `<name>` | Speaks as one character for this session alone, from that reply on; other sessions and the next start are untouched. From a shell, where no session is running, exits 1. |
83
+ | `pin` | `<name>` | Fixes one character for every session until unpinned, and for this session from that reply on. |
84
+ | `unpin` | — | Removes the pin; the pick decides again from the next session, and for this session from that reply on. |
85
+ | `voice` | `[en \| ja \| ko \| zh]` | Sets up spoken lines in that dub — installing the engine's runtime and weights on the first run, about a gigabyte — and ends by speaking one sentence; replies are read from the next reply on. With no dub, reports what is set up. |
86
+ | `mute` | — | Stops replies' spoken lines being read aloud. The pick and the card are unaffected. |
87
+ | `unmute` | — | Lets replies' spoken lines be read aloud again. |
88
+ | `volume` | `<0–100>` | Sets how loud replies are spoken, as a whole number, from the next reply on; `0` is silence the engine is not woken for. |
89
+ | `language` | `[language]` | Sets the interface language — every word the plugin writes — and carries the reply language with it. With none, reports what is set and lists the languages on offer. |
90
+ | `reply` | `[language]` | Sets the language replies are written in, on its own. With none, reports what is set and whether it was set or cascaded from the interface language. |
91
+ | `status` | — | Reports every setting at once and where each value came from. Changes nothing. |
92
+ | `teardown` | — | Removes what `voice` installed: the runtime, the weights, the cached clips and the dub. The picks, the pin and the languages stay; `voice` brings spoken lines back. |
94
93
 
95
94
  Four more are the authoring queues of one more skill, `genshin-author` — script-only and on no menu:
96
95
 
@@ -103,38 +102,38 @@ Four more are the authoring queues of one more skill, `genshin-author` — scrip
103
102
 
104
103
  ### What it ships
105
104
 
106
- | Component | Role |
107
- | :------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
108
- | `hooks/hooks.json` | A session-start hook that picks the character, greets you with its name, what the pick weighed and who is close, and prints its card as context. The hook that reads a reply's spoken lines is a user setting `voice` writes. |
109
- | `output-styles/in-character.md` | The standing rules, forced on while the plugin is enabled: a blockquote line is the character's spoken line, the ask decides how much of a reply that is, and the rest is plain. |
110
- | `skills/<verb>/SKILL.md` | One slash command per verb, `/genshin-persona:<verb>` — the command reference above — yours to invoke. |
111
- | `skills/genshin/SKILL.md` | The model's route from a request in words to one of those verbs; hidden from the menu. |
112
- | `skills/genshin-author/SKILL.md` | How a persona card and its spinner verbs are written, the command that prints a character's own lines to write from, and the two queues. |
113
- | `src/personaCards/` | Authored persona cards, one typed module per character, in our words: how the character speaks for the model, their spinner verbs for you, and the reference line only where an ear chose one. |
114
- | `src/generated/PersonaReferenceMap.ts` | The measured reference line and its likeness per character, one generated map written by the repository's reference selection and never by hand. |
115
- | `runtime/` | The manifest and lockfile of the speech engine's runtime, which the `voice` verb installs into the state directory rather than the plugin carrying it. |
116
- | `src/localizations/` | One authored module per language for the words the data package does not carry: the base Teyvat verbs, each character's spinner gerunds, and every line the verbs print. |
117
- | `scripts/` | The hook entrypoints, the commands' script, the status-line script, the spinner rewrite and the resident synthesizer, TypeScript run directly by node. |
105
+ | Component | Role |
106
+ | :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
107
+ | `hooks/hooks.json` | A session-start hook that picks the character, greets you with its name, what the pick weighed and who is close, and prints its card as context, and the hooks module beside it. |
108
+ | `mod/` | The hooks module: the spinner's verbs and the prompt hint's tips of this session's character, the spoken replies, and one slash command per verb. |
109
+ | `types/index.d.ts` | The state the hooks module publishes — the session's character and its colour — which other plugins draw by. |
110
+ | `output-styles/in-character.md` | The standing rules, forced on while the plugin is enabled: a blockquote line is the character's spoken line, the ask decides how much of a reply that is, and the rest is plain. |
111
+ | `skills/genshin/SKILL.md` | The model's route from a request in words to one of the verbs; hidden from the menu. |
112
+ | `skills/genshin-author/SKILL.md` | How a persona card and its spinner verbs are written, the command that prints a character's own lines to write from, and the two queues. |
113
+ | `src/personaCards/` | Authored persona cards, one typed module per character, in our words: how the character speaks for the model, their spinner verbs for you, and the reference line only where an ear chose one. |
114
+ | `src/generated/PersonaReferenceMap.ts` | The measured reference line and its likeness per character, one generated map written by the repository's reference selection and never by hand. |
115
+ | `runtime/` | The manifest and lockfile of the speech engine's runtime, which the `voice` verb installs into the state directory rather than the plugin carrying it. |
116
+ | `src/localizations/` | One authored module per language for the words the data package does not carry: the base Teyvat verbs, each character's spinner gerunds, and every line the verbs print. |
117
+ | `scripts/` | The session-start hook, the commands' script, the character and spinner the hooks module reads, the status line a person can name in their settings, and the resident synthesizer, each an `.mjs` that registers `tsx` before its TypeScript. |
118
118
 
119
- ### Status line and spinner
119
+ ### Spinner and prompt hint
120
120
 
121
- A plugin cannot ship a status line, spinner verbs or spinner tips, so one command writes them into your user settings, and its twin removes them:
121
+ Nothing to set up: the hooks module turns the spinner's words into the session's character's verbs and the prompt hint into one of their lines under their name, a new one each turn, names the character among the labels at the right of the prompt's footer, and publishes the character's colour, which [`genshin-mods`](https://github.com/Esposter/Esposter/tree/main/packages/genshin-mods) draws its band in. Each session draws its own character, and a switch shows at once. Why each is shaped as it is: the [persona plugin](https://esposter.com/docs/infra/claude-interface/persona-plugin) page.
122
122
 
123
- ```bash
124
- node "<plugin root>/scripts/genshin.ts" setup # or: /genshin-persona:setup
125
- node "<plugin root>/scripts/genshin.ts" teardown
126
- ```
123
+ The plugin writes nothing into your settings, so your own status line stays yours. To have it show the character's nameplate, the name in their colour on a badge of that colour's tone, point it at the script the plugin ships:
127
124
 
128
- The status line prints the session's character in their own colour, on a badge of its tone, from the plugin's state files alone, and the spinner shows the character's own verbs and lines under their name. Both follow a plugin update on the next session, and the spinner follows a change of character on the next session too, since Claude Code reads its keys once per process. Why each is shaped as it is: the [persona plugin](https://esposter.com/docs/infra/claude-interface/persona-plugin) page.
125
+ ```json
126
+ { "statusLine": { "command": "node \"<plugin root>/scripts/statusLine.mjs\"", "type": "command" } }
127
+ ```
129
128
 
130
129
  ### Languages
131
130
 
132
131
  Three settings, and they are not one axis: the **interface language** is every word the plugin writes and the master toggle, the **reply language** is what the model writes in and follows the interface language until set on its own, and the **dub** says whose voice reads a line and is neither, because it comes from a set of four and costs an install:
133
132
 
134
133
  ```bash
135
- node "<plugin root>/scripts/genshin.ts" language japanese # or: /genshin-persona:language japanese
136
- node "<plugin root>/scripts/genshin.ts" reply english # labels in one language, prose in another
137
- node "<plugin root>/scripts/genshin.ts" status # every setting, and where each value came from
134
+ node "<plugin root>/scripts/genshin.mjs" language japanese # or: /genshin-language japanese
135
+ node "<plugin root>/scripts/genshin.mjs" reply english # labels in one language, prose in another
136
+ node "<plugin root>/scripts/genshin.mjs" status # every setting, and where each value came from
138
137
  ```
139
138
 
140
139
  The languages on offer are the data package's, so `language` with no argument lists them, each in its own words beside the word you type for it, and either resolves. What arrives localized for nothing and what is written by hand per language is the [persona plugin](https://esposter.com/docs/infra/claude-interface/persona-plugin) page's.
@@ -148,7 +147,7 @@ Every playable character comes from the game-data dependency — no generated ro
148
147
  `use <name>` makes this session speak as one character from that reply on; `pin <name>` fixes one for every session from the next start and this one at once; `unpin` hands both back to the pick:
149
148
 
150
149
  ```bash
151
- node "<plugin root>/scripts/genshin.ts" use furina # or: /genshin-persona:use furina
150
+ node "<plugin root>/scripts/genshin.mjs" use furina # or: /genshin-use furina
152
151
  ```
153
152
 
154
153
  ### Spoken lines
@@ -156,10 +155,10 @@ node "<plugin root>/scripts/genshin.ts" use furina # or: /genshin-persona:u
156
155
  A reply to an ask of the assistant opens with one blockquote line in the character's voice and may close with one; a reply to an ask of the character — a joke, a hello, an opinion, however much was pasted to form it — is such lines and nothing else, as many as the ask deserves, a list of them included. A hook hands each piece of the reply to a resident synthesizer as it lands, which reads the lines in the order they were written, in the character's cloned voice, while the reply is still being written. `mute` and `unmute` decide whether the hook asks it at all, and so does a `volume` of zero. `volume <number>` is a whole number from 0 to 100, applied as a gain, from the next reply on:
157
156
 
158
157
  ```bash
159
- node "<plugin root>/scripts/genshin.ts" volume 60 # or: /genshin-persona:volume 60
158
+ node "<plugin root>/scripts/genshin.mjs" volume 60 # or: /genshin-volume 60
160
159
  ```
161
160
 
162
- `teardown` removes the runtime, the weights, the cached clips, the dub and the hook along with the status line and spinner settings.
161
+ `teardown` removes the runtime, the weights, the cached clips and the dub, and spoken lines stay silent until `voice` sets them up again.
163
162
 
164
163
  ### Commands
165
164
 
package/hooks/hooks.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "hooks": {
3
3
  "SessionStart": [{ "hooks": [{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pick.mjs\"" }] }]
4
- }
4
+ },
5
+ "modules": ["../mod/register.ts"]
5
6
  }
@@ -0,0 +1,9 @@
1
+ import { GenshinVerb } from "../src/models/GenshinVerb";
2
+
3
+ // The verbs that print a card, which the model answers as from the reply that relays it
4
+ export const CardVerbs: readonly GenshinVerb[] = [
5
+ GenshinVerb.Language,
6
+ GenshinVerb.Pin,
7
+ GenshinVerb.Unpin,
8
+ GenshinVerb.Use,
9
+ ];
@@ -0,0 +1,22 @@
1
+ import { GenshinVerb } from "../src/models/GenshinVerb";
2
+
3
+ // Each verb a person runs, as `/genshin-<verb>`, and what the command menu says of it; the authoring verbs are the
4
+ // `genshin-author` skill's and have no command
5
+ export const VerbDescriptionMap: Partial<Record<GenshinVerb, string>> = {
6
+ [GenshinVerb.Language]:
7
+ "Sets the language every word the plugin writes is in, and the reply language with it; given nothing, reports what is set.",
8
+ [GenshinVerb.Mute]: "Stops replies' spoken lines being read aloud. The pick and the card are unaffected.",
9
+ [GenshinVerb.Pin]: "Fixes one character for every session until unpinned, this one from this reply on.",
10
+ [GenshinVerb.Reply]: "Sets the language replies are written in, on its own; given nothing, reports what is set.",
11
+ [GenshinVerb.Roster]: "Every playable character, one line each.",
12
+ [GenshinVerb.Status]: "Reports every setting the plugin holds and where each came from. Changes nothing.",
13
+ [GenshinVerb.Teardown]:
14
+ "Removes the voice's runtime, weights, references and dub; the picks, the pin and the languages stay.",
15
+ [GenshinVerb.Today]: "The card of this session's character.",
16
+ [GenshinVerb.Unmute]: "Lets replies' spoken lines be read aloud again.",
17
+ [GenshinVerb.Unpin]: "Removes the pin; the pick decides again, for this session from this reply on.",
18
+ [GenshinVerb.Use]: "Speaks as one character for this session alone, from this reply on.",
19
+ [GenshinVerb.Voice]:
20
+ "Sets up spoken replies in the character's own cloned voice, or switches the dub (en, ja, ko or zh); given nothing, reports what is installed.",
21
+ [GenshinVerb.Volume]: "Sets how loud replies are spoken, as a whole number from 0 to 100.",
22
+ };
@@ -0,0 +1,140 @@
1
+ import type { EngineInterface, Register } from "claude-code";
2
+
3
+ import { atom, read, update } from "claude-code";
4
+
5
+ import type { PersonaSpinner, PersonaStatus } from "../types";
6
+
7
+ import { hashString } from "../src/util/hashString";
8
+ import { CardVerbs } from "./CardVerbs";
9
+ import { VerbDescriptionMap } from "./VerbDescriptionMap";
10
+
11
+ // The persona's surfaces drawn by the engine itself, so nothing is written into the person's settings: a spinner and
12
+ // A hint of this session's own character, its colour published for the mods, the spoken replies and one command per
13
+ // Verb. Every read of game data, state or audio is the plugin's node scripts, run as commands, since a hooks module
14
+ // Has no Node
15
+ const characterAtom = atom({ key: "character", plugin: "genshin-persona" } as const, {
16
+ color: "",
17
+ displayName: "",
18
+ name: "",
19
+ });
20
+ const isCharacterRecordedAtom = atom({ key: "isCharacterRecorded", plugin: "genshin-persona" } as const, false);
21
+ const spinnerAtom = atom({ key: "spinner", plugin: "genshin-persona" } as const, { label: "", tips: [], verbs: [] });
22
+ const tipIndexAtom = atom({ key: "tipIndex", plugin: "genshin-persona" } as const, 0);
23
+ // The spinner's lines cost the data package or the wiki on a first read, so its script gets the longest run allowed
24
+ const SPINNER_TIMEOUT_MS = Temporal.Duration.from({ minutes: 10 }).total("milliseconds");
25
+
26
+ const readStateDirectory = async ($: EngineInterface) => {
27
+ const home = (await $.env.get("HOME")) ?? (await $.env.get("USERPROFILE")) ?? "";
28
+ return `${home}/.claude/genshin-persona`;
29
+ };
30
+
31
+ // The session's character, from the record its start hook wrote, else the pin, else the birthday pick; the spinner
32
+ // Is read again only when the character or the language that spells them changed
33
+ const refresh = async ($: EngineInterface) => {
34
+ const sessionId = await $.session.id();
35
+ const status = await $.process.run(["node", `${$.plugin.root}/scripts/status.mjs`], {
36
+ stdin: JSON.stringify({ session_id: sessionId }),
37
+ });
38
+ if (!status.stdout.trim()) return;
39
+ // oxlint-disable-next-line no-restricted-properties -- The plugin's own script prints this shape, with no date in it, and a mod cannot import the shared reviver
40
+ const { character, isRecorded } = JSON.parse(status.stdout) as PersonaStatus;
41
+ await update($, isCharacterRecordedAtom, () => isRecorded);
42
+ const previous = await read($, characterAtom);
43
+ await update($, characterAtom, () => character);
44
+ if (previous.name === character.name && previous.displayName === character.displayName) return;
45
+
46
+ const spinner = await $.process.run(["node", `${$.plugin.root}/scripts/spinner.mjs`, character.name], {
47
+ timeoutMs: SPINNER_TIMEOUT_MS,
48
+ });
49
+ // oxlint-disable-next-line no-restricted-properties -- The plugin's own script prints this shape, with no date in it, and a mod cannot import the shared reviver
50
+ if (spinner.stdout.trim()) await update($, spinnerAtom, () => JSON.parse(spinner.stdout) as PersonaSpinner);
51
+ };
52
+
53
+ // Off the dispatch that asked, so neither the session's start nor a verb's answer waits on a node start
54
+ const refreshLater = ($: EngineInterface) => {
55
+ $.clock.after(0, () => {
56
+ // oxlint-disable-next-line typescript/no-floating-promises -- The engine's timer slot takes no promise
57
+ refresh($);
58
+ });
59
+ };
60
+
61
+ export const register: Register = (on) => {
62
+ on("session.start", async ($, e, next) => {
63
+ await Promise.all(
64
+ Object.entries(VerbDescriptionMap).map(([verb, description]) =>
65
+ $.command.register({ argumentHint: "[name or value]", description, name: `genshin-${verb}` }),
66
+ ),
67
+ );
68
+ refreshLater($);
69
+ return next(e);
70
+ });
71
+
72
+ // Each verb runs the plugin's own script in this session, which the script reads off the environment, and the
73
+ // Character it may have switched is read again behind the answer
74
+ for (const verb of Object.keys(VerbDescriptionMap))
75
+ on("command.run", { command: `genshin-${verb}` }, async ($, e) => {
76
+ const sessionId = await $.session.id();
77
+ const { stderr, stdout } = await $.process.run(
78
+ ["node", `${$.plugin.root}/scripts/genshin.mjs`, verb, ...e.args.split(" ").filter(Boolean)],
79
+ { env: { CLAUDE_CODE_SESSION_ID: sessionId } },
80
+ );
81
+ refreshLater($);
82
+ const text = [stdout, stderr].filter(Boolean).join("\n").trim();
83
+ return CardVerbs.some((cardVerb) => cardVerb === verb)
84
+ ? { context: ["Answer as the card this printed from this reply on."], text }
85
+ : { text };
86
+ });
87
+
88
+ // A `/clear` or a resume carries on under another session id with no `session.start` for it, and the start hook
89
+ // Picks for that id, so the character is unsettled again until its record is read
90
+ on("session.end", async ($, e, next) => {
91
+ if (e.reason === "clear" || e.reason === "resume") await update($, isCharacterRecordedAtom, () => false);
92
+ return next(e);
93
+ });
94
+
95
+ // The character is read again at each turn's start until the start hook's record exists, since the pick may land
96
+ // After the session start's first read
97
+ on("turn.start", async ($, e, next) => {
98
+ if (!(await read($, isCharacterRecordedAtom))) refreshLater($);
99
+ return next(e);
100
+ });
101
+
102
+ on("turn.complete", async ($, e, next) => {
103
+ const result = await next(e);
104
+ if (e.agentId === undefined) await update($, tipIndexAtom, (index) => index + 1);
105
+ return result;
106
+ });
107
+
108
+ // The engine samples a word per turn; the same word always maps to the same verb of this session's character
109
+ on("ui.render", { component: "Spinner" }, async ($, e, next) => {
110
+ const { verbs } = await read($, spinnerAtom);
111
+ if (verbs.length === 0 || e.props.message !== null) return next(e);
112
+ const word = verbs[hashString(e.props.word) % verbs.length] ?? e.props.word;
113
+ return next({ ...e, props: { ...e.props, word } });
114
+ });
115
+
116
+ on("ui.render", { component: "PromptHint" }, async ($, e, next) => {
117
+ const { label, tips } = await read($, spinnerAtom);
118
+ const tip = tips[(await read($, tipIndexAtom)) % Math.max(tips.length, 1)];
119
+ if (!tip || e.props.isDraft) return next(e);
120
+ else return next({ ...e, props: { ...e.props, tail: `${label}: ${tip}` } });
121
+ });
122
+
123
+ // The character named among the footer's mode labels, where a standing word about the session belongs: the status
124
+ // Line is a settings command no plugin can set, and a label added to `modes` leaves every other plugin's in place
125
+ on("ui.render", { component: "SessionMode" }, async ($, e, next) => {
126
+ const { displayName } = await read($, characterAtom);
127
+ if (displayName) return next({ ...e, props: { ...e.props, modes: [...e.props.modes, displayName] } });
128
+ else return next(e);
129
+ });
130
+
131
+ // Every flushed piece of a reply, handed to the speak script as the settings hook handed it; a session with no
132
+ // Voice set up, or muted, pays a file check and no node start
133
+ on("classic.MessageDisplay", async ($, e, next) => {
134
+ const stateDirectory = await readStateDirectory($);
135
+ const isSpeaking =
136
+ (await $.fs.exists(`${stateDirectory}/voice-language`)) && !(await $.fs.exists(`${stateDirectory}/muted`));
137
+ if (isSpeaking) await $.process.run(["node", `${$.plugin.root}/scripts/speak.mjs`], { stdin: JSON.stringify(e) });
138
+ return next(e);
139
+ });
140
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genshin-persona",
3
- "version": "3.9.0",
3
+ "version": "4.1.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",
@@ -24,19 +24,24 @@
24
24
  },
25
25
  "files": [
26
26
  ".claude-plugin",
27
+ ".mcp.json",
27
28
  "hooks",
28
29
  "output-styles",
29
30
  "runtime",
30
31
  "scripts",
31
32
  "skills",
33
+ "mod",
32
34
  "src",
33
- "!src/**/*.test.ts"
35
+ "types",
36
+ "!**/*.test.ts",
37
+ "!.claude-plugin/types"
34
38
  ],
35
39
  "type": "module",
36
40
  "imports": {
37
41
  "#src/*": "./src/*.ts",
38
42
  "#src/*.json": "./src/*.json",
39
- "#scripts/*": "./scripts/*.ts"
43
+ "#scripts/*": "./scripts/*.ts",
44
+ "#types": "./types/index.d.ts"
40
45
  },
41
46
  "publishConfig": {
42
47
  "access": "public"
@@ -50,6 +55,7 @@
50
55
  "typecheck": "tsc"
51
56
  },
52
57
  "dependencies": {
58
+ "@modelcontextprotocol/sdk": "^1.32.1",
53
59
  "@typesafe-ai/sdk": "^0.6.0",
54
60
  "@wasm-audio-decoders/ogg-vorbis": "^0.1.20",
55
61
  "citty": "^0.2.2",
@@ -57,7 +63,7 @@
57
63
  "tsx": "^4.23.15"
58
64
  },
59
65
  "engines": {
60
- "node": ">=26.10.0"
66
+ "node": ">=26.11.1"
61
67
  },
62
- "gitHead": "f9f6208c35e6cff4ed33910623c798cd88f09301"
68
+ "gitHead": "68de4c22fa7591bf6f6cc0fab2c9a906d8c4d71a"
63
69
  }
@@ -6,7 +6,7 @@
6
6
  "": {
7
7
  "name": "genshin-persona-runtime",
8
8
  "dependencies": {
9
- "@huggingface/transformers": "^4.3.0"
9
+ "@huggingface/transformers": "^4.3.1"
10
10
  }
11
11
  },
12
12
  "node_modules/@emnapi/runtime": {
@@ -35,9 +35,9 @@
35
35
  "license": "Apache-2.0"
36
36
  },
37
37
  "node_modules/@huggingface/transformers": {
38
- "version": "4.3.0",
39
- "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.3.0.tgz",
40
- "integrity": "sha512-fL1A/WUZwouPrOlYxU5dzIwD2T5J781JiB2jDR8bFe5DwCj0Gfudq+NEXCMno49kQgajHA7xQkrRLJlqG1veEA==",
38
+ "version": "4.3.1",
39
+ "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.3.1.tgz",
40
+ "integrity": "sha512-5+voSeUCFK6PCKlY15dPhNOKq3/zOTxDN7oC+Wy9dFb1QZ0TSQomZRRy7URRK8YRuUoE9fGMQTTPC5d/pWBJhA==",
41
41
  "license": "Apache-2.0",
42
42
  "dependencies": {
43
43
  "@huggingface/jinja": "^0.5.10",
@@ -653,9 +653,9 @@
653
653
  "license": "BSD-3-Clause"
654
654
  },
655
655
  "node_modules/@types/node": {
656
- "version": "26.6.3",
657
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz",
658
- "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==",
656
+ "version": "26.6.4",
657
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz",
658
+ "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==",
659
659
  "license": "MIT",
660
660
  "dependencies": {
661
661
  "undici-types": "~8.9.0"
@@ -3,6 +3,6 @@
3
3
  "private": true,
4
4
  "description": "The persona plugin's speech engine runtime, installed by its voice verb into the plugin's state directory rather than shipped with the plugin.",
5
5
  "dependencies": {
6
- "@huggingface/transformers": "^4.3.0"
6
+ "@huggingface/transformers": "^4.3.1"
7
7
  }
8
8
  }
@@ -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/channel");
@@ -0,0 +1,98 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { AddressInfo } from "node:net";
3
+
4
+ import { checkIsChannelRequestAllowed } from "#src/services/checkIsChannelRequestAllowed";
5
+ import { createChannelPage } from "#src/services/createChannelPage";
6
+ import { readChannelLine } from "#src/services/readChannelLine";
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
10
+ import { randomBytes } from "node:crypto";
11
+ import { createServer } from "node:http";
12
+
13
+ // The chat window's MCP server, started with the session over stdio. It serves the page on the loopback under a token
14
+ // Printed once to the terminal, takes typed lines into the session as channel events, and offers the reply tool that
15
+ // Sends the plain half of an answer back to the page. The character's spoken lines need no forwarding: the message
16
+ // Display hook already reads them to the viewer
17
+ const token = randomBytes(16).toString("hex");
18
+ const replyStreams = new Set<ServerResponse>();
19
+
20
+ // The low-level Server keeps the raw request handlers and the channel notification, which McpServer's tool API does not expose
21
+ // oxlint-disable-next-line typescript/no-deprecated -- see the comment above
22
+ const mcp = new Server(
23
+ { name: "genshin-persona", version: "4.0.0" },
24
+ {
25
+ capabilities: { experimental: { "claude/channel": {} }, tools: {} },
26
+ instructions:
27
+ 'Lines typed at the desk arrive as <channel source="genshin-persona" sender="desk">. The character\'s spoken lines already reach the viewer, so do not forward them. When a desk line asks something, also call the reply tool with the plain half of the answer; the desk reads it on the chat page.',
28
+ },
29
+ );
30
+
31
+ mcp.setRequestHandler(ListToolsRequestSchema, () => ({
32
+ tools: [
33
+ {
34
+ description: "Show the plain half of an answer to a line typed at the desk on the chat page",
35
+ inputSchema: { properties: { text: { type: "string" } }, required: ["text"], type: "object" },
36
+ name: "reply",
37
+ },
38
+ ],
39
+ }));
40
+
41
+ mcp.setRequestHandler(CallToolRequestSchema, (request) => {
42
+ // oxlint-disable-next-line error-handling/no-bare-error -- an unlisted tool name is the SDK's own error to report to the caller
43
+ if (request.params.name !== "reply") throw new Error(`Unknown tool ${request.params.name}`);
44
+
45
+ // oxlint-disable-next-line typescript/no-base-to-string -- the arguments are the caller's own JSON, and its text is the reply as sent
46
+ const text = String(request.params.arguments?.text ?? "");
47
+ for (const stream of replyStreams) stream.write(`event: reply\ndata: ${JSON.stringify({ text })}\n\n`);
48
+ return { content: [{ text: "sent", type: "text" }] };
49
+ });
50
+
51
+ const writeEnded = (response: ServerResponse, statusCode: number) => {
52
+ response.writeHead(statusCode).end();
53
+ };
54
+
55
+ const handleSay = (request: IncomingMessage, response: ServerResponse) => {
56
+ // Decoded as the stream arrives, so a character split across two chunks is joined rather than read as two bad bytes
57
+ request.setEncoding("utf8");
58
+ let body = "";
59
+ request.on("data", (chunk: string) => {
60
+ body += chunk;
61
+ });
62
+ request.on("end", () => {
63
+ const line = readChannelLine(body);
64
+ if (line === undefined) return writeEnded(response, 400);
65
+
66
+ mcp.notification({ method: "notifications/claude/channel", params: { content: line, meta: { sender: "desk" } } });
67
+ writeEnded(response, 204);
68
+ });
69
+ };
70
+
71
+ const handleEvents = (request: IncomingMessage, response: ServerResponse) => {
72
+ response.writeHead(200, { "cache-control": "no-cache", "content-type": "text/event-stream" });
73
+ response.write(": open\n\n");
74
+ replyStreams.add(response);
75
+ request.on("close", () => replyStreams.delete(response));
76
+ };
77
+
78
+ const http = createServer((request, response) => {
79
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
80
+ const [, presentedToken = "", route = ""] = url.pathname.split("/");
81
+ if (!checkIsChannelRequestAllowed(request.socket.remoteAddress, presentedToken, token))
82
+ return writeEnded(response, 404);
83
+
84
+ if (route === "" && request.method === "GET") {
85
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(createChannelPage());
86
+ return;
87
+ }
88
+ if (route === "events" && request.method === "GET") return handleEvents(request, response);
89
+ if (route === "say" && request.method === "POST") return handleSay(request, response);
90
+ writeEnded(response, 404);
91
+ });
92
+
93
+ http.listen(0, "127.0.0.1", () => {
94
+ const { port } = http.address() as AddressInfo;
95
+ console.error(`Chat into the session: http://127.0.0.1:${port}/${token}/`);
96
+ });
97
+
98
+ await mcp.connect(new StdioServerTransport());
@@ -4,8 +4,8 @@ import { readInterfaceLanguage } from "#src/services/readInterfaceLanguage";
4
4
  import { readLocalization } from "#src/services/readLocalization";
5
5
  import { runMain } from "citty";
6
6
 
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
7
+ // Every line the plugin prints is in the interface language, so a verb it does not know, or none, or a request for
8
+ // Help, is answered with its own usage line, and exits before citty would add an English one of its own
9
9
  await runMain(genshinCommand, {
10
10
  showUsage: async () => {
11
11
  const { strings } = await readLocalization(readInterfaceLanguage());