@remcp/runtime 0.2.123 → 0.2.125
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/CHANGELOG.md +12 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/documents.mjs +9 -6
- package/src/extended/catalog.mjs +19 -4
- package/src/extended/diagnostics.mjs +118 -6
- package/src/extended/documents.mjs +22 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.125 — 2026-10-01
|
|
4
|
+
|
|
5
|
+
- Windows `audio` now controls the default render endpoint through the native Core Audio `IAudioEndpointVolume` interface hosted by inbox PowerShell `Add-Type`.
|
|
6
|
+
- `status` returns the actual 0–100 master level and mute state; `set_volume` is absolute; `mute`/`unmute` use idempotent `SetMute(true/false)` instead of a shared toggle key.
|
|
7
|
+
- Windows release CI compiles the interop definition with PowerShell 5.1 on `windows-latest`. No third-party runtime dependency is added and the 83 tool schemas are unchanged.
|
|
8
|
+
|
|
9
|
+
## 0.2.124 — 2026-10-01
|
|
10
|
+
|
|
11
|
+
- P3 document capability discovery is cross-platform: Windows uses the native .NET OOXML archive backend, while macOS/Linux require both `zip` and `unzip` before advertising the XLSX/DOCX editor tools.
|
|
12
|
+
- `read_document` and `pdf_action` remain discoverable on supported desktop platforms even without an OOXML archive backend; DOCX/XLSX read attempts still fail closed at execution time when extraction is unavailable. `edit_spreadsheet` and `edit_document` explicitly require `ooxml`.
|
|
13
|
+
- Windows and macOS release CI run real candidate XLSX/DOCX create/read round trips before publication. The 83-tool names and schemas are unchanged.
|
|
14
|
+
|
|
3
15
|
## 0.2.123 — 2026-10-01
|
|
4
16
|
|
|
5
17
|
- The compact local MCP facade now tracks live capability changes after startup. It advertises `tools.listChanged`, refreshes grouped input/output schemas, and adds or removes domains such as `control_browser` when loopback CDP becomes available or unavailable.
|
package/README.md
CHANGED
|
@@ -67,7 +67,8 @@ For frontend QA, use semantic data to operate the page and rendered pixels to ve
|
|
|
67
67
|
`set_config_value` remains deliberately narrow: it can change only `telemetryEnabled`,
|
|
68
68
|
`maxReadLines`, `maxBufferedLines`, and `maxOutputBytes`. Access roots, blocked commands,
|
|
69
69
|
the command guardrail, shell, write limit, runtime name, and unrestricted mode stay with the person
|
|
70
|
-
at the computer. DOCX/XLSX edits operate directly on OOXML
|
|
70
|
+
at the computer. DOCX/XLSX edits operate directly on OOXML: Windows uses the built-in .NET ZIP backend,
|
|
71
|
+
while macOS/Linux advertise the OOXML editor tools only when both `zip` and `unzip` are available. `read_document` stays discoverable for PDF/plain-text formats when that backend is absent and fails closed only for DOCX/XLSX reads. Structural PDF writes use existing
|
|
71
72
|
system tools such as qpdf/poppler when present. PDF text reading uses the built-in parser first and can fall back to local `pdftotext` for embedded-font PDFs; ReMCP still does not bundle Chromium, Puppeteer,
|
|
72
73
|
`sharp`, or `exceljs`.
|
|
73
74
|
|
package/package.json
CHANGED
package/src/documents.mjs
CHANGED
|
@@ -40,7 +40,7 @@ function unzipEntry(buffer, wanted) {
|
|
|
40
40
|
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
41
41
|
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
42
42
|
const localOffset = buffer.readUInt32LE(offset + 42);
|
|
43
|
-
const name = buffer.toString('utf8', offset + 46, offset + 46 + nameLength);
|
|
43
|
+
const name = buffer.toString('utf8', offset + 46, offset + 46 + nameLength).replace(/\\/g, '/');
|
|
44
44
|
if (name === wanted) {
|
|
45
45
|
const localNameLength = buffer.readUInt16LE(localOffset + 26);
|
|
46
46
|
const localExtraLength = buffer.readUInt16LE(localOffset + 28);
|
|
@@ -116,16 +116,19 @@ function wordXmlText(xml) {
|
|
|
116
116
|
return output;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
export function
|
|
120
|
-
|
|
121
|
-
if (!document) throw new Error('This file is not a readable .docx (its word/document.xml is missing or compressed in an unsupported way)');
|
|
122
|
-
const xml = document.toString('utf8');
|
|
123
|
-
return wordXmlText(xml)
|
|
119
|
+
export function readDocxXmlText(xml) {
|
|
120
|
+
return wordXmlText(String(xml))
|
|
124
121
|
.replace(/&(amp|lt|gt|quot|apos);/g, match => DOCX_ENTITIES[match])
|
|
125
122
|
.replace(/\n{3,}/g, '\n\n')
|
|
126
123
|
.trim();
|
|
127
124
|
}
|
|
128
125
|
|
|
126
|
+
export function readDocxText(buffer) {
|
|
127
|
+
const document = unzipEntry(buffer, 'word/document.xml');
|
|
128
|
+
if (!document) throw new Error('This file is not a readable .docx (its word/document.xml is missing or compressed in an unsupported way)');
|
|
129
|
+
return readDocxXmlText(document.toString('utf8'));
|
|
130
|
+
}
|
|
131
|
+
|
|
129
132
|
function decodePdfString(raw) {
|
|
130
133
|
// Literal strings arrive with backslash escapes; hex strings are pairs of hex digits.
|
|
131
134
|
return raw
|
package/src/extended/catalog.mjs
CHANGED
|
@@ -569,8 +569,8 @@ export const extendedToolDefinitions = [
|
|
|
569
569
|
define('record_screen', 'Record screen', 'Record a short bounded desktop video to a permitted local file for debugging and return its path and size. Uses native/available recording helpers.', o({ duration_seconds:n('1..120 seconds.'), fps:n('1..60.'), destination:s('Permitted output path. If an extension is supplied, use .webm for native GNOME Wayland recording and .mp4 for the other recorder backends.') }), mutating, diagnosticHandlers.record_screen, ['record']),
|
|
570
570
|
|
|
571
571
|
define('read_document', 'Read document', 'Read PDF, DOCX, XLSX, TXT, Markdown, CSV, JSON, or XML. PDF/DOCX extraction is built in and XLSX is parsed from its OOXML worksheet cells.', o({ path:s('Document path.'), sheet:s('Worksheet name for XLSX.'), max_cells:n() }, ['path']), readOnly, documentHandlers.read_document, ['documents']),
|
|
572
|
-
define('edit_spreadsheet', 'Edit spreadsheet', 'Create a new XLSX workbook or edit up to 500 expanded XLSX cells directly in OOXML without launching Excel. For creation set create=true and use path as a new destination; existing files are never overwritten by create mode. Each edit may target one cell or a rectangular range, with a scalar fill, a values matrix, or a formula.', o({ path:s('Existing .xlsx path, or the new workbook destination when create=true.'), create:b('Create a new XLSX at path. Fails if path already exists; do not combine with output.'), output:s('Optional output .xlsx when editing an existing workbook; defaults to replacing input.'), sheet:s('Worksheet name. For create=true, defaults to Sheet1 and becomes the new workbook sheet name.'), edits:{ type:'array', maxItems:500, items:o({ cell:s('Single A1 cell reference.'), range:s('Rectangular range such as A1:C3.'), value:{ description:'Scalar value for one cell or to fill a range.' }, values:{ type:'array', items:{ type:'array', items:{} }, description:'2D matrix matching the range dimensions.' }, formula:s('Optional formula; for a range it is written to each expanded cell.') }) } }, ['path','edits']), mutating, documentHandlers.edit_spreadsheet, ['documents']),
|
|
573
|
-
define('edit_document', 'Edit DOCX document', 'Create a new DOCX document or edit DOCX paragraph structure directly in OOXML: replace text, append/prepend paragraphs, insert before/after matching paragraphs, or delete matching paragraphs. For creation set create=true and use path as a new destination; existing files are never overwritten by create mode.', o({ path:s('Existing .docx path, or the new document destination when create=true.'), create:b('Create a new DOCX at path. Fails if path already exists; do not combine with output.'), output:s('Optional output .docx path when editing an existing document.'), operations:{ type:'array', maxItems:100, items:o({ action:e(['replace','append_paragraph','prepend_paragraph','insert_paragraph_before','insert_paragraph_after','delete_paragraph']), search:s('Paragraph text substring for replace/insert/delete operations.'), replacement:s(), all:b('Apply to all matching paragraphs; default true.'), text:s('Paragraph text to append/prepend/insert.') }, ['action']) } }, ['path','operations']), mutating, documentHandlers.edit_document, ['documents']),
|
|
572
|
+
define('edit_spreadsheet', 'Edit spreadsheet', 'Create a new XLSX workbook or edit up to 500 expanded XLSX cells directly in OOXML without launching Excel. For creation set create=true and use path as a new destination; existing files are never overwritten by create mode. Each edit may target one cell or a rectangular range, with a scalar fill, a values matrix, or a formula.', o({ path:s('Existing .xlsx path, or the new workbook destination when create=true.'), create:b('Create a new XLSX at path. Fails if path already exists; do not combine with output.'), output:s('Optional output .xlsx when editing an existing workbook; defaults to replacing input.'), sheet:s('Worksheet name. For create=true, defaults to Sheet1 and becomes the new workbook sheet name.'), edits:{ type:'array', maxItems:500, items:o({ cell:s('Single A1 cell reference.'), range:s('Rectangular range such as A1:C3.'), value:{ description:'Scalar value for one cell or to fill a range.' }, values:{ type:'array', items:{ type:'array', items:{} }, description:'2D matrix matching the range dimensions.' }, formula:s('Optional formula; for a range it is written to each expanded cell.') }) } }, ['path','edits']), mutating, documentHandlers.edit_spreadsheet, ['documents','ooxml']),
|
|
573
|
+
define('edit_document', 'Edit DOCX document', 'Create a new DOCX document or edit DOCX paragraph structure directly in OOXML: replace text, append/prepend paragraphs, insert before/after matching paragraphs, or delete matching paragraphs. For creation set create=true and use path as a new destination; existing files are never overwritten by create mode.', o({ path:s('Existing .docx path, or the new document destination when create=true.'), create:b('Create a new DOCX at path. Fails if path already exists; do not combine with output.'), output:s('Optional output .docx path when editing an existing document.'), operations:{ type:'array', maxItems:100, items:o({ action:e(['replace','append_paragraph','prepend_paragraph','insert_paragraph_before','insert_paragraph_after','delete_paragraph']), search:s('Paragraph text substring for replace/insert/delete operations.'), replacement:s(), all:b('Apply to all matching paragraphs; default true.'), text:s('Paragraph text to append/prepend/insert.') }, ['action']) } }, ['path','operations']), mutating, documentHandlers.edit_document, ['documents','ooxml']),
|
|
574
574
|
define('pdf_action', 'PDF action', 'Inspect PDF annotations or metadata, merge PDFs, split a PDF into pages, or extract selected page ranges. Structural writes use qpdf/poppler/pdftk when installed.', o({ action:e(['merge','split','extract_pages','annotations','info']), path:s(), paths:{type:'array',items:{type:'string'}}, output:s(), output_dir:s(), pattern:s(), pages:s('Page list/ranges such as 1-3,5.') }, ['action']), mutating, documentHandlers.pdf_action, ['documents']),
|
|
575
575
|
];
|
|
576
576
|
|
|
@@ -598,6 +598,21 @@ async function linuxHasPyAtSpi() {
|
|
|
598
598
|
return pyAtSpiCache.value;
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
+
export function documentCapabilitySnapshot({
|
|
602
|
+
platform = process.platform,
|
|
603
|
+
commandExistsFn = commandExists,
|
|
604
|
+
} = {}) {
|
|
605
|
+
const supportedPlatform = ['win32','darwin','linux'].includes(platform);
|
|
606
|
+
const nativeOoxml = platform === 'win32';
|
|
607
|
+
const archiveOoxml = (platform === 'darwin' || platform === 'linux')
|
|
608
|
+
&& commandExistsFn('zip')
|
|
609
|
+
&& commandExistsFn('unzip');
|
|
610
|
+
return {
|
|
611
|
+
documents:supportedPlatform,
|
|
612
|
+
ooxml:supportedPlatform && (nativeOoxml || archiveOoxml),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
601
616
|
export async function capabilitySnapshot() {
|
|
602
617
|
const win = process.platform === 'win32';
|
|
603
618
|
const mac = process.platform === 'darwin';
|
|
@@ -608,6 +623,7 @@ export async function capabilitySnapshot() {
|
|
|
608
623
|
const portalInput = wayland && waylandPortalCandidate();
|
|
609
624
|
const x11Input = linux && !wayland && (commandExists('wdotool') || commandExists('xdotool'));
|
|
610
625
|
const browserCdp = browserRemoteEnabled() && await browserControlAvailable(undefined, 500);
|
|
626
|
+
const documentCapabilities = documentCapabilitySnapshot();
|
|
611
627
|
return {
|
|
612
628
|
windows: win || mac || commandExists('wdotool') || commandExists('wmctrl') || (wayland && pyAtSpi),
|
|
613
629
|
ui: win || mac || pyAtSpi,
|
|
@@ -620,7 +636,6 @@ export async function capabilitySnapshot() {
|
|
|
620
636
|
notifications: win || mac || commandExists('notify-send'),
|
|
621
637
|
browser_cdp: browserCdp,
|
|
622
638
|
browser_evaluate: browserCdp && (process.env.NODE_ENV !== 'production' || ['1', 'true', 'yes', 'on'].includes(String(process.env.REMCP_BROWSER_ALLOW_EVALUATE || '').toLowerCase())),
|
|
623
|
-
documents: linux,
|
|
624
639
|
services: win || mac || commandExists('systemctl'),
|
|
625
640
|
|
|
626
641
|
logs: win || mac || commandExists('journalctl'),
|
|
@@ -628,7 +643,7 @@ export async function capabilitySnapshot() {
|
|
|
628
643
|
audio: win || mac || commandExists('wpctl') || commandExists('pactl') || commandExists('amixer'),
|
|
629
644
|
power: win || mac || commandExists('systemctl') || commandExists('loginctl'),
|
|
630
645
|
record: recordScreenAvailable({ platform:process.platform, wayland, gnomeSupported:gnomeRecord }),
|
|
631
|
-
|
|
646
|
+
...documentCapabilities,
|
|
632
647
|
};
|
|
633
648
|
}
|
|
634
649
|
|
|
@@ -183,6 +183,117 @@ export async function environmentTool(args) {
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
const WINDOWS_AUDIO_CORE_CSHARP = String.raw`
|
|
187
|
+
using System;
|
|
188
|
+
using System.Runtime.InteropServices;
|
|
189
|
+
|
|
190
|
+
namespace ReMCP {
|
|
191
|
+
enum EDataFlow { eRender = 0, eCapture = 1, eAll = 2 }
|
|
192
|
+
enum ERole { eConsole = 0, eMultimedia = 1, eCommunications = 2 }
|
|
193
|
+
|
|
194
|
+
[ComImport]
|
|
195
|
+
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
|
196
|
+
class MMDeviceEnumeratorComObject { }
|
|
197
|
+
|
|
198
|
+
[ComImport]
|
|
199
|
+
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
|
|
200
|
+
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
201
|
+
interface IMMDeviceEnumerator {
|
|
202
|
+
[PreserveSig] int EnumAudioEndpoints(EDataFlow dataFlow, uint stateMask, out IntPtr devices);
|
|
203
|
+
[PreserveSig] int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice endpoint);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
[ComImport]
|
|
207
|
+
[Guid("D666063F-1587-4E43-81F1-B948E807363F")]
|
|
208
|
+
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
209
|
+
interface IMMDevice {
|
|
210
|
+
[PreserveSig] int Activate(ref Guid iid, uint clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object instance);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
[ComImport]
|
|
214
|
+
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A")]
|
|
215
|
+
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
216
|
+
interface IAudioEndpointVolume {
|
|
217
|
+
[PreserveSig] int RegisterControlChangeNotify(IntPtr notify);
|
|
218
|
+
[PreserveSig] int UnregisterControlChangeNotify(IntPtr notify);
|
|
219
|
+
[PreserveSig] int GetChannelCount(out uint count);
|
|
220
|
+
[PreserveSig] int SetMasterVolumeLevel(float levelDb, ref Guid eventContext);
|
|
221
|
+
[PreserveSig] int SetMasterVolumeLevelScalar(float level, ref Guid eventContext);
|
|
222
|
+
[PreserveSig] int GetMasterVolumeLevel(out float levelDb);
|
|
223
|
+
[PreserveSig] int GetMasterVolumeLevelScalar(out float level);
|
|
224
|
+
[PreserveSig] int SetChannelVolumeLevel(uint channel, float levelDb, ref Guid eventContext);
|
|
225
|
+
[PreserveSig] int SetChannelVolumeLevelScalar(uint channel, float level, ref Guid eventContext);
|
|
226
|
+
[PreserveSig] int GetChannelVolumeLevel(uint channel, out float levelDb);
|
|
227
|
+
[PreserveSig] int GetChannelVolumeLevelScalar(uint channel, out float level);
|
|
228
|
+
[PreserveSig] int SetMute([MarshalAs(UnmanagedType.Bool)] bool muted, ref Guid eventContext);
|
|
229
|
+
[PreserveSig] int GetMute([MarshalAs(UnmanagedType.Bool)] out bool muted);
|
|
230
|
+
[PreserveSig] int GetVolumeStepInfo(out uint step, out uint stepCount);
|
|
231
|
+
[PreserveSig] int VolumeStepUp(ref Guid eventContext);
|
|
232
|
+
[PreserveSig] int VolumeStepDown(ref Guid eventContext);
|
|
233
|
+
[PreserveSig] int QueryHardwareSupport(out uint mask);
|
|
234
|
+
[PreserveSig] int GetVolumeRange(out float minDb, out float maxDb, out float incrementDb);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
public static class WindowsAudio {
|
|
238
|
+
const uint CLSCTX_ALL = 23;
|
|
239
|
+
|
|
240
|
+
static IAudioEndpointVolume Endpoint() {
|
|
241
|
+
IMMDeviceEnumerator enumerator = (IMMDeviceEnumerator)(new MMDeviceEnumeratorComObject());
|
|
242
|
+
IMMDevice device;
|
|
243
|
+
Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out device));
|
|
244
|
+
Guid iid = typeof(IAudioEndpointVolume).GUID;
|
|
245
|
+
object endpoint;
|
|
246
|
+
Marshal.ThrowExceptionForHR(device.Activate(ref iid, CLSCTX_ALL, IntPtr.Zero, out endpoint));
|
|
247
|
+
return (IAudioEndpointVolume)endpoint;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
public static string GetStateJson() {
|
|
251
|
+
IAudioEndpointVolume endpoint = Endpoint();
|
|
252
|
+
float scalar;
|
|
253
|
+
bool muted;
|
|
254
|
+
Marshal.ThrowExceptionForHR(endpoint.GetMasterVolumeLevelScalar(out scalar));
|
|
255
|
+
Marshal.ThrowExceptionForHR(endpoint.GetMute(out muted));
|
|
256
|
+
int volume = (int)Math.Round(Math.Max(0.0f, Math.Min(1.0f, scalar)) * 100.0f, MidpointRounding.AwayFromZero);
|
|
257
|
+
return "{\"volume\":" + volume.ToString() + ",\"muted\":" + (muted ? "true" : "false") + "}";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public static void SetVolume(int volume) {
|
|
261
|
+
if (volume < 0 || volume > 100) throw new ArgumentOutOfRangeException("volume");
|
|
262
|
+
IAudioEndpointVolume endpoint = Endpoint();
|
|
263
|
+
Guid context = Guid.Empty;
|
|
264
|
+
Marshal.ThrowExceptionForHR(endpoint.SetMasterVolumeLevelScalar(volume / 100.0f, ref context));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public static void SetMuted(bool muted) {
|
|
268
|
+
IAudioEndpointVolume endpoint = Endpoint();
|
|
269
|
+
Guid context = Guid.Empty;
|
|
270
|
+
Marshal.ThrowExceptionForHR(endpoint.SetMute(muted, ref context));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
`;
|
|
275
|
+
|
|
276
|
+
export function windowsAudioPowerShell(action = 'status', volume = 50, { compileOnly = false } = {}) {
|
|
277
|
+
const selected = requireEnum(action, 'action', ['status','set_volume','mute','unmute']);
|
|
278
|
+
const normalizedVolume = clamp(volume, 50, 0, 100);
|
|
279
|
+
const mutation = selected === 'set_volume'
|
|
280
|
+
? `[ReMCP.WindowsAudio]::SetVolume(${normalizedVolume})`
|
|
281
|
+
: selected === 'mute'
|
|
282
|
+
? '[ReMCP.WindowsAudio]::SetMuted($true)'
|
|
283
|
+
: selected === 'unmute'
|
|
284
|
+
? '[ReMCP.WindowsAudio]::SetMuted($false)'
|
|
285
|
+
: '';
|
|
286
|
+
return [
|
|
287
|
+
"$ErrorActionPreference='Stop'",
|
|
288
|
+
"$source=@'",
|
|
289
|
+
WINDOWS_AUDIO_CORE_CSHARP,
|
|
290
|
+
"'@",
|
|
291
|
+
'Add-Type -TypeDefinition $source -Language CSharp -ErrorAction Stop',
|
|
292
|
+
compileOnly ? "Write-Output 'compiled'" : mutation,
|
|
293
|
+
compileOnly ? '' : '[Console]::Out.WriteLine([ReMCP.WindowsAudio]::GetStateJson())',
|
|
294
|
+
].filter(Boolean).join('\n');
|
|
295
|
+
}
|
|
296
|
+
|
|
186
297
|
export async function audioTool(args) {
|
|
187
298
|
const action = requireEnum(args.action || 'status', 'action', ['status','set_volume','mute','unmute']);
|
|
188
299
|
const volume = clamp(args.volume, 50, 0, 100);
|
|
@@ -196,12 +307,13 @@ export async function audioTool(args) {
|
|
|
196
307
|
}
|
|
197
308
|
if (process.platform === 'win32') {
|
|
198
309
|
if (!commandExists('powershell.exe') && !process.env.SystemRoot) unavailable('Windows audio control');
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
310
|
+
const { stdout } = await runPowerShell(windowsAudioPowerShell(action, volume), { label:'Windows CoreAudio control', timeout:30_000 });
|
|
311
|
+
const rendered = stdout.trim();
|
|
312
|
+
try {
|
|
313
|
+
return jsonResult(JSON.parse(rendered));
|
|
314
|
+
} catch {
|
|
315
|
+
throw new Error(`Windows CoreAudio returned an invalid state payload: ${rendered || '(empty)'}`);
|
|
316
|
+
}
|
|
205
317
|
}
|
|
206
318
|
if (commandExists('wpctl')) {
|
|
207
319
|
if (action === 'status') return text((await runFile('wpctl', ['get-volume','@DEFAULT_AUDIO_SINK@'], { label:'audio status' })).stdout.trim());
|
|
@@ -3,7 +3,7 @@ import process from 'node:process';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { constants } from 'node:fs';
|
|
5
5
|
import { copyFile, link, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, stat, unlink, writeFile } from 'node:fs/promises';
|
|
6
|
-
import {
|
|
6
|
+
import { readDocxXmlText, readPdfText } from '../documents.mjs';
|
|
7
7
|
import { openDirectoryPath } from '../tools/files.mjs';
|
|
8
8
|
import { resolveSafePath, text } from '../util.mjs';
|
|
9
9
|
import {
|
|
@@ -353,6 +353,25 @@ async function readXlsx(filePath, args) {
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
async function readDocxDocument(filePath) {
|
|
357
|
+
const dir = await tempDir('remcp-docx-read-');
|
|
358
|
+
try {
|
|
359
|
+
await extractZip(filePath, dir);
|
|
360
|
+
let xml;
|
|
361
|
+
try {
|
|
362
|
+
xml = await readFile(path.join(dir, 'word', 'document.xml'), 'utf8');
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (error?.code === 'ENOENT') {
|
|
365
|
+
throw new Error('This file is not a readable .docx (word/document.xml is missing)');
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
return readDocxXmlText(xml);
|
|
370
|
+
} finally {
|
|
371
|
+
await removeTemp(dir);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
356
375
|
async function readPdfDocument(data) {
|
|
357
376
|
try {
|
|
358
377
|
return readPdfText(data);
|
|
@@ -380,10 +399,10 @@ async function readPdfDocument(data) {
|
|
|
380
399
|
export async function readDocument(args) {
|
|
381
400
|
const filePath = await resolveSafePath(args.path,'path');
|
|
382
401
|
const lower = filePath.toLowerCase();
|
|
402
|
+
if (lower.endsWith('.docx')) return text(await readDocxDocument(filePath));
|
|
403
|
+
if (lower.endsWith('.xlsx')) return jsonResult(await readXlsx(filePath,args));
|
|
383
404
|
const data = await readDocumentFile(filePath);
|
|
384
|
-
if (lower.endsWith('.docx')) return text(readDocxText(data));
|
|
385
405
|
if (lower.endsWith('.pdf')) return text(await readPdfDocument(data));
|
|
386
|
-
if (lower.endsWith('.xlsx')) return jsonResult(await readXlsx(filePath,args));
|
|
387
406
|
if (/\.(txt|md|csv|json|xml|yaml|yml)$/i.test(lower)) return text(data.toString('utf8'));
|
|
388
407
|
throw new Error('read_document supports PDF, DOCX, XLSX, TXT, Markdown, CSV, JSON, XML, YAML');
|
|
389
408
|
}
|