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.
Files changed (55) hide show
  1. package/MANIFEST.SHA256 +54 -32
  2. package/PROVENANCE.json +13 -7
  3. package/README.txt +7 -7
  4. package/RELEASE.json +25 -6
  5. package/SBOM.cdx.json +3 -3
  6. package/TEST-MATRIX.json +26 -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/commander-compat-contracts.mjs +10 -0
  22. package/src/commander-core-bridge.mjs +2 -2
  23. package/src/commander-extended-bridge-r47.mjs +116 -12
  24. package/src/commander-extended-runtime.mjs +33 -6
  25. package/src/commander-external-process-r48.mjs +125 -0
  26. package/src/commander-observability.mjs +66 -16
  27. package/src/commander-office-reader-r48.mjs +548 -0
  28. package/src/commander-pdf-r48.mjs +448 -0
  29. package/src/commander-ui-resources.mjs +26 -0
  30. package/src/commander-url-reader-r48.mjs +201 -0
  31. package/src/commander-wire-adapter-r47.mjs +595 -101
  32. package/src/deadbyte-cli.mjs +73 -8
  33. package/src/mcp-server.mjs +9 -2
  34. package/src/observation-output-contracts.mjs +2 -1
  35. package/src/observation-runtime.mjs +4 -2
  36. package/src/version.mjs +1 -1
  37. package/test/coding-search-r36.test.mjs +71 -0
  38. package/test/commander-core-bridge-r47.test.mjs +32 -3
  39. package/test/commander-exhaustive-surface-r47.test.mjs +5 -1
  40. package/test/commander-extended-runtime-r47.test.mjs +8 -0
  41. package/test/commander-extended-wire-r48.test.mjs +98 -0
  42. package/test/commander-external-process-r48.test.mjs +76 -0
  43. package/test/commander-office-reader-r48.test.mjs +152 -0
  44. package/test/commander-pdf-r48.test.mjs +103 -0
  45. package/test/commander-protocol-r47.test.mjs +23 -1
  46. package/test/commander-reference-contract-r48.test.mjs +26 -0
  47. package/test/commander-schema-differential-r48.test.mjs +88 -0
  48. package/test/commander-url-reader-r48.test.mjs +68 -0
  49. package/test/commander-wire-contract-r47.test.mjs +461 -41
  50. package/test/controller-r212.test.mjs +23 -2
  51. package/test/observation-runtime.test.mjs +4 -3
  52. package/test/prompts-resources.test.mjs +1 -1
  53. package/test/r47-release-contract.test.mjs +9 -8
  54. package/test/r48-release-contract.test.mjs +36 -0
  55. package/test/remote-session-cli.test.mjs +34 -1
@@ -1,59 +1,85 @@
1
1
  import * as z from 'zod/v4';
2
2
  import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
+ import { readCommanderUrl } from './commander-url-reader-r48.mjs';
5
+ import { readDocxBytes, createDocxBytes, editDocxBytes, readXlsxBytes, editXlsxBytes, createXlsxBytes, appendXlsxBytes } from './commander-office-reader-r48.mjs';
6
+ import { editDeadbytePdf, parseDeadbytePdf, readPdfText } from './commander-pdf-r48.mjs';
4
7
 
5
8
  const sha256Bytes = bytes => createHash('sha256').update(bytes).digest('hex');
9
+ const RDC_PROCESS_READ_CHUNK=256*1024;
10
+ const RDC_PROCESS_SNAPSHOT_MAX=16*1024*1024;
11
+
12
+ function splitLineRanges(bytes){
13
+ const ranges=[];
14
+ let start=0;
15
+ for(let i=0;i<bytes.length;i++){
16
+ const b=bytes[i];
17
+ if(b!==0x0a&&b!==0x0d) continue;
18
+ let next=i+1;
19
+ if(b===0x0d&&next<bytes.length&&bytes[next]===0x0a) next++;
20
+ ranges.push({start,end:i,next});
21
+ start=next;i=next-1;
22
+ }
23
+ if(start<bytes.length) ranges.push({start,end:bytes.length,next:bytes.length});
24
+ return ranges;
25
+ }
26
+ function decodeLines(bytes,ranges){return ranges.map(r=>bytes.subarray(r.start,r.end).toString('utf8'));}
6
27
 
7
28
  const anyRecord = z.record(z.string(), z.unknown());
8
- const empty = z.strictObject({});
29
+ const empty = z.object({});
9
30
  const origin = z.enum(['ui','llm']).optional();
10
31
 
11
32
  const RDC = Object.freeze({
12
- read_file: z.strictObject({
33
+ read_file: z.object({
13
34
  path:z.string(), isUrl:z.boolean().optional().default(false),
14
35
  offset:z.number().optional().default(0), length:z.number().optional().default(1000),
15
36
  sheet:z.string().optional(), range:z.string().optional(), options:anyRecord.optional(),
16
37
  origin
17
38
  }),
18
- read_multiple_files:z.strictObject({paths:z.array(z.string())}),
19
- write_file:z.strictObject({
39
+ read_multiple_files:z.object({paths:z.array(z.string())}),
40
+ write_file:z.object({
20
41
  path:z.string(), content:z.string(),
21
42
  mode:z.enum(['rewrite','append']).default('rewrite'), origin
22
43
  }),
23
- create_directory:z.strictObject({path:z.string()}),
24
- list_directory:z.strictObject({path:z.string(),depth:z.number().optional().default(2),origin}),
25
- move_file:z.strictObject({source:z.string(),destination:z.string()}),
26
- get_file_info:z.strictObject({path:z.string()}),
44
+ create_directory:z.object({path:z.string()}),
45
+ list_directory:z.object({path:z.string(),depth:z.number().optional().default(2),origin}),
46
+ move_file:z.object({source:z.string(),destination:z.string()}),
47
+ get_file_info:z.object({path:z.string()}),
27
48
  edit_block:z.object({
28
49
  file_path:z.string(), old_string:z.string().optional(), new_string:z.string().optional(),
29
50
  expected_replacements:z.number().optional().default(1), range:z.string().optional(),
30
51
  content:z.unknown().optional(), options:anyRecord.optional(), origin
31
- }),
32
- start_process:z.strictObject({command:z.string(),timeout_ms:z.number(),shell:z.string().optional(),verbose_timing:z.boolean().optional(),origin}),
33
- read_process_output:z.strictObject({pid:z.number(),timeout_ms:z.number().optional(),offset:z.number().optional(),length:z.number().optional(),verbose_timing:z.boolean().optional()}),
34
- interact_with_process:z.strictObject({pid:z.number(),input:z.string(),timeout_ms:z.number().optional(),wait_for_prompt:z.boolean().optional(),verbose_timing:z.boolean().optional()}),
35
- force_terminate:z.strictObject({pid:z.number()}),
52
+ }).refine(data=>((data.old_string!==undefined&&data.old_string!==''&&data.new_string!==undefined)||(data.range!==undefined&&data.range!==''&&data.content!==undefined&&data.content!=='')),{message:'Must provide either (old_string + new_string) or (range + content)'}),
53
+ start_process:z.object({command:z.string(),timeout_ms:z.number(),shell:z.string().optional(),verbose_timing:z.boolean().optional(),origin}),
54
+ read_process_output:z.object({pid:z.number(),timeout_ms:z.number().optional(),offset:z.number().optional(),length:z.number().optional(),verbose_timing:z.boolean().optional()}),
55
+ interact_with_process:z.object({pid:z.number(),input:z.string(),timeout_ms:z.number().optional(),wait_for_prompt:z.boolean().optional(),verbose_timing:z.boolean().optional()}),
56
+ force_terminate:z.object({pid:z.number()}),
36
57
  list_sessions:empty,
37
58
  list_processes:empty,
38
- kill_process:z.strictObject({pid:z.number()}),
39
- start_search:z.strictObject({
59
+ kill_process:z.object({pid:z.number()}),
60
+ start_search:z.object({
40
61
  path:z.string(),pattern:z.string(),searchType:z.enum(['files','content']).default('files'),
41
62
  filePattern:z.string().optional(),ignoreCase:z.boolean().optional().default(true),
42
63
  maxResults:z.number().optional(),includeHidden:z.boolean().optional().default(false),
43
64
  contextLines:z.number().optional().default(5),timeout_ms:z.number().optional(),
44
65
  earlyTermination:z.boolean().optional(),literalSearch:z.boolean().optional().default(false),origin
45
66
  }),
46
- get_more_search_results:z.strictObject({sessionId:z.string(),offset:z.number().optional().default(0),length:z.number().optional().default(100)}),
47
- stop_search:z.strictObject({sessionId:z.string()}),
67
+ get_more_search_results:z.object({sessionId:z.string(),offset:z.number().optional().default(0),length:z.number().optional().default(100)}),
68
+ stop_search:z.object({sessionId:z.string()}),
48
69
  list_searches:empty,
49
- write_pdf:z.strictObject({
70
+ write_pdf:z.object({
50
71
  path:z.string(),
51
- content:z.union([z.string(),z.array(z.union([
52
- z.strictObject({type:z.literal('insert'),pageIndex:z.number(),markdown:z.string().optional(),sourcePdfPath:z.string().optional(),pdfOptions:anyRecord.optional()}),
53
- z.strictObject({type:z.literal('delete'),pageIndexes:z.array(z.number())})
54
- ]))]),
72
+ content:z.preprocess(value=>{
73
+ if(typeof value==='string'&&value.trim().startsWith('[')){
74
+ try{return JSON.parse(value);}catch{}
75
+ }
76
+ return value;
77
+ },z.union([z.string(),z.array(z.union([
78
+ z.object({type:z.literal('insert'),pageIndex:z.number(),markdown:z.string().optional(),sourcePdfPath:z.string().optional(),pdfOptions:anyRecord.optional()}),
79
+ z.object({type:z.literal('delete'),pageIndexes:z.array(z.number())})
80
+ ]))])),
55
81
  outputPath:z.string().optional(),
56
- options:anyRecord.optional(), origin
82
+ options:anyRecord.optional()
57
83
  })
58
84
  });
59
85
 
@@ -114,77 +140,204 @@ function richFormat(pathname){
114
140
  }[ext]??null;
115
141
  }
116
142
 
117
- export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime=null,searchRuntime=null,extendedRuntime=null}={}){
143
+ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime=null,searchRuntime=null,extendedRuntime=null,externalProcessRuntime=null,urlReader=readCommanderUrl}={}){
118
144
  const searchSessions=new Map();
119
145
 
120
146
  async function readFile(args){
121
- if(!machineFsRuntime) throw new Error('machine filesystem runtime is disabled');
122
- if(args.isUrl) throw new Error('URL reads are not available in the native DEADBYTE plane');
123
- if(args.sheet!==undefined||args.range!==undefined) {
124
- throw new Error('sheet/range reads require a native spreadsheet adapter; request rejected rather than silently ignored');
125
- }
126
- if(args.options!==undefined && Object.keys(args.options).length>0) {
127
- throw new Error('read_file options require a native format adapter; unsupported options are rejected rather than silently ignored');
147
+ const requestedOffset=args.offset??0;
148
+ const requestedLength=args.length??1000;
149
+ if(!Number.isInteger(requestedOffset)) throw new Error('offset must be an integer');
150
+ if(!Number.isInteger(requestedLength)||requestedLength<0) throw new Error('length must be a non-negative integer');
151
+ const ignoredOptions=args.options&&typeof args.options==='object'&&!Array.isArray(args.options)?Object.keys(args.options).sort():[];
152
+ const optionMeta=ignoredOptions.length?{ignoredOptions}:{};
153
+ if(args.isUrl){
154
+ if(args.sheet!==undefined||args.range!==undefined) throw new Error('sheet/range options are not valid for URL reads');
155
+ const remote=await urlReader(args.path);
156
+ if(remote.isPdf===true){
157
+ const pdf=readPdfText(Buffer.from(remote.content,'base64'));
158
+ const content=pdf.pages.map((page,index)=>`## Page ${index+1}\n${page}`).join('\n\n');
159
+ return text(content,false,{
160
+ path:args.path,content,mimeType:'text/markdown',isImage:false,isPdf:true,
161
+ pageOffset:0,pageCount:pdf.page_count,totalPages:pdf.page_count,profile:pdf.profile,partial:pdf.partial===true,warnings:pdf.warnings??[],
162
+ fileSha256:remote.sha256,url:remote.url,...optionMeta
163
+ });
164
+ }
165
+ return text(remote.content,false,{
166
+ path:args.path,content:remote.content,mimeType:remote.mimeType,
167
+ isImage:remote.isImage===true,isPdf:false,
168
+ lineOffset:0,lineCount:remote.isImage?0:remote.content.replaceAll('\r\n','\n').replaceAll('\r','\n').split('\n').length,
169
+ fileSha256:remote.sha256,url:remote.url,...optionMeta
170
+ });
128
171
  }
129
- if(!Number.isInteger(args.offset??0)||args.offset<0) throw new Error('offset must be a non-negative integer');
130
- if(!Number.isInteger(args.length??1000)||args.length<0) throw new Error('length must be a non-negative integer');
172
+ if(!machineFsRuntime) throw new Error('machine filesystem runtime is disabled');
131
173
  const target=resolveRdcPath(machineFsRuntime,args.path);
132
- const start=args.offset??0;
133
- const length=args.length??1000;
134
- const raw=await machineFsRuntime.read({rootId:target.rootId,relativePath:target.relativePath,offset:0,maxBytes:Math.min(4*1024*1024,machineFsRuntime.policy.max_read_bytes??4*1024*1024),hashFile:false});
135
- const fileSha256=raw.file_sha256??sha256Bytes(Buffer.from(raw.data_base64,'base64'));
136
174
  const ext=path.extname(args.path).toLowerCase();
175
+ const richDocument=ext==='.docx'||ext==='.xlsx'||ext==='.xlsm'||ext==='.pdf';
137
176
  const imageMime={'.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.gif':'image/gif','.webp':'image/webp'}[ext];
177
+ if((args.sheet!==undefined||args.range!==undefined)&&!(ext==='.xlsx'||ext==='.xlsm')) {
178
+ throw new Error('sheet/range reads are valid only for native XLSX/XLSM reads');
179
+ }
180
+ if(requestedOffset<0&&!richDocument&&!imageMime&&ext!=='.xls'){
181
+ const policyMax=Math.min(64*1024*1024,machineFsRuntime.policy.max_read_bytes??4*1024*1024);
182
+ const tail=await machineFsRuntime.read({
183
+ rootId:target.rootId,relativePath:target.relativePath,offset:-policyMax,maxBytes:policyMax,hashFile:false
184
+ });
185
+ let decoded=Buffer.from(tail.data_base64,'base64').toString('utf8').replaceAll('\r\n','\n').replaceAll('\r','\n');
186
+ if((tail.offset??0)>0){
187
+ const firstBreak=decoded.indexOf('\n');
188
+ if(firstBreak<0) throw new Error('text tail window contains no complete line');
189
+ decoded=decoded.slice(firstBreak+1);
190
+ }
191
+ let all=decoded.split('\n');
192
+ if(all.length&&all.at(-1)==='') all.pop();
193
+ const wanted=Math.abs(requestedOffset);
194
+ if((tail.offset??0)>0&&all.length<wanted) throw new Error(`text tail window does not contain ${wanted} complete lines`);
195
+ const selected=all.slice(Math.max(0,all.length-wanted));
196
+ const content=selected.join('\n');
197
+ return text(content,false,{
198
+ path:args.path,content,mimeType:'text/plain',isImage:false,isPdf:false,
199
+ lineOffset:Math.max(0,all.length-selected.length),lineCount:selected.length,
200
+ fileSha256:tail.file_sha256??null,tailByteOffset:tail.offset??null,fileSize:tail.file_size??null,...optionMeta
201
+ });
202
+ }
203
+ const maxRead=richDocument?64*1024*1024:4*1024*1024;
204
+ const raw=await machineFsRuntime.read({
205
+ rootId:target.rootId,relativePath:target.relativePath,offset:0,
206
+ maxBytes:Math.min(maxRead,machineFsRuntime.policy.max_read_bytes??maxRead),
207
+ hashFile:richDocument
208
+ });
209
+ if(richDocument&&raw.eof===false) throw new Error('document exceeds bounded native read limit');
210
+ if(!richDocument&&raw.eof===false) throw new Error('text file exceeds bounded compatibility read limit');
211
+ const bytes=Buffer.from(raw.data_base64,'base64');
212
+ const fileSha256=raw.file_sha256??sha256Bytes(bytes);
213
+
214
+ if(ext==='.docx'){
215
+ const doc=readDocxBytes(bytes,{path:args.path,offset:requestedOffset,length:requestedLength});
216
+ return text(doc.content,false,{path:args.path,content:doc.content,mimeType:doc.mimeType,isImage:false,isPdf:false,fileSha256,...doc.metadata,outline:doc.outline,...optionMeta});
217
+ }
218
+ if(ext==='.xlsx'||ext==='.xlsm'){
219
+ const book=readXlsxBytes(bytes,{path:args.path,sheet:args.sheet,range:args.range,offset:requestedOffset,length:requestedLength});
220
+ return text(book.content,false,{path:args.path,content:book.content,mimeType:book.mimeType,isImage:false,isPdf:false,fileSha256,...book.metadata,sheetName:book.sheetName,data:book.data,totalRows:book.totalRows,returnedRows:book.returnedRows,...optionMeta});
221
+ }
222
+ if(ext==='.pdf'){
223
+ const pdf=readPdfText(bytes);
224
+ const start=requestedOffset<0?Math.max(0,pdf.page_count+requestedOffset):Math.min(requestedOffset,pdf.page_count);
225
+ const count=requestedOffset<0?pdf.page_count-start:requestedLength;
226
+ const selected=pdf.pages.slice(start,start+count);
227
+ const content=selected.map((page,index)=>`## Page ${start+index+1}\n${page}`).join('\n\n');
228
+ return text(content,false,{
229
+ path:args.path,content,mimeType:'text/markdown',isImage:false,isPdf:true,fileSha256,
230
+ pageOffset:start,pageCount:selected.length,totalPages:pdf.page_count,profile:pdf.profile,partial:pdf.partial===true,warnings:pdf.warnings??[],...optionMeta
231
+ });
232
+ }
233
+
138
234
  if(imageMime) {
139
235
  const content=raw.data_base64;
140
- return text(content,false,{path:args.path,content,mimeType:imageMime,isImage:true,isPdf:false,fileSha256});
236
+ return text(content,false,{path:args.path,content,mimeType:imageMime,isImage:true,isPdf:false,fileSha256,...optionMeta});
141
237
  }
142
- if(['.xlsx','.xls','.xlsm','.pdf','.docx'].includes(ext)) {
143
- throw new Error(`format ${ext} requires a native document adapter; request rejected rather than returned as plain text`);
238
+ if(ext==='.xls') {
239
+ throw new Error('format .xls requires a native BIFF adapter; request rejected rather than returned as plain text');
144
240
  }
145
241
  const all=linesFrom(raw.data_base64);
146
- const selected=all.slice(start,start+length);
147
- const content=selected.join('\\n')+(start+length<all.length?'\\n':'');
148
- return text(content,false,{path:args.path,content,mimeType:'text/plain',isImage:false,isPdf:false,lineOffset:start,lineCount:selected.length,fileSha256});
242
+ const start=requestedOffset<0?Math.max(0,all.length+requestedOffset):Math.min(requestedOffset,all.length);
243
+ const end=requestedOffset<0?all.length:Math.min(all.length,start+requestedLength);
244
+ const selected=all.slice(start,end);
245
+ const content=selected.join('\n')+(requestedOffset>=0&&end<all.length?'\n':'');
246
+ return text(content,false,{path:args.path,content,mimeType:'text/plain',isImage:false,isPdf:false,lineOffset:start,lineCount:selected.length,fileSha256,...optionMeta});
149
247
  }
150
248
 
151
249
  async function readMultipleFiles(args){
152
- if(!machineFsRuntime) throw new Error('machine filesystem runtime is disabled');
153
250
  const results=[];
154
251
  for(const pathname of args.paths){
155
252
  try{
156
- const target=resolveRdcPath(machineFsRuntime,pathname);
157
- const raw=await machineFsRuntime.read({rootId:target.rootId,relativePath:target.relativePath,offset:0,maxBytes:Math.min(4*1024*1024,machineFsRuntime.policy.max_read_bytes??4*1024*1024),hashFile:false});
158
- const fileSha256=raw.file_sha256??sha256Bytes(Buffer.from(raw.data_base64,'base64'));
159
- const ext=path.extname(pathname).toLowerCase();
160
- const mime=richFormat(pathname);
161
- if(['.png','.jpg','.jpeg','.gif','.webp'].includes(ext)){
162
- results.push(rdcFileResult(pathname,{content:raw.data_base64,mime_type:mime,isImage:true,isPdf:false,fileSha256}));
163
- } else if(['.xlsx','.xls','.xlsm','.pdf','.docx'].includes(ext)){
164
- results.push({path:pathname,error:'format '+ext+' requires a native document adapter; request rejected rather than returned as plain text'});
165
- } else {
166
- const content=Buffer.from(raw.data_base64,'base64').toString('utf8');
167
- results.push(rdcFileResult(pathname,{content,mime_type:'text/plain',isImage:false,isPdf:false,fileSha256}));
168
- }
253
+ const result=await readFile({path:pathname,isUrl:false,offset:0,length:1000});
254
+ if(result?.isError) throw new Error(result?.structuredContent?.error??result?.content?.[0]?.text??'read failed');
255
+ const item=result?.structuredContent??{};
256
+ results.push({
257
+ path:pathname,
258
+ content:String(item.content??result?.content?.[0]?.text??''),
259
+ mimeType:item.mimeType??'text/plain',
260
+ isImage:item.isImage===true,
261
+ isPdf:item.isPdf===true,
262
+ fileSha256:item.fileSha256??null,
263
+ ...(item.isPdf?{payload:{
264
+ metadata:{totalPages:item.totalPages??item.pageCount??0,profile:item.profile??null},
265
+ pages:item.content?String(item.content).split(/\n\n(?=## Page \d+\n)/):[]
266
+ }}:{})
267
+ });
169
268
  }catch(error){
170
269
  results.push({path:pathname,error:error instanceof Error?error.message:String(error)});
171
270
  }
172
271
  }
173
- return text(results.map(x=>x.error?`${x.path}: Error: ${x.error}`:`${x.path}:\\n${x.content}`).join('\\n\\n'),false,{results});
272
+ return text(results.map(x=>x.error?`${x.path}: Error: ${x.error}`:`${x.path}:\n${x.content}`).join('\n\n'),false,{results});
174
273
  }
175
274
 
176
275
  async function writeFile(args){
177
276
  const target=resolveRdcPath(machineFsRuntime,args.path);
277
+ const ext=path.extname(args.path).toLowerCase();
278
+ const imageExts=new Set(['.png','.jpg','.jpeg','.gif','.webp']);
279
+ if(ext==='.pdf') throw new Error('PDF writes must use write_pdf to preserve document semantics');
280
+ if(ext==='.docx'){
281
+ if(args.mode==='append') throw new Error('DOCX append not supported. Use edit_block to modify existing DOCX files.');
282
+ const generated=createDocxBytes(args.content);
283
+ let expected='absent';
284
+ try{
285
+ const current=await machineFsRuntime.read({rootId:target.rootId,relativePath:target.relativePath,offset:0,maxBytes:Math.min(64*1024*1024,machineFsRuntime.policy.max_read_bytes??64*1024*1024),hashFile:true});
286
+ if(current.eof===false) throw new Error('DOCX target exceeds bounded native write limit');
287
+ expected=current.file_sha256??sha256Bytes(Buffer.from(current.data_base64,'base64'));
288
+ }catch(error){if(!/not found|ENOENT/i.test(String(error))) throw error;}
289
+ const result=await machineFsRuntime.write({rootId:target.rootId,relativePath:target.relativePath,content:generated.bytes.toString('base64'),encoding:'base64',expectedSha256:expected});
290
+ return text(`Successfully wrote ${args.path}`,false,{path:args.path,mode:args.mode,status:'committed',format:'docx',bytesSha256:generated.bytes_sha256,...result});
291
+ }
292
+ if(imageExts.has(ext)) throw new Error(`image write_file for ${ext} requires a dedicated native writer`);
293
+ if(ext==='.xls') throw new Error('legacy .xls BIFF write is not supported by a native writer');
294
+
295
+ if(ext==='.xlsx'||ext==='.xlsm'){
296
+ let parsed;
297
+ try{parsed=JSON.parse(args.content);}catch{throw new Error('Invalid XLSX content: expected JSON 2D array or object of sheet arrays');}
298
+ let expected='absent',existing=null;
299
+ try{
300
+ const maxRead=Math.min(64*1024*1024,machineFsRuntime.policy.max_read_bytes??64*1024*1024);
301
+ const current=await machineFsRuntime.read({
302
+ rootId:target.rootId,relativePath:target.relativePath,offset:0,maxBytes:maxRead,hashFile:true
303
+ });
304
+ if(current.eof===false) throw new Error('XLSX target exceeds bounded native write limit');
305
+ existing=Buffer.from(current.data_base64,'base64');
306
+ expected=current.file_sha256??sha256Bytes(existing);
307
+ }catch(error){
308
+ if(!/not found|ENOENT/i.test(String(error))) throw error;
309
+ if(args.mode==='append') existing=null;
310
+ }
311
+ const generated=args.mode==='append'&&existing?appendXlsxBytes(existing,parsed):createXlsxBytes(parsed);
312
+ const bytes=generated.bytes??Buffer.from(generated.bytes_base64,'base64');
313
+ const result=await machineFsRuntime.write({
314
+ rootId:target.rootId,relativePath:target.relativePath,
315
+ content:bytes.toString('base64'),encoding:'base64',expectedSha256:expected
316
+ });
317
+ return text(`Successfully wrote ${args.path}`,false,{
318
+ path:args.path,mode:args.mode,status:'committed',format:'xlsx',
319
+ bytesSha256:generated.bytes_sha256??generated.after_sha256??sha256Bytes(bytes),
320
+ appendedRows:generated.appended_rows??0,sheetCount:generated.sheet_count??null,...result
321
+ });
322
+ }
323
+
178
324
  let expected='absent';
179
325
  let content=args.content;
180
326
  try{
181
- const current=await machineFsRuntime.read({rootId:target.rootId,relativePath:target.relativePath,offset:0,maxBytes:Math.min(4*1024*1024,machineFsRuntime.policy.max_read_bytes??4*1024*1024),hashFile:true});
182
- expected=current.file_sha256??'absent';
183
- if(args.mode==='append') content=Buffer.from(current.data_base64,'base64').toString('utf8')+args.content;
327
+ const current=await machineFsRuntime.read({
328
+ rootId:target.rootId,relativePath:target.relativePath,offset:0,
329
+ maxBytes:Math.min(4*1024*1024,machineFsRuntime.policy.max_read_bytes??4*1024*1024),hashFile:true
330
+ });
331
+ if(current.eof===false) throw new Error('text write target exceeds bounded compatibility limit');
332
+ const currentBytes=Buffer.from(current.data_base64,'base64');
333
+ expected=current.file_sha256??sha256Bytes(currentBytes);
334
+ if(args.mode==='append') content=currentBytes.toString('utf8')+args.content;
184
335
  }catch(error){
185
- if(args.mode==='append' || !/not found|ENOENT/i.test(String(error))) throw error;
336
+ if(args.mode==='append'||!/not found|ENOENT/i.test(String(error))) throw error;
186
337
  }
187
- const result=await machineFsRuntime.write({rootId:target.rootId,relativePath:target.relativePath,content,encoding:'utf8',expectedSha256:expected});
338
+ const result=await machineFsRuntime.write({
339
+ rootId:target.rootId,relativePath:target.relativePath,content,encoding:'utf8',expectedSha256:expected
340
+ });
188
341
  return text(`Successfully wrote ${args.path}`,false,{path:args.path,mode:args.mode,status:'committed',...result});
189
342
  }
190
343
 
@@ -196,9 +349,40 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
196
349
 
197
350
  async function listDirectory(args){
198
351
  const target=resolveRdcPath(machineFsRuntime,args.path);
199
- const entries=await machineFsRuntime.list({rootId:target.rootId,relativePath:target.relativePath,limit:1000,hashFiles:false});
200
- const rows=entries.entries.map(e=>`${e.type==='directory'?'[DIR]':'[FILE]'} ${e.path}`);
201
- return text(rows.join('\\n'),false,{path:args.path,entries:entries.entries,next_cursor:entries.next_cursor??null});
352
+ const depth=args.depth??2;
353
+ if(!Number.isInteger(depth)||depth<1||depth>10) throw new Error('list_directory depth must be an integer in [1, 10]');
354
+ const maxEntries=4096,all=[];
355
+ let truncated=false;
356
+ let frontier=[{relativePath:target.relativePath,level:1}];
357
+ for(let level=1;level<=depth&&frontier.length&&!truncated;level++){
358
+ const next=[];
359
+ for(const node of frontier){
360
+ let cursor=null;
361
+ const seen=new Set();
362
+ do{
363
+ const page=await machineFsRuntime.list({
364
+ rootId:target.rootId,relativePath:node.relativePath,cursor,limit:1000,hashFiles:false
365
+ });
366
+ for(const entry of page.entries??[]){
367
+ if(all.length>=maxEntries){truncated=true;break;}
368
+ all.push(entry);
369
+ const kind=entry.kind??entry.type;
370
+ if(kind==='directory'&&level<depth) next.push({relativePath:entry.path,level:level+1});
371
+ }
372
+ if(truncated) break;
373
+ const nextCursor=page.next_cursor??null;
374
+ if(nextCursor!==null){
375
+ if(seen.has(nextCursor)) throw new Error('list_directory cursor loop detected');
376
+ seen.add(nextCursor);
377
+ }
378
+ cursor=nextCursor;
379
+ }while(cursor!==null);
380
+ if(truncated) break;
381
+ }
382
+ frontier=next;
383
+ }
384
+ const rows=all.map(e=>`${(e.kind??e.type)==='directory'?'[DIR]':'[FILE]'} ${e.path}`);
385
+ return text(rows.join('\n'),false,{path:args.path,depth,entries:all,truncated,entryCount:all.length});
202
386
  }
203
387
 
204
388
  async function moveFile(args){
@@ -216,24 +400,99 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
216
400
  }
217
401
 
218
402
  async function editBlock(args){
219
- if(args.range!==undefined) throw new Error('structured range edits require a native DEADBYTE document adapter');
220
- if(args.old_string===undefined||args.new_string===undefined) throw new Error('Must provide either old_string + new_string');
221
403
  const target=resolveRdcPath(machineFsRuntime,args.file_path);
404
+ const ext=path.extname(args.file_path).toLowerCase();
405
+ if(ext==='.docx'){
406
+ if(args.range!==undefined) throw new Error('DOCX edit_block uses old_string/new_string XML replacement, not range mode');
407
+ if(args.old_string===undefined||args.new_string===undefined) throw new Error('DOCX edit requires old_string + new_string');
408
+ const maxRead=Math.min(64*1024*1024,machineFsRuntime.policy.max_read_bytes??64*1024*1024);
409
+ const raw=await machineFsRuntime.read({
410
+ rootId:target.rootId,relativePath:target.relativePath,offset:0,maxBytes:maxRead,hashFile:true
411
+ });
412
+ if(raw.eof===false) throw new Error('DOCX document exceeds bounded native edit limit');
413
+ const before=Buffer.from(raw.data_base64,'base64');
414
+ const beforeSha=raw.file_sha256??sha256Bytes(before);
415
+ const edited=editDocxBytes(before,{
416
+ oldString:args.old_string,newString:args.new_string,expectedReplacements:args.expected_replacements
417
+ });
418
+ const result=await machineFsRuntime.write({
419
+ rootId:target.rootId,relativePath:target.relativePath,
420
+ content:edited.bytes_base64,encoding:'base64',expectedSha256:beforeSha
421
+ });
422
+ return text(`Successfully edited ${args.file_path}`,false,{
423
+ path:args.file_path,status:'committed',format:'docx',replacements:edited.replacements,
424
+ beforeSha256:edited.before_sha256,afterSha256:edited.after_sha256,...result
425
+ });
426
+ }
427
+ if(args.range!==undefined){
428
+ const ext=path.extname(args.file_path).toLowerCase();
429
+ if(ext!=='.xlsx'&&ext!=='.xlsm') throw new Error('structured range edits are supported only for XLSX/XLSM');
430
+ if(args.content===undefined) throw new Error('range edit requires content');
431
+ const maxRead=Math.min(64*1024*1024,machineFsRuntime.policy.max_read_bytes??64*1024*1024);
432
+ const raw=await machineFsRuntime.read({
433
+ rootId:target.rootId,relativePath:target.relativePath,offset:0,maxBytes:maxRead,hashFile:true
434
+ });
435
+ if(raw.eof===false) throw new Error('XLSX document exceeds bounded native edit limit');
436
+ const before=Buffer.from(raw.data_base64,'base64');
437
+ const beforeSha=raw.file_sha256??sha256Bytes(before);
438
+ const edited=editXlsxBytes(before,{range:args.range,content:args.content});
439
+ const result=await machineFsRuntime.write({
440
+ rootId:target.rootId,relativePath:target.relativePath,
441
+ content:edited.bytes_base64,encoding:'base64',expectedSha256:beforeSha
442
+ });
443
+ const ignoredOptions=args.options&&typeof args.options==='object'&&!Array.isArray(args.options)?Object.keys(args.options).sort():[];
444
+ return text(`Successfully edited ${args.file_path} range ${args.range}`,false,{
445
+ path:args.file_path,status:'committed',range:args.range,sheet:edited.sheet,
446
+ updatedCells:edited.updated_cells,beforeSha256:edited.before_sha256,afterSha256:edited.after_sha256,
447
+ ...(ignoredOptions.length?{ignoredOptions}:{}),...result
448
+ });
449
+ }
450
+ if(args.old_string===undefined||args.new_string===undefined) throw new Error('Must provide either old_string + new_string');
222
451
  const raw=await machineFsRuntime.read({rootId:target.rootId,relativePath:target.relativePath,offset:0,maxBytes:Math.min(4*1024*1024,machineFsRuntime.policy.max_read_bytes??4*1024*1024),hashFile:false});
223
- const fileSha256=raw.file_sha256??sha256Bytes(Buffer.from(raw.data_base64,'base64'));
224
- const current=Buffer.from(raw.data_base64,'base64').toString('utf8');
452
+ if(raw.eof===false) throw new Error('text edit target exceeds bounded compatibility edit limit');
453
+ const before=Buffer.from(raw.data_base64,'base64');
454
+ const fileSha256=raw.file_sha256??sha256Bytes(before);
455
+ const current=before.toString('utf8');
225
456
  let count=0,index=0;
226
457
  while((index=current.indexOf(args.old_string,index))>=0){count++;index+=Math.max(1,args.old_string.length);}
227
458
  if(count!==args.expected_replacements) throw new Error(`expected ${args.expected_replacements} replacements, found ${count}`);
228
459
  const next=current.split(args.old_string).join(args.new_string);
229
- const result=await machineFsRuntime.write({rootId:target.rootId,relativePath:target.relativePath,content:next,encoding:'utf8',expectedSha256:raw.file_sha256});
460
+ const result=await machineFsRuntime.write({rootId:target.rootId,relativePath:target.relativePath,content:next,encoding:'utf8',expectedSha256:fileSha256});
230
461
  return text(`Successfully edited ${args.file_path}`,false,{path:args.file_path,status:'committed',replacements:count,...result});
231
462
  }
232
463
 
233
- function profileForCommand(){
234
- const id=process.env.DEADBYTE_COMMANDER_COMPAT_PROFILE;
235
- if(!id) throw new Error('RDC start_process requires DEADBYTE_COMMANDER_COMPAT_PROFILE to bind command execution to a configured profile');
236
- return id;
464
+ function shellKey(value){
465
+ if(typeof value!=='string'||!value.trim()) return null;
466
+ const base=path.win32.basename(value.trim()).toLowerCase();
467
+ return base.endsWith('.exe')?base.slice(0,-4):base;
468
+ }
469
+ function profileForCommand(shell=null){
470
+ const forced=process.env.DEADBYTE_COMMANDER_COMPAT_PROFILE;
471
+ if(forced) return forced;
472
+ const profiles=processRuntime?.policy?.profiles??{};
473
+ const requested=shellKey(shell);
474
+ if(requested){
475
+ const matches=Object.entries(profiles).filter(([,profile])=>shellKey(profile?.executable)===requested);
476
+ const raw=matches.find(([,profile])=>profile?.allow_raw_input===true);
477
+ if(raw) return raw[0];
478
+ if(matches.length===1) return matches[0][0];
479
+ if(matches.length>1) throw new Error(`RDC shell '${shell}' maps to multiple process profiles; set DEADBYTE_COMMANDER_COMPAT_PROFILE`);
480
+ throw new Error(`RDC shell '${shell}' is not represented by an approved DEADBYTE process profile`);
481
+ }
482
+ if(profiles.powershell_repl?.allow_raw_input===true) return 'powershell_repl';
483
+ const raw=Object.entries(profiles).filter(([,profile])=>profile?.allow_raw_input===true);
484
+ if(raw.length===1) return raw[0][0];
485
+ throw new Error('RDC start_process requires an approved raw-input process profile or DEADBYTE_COMMANDER_COMPAT_PROFILE');
486
+ }
487
+ function assertRequestedShell(profileId,shell){
488
+ if(shell===undefined) return;
489
+ const profile=processRuntime?.policy?.profiles?.[profileId];
490
+ if(profile){
491
+ if(shellKey(profile.executable)!==shellKey(shell)) throw new Error(`RDC shell '${shell}' does not match approved profile '${profileId}'`);
492
+ return;
493
+ }
494
+ const configured=process.env.DEADBYTE_COMMANDER_COMPAT_SHELL;
495
+ if(configured!==shell) throw new Error('RDC shell does not match the explicitly configured compatibility shell');
237
496
  }
238
497
  function defaultProcessRoot(){
239
498
  const id=process.env.DEADBYTE_COMMANDER_COMPAT_ROOT;
@@ -243,14 +502,114 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
243
502
  throw new Error('RDC start_process requires DEADBYTE_COMMANDER_COMPAT_ROOT when multiple process roots exist');
244
503
  }
245
504
  const pidMap=new Map();
505
+ const processCompatState=new Map();
506
+
507
+ function boundedLineLength(value){
508
+ const n=value??1000;
509
+ if(!Number.isInteger(n)||n<1||n>100000) throw new Error('RDC process length must be an integer in [1,100000]');
510
+ return n;
511
+ }
512
+ function boundedWait(value,fallback){
513
+ const n=value??fallback;
514
+ if(!Number.isFinite(n)||n<0||n>300000) throw new Error('RDC process timeout_ms must be in [0,300000]');
515
+ return Math.floor(n);
516
+ }
517
+ async function processStreamSnapshot(sessionId){
518
+ const probe=await processRuntime.read({sessionId,offset:0,maxBytes:1,waitMs:0});
519
+ const earliest=Number.isInteger(probe.earliest_offset)?probe.earliest_offset:probe.offset;
520
+ const streamHead=Number.isInteger(probe.stream_head)?probe.stream_head:probe.next_offset;
521
+ if(!Number.isInteger(earliest)||!Number.isInteger(streamHead)||earliest<0||streamHead<earliest) throw new Error('native process stream identity invalid');
522
+ if(streamHead-earliest>RDC_PROCESS_SNAPSHOT_MAX) throw new Error(`RDC retained process output exceeds ${RDC_PROCESS_SNAPSHOT_MAX} byte compatibility bound`);
523
+ const chunks=[];
524
+ let position=earliest;
525
+ while(position<streamHead){
526
+ const page=await processRuntime.read({
527
+ sessionId,offset:position,maxBytes:Math.min(RDC_PROCESS_READ_CHUNK,streamHead-position),waitMs:0
528
+ });
529
+ if(page.offset!==position) throw new Error('native process output moved during compatibility snapshot');
530
+ const data=Buffer.from(page.data_base64??'','base64');
531
+ if(data.length<1) throw new Error('native process output snapshot made no forward progress');
532
+ chunks.push(data);
533
+ const next=page.next_offset;
534
+ if(!Number.isInteger(next)||next<=position||next>streamHead) throw new Error('native process output cursor invalid');
535
+ position=next;
536
+ }
537
+ return {
538
+ bytes:Buffer.concat(chunks),
539
+ earliest_offset:earliest,
540
+ stream_head:streamHead,
541
+ state:probe.state,
542
+ exit_code:probe.exit_code
543
+ };
544
+ }
545
+ async function waitForStreamAdvance(sessionId,baseline,timeoutMs){
546
+ let remaining=timeoutMs;
547
+ while(remaining>0){
548
+ const slice=Math.min(30000,remaining);
549
+ const observed=await processRuntime.read({sessionId,offset:baseline,maxBytes:1,waitMs:slice});
550
+ if((Number.isInteger(observed.stream_head)&&observed.stream_head>baseline)||observed.state!=='running') return observed;
551
+ remaining-=slice;
552
+ }
553
+ return null;
554
+ }
555
+ async function readProcessRange(sessionId,start,end){
556
+ if(!Number.isInteger(start)||!Number.isInteger(end)||start<0||end<start) throw new Error('native process range invalid');
557
+ if(end-start>RDC_PROCESS_SNAPSHOT_MAX) throw new Error(`RDC process response exceeds ${RDC_PROCESS_SNAPSHOT_MAX} byte compatibility bound`);
558
+ const chunks=[];
559
+ let position=start;
560
+ while(position<end){
561
+ const page=await processRuntime.read({sessionId,offset:position,maxBytes:Math.min(RDC_PROCESS_READ_CHUNK,end-position),waitMs:0});
562
+ if(page.offset!==position) throw new Error('native process response moved during compatibility read');
563
+ const data=Buffer.from(page.data_base64??'','base64');
564
+ if(data.length<1) throw new Error('native process response made no forward progress');
565
+ chunks.push(data);
566
+ const next=page.next_offset;
567
+ if(!Number.isInteger(next)||next<=position||next>end) throw new Error('native process response cursor invalid');
568
+ position=next;
569
+ }
570
+ return Buffer.concat(chunks);
571
+ }
572
+ function lineIndexAtByte(ranges,relativeByte){
573
+ let index=0;
574
+ while(index<ranges.length&&ranges[index].next<=relativeByte) index++;
575
+ return index;
576
+ }
577
+ function processOutputText({readFrom,readCount,totalLines,remaining,lines,state,exitCode,truncated}){
578
+ let header=readFrom===0
579
+ ?`[Reading ${readCount} lines (total: ${totalLines} lines)]`
580
+ :`[Reading ${readCount} lines from line ${readFrom} (total: ${totalLines} lines, ${remaining} remaining)]`;
581
+ if(truncated) header+='\n[WARNING: earliest retained process output was evicted; line numbers refer to retained output only]';
582
+ const body=lines.length?lines.join('\n'):'(No output in requested range)';
583
+ const terminal=state==='running'?'':`\nProcess ${state}${exitCode===null||exitCode===undefined?'':` with exit code ${exitCode}`}`;
584
+ return `${header}\n\n${body}${terminal}`;
585
+ }
246
586
 
247
587
  async function startProcess(args){
248
- if(args.shell!==undefined && args.shell!==process.env.DEADBYTE_COMMANDER_COMPAT_SHELL) throw new Error('RDC shell is not silently ignored; configure DEADBYTE_COMMANDER_COMPAT_SHELL to the approved native shell');
249
- const profileId=profileForCommand(),rootId=defaultProcessRoot();
250
- const native=await processRuntime.start({profileId,rootId,cwd:'.',args:args.command.trim()===''?[]:args.command.trim().split(/\\s+/),timeoutMs:args.timeout_ms});
588
+ if(!processRuntime) throw new Error('process runtime is disabled');
589
+ const profileId=profileForCommand(args.shell??null),rootId=defaultProcessRoot();
590
+ assertRequestedShell(profileId,args.shell);
591
+ const profile=processRuntime?.policy?.profiles?.[profileId]??null;
592
+ const rawInput=profile?.allow_raw_input===true;
593
+ const native=await processRuntime.start({
594
+ profileId,rootId,cwd:'.',
595
+ args:rawInput?[]:(args.command.length===0?[]:[args.command]),
596
+ timeoutMs:args.timeout_ms
597
+ });
598
+ try{
599
+ if(rawInput&&args.command.length>0){
600
+ await processRuntime.write({sessionId:native.session_id,input:args.command,appendNewline:true});
601
+ }
602
+ }catch(error){
603
+ await processRuntime.terminate({sessionId:native.session_id}).catch(()=>{});
604
+ throw error;
605
+ }
251
606
  const compatPid=native.child_pid??0;
252
- if(!Number.isInteger(compatPid)||compatPid<=0) throw new Error('native process did not expose a usable child PID');
607
+ if(!Number.isInteger(compatPid)||compatPid<=0) {
608
+ await processRuntime.terminate({sessionId:native.session_id}).catch(()=>{});
609
+ throw new Error('native process did not expose a usable child PID');
610
+ }
253
611
  pidMap.set(compatPid,native.session_id);
612
+ processCompatState.set(compatPid,{last_read_byte:0});
254
613
  return text(`Process started with PID ${compatPid} (DEADBYTE session ${native.session_id})\\n`,false,{pid:compatPid,session_id:native.session_id});
255
614
  }
256
615
  function sessionForPid(pid){
@@ -260,60 +619,193 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
260
619
  }
261
620
  async function readProcessOutput(args){
262
621
  const sessionId=sessionForPid(args.pid);
263
- const native=await processRuntime.read({sessionId,offset:args.offset??0,maxBytes:262144,waitMs:args.timeout_ms??0});
264
- const output=Buffer.from(native.data_base64,'base64').toString('utf8');
265
- return text(output,false,{pid:args.pid,session_id:sessionId,offset:native.offset,next_offset:native.next_offset,state:native.state,exit_code:native.exit_code});
622
+ const state=processCompatState.get(args.pid)??{last_read_byte:0};
623
+ const requestedOffset=args.offset??0;
624
+ if(!Number.isInteger(requestedOffset)||requestedOffset<-(1<<30)||requestedOffset>(1<<30)) throw new Error('RDC process offset invalid');
625
+ const length=boundedLineLength(args.length);
626
+ const waitMs=boundedWait(args.timeout_ms,5000);
627
+ let snapshot=await processStreamSnapshot(sessionId);
628
+ let retainedRanges=splitLineRanges(snapshot.bytes);
629
+ if(requestedOffset===0){
630
+ let startByte=Math.max(state.last_read_byte,snapshot.earliest_offset);
631
+ if(startByte>=snapshot.stream_head&&snapshot.state==='running'&&waitMs>0){
632
+ await waitForStreamAdvance(sessionId,snapshot.stream_head,waitMs);
633
+ snapshot=await processStreamSnapshot(sessionId);
634
+ retainedRanges=splitLineRanges(snapshot.bytes);
635
+ startByte=Math.max(state.last_read_byte,snapshot.earliest_offset);
636
+ }
637
+ const wasTruncated=state.last_read_byte<snapshot.earliest_offset;
638
+ const relativeStart=Math.max(0,startByte-snapshot.earliest_offset);
639
+ const unread=snapshot.bytes.subarray(relativeStart);
640
+ const unreadRanges=splitLineRanges(unread);
641
+ const selected=unreadRanges.slice(0,length);
642
+ const lines=decodeLines(unread,selected);
643
+ const readFrom=lineIndexAtByte(retainedRanges,relativeStart);
644
+ const readCount=selected.length;
645
+ const remaining=Math.max(0,unreadRanges.length-readCount);
646
+ const consumed=selected.length?selected.at(-1).next:0;
647
+ state.last_read_byte=startByte+consumed;
648
+ processCompatState.set(args.pid,state);
649
+ return text(processOutputText({
650
+ readFrom,readCount,totalLines:retainedRanges.length,remaining,lines,
651
+ state:snapshot.state,exitCode:snapshot.exit_code,truncated:wasTruncated
652
+ }),false,{
653
+ pid:args.pid,session_id:sessionId,read_from:readFrom,read_count:readCount,total_lines:retainedRanges.length,
654
+ remaining,next_line:readFrom+readCount,stream_head:snapshot.stream_head,state:snapshot.state,exit_code:snapshot.exit_code
655
+ });
656
+ }
657
+ const totalLines=retainedRanges.length;
658
+ const readFrom=requestedOffset<0?Math.max(0,totalLines+requestedOffset):Math.min(requestedOffset,totalLines);
659
+ const selected=retainedRanges.slice(readFrom,readFrom+length);
660
+ const lines=decodeLines(snapshot.bytes,selected);
661
+ const readCount=selected.length,remaining=Math.max(0,totalLines-(readFrom+readCount));
662
+ return text(processOutputText({
663
+ readFrom,readCount,totalLines,remaining,lines,state:snapshot.state,exitCode:snapshot.exit_code,truncated:snapshot.earliest_offset>0
664
+ }),false,{
665
+ pid:args.pid,session_id:sessionId,read_from:readFrom,read_count:readCount,total_lines:totalLines,
666
+ remaining,next_line:readFrom+readCount,stream_head:snapshot.stream_head,state:snapshot.state,exit_code:snapshot.exit_code
667
+ });
266
668
  }
267
669
  async function interactProcess(args){
268
670
  const sessionId=sessionForPid(args.pid);
269
- const native=await processRuntime.write({sessionId,input:args.input,appendNewline:true});
270
- return text(`Input sent to process ${args.pid}`,false,{pid:args.pid,session_id:sessionId,...native});
671
+ const before=await processStreamSnapshot(sessionId);
672
+ const written=await processRuntime.write({sessionId,input:args.input,appendNewline:true});
673
+ const waitForPrompt=args.wait_for_prompt??true;
674
+ if(!waitForPrompt){
675
+ return text(`Input sent to process ${args.pid}. Use read_process_output to get the response.`,false,{
676
+ pid:args.pid,session_id:sessionId,wait_for_prompt:false,...written
677
+ });
678
+ }
679
+ const waitMs=boundedWait(args.timeout_ms,8000);
680
+ let observed=await processRuntime.read({sessionId,offset:before.stream_head,maxBytes:1,waitMs:0});
681
+ if((observed.stream_head??before.stream_head)<=before.stream_head&&observed.state==='running'&&waitMs>0){
682
+ observed=await waitForStreamAdvance(sessionId,before.stream_head,waitMs)??observed;
683
+ }
684
+ const streamHead=Number.isInteger(observed.stream_head)?observed.stream_head:before.stream_head;
685
+ const earliest=Number.isInteger(observed.earliest_offset)?observed.earliest_offset:observed.offset;
686
+ const start=Math.max(before.stream_head,earliest);
687
+ const responseBytes=streamHead>start?await readProcessRange(sessionId,start,streamHead):Buffer.alloc(0);
688
+ const response=responseBytes.toString('utf8');
689
+ const output=response.length?response:'(No output produced)';
690
+ return text(`Input executed in process ${args.pid}:\n\n${output}`,false,{
691
+ pid:args.pid,session_id:sessionId,wait_for_prompt:true,response_from_offset:start,
692
+ stream_head:streamHead,state:observed.state,exit_code:observed.exit_code,...written
693
+ });
271
694
  }
272
695
  async function terminateProcess(args){
273
696
  const sessionId=sessionForPid(args.pid);
274
697
  const native=await processRuntime.terminate({sessionId});
275
698
  pidMap.delete(args.pid);
699
+ processCompatState.delete(args.pid);
276
700
  return text(`Successfully terminated process ${args.pid}`,false,{pid:args.pid,session_id:sessionId,...native});
277
701
  }
278
702
  async function listSessions(){
279
703
  const native=await processRuntime.list();
280
704
  return text(native.sessions.map(s=>`Session ${s.session_id}: state=${s.state}, profile=${s.profile_id}, root=${s.root_id}`).join('\\n'),false,{sessions:native.sessions});
281
705
  }
282
- async function listProcesses(){ return listSessions(); }
283
- async function killProcess(args){ return terminateProcess(args); }
706
+ async function listProcesses(){
707
+ if(!externalProcessRuntime) return listSessions();
708
+ const native=await externalProcessRuntime.list();
709
+ const lines=native.processes.map(p=>`PID: ${p.pid}, Command: ${p.name}, CPU: n/a, Memory: n/a`);
710
+ return text(lines.join('\n'),false,{...native});
711
+ }
712
+ async function killProcess(args){
713
+ if(pidMap.has(args.pid)) return terminateProcess(args);
714
+ if(!externalProcessRuntime) throw new Error(`unknown RDC pid ${args.pid}`);
715
+ const native=await externalProcessRuntime.terminate({pid:args.pid});
716
+ return text(`Successfully terminated process ${args.pid}`,false,native);
717
+ }
284
718
 
285
719
  async function writePdf(args){
286
- if(!extendedRuntime) throw new Error('write_pdf compatibility requires the DEADBYTE document runtime');
287
- if(Array.isArray(args.content)) throw new Error('write_pdf operation arrays are wire-accepted but semantic PDF page editing is not yet implemented; request rejected rather than approximated');
288
- const pdf=extendedRuntime.writePdf({text:args.content,metadata:{...args.options??{}}});
720
+ if(!machineFsRuntime) throw new Error('write_pdf compatibility requires the machine filesystem runtime');
721
+ async function readPdfBytes(compatPath,label){
722
+ const resolved=resolveRdcPath(machineFsRuntime,compatPath);
723
+ const maxRead=Math.min(64*1024*1024,machineFsRuntime.policy.max_read_bytes??64*1024*1024);
724
+ const raw=await machineFsRuntime.read({rootId:resolved.rootId,relativePath:resolved.relativePath,offset:0,maxBytes:maxRead,hashFile:true});
725
+ if(raw.eof===false) throw new Error(`${label} exceeds bounded native PDF read limit`);
726
+ return {resolved,raw,bytes:Buffer.from(raw.data_base64,'base64')};
727
+ }
728
+ let pdf;
729
+ let ignoredPdfOptions=[];
730
+ if(Array.isArray(args.content)){
731
+ ignoredPdfOptions=args.content.flatMap((op,index)=>
732
+ op?.type==='insert'&&op.pdfOptions!==undefined&&Object.keys(op.pdfOptions??{}).length>0?[index]:[]
733
+ );
734
+ const base=await readPdfBytes(args.path,'base PDF');
735
+ pdf=await editDeadbytePdf(base.bytes,args.content,{
736
+ sourceResolver:async sourcePdfPath=>(await readPdfBytes(sourcePdfPath,'source PDF')).bytes
737
+ });
738
+ }else{
739
+ if(!extendedRuntime) throw new Error('write_pdf string mode requires the DEADBYTE document runtime');
740
+ pdf=extendedRuntime.writePdf({text:args.content,metadata:{...args.options??{}}});
741
+ }
289
742
  const targetPath=args.outputPath??args.path;
290
743
  const target=resolveRdcPath(machineFsRuntime,targetPath);
291
744
  let expected='absent';
292
745
  try{
293
- const current=await machineFsRuntime.read({rootId:target.rootId,relativePath:target.relativePath,offset:0,maxBytes:128*1024*1024,hashFile:true});
294
- expected=current.file_sha256??'absent';
746
+ const current=await machineFsRuntime.read({
747
+ rootId:target.rootId,relativePath:target.relativePath,offset:0,
748
+ maxBytes:Math.min(64*1024*1024,machineFsRuntime.policy.max_read_bytes??64*1024*1024),
749
+ hashFile:true
750
+ });
751
+ if(current.eof===false) throw new Error('target PDF exceeds bounded native read limit');
752
+ expected=current.file_sha256??sha256Bytes(Buffer.from(current.data_base64,'base64'));
295
753
  }catch(error){
296
754
  if(!/not found|ENOENT/i.test(String(error))) throw error;
297
755
  }
298
- const result=await machineFsRuntime.write({rootId:target.rootId,relativePath:target.relativePath,content:pdf.bytes_base64,encoding:'base64',expectedSha256:expected});
299
- return text(`Successfully wrote PDF to ${targetPath}${args.outputPath?`\\nOriginal file: ${args.path}`:''}`,false,{
300
- path:args.path,outputPath:args.outputPath??null,status:'committed',mimeType:'application/pdf',bytes:pdf.bytes,bytesSha256:pdf.bytes_sha256,...result
756
+ const bytesBase64=pdf.bytes_base64??pdf.bytes?.toString('base64');
757
+ const bytesCount=pdf.bytes?.length??Buffer.from(bytesBase64,'base64').length;
758
+ const digest=pdf.bytes_sha256??sha256Bytes(Buffer.from(bytesBase64,'base64'));
759
+ const result=await machineFsRuntime.write({
760
+ rootId:target.rootId,relativePath:target.relativePath,
761
+ content:bytesBase64,encoding:'base64',expectedSha256:expected
762
+ });
763
+ return text(`Successfully wrote PDF to ${targetPath}${args.outputPath?`\nOriginal file: ${args.path}`:''}`,false,{
764
+ path:args.path,outputPath:args.outputPath??null,status:'committed',mimeType:'application/pdf',
765
+ bytes:bytesCount,bytesSha256:digest,pageCount:pdf.page_count??null,profile:pdf.profile??null,
766
+ ...(ignoredPdfOptions.length?{ignoredPdfOptions}:{}),...result
301
767
  });
302
768
  }
303
769
 
304
770
  async function startSearch(args){
305
771
  if(!searchRuntime) throw new Error('dedicated search runtime is disabled');
306
772
  const id=`search-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,8)}`;
307
- const state={id,created:Date.now(),args,isComplete:false,isError:false,results:[],error:null};
773
+ const exactFilename=args.searchType==='files'&&/\.[a-zA-Z0-9]+$/.test(args.pattern)&&!/[\*\?\[\]\{\}]/.test(args.pattern);
774
+ const timeoutMs=args.timeout_ms??(exactFilename?1500:null);
775
+ const state={id,created:Date.now(),args,isComplete:false,isError:false,results:[],error:null,timedOut:false,abortController:new AbortController()};
308
776
  searchSessions.set(id,state);
777
+ let timeoutHandle=null;
778
+ if(timeoutMs){
779
+ timeoutHandle=setTimeout(()=>{
780
+ if(state.isComplete) return;
781
+ state.timedOut=true;
782
+ state.abortController.abort();
783
+ state.isComplete=true;
784
+ },Math.max(0,timeoutMs));
785
+ }
309
786
  state.promise=(async()=>{
310
787
  try{
311
788
  const target=machineFsRuntime?resolveRdcPath(machineFsRuntime,args.path):{rootId:'core',relativePath:args.path};
312
- const query={rootId:target.rootId,path:target.relativePath,pattern:args.pattern,target:args.searchType==='content'?'content':'path',mode:args.literalSearch?'literal':'regex',caseSensitive:!args.ignoreCase,maxResults:Math.min(Math.max(Math.trunc(args.maxResults??500),1),5000),maxFileBytes:262144,maxScanBytes:64*1024*1024,pageSize:500,cursor:null,filePattern:args.filePattern,includeHidden:args.includeHidden,contextLines:args.contextLines,earlyTermination:args.earlyTermination};
313
- const r=await searchRuntime(query);
314
- state.results=(r.matches??[]).map(x=>({file:x.path,line:x.line,match:x.preview,type:args.searchType==='content'?'content':'file'}));
315
- state.isComplete=true;
316
- }catch(error){state.isError=true;state.error=String(error?.message??error);state.isComplete=true;}
789
+ const query={rootId:target.rootId,path:target.relativePath,pattern:args.pattern,target:args.searchType==='content'?'content':'path',mode:args.literalSearch?'literal':'regex',caseSensitive:!args.ignoreCase,maxResults:Math.min(Math.max(Math.trunc(args.maxResults??500),1),5000),maxFileBytes:262144,maxScanBytes:64*1024*1024,pageSize:500,cursor:null,filePattern:args.filePattern,includeHidden:args.includeHidden,contextLines:args.contextLines,earlyTermination:args.searchType==='files'?args.earlyTermination!==false:false};
790
+ const seenCursors=new Set();
791
+ if(args.searchType!=='content'){
792
+ query.mode='literal';
793
+ query.pathGlob=true;
794
+ }
795
+ do{
796
+ const r=await searchRuntime({...query,signal:state.abortController.signal});
797
+ if(state.cancelled||state.timedOut) break;
798
+ const remaining=query.maxResults-state.results.length;
799
+ state.results.push(...(r.matches??[]).slice(0,remaining).map(x=>({file:x.path,line:x.line,match:x.preview,type:args.searchType==='content'?'content':'file'})));
800
+ query.cursor=r.next_cursor??null;
801
+ if(query.cursor!==null){
802
+ if(seenCursors.has(query.cursor)) throw new Error('search runtime repeated a paging cursor');
803
+ seenCursors.add(query.cursor);
804
+ }
805
+ }while(query.cursor!==null&&state.results.length<query.maxResults);
806
+ if(!state.cancelled&&!state.timedOut) state.isComplete=true;
807
+ }catch(error){if(!state.cancelled&&!state.timedOut){state.isError=true;state.error=String(error?.message??error);}state.isComplete=true;}
808
+ finally{if(timeoutHandle!==null) clearTimeout(timeoutHandle);}
317
809
  })();
318
810
  return {sessionId:id,isComplete:false,isError:false,results:[],totalResults:0};
319
811
  }
@@ -328,8 +820,10 @@ export function createCommanderWireAdapter({machineFsRuntime=null,processRuntime
328
820
  }
329
821
  async function stopSearch(args){
330
822
  const s=searchSessions.get(args.sessionId);
331
- if(!s) return false;
823
+ if(!s||s.isComplete) return false;
332
824
  s.cancelled=true;
825
+ s.abortController.abort();
826
+ s.isComplete=true;
333
827
  return true;
334
828
  }
335
829
  async function listSearches(){