iobroker.assistant 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/admin/assistant.svg +19 -0
- package/admin/custom/.vite/manifest.json +169 -0
- package/admin/custom/assets/Components-DcnuVFM6.js +1 -0
- package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-DogEjv3U.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-Dw5TkU1t.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-BEFjTs9S.js +1 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-C4r3udVz.js +1243 -0
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-DlOBHPL0.js +1494 -0
- package/admin/custom/assets/hostInit-DKMI-sLp.js +1 -0
- package/admin/custom/assets/index-B8wW0kQx.js +1 -0
- package/admin/custom/assets/lib-DpE8sc4h.js +3 -0
- package/admin/custom/assets/mf-entry-bootstrap-0-e7fa582d.js +36 -0
- package/admin/custom/assets/rolldown-runtime-C0FnF6B9.js +1 -0
- package/admin/custom/assets/virtualExposes-DOTO22UK.js +1 -0
- package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomAssistant__customComponents_js-CQa59yVi.js +1 -0
- package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__ConfigCustomAssistant__customComponents_js-9g2Fj8tk.js +2 -0
- package/admin/custom/assets/virtual_mf-exposes___mfe_internal__ConfigCustomAssistant__customComponents_js-BTUQTbYo.js +2 -0
- package/admin/custom/assets/vite-preload-helper-B7qeedMF.js +1 -0
- package/admin/custom/customComponents.js +1 -0
- package/admin/custom/customComponents.ssr.js +1 -0
- package/admin/custom/i18n/de.json +58 -0
- package/admin/custom/i18n/en.json +58 -0
- package/admin/custom/i18n/es.json +34 -0
- package/admin/custom/i18n/fr.json +34 -0
- package/admin/custom/i18n/it.json +34 -0
- package/admin/custom/i18n/nl.json +34 -0
- package/admin/custom/i18n/pl.json +34 -0
- package/admin/custom/i18n/pt.json +34 -0
- package/admin/custom/i18n/ru.json +34 -0
- package/admin/custom/i18n/uk.json +34 -0
- package/admin/custom/i18n/zh-cn.json +34 -0
- package/admin/custom/index.html +32 -0
- package/admin/custom/mf-manifest.json +1 -0
- package/admin/custom/mf-stats.json +1 -0
- package/admin/i18n/de.json +130 -0
- package/admin/i18n/en.json +130 -0
- package/admin/i18n/es.json +106 -0
- package/admin/i18n/fr.json +106 -0
- package/admin/i18n/it.json +106 -0
- package/admin/i18n/nl.json +106 -0
- package/admin/i18n/pl.json +106 -0
- package/admin/i18n/pt.json +106 -0
- package/admin/i18n/ru.json +106 -0
- package/admin/i18n/uk.json +106 -0
- package/admin/i18n/zh-cn.json +106 -0
- package/admin/jsonConfig.json +743 -0
- package/admin/sounds/alarm.wav +0 -0
- package/admin/sounds/timer.wav +0 -0
- package/build/lib/alarms.js +281 -0
- package/build/lib/alarms.js.map +1 -0
- package/build/lib/context.js +52 -0
- package/build/lib/context.js.map +1 -0
- package/build/lib/credentials.js +121 -0
- package/build/lib/credentials.js.map +1 -0
- package/build/lib/llm.js +303 -0
- package/build/lib/llm.js.map +1 -0
- package/build/lib/localLlm.js +221 -0
- package/build/lib/localLlm.js.map +1 -0
- package/build/lib/memory.js +190 -0
- package/build/lib/memory.js.map +1 -0
- package/build/lib/nlu.js +929 -0
- package/build/lib/nlu.js.map +1 -0
- package/build/lib/timers.js +245 -0
- package/build/lib/timers.js.map +1 -0
- package/build/lib/tools.js +299 -0
- package/build/lib/tools.js.map +1 -0
- package/build/lib/voice/aws.js +102 -0
- package/build/lib/voice/aws.js.map +1 -0
- package/build/lib/voice/azure.js +144 -0
- package/build/lib/voice/azure.js.map +1 -0
- package/build/lib/voice/download.js +76 -0
- package/build/lib/voice/download.js.map +1 -0
- package/build/lib/voice/engines.js +89 -0
- package/build/lib/voice/engines.js.map +1 -0
- package/build/lib/voice/lang.js +33 -0
- package/build/lib/voice/lang.js.map +1 -0
- package/build/lib/voice/piper.js +191 -0
- package/build/lib/voice/piper.js.map +1 -0
- package/build/lib/voice/protocol.js +35 -0
- package/build/lib/voice/protocol.js.map +1 -0
- package/build/lib/voice/stt.js +116 -0
- package/build/lib/voice/stt.js.map +1 -0
- package/build/lib/voice/tts.js +36 -0
- package/build/lib/voice/tts.js.map +1 -0
- package/build/lib/voice/voiceServer.js +249 -0
- package/build/lib/voice/voiceServer.js.map +1 -0
- package/build/lib/voice/vosk.js +260 -0
- package/build/lib/voice/vosk.js.map +1 -0
- package/build/lib/voice/wyoming.js +320 -0
- package/build/lib/voice/wyoming.js.map +1 -0
- package/build/lib/weather.js +423 -0
- package/build/lib/weather.js.map +1 -0
- package/build/main.js +3152 -0
- package/build/main.js.map +1 -0
- package/io-package.json +528 -0
- package/package.json +68 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Long-term memory (roadmap #6) — durable facts the assistant remembers across sessions/restarts
|
|
4
|
+
* ("meine Tochter heißt Lena", "das WLAN-Passwort steht im Flur"). Storage is ioBroker-first: this store
|
|
5
|
+
* holds the facts in memory and emits an `onChange` callback; `main.ts` mirrors them into `memory.*` states
|
|
6
|
+
* (+ one editable object per entry) and, before each cloud-LLM call, injects them compactly into the system
|
|
7
|
+
* prompt (retrieval = "inject all", capped — enough for the handful of household facts a home assistant
|
|
8
|
+
* accumulates; semantic search can be added later without touching the storage format).
|
|
9
|
+
*
|
|
10
|
+
* The store itself is transport-agnostic and testable (CRUD + dedup + caps + persistence round-trip).
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.MemoryStore = void 0;
|
|
14
|
+
exports.buildMemoryPrompt = buildMemoryPrompt;
|
|
15
|
+
/** In-memory long-term fact store with dedup, size caps and JSON persistence (no scheduling). */
|
|
16
|
+
class MemoryStore {
|
|
17
|
+
opts;
|
|
18
|
+
entries = new Map();
|
|
19
|
+
seq = 0;
|
|
20
|
+
now;
|
|
21
|
+
maxEntries;
|
|
22
|
+
maxTextLength;
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
this.opts = opts;
|
|
25
|
+
this.now = opts.now || Date.now;
|
|
26
|
+
this.maxEntries = opts.maxEntries ?? 100;
|
|
27
|
+
this.maxTextLength = opts.maxTextLength ?? 500;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Remember a fact. If `key` is given and already exists (or, without a key, an identical text exists),
|
|
31
|
+
* the existing entry is updated instead of duplicated. Enforces the size cap by dropping the oldest.
|
|
32
|
+
* Returns the stored entry, or null for empty text.
|
|
33
|
+
*/
|
|
34
|
+
add(o) {
|
|
35
|
+
const text = (o.text || '').trim().slice(0, this.maxTextLength);
|
|
36
|
+
if (!text) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const key = (o.key || '').trim().toLowerCase();
|
|
40
|
+
const source = o.source || '';
|
|
41
|
+
const now = this.now();
|
|
42
|
+
// Dedup: same key, or (keyless) identical text → update in place.
|
|
43
|
+
const existing = key
|
|
44
|
+
? [...this.entries.values()].find(e => e.key === key)
|
|
45
|
+
: [...this.entries.values()].find(e => e.text.toLowerCase() === text.toLowerCase());
|
|
46
|
+
if (existing) {
|
|
47
|
+
existing.text = text;
|
|
48
|
+
existing.source = source || existing.source;
|
|
49
|
+
existing.updatedAt = now;
|
|
50
|
+
this.emitChange();
|
|
51
|
+
return existing;
|
|
52
|
+
}
|
|
53
|
+
const entry = { id: this.genId(now), text, key, source, createdAt: now, updatedAt: now };
|
|
54
|
+
this.entries.set(entry.id, entry);
|
|
55
|
+
this.enforceCap();
|
|
56
|
+
this.opts.log?.debug(`memory added: ${entry.id}${key ? ` [${key}]` : ''} "${text}"`);
|
|
57
|
+
this.emitChange();
|
|
58
|
+
return entry;
|
|
59
|
+
}
|
|
60
|
+
/** Update an entry's text by id. Returns true if it existed. */
|
|
61
|
+
update(id, text) {
|
|
62
|
+
const e = this.entries.get(id);
|
|
63
|
+
if (!e) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const t = (text || '').trim().slice(0, this.maxTextLength);
|
|
67
|
+
if (!t) {
|
|
68
|
+
return this.forgetById(id);
|
|
69
|
+
}
|
|
70
|
+
e.text = t;
|
|
71
|
+
e.updatedAt = this.now();
|
|
72
|
+
this.emitChange();
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
/** Forget entries matching an id or a key. Returns the number removed. */
|
|
76
|
+
forget(idOrKey) {
|
|
77
|
+
const needle = (idOrKey || '').trim().toLowerCase();
|
|
78
|
+
if (!needle) {
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
const victims = [...this.entries.values()].filter(e => e.id.toLowerCase() === needle || (e.key && e.key === needle));
|
|
82
|
+
for (const v of victims) {
|
|
83
|
+
this.entries.delete(v.id);
|
|
84
|
+
}
|
|
85
|
+
if (victims.length) {
|
|
86
|
+
this.emitChange();
|
|
87
|
+
}
|
|
88
|
+
return victims.length;
|
|
89
|
+
}
|
|
90
|
+
/** Remove exactly one entry by id (no key match). Returns true if it existed. */
|
|
91
|
+
forgetById(id) {
|
|
92
|
+
if (!this.entries.delete(id)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
this.emitChange();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
/** Forget everything. Returns the number removed. */
|
|
99
|
+
clear() {
|
|
100
|
+
const n = this.entries.size;
|
|
101
|
+
if (n) {
|
|
102
|
+
this.entries.clear();
|
|
103
|
+
this.emitChange();
|
|
104
|
+
}
|
|
105
|
+
return n;
|
|
106
|
+
}
|
|
107
|
+
/** All facts, newest-updated first. */
|
|
108
|
+
list() {
|
|
109
|
+
return [...this.entries.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
110
|
+
}
|
|
111
|
+
get(id) {
|
|
112
|
+
return this.entries.get(id);
|
|
113
|
+
}
|
|
114
|
+
size() {
|
|
115
|
+
return this.entries.size;
|
|
116
|
+
}
|
|
117
|
+
/** Load persisted facts (replacing the current set). Returns how many were restored. */
|
|
118
|
+
restore(list) {
|
|
119
|
+
this.entries.clear();
|
|
120
|
+
let n = 0;
|
|
121
|
+
for (const e of Array.isArray(list) ? list : []) {
|
|
122
|
+
if (!e || typeof e.id !== 'string' || typeof e.text !== 'string' || !e.text.trim()) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
this.entries.set(e.id, {
|
|
126
|
+
id: e.id,
|
|
127
|
+
text: e.text,
|
|
128
|
+
key: e.key || '',
|
|
129
|
+
source: e.source || '',
|
|
130
|
+
createdAt: e.createdAt || this.now(),
|
|
131
|
+
updatedAt: e.updatedAt || e.createdAt || this.now(),
|
|
132
|
+
});
|
|
133
|
+
n++;
|
|
134
|
+
}
|
|
135
|
+
this.enforceCap();
|
|
136
|
+
this.emitChange();
|
|
137
|
+
return n;
|
|
138
|
+
}
|
|
139
|
+
enforceCap() {
|
|
140
|
+
if (this.entries.size <= this.maxEntries) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// Drop the oldest (least-recently-updated) until within the cap.
|
|
144
|
+
const byAge = [...this.entries.values()].sort((a, b) => a.updatedAt - b.updatedAt);
|
|
145
|
+
for (const e of byAge) {
|
|
146
|
+
if (this.entries.size <= this.maxEntries) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
this.entries.delete(e.id);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
genId(now) {
|
|
153
|
+
return `m${now.toString(36)}${(this.seq++).toString(36)}`;
|
|
154
|
+
}
|
|
155
|
+
emitChange() {
|
|
156
|
+
try {
|
|
157
|
+
this.opts.onChange(this.list());
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
this.opts.log?.warn(`memory onChange failed: ${e.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.MemoryStore = MemoryStore;
|
|
165
|
+
/**
|
|
166
|
+
* Compact system-prompt block of all remembered facts (retrieval = "inject all"), localized header. Returns
|
|
167
|
+
* '' when there is nothing to inject. `budget` caps the number of characters to keep the prompt bounded.
|
|
168
|
+
*/
|
|
169
|
+
function buildMemoryPrompt(entries, lang, budget = 4000) {
|
|
170
|
+
if (!entries.length) {
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
173
|
+
const header = lang === 'ru'
|
|
174
|
+
? 'Известные факты о пользователе/доме (используй их, если уместно; не выдумывай новые):'
|
|
175
|
+
: lang === 'de'
|
|
176
|
+
? 'Bekannte Fakten über den Nutzer/Haushalt (nutze sie, wenn relevant; erfinde keine neuen):'
|
|
177
|
+
: 'Known facts about the user/household (use them when relevant; do not invent new ones):';
|
|
178
|
+
const lines = [];
|
|
179
|
+
let used = header.length;
|
|
180
|
+
for (const e of entries) {
|
|
181
|
+
const line = `- ${e.text}`;
|
|
182
|
+
if (used + line.length + 1 > budget) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
lines.push(line);
|
|
186
|
+
used += line.length + 1;
|
|
187
|
+
}
|
|
188
|
+
return `${header}\n${lines.join('\n')}`;
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/lib/memory.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Long-term memory (roadmap #6) — durable facts the assistant remembers across sessions/restarts\n * (\"meine Tochter heißt Lena\", \"das WLAN-Passwort steht im Flur\"). Storage is ioBroker-first: this store\n * holds the facts in memory and emits an `onChange` callback; `main.ts` mirrors them into `memory.*` states\n * (+ one editable object per entry) and, before each cloud-LLM call, injects them compactly into the system\n * prompt (retrieval = \"inject all\", capped — enough for the handful of household facts a home assistant\n * accumulates; semantic search can be added later without touching the storage format).\n *\n * The store itself is transport-agnostic and testable (CRUD + dedup + caps + persistence round-trip).\n */\n\n/** One remembered fact. Plain data so it round-trips through JSON for persistence. */\nexport interface MemoryEntry {\n /** Short unique id (also the `memory.items.<id>` state-object segment). */\n id: string;\n /** The fact text. */\n text: string;\n /** Optional short key/topic (\"tochter\", \"wlan\") used for dedup and targeted forgetting; '' if none. */\n key: string;\n /** Where it came from (satellite / 'chat' / '' …), for context. */\n source: string;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface MemoryStoreOptions {\n /** Called whenever the list changes (add/update/forget/clear/restore) with the new list. */\n onChange: (entries: MemoryEntry[]) => void;\n /** Max number of stored facts; the oldest (by updatedAt) are dropped past this. Default 100. */\n maxEntries?: number;\n /** Max characters per fact (longer is truncated). Default 500. */\n maxTextLength?: number;\n log?: Pick<ioBroker.Log, 'debug' | 'info' | 'warn'>;\n /** Injectable clock (tests). Defaults to `Date.now`. */\n now?: () => number;\n}\n\n/** In-memory long-term fact store with dedup, size caps and JSON persistence (no scheduling). */\nexport class MemoryStore {\n private readonly entries = new Map<string, MemoryEntry>();\n private seq = 0;\n private readonly now: () => number;\n private readonly maxEntries: number;\n private readonly maxTextLength: number;\n\n constructor(private readonly opts: MemoryStoreOptions) {\n this.now = opts.now || Date.now;\n this.maxEntries = opts.maxEntries ?? 100;\n this.maxTextLength = opts.maxTextLength ?? 500;\n }\n\n /**\n * Remember a fact. If `key` is given and already exists (or, without a key, an identical text exists),\n * the existing entry is updated instead of duplicated. Enforces the size cap by dropping the oldest.\n * Returns the stored entry, or null for empty text.\n */\n add(o: { text: string; key?: string; source?: string }): MemoryEntry | null {\n const text = (o.text || '').trim().slice(0, this.maxTextLength);\n if (!text) {\n return null;\n }\n const key = (o.key || '').trim().toLowerCase();\n const source = o.source || '';\n const now = this.now();\n\n // Dedup: same key, or (keyless) identical text → update in place.\n const existing = key\n ? [...this.entries.values()].find(e => e.key === key)\n : [...this.entries.values()].find(e => e.text.toLowerCase() === text.toLowerCase());\n if (existing) {\n existing.text = text;\n existing.source = source || existing.source;\n existing.updatedAt = now;\n this.emitChange();\n return existing;\n }\n\n const entry: MemoryEntry = { id: this.genId(now), text, key, source, createdAt: now, updatedAt: now };\n this.entries.set(entry.id, entry);\n this.enforceCap();\n this.opts.log?.debug(`memory added: ${entry.id}${key ? ` [${key}]` : ''} \"${text}\"`);\n this.emitChange();\n return entry;\n }\n\n /** Update an entry's text by id. Returns true if it existed. */\n update(id: string, text: string): boolean {\n const e = this.entries.get(id);\n if (!e) {\n return false;\n }\n const t = (text || '').trim().slice(0, this.maxTextLength);\n if (!t) {\n return this.forgetById(id);\n }\n e.text = t;\n e.updatedAt = this.now();\n this.emitChange();\n return true;\n }\n\n /** Forget entries matching an id or a key. Returns the number removed. */\n forget(idOrKey: string): number {\n const needle = (idOrKey || '').trim().toLowerCase();\n if (!needle) {\n return 0;\n }\n const victims = [...this.entries.values()].filter(\n e => e.id.toLowerCase() === needle || (e.key && e.key === needle),\n );\n for (const v of victims) {\n this.entries.delete(v.id);\n }\n if (victims.length) {\n this.emitChange();\n }\n return victims.length;\n }\n\n /** Remove exactly one entry by id (no key match). Returns true if it existed. */\n forgetById(id: string): boolean {\n if (!this.entries.delete(id)) {\n return false;\n }\n this.emitChange();\n return true;\n }\n\n /** Forget everything. Returns the number removed. */\n clear(): number {\n const n = this.entries.size;\n if (n) {\n this.entries.clear();\n this.emitChange();\n }\n return n;\n }\n\n /** All facts, newest-updated first. */\n list(): MemoryEntry[] {\n return [...this.entries.values()].sort((a, b) => b.updatedAt - a.updatedAt);\n }\n\n get(id: string): MemoryEntry | undefined {\n return this.entries.get(id);\n }\n\n size(): number {\n return this.entries.size;\n }\n\n /** Load persisted facts (replacing the current set). Returns how many were restored. */\n restore(list: MemoryEntry[]): number {\n this.entries.clear();\n let n = 0;\n for (const e of Array.isArray(list) ? list : []) {\n if (!e || typeof e.id !== 'string' || typeof e.text !== 'string' || !e.text.trim()) {\n continue;\n }\n this.entries.set(e.id, {\n id: e.id,\n text: e.text,\n key: e.key || '',\n source: e.source || '',\n createdAt: e.createdAt || this.now(),\n updatedAt: e.updatedAt || e.createdAt || this.now(),\n });\n n++;\n }\n this.enforceCap();\n this.emitChange();\n return n;\n }\n\n private enforceCap(): void {\n if (this.entries.size <= this.maxEntries) {\n return;\n }\n // Drop the oldest (least-recently-updated) until within the cap.\n const byAge = [...this.entries.values()].sort((a, b) => a.updatedAt - b.updatedAt);\n for (const e of byAge) {\n if (this.entries.size <= this.maxEntries) {\n break;\n }\n this.entries.delete(e.id);\n }\n }\n\n private genId(now: number): string {\n return `m${now.toString(36)}${(this.seq++).toString(36)}`;\n }\n\n private emitChange(): void {\n try {\n this.opts.onChange(this.list());\n } catch (e) {\n this.opts.log?.warn(`memory onChange failed: ${(e as Error).message}`);\n }\n }\n}\n\n/**\n * Compact system-prompt block of all remembered facts (retrieval = \"inject all\"), localized header. Returns\n * '' when there is nothing to inject. `budget` caps the number of characters to keep the prompt bounded.\n */\nexport function buildMemoryPrompt(entries: MemoryEntry[], lang: string, budget = 4000): string {\n if (!entries.length) {\n return '';\n }\n const header =\n lang === 'ru'\n ? 'Известные факты о пользователе/доме (используй их, если уместно; не выдумывай новые):'\n : lang === 'de'\n ? 'Bekannte Fakten über den Nutzer/Haushalt (nutze sie, wenn relevant; erfinde keine neuen):'\n : 'Known facts about the user/household (use them when relevant; do not invent new ones):';\n const lines: string[] = [];\n let used = header.length;\n for (const e of entries) {\n const line = `- ${e.text}`;\n if (used + line.length + 1 > budget) {\n break;\n }\n lines.push(line);\n used += line.length + 1;\n }\n return `${header}\\n${lines.join('\\n')}`;\n}\n"]}
|