deadbyte-mcp 0.15.18 → 0.15.19
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/MANIFEST.SHA256 +54 -32
- package/PROVENANCE.json +13 -7
- package/README.txt +7 -7
- package/RELEASE.json +25 -6
- package/SBOM.cdx.json +3 -3
- package/TEST-MATRIX.json +26 -7
- package/controller/deadbyte-controller.ps1 +56 -1
- package/docs/COMMANDER_EXHAUSTIVE_SURFACE.json +247 -44
- package/docs/COMMANDER_REFERENCE_0_2_51.json +46 -0
- package/docs/R47-PROVENANCE.json +11 -0
- package/docs/R47-RELEASE.json +58 -0
- package/docs/plans/2026-09-27-desktop-commander-drop-in-parity.md +92 -0
- package/package.json +4 -2
- package/proof/commander-closure-20260927/CONTINUATION.json +43 -0
- package/proof/commander-closure-20260927/RESULT.json +30 -0
- package/proof/commander-closure-20260927/SEARCH-CONTINUATION.json +30 -0
- package/proof/commander-closure-20260927/XLS-REFERENCE-PROBE.json +13 -0
- package/scripts/deploy-r48.mjs +86 -0
- package/scripts/full-regression-r48.mjs +28 -0
- package/src/coding-search.mjs +65 -7
- package/src/commander-compat-contracts.mjs +10 -0
- package/src/commander-core-bridge.mjs +2 -2
- package/src/commander-extended-bridge-r47.mjs +116 -12
- package/src/commander-extended-runtime.mjs +33 -6
- package/src/commander-external-process-r48.mjs +125 -0
- package/src/commander-observability.mjs +66 -16
- package/src/commander-office-reader-r48.mjs +548 -0
- package/src/commander-pdf-r48.mjs +448 -0
- package/src/commander-ui-resources.mjs +26 -0
- package/src/commander-url-reader-r48.mjs +201 -0
- package/src/commander-wire-adapter-r47.mjs +595 -101
- package/src/deadbyte-cli.mjs +73 -8
- package/src/mcp-server.mjs +9 -2
- package/src/observation-output-contracts.mjs +2 -1
- package/src/observation-runtime.mjs +4 -2
- package/src/version.mjs +1 -1
- package/test/coding-search-r36.test.mjs +71 -0
- package/test/commander-core-bridge-r47.test.mjs +32 -3
- package/test/commander-exhaustive-surface-r47.test.mjs +5 -1
- package/test/commander-extended-runtime-r47.test.mjs +8 -0
- package/test/commander-extended-wire-r48.test.mjs +98 -0
- package/test/commander-external-process-r48.test.mjs +76 -0
- package/test/commander-office-reader-r48.test.mjs +152 -0
- package/test/commander-pdf-r48.test.mjs +103 -0
- package/test/commander-protocol-r47.test.mjs +23 -1
- package/test/commander-reference-contract-r48.test.mjs +26 -0
- package/test/commander-schema-differential-r48.test.mjs +88 -0
- package/test/commander-url-reader-r48.test.mjs +68 -0
- package/test/commander-wire-contract-r47.test.mjs +461 -41
- package/test/controller-r212.test.mjs +23 -2
- package/test/observation-runtime.test.mjs +4 -3
- package/test/prompts-resources.test.mjs +1 -1
- package/test/r47-release-contract.test.mjs +9 -8
- package/test/r48-release-contract.test.mjs +36 -0
- package/test/remote-session-cli.test.mjs +34 -1
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { inflateSync } from 'node:zlib';
|
|
3
|
+
|
|
4
|
+
const PROFILE_MARKER='%DEADBYTE-PDF-R48';
|
|
5
|
+
const MAX_PAGES=512;
|
|
6
|
+
const MAX_FILE_BYTES=64*1024*1024;
|
|
7
|
+
const MAX_PAGE_TEXT_BYTES=1024*1024;
|
|
8
|
+
const UTF8=new TextDecoder('utf-8',{fatal:true});
|
|
9
|
+
|
|
10
|
+
const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
11
|
+
|
|
12
|
+
function assertPageText(value,index){
|
|
13
|
+
if(typeof value!=='string') throw new Error(`PDF page ${index} text must be string`);
|
|
14
|
+
if(Buffer.byteLength(value,'utf8')>MAX_PAGE_TEXT_BYTES) throw new Error(`PDF page ${index} text exceeds byte limit`);
|
|
15
|
+
}
|
|
16
|
+
function renderableByte(ch){
|
|
17
|
+
const cp=ch.codePointAt(0);
|
|
18
|
+
if(cp===9) return ' ';
|
|
19
|
+
if(cp>=32&&cp<=126) return ch;
|
|
20
|
+
if(cp>=160&&cp<=255) return ch;
|
|
21
|
+
if(cp===10||cp===13) return '';
|
|
22
|
+
return '?';
|
|
23
|
+
}
|
|
24
|
+
function pdfEscape(text){
|
|
25
|
+
let out='';
|
|
26
|
+
for(const ch of text) out+=renderableByte(ch).replaceAll('\\','\\\\').replaceAll('(','\\(').replaceAll(')','\\)');
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function renderPage(text){
|
|
30
|
+
const lines=String(text).replaceAll('\r\n','\n').replaceAll('\r','\n').split('\n').slice(0,2000);
|
|
31
|
+
const out=['BT','/F1 11 Tf','50 742 Td','14 TL'];
|
|
32
|
+
for(const line of lines){
|
|
33
|
+
out.push(`(${pdfEscape(line).slice(0,4096)}) Tj`);
|
|
34
|
+
out.push('0 -14 Td');
|
|
35
|
+
}
|
|
36
|
+
out.push('ET');
|
|
37
|
+
return out.join('\n');
|
|
38
|
+
}
|
|
39
|
+
function utf8Hex(text){return Buffer.from(text,'utf8').toString('hex').toUpperCase();}
|
|
40
|
+
function decodeHexUtf8(hex){
|
|
41
|
+
if(typeof hex!=='string'||hex.length%2!==0||!/^[0-9A-Fa-f]*$/.test(hex)) throw new Error('PDF DBText hex invalid');
|
|
42
|
+
try{return UTF8.decode(Buffer.from(hex,'hex'));}catch{throw new Error('PDF DBText is not valid UTF-8');}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createDeadbytePdf(pages,{metadata={}}={}){
|
|
46
|
+
if(!Array.isArray(pages)) throw new Error('PDF pages must be array');
|
|
47
|
+
if(pages.length>MAX_PAGES) throw new Error(`PDF page count exceeds ${MAX_PAGES}`);
|
|
48
|
+
pages.forEach(assertPageText);
|
|
49
|
+
const nl='\n';
|
|
50
|
+
const fontId=3;
|
|
51
|
+
const pageId=i=>4+i*2;
|
|
52
|
+
const contentId=i=>5+i*2;
|
|
53
|
+
const kids=pages.map((_,i)=>`${pageId(i)} 0 R`).join(' ');
|
|
54
|
+
const objects=new Map();
|
|
55
|
+
objects.set(1,'<< /Type /Catalog /Pages 2 0 R >>');
|
|
56
|
+
objects.set(2,`<< /Type /Pages /Kids [${kids}] /Count ${pages.length} >>`);
|
|
57
|
+
objects.set(3,'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>');
|
|
58
|
+
for(let i=0;i<pages.length;i++){
|
|
59
|
+
const stream=renderPage(pages[i]);
|
|
60
|
+
objects.set(pageId(i),`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 ${fontId} 0 R >> >> /Contents ${contentId(i)} 0 R /DBPageIndex ${i} /DBText <${utf8Hex(pages[i])}> >>`);
|
|
61
|
+
objects.set(contentId(i),`<< /Length ${Buffer.byteLength(stream,'latin1')} >>${nl}stream${nl}${stream}${nl}endstream`);
|
|
62
|
+
}
|
|
63
|
+
const maxId=3+pages.length*2;
|
|
64
|
+
let out=`%PDF-1.4${nl}${PROFILE_MARKER}${nl}`;
|
|
65
|
+
const offsets=new Array(maxId+1).fill(0);
|
|
66
|
+
for(let id=1;id<=maxId;id++){
|
|
67
|
+
const body=objects.get(id);
|
|
68
|
+
if(body===undefined) throw new Error(`PDF object ${id} missing`);
|
|
69
|
+
offsets[id]=Buffer.byteLength(out,'latin1');
|
|
70
|
+
out+=`${id} 0 obj${nl}${body}${nl}endobj${nl}`;
|
|
71
|
+
}
|
|
72
|
+
const xref=Buffer.byteLength(out,'latin1');
|
|
73
|
+
out+=`xref${nl}0 ${maxId+1}${nl}0000000000 65535 f ${nl}`;
|
|
74
|
+
for(let id=1;id<=maxId;id++) out+=`${String(offsets[id]).padStart(10,'0')} 00000 n ${nl}`;
|
|
75
|
+
out+=`trailer${nl}<< /Size ${maxId+1} /Root 1 0 R >>${nl}startxref${nl}${xref}${nl}%%EOF${nl}`;
|
|
76
|
+
const bytes=Buffer.from(out,'latin1');
|
|
77
|
+
if(bytes.length>MAX_FILE_BYTES) throw new Error('PDF output exceeds byte limit');
|
|
78
|
+
return {profile:'deadbyte-pdf-r48',mime_type:'application/pdf',page_count:pages.length,bytes,bytes_base64:bytes.toString('base64'),bytes_sha256:sha256(bytes),metadata};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function parseDeadbytePdf(input){
|
|
82
|
+
const bytes=Buffer.isBuffer(input)?input:Buffer.from(input);
|
|
83
|
+
if(bytes.length>MAX_FILE_BYTES) throw new Error('PDF input exceeds byte limit');
|
|
84
|
+
const head=bytes.subarray(0,256).toString('latin1');
|
|
85
|
+
if(!head.startsWith('%PDF-')||!head.includes(PROFILE_MARKER)) throw new Error('unsupported PDF profile: expected DEADBYTE-PDF-R48');
|
|
86
|
+
const text=bytes.toString('latin1');
|
|
87
|
+
const pages=[];
|
|
88
|
+
const re=/(\d+)\s+0\s+obj\s*([\s\S]*?)\s*endobj/g;
|
|
89
|
+
let m;
|
|
90
|
+
while((m=re.exec(text))){
|
|
91
|
+
const body=m[2];
|
|
92
|
+
if(!/\/Type\s*\/Page\b/.test(body)||/\/Type\s*\/Pages\b/.test(body)) continue;
|
|
93
|
+
const indexRaw=body.match(/\/DBPageIndex\s+(\d+)/)?.[1];
|
|
94
|
+
const hex=body.match(/\/DBText\s*<([0-9A-Fa-f]*)>/)?.[1];
|
|
95
|
+
if(indexRaw===undefined||hex===undefined) throw new Error('unsupported PDF profile: page metadata missing');
|
|
96
|
+
const index=Number(indexRaw);
|
|
97
|
+
if(!Number.isInteger(index)||index<0||index>=MAX_PAGES) throw new Error('PDF page index invalid');
|
|
98
|
+
if(pages[index]!==undefined) throw new Error('PDF page index duplicate');
|
|
99
|
+
pages[index]=decodeHexUtf8(hex);
|
|
100
|
+
}
|
|
101
|
+
const countRaw=text.match(/\/Type\s*\/Pages\b[\s\S]*?\/Count\s+(\d+)/)?.[1];
|
|
102
|
+
if(countRaw===undefined) throw new Error('unsupported PDF profile: Pages count missing');
|
|
103
|
+
const count=Number(countRaw);
|
|
104
|
+
if(!Number.isInteger(count)||count<0||count>MAX_PAGES) throw new Error('PDF page count invalid');
|
|
105
|
+
if(pages.length!==count||pages.some(x=>x===undefined)) throw new Error('unsupported PDF profile: page index set incomplete');
|
|
106
|
+
return {profile:'deadbyte-pdf-r48',page_count:count,pages};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
const MAX_GENERIC_OBJECTS=10000;
|
|
111
|
+
const MAX_GENERIC_STREAM_BYTES=16*1024*1024;
|
|
112
|
+
|
|
113
|
+
function decodePdfTextBytes(bytes){
|
|
114
|
+
if(bytes.length>=2&&bytes[0]===0xfe&&bytes[1]===0xff){
|
|
115
|
+
let out='';
|
|
116
|
+
for(let i=2;i+1<bytes.length;i+=2) out+=String.fromCharCode((bytes[i]<<8)|bytes[i+1]);
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
if(bytes.length>=2&&bytes[0]===0xff&&bytes[1]===0xfe){
|
|
120
|
+
return bytes.subarray(2).toString('utf16le');
|
|
121
|
+
}
|
|
122
|
+
return bytes.toString('latin1');
|
|
123
|
+
}
|
|
124
|
+
function decodePdfLiteralToken(token){
|
|
125
|
+
if(!token.startsWith('(')||!token.endsWith(')')) throw new Error('PDF literal token invalid');
|
|
126
|
+
const src=token.slice(1,-1),out=[];
|
|
127
|
+
for(let i=0;i<src.length;i++){
|
|
128
|
+
const code=src.charCodeAt(i)&0xff;
|
|
129
|
+
if(code!==0x5c){out.push(code);continue;}
|
|
130
|
+
if(++i>=src.length) break;
|
|
131
|
+
const ch=src[i];
|
|
132
|
+
const simple={n:0x0a,r:0x0d,t:0x09,b:0x08,f:0x0c,'(':0x28,')':0x29,'\\':0x5c}[ch];
|
|
133
|
+
if(simple!==undefined){out.push(simple);continue;}
|
|
134
|
+
if(ch==='\r'||ch==='\n'){
|
|
135
|
+
if(ch==='\r'&&src[i+1]==='\n') i++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if(/[0-7]/.test(ch)){
|
|
139
|
+
let oct=ch;
|
|
140
|
+
for(let k=0;k<2&&i+1<src.length&&/[0-7]/.test(src[i+1]);k++) oct+=src[++i];
|
|
141
|
+
out.push(parseInt(oct,8)&0xff);continue;
|
|
142
|
+
}
|
|
143
|
+
out.push(ch.charCodeAt(0)&0xff);
|
|
144
|
+
}
|
|
145
|
+
return decodePdfTextBytes(Buffer.from(out));
|
|
146
|
+
}
|
|
147
|
+
function decodePdfHexToken(token){
|
|
148
|
+
let hex=token.slice(1,-1).replace(/\s+/g,'');
|
|
149
|
+
if(!/^[0-9A-Fa-f]*$/.test(hex)) throw new Error('PDF hex string invalid');
|
|
150
|
+
if(hex.length%2) hex+='0';
|
|
151
|
+
return decodePdfTextBytes(Buffer.from(hex,'hex'));
|
|
152
|
+
}
|
|
153
|
+
function decodePdfStringToken(token){
|
|
154
|
+
return token.startsWith('(')?decodePdfLiteralToken(token):decodePdfHexToken(token);
|
|
155
|
+
}
|
|
156
|
+
function extractTextFromContentStream(bytes){
|
|
157
|
+
if(bytes.length>MAX_GENERIC_STREAM_BYTES) throw new Error('PDF content stream exceeds decoded byte limit');
|
|
158
|
+
const source=bytes.toString('latin1');
|
|
159
|
+
const blocks=[...source.matchAll(/BT\b([\s\S]*?)\bET/g)].map(m=>m[1]);
|
|
160
|
+
const lines=[];let current='';
|
|
161
|
+
const flush=()=>{if(current.length){lines.push(current);current='';}};
|
|
162
|
+
const stringToken='(?:\\((?:\\\\[\\s\\S]|[^\\\\()])*\\)|<(?!<)[0-9A-Fa-f\\s]*>)';
|
|
163
|
+
const event=new RegExp(
|
|
164
|
+
`(${stringToken})\\s*Tj\\b|\\[((?:${stringToken}|[^\\]])*)\\]\\s*TJ\\b|(${stringToken})\\s*'|[-+]?\\d*\\.?\\d+\\s+[-+]?\\d*\\.?\\d+\\s+(${stringToken})\\s*"|\\bT\\*\\b|[-+]?\\d*\\.?\\d+\\s+[-+]?\\d*\\.?\\d+\\s+T[dD]\\b`,
|
|
165
|
+
'g'
|
|
166
|
+
);
|
|
167
|
+
for(const block of blocks){
|
|
168
|
+
let match;
|
|
169
|
+
while((match=event.exec(block))){
|
|
170
|
+
const full=match[0];
|
|
171
|
+
if(/\bT\*$/.test(full)||/\sT[dD]\b$/.test(full)){flush();continue;}
|
|
172
|
+
if(match[1]){current+=decodePdfStringToken(match[1]);continue;}
|
|
173
|
+
if(match[2]!==undefined){
|
|
174
|
+
const strings=[...match[2].matchAll(new RegExp(stringToken,'g'))].map(x=>decodePdfStringToken(x[0]));
|
|
175
|
+
current+=strings.join('');continue;
|
|
176
|
+
}
|
|
177
|
+
if(match[3]){current+=decodePdfStringToken(match[3]);flush();continue;}
|
|
178
|
+
if(match[4]){current+=decodePdfStringToken(match[4]);flush();continue;}
|
|
179
|
+
}
|
|
180
|
+
flush();
|
|
181
|
+
}
|
|
182
|
+
return lines.join('\n').replace(/[ \t]+\n/g,'\n').trim();
|
|
183
|
+
}
|
|
184
|
+
function parseGenericPdfObjects(bytes){
|
|
185
|
+
if(bytes.length>MAX_FILE_BYTES) throw new Error('PDF input exceeds byte limit');
|
|
186
|
+
const text=bytes.toString('latin1');
|
|
187
|
+
if(!text.startsWith('%PDF-')) throw new Error('invalid PDF header');
|
|
188
|
+
if(/\/Encrypt\b/.test(text)) throw new Error('encrypted PDF is unsupported');
|
|
189
|
+
const objects=new Map(),header=/(\d+)\s+(\d+)\s+obj\b/g;
|
|
190
|
+
let match,count=0;
|
|
191
|
+
while((match=header.exec(text))){
|
|
192
|
+
if(++count>MAX_GENERIC_OBJECTS) throw new Error('PDF object count exceeds limit');
|
|
193
|
+
const end=text.indexOf('endobj',header.lastIndex);
|
|
194
|
+
if(end<0) throw new Error(`PDF object ${match[1]} ${match[2]} missing endobj`);
|
|
195
|
+
const key=`${match[1]} ${match[2]}`;
|
|
196
|
+
if(objects.has(key)) throw new Error(`PDF duplicate object ${key}`);
|
|
197
|
+
objects.set(key,{id:Number(match[1]),generation:Number(match[2]),body:text.slice(header.lastIndex,end),offset:match.index});
|
|
198
|
+
header.lastIndex=end+6;
|
|
199
|
+
}
|
|
200
|
+
if(objects.size===0) throw new Error('PDF contains no direct objects');
|
|
201
|
+
let expandedBytes=0;
|
|
202
|
+
for(const container of [...objects.values()]){
|
|
203
|
+
if(!/\/Type\s*\/ObjStm\b/.test(container.body))continue;
|
|
204
|
+
const n=Number(container.body.match(/\/N\s+(\d+)/)?.[1]);
|
|
205
|
+
const first=Number(container.body.match(/\/First\s+(\d+)/)?.[1]);
|
|
206
|
+
if(!Number.isSafeInteger(n)||n<1||n>MAX_GENERIC_OBJECTS||!Number.isSafeInteger(first)||first<0)
|
|
207
|
+
throw new Error('PDF object stream header invalid');
|
|
208
|
+
const decoded=decodedObjectStream(container);
|
|
209
|
+
expandedBytes+=decoded.length;
|
|
210
|
+
if(expandedBytes>MAX_FILE_BYTES||first>decoded.length)throw new Error('PDF object stream expansion exceeds limit');
|
|
211
|
+
const headerText=decoded.subarray(0,first).toString('ascii').trim();
|
|
212
|
+
if(!/^\d+(?:\s+\d+)*$/.test(headerText))throw new Error('PDF object stream index invalid');
|
|
213
|
+
const index=headerText.split(/\s+/).map(Number);
|
|
214
|
+
if(index.length!==n*2)throw new Error('PDF object stream index count mismatch');
|
|
215
|
+
for(let i=0;i<n;i++){
|
|
216
|
+
const id=index[i*2],offset=index[i*2+1],next=i+1<n?index[(i+1)*2+1]:decoded.length-first;
|
|
217
|
+
if(!Number.isSafeInteger(id)||id<1||!Number.isSafeInteger(offset)||offset<0||next<offset||first+next>decoded.length)
|
|
218
|
+
throw new Error('PDF object stream entry invalid');
|
|
219
|
+
const key=`${id} 0`;
|
|
220
|
+
if(objects.has(key))throw new Error(`PDF duplicate object ${key}`);
|
|
221
|
+
if(objects.size>=MAX_GENERIC_OBJECTS)throw new Error('PDF object count exceeds limit');
|
|
222
|
+
objects.set(key,{id,generation:0,body:decoded.subarray(first+offset,first+next).toString('latin1'),offset:container.offset+i/n});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return objects;
|
|
226
|
+
}
|
|
227
|
+
function objectRefs(fragment){
|
|
228
|
+
return [...String(fragment).matchAll(/(\d+)\s+(\d+)\s+R/g)].map(m=>`${m[1]} ${m[2]}`);
|
|
229
|
+
}
|
|
230
|
+
function orderedGenericPages(objects){
|
|
231
|
+
const catalog=[...objects.values()].find(o=>/\/Type\s*\/Catalog\b/.test(o.body));
|
|
232
|
+
const rootRef=catalog?.body.match(/\/Pages\s+(\d+)\s+(\d+)\s+R/)?.slice(1,3);
|
|
233
|
+
const pages=[],visited=new Set();
|
|
234
|
+
const walk=key=>{
|
|
235
|
+
if(visited.has(key)) throw new Error('PDF page tree cycle detected');
|
|
236
|
+
visited.add(key);
|
|
237
|
+
const obj=objects.get(key);
|
|
238
|
+
if(!obj) throw new Error(`PDF page tree reference missing: ${key}`);
|
|
239
|
+
if(/\/Type\s*\/Page\b/.test(obj.body)&&!/\/Type\s*\/Pages\b/.test(obj.body)){pages.push(obj);return;}
|
|
240
|
+
if(!/\/Type\s*\/Pages\b/.test(obj.body)) throw new Error(`PDF page tree object ${key} has invalid type`);
|
|
241
|
+
const kids=obj.body.match(/\/Kids\s*\[([\s\S]*?)\]/)?.[1];
|
|
242
|
+
if(kids===undefined) throw new Error(`PDF Pages object ${key} missing Kids`);
|
|
243
|
+
for(const ref of objectRefs(kids)) walk(ref);
|
|
244
|
+
};
|
|
245
|
+
if(rootRef) walk(`${rootRef[0]} ${rootRef[1]}`);
|
|
246
|
+
else pages.push(...[...objects.values()].filter(o=>/\/Type\s*\/Page\b/.test(o.body)&&!/\/Type\s*\/Pages\b/.test(o.body)).sort((a,b)=>a.offset-b.offset));
|
|
247
|
+
if(pages.length>MAX_PAGES) throw new Error(`PDF page count exceeds ${MAX_PAGES}`);
|
|
248
|
+
return pages;
|
|
249
|
+
}
|
|
250
|
+
function decodedObjectStream(obj){
|
|
251
|
+
const streamMatch=obj.body.match(/\bstream\r?\n/);
|
|
252
|
+
if(!streamMatch) throw new Error(`PDF content object ${obj.id} has no stream`);
|
|
253
|
+
const start=streamMatch.index+streamMatch[0].length;
|
|
254
|
+
const end=obj.body.indexOf('endstream',start);
|
|
255
|
+
if(end<0) throw new Error(`PDF content object ${obj.id} missing endstream`);
|
|
256
|
+
const dictionary=obj.body.slice(0,streamMatch.index);
|
|
257
|
+
const raw=Buffer.from(obj.body.slice(start,end).replace(/\r?\n$/,''),'latin1');
|
|
258
|
+
const filter= dictionary.match(/\/Filter\s*\/([A-Za-z0-9]+)/)?.[1] ??
|
|
259
|
+
dictionary.match(/\/Filter\s*\[\s*\/([A-Za-z0-9]+)\s*\]/)?.[1] ?? null;
|
|
260
|
+
if(/\/Filter\b/.test(dictionary)&&filter===null) throw new Error('unsupported PDF stream filter chain');
|
|
261
|
+
if(filter===null) return raw;
|
|
262
|
+
if(filter!=='FlateDecode') throw new Error(`unsupported PDF stream filter /${filter}`);
|
|
263
|
+
try{return inflateSync(raw,{maxOutputLength:MAX_GENERIC_STREAM_BYTES});}
|
|
264
|
+
catch(error){throw new Error(`PDF Flate stream decode failed: ${error?.message??error}`);}
|
|
265
|
+
}
|
|
266
|
+
function pageContentRefs(page){
|
|
267
|
+
const array=page.body.match(/\/Contents\s*\[([\s\S]*?)\]/)?.[1];
|
|
268
|
+
if(array!==undefined) return objectRefs(array);
|
|
269
|
+
const one=page.body.match(/\/Contents\s+(\d+)\s+(\d+)\s+R/);
|
|
270
|
+
return one?[`${one[1]} ${one[2]}`]:[];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function readPdfText(input){
|
|
274
|
+
const bytes=Buffer.isBuffer(input)?Buffer.from(input):Buffer.from(input);
|
|
275
|
+
const head=bytes.subarray(0,256).toString('latin1');
|
|
276
|
+
if(head.includes(PROFILE_MARKER)){
|
|
277
|
+
const exact=parseDeadbytePdf(bytes);
|
|
278
|
+
return {...exact,partial:false,warnings:[]};
|
|
279
|
+
}
|
|
280
|
+
const objects=parseGenericPdfObjects(bytes),pageObjects=orderedGenericPages(objects);
|
|
281
|
+
const pages=[],warnings=[];
|
|
282
|
+
for(let i=0;i<pageObjects.length;i++){
|
|
283
|
+
const refs=pageContentRefs(pageObjects[i]);
|
|
284
|
+
let pageText='';
|
|
285
|
+
for(const ref of refs){
|
|
286
|
+
const obj=objects.get(ref);
|
|
287
|
+
if(!obj) throw new Error(`PDF content reference missing: ${ref}`);
|
|
288
|
+
const extracted=extractTextFromContentStream(decodedObjectStream(obj));
|
|
289
|
+
if(extracted) pageText+=(pageText?'\n':'')+extracted;
|
|
290
|
+
}
|
|
291
|
+
if(Buffer.byteLength(pageText,'utf8')>MAX_PAGE_TEXT_BYTES) throw new Error(`PDF page ${i} text exceeds byte limit`);
|
|
292
|
+
pages.push(pageText);
|
|
293
|
+
if(refs.length===0) warnings.push(`page ${i+1}: no direct Contents stream`);
|
|
294
|
+
}
|
|
295
|
+
return {profile:'generic-pdf-r48',page_count:pages.length,pages,partial:warnings.length>0,warnings};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function normalizeIndexes(values,count){
|
|
299
|
+
if(!Array.isArray(values)) throw new Error('PDF pageIndexes must be array');
|
|
300
|
+
const out=[];
|
|
301
|
+
for(const raw of values){
|
|
302
|
+
if(!Number.isInteger(raw)) throw new Error('PDF page index must be integer');
|
|
303
|
+
const n=raw<0?count+raw:raw;
|
|
304
|
+
if(n>=0&&n<count&&!out.includes(n)) out.push(n);
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
function splitInsertedMarkdown(markdown){
|
|
309
|
+
if(typeof markdown!=='string') throw new Error('PDF insert markdown must be string');
|
|
310
|
+
const pages=markdown.split('\f');
|
|
311
|
+
pages.forEach(assertPageText);
|
|
312
|
+
return pages;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function rewritePdfRefs(body,resolve){
|
|
316
|
+
const replace=fragment=>fragment.replace(/(\d+)\s+(\d+)\s+R\b/g,(_,id,generation)=>`${resolve(`${id} ${generation}`)} 0 R`);
|
|
317
|
+
const stream=body.match(/\bstream\r?\n/);
|
|
318
|
+
if(!stream)return replace(body);
|
|
319
|
+
const start=stream.index+stream[0].length,end=body.indexOf('endstream',start);
|
|
320
|
+
if(end<0)throw new Error('PDF stream missing endstream during rebuild');
|
|
321
|
+
return replace(body.slice(0,start))+body.slice(start,end)+replace(body.slice(end));
|
|
322
|
+
}
|
|
323
|
+
function inheritedPageEntry(page,objects,name){
|
|
324
|
+
let current=page,depth=0;
|
|
325
|
+
while(current&&depth++<64){
|
|
326
|
+
const array=current.body.match(new RegExp(`/${name}\\s*(\\[[^\\]]*\\])`))?.[1];
|
|
327
|
+
if(array)return array;
|
|
328
|
+
const ref=current.body.match(new RegExp(`/${name}\\s+(\\d+\\s+\\d+\\s+R)`))?.[1];
|
|
329
|
+
if(ref)return ref;
|
|
330
|
+
const parent=current.body.match(/\/Parent\s+(\d+)\s+(\d+)\s+R/)?.slice(1,3);
|
|
331
|
+
current=parent?objects.get(`${parent[0]} ${parent[1]}`):null;
|
|
332
|
+
}
|
|
333
|
+
if(depth>=64)throw new Error('PDF page inheritance depth exceeds limit');
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
function genericPageSet(input,label){
|
|
337
|
+
const bytes=Buffer.isBuffer(input)?Buffer.from(input):Buffer.from(input);
|
|
338
|
+
const objects=parseGenericPdfObjects(bytes),pages=orderedGenericPages(objects);
|
|
339
|
+
return pages.map((page,index)=>({objects,page,label:`${label}:${index}`}));
|
|
340
|
+
}
|
|
341
|
+
function rebuildGenericPdf(pageSet){
|
|
342
|
+
if(pageSet.length>MAX_PAGES)throw new Error(`PDF page count exceeds ${MAX_PAGES}`);
|
|
343
|
+
const bodies=new Map([[1,'<< /Type /Catalog /Pages 2 0 R >>'],[2,null]]),memo=new Map();
|
|
344
|
+
let nextId=3;
|
|
345
|
+
const sourceKey=(entry,key)=>`${entry.label}|${key}`;
|
|
346
|
+
const cloneObject=(entry,key)=>{
|
|
347
|
+
const memoKey=sourceKey(entry,key);
|
|
348
|
+
if(memo.has(memoKey))return memo.get(memoKey);
|
|
349
|
+
const source=entry.objects.get(key);
|
|
350
|
+
if(!source)throw new Error(`PDF rebuild reference missing: ${key}`);
|
|
351
|
+
if(/\/Type\s*\/(?:Catalog|Pages?)\b/.test(source.body))throw new Error(`PDF rebuild dependency crosses document topology: ${key}`);
|
|
352
|
+
if(bodies.size>=MAX_GENERIC_OBJECTS)throw new Error('PDF rebuild object count exceeds limit');
|
|
353
|
+
const id=nextId++;memo.set(memoKey,id);
|
|
354
|
+
bodies.set(id,rewritePdfRefs(source.body,ref=>cloneObject(entry,ref)));
|
|
355
|
+
return id;
|
|
356
|
+
};
|
|
357
|
+
const pageIds=[];
|
|
358
|
+
for(const entry of pageSet){
|
|
359
|
+
const pageKey=`${entry.page.id} ${entry.page.generation}`;
|
|
360
|
+
const pageId=nextId++,memoKey=sourceKey(entry,pageKey);
|
|
361
|
+
memo.set(memoKey,pageId);pageIds.push(pageId);bodies.set(pageId,null);
|
|
362
|
+
let body=entry.page.body.replace(/\/Parent\s+\d+\s+\d+\s+R\b/,'').trim();
|
|
363
|
+
if(!/\/MediaBox\b/.test(body)){
|
|
364
|
+
const inherited=inheritedPageEntry(entry.page,entry.objects,'MediaBox');
|
|
365
|
+
if(inherited)body=body.replace(/>>\s*$/,` /MediaBox ${inherited} >>`);
|
|
366
|
+
}
|
|
367
|
+
if(!/\/Resources\b/.test(body)){
|
|
368
|
+
const inherited=inheritedPageEntry(entry.page,entry.objects,'Resources');
|
|
369
|
+
if(inherited)body=body.replace(/>>\s*$/,` /Resources ${inherited} >>`);
|
|
370
|
+
}
|
|
371
|
+
body=rewritePdfRefs(body,ref=>cloneObject(entry,ref));
|
|
372
|
+
bodies.set(pageId,body.replace(/>>\s*$/,` /Parent 2 0 R >>`));
|
|
373
|
+
}
|
|
374
|
+
bodies.set(2,`<< /Type /Pages /Kids [${pageIds.map(id=>`${id} 0 R`).join(' ')}] /Count ${pageIds.length} >>`);
|
|
375
|
+
const nl='\n',maxId=nextId-1,offsets=new Array(maxId+1).fill(0);
|
|
376
|
+
let out=`%PDF-1.4${nl}`;
|
|
377
|
+
for(let id=1;id<=maxId;id++){
|
|
378
|
+
const body=bodies.get(id);if(body===undefined||body===null)throw new Error(`PDF rebuild object ${id} missing`);
|
|
379
|
+
offsets[id]=Buffer.byteLength(out,'latin1');out+=`${id} 0 obj${nl}${body}${nl}endobj${nl}`;
|
|
380
|
+
}
|
|
381
|
+
const xref=Buffer.byteLength(out,'latin1');
|
|
382
|
+
out+=`xref${nl}0 ${maxId+1}${nl}0000000000 65535 f ${nl}`;
|
|
383
|
+
for(let id=1;id<=maxId;id++)out+=`${String(offsets[id]).padStart(10,'0')} 00000 n ${nl}`;
|
|
384
|
+
out+=`trailer${nl}<< /Size ${maxId+1} /Root 1 0 R >>${nl}startxref${nl}${xref}${nl}%%EOF${nl}`;
|
|
385
|
+
const bytes=Buffer.from(out,'latin1');if(bytes.length>MAX_FILE_BYTES)throw new Error('PDF output exceeds byte limit');
|
|
386
|
+
return {profile:'generic-pdf-r48',mime_type:'application/pdf',page_count:pageSet.length,bytes,bytes_base64:bytes.toString('base64'),bytes_sha256:sha256(bytes),metadata:{rebuilt:true}};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function editGenericPdf(input,operations,{sourceResolver=null}={}){
|
|
390
|
+
if(!Array.isArray(operations))throw new Error('PDF operations must be array');
|
|
391
|
+
if(operations.length>256)throw new Error('PDF operation count exceeds 256');
|
|
392
|
+
const pages=genericPageSet(input,'base');let serial=0;
|
|
393
|
+
for(const op of operations){
|
|
394
|
+
if(!op||typeof op!=='object'||Array.isArray(op))throw new Error('PDF operation invalid');
|
|
395
|
+
if(op.type==='delete'){
|
|
396
|
+
for(const index of normalizeIndexes(op.pageIndexes,pages.length).sort((a,b)=>b-a))pages.splice(index,1);
|
|
397
|
+
}else if(op.type==='insert'){
|
|
398
|
+
if(!Number.isInteger(op.pageIndex))throw new Error('PDF insert pageIndex must be integer');
|
|
399
|
+
const at=op.pageIndex<0?pages.length+op.pageIndex:op.pageIndex;
|
|
400
|
+
if(at<0||at>pages.length)throw new Error('Invalid page index');
|
|
401
|
+
let inserted;
|
|
402
|
+
if(op.markdown!==undefined){
|
|
403
|
+
const generated=createDeadbytePdf(splitInsertedMarkdown(op.markdown));
|
|
404
|
+
inserted=genericPageSet(generated.bytes,`markdown-${serial++}`);
|
|
405
|
+
}else if(typeof op.sourcePdfPath==='string'&&op.sourcePdfPath){
|
|
406
|
+
if(typeof sourceResolver!=='function')throw new Error('PDF source resolver required');
|
|
407
|
+
inserted=genericPageSet(await sourceResolver(op.sourcePdfPath),`source-${serial++}`);
|
|
408
|
+
}else throw new Error('No source provided for insert operation');
|
|
409
|
+
pages.splice(at,0,...inserted);
|
|
410
|
+
if(pages.length>MAX_PAGES)throw new Error(`PDF page count exceeds ${MAX_PAGES}`);
|
|
411
|
+
}else throw new Error(`unsupported PDF operation '${String(op.type)}'`);
|
|
412
|
+
}
|
|
413
|
+
return rebuildGenericPdf(pages);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export async function editDeadbytePdf(input,operations,{sourceResolver=null}={}){
|
|
417
|
+
const bytes=Buffer.isBuffer(input)?input:Buffer.from(input);
|
|
418
|
+
if(!bytes.subarray(0,256).toString('latin1').includes(PROFILE_MARKER))return editGenericPdf(bytes,operations,{sourceResolver});
|
|
419
|
+
const parsed=parseDeadbytePdf(input);
|
|
420
|
+
if(!Array.isArray(operations)) throw new Error('PDF operations must be array');
|
|
421
|
+
if(operations.length>256) throw new Error('PDF operation count exceeds 256');
|
|
422
|
+
const pages=[...parsed.pages];
|
|
423
|
+
for(const op of operations){
|
|
424
|
+
if(!op||typeof op!=='object'||Array.isArray(op)) throw new Error('PDF operation invalid');
|
|
425
|
+
if(op.type==='delete'){
|
|
426
|
+
const indexes=normalizeIndexes(op.pageIndexes,pages.length).sort((a,b)=>b-a);
|
|
427
|
+
for(const index of indexes) pages.splice(index,1);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if(op.type==='insert'){
|
|
431
|
+
if(!Number.isInteger(op.pageIndex)) throw new Error('PDF insert pageIndex must be integer');
|
|
432
|
+
const insertPosition=op.pageIndex<0?pages.length+op.pageIndex:op.pageIndex;
|
|
433
|
+
if(insertPosition<0||insertPosition>pages.length) throw new Error('Invalid page index');
|
|
434
|
+
let inserted;
|
|
435
|
+
if(op.markdown!==undefined) inserted=splitInsertedMarkdown(op.markdown);
|
|
436
|
+
else if(typeof op.sourcePdfPath==='string'&&op.sourcePdfPath){
|
|
437
|
+
if(typeof sourceResolver!=='function') throw new Error('PDF source resolver required');
|
|
438
|
+
const source=await sourceResolver(op.sourcePdfPath);
|
|
439
|
+
inserted=parseDeadbytePdf(source).pages;
|
|
440
|
+
}else throw new Error('No source provided for insert operation');
|
|
441
|
+
pages.splice(insertPosition,0,...inserted);
|
|
442
|
+
if(pages.length>MAX_PAGES) throw new Error(`PDF page count exceeds ${MAX_PAGES}`);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
throw new Error(`unsupported PDF operation '${String(op.type)}'`);
|
|
446
|
+
}
|
|
447
|
+
return createDeadbytePdf(pages);
|
|
448
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const MIME='text/html;profile=mcp-app';
|
|
2
|
+
const resources=[
|
|
3
|
+
{uri:'ui://desktop-commander/file-preview',name:'DEADBYTE File Preview',title:'File preview',description:'Bounded local preview of tool output.'},
|
|
4
|
+
{uri:'ui://desktop-commander/config-editor',name:'DEADBYTE Config Editor',title:'Configuration',description:'Configuration display; changes use set_config_value and native policy enforcement.'}
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
function html(title){
|
|
8
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'none'"><title>${title}</title><style>body{font:14px system-ui;margin:16px}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><h1>${title}</h1><pre id="output">Waiting for tool output.</pre><script>
|
|
9
|
+
const output=document.getElementById('output');
|
|
10
|
+
window.addEventListener('message',event=>{
|
|
11
|
+
if(event.source!==window.parent||!event.data||event.data.jsonrpc!=='2.0')return;
|
|
12
|
+
if(event.data.method!=='ui/notifications/tool-result')return;
|
|
13
|
+
const value=event.data.params;
|
|
14
|
+
let text;try{text=JSON.stringify(value,null,2);}catch{return;}
|
|
15
|
+
if(typeof text==='string')output.textContent=text.slice(0,32768);
|
|
16
|
+
});
|
|
17
|
+
</script></body></html>`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function registerCommanderUiResources(server){
|
|
21
|
+
for(const resource of resources){
|
|
22
|
+
const text=html(resource.title);
|
|
23
|
+
server.registerResource(resource.name,resource.uri,{description:resource.description,mimeType:MIME},
|
|
24
|
+
async()=>({contents:[{uri:resource.uri,mimeType:MIME,text}]}));
|
|
25
|
+
}
|
|
26
|
+
}
|