@hunterzhu/pulse-adapters 0.6.0 → 0.7.0

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.
@@ -0,0 +1,2 @@
1
+ /** Parse untrusted Office input off the host event loop, with bounded lifetime. */
2
+ export declare function extractDocxText(data: Buffer, signal: AbortSignal, timeoutMs?: number): Promise<string>;
@@ -0,0 +1,45 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { createRequire } from 'node:module';
3
+ import { ToolError } from '@hunterzhu/pulse-tool-sdk';
4
+ import { validateOfficeArchive } from './tools.js';
5
+ /** Parse untrusted Office input off the host event loop, with bounded lifetime. */
6
+ export async function extractDocxText(data, signal, timeoutMs = 30_000) {
7
+ if (signal.aborted)
8
+ throw new ToolError('ABORTED', 'ABORTED');
9
+ if (data.length > 20 * 1024 * 1024)
10
+ throw new ToolError('DOCUMENT_TOO_LARGE', 'DOCUMENT_TOO_LARGE');
11
+ try {
12
+ validateOfficeArchive(data);
13
+ }
14
+ catch (error) {
15
+ const code = error instanceof ToolError ? error.code.replaceAll('XLSX', 'DOCX') : 'INVALID_DOCX';
16
+ throw new ToolError(code, code);
17
+ }
18
+ const modulePath = createRequire(import.meta.url).resolve('mammoth');
19
+ return new Promise((resolve, reject) => {
20
+ const worker = new Worker(`
21
+ const { parentPort, workerData } = require('node:worker_threads');
22
+ require(workerData.modulePath).extractRawText({ buffer: Buffer.from(workerData.data) })
23
+ .then(result => parentPort.postMessage({ text: result.value }))
24
+ .catch(() => parentPort.postMessage({ error: 'INVALID_DOCX' }));
25
+ `, { eval: true, workerData: { modulePath, data }, resourceLimits: { maxOldGenerationSizeMb: 128 } });
26
+ let finished = false;
27
+ const finish = (error, text) => {
28
+ if (finished)
29
+ return;
30
+ finished = true;
31
+ clearTimeout(timer);
32
+ signal.removeEventListener('abort', abort);
33
+ void worker.terminate().then(() => error ? reject(new ToolError(error, error)) : resolve(text ?? ''));
34
+ };
35
+ const abort = () => finish('ABORTED');
36
+ const timer = setTimeout(() => finish('DOCX_PARSE_TIMEOUT'), timeoutMs);
37
+ signal.addEventListener('abort', abort, { once: true });
38
+ if (signal.aborted)
39
+ abort();
40
+ worker.once('message', message => typeof message?.text === 'string' ? finish(undefined, message.text) : finish('INVALID_DOCX'));
41
+ worker.once('error', () => finish('INVALID_DOCX'));
42
+ worker.once('exit', () => { if (!finished)
43
+ finish('INVALID_DOCX'); });
44
+ });
45
+ }
@@ -2,16 +2,19 @@ import { z } from 'zod';
2
2
  import { type ToolDefinition } from '@hunterzhu/pulse-tool-sdk';
3
3
  declare const pdfInput: z.ZodObject<{
4
4
  path: z.ZodString;
5
+ offset: z.ZodOptional<z.ZodNumber>;
5
6
  startPage: z.ZodOptional<z.ZodNumber>;
6
7
  endPage: z.ZodOptional<z.ZodNumber>;
7
8
  maxChars: z.ZodDefault<z.ZodNumber>;
8
9
  }, "strict", z.ZodTypeAny, {
9
10
  path: string;
10
11
  maxChars: number;
12
+ offset?: number | undefined;
11
13
  startPage?: number | undefined;
12
14
  endPage?: number | undefined;
13
15
  }, {
14
16
  path: string;
17
+ offset?: number | undefined;
15
18
  startPage?: number | undefined;
16
19
  endPage?: number | undefined;
17
20
  maxChars?: number | undefined;
@@ -19,6 +22,7 @@ declare const pdfInput: z.ZodObject<{
19
22
  declare const xlsxInput: z.ZodObject<{
20
23
  path: z.ZodString;
21
24
  sheet: z.ZodOptional<z.ZodString>;
25
+ startRow: z.ZodOptional<z.ZodNumber>;
22
26
  maxRows: z.ZodDefault<z.ZodNumber>;
23
27
  maxColumns: z.ZodDefault<z.ZodNumber>;
24
28
  }, "strict", z.ZodTypeAny, {
@@ -26,9 +30,11 @@ declare const xlsxInput: z.ZodObject<{
26
30
  maxRows: number;
27
31
  maxColumns: number;
28
32
  sheet?: string | undefined;
33
+ startRow?: number | undefined;
29
34
  }, {
30
35
  path: string;
31
36
  sheet?: string | undefined;
37
+ startRow?: number | undefined;
32
38
  maxRows?: number | undefined;
33
39
  maxColumns?: number | undefined;
34
40
  }>;
@@ -37,6 +43,8 @@ type XlsxInput = z.infer<typeof xlsxInput>;
37
43
  export interface PdfReadOutput {
38
44
  path: string;
39
45
  totalPages: number;
46
+ nextPage?: number | null;
47
+ nextOffset?: number | null;
40
48
  firstPage: number;
41
49
  lastPage: number;
42
50
  title?: string;
@@ -63,5 +71,9 @@ export interface XlsxInspectOutput {
63
71
  }>;
64
72
  truncated: boolean;
65
73
  }
74
+ export declare function readPdf(root: string, input: PdfInput, signal: AbortSignal): Promise<PdfReadOutput>;
75
+ export declare function inspectXlsx(root: string, input: XlsxInput, signal: AbortSignal): Promise<XlsxInspectOutput>;
66
76
  export declare function createDocumentTools(workspaceRoot: string): [ToolDefinition<PdfInput, PdfReadOutput>, ToolDefinition<XlsxInput, XlsxInspectOutput>];
77
+ /** Apply the same bounded ZIP admission used by XLSX before parsing DOCX. */
78
+ export declare function validateOfficeArchive(data: Buffer): void;
67
79
  export {};
@@ -15,6 +15,7 @@ const MAX_XLSX_ENTRY_BYTES = 16 * 1024 * 1024;
15
15
  const MAX_XLSX_SHEETS = 100;
16
16
  const pdfInput = z.object({
17
17
  path: z.string().min(1).max(1_024),
18
+ offset: z.number().int().min(0).optional(),
18
19
  startPage: z.number().int().positive().optional(),
19
20
  endPage: z.number().int().positive().optional(),
20
21
  maxChars: z.number().int().positive().max(MAX_OUTPUT_CHARS).default(20_000),
@@ -22,6 +23,7 @@ const pdfInput = z.object({
22
23
  const xlsxInput = z.object({
23
24
  path: z.string().min(1).max(1_024),
24
25
  sheet: z.string().min(1).max(128).optional(),
26
+ startRow: z.number().int().positive().optional(),
25
27
  maxRows: z.number().int().positive().max(100).default(20),
26
28
  maxColumns: z.number().int().positive().max(50).default(20),
27
29
  }).strict();
@@ -72,7 +74,7 @@ async function readWorkspaceFile(rootInput, path, signal) {
72
74
  function pdfText(items) {
73
75
  return items.map((item) => item !== null && typeof item === 'object' && 'str' in item && typeof item.str === 'string' ? item.str : '').join(' ');
74
76
  }
75
- async function readPdf(root, input, signal) {
77
+ export async function readPdf(root, input, signal) {
76
78
  const { absolutePath, data } = await readWorkspaceFile(root, input.path, signal);
77
79
  let destroyDocument;
78
80
  try {
@@ -92,23 +94,31 @@ async function readPdf(root, input, signal) {
92
94
  const chunks = [];
93
95
  let used = 0;
94
96
  let truncated = false;
97
+ let nextPage = lastPage < pdf.numPages ? lastPage + 1 : null;
98
+ let nextOffset = nextPage === null ? null : 0;
95
99
  for (let pageNumber = firstPage; pageNumber <= lastPage; pageNumber++) {
96
100
  if (signal.aborted)
97
101
  throw docError('ABORTED');
98
102
  const page = await pdf.getPage(pageNumber);
99
103
  try {
100
104
  const content = await page.getTextContent();
101
- const text = pdfText(content.items);
105
+ const pageOffset = pageNumber === firstPage ? input.offset ?? 0 : 0;
106
+ const text = pdfText(content.items).slice(pageOffset);
102
107
  const remaining = input.maxChars - used;
103
108
  if (remaining <= 0) {
104
109
  truncated = true;
110
+ nextPage = pageNumber;
111
+ nextOffset = pageOffset;
105
112
  break;
106
113
  }
107
114
  const bounded = text.slice(0, remaining);
108
- if (bounded.length < text.length)
115
+ if (bounded.length < text.length) {
109
116
  truncated = true;
110
- chunks.push(`--- Page ${pageNumber} ---\n${bounded}`);
111
- used += bounded.length;
117
+ nextPage = pageNumber;
118
+ nextOffset = pageOffset + bounded.length;
119
+ }
120
+ chunks.push(input.offset === undefined ? `--- Page ${pageNumber} ---\n${bounded}` : bounded);
121
+ used += bounded.length + (input.offset === undefined ? 0 : 2);
112
122
  }
113
123
  finally {
114
124
  page.cleanup();
@@ -119,6 +129,7 @@ async function readPdf(root, input, signal) {
119
129
  return {
120
130
  path: input.path,
121
131
  totalPages: pdf.numPages,
132
+ ...(input.offset === undefined ? {} : { nextPage, nextOffset }),
122
133
  firstPage,
123
134
  lastPage: Math.min(lastPage, firstPage + chunks.length - 1),
124
135
  ...(typeof info.Title === 'string' && info.Title ? { title: info.Title.slice(0, 500) } : {}),
@@ -232,7 +243,7 @@ function cellValue(value, maxChars) {
232
243
  }
233
244
  return String(value).slice(0, maxChars);
234
245
  }
235
- async function inspectXlsx(root, input, signal) {
246
+ export async function inspectXlsx(root, input, signal) {
236
247
  const { data } = await readWorkspaceFile(root, input.path, signal);
237
248
  checkXlsxArchive(Buffer.from(data));
238
249
  const workbook = new ExcelJS.Workbook();
@@ -262,6 +273,8 @@ async function inspectXlsx(root, input, signal) {
262
273
  const previewLimit = Symbol('preview-limit');
263
274
  try {
264
275
  sheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
276
+ if (rowNumber < (input.startRow ?? 1))
277
+ return;
265
278
  if (preview.length >= input.maxRows || remaining <= 0) {
266
279
  truncated = true;
267
280
  throw previewLimit;
@@ -288,7 +301,7 @@ async function inspectXlsx(root, input, signal) {
288
301
  if (cause !== previewLimit)
289
302
  throw cause;
290
303
  }
291
- if (rowCount > input.maxRows)
304
+ if (sheet.rowCount >= (input.startRow ?? 1) + input.maxRows)
292
305
  truncated = true;
293
306
  }
294
307
  worksheets.push({ id: sheet.id, name: sheet.name.slice(0, 128), state: String(sheet.state), rowCount, columnCount, ...(preview === undefined ? {} : { preview }) });
@@ -318,7 +331,7 @@ function manifest(name, description, inputSchema, outputSchema) {
318
331
  return { name, version: '1.0.0', description, tags: ['documents', 'read'], inputSchema, outputSchema, concurrencyClass: 'tool', locks: [], supportsAbortSignal: true, sideEffectPolicy: 'read', retrySafety: 'read_only', defaultTimeoutMs: 60_000, maxResultSummaryBytes: 4_096 };
319
332
  }
320
333
  const pdfManifest = manifest('document.pdf.read', 'Extract bounded text and metadata from a PDF in the workspace.', { type: 'object', required: ['path'], properties: { path: { type: 'string' }, startPage: { type: 'integer', minimum: 1 }, endPage: { type: 'integer', minimum: 1 }, maxChars: { type: 'integer', minimum: 1, maximum: MAX_OUTPUT_CHARS, default: 20_000 } }, additionalProperties: false }, { type: 'object', required: ['path', 'totalPages', 'firstPage', 'lastPage', 'text', 'truncated'], properties: { path: { type: 'string' }, totalPages: { type: 'integer' }, firstPage: { type: 'integer' }, lastPage: { type: 'integer' }, title: { type: 'string' }, author: { type: 'string' }, text: { type: 'string' }, truncated: { type: 'boolean' } }, additionalProperties: false });
321
- const xlsxManifest = manifest('document.xlsx.inspect', 'List workbook metadata and worksheet dimensions; optionally return a bounded cell preview.', { type: 'object', required: ['path'], properties: { path: { type: 'string' }, sheet: { type: 'string' }, maxRows: { type: 'integer', minimum: 1, maximum: 100, default: 20 }, maxColumns: { type: 'integer', minimum: 1, maximum: 50, default: 20 } }, additionalProperties: false }, { type: 'object', required: ['path', 'sheetCount', 'worksheets', 'truncated'], properties: { path: { type: 'string' }, creator: { type: 'string' }, title: { type: 'string' }, subject: { type: 'string' }, sheetCount: { type: 'integer' }, worksheets: { type: 'array', items: { type: 'object', required: ['id', 'name', 'state', 'rowCount', 'columnCount'], properties: { id: { type: 'integer' }, name: { type: 'string' }, state: { type: 'string' }, rowCount: { type: 'integer' }, columnCount: { type: 'integer' }, preview: { type: 'array', items: { type: 'object', required: ['row', 'cells'], properties: { row: { type: 'integer' }, cells: { type: 'array', items: { anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }, { type: 'null' }] } } }, additionalProperties: false } } }, additionalProperties: false } }, truncated: { type: 'boolean' } }, additionalProperties: false });
334
+ const xlsxManifest = manifest('document.xlsx.inspect', 'List workbook metadata and worksheet dimensions; optionally return a bounded cell preview.', { type: 'object', required: ['path'], properties: { path: { type: 'string' }, sheet: { type: 'string' }, startRow: { type: 'integer', minimum: 1 }, maxRows: { type: 'integer', minimum: 1, maximum: 100, default: 20 }, maxColumns: { type: 'integer', minimum: 1, maximum: 50, default: 20 } }, additionalProperties: false }, { type: 'object', required: ['path', 'sheetCount', 'worksheets', 'truncated'], properties: { path: { type: 'string' }, creator: { type: 'string' }, title: { type: 'string' }, subject: { type: 'string' }, sheetCount: { type: 'integer' }, worksheets: { type: 'array', items: { type: 'object', required: ['id', 'name', 'state', 'rowCount', 'columnCount'], properties: { id: { type: 'integer' }, name: { type: 'string' }, state: { type: 'string' }, rowCount: { type: 'integer' }, columnCount: { type: 'integer' }, preview: { type: 'array', items: { type: 'object', required: ['row', 'cells'], properties: { row: { type: 'integer' }, cells: { type: 'array', items: { anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }, { type: 'null' }] } } }, additionalProperties: false } } }, additionalProperties: false } }, truncated: { type: 'boolean' } }, additionalProperties: false });
322
335
  export function createDocumentTools(workspaceRoot) {
323
336
  const root = resolve(workspaceRoot);
324
337
  return [
@@ -336,3 +349,5 @@ export function createDocumentTools(workspaceRoot) {
336
349
  },
337
350
  ];
338
351
  }
352
+ /** Apply the same bounded ZIP admission used by XLSX before parsing DOCX. */
353
+ export function validateOfficeArchive(data) { checkXlsxArchive(data); }
package/dist/index.d.ts CHANGED
@@ -12,3 +12,4 @@ export * from './documents/tools.js';
12
12
  export * from './tools/registry.js';
13
13
  export * from './workers/http.js';
14
14
  export * from './mcp/index.js';
15
+ export * from './documents/docx.js';
package/dist/index.js CHANGED
@@ -12,3 +12,4 @@ export * from './documents/tools.js';
12
12
  export * from './tools/registry.js';
13
13
  export * from './workers/http.js';
14
14
  export * from './mcp/index.js';
15
+ export * from './documents/docx.js';
Binary file
@@ -7,5 +7,5 @@
7
7
  "patchUrl": "https://github.com/anthropics/sandbox-runtime/pull/523",
8
8
  "patchSha256": "3cf1d16ef273c9d066ff0479b1cd28178a7453a41dbdf80cbc3239dd831ca067",
9
9
  "testAdaptation": "Parent SDDL round-trip permits only D:( to D:AI( canonicalization; all owner/group/ACE/protection comparisons remain exact.",
10
- "sha256": "17d3dd079dbfc8ec42d6e623f612d31d8ca3a2633627910d8bcc4ae5eecfec06"
10
+ "sha256": "c091963f50f93cd5b3b14bad6b6d7dcbbe00f0836dc822e75ea9c4b93fcaabc5"
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunterzhu/pulse-adapters",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "license": "PolyForm-Noncommercial-1.0.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,9 +18,10 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@anthropic-ai/sandbox-runtime": "0.0.77",
21
- "@hunterzhu/pulse-runtime": "0.6.0",
22
- "@hunterzhu/pulse-tool-sdk": "0.6.0",
21
+ "@hunterzhu/pulse-runtime": "0.7.0",
22
+ "@hunterzhu/pulse-tool-sdk": "0.7.0",
23
23
  "exceljs": "4.4.0",
24
+ "mammoth": "1.11.0",
24
25
  "pdfjs-dist": "4.10.38"
25
26
  }
26
27
  }