@thijshakkenberg/valkyrie-mom-mcp 2.5.0 → 2.5.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valkyrie-mom",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "AI-assisted Mansions of Madness scenario editor for Valkyrie",
5
5
  "author": {
6
6
  "name": "Thijs Hakkenberg"
@@ -1 +1 @@
1
- {"version":3,"file":"localization-io.d.ts","sourceRoot":"","sources":["../../src/io/localization-io.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,CA4CrE;AAcD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAexF"}
1
+ {"version":3,"file":"localization-io.d.ts","sourceRoot":"","sources":["../../src/io/localization-io.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,CA4CrE;AAcD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAiBxF"}
@@ -77,7 +77,9 @@ export function writeLocalization(language, entries) {
77
77
  const lines = [`.,${language}`];
78
78
  for (const [key, rawValue] of entries) {
79
79
  const value = rawValue.replace(/\r\n|\r|\n/g, '\\n');
80
- if (value.includes('"') && !value.includes(TRIPLE)) {
80
+ // ParseEntry strips a pair of outer quotes even inside |||, so |||"dialogue"||| would lose them
81
+ const quotedAtBothEnds = value.length > 1 && value.startsWith('"') && value.endsWith('"');
82
+ if (value.includes('"') && !value.includes(TRIPLE) && !quotedAtBothEnds) {
81
83
  lines.push(`${key},${TRIPLE}${value}${TRIPLE}`);
82
84
  }
83
85
  else if (value.includes('"') || value.includes(TRIPLE) || value.includes('\\n') || value.includes(',')) {
@@ -1 +1 @@
1
- {"version":3,"file":"localization-io.js","sourceRoot":"","sources":["../../src/io/localization-io.ts"],"names":[],"mappings":"AAKA,uEAAuE;AACvE,MAAM,MAAM,GAAG,KAAK,CAAC;AAErB;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACrE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE1C,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAE1F,8DAA8D;IAC9D,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,OAAO,GAAa,EAAE,CAAC;IAC3B,IAAI,UAAU,GAAG,KAAK,CAAC;IACvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC;QACrC,IAAI,OAAO,IAAI,IAAI,KAAK,EAAE;YAAE,SAAS;QACrC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEnB,IAAI,GAAY,CAAC;QACjB,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,EAAE,CAAC,CAAC,IAAI,UAAU,EAAE,CAAC;YAC3D,UAAU,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC9C,GAAG,GAAG,CAAC,UAAU,CAAC;QACpB,CAAC;aAAM,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5C,yEAAyE;YACzE,GAAG,GAAG,OAAO,CAAC;QAChB,CAAC;aAAM,CAAC;YACN,gEAAgE;YAChE,GAAG,GAAG,CAAC,OAAO,CAAC;QACjB,CAAC;QAED,IAAI,GAAG,EAAE,CAAC;YACR,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;YAClC,OAAO,GAAG,EAAE,CAAC;YACb,UAAU,GAAG,KAAK,CAAC;QACrB,CAAC;IACH,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAE1D,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,QAAQ,KAAK,CAAC,CAAC;YAAE,SAAS;QAC9B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC;AAED,qGAAqG;AACrG,SAAS,UAAU,CAAC,GAAW;IAC7B,IAAI,KAAK,GAAG,GAAG,CAAC;IAChB,IAAI,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACnG,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5D,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,OAA4B;IAC9E,MAAM,KAAK,GAAa,CAAC,KAAK,QAAQ,EAAE,CAAC,CAAC;IAE1C,KAAK,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,OAAO,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;QACrD,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACnD,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,MAAM,GAAG,KAAK,GAAG,MAAM,EAAE,CAAC,CAAC;QAClD,CAAC;aAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACzG,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,KAAK,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACtD,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACjC,CAAC"}
1
+ {"version":3,"file":"localization-io.js","sourceRoot":"","sources":["../../src/io/localization-io.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thijshakkenberg/valkyrie-mom-mcp",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "MCP server for AI-assisted Mansions of Madness scenario creation with Valkyrie",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -70,20 +70,22 @@ A translated copy goes in a language folder: `English/img/X.jpg` or `img/English
70
70
 
71
71
  ## Pattern: intro cutscene
72
72
 
73
- This is a full-screen opening with the picture on the left and the story on the right, as community scenarios such as EMET do it. `EventStart` becomes hidden and only puts the UI on screen. The Begin button's event removes the UI and sets up the board.
73
+ This is a full-screen opening laid out like the official app's: a framed picture on the left and the story on the right, under the app's own ornament. `EventStart` becomes hidden and only puts the UI on screen. The Begin button's event removes the UI and sets up the board.
74
74
 
75
75
  ```
76
- set_quest_config({ image: "img/Intro.jpg" }) # same picture as the cover
76
+ set_quest_config({ image: "img/Intro.jpg" }) # same picture as the cover (a square cover/intro preset)
77
77
 
78
- upsert_ui("UIIntroBG", { image: "ImageCutsceneBG", xposition: "-0.06", yposition: "0", size: "1", vunits: "True", display: "false", buttons: "0" })
79
- upsert_ui("UIIntroPicture", { image: "img/Intro.jpg", xposition: "-0.45", yposition: "0", size: "0.9", vunits: "True", display: "false", buttons: "0" })
80
- upsert_ui("UIIntroText", { xposition: "0.35", yposition: "0.05", size: "0.95", textsize: "0.8", textaspect: "0.95", textAlignment: "TOP", richText: "True", vunits: "True", display: "false", buttons: "0" })
81
- upsert_ui("UIIntroButtonFrame", { image: "ImageInvestigatorSelectTitle", xposition: "0.4", yposition: "0", size: "0.1", valign: "bottom", vunits: "True", display: "false", buttons: "0" })
82
- upsert_ui("UIIntroButton", { xposition: "0.4", yposition: "0.04", size: "1", textcolor: "black", textAlignment: "BOTTOM", richText: "True", valign: "bottom", vunits: "True", display: "false", buttons: "1", event1: "EventSetup" }) # button LAST
78
+ upsert_ui("UIIntroBG", { image: "ImageCutsceneBG", xposition: "0", yposition: "0", size: "1", vunits: "True", display: "false", buttons: "0" })
79
+ upsert_ui("UIIntroPicture", { image: "img/Intro.jpg", xposition: "-0.28", yposition: "0", size: "0.80", vunits: "True", display: "false", buttons: "0" })
80
+ upsert_ui("UIIntroFrame", { image: "ImageCutsceneFrame", xposition: "-0.28", yposition: "0", size: "0.88", vunits: "True", display: "false", buttons: "0" })
81
+ upsert_ui("UIIntroTopper", { image: "ImageCutsceneTextTopper", xposition: "0.47", yposition: "-0.364", size: "0.031", vunits: "True", display: "false", buttons: "0" })
82
+ upsert_ui("UIIntroText", { xposition: "0.47", yposition: "0.015", size: "0.69", textaspect: "0.725", textsize: "0.7", textAlignment: "TOP", richText: "True", vunits: "True", display: "false", buttons: "0" })
83
+ upsert_ui("UIIntroButtonFrame", { image: "ImageInvestigatorSelectTitle", xposition: "0.47", yposition: "0", size: "0.078", valign: "bottom", vunits: "True", display: "false", buttons: "0" })
84
+ upsert_ui("UIIntroButton", { xposition: "0.47", yposition: "0", size: "0.078", textaspect: "7", textcolor: "black", textAlignment: "CENTER", richText: "True", valign: "bottom", vunits: "True", display: "false", buttons: "1", event1: "EventSetup" }) # button LAST
83
85
 
84
86
  upsert_event("EventStart", { trigger: "EventStart", display: "false", buttons: "0",
85
- add: "UIIntroBG UIIntroPicture UIIntroText UIIntroButtonFrame UIIntroButton" })
86
- upsert_event("EventSetup", { ..., remove: "UIIntroBG UIIntroPicture UIIntroText UIIntroButtonFrame UIIntroButton", add: "<first tile and tokens>" })
87
+ add: "UIIntroBG UIIntroPicture UIIntroFrame UIIntroTopper UIIntroText UIIntroButtonFrame UIIntroButton" })
88
+ upsert_event("EventSetup", { ..., remove: "UIIntroBG UIIntroPicture UIIntroFrame UIIntroTopper UIIntroText UIIntroButtonFrame UIIntroButton", add: "<first tile and tokens>" })
87
89
 
88
90
  set_localization({
89
91
  "UIIntroText.uitext": "<align=\"left\"><b>Title</b>\n\nThe story so far...</align>",
@@ -92,6 +94,15 @@ set_localization({
92
94
  })
93
95
  ```
94
96
 
97
+ Why these numbers (measured from the imported assets and Valkyrie's `Quest.UI` layout code):
98
+
99
+ - **One centred group, 1.44 screen heights wide** (frame 0.88, gap 0.06, text 0.50), so it fits every screen from 3:2 to 2:1 and sits in the middle of wider ones. Anchoring the picture to the left edge and the text to the right edge instead pulls them apart on wide screens. The older pattern (picture at `-0.45` size 0.9, text at `0.35` with `textaspect` 0.95) put the text over the picture's right part on every screen, cut the picture off at the left edge on 16:9 and narrower screens, and ran the text off the right edge narrower than 16:10.
100
+ - **`ImageCutsceneBG` stays centred.** It is 2:1, so centred it covers screens up to 2:1. It has a transparent portrait window (where the official app plays its video) at −0.61…−0.13 screen heights from the centre, and the frame and picture at `-0.28` cover it. Moving the background leaves the window showing beside the frame, or uncovers the screen edges.
101
+ - **`ImageCutsceneFrame`'s opening is 84% wide and 91% tall.** A square picture at 0.80 fills it, and its sides tuck under the frame. The frame's bottom edge is partly transparent between the corners, so a frame smaller than 0.88 lets the background's window show below it.
102
+ - **The text column is 0.50 wide and 0.69 tall** (`size` is the height, `textaspect` the width/height ratio), starting under the ornament and below Valkyrie's Menu button. Font size is `textsize` × screen height / 30: three short paragraphs fit at 0.7, longer text needs 0.6. Keep the intro short anyway, as the official scenarios do.
103
+ - **The button's own text box matches its frame** (`size` 0.078, `textaspect` 7). A text element with `size` 1 is a full screen-height square, all of it clickable.
104
+ - Valkyrie can't play video or reveal paragraphs on a timer. For more pictures or a longer story, chain cutscene screens: the button of one adds the next and removes itself.
105
+
95
106
  ## Pattern: a picture during play
96
107
 
97
108
  The event that shows the dialog also adds the picture. It sits centred just below the dialog, which Valkyrie draws at the top of the screen. Whatever runs next removes it again, so remove it on **every** button: a button with no event of its own needs a small hidden event that does the removal.
@@ -109,7 +120,8 @@ upsert_event("EventHideLetter", { display: "false", buttons: "0", remove: "UILet
109
120
  ## UI positioning (how Valkyrie computes it)
110
121
 
111
122
  - With `vunits=True`, the unit is **screen heights**. `size` is the element's height, and its width follows the image's aspect ratio.
112
- - Without `halign`/`valign`, an element is **centred**: `xposition`/`yposition` shift it from the screen centre. Positive values go right and down, so `0,0` is dead centre and `-0.45` moves the element left.
123
+ - Without `halign`/`valign`, an element is **centred**: `xposition`/`yposition` shift the element's centre from the screen centre. Positive values go right and down, so `0,0` is dead centre.
124
+ - A 16:10 screen spans ±0.8 screen heights from the centre, 16:9 ±0.89 and 3:2 ±0.75. An element at `x` with width `w` reaches from `x - w/2` to `x + w/2`: check both against the narrowest screen you support (3:2 is a safe minimum), and check that neighbours don't overlap.
113
125
  - `halign=left|right` and `valign=top|bottom` anchor the element to that screen edge instead, and the position becomes the distance from that edge.
114
126
  - Elements draw in the order they were added. Add clickable elements last.
115
127
 
@@ -38,6 +38,8 @@ Run `narration_status`. If the engine is missing, `narration_status install=true
38
38
  | `{ffg:MONSTER_DEEP_ONE}`, `{c:TileStudy}` | "deep one", "Study" |
39
39
  | `\n` | A short pause; blank lines separate paragraphs |
40
40
 
41
+ **Keep it short, like the official scenarios' intros.** `af_nicole` at speed 0.9 reads about 85 words a minute, pauses included, so a 30-second clip is 40–45 words. A three-paragraph intro cutscene (165 words) came out at 98 seconds. It sounded good, but it was like listening to an audiobook, and the clip keeps playing after the players click on. The screen can keep the full text: pass `text` with a few of its sentences (the hook, the stakes) and narrate those.
42
+
41
43
  Write narrated text for the ear: short sentences, no "as shown", no numbers the players must act on. When the dialog text doesn't read well aloud, pass `text` with a spoken version. The dialog stays as it is.
42
44
 
43
45
  ## Workflow
@@ -54,7 +56,7 @@ Write narrated text for the ear: short sentences, no "as shown", no numbers the
54
56
 
55
57
  ## Choosing what to narrate
56
58
 
57
- Good candidates: the introduction (`EventStart` or the first story event), discovering a key clue, a letter or diary page read aloud (pair it with a handout picture from `/artwork`), the monster reveal, and the endings (win, lose, `Eliminated`).
59
+ Good candidates: the introduction (`EventStart` or the first story event; for an intro cutscene built from UI elements, put the clip on the event that adds them), discovering a key clue, a letter or diary page read aloud (pair it with a handout picture from `/artwork`), the monster reveal, and the endings (win, lose, `Eliminated`).
58
60
 
59
61
  Skip: placement instructions, tests and their pass/fail results, mythos events that repeat every round (the same clip gets tiresome), and anything shown while players are busy with the board.
60
62
 
@@ -23,7 +23,7 @@ Always use vertical units: sizes and positions are then fractions of the **scree
23
23
  | `image` | A built-in image (`ImageCutsceneBG`, `ImageInvestigatorSelectTitle`, ...) or a file in the scenario folder (`img/Letter.jpg`) |
24
24
  | `textsize`, `textaspect`, `textcolor`, `textAlignment`, `richText` | Text styling for elements with `<name>.uitext` |
25
25
 
26
- For example, `xposition=0 yposition=0 size=1` fills the middle of the screen, and `xposition=-0.45 size=0.9` puts a square picture on the left half. On a 16:9 screen the visible range is roughly ±0.89 horizontally and ±0.5 vertically.
26
+ For example, `xposition=0 yposition=0 size=1` fills the middle of the screen, and `xposition=-0.28 size=0.8` puts a square picture left of centre (it spans −0.68…+0.12). The visible range is ±0.5 vertically and, horizontally, ±0.75 on a 3:2 screen, ±0.8 on 16:10 and ±0.89 on 16:9. Check an element's edges (`x ± width/2`) against the narrowest screen you support, and check that neighbours don't overlap.
27
27
 
28
28
  ### Layering Order
29
29
 
@@ -45,7 +45,7 @@ While **any** UI element is on the board, Valkyrie's next phase button does noth
45
45
 
46
46
  ## Prologue / Intro Cutscene
47
47
 
48
- `/artwork` has the complete, tested pattern. It uses a full-screen `ImageCutsceneBG` backdrop, the scenario's own picture on the left (`xposition=-0.45`), the story on the right (`xposition=0.35`, `textAlignment=TOP`), and a Begin button anchored to the bottom (`valign=bottom`) whose event removes the UI and sets up the board. `/artwork` also covers showing handouts and scene pictures during play, and generating the pictures with ComfyUI.
48
+ `/artwork` has the complete pattern, laid out like the official app's: the `ImageCutsceneBG` backdrop, the scenario's picture in the app's `ImageCutsceneFrame` on the left, the story under the `ImageCutsceneTextTopper` ornament on the right, and a Begin button anchored to the bottom (`valign=bottom`) whose event removes the UI and sets up the board. It is one centred group that fits screens from 3:2 to 2:1. `/artwork` also covers showing handouts and scene pictures during play, and generating the pictures with ComfyUI.
49
49
 
50
50
 
51
51
  ## Interactive Journal
@@ -87,7 +87,9 @@ export function writeLocalization(language: string, entries: Map<string, string>
87
87
 
88
88
  for (const [key, rawValue] of entries) {
89
89
  const value = rawValue.replace(/\r\n|\r|\n/g, '\\n');
90
- if (value.includes('"') && !value.includes(TRIPLE)) {
90
+ // ParseEntry strips a pair of outer quotes even inside |||, so |||"dialogue"||| would lose them
91
+ const quotedAtBothEnds = value.length > 1 && value.startsWith('"') && value.endsWith('"');
92
+ if (value.includes('"') && !value.includes(TRIPLE) && !quotedAtBothEnds) {
91
93
  lines.push(`${key},${TRIPLE}${value}${TRIPLE}`);
92
94
  } else if (value.includes('"') || value.includes(TRIPLE) || value.includes('\\n') || value.includes(',')) {
93
95
  lines.push(`${key},"${value.replace(/"/g, '""')}"`);