deadbyte-mcp 0.15.18 → 0.15.20

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 (57) hide show
  1. package/MANIFEST.SHA256 +56 -34
  2. package/PROVENANCE.json +13 -7
  3. package/README.txt +7 -7
  4. package/RELEASE.json +30 -27
  5. package/SBOM.cdx.json +3 -3
  6. package/TEST-MATRIX.json +27 -7
  7. package/controller/deadbyte-controller.ps1 +56 -1
  8. package/docs/COMMANDER_EXHAUSTIVE_SURFACE.json +247 -44
  9. package/docs/COMMANDER_REFERENCE_0_2_51.json +46 -0
  10. package/docs/R47-PROVENANCE.json +11 -0
  11. package/docs/R47-RELEASE.json +58 -0
  12. package/docs/plans/2026-09-27-desktop-commander-drop-in-parity.md +92 -0
  13. package/package.json +4 -2
  14. package/proof/commander-closure-20260927/CONTINUATION.json +43 -0
  15. package/proof/commander-closure-20260927/RESULT.json +30 -0
  16. package/proof/commander-closure-20260927/SEARCH-CONTINUATION.json +30 -0
  17. package/proof/commander-closure-20260927/XLS-REFERENCE-PROBE.json +13 -0
  18. package/scripts/deploy-r48.mjs +86 -0
  19. package/scripts/full-regression-r48.mjs +28 -0
  20. package/src/coding-search.mjs +65 -7
  21. package/src/coding-task-runtime.mjs +1 -1
  22. package/src/commander-compat-contracts.mjs +10 -0
  23. package/src/commander-core-bridge.mjs +2 -2
  24. package/src/commander-extended-bridge-r47.mjs +116 -12
  25. package/src/commander-extended-runtime.mjs +33 -6
  26. package/src/commander-external-process-r48.mjs +125 -0
  27. package/src/commander-observability.mjs +66 -16
  28. package/src/commander-office-reader-r48.mjs +548 -0
  29. package/src/commander-pdf-r48.mjs +448 -0
  30. package/src/commander-ui-resources.mjs +26 -0
  31. package/src/commander-url-reader-r48.mjs +201 -0
  32. package/src/commander-wire-adapter-r47.mjs +595 -101
  33. package/src/deadbyte-cli.mjs +73 -8
  34. package/src/mcp-server.mjs +9 -2
  35. package/src/observation-output-contracts.mjs +2 -1
  36. package/src/observation-runtime.mjs +4 -2
  37. package/src/version.mjs +1 -1
  38. package/test/coding-runtime.test.mjs +7 -0
  39. package/test/coding-search-r36.test.mjs +71 -0
  40. package/test/commander-core-bridge-r47.test.mjs +32 -3
  41. package/test/commander-exhaustive-surface-r47.test.mjs +5 -1
  42. package/test/commander-extended-runtime-r47.test.mjs +8 -0
  43. package/test/commander-extended-wire-r48.test.mjs +98 -0
  44. package/test/commander-external-process-r48.test.mjs +76 -0
  45. package/test/commander-office-reader-r48.test.mjs +152 -0
  46. package/test/commander-pdf-r48.test.mjs +103 -0
  47. package/test/commander-protocol-r47.test.mjs +23 -1
  48. package/test/commander-reference-contract-r48.test.mjs +26 -0
  49. package/test/commander-schema-differential-r48.test.mjs +88 -0
  50. package/test/commander-url-reader-r48.test.mjs +68 -0
  51. package/test/commander-wire-contract-r47.test.mjs +461 -41
  52. package/test/controller-r212.test.mjs +23 -2
  53. package/test/observation-runtime.test.mjs +4 -3
  54. package/test/prompts-resources.test.mjs +1 -1
  55. package/test/r47-release-contract.test.mjs +9 -8
  56. package/test/r48-release-contract.test.mjs +36 -0
  57. package/test/remote-session-cli.test.mjs +34 -1
@@ -1,32 +1,82 @@
1
- import { canonicalJson } from './canonical-json.mjs';
2
-
3
1
  const MAX_LIMIT=1000;
4
- const SCHEMA='deadbyte.commander-observability.v1';
2
+ const SCHEMA='deadbyte.commander-observability.v2';
5
3
 
6
- function boundLimit(limit){if(!Number.isInteger(limit)||limit<1||limit>MAX_LIMIT)throw new Error('observability limit out of range');return limit;}
4
+ function boundLimit(limit){
5
+ if(!Number.isInteger(limit)||limit<1||limit>MAX_LIMIT)throw new Error('observability limit out of range');
6
+ return limit;
7
+ }
8
+ function parseSince(value){
9
+ if(value===undefined||value===null)return null;
10
+ if(typeof value!=='string')throw new Error('since must be an ISO timestamp string');
11
+ const ms=Date.parse(value);
12
+ if(!Number.isFinite(ms))throw new Error('since must be a valid ISO timestamp');
13
+ return ms;
14
+ }
15
+ function retainedPage(history){
16
+ if(typeof history.snapshot==='function')return history.snapshot();
17
+ return history.read({afterSequence:0,limit:MAX_LIMIT});
18
+ }
19
+ function visibleToolEvent(event){
20
+ return event?.event_type==='tool.call'||event?.event_type==='tool.result'||event?.event_type==='tool.error';
21
+ }
7
22
 
8
23
  export function createCommanderObservability({history}={}){
9
- if(!history||typeof history.read!=='function'||typeof history.verify!=='function'||typeof history.operationId!=='string'||history.operationId.length<1) throw new Error('observability requires operation history');
10
- function recentToolCalls({afterSequence=0,limit=100}={}){
11
- boundLimit(limit);
12
- const page=history.read({afterSequence,limit});
13
- const events=page.events.filter(e=>e.event_type==='tool.call'||e.event_type==='tool.result'||e.event_type==='tool.error');
14
- return {schema:SCHEMA,operation_id:page.operation_id,events,next_sequence:page.next_sequence,head_event_hash:page.head_event_hash,earliest_sequence:page.earliest_sequence,overflow:page.overflow};
24
+ if(!history||typeof history.read!=='function'||typeof history.verify!=='function'||typeof history.operationId!=='string'||history.operationId.length<1) {
25
+ throw new Error('observability requires operation history');
15
26
  }
27
+
28
+ function recentToolCalls({afterSequence=null,limit=null,maxResults=null,toolName=null,since=null}={}){
29
+ const referenceMode=maxResults!==null||toolName!==null||since!==null;
30
+ if(referenceMode){
31
+ const requested=maxResults??50;
32
+ if(!Number.isFinite(requested)||requested<1||requested>MAX_LIMIT) throw new Error('observability limit out of range');
33
+ // Reference Array.slice(-limit) truncates fractional counts toward zero.
34
+ const bounded=Math.trunc(requested);
35
+ if(toolName!==null&&typeof toolName!=='string')throw new Error('toolName invalid');
36
+ const sinceMs=parseSince(since);
37
+ const page=retainedPage(history);
38
+ let events=page.events.filter(visibleToolEvent);
39
+ if(toolName!==null)events=events.filter(event=>event.payload?.tool===toolName);
40
+ if(sinceMs!==null)events=events.filter(event=>Number(event.at_ms)>=sinceMs);
41
+ if(events.length>bounded)events=events.slice(events.length-bounded);
42
+ return {
43
+ schema:SCHEMA,operation_id:page.operation_id,events,
44
+ next_sequence:page.next_sequence,head_event_hash:page.head_event_hash,
45
+ earliest_sequence:page.earliest_sequence,overflow:page.overflow
46
+ };
47
+ }
48
+ const cursor=afterSequence??0;
49
+ const bounded=boundLimit(limit??100);
50
+ const page=history.read({afterSequence:cursor,limit:bounded});
51
+ const events=page.events.filter(visibleToolEvent);
52
+ return {
53
+ schema:SCHEMA,operation_id:page.operation_id,events,
54
+ next_sequence:page.next_sequence,head_event_hash:page.head_event_hash,
55
+ earliest_sequence:page.earliest_sequence,overflow:page.overflow
56
+ };
57
+ }
58
+
16
59
  function usageStats(){
17
- const page=history.read({afterSequence:0,limit:MAX_LIMIT});
60
+ const page=retainedPage(history);
18
61
  const counts={};
19
62
  let toolCalls=0,toolResults=0,toolErrors=0;
20
63
  for(const event of page.events){
21
- if(event.event_type==='tool.call') toolCalls++;
22
- else if(event.event_type==='tool.result') toolResults++;
23
- else if(event.event_type==='tool.error') toolErrors++;
64
+ if(event.event_type==='tool.call')toolCalls++;
65
+ else if(event.event_type==='tool.result')toolResults++;
66
+ else if(event.event_type==='tool.error')toolErrors++;
24
67
  else continue;
25
68
  const name=event.payload?.tool;
26
- if(typeof name==='string'&&name.length) counts[name]=(counts[name]??0)+1;
69
+ if(typeof name==='string'&&name.length)counts[name]=(counts[name]??0)+1;
27
70
  }
28
- return {schema:SCHEMA,operation_id:page.operation_id,events_visible:page.events.length,tool_calls:toolCalls,tool_results:toolResults,tool_errors:toolErrors,tool_counts:Object.fromEntries(Object.entries(counts).sort(([a],[b])=>a.localeCompare(b))),head_event_hash:page.head_event_hash,earliest_sequence:page.earliest_sequence,overflow:page.overflow,integrity:history.verify()};
71
+ return {
72
+ schema:SCHEMA,operation_id:page.operation_id,events_visible:page.events.length,
73
+ tool_calls:toolCalls,tool_results:toolResults,tool_errors:toolErrors,
74
+ tool_counts:Object.fromEntries(Object.entries(counts).sort(([a],[b])=>a.localeCompare(b))),
75
+ head_event_hash:page.head_event_hash,earliest_sequence:page.earliest_sequence,
76
+ overflow:page.overflow,integrity:history.verify()
77
+ };
29
78
  }
79
+
30
80
  return Object.freeze({usageStats,recentToolCalls});
31
81
  }
32
82
 
@@ -0,0 +1,548 @@
1
+ import { inflateRawSync } from 'node:zlib';
2
+ import { createHash } from 'node:crypto';
3
+ import path from 'node:path';
4
+
5
+ const ZIP_LOCAL=0x04034b50;
6
+ const ZIP_CENTRAL=0x02014b50;
7
+ const ZIP_EOCD=0x06054b50;
8
+ const MAX_ENTRIES=4096;
9
+ const MAX_ENTRY_BYTES=32*1024*1024;
10
+ const MAX_TOTAL_BYTES=64*1024*1024;
11
+ const UTF8=new TextDecoder('utf-8',{fatal:true});
12
+
13
+ let crcTable=null;
14
+ function getCrcTable(){
15
+ if(crcTable) return crcTable;
16
+ crcTable=new Uint32Array(256);
17
+ for(let i=0;i<256;i++){
18
+ let c=i;
19
+ for(let k=0;k<8;k++) c=(c>>>1)^((c&1)?0xedb88320:0);
20
+ crcTable[i]=c>>>0;
21
+ }
22
+ return crcTable;
23
+ }
24
+ function crc32(bytes){
25
+ const table=getCrcTable(); let c=0xffffffff;
26
+ for(const b of bytes)c=(c>>>8)^table[(c^b)&0xff];
27
+ return (c^0xffffffff)>>>0;
28
+ }
29
+ function decodeUtf8(bytes,label){
30
+ try{return UTF8.decode(bytes);}catch{throw new Error(`${label} is not valid UTF-8`);}
31
+ }
32
+ function safeZipName(name){
33
+ if(!name||name.includes('\0')||name.includes('\\')||name.startsWith('/')||/^[A-Za-z]:/.test(name)) throw new Error(`unsafe ZIP entry '${name}'`);
34
+ const parts=name.split('/');
35
+ if(parts.some(p=>p==='..'||p==='')) {
36
+ if(name.endsWith('/')&&parts.at(-1)==='') parts.pop();
37
+ if(parts.some(p=>p==='..'||p==='')) throw new Error(`unsafe ZIP entry '${name}'`);
38
+ }
39
+ return parts.join('/')+(name.endsWith('/')?'/':'');
40
+ }
41
+ function findEocd(buf){
42
+ if(buf.length<22) throw new Error('invalid ZIP: EOCD missing');
43
+ const floor=Math.max(0,buf.length-22-65535);
44
+ for(let i=buf.length-22;i>=floor;i--){
45
+ if(buf.readUInt32LE(i)===ZIP_EOCD){
46
+ const comment=buf.readUInt16LE(i+20);
47
+ if(i+22+comment===buf.length) return i;
48
+ }
49
+ }
50
+ throw new Error('invalid ZIP: EOCD missing');
51
+ }
52
+ function unzipOffice(input){
53
+ const buf=Buffer.isBuffer(input)?input:Buffer.from(input);
54
+ const eocd=findEocd(buf);
55
+ const disk=buf.readUInt16LE(eocd+4),centralDisk=buf.readUInt16LE(eocd+6);
56
+ const diskEntries=buf.readUInt16LE(eocd+8),entries=buf.readUInt16LE(eocd+10);
57
+ const centralSize=buf.readUInt32LE(eocd+12),centralOffset=buf.readUInt32LE(eocd+16);
58
+ if(disk!==0||centralDisk!==0||diskEntries!==entries) throw new Error('multi-disk ZIP is not supported');
59
+ if(entries===0xffff||centralSize===0xffffffff||centralOffset===0xffffffff) throw new Error('ZIP64 office packages are not supported');
60
+ if(entries>MAX_ENTRIES) throw new Error(`ZIP entry count exceeds ${MAX_ENTRIES}`);
61
+ if(centralOffset+centralSize>eocd) throw new Error('invalid ZIP central directory bounds');
62
+ const files=new Map();
63
+ let p=centralOffset,total=0;
64
+ for(let index=0;index<entries;index++){
65
+ if(p+46>centralOffset+centralSize||buf.readUInt32LE(p)!==ZIP_CENTRAL) throw new Error('invalid ZIP central directory entry');
66
+ const flags=buf.readUInt16LE(p+8),method=buf.readUInt16LE(p+10),expectedCrc=buf.readUInt32LE(p+16);
67
+ const compressedSize=buf.readUInt32LE(p+20),uncompressedSize=buf.readUInt32LE(p+24);
68
+ const nameLen=buf.readUInt16LE(p+28),extraLen=buf.readUInt16LE(p+30),commentLen=buf.readUInt16LE(p+32);
69
+ const diskStart=buf.readUInt16LE(p+34),localOffset=buf.readUInt32LE(p+42);
70
+ const recordEnd=p+46+nameLen+extraLen+commentLen;
71
+ if(recordEnd>centralOffset+centralSize) throw new Error('invalid ZIP central entry bounds');
72
+ if(diskStart!==0) throw new Error('multi-disk ZIP entry is not supported');
73
+ if(flags&1) throw new Error('encrypted ZIP entries are not supported');
74
+ if(method!==0&&method!==8) throw new Error(`ZIP compression method ${method} is not supported`);
75
+ if(uncompressedSize>MAX_ENTRY_BYTES) throw new Error(`ZIP entry exceeds ${MAX_ENTRY_BYTES} byte limit`);
76
+ total+=uncompressedSize;
77
+ if(total>MAX_TOTAL_BYTES) throw new Error(`ZIP uncompressed content exceeds ${MAX_TOTAL_BYTES} byte limit`);
78
+ const name=safeZipName(decodeUtf8(buf.subarray(p+46,p+46+nameLen),'ZIP entry name'));
79
+ if(files.has(name)) throw new Error(`duplicate ZIP entry '${name}'`);
80
+ if(localOffset+30>centralOffset||buf.readUInt32LE(localOffset)!==ZIP_LOCAL) throw new Error(`invalid local ZIP header for '${name}'`);
81
+ const localFlags=buf.readUInt16LE(localOffset+6),localMethod=buf.readUInt16LE(localOffset+8);
82
+ const localNameLen=buf.readUInt16LE(localOffset+26),localExtraLen=buf.readUInt16LE(localOffset+28);
83
+ if((localFlags&1)!==0||localMethod!==method) throw new Error(`ZIP local header mismatch for '${name}'`);
84
+ const localName=decodeUtf8(buf.subarray(localOffset+30,localOffset+30+localNameLen),'ZIP local entry name');
85
+ if(localName!==name) throw new Error(`ZIP local name mismatch for '${name}'`);
86
+ const dataStart=localOffset+30+localNameLen+localExtraLen,dataEnd=dataStart+compressedSize;
87
+ if(dataStart<0||dataEnd>centralOffset||dataEnd>buf.length) throw new Error(`ZIP compressed data bounds invalid for '${name}'`);
88
+ if(!name.endsWith('/')){
89
+ const compressed=buf.subarray(dataStart,dataEnd);
90
+ let data;
91
+ try{
92
+ data=method===0?Buffer.from(compressed):inflateRawSync(compressed,{maxOutputLength:MAX_ENTRY_BYTES});
93
+ }catch(error){throw new Error(`ZIP inflate failed for '${name}': ${error instanceof Error?error.message:String(error)}`);}
94
+ if(data.length!==uncompressedSize) throw new Error(`ZIP size mismatch for '${name}'`);
95
+ if(crc32(data)!==expectedCrc) throw new Error(`ZIP CRC32 mismatch for '${name}'`);
96
+ files.set(name,data);
97
+ }
98
+ p=recordEnd;
99
+ }
100
+ if(p!==centralOffset+centralSize) throw new Error('ZIP central directory size mismatch');
101
+ return files;
102
+ }
103
+
104
+ function xmlDecode(value){
105
+ return String(value)
106
+ .replace(/&#x([0-9a-fA-F]+);/g,(_,x)=>String.fromCodePoint(parseInt(x,16)))
107
+ .replace(/&#([0-9]+);/g,(_,x)=>String.fromCodePoint(parseInt(x,10)))
108
+ .replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"').replace(/&apos;/g,"'").replace(/&amp;/g,'&');
109
+ }
110
+ function xmlAttrs(fragment){
111
+ const out={};
112
+ const re=/([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
113
+ for(const m of fragment.matchAll(re)) out[m[1]]=xmlDecode(m[2]??m[3]??'');
114
+ return out;
115
+ }
116
+ function xmlTextFragments(fragment,tagPattern='(?:w:)?t'){
117
+ const re=new RegExp(`<${tagPattern}\\b[^>]*>([\\s\\S]*?)<\\/${tagPattern}>`,'g');
118
+ return [...fragment.matchAll(re)].map(m=>xmlDecode(m[1].replace(/<[^>]+>/g,'')));
119
+ }
120
+ function wordText(fragment){
121
+ const token=/<w:t\b[^>]*>[\s\S]*?<\/w:t>|<w:tab\b[^>]*\/>|<w:(?:br|cr)\b[^>]*\/>/g;
122
+ let out='';
123
+ for(const m of fragment.matchAll(token)){
124
+ const s=m[0];
125
+ if(/^<w:t(?:\s|>)/.test(s)) out+=xmlDecode((s.match(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/)?.[1]??'').replace(/<[^>]+>/g,''));
126
+ else if(s.startsWith('<w:tab')) out+='\t';
127
+ else out+='\n';
128
+ }
129
+ return out;
130
+ }
131
+ function prettyXml(xml){
132
+ const pieces=xml.replace(/>\s+</g,'><').split(/(?<=>)(?=<)/);
133
+ const lines=[]; let depth=0;
134
+ for(const piece of pieces){
135
+ const t=piece.trim(); if(!t) continue;
136
+ const closing=t.startsWith('</'),self=t.endsWith('/>'),pi=t.startsWith('<?'),inline=!closing&&!self&&/^<[^>]+>.*<\//s.test(t);
137
+ if(closing) depth=Math.max(0,depth-1);
138
+ lines.push(' '.repeat(depth)+t);
139
+ if(!closing&&!self&&!pi&&!inline) depth++;
140
+ }
141
+ return lines.join('\n');
142
+ }
143
+ function docxOutline(xml){
144
+ const body=xml.match(/<w:body\b[^>]*>([\s\S]*?)<\/w:body>/)?.[1];
145
+ if(body===undefined) return '[No w:body found in document.xml]';
146
+ const elements=[...body.matchAll(/<w:(p|tbl)\b[\s\S]*?<\/w:\1>/g)].map(m=>({kind:m[1],xml:m[0]}));
147
+ const lines=[];
148
+ for(let i=0;i<elements.length;i++){
149
+ const item=elements[i];
150
+ if(item.kind==='p'){
151
+ const style=item.xml.match(/<w:pStyle\b[^>]*w:val=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean);
152
+ let line=`[${i}] w:p`;
153
+ if(style) line+=` style="${xmlDecode(style)}"`;
154
+ const value=wordText(item.xml);
155
+ if(value) line+='\n '+value;
156
+ lines.push(line);
157
+ }else{
158
+ lines.push(`[${i}] w:tbl`);
159
+ const rows=[...item.xml.matchAll(/<w:tr\b[\s\S]*?<\/w:tr>/g)].map(m=>m[0]);
160
+ for(let r=0;r<rows.length;r++){
161
+ const cells=[...rows[r].matchAll(/<w:tc\b[\s\S]*?<\/w:tc>/g)].map(m=>wordText(m[0]).replace(/\s*\n\s*/g,' ').trim());
162
+ lines.push(` Row ${r}: ${cells.join(' | ')}`);
163
+ }
164
+ }
165
+ }
166
+ return lines.join('\n');
167
+ }
168
+
169
+ export function readDocxBytes(input,{path:displayPath='document.docx',offset=0,length=1000}={}){
170
+ const files=unzipOffice(input);
171
+ const doc=files.get('word/document.xml');
172
+ if(!doc) throw new Error('Invalid DOCX: missing word/document.xml');
173
+ const xml=decodeUtf8(doc,'word/document.xml');
174
+ const pretty=prettyXml(xml),allLines=pretty.split('\n'),totalLines=allLines.length;
175
+ if(!Number.isInteger(offset)||!Number.isInteger(length)||length<0) throw new Error('DOCX offset/length invalid');
176
+ if(offset!==0){
177
+ const start=offset<0?Math.max(0,totalLines+offset):Math.min(offset,totalLines);
178
+ const count=offset<0?totalLines-start:length;
179
+ const sliced=allLines.slice(start,start+count),remaining=Math.max(0,totalLines-(start+sliced.length));
180
+ return {content:`[DOCX XML: lines ${start}-${Math.max(start,start+sliced.length-1)} of ${totalLines} (${remaining} remaining)]\n${sliced.join('\n')}`,
181
+ mimeType:'application/xml',metadata:{isDocx:true,lineCount:totalLines},outline:false};
182
+ }
183
+ const outline=docxOutline(xml);
184
+ const rawSizeKB=(Buffer.byteLength(xml)/1024).toFixed(1);
185
+ return {content:`${outline}\n\nRaw XML: ${totalLines} lines, ${rawSizeKB}KB.`,mimeType:'text/plain',metadata:{isDocx:true,lineCount:totalLines},outline:true};
186
+ }
187
+
188
+
189
+ export function createDocxBytes(content){
190
+ if(typeof content!=='string') throw new Error('DOCX content must be string');
191
+ if(Buffer.byteLength(content,'utf8')>4*1024*1024) throw new Error('DOCX content exceeds bounded native write limit');
192
+ if(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/u.test(content)) throw new Error('DOCX content contains invalid XML control character');
193
+ const paragraphs=content.split('\n').map(line=>{
194
+ const heading=/^(#{1,6})\s+(.+)/.exec(line);
195
+ if(heading) return `<w:p><w:pPr><w:pStyle w:val="Heading${heading[1].length}"/></w:pPr><w:r><w:t>${xmlEscapeText(heading[2])}</w:t></w:r></w:p>`;
196
+ if(!line.trim()) return '<w:p/>';
197
+ return `<w:p><w:r><w:t xml:space="preserve">${xmlEscapeText(line)}</w:t></w:r></w:p>`;
198
+ }).join('');
199
+ const files=new Map([
200
+ ['[Content_Types].xml',Buffer.from('<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>')],
201
+ ['_rels/.rels',Buffer.from('<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>')],
202
+ ['word/document.xml',Buffer.from(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>${paragraphs}<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/></w:sectPr></w:body></w:document>`)]
203
+ ]);
204
+ const bytes=writeStoredZip(files);
205
+ unzipOffice(bytes);
206
+ return {bytes,bytes_sha256:officeSha256(bytes)};
207
+ }
208
+
209
+ export function editDocxBytes(input,{oldString,newString,expectedReplacements=1}={}){
210
+ const original=Buffer.isBuffer(input)?Buffer.from(input):Buffer.from(input);
211
+ if(typeof oldString!=='string'||oldString.length<1) throw new Error('DOCX oldString must be non-empty string');
212
+ if(typeof newString!=='string') throw new Error('DOCX newString must be string');
213
+ if(!Number.isInteger(expectedReplacements)||expectedReplacements<0) throw new Error('DOCX expectedReplacements must be non-negative integer');
214
+ const files=unzipOffice(original);
215
+ const doc=files.get('word/document.xml');
216
+ if(!doc) throw new Error('Invalid DOCX: missing word/document.xml');
217
+ const xml=decodeUtf8(doc,'word/document.xml');
218
+ const pretty=prettyXml(xml);
219
+ let count=0,index=0;
220
+ while((index=pretty.indexOf(oldString,index))>=0){count++;index+=Math.max(1,oldString.length);}
221
+ if(count!==expectedReplacements) throw new Error(`DOCX expected ${expectedReplacements} replacements, found ${count}`);
222
+ const editedPretty=pretty.split(oldString).join(newString);
223
+ const compact=editedPretty.split('\n').map(line=>line.trimStart()).join('');
224
+ files.set('word/document.xml',Buffer.from(compact,'utf8'));
225
+ const bytes=writeStoredZip(files);
226
+ const verify=unzipOffice(bytes);
227
+ const verifiedDoc=verify.get('word/document.xml');
228
+ if(!verifiedDoc) throw new Error('DOCX repack verification missing word/document.xml');
229
+ decodeUtf8(verifiedDoc,'word/document.xml');
230
+ return {
231
+ bytes,bytes_base64:bytes.toString('base64'),replacements:count,
232
+ before_sha256:officeSha256(original),after_sha256:officeSha256(bytes)
233
+ };
234
+ }
235
+
236
+ function normalizeRelTarget(target){
237
+ const clean=String(target).replaceAll('\\','/');
238
+ const joined=path.posix.normalize(path.posix.join('xl',clean));
239
+ if(joined==='..'||joined.startsWith('../')||!joined.startsWith('xl/')) throw new Error('unsafe XLSX relationship target');
240
+ return joined;
241
+ }
242
+ function parseWorkbook(files){
243
+ const workbookBytes=files.get('xl/workbook.xml');
244
+ const relBytes=files.get('xl/_rels/workbook.xml.rels');
245
+ if(!workbookBytes||!relBytes) throw new Error('Invalid XLSX: workbook metadata missing');
246
+ const workbook=decodeUtf8(workbookBytes,'xl/workbook.xml');
247
+ const rels=decodeUtf8(relBytes,'xl/_rels/workbook.xml.rels');
248
+ const relMap=new Map();
249
+ for(const m of rels.matchAll(/<Relationship\b([^>]*)\/?\s*>/g)){
250
+ const a=xmlAttrs(m[1]);
251
+ if(a.Id&&a.Target) relMap.set(a.Id,normalizeRelTarget(a.Target));
252
+ }
253
+ const sheets=[];
254
+ for(const m of workbook.matchAll(/<sheet\b([^>]*)\/?\s*>/g)){
255
+ const a=xmlAttrs(m[1]),id=a['r:id'];
256
+ if(!a.name||!id||!relMap.has(id)) continue;
257
+ sheets.push({name:a.name,path:relMap.get(id)});
258
+ }
259
+ if(sheets.length===0) throw new Error('Invalid XLSX: no worksheets');
260
+ return sheets;
261
+ }
262
+ function sharedStrings(files){
263
+ const bytes=files.get('xl/sharedStrings.xml'); if(!bytes) return [];
264
+ const xml=decodeUtf8(bytes,'xl/sharedStrings.xml');
265
+ return [...xml.matchAll(/<si\b[^>]*>([\s\S]*?)<\/si>/g)].map(m=>xmlTextFragments(m[1],'t').join(''));
266
+ }
267
+ function columnNumber(ref){
268
+ const letters=ref.match(/^[A-Za-z]+/)?.[0]; if(!letters) return null;
269
+ let n=0; for(const ch of letters.toUpperCase()) n=n*26+(ch.charCodeAt(0)-64);
270
+ return n;
271
+ }
272
+ function cellReference(ref){
273
+ const m=/^([A-Za-z]+)([1-9][0-9]*)$/.exec(ref); if(!m) return null;
274
+ return {col:columnNumber(m[1]),row:Number(m[2])};
275
+ }
276
+ function parseWorksheet(files,sheet,shared){
277
+ const bytes=files.get(sheet.path); if(!bytes) throw new Error(`Invalid XLSX: worksheet '${sheet.name}' is missing`);
278
+ const xml=decodeUtf8(bytes,sheet.path);
279
+ const cells=new Map(); let maxRow=0,maxCol=0;
280
+ for(const m of xml.matchAll(/<c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/g)){
281
+ const a=xmlAttrs(m[1]),ref=cellReference(a.r??''); if(!ref) continue;
282
+ const body=m[2]??'',type=a.t??'';
283
+ const rawV=body.match(/<v\b[^>]*>([\s\S]*?)<\/v>/)?.[1]??null;
284
+ let value=null;
285
+ if(type==='s'&&rawV!==null){const index=Number(rawV);value=Number.isInteger(index)&&index>=0&&index<shared.length?shared[index]:null;}
286
+ else if(type==='inlineStr') value=xmlTextFragments(body,'t').join('');
287
+ else if(type==='b'&&rawV!==null) value=String(rawV).trim()==='1';
288
+ else if((type==='str'||type==='d'||type==='e')&&rawV!==null) value=xmlDecode(rawV);
289
+ else if(rawV!==null){const s=xmlDecode(rawV).trim();const n=Number(s);value=s!==''&&Number.isFinite(n)?n:s;}
290
+ cells.set(`${ref.row}:${ref.col}`,value);maxRow=Math.max(maxRow,ref.row);maxCol=Math.max(maxCol,ref.col);
291
+ }
292
+ return {sheet,cells,maxRow,maxCol};
293
+ }
294
+ function parseA1Range(value){
295
+ const m=/^([A-Za-z]+[1-9][0-9]*)(?::([A-Za-z]+[1-9][0-9]*))?$/.exec(value??'');
296
+ if(!m) throw new Error(`invalid XLSX range '${value}'`);
297
+ const a=cellReference(m[1]),b=cellReference(m[2]??m[1]);
298
+ if(!a||!b||b.row<a.row||b.col<a.col) throw new Error(`invalid XLSX range '${value}'`);
299
+ return {startRow:a.row,startCol:a.col,endRow:b.row,endCol:b.col};
300
+ }
301
+ function parseSheetQualifiedRange(value){
302
+ if(typeof value!=='string'||!value.includes('!')) return {sheet:null,range:value};
303
+ const bang=value.lastIndexOf('!');
304
+ let sheet=value.slice(0,bang),range=value.slice(bang+1);
305
+ if(sheet.startsWith("'")&&sheet.endsWith("'")) sheet=sheet.slice(1,-1).replaceAll("''","'");
306
+ return {sheet,range};
307
+ }
308
+ function chooseSheet(sheets,sheetRef){
309
+ if(sheetRef===undefined||sheetRef===null) return sheets[0];
310
+ if(typeof sheetRef==='number'){
311
+ if(!Number.isInteger(sheetRef)||sheetRef<0||sheetRef>=sheets.length) throw new Error(`Sheet index ${sheetRef} out of range`);
312
+ return sheets[sheetRef];
313
+ }
314
+ const found=sheets.find(s=>s.name===sheetRef);
315
+ if(!found) throw new Error(`Sheet "${sheetRef}" not found. Available sheets: ${sheets.map(s=>s.name).join(', ')}`);
316
+ return found;
317
+ }
318
+ function sheetArray(parsed,{range,offset=0,length=1000}={}){
319
+ let startRow=1,startCol=1,endRow=Math.max(1,parsed.maxRow),endCol=Math.max(1,parsed.maxCol);
320
+ if(range){const r=parseA1Range(range);({startRow,startCol,endRow,endCol}=r);}
321
+ const totalRows=Math.max(0,endRow-startRow+1);
322
+ if(!Number.isInteger(offset)||!Number.isInteger(length)||length<0) throw new Error('XLSX offset/length invalid');
323
+ if(offset<0) startRow=Math.max(startRow,endRow-Math.abs(offset)+1);
324
+ else if(offset>0) startRow+=offset;
325
+ if(offset>=0&&length>0) endRow=Math.min(endRow,startRow+length-1);
326
+ if(startRow>endRow) return {data:[],totalRows,returnedRows:0};
327
+ const data=[];
328
+ for(let row=startRow;row<=endRow;row++){
329
+ const out=[];for(let col=startCol;col<=endCol;col++) out.push(parsed.cells.get(`${row}:${col}`)??null);
330
+ data.push(out);
331
+ }
332
+ return {data,totalRows,returnedRows:data.length};
333
+ }
334
+
335
+ export function readXlsxBytes(input,{path:displayPath='workbook.xlsx',sheet=undefined,range=undefined,offset=0,length=1000}={}){
336
+ const bytes=Buffer.isBuffer(input)?input:Buffer.from(input);
337
+ const files=unzipOffice(bytes),sheets=parseWorkbook(files),shared=sharedStrings(files);
338
+ let rangeOnly=range,sheetRef=sheet;
339
+ if(range&&range.includes('!')){
340
+ const parsed=parseSheetQualifiedRange(range); rangeOnly=parsed.range;
341
+ if(sheetRef===undefined) sheetRef=parsed.sheet;
342
+ }
343
+ const selected=chooseSheet(sheets,sheetRef);
344
+ const parsedSelected=parseWorksheet(files,selected,shared);
345
+ const rows=sheetArray(parsedSelected,{range:rangeOnly,offset,length});
346
+ const metadataSheets=sheets.map(s=>{
347
+ const p=parseWorksheet(files,s,shared);return {name:s.name,rowCount:p.maxRow,colCount:p.maxCol};
348
+ });
349
+ const pagination=rows.totalRows>rows.returnedRows?`\n[Showing rows ${(offset>=0?offset:0)+1}-${(offset>=0?offset:0)+rows.returnedRows} of ${rows.totalRows} total. Use offset/length to paginate.]`:'';
350
+ const example=/\s/.test(selected.name)?selected.name:'Sheet1';
351
+ const content=`[Sheet: '${selected.name}' from ${displayPath}]${pagination}\n[To MODIFY cells: use edit_block with range param, e.g., edit_block(path, {range: "${example}!E5", content: [[newValue]]}). read_file accepts the same range form, or pass sheet + range separately.]\n\n${JSON.stringify(rows.data)}`;
352
+ return {content,mimeType:'application/json',metadata:{isExcelFile:true,sheets:metadataSheets,fileSize:bytes.length,isLargeFile:bytes.length>10*1024*1024},
353
+ sheetName:selected.name,data:rows.data,totalRows:rows.totalRows,returnedRows:rows.returnedRows};
354
+ }
355
+
356
+
357
+ function officeSha256(bytes){return createHash('sha256').update(bytes).digest('hex');}
358
+ function xmlEscapeText(value){
359
+ return String(value).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;');
360
+ }
361
+ function columnLetters(column){
362
+ if(!Number.isInteger(column)||column<1) throw new Error('XLSX column invalid');
363
+ let n=column,out='';
364
+ while(n>0){n--;out=String.fromCharCode(65+(n%26))+out;n=Math.floor(n/26);}
365
+ return out;
366
+ }
367
+ function xlsxCellXml(ref,value){
368
+ if(value===null) return `<c r="${ref}"/>`;
369
+ if(typeof value==='string'){
370
+ const preserve=/^\s|\s$/.test(value)?' xml:space="preserve"':'';
371
+ return `<c r="${ref}" t="inlineStr"><is><t${preserve}>${xmlEscapeText(value)}</t></is></c>`;
372
+ }
373
+ if(typeof value==='number'){
374
+ if(!Number.isFinite(value)) throw new Error('unsupported XLSX cell value: non-finite number');
375
+ return `<c r="${ref}"><v>${String(value)}</v></c>`;
376
+ }
377
+ if(typeof value==='boolean') return `<c r="${ref}" t="b"><v>${value?'1':'0'}</v></c>`;
378
+ throw new Error(`unsupported XLSX cell value type '${typeof value}'`);
379
+ }
380
+ function rowNumberFromXml(fragment){
381
+ const open=fragment.match(/^<row\b([^>]*)>/)?.[1]??'';
382
+ const value=xmlAttrs(open).r;
383
+ const n=Number(value);
384
+ return Number.isInteger(n)&&n>0?n:null;
385
+ }
386
+ function cellColumnFromXml(fragment){
387
+ const open=fragment.match(/^<c\b([^>]*?)(?:\/?>)/)?.[1]??'';
388
+ const ref=xmlAttrs(open).r??'';
389
+ return cellReference(ref)?.col??null;
390
+ }
391
+ function editWorksheetCell(xml,row,column,value){
392
+ const ref=`${columnLetters(column)}${row}`,replacement=xlsxCellXml(ref,value);
393
+ const sheetData=xml.match(/<sheetData\b[^>]*>([\s\S]*?)<\/sheetData>/);
394
+ if(!sheetData) throw new Error('Invalid XLSX: worksheet sheetData missing');
395
+ const full=sheetData[0],body=sheetData[1];
396
+ const bodyOffset=full.indexOf(body);
397
+ const rows=[...body.matchAll(/<row\b[^>]*>[\s\S]*?<\/row>/g)];
398
+ const rowMatch=rows.find(m=>rowNumberFromXml(m[0])===row);
399
+ let nextBody;
400
+ if(rowMatch){
401
+ const rowXml=rowMatch[0],openEnd=rowXml.indexOf('>')+1,closeStart=rowXml.lastIndexOf('</row>');
402
+ const rowOpen=rowXml.slice(0,openEnd),rowInner=rowXml.slice(openEnd,closeStart);
403
+ const cells=[...rowInner.matchAll(/<c\b[^>]*(?:\/>|>[\s\S]*?<\/c>)/g)];
404
+ const exact=cells.find(m=>(m[0].match(/^<c\b([^>]*?)(?:\/?>)/)?.[1]&&xmlAttrs(m[0].match(/^<c\b([^>]*?)(?:\/?>)/)[1]).r===ref));
405
+ let nextInner;
406
+ if(exact){
407
+ nextInner=rowInner.slice(0,exact.index)+replacement+rowInner.slice(exact.index+exact[0].length);
408
+ }else{
409
+ const later=cells.find(m=>(cellColumnFromXml(m[0])??Number.MAX_SAFE_INTEGER)>column);
410
+ const at=later?later.index:rowInner.length;
411
+ nextInner=rowInner.slice(0,at)+replacement+rowInner.slice(at);
412
+ }
413
+ const nextRow=rowOpen+nextInner+'</row>';
414
+ nextBody=body.slice(0,rowMatch.index)+nextRow+body.slice(rowMatch.index+rowXml.length);
415
+ }else{
416
+ const newRow=`<row r="${row}">${replacement}</row>`;
417
+ const later=rows.find(m=>(rowNumberFromXml(m[0])??Number.MAX_SAFE_INTEGER)>row);
418
+ const at=later?later.index:body.length;
419
+ nextBody=body.slice(0,at)+newRow+body.slice(at);
420
+ }
421
+ return xml.slice(0,sheetData.index+bodyOffset)+nextBody+xml.slice(sheetData.index+bodyOffset+body.length);
422
+ }
423
+ function writeStoredZip(files){
424
+ const locals=[],centrals=[];let offset=0;
425
+ const entries=[...files.entries()].filter(([name])=>!name.endsWith('/')).sort((a,b)=>a[0].localeCompare(b[0],'en'));
426
+ for(const [name,value] of entries){
427
+ const n=Buffer.from(name,'utf8'),data=Buffer.from(value),crc=crc32(data);
428
+ const local=Buffer.alloc(30);
429
+ local.writeUInt32LE(ZIP_LOCAL,0);local.writeUInt16LE(20,4);local.writeUInt16LE(0x800,6);local.writeUInt16LE(0,8);
430
+ local.writeUInt32LE(crc,14);local.writeUInt32LE(data.length,18);local.writeUInt32LE(data.length,22);local.writeUInt16LE(n.length,26);
431
+ locals.push(local,n,data);
432
+ const central=Buffer.alloc(46);
433
+ central.writeUInt32LE(ZIP_CENTRAL,0);central.writeUInt16LE(20,4);central.writeUInt16LE(20,6);central.writeUInt16LE(0x800,8);central.writeUInt16LE(0,10);
434
+ central.writeUInt32LE(crc,16);central.writeUInt32LE(data.length,20);central.writeUInt32LE(data.length,24);central.writeUInt16LE(n.length,28);central.writeUInt32LE(offset,42);
435
+ centrals.push(central,n);offset+=local.length+n.length+data.length;
436
+ }
437
+ const localBytes=Buffer.concat(locals),centralBytes=Buffer.concat(centrals),eocd=Buffer.alloc(22);
438
+ eocd.writeUInt32LE(ZIP_EOCD,0);eocd.writeUInt16LE(entries.length,8);eocd.writeUInt16LE(entries.length,10);
439
+ eocd.writeUInt32LE(centralBytes.length,12);eocd.writeUInt32LE(localBytes.length,16);
440
+ return Buffer.concat([localBytes,centralBytes,eocd]);
441
+ }
442
+
443
+ export function editXlsxBytes(input,{range,content}={}){
444
+ const original=Buffer.isBuffer(input)?Buffer.from(input):Buffer.from(input);
445
+ const files=unzipOffice(original),sheets=parseWorkbook(files);
446
+ const qualified=parseSheetQualifiedRange(range);
447
+ const selected=chooseSheet(sheets,qualified.sheet);
448
+ const bounds=parseA1Range(qualified.range);
449
+ const expectedRows=bounds.endRow-bounds.startRow+1,expectedCols=bounds.endCol-bounds.startCol+1;
450
+ if(!Array.isArray(content)||content.length!==expectedRows||content.some(row=>!Array.isArray(row)||row.length!==expectedCols)) {
451
+ throw new Error(`XLSX range/content shape mismatch: expected ${expectedRows}x${expectedCols}`);
452
+ }
453
+ const sheetBytes=files.get(selected.path);
454
+ if(!sheetBytes) throw new Error(`Invalid XLSX: worksheet '${selected.name}' is missing`);
455
+ let xml=decodeUtf8(sheetBytes,selected.path),updated=0;
456
+ for(let r=0;r<expectedRows;r++) for(let col=0;col<expectedCols;col++){
457
+ xml=editWorksheetCell(xml,bounds.startRow+r,bounds.startCol+col,content[r][col]);updated++;
458
+ }
459
+ files.set(selected.path,Buffer.from(xml,'utf8'));
460
+ const bytes=writeStoredZip(files);
461
+ unzipOffice(bytes);
462
+ return {bytes,bytes_base64:bytes.toString('base64'),before_sha256:officeSha256(original),after_sha256:officeSha256(bytes),updated_cells:updated,sheet:selected.name,range:qualified.range};
463
+ }
464
+
465
+
466
+ function xmlEscapeAttr(value){
467
+ return xmlEscapeText(value).replaceAll('"','&quot;').replaceAll("'","&apos;");
468
+ }
469
+ function validateSheetName(name){
470
+ const forbidden=['[',']',':','*','?','/','\\\\'];
471
+ if(typeof name!=='string'||name.length<1||name.length>31||forbidden.some(ch=>name.includes(ch))) throw new Error(`XLSX sheet name invalid: '${String(name)}'`);
472
+ return name;
473
+ }
474
+ function validateSheetRows(rows,label){
475
+ if(!Array.isArray(rows)) throw new Error(`XLSX sheet '${label}' data must be a 2D array`);
476
+ if(rows.length>1048576) throw new Error(`XLSX sheet '${label}' exceeds row limit`);
477
+ let width=null,cells=0;
478
+ for(let r=0;r<rows.length;r++){
479
+ if(!Array.isArray(rows[r])) throw new Error(`XLSX sheet '${label}' row ${r+1} must be array`);
480
+ if(width===null) width=rows[r].length;
481
+ if(rows[r].length!==width) throw new Error(`XLSX sheet '${label}' data must be rectangular`);
482
+ if(rows[r].length>16384) throw new Error(`XLSX sheet '${label}' exceeds column limit`);
483
+ cells+=rows[r].length;
484
+ for(let col=0;col<rows[r].length;col++) xlsxCellXml(`${columnLetters(col+1)}${r+1}`,rows[r][col]);
485
+ }
486
+ if(cells>1000000) throw new Error(`XLSX sheet '${label}' exceeds 1000000 cell budget`);
487
+ return {rows:rows.length,cols:width??0,cells};
488
+ }
489
+ function normalizeWorkbookInput(value){
490
+ if(Array.isArray(value)) return new Map([['Sheet1',value]]);
491
+ if(!value||typeof value!=='object') throw new Error('XLSX content must be a 2D array or object of sheet arrays');
492
+ const out=new Map(),seen=new Set();
493
+ for(const [rawName,rows] of Object.entries(value)){
494
+ const name=validateSheetName(rawName),fold=name.toLowerCase();
495
+ if(seen.has(fold)) throw new Error(`XLSX duplicate sheet name '${name}'`);
496
+ seen.add(fold);validateSheetRows(rows,name);out.set(name,rows);
497
+ }
498
+ if(out.size<1) throw new Error('XLSX workbook must contain at least one sheet');
499
+ return out;
500
+ }
501
+ function worksheetXml(rows){
502
+ validateSheetRows(rows,'Sheet1');
503
+ const body=[];
504
+ for(let r=0;r<rows.length;r++){
505
+ const cells=rows[r].map((value,col)=>xlsxCellXml(`${columnLetters(col+1)}${r+1}`,value)).join('');
506
+ body.push(`<row r="${r+1}">${cells}</row>`);
507
+ }
508
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body.join('')}</sheetData></worksheet>`;
509
+ }
510
+ export function createXlsxBytes(value){
511
+ const workbook=normalizeWorkbookInput(value),files=new Map();
512
+ const sheets=[...workbook.entries()];
513
+ const contentOverrides=sheets.map((_,i)=>`<Override PartName="/xl/worksheets/sheet${i+1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`).join('');
514
+ files.set('[Content_Types].xml',Buffer.from(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>${contentOverrides}</Types>`,'utf8'));
515
+ files.set('_rels/.rels',Buffer.from('<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>','utf8'));
516
+ files.set('xl/workbook.xml',Buffer.from(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>${sheets.map(([name],i)=>`<sheet name="${xmlEscapeAttr(name)}" sheetId="${i+1}" r:id="rId${i+1}"/>`).join('')}</sheets></workbook>`,'utf8'));
517
+ files.set('xl/_rels/workbook.xml.rels',Buffer.from(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">${sheets.map((_,i)=>`<Relationship Id="rId${i+1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${i+1}.xml"/>`).join('')}</Relationships>`,'utf8'));
518
+ sheets.forEach(([name,rows],i)=>files.set(`xl/worksheets/sheet${i+1}.xml`,Buffer.from(worksheetXml(rows),'utf8')));
519
+ const bytes=writeStoredZip(files);
520
+ unzipOffice(bytes);
521
+ return {bytes,bytes_base64:bytes.toString('base64'),bytes_sha256:officeSha256(bytes),sheet_count:sheets.length};
522
+ }
523
+ export function appendXlsxBytes(input,value){
524
+ const original=Buffer.isBuffer(input)?Buffer.from(input):Buffer.from(input);
525
+ const files=unzipOffice(original),sheets=parseWorkbook(files),appends=normalizeWorkbookInput(value);
526
+ let appendedRows=0,updatedCells=0;
527
+ for(const [sheetName,rows] of appends){
528
+ validateSheetRows(rows,sheetName);
529
+ const selected=chooseSheet(sheets,sheetName);
530
+ const sheetBytes=files.get(selected.path);
531
+ if(!sheetBytes) throw new Error(`Invalid XLSX: worksheet '${selected.name}' is missing`);
532
+ let xml=decodeUtf8(sheetBytes,selected.path);
533
+ let maxRow=0;
534
+ for(const match of xml.matchAll(/<row\b[^>]*>[\s\S]*?<\/row>/g)){
535
+ const n=rowNumberFromXml(match[0]);if(n!==null&&n>maxRow) maxRow=n;
536
+ }
537
+ for(let r=0;r<rows.length;r++) for(let col=0;col<rows[r].length;col++){
538
+ xml=editWorksheetCell(xml,maxRow+r+1,col+1,rows[r][col]);updatedCells++;
539
+ }
540
+ appendedRows+=rows.length;
541
+ files.set(selected.path,Buffer.from(xml,'utf8'));
542
+ }
543
+ const bytes=writeStoredZip(files);
544
+ unzipOffice(bytes);
545
+ return {bytes,bytes_base64:bytes.toString('base64'),before_sha256:officeSha256(original),after_sha256:officeSha256(bytes),appended_rows:appendedRows,updated_cells:updatedCells};
546
+ }
547
+
548
+ export { unzipOffice as parseOfficeZip };