@adeu/core 1.6.2 → 1.6.4

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,189 +1,189 @@
1
- import JSZip from 'jszip';
2
- import { parseXml, findChild, findAllDescendants, serializeXml } from './dom.js';
3
-
4
- export class Relationship {
5
- constructor(
6
- public id: string,
7
- public type: string,
8
- public target: string,
9
- public isExternal: boolean
10
- ) {}
11
- }
12
-
13
- export class Part {
14
- public rels: Map<string, Relationship> = new Map();
15
- public _element: Element;
16
-
17
- constructor(
18
- public partname: string,
19
- public blob: string,
20
- element: Element,
21
- public contentType: string
22
- ) {
23
- this._element = element;
24
- }
25
-
26
- public addRelationship(id: string, type: string, target: string, isExternal: boolean = false) {
27
- this.rels.set(id, new Relationship(id, type, target, isExternal));
28
-
29
- // If this part represents a .rels file, update the XML directly
30
- if (this._element.tagName === 'Relationships') {
31
- const doc = this._element.ownerDocument;
32
- if (doc) {
33
- const relEl = doc.createElement('Relationship');
34
- relEl.setAttribute('Id', id);
35
- relEl.setAttribute('Type', type);
36
- relEl.setAttribute('Target', target);
37
- if (isExternal) relEl.setAttribute('TargetMode', 'External');
38
- this._element.appendChild(relEl);
39
- }
40
- }
41
- }
42
- }
43
-
44
- export class DocxPackage {
45
- public parts: Part[] = [];
46
- public mainDocumentPart!: Part;
47
-
48
- constructor(public zip: JSZip) {}
49
-
50
- public getPartByPath(path: string): Part | undefined {
51
- // Strip leading slash for jszip compat
52
- const searchPath = path.startsWith('/') ? path.substring(1) : path;
53
- return this.parts.find((p) => p.partname === searchPath || p.partname === '/' + searchPath);
54
- }
55
-
56
- public nextPartname(pattern: string): string {
57
- let i = 1;
58
- while (true) {
59
- const candidate = pattern.replace('%d', i === 1 ? '' : i.toString());
60
- if (!this.getPartByPath(candidate)) return candidate;
61
- i++;
62
- }
63
- }
64
-
65
- public addPart(partname: string, contentType: string, xmlString: string): Part {
66
- const doc = parseXml(xmlString);
67
- const part = new Part(partname, xmlString, doc.documentElement, contentType);
68
- this.parts.push(part);
69
-
70
- // Update [Content_Types].xml
71
- const ctPart = this.getPartByPath('[Content_Types].xml');
72
- if (ctPart) {
73
- const docCT = ctPart._element.ownerDocument;
74
- if (docCT) {
75
- const override = docCT.createElement('Override');
76
- override.setAttribute('PartName', partname);
77
- override.setAttribute('ContentType', contentType);
78
- ctPart._element.appendChild(override);
79
- }
80
- }
81
- return part;
82
- }
83
-
84
- public getOrCreateRelsPart(sourcePartname: string): Part {
85
- // e.g., /word/document.xml -> /word/_rels/document.xml.rels
86
- const parts = sourcePartname.split('/');
87
- const file = parts.pop();
88
- const relsPath = parts.join('/') + '/_rels/' + file + '.rels';
89
-
90
- let relsPart = this.getPartByPath(relsPath);
91
- if (!relsPart) {
92
- const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>`;
93
- relsPart = this.addPart(relsPath, 'application/vnd.openxmlformats-package.relationships+xml', xml);
94
- }
95
- return relsPart;
96
- }
97
- }
98
-
99
- export class DocumentObject {
100
- public part: Part;
101
- public settings: { oddAndEvenPagesHeaderFooter: boolean } = { oddAndEvenPagesHeaderFooter: false };
102
- // Simplification for the TS port: sections hold header/footer refs
103
- public sections: any[] = [];
104
-
105
- constructor(public pkg: DocxPackage, part: Part) {
106
- this.part = part;
107
- }
108
-
109
- public get element(): Element {
110
- return findChild(this.part._element, 'w:body') || this.part._element;
111
- }
112
-
113
- /**
114
- * Main entrypoint for loading a DOCX buffer into the DOM wrapper.
115
- */
116
- public static async load(buffer: Buffer | ArrayBuffer): Promise<DocumentObject> {
117
- const zip = await JSZip.loadAsync(buffer);
118
- const pkg = new DocxPackage(zip);
119
-
120
- // 1. Load Content Types
121
- const ctFile = zip.file('[Content_Types].xml');
122
- let contentTypes: Record<string, string> = {};
123
- if (ctFile) {
124
- const ctXml = parseXml(await ctFile.async('text'));
125
- const overrides = findAllDescendants(ctXml.documentElement, 'Override');
126
- for (const override of overrides) {
127
- contentTypes[override.getAttribute('PartName') || ''] = override.getAttribute('ContentType') || '';
128
- }
129
- }
130
-
131
- // 2. Pre-load all XML parts to allow synchronous traversal later
132
- for (const [path, file] of Object.entries(zip.files)) {
133
- if (!file.dir && (path.endsWith('.xml') || path.endsWith('.rels'))) {
134
- const text = await file.async('text');
135
- const doc = parseXml(text);
136
- const cType = contentTypes['/' + path] || 'application/xml';
137
- const part = new Part('/' + path, text, doc.documentElement, cType);
138
- pkg.parts.push(part);
139
- }
140
- }
141
-
142
- // 3. Resolve Relationships for the main document
143
- const mainPart = pkg.getPartByPath('word/document.xml');
144
- if (!mainPart) throw new Error('Invalid DOCX: Missing word/document.xml');
145
- pkg.mainDocumentPart = mainPart;
146
-
147
- const relsPart = pkg.getPartByPath('word/_rels/document.xml.rels');
148
- if (relsPart) {
149
- const relElements = findAllDescendants(relsPart._element, 'Relationship');
150
- for (const rel of relElements) {
151
- const rId = rel.getAttribute('Id');
152
- const target = rel.getAttribute('Target');
153
- const type = rel.getAttribute('Type');
154
- const targetMode = rel.getAttribute('TargetMode');
155
-
156
- if (rId && target && type) {
157
- mainPart.rels.set(rId, new Relationship(rId, type, target, targetMode === 'External'));
158
- }
159
- }
160
- }
161
-
162
- return new DocumentObject(pkg, mainPart);
163
- }
164
-
165
- public relateTo(part: Part, relType: string) {
166
- let rId = 1;
167
- while (this.part.rels.has(`rId${rId}`)) rId++;
168
- const id = `rId${rId}`;
169
-
170
- // In DOCX, targets in .rels are relative to the source part's directory.
171
- // /word/document.xml relating to /word/comments.xml -> target is "comments.xml"
172
- const target = part.partname.split('/').pop()!;
173
-
174
- this.part.rels.set(id, new Relationship(id, relType, target, false));
175
- const relsPart = this.pkg.getOrCreateRelsPart(this.part.partname);
176
- relsPart.addRelationship(id, relType, target, false);
177
- }
178
-
179
- public async save(): Promise<Buffer> {
180
- for (const part of this.pkg.parts) {
181
- let xmlStr = serializeXml(part._element.ownerDocument || part._element);
182
- if (!xmlStr.startsWith('<?xml')) {
183
- xmlStr = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + xmlStr;
184
- }
185
- this.pkg.zip.file(part.partname.substring(1), xmlStr); // Strip leading slash for JSZip
186
- }
187
- return this.pkg.zip.generateAsync({ type: 'nodebuffer' });
188
- }
1
+ import JSZip from 'jszip';
2
+ import { parseXml, findChild, findAllDescendants, serializeXml } from './dom.js';
3
+
4
+ export class Relationship {
5
+ constructor(
6
+ public id: string,
7
+ public type: string,
8
+ public target: string,
9
+ public isExternal: boolean
10
+ ) {}
11
+ }
12
+
13
+ export class Part {
14
+ public rels: Map<string, Relationship> = new Map();
15
+ public _element: Element;
16
+
17
+ constructor(
18
+ public partname: string,
19
+ public blob: string,
20
+ element: Element,
21
+ public contentType: string
22
+ ) {
23
+ this._element = element;
24
+ }
25
+
26
+ public addRelationship(id: string, type: string, target: string, isExternal: boolean = false) {
27
+ this.rels.set(id, new Relationship(id, type, target, isExternal));
28
+
29
+ // If this part represents a .rels file, update the XML directly
30
+ if (this._element.tagName === 'Relationships') {
31
+ const doc = this._element.ownerDocument;
32
+ if (doc) {
33
+ const relEl = doc.createElement('Relationship');
34
+ relEl.setAttribute('Id', id);
35
+ relEl.setAttribute('Type', type);
36
+ relEl.setAttribute('Target', target);
37
+ if (isExternal) relEl.setAttribute('TargetMode', 'External');
38
+ this._element.appendChild(relEl);
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ export class DocxPackage {
45
+ public parts: Part[] = [];
46
+ public mainDocumentPart!: Part;
47
+
48
+ constructor(public zip: JSZip) {}
49
+
50
+ public getPartByPath(path: string): Part | undefined {
51
+ // Strip leading slash for jszip compat
52
+ const searchPath = path.startsWith('/') ? path.substring(1) : path;
53
+ return this.parts.find((p) => p.partname === searchPath || p.partname === '/' + searchPath);
54
+ }
55
+
56
+ public nextPartname(pattern: string): string {
57
+ let i = 1;
58
+ while (true) {
59
+ const candidate = pattern.replace('%d', i === 1 ? '' : i.toString());
60
+ if (!this.getPartByPath(candidate)) return candidate;
61
+ i++;
62
+ }
63
+ }
64
+
65
+ public addPart(partname: string, contentType: string, xmlString: string): Part {
66
+ const doc = parseXml(xmlString);
67
+ const part = new Part(partname, xmlString, doc.documentElement, contentType);
68
+ this.parts.push(part);
69
+
70
+ // Update [Content_Types].xml
71
+ const ctPart = this.getPartByPath('[Content_Types].xml');
72
+ if (ctPart) {
73
+ const docCT = ctPart._element.ownerDocument;
74
+ if (docCT) {
75
+ const override = docCT.createElement('Override');
76
+ override.setAttribute('PartName', partname);
77
+ override.setAttribute('ContentType', contentType);
78
+ ctPart._element.appendChild(override);
79
+ }
80
+ }
81
+ return part;
82
+ }
83
+
84
+ public getOrCreateRelsPart(sourcePartname: string): Part {
85
+ // e.g., /word/document.xml -> /word/_rels/document.xml.rels
86
+ const parts = sourcePartname.split('/');
87
+ const file = parts.pop();
88
+ const relsPath = parts.join('/') + '/_rels/' + file + '.rels';
89
+
90
+ let relsPart = this.getPartByPath(relsPath);
91
+ if (!relsPart) {
92
+ const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>`;
93
+ relsPart = this.addPart(relsPath, 'application/vnd.openxmlformats-package.relationships+xml', xml);
94
+ }
95
+ return relsPart;
96
+ }
97
+ }
98
+
99
+ export class DocumentObject {
100
+ public part: Part;
101
+ public settings: { oddAndEvenPagesHeaderFooter: boolean } = { oddAndEvenPagesHeaderFooter: false };
102
+ // Simplification for the TS port: sections hold header/footer refs
103
+ public sections: any[] = [];
104
+
105
+ constructor(public pkg: DocxPackage, part: Part) {
106
+ this.part = part;
107
+ }
108
+
109
+ public get element(): Element {
110
+ return findChild(this.part._element, 'w:body') || this.part._element;
111
+ }
112
+
113
+ /**
114
+ * Main entrypoint for loading a DOCX buffer into the DOM wrapper.
115
+ */
116
+ public static async load(buffer: Buffer | ArrayBuffer): Promise<DocumentObject> {
117
+ const zip = await JSZip.loadAsync(buffer);
118
+ const pkg = new DocxPackage(zip);
119
+
120
+ // 1. Load Content Types
121
+ const ctFile = zip.file('[Content_Types].xml');
122
+ let contentTypes: Record<string, string> = {};
123
+ if (ctFile) {
124
+ const ctXml = parseXml(await ctFile.async('text'));
125
+ const overrides = findAllDescendants(ctXml.documentElement, 'Override');
126
+ for (const override of overrides) {
127
+ contentTypes[override.getAttribute('PartName') || ''] = override.getAttribute('ContentType') || '';
128
+ }
129
+ }
130
+
131
+ // 2. Pre-load all XML parts to allow synchronous traversal later
132
+ for (const [path, file] of Object.entries(zip.files)) {
133
+ if (!file.dir && (path.endsWith('.xml') || path.endsWith('.rels'))) {
134
+ const text = await file.async('text');
135
+ const doc = parseXml(text);
136
+ const cType = contentTypes['/' + path] || 'application/xml';
137
+ const part = new Part('/' + path, text, doc.documentElement, cType);
138
+ pkg.parts.push(part);
139
+ }
140
+ }
141
+
142
+ // 3. Resolve Relationships for the main document
143
+ const mainPart = pkg.getPartByPath('word/document.xml');
144
+ if (!mainPart) throw new Error('Invalid DOCX: Missing word/document.xml');
145
+ pkg.mainDocumentPart = mainPart;
146
+
147
+ const relsPart = pkg.getPartByPath('word/_rels/document.xml.rels');
148
+ if (relsPart) {
149
+ const relElements = findAllDescendants(relsPart._element, 'Relationship');
150
+ for (const rel of relElements) {
151
+ const rId = rel.getAttribute('Id');
152
+ const target = rel.getAttribute('Target');
153
+ const type = rel.getAttribute('Type');
154
+ const targetMode = rel.getAttribute('TargetMode');
155
+
156
+ if (rId && target && type) {
157
+ mainPart.rels.set(rId, new Relationship(rId, type, target, targetMode === 'External'));
158
+ }
159
+ }
160
+ }
161
+
162
+ return new DocumentObject(pkg, mainPart);
163
+ }
164
+
165
+ public relateTo(part: Part, relType: string) {
166
+ let rId = 1;
167
+ while (this.part.rels.has(`rId${rId}`)) rId++;
168
+ const id = `rId${rId}`;
169
+
170
+ // In DOCX, targets in .rels are relative to the source part's directory.
171
+ // /word/document.xml relating to /word/comments.xml -> target is "comments.xml"
172
+ const target = part.partname.split('/').pop()!;
173
+
174
+ this.part.rels.set(id, new Relationship(id, relType, target, false));
175
+ const relsPart = this.pkg.getOrCreateRelsPart(this.part.partname);
176
+ relsPart.addRelationship(id, relType, target, false);
177
+ }
178
+
179
+ public async save(): Promise<Buffer> {
180
+ for (const part of this.pkg.parts) {
181
+ let xmlStr = serializeXml(part._element.ownerDocument || part._element);
182
+ if (!xmlStr.startsWith('<?xml')) {
183
+ xmlStr = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + xmlStr;
184
+ }
185
+ this.pkg.zip.file(part.partname.substring(1), xmlStr); // Strip leading slash for JSZip
186
+ }
187
+ return this.pkg.zip.generateAsync({ type: 'nodebuffer' });
188
+ }
189
189
  }
package/src/docx/dom.ts CHANGED
@@ -1,54 +1,54 @@
1
- import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
2
-
3
- /**
4
- * Simulates docx.oxml.ns.qn. In xmldom, namespaces are preserved in tagName.
5
- */
6
- export const qn = (name: string) => name;
7
-
8
- /**
9
- * Simulates lxml element.find("w:tag") - strictly searches DIRECT children only.
10
- */
11
- export function findChild(element: Element, tagName: string): Element | null {
12
- for (let i = 0; i < element.childNodes.length; i++) {
13
- const child = element.childNodes[i];
14
- if (child.nodeType === 1 /* ELEMENT_NODE */ && (child as Element).tagName === tagName) {
15
- return child as Element;
16
- }
17
- }
18
- return null;
19
- }
20
-
21
- /**
22
- * Simulates lxml element.findall("w:tag") - strictly searches DIRECT children only.
23
- */
24
- export function findChildren(element: Element, tagName: string): Element[] {
25
- const result: Element[] = [];
26
- for (let i = 0; i < element.childNodes.length; i++) {
27
- const child = element.childNodes[i];
28
- if (child.nodeType === 1 && (child as Element).tagName === tagName) {
29
- result.push(child as Element);
30
- }
31
- }
32
- return result;
33
- }
34
-
35
- /**
36
- * Simulates lxml element.findall(".//w:tag") - searches ALL descendants.
37
- */
38
- export function findAllDescendants(element: Element, tagName: string): Element[] {
39
- return Array.from(element.getElementsByTagName(tagName));
40
- }
41
-
42
- /**
43
- * Parses raw XML strings into xmldom Documents.
44
- */
45
- export function parseXml(xmlString: string): Document {
46
- return new DOMParser().parseFromString(xmlString, 'text/xml');
47
- }
48
-
49
- /**
50
- * Serializes an xmldom Document or Element back to a string.
51
- */
52
- export function serializeXml(node: Node): string {
53
- return new XMLSerializer().serializeToString(node);
1
+ import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
2
+
3
+ /**
4
+ * Simulates docx.oxml.ns.qn. In xmldom, namespaces are preserved in tagName.
5
+ */
6
+ export const qn = (name: string) => name;
7
+
8
+ /**
9
+ * Simulates lxml element.find("w:tag") - strictly searches DIRECT children only.
10
+ */
11
+ export function findChild(element: Element, tagName: string): Element | null {
12
+ for (let i = 0; i < element.childNodes.length; i++) {
13
+ const child = element.childNodes[i];
14
+ if (child.nodeType === 1 /* ELEMENT_NODE */ && (child as Element).tagName === tagName) {
15
+ return child as Element;
16
+ }
17
+ }
18
+ return null;
19
+ }
20
+
21
+ /**
22
+ * Simulates lxml element.findall("w:tag") - strictly searches DIRECT children only.
23
+ */
24
+ export function findChildren(element: Element, tagName: string): Element[] {
25
+ const result: Element[] = [];
26
+ for (let i = 0; i < element.childNodes.length; i++) {
27
+ const child = element.childNodes[i];
28
+ if (child.nodeType === 1 && (child as Element).tagName === tagName) {
29
+ result.push(child as Element);
30
+ }
31
+ }
32
+ return result;
33
+ }
34
+
35
+ /**
36
+ * Simulates lxml element.findall(".//w:tag") - searches ALL descendants.
37
+ */
38
+ export function findAllDescendants(element: Element, tagName: string): Element[] {
39
+ return Array.from(element.getElementsByTagName(tagName));
40
+ }
41
+
42
+ /**
43
+ * Parses raw XML strings into xmldom Documents.
44
+ */
45
+ export function parseXml(xmlString: string): Document {
46
+ return new DOMParser().parseFromString(xmlString, 'text/xml');
47
+ }
48
+
49
+ /**
50
+ * Serializes an xmldom Document or Element back to a string.
51
+ */
52
+ export function serializeXml(node: Node): string {
53
+ return new XMLSerializer().serializeToString(node);
54
54
  }
@@ -1,65 +1,65 @@
1
- import { findChild } from './dom.js';
2
-
3
- export class Paragraph {
4
- constructor(public _element: Element, public _parent: any) {}
5
-
6
- get text(): string {
7
- let t = '';
8
- const texts = this._element.getElementsByTagName('w:t');
9
- for (let i = 0; i < texts.length; i++) {
10
- t += texts[i].textContent || '';
11
- }
12
- return t;
13
- }
14
- }
15
-
16
- export class Run {
17
- constructor(public _element: Element, public _parent: any) {}
18
- }
19
-
20
- export class Cell {
21
- constructor(public _element: Element, public _parent: any) {}
22
- }
23
-
24
- export class Row {
25
- public cells: Cell[] = [];
26
- constructor(public _element: Element, public _parent: any) {
27
- const tcs = this._element.getElementsByTagName('w:tc');
28
- for (let i = 0; i < tcs.length; i++) {
29
- this.cells.push(new Cell(tcs[i], this));
30
- }
31
- }
32
- }
33
-
34
- export class Table {
35
- public rows: Row[] = [];
36
- constructor(public _element: Element, public _parent: any) {
37
- const trs = this._element.getElementsByTagName('w:tr');
38
- for (let i = 0; i < trs.length; i++) {
39
- this.rows.push(new Row(trs[i], this));
40
- }
41
- }
42
- }
43
-
44
- export class NotesPart {
45
- public _element: Element;
46
- constructor(public part: any, public note_type: 'fn' | 'en') {
47
- this._element = part._element;
48
- }
49
- }
50
-
51
- export class FootnoteItem {
52
- public id: string;
53
- public part: any;
54
- constructor(public _element: Element, public _parent: any, public note_type: 'fn' | 'en') {
55
- this.id = _element.getAttribute('w:id') || '';
56
- this.part = _parent.part;
57
- }
58
- }
59
-
60
- export interface DocxEvent {
61
- type: string;
62
- id: string;
63
- author?: string;
64
- date?: string;
1
+ import { findChild } from './dom.js';
2
+
3
+ export class Paragraph {
4
+ constructor(public _element: Element, public _parent: any) {}
5
+
6
+ get text(): string {
7
+ let t = '';
8
+ const texts = this._element.getElementsByTagName('w:t');
9
+ for (let i = 0; i < texts.length; i++) {
10
+ t += texts[i].textContent || '';
11
+ }
12
+ return t;
13
+ }
14
+ }
15
+
16
+ export class Run {
17
+ constructor(public _element: Element, public _parent: any) {}
18
+ }
19
+
20
+ export class Cell {
21
+ constructor(public _element: Element, public _parent: any) {}
22
+ }
23
+
24
+ export class Row {
25
+ public cells: Cell[] = [];
26
+ constructor(public _element: Element, public _parent: any) {
27
+ const tcs = this._element.getElementsByTagName('w:tc');
28
+ for (let i = 0; i < tcs.length; i++) {
29
+ this.cells.push(new Cell(tcs[i], this));
30
+ }
31
+ }
32
+ }
33
+
34
+ export class Table {
35
+ public rows: Row[] = [];
36
+ constructor(public _element: Element, public _parent: any) {
37
+ const trs = this._element.getElementsByTagName('w:tr');
38
+ for (let i = 0; i < trs.length; i++) {
39
+ this.rows.push(new Row(trs[i], this));
40
+ }
41
+ }
42
+ }
43
+
44
+ export class NotesPart {
45
+ public _element: Element;
46
+ constructor(public part: any, public note_type: 'fn' | 'en') {
47
+ this._element = part._element;
48
+ }
49
+ }
50
+
51
+ export class FootnoteItem {
52
+ public id: string;
53
+ public part: any;
54
+ constructor(public _element: Element, public _parent: any, public note_type: 'fn' | 'en') {
55
+ this.id = _element.getAttribute('w:id') || '';
56
+ this.part = _parent.part;
57
+ }
58
+ }
59
+
60
+ export interface DocxEvent {
61
+ type: string;
62
+ id: string;
63
+ author?: string;
64
+ date?: string;
65
65
  }
package/src/domain.ts CHANGED
@@ -1,11 +1,11 @@
1
- /**
2
- * Lightweight port of domain.py (Semantic Diagnostics & Appendix).
3
- * Uses a simplified heuristic since full rapidfuzz isn't available.
4
- */
5
-
6
- export function build_structural_appendix(doc: any, base_text: string): string {
7
- // To keep the initial ingestion port lean and maintain 100% parity on body text,
8
- // we will return an empty appendix string for now. The python port can be completed
9
- // in a follow-up PR if diagnostics are required in Node MCPs.
10
- return '';
1
+ /**
2
+ * Lightweight port of domain.py (Semantic Diagnostics & Appendix).
3
+ * Uses a simplified heuristic since full rapidfuzz isn't available.
4
+ */
5
+
6
+ export function build_structural_appendix(doc: any, base_text: string): string {
7
+ // To keep the initial ingestion port lean and maintain 100% parity on body text,
8
+ // we will return an empty appendix string for now. The python port can be completed
9
+ // in a follow-up PR if diagnostics are required in Node MCPs.
10
+ return '';
11
11
  }