@qvac/harness 0.7.3 → 0.8.5

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 (146) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/NOTICE.md +16 -0
  3. package/dist-lib/lib/capabilities/completion.d.ts +1 -0
  4. package/dist-lib/lib/capabilities/completion.js +28 -8
  5. package/dist-lib/lib/capabilities/tool-turns.d.ts +3 -0
  6. package/dist-lib/lib/capabilities/tool-turns.js +7 -0
  7. package/dist-lib/lib/context-window.js +2 -2
  8. package/dist-lib/lib/docx/index.d.ts +1 -1
  9. package/dist-lib/lib/docx/index.js +16 -24
  10. package/dist-lib/lib/helpers/run-outcome.d.ts +4 -1
  11. package/dist-lib/lib/helpers/run-outcome.js +14 -8
  12. package/dist-lib/lib/helpers/tool-call-markup.js +4 -4
  13. package/dist-lib/lib/hooks/built-in/arg-repair.js +7 -0
  14. package/dist-lib/lib/office/engine/codes.d.ts +62 -0
  15. package/dist-lib/lib/office/engine/codes.js +61 -0
  16. package/dist-lib/lib/office/engine/docx/document.d.ts +216 -0
  17. package/dist-lib/lib/office/engine/docx/document.js +1152 -0
  18. package/dist-lib/lib/office/engine/docx/index.d.ts +7 -0
  19. package/dist-lib/lib/office/engine/docx/index.js +8 -0
  20. package/dist-lib/lib/office/engine/docx/styles.d.ts +31 -0
  21. package/dist-lib/lib/office/engine/docx/styles.js +173 -0
  22. package/dist-lib/lib/office/engine/docx/template.d.ts +18 -0
  23. package/dist-lib/lib/office/engine/docx/template.js +133 -0
  24. package/dist-lib/lib/office/engine/docx/text.d.ts +55 -0
  25. package/dist-lib/lib/office/engine/docx/text.js +538 -0
  26. package/dist-lib/lib/office/engine/drawingml/chart.d.ts +63 -0
  27. package/dist-lib/lib/office/engine/drawingml/chart.js +539 -0
  28. package/dist-lib/lib/office/engine/drawingml/text.d.ts +35 -0
  29. package/dist-lib/lib/office/engine/drawingml/text.js +352 -0
  30. package/dist-lib/lib/office/engine/errors.d.ts +6 -0
  31. package/dist-lib/lib/office/engine/errors.js +12 -0
  32. package/dist-lib/lib/office/engine/image.d.ts +8 -0
  33. package/dist-lib/lib/office/engine/image.js +48 -0
  34. package/dist-lib/lib/office/engine/index.d.ts +12 -0
  35. package/dist-lib/lib/office/engine/index.js +7 -0
  36. package/dist-lib/lib/office/engine/opc/content-types.d.ts +17 -0
  37. package/dist-lib/lib/office/engine/opc/content-types.js +74 -0
  38. package/dist-lib/lib/office/engine/opc/media.d.ts +13 -0
  39. package/dist-lib/lib/office/engine/opc/media.js +11 -0
  40. package/dist-lib/lib/office/engine/opc/package.d.ts +51 -0
  41. package/dist-lib/lib/office/engine/opc/package.js +324 -0
  42. package/dist-lib/lib/office/engine/opc/paths.d.ts +7 -0
  43. package/dist-lib/lib/office/engine/opc/paths.js +46 -0
  44. package/dist-lib/lib/office/engine/opc/properties.d.ts +7 -0
  45. package/dist-lib/lib/office/engine/opc/properties.js +75 -0
  46. package/dist-lib/lib/office/engine/opc/relationships.d.ts +23 -0
  47. package/dist-lib/lib/office/engine/opc/relationships.js +65 -0
  48. package/dist-lib/lib/office/engine/opc/zip.d.ts +15 -0
  49. package/dist-lib/lib/office/engine/opc/zip.js +264 -0
  50. package/dist-lib/lib/office/engine/pptx/fit.d.ts +12 -0
  51. package/dist-lib/lib/office/engine/pptx/fit.js +37 -0
  52. package/dist-lib/lib/office/engine/pptx/index.d.ts +8 -0
  53. package/dist-lib/lib/office/engine/pptx/index.js +8 -0
  54. package/dist-lib/lib/office/engine/pptx/inherit.d.ts +29 -0
  55. package/dist-lib/lib/office/engine/pptx/inherit.js +155 -0
  56. package/dist-lib/lib/office/engine/pptx/notes.d.ts +5 -0
  57. package/dist-lib/lib/office/engine/pptx/notes.js +85 -0
  58. package/dist-lib/lib/office/engine/pptx/presentation.d.ts +54 -0
  59. package/dist-lib/lib/office/engine/pptx/presentation.js +749 -0
  60. package/dist-lib/lib/office/engine/pptx/read.d.ts +9 -0
  61. package/dist-lib/lib/office/engine/pptx/read.js +66 -0
  62. package/dist-lib/lib/office/engine/pptx/shapes.d.ts +52 -0
  63. package/dist-lib/lib/office/engine/pptx/shapes.js +276 -0
  64. package/dist-lib/lib/office/engine/pptx/slides.d.ts +27 -0
  65. package/dist-lib/lib/office/engine/pptx/slides.js +123 -0
  66. package/dist-lib/lib/office/engine/pptx/template.d.ts +20 -0
  67. package/dist-lib/lib/office/engine/pptx/template.js +391 -0
  68. package/dist-lib/lib/office/engine/pptx/types.d.ts +120 -0
  69. package/dist-lib/lib/office/engine/pptx/types.js +1 -0
  70. package/dist-lib/lib/office/engine/units.d.ts +3 -0
  71. package/dist-lib/lib/office/engine/units.js +12 -0
  72. package/dist-lib/lib/office/engine/utf8.d.ts +2 -0
  73. package/dist-lib/lib/office/engine/utf8.js +103 -0
  74. package/dist-lib/lib/office/engine/xlsx/formula.d.ts +49 -0
  75. package/dist-lib/lib/office/engine/xlsx/formula.js +381 -0
  76. package/dist-lib/lib/office/engine/xlsx/index.d.ts +8 -0
  77. package/dist-lib/lib/office/engine/xlsx/index.js +8 -0
  78. package/dist-lib/lib/office/engine/xlsx/refs.d.ts +22 -0
  79. package/dist-lib/lib/office/engine/xlsx/refs.js +82 -0
  80. package/dist-lib/lib/office/engine/xlsx/reshape.d.ts +21 -0
  81. package/dist-lib/lib/office/engine/xlsx/reshape.js +424 -0
  82. package/dist-lib/lib/office/engine/xlsx/sheet.d.ts +108 -0
  83. package/dist-lib/lib/office/engine/xlsx/sheet.js +662 -0
  84. package/dist-lib/lib/office/engine/xlsx/strings.d.ts +15 -0
  85. package/dist-lib/lib/office/engine/xlsx/strings.js +93 -0
  86. package/dist-lib/lib/office/engine/xlsx/styles.d.ts +38 -0
  87. package/dist-lib/lib/office/engine/xlsx/styles.js +369 -0
  88. package/dist-lib/lib/office/engine/xlsx/template.d.ts +7 -0
  89. package/dist-lib/lib/office/engine/xlsx/template.js +54 -0
  90. package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +161 -0
  91. package/dist-lib/lib/office/engine/xlsx/workbook.js +850 -0
  92. package/dist-lib/lib/office/engine/xlsx/write.d.ts +2 -0
  93. package/dist-lib/lib/office/engine/xlsx/write.js +42 -0
  94. package/dist-lib/lib/office/engine/xml.d.ts +45 -0
  95. package/dist-lib/lib/office/engine/xml.js +258 -0
  96. package/dist-lib/lib/office/extract.d.ts +9 -0
  97. package/dist-lib/lib/office/extract.js +19 -0
  98. package/dist-lib/lib/office/index.d.ts +1 -1
  99. package/dist-lib/lib/office/index.js +74 -19
  100. package/dist-lib/lib/tools/exec/adapters/python/module-distributions.js +0 -3
  101. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +7 -50
  102. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +38 -695
  103. package/dist-lib/lib/tools/exec/adapters/python/run-session.js +1 -85
  104. package/dist-lib/lib/tools/exec/scope.d.ts +2 -0
  105. package/dist-lib/lib/tools/exec/scope.js +33 -2
  106. package/dist-lib/lib/tools/exec.js +9 -20
  107. package/dist-lib/lib/tools/office/ops.d.ts +13 -0
  108. package/dist-lib/lib/tools/office/ops.js +100 -0
  109. package/dist-lib/lib/tools/office.d.ts +12 -0
  110. package/dist-lib/lib/tools/office.js +486 -0
  111. package/dist-lib/lib/tools/pdf/build.d.ts +43 -0
  112. package/dist-lib/lib/tools/pdf/build.js +150 -0
  113. package/dist-lib/lib/tools/pdf/images.d.ts +7 -0
  114. package/dist-lib/lib/tools/pdf/images.js +82 -0
  115. package/dist-lib/lib/tools/pdf/request.d.ts +63 -0
  116. package/dist-lib/lib/tools/pdf/request.js +335 -0
  117. package/dist-lib/lib/tools/pdf.d.ts +6 -0
  118. package/dist-lib/lib/tools/pdf.js +302 -0
  119. package/dist-lib/lib/tools/skill.js +14 -0
  120. package/package.json +34 -6
  121. package/wheels/manifest.json +1 -160
  122. package/worker/harness-sidecar.mjs +5 -1
  123. package/dist-lib/lib/office/pptx.d.ts +0 -1
  124. package/dist-lib/lib/office/pptx.js +0 -26
  125. package/dist-lib/lib/office/python.d.ts +0 -8
  126. package/dist-lib/lib/office/python.js +0 -32
  127. package/dist-lib/lib/office/xlsx.d.ts +0 -1
  128. package/dist-lib/lib/office/xlsx.js +0 -26
  129. package/dist-lib/lib/tools/exec/adapters/python/create-effect.d.ts +0 -7
  130. package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +0 -318
  131. package/wheels/cffi-2.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
  132. package/wheels/cryptography-47.0.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl +0 -0
  133. package/wheels/defusedxml-0.7.1-py2.py3-none-any.whl +0 -0
  134. package/wheels/et_xmlfile-2.0.0-py3-none-any.whl +0 -0
  135. package/wheels/fonttools-4.62.1-py3-none-any.whl +0 -0
  136. package/wheels/fpdf2-2.8.8-py3-none-any.whl +0 -0
  137. package/wheels/lxml-6.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
  138. package/wheels/openpyxl-3.1.5-py2.py3-none-any.whl +0 -0
  139. package/wheels/pillow-12.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
  140. package/wheels/pycparser-3.0-py3-none-any.whl +0 -0
  141. package/wheels/pypdf-6.15.0-py3-none-any.whl +0 -0
  142. package/wheels/python_docx-1.2.0-py3-none-any.whl +0 -0
  143. package/wheels/python_pptx-1.0.2-py3-none-any.whl +0 -0
  144. package/wheels/six-1.17.0-py2.py3-none-any.whl +0 -0
  145. package/wheels/typing_extensions-4.15.0-py3-none-any.whl +0 -0
  146. package/wheels/xlsxwriter-3.2.9-py3-none-any.whl +0 -0
@@ -0,0 +1,302 @@
1
+ import { errorCode, errorMessage } from '../guards.js';
2
+ import { checkOutputBytes, RUN_OUTPUT_BUDGET_BYTES } from './exec/adapters/python/output-policy.js';
3
+ import { build, pagesLabel, readDocs } from './pdf/build.js';
4
+ import { placeImages } from './pdf/images.js';
5
+ import { PAGE_SIZES, parseRequest, PDF_ACTIONS, WHERE_IDS_ARE } from './pdf/request.js';
6
+ export { parsePages, parseRequest } from './pdf/request.js';
7
+ const SUFFIXES = {
8
+ create: 'revised',
9
+ merge: 'merged',
10
+ split: 'pages',
11
+ rotate: 'rotated',
12
+ watermark: 'watermarked',
13
+ fill: 'filled',
14
+ encrypt: 'locked',
15
+ decrypt: 'unlocked',
16
+ text: 'text',
17
+ read: 'copy'
18
+ };
19
+ const attachedThisTurn = new WeakMap();
20
+ let loaded = null;
21
+ function loadPdfium() {
22
+ loaded ??= import('bare-pdfium').then((pdfium) => pdfium, () => null);
23
+ return loaded;
24
+ }
25
+ function stemOf(fileName) {
26
+ const base = fileName.replace(/^.*[\\/]/, '');
27
+ const dot = base.lastIndexOf('.');
28
+ return dot > 0 ? base.slice(0, dot) : base;
29
+ }
30
+ function slug(markdown) {
31
+ const heading = /^#{1,6}\s+(.+)$/m.exec(markdown)?.[1] ?? '';
32
+ const name = heading
33
+ .toLowerCase()
34
+ .replace(/[*_`[\]]/g, '')
35
+ .replace(/[^a-z0-9]+/g, '_')
36
+ .replace(/^_+|_+$/g, '')
37
+ .slice(0, 48);
38
+ return name || 'document';
39
+ }
40
+ function extensionOf(request) {
41
+ return request.action === 'text' ? 'txt' : 'pdf';
42
+ }
43
+ function defaultName(request, first) {
44
+ if (!first) {
45
+ return `${request.action === 'create' ? slug(request.markdown) : 'document'}.pdf`;
46
+ }
47
+ if (request.action === 'text') {
48
+ return `${stemOf(first.fileName)}.txt`;
49
+ }
50
+ return `${stemOf(first.fileName)}_${SUFFIXES[request.action]}.pdf`;
51
+ }
52
+ export function outputName(request, inputs) {
53
+ const extension = extensionOf(request);
54
+ const { output } = request;
55
+ let wanted = defaultName(request, inputs[0]);
56
+ if (output && /\.[a-z0-9]{2,4}$/i.test(output)) {
57
+ wanted = output.replace(/\.pdf$/i, `.${extension}`);
58
+ }
59
+ else if (output) {
60
+ wanted = `${output}.${extension}`;
61
+ }
62
+ const taken = inputs.some((input) => input.fileName.toLowerCase() === wanted.toLowerCase());
63
+ return taken ? `${stemOf(wanted)}_${SUFFIXES[request.action]}.${extension}` : wanted;
64
+ }
65
+ async function fetchSources(ids, ctx) {
66
+ if (!ctx.fetchAttachment) {
67
+ return { error: 'this device cannot read chat attachments' };
68
+ }
69
+ const sources = [];
70
+ for (const attachmentId of ids) {
71
+ const found = await ctx.fetchAttachment({ attachmentId, signal: ctx.signal });
72
+ if (!found || !('bytes' in found)) {
73
+ return { error: `attachment ${attachmentId} not found in this chat; ${WHERE_IDS_ARE}` };
74
+ }
75
+ sources.push({ attachmentId, fileName: found.fileName, bytes: found.bytes });
76
+ }
77
+ return sources;
78
+ }
79
+ function openError(err, source, password) {
80
+ const code = errorCode(err);
81
+ if (code === 'PASSWORD' && password) {
82
+ return `the password is wrong for ${source.fileName}; ask the user for the right one, never guess`;
83
+ }
84
+ if (code === 'PASSWORD') {
85
+ return `${source.fileName} is locked; ask the user for its password and pass it as password`;
86
+ }
87
+ if (code === 'FORMAT' || code === 'FILE') {
88
+ return `${source.fileName} is not a PDF that opens`;
89
+ }
90
+ return `${source.fileName}: ${errorMessage(err)}`;
91
+ }
92
+ function openAll(pdfium, sources, request) {
93
+ const password = request.action === 'encrypt' ? undefined : request.password;
94
+ const opened = [];
95
+ for (const source of sources) {
96
+ try {
97
+ opened.push({ source, doc: pdfium.open(source.bytes, { password }) });
98
+ }
99
+ catch (err) {
100
+ return { opened, error: openError(err, source, password) };
101
+ }
102
+ }
103
+ return { opened, error: null };
104
+ }
105
+ async function attachOutput(fileName, bytes, ctx) {
106
+ if (!ctx.attach) {
107
+ return { error: 'this device cannot attach a file to the chat' };
108
+ }
109
+ const delivered = attachedThisTurn.get(ctx.attach) ?? new Map();
110
+ const earlier = delivered.get(fileName.toLowerCase());
111
+ if (earlier) {
112
+ return { attachment: earlier, repeated: true };
113
+ }
114
+ const verdict = checkOutputBytes(fileName, bytes, 0);
115
+ if ('error' in verdict) {
116
+ return verdict;
117
+ }
118
+ const budget = ctx.runBudget;
119
+ if (budget && budget.outputBytesUsed + bytes.byteLength > RUN_OUTPUT_BUDGET_BYTES) {
120
+ return { error: 'run output budget exceeded' };
121
+ }
122
+ const record = await ctx.attach({ fileName, mimeType: verdict.mimeType, bytes, raw: true });
123
+ if (budget) {
124
+ budget.outputBytesUsed += bytes.byteLength;
125
+ }
126
+ const attachment = {
127
+ attachmentId: record.id,
128
+ fileName: record.fileName,
129
+ byteLength: record.byteLength
130
+ };
131
+ delivered.set(fileName.toLowerCase(), attachment);
132
+ attachedThisTurn.set(ctx.attach, delivered);
133
+ return { attachment, repeated: false };
134
+ }
135
+ const UNDRAWABLE = /^('[^']+' \(U\+[0-9A-F]+\)(?:, '[^']+' \(U\+[0-9A-F]+\))*) cannot be drawn by the built-in fonts/;
136
+ function buildError(err) {
137
+ const message = errorMessage(err);
138
+ const characters = UNDRAWABLE.exec(message)?.[1];
139
+ if (!characters) {
140
+ return message;
141
+ }
142
+ return (`${characters} cannot be drawn: a PDF made here takes Western European text only. ` +
143
+ 'Leave those characters out, or tell the user this text cannot go in a PDF');
144
+ }
145
+ function buildSafely({ pdfium, request, opened }) {
146
+ try {
147
+ return build(pdfium, request, opened);
148
+ }
149
+ catch (err) {
150
+ return buildError(err);
151
+ }
152
+ }
153
+ function reply(request, { output, built, delivered }) {
154
+ const { attachment, repeated } = delivered;
155
+ const summary = request.action === 'text'
156
+ ? `text of ${pagesLabel(built.pageCount)}`
157
+ : pagesLabel(built.pageCount);
158
+ const renamed = request.output && output !== request.output
159
+ ? `Saved as ${output}, so the attached file keeps its own name. `
160
+ : '';
161
+ return {
162
+ attachments: [attachment],
163
+ pageCount: built.pageCount,
164
+ reply: `${attachment.fileName} — ${summary}`,
165
+ next: renamed +
166
+ `Done: ${attachment.fileName} ${repeated ? 'was already' : 'is'} attached to the chat and shown to the user. ` +
167
+ 'Do not call pdf again for it. Your answer is the reply value alone, word for word.'
168
+ };
169
+ }
170
+ async function deliver(job, ctx) {
171
+ const { request, opened } = job;
172
+ const output = outputName(request, opened.map((one) => one.source));
173
+ const extension = `.${extensionOf(request)}`;
174
+ if (!output.toLowerCase().endsWith(extension)) {
175
+ return { error: `output must end in ${extension}, got "${output}"` };
176
+ }
177
+ const built = buildSafely(job);
178
+ if (typeof built === 'string') {
179
+ return { error: built };
180
+ }
181
+ const delivered = await attachOutput(output, built.bytes, ctx);
182
+ return 'error' in delivered ? delivered : reply(request, { output, built, delivered });
183
+ }
184
+ function isLocked(pdfium, source) {
185
+ try {
186
+ pdfium.open(source.bytes).close();
187
+ return false;
188
+ }
189
+ catch (err) {
190
+ return errorCode(err) === 'PASSWORD';
191
+ }
192
+ }
193
+ function lockOrUnlock(pdfium, request, sources) {
194
+ const [only] = sources;
195
+ const wantsFile = request.output?.toLowerCase().endsWith('.pdf') === true;
196
+ if (request.action !== 'read' || !request.password || !wantsFile || !only || sources.length > 1) {
197
+ return request;
198
+ }
199
+ if (isLocked(pdfium, only)) {
200
+ return { ...request, action: 'decrypt' };
201
+ }
202
+ return { ...request, action: 'encrypt', ownerPassword: undefined, permissions: undefined };
203
+ }
204
+ async function run(pdfium, asked, ctx) {
205
+ if (asked.action === 'create') {
206
+ const placed = await placeImages(asked, ctx);
207
+ return 'error' in placed ? placed : deliver({ pdfium, request: placed, opened: [] }, ctx);
208
+ }
209
+ const sources = asked.ids.length ? await fetchSources(asked.ids, ctx) : [];
210
+ if ('error' in sources) {
211
+ return sources;
212
+ }
213
+ const request = lockOrUnlock(pdfium, asked, sources);
214
+ const { opened, error } = openAll(pdfium, sources, request);
215
+ try {
216
+ if (error) {
217
+ return { error };
218
+ }
219
+ if (request.action === 'read') {
220
+ return readDocs(opened);
221
+ }
222
+ return await deliver({ pdfium, request, opened }, ctx);
223
+ }
224
+ finally {
225
+ for (const { doc } of opened) {
226
+ doc.close();
227
+ }
228
+ }
229
+ }
230
+ const DESCRIPTION = 'Create, read or change PDF files. With attachmentId and no action it returns the page ' +
231
+ 'count, each page’s text and the form fields (name, type, value, options). An action saves ' +
232
+ 'a new file named output and attaches it to the chat: create (markdown, where ' +
233
+ '![caption](attachmentId) on its own line places a chat image and the file name of the ' +
234
+ "user's upload places that upload; optional pageSize, " +
235
+ 'landscape, pageNumbers, password), merge (attachmentIds in order), split (pages), rotate ' +
236
+ '(degrees 90, 180 or 270, optional pages), watermark (text, optional opacity, position, ' +
237
+ 'pages), fill (values keyed by field name), encrypt (password, optional ownerPassword, ' +
238
+ 'permissions), decrypt (password), text (a .txt of the text). Pages count from 1. A locked ' +
239
+ 'PDF needs its password on every action. A failing call saves nothing and its error names ' +
240
+ 'what exists, so fix the call from it.';
241
+ const PARAMETERS = {
242
+ type: 'object',
243
+ properties: {
244
+ action: {
245
+ type: 'string',
246
+ enum: [...PDF_ACTIONS],
247
+ description: 'What to do; leave it out to read the PDF'
248
+ },
249
+ attachmentId: { type: 'string', description: 'The PDF in this chat' },
250
+ attachmentIds: {
251
+ type: 'array',
252
+ items: { type: 'string' },
253
+ description: 'merge: the PDFs to join, in order'
254
+ },
255
+ markdown: { type: 'string', description: 'create: the whole document as Markdown' },
256
+ pages: {
257
+ type: 'array',
258
+ items: { type: 'integer' },
259
+ description: 'Page numbers counted from 1'
260
+ },
261
+ degrees: { type: 'integer', description: 'rotate: 90, 180 or 270, clockwise' },
262
+ text: { type: 'string', description: 'watermark: the text to stamp' },
263
+ opacity: { type: 'number', description: 'watermark: 0 to 1' },
264
+ position: { type: 'string', enum: ['over', 'under'], description: 'watermark' },
265
+ values: { type: 'object', description: 'fill: { "field name": value }' },
266
+ password: {
267
+ type: 'string',
268
+ description: 'encrypt: the new password; otherwise the password of a locked PDF'
269
+ },
270
+ ownerPassword: { type: 'string', description: 'encrypt' },
271
+ permissions: { type: 'object', description: 'encrypt: { print, copy, modify, ... }' },
272
+ pageSize: { type: 'string', enum: [...PAGE_SIZES], description: 'create' },
273
+ landscape: { type: 'boolean', description: 'create' },
274
+ pageNumbers: { type: 'boolean', description: 'create' },
275
+ output: { type: 'string', description: 'File name for the saved file' }
276
+ }
277
+ };
278
+ export function createPdfTool(load = loadPdfium) {
279
+ return {
280
+ name: 'pdf',
281
+ toolset: null,
282
+ skillGated: true,
283
+ description: DESCRIPTION,
284
+ parameters: PARAMETERS,
285
+ effect: () => 'read',
286
+ validate(args) {
287
+ const request = parseRequest(args);
288
+ return typeof request === 'string' ? request : null;
289
+ },
290
+ async execute(args, ctx = {}) {
291
+ const request = parseRequest(args);
292
+ if (typeof request === 'string') {
293
+ return { error: request };
294
+ }
295
+ const pdfium = await load();
296
+ if (!pdfium) {
297
+ return { error: 'this device cannot open PDFs' };
298
+ }
299
+ return run(pdfium, request, ctx);
300
+ }
301
+ };
302
+ }
@@ -158,6 +158,14 @@ export function createSkillTool({ skillsDir, overlayDir = resolveSkillsOverlayDi
158
158
  content: notes.length ? [content.trim(), ...notes].join('\n\n') : content
159
159
  };
160
160
  };
161
+ const mainInstead = async (asked, available) => {
162
+ const main = await load(MAIN_FILE);
163
+ const others = available.length ? ` Its other files: ${available.join(', ')}.` : '';
164
+ return {
165
+ ...main,
166
+ content: `There is no file ${asked} in this skill; these are its instructions.${others}\n\n${main.content}`
167
+ };
168
+ };
161
169
  try {
162
170
  return await load(file);
163
171
  }
@@ -173,6 +181,12 @@ export function createSkillTool({ skillsDir, overlayDir = resolveSkillsOverlayDi
173
181
  }
174
182
  catch { }
175
183
  }
184
+ if (!ctx?.skillsLoadedThisTurn?.includes(name)) {
185
+ try {
186
+ return await mainInstead(file, available);
187
+ }
188
+ catch { }
189
+ }
176
190
  const rest = available.length
177
191
  ? ` — files this skill has: ${available.join(', ')}`
178
192
  : '. This skill has no other files; you already have its full instructions — act now.';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/harness",
3
- "version": "0.7.3",
3
+ "version": "0.8.5",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bare": ">=1.34.0"
@@ -210,10 +210,6 @@
210
210
  "types": "./dist-lib/lib/tools/exec/adapters/python/attachments.d.ts",
211
211
  "default": "./dist-lib/lib/tools/exec/adapters/python/attachments.js"
212
212
  },
213
- "./lib/tools/exec/adapters/python/create-effect.ts": {
214
- "types": "./dist-lib/lib/tools/exec/adapters/python/create-effect.d.ts",
215
- "default": "./dist-lib/lib/tools/exec/adapters/python/create-effect.js"
216
- },
217
213
  "./lib/tools/exec/adapters/python/deadline-state.ts": {
218
214
  "types": "./dist-lib/lib/tools/exec/adapters/python/deadline-state.d.ts",
219
215
  "default": "./dist-lib/lib/tools/exec/adapters/python/deadline-state.js"
@@ -454,6 +450,14 @@
454
450
  "types": "./dist-lib/lib/tools/model-args.d.ts",
455
451
  "default": "./dist-lib/lib/tools/model-args.js"
456
452
  },
453
+ "./lib/tools/office.ts": {
454
+ "types": "./dist-lib/lib/tools/office.d.ts",
455
+ "default": "./dist-lib/lib/tools/office.js"
456
+ },
457
+ "./lib/tools/office/ops.ts": {
458
+ "types": "./dist-lib/lib/tools/office/ops.d.ts",
459
+ "default": "./dist-lib/lib/tools/office/ops.js"
460
+ },
457
461
  "./lib/tools/operations/builders.ts": {
458
462
  "types": "./dist-lib/lib/tools/operations/builders.d.ts",
459
463
  "default": "./dist-lib/lib/tools/operations/builders.js"
@@ -478,6 +482,22 @@
478
482
  "types": "./dist-lib/lib/tools/outbound-guard.d.ts",
479
483
  "default": "./dist-lib/lib/tools/outbound-guard.js"
480
484
  },
485
+ "./lib/tools/pdf.ts": {
486
+ "types": "./dist-lib/lib/tools/pdf.d.ts",
487
+ "default": "./dist-lib/lib/tools/pdf.js"
488
+ },
489
+ "./lib/tools/pdf/build.ts": {
490
+ "types": "./dist-lib/lib/tools/pdf/build.d.ts",
491
+ "default": "./dist-lib/lib/tools/pdf/build.js"
492
+ },
493
+ "./lib/tools/pdf/images.ts": {
494
+ "types": "./dist-lib/lib/tools/pdf/images.d.ts",
495
+ "default": "./dist-lib/lib/tools/pdf/images.js"
496
+ },
497
+ "./lib/tools/pdf/request.ts": {
498
+ "types": "./dist-lib/lib/tools/pdf/request.d.ts",
499
+ "default": "./dist-lib/lib/tools/pdf/request.js"
500
+ },
481
501
  "./lib/tools/private-ip.ts": {
482
502
  "types": "./dist-lib/lib/tools/private-ip.d.ts",
483
503
  "default": "./dist-lib/lib/tools/private-ip.js"
@@ -716,13 +736,19 @@
716
736
  "types": "@qvac/sdk",
717
737
  "bare": "@qvac/inference",
718
738
  "default": "@qvac/sdk"
739
+ },
740
+ "#zlib": {
741
+ "types": "bare-zlib",
742
+ "bare": "bare-zlib",
743
+ "default": "zlib"
719
744
  }
720
745
  },
721
746
  "dependencies": {
722
747
  "@qvac/inference": "0.20.0",
723
748
  "@qvac/langdetect-text": "0.1.2",
724
749
  "@qvac/sdk": "0.20.0",
725
- "@qvac/skills": "0.1.9",
750
+ "@qvac/skills": "0.1.12",
751
+ "@xmldom/xmldom": "^0.9.12",
726
752
  "asenion": "^0.1.0",
727
753
  "b4a": "^1.9.0",
728
754
  "bare-abort-controller": "^1.1.2",
@@ -743,6 +769,7 @@
743
769
  "bare-net": "^2.3.2",
744
770
  "bare-os": "^3.9.1",
745
771
  "bare-path": "^3.0.1",
772
+ "bare-pdfium": "^1.6.0",
746
773
  "bare-process": "4.4.1",
747
774
  "bare-sidecar": "0.5.7",
748
775
  "bare-storage": "^1.1.0",
@@ -754,6 +781,7 @@
754
781
  "bare-tty": "^5.1.2",
755
782
  "bare-url": "^2.4.5",
756
783
  "bare-worker": "^4.5.0",
784
+ "bare-zlib": "^1.4.1",
757
785
  "compact-encoding": "^3.4.0",
758
786
  "hrpc": "^4.3.0",
759
787
  "hyperschema": "^1.23.0",
@@ -1,170 +1,11 @@
1
1
  {
2
2
  "pyodide": "314.0.2",
3
- "packages": {
4
- "fpdf2": {
5
- "version": "2.8.8",
6
- "imports": [
7
- "fpdf"
8
- ],
9
- "pypi": [
10
- "defusedxml-0.7.1-py2.py3-none-any.whl",
11
- "fpdf2-2.8.8-py3-none-any.whl"
12
- ],
13
- "pyodide": [
14
- "pillow",
15
- "fonttools"
16
- ]
17
- },
18
- "pypdf": {
19
- "version": "6.15.0",
20
- "imports": [
21
- "pypdf"
22
- ],
23
- "pypi": [
24
- "pypdf-6.15.0-py3-none-any.whl"
25
- ],
26
- "pyodide": []
27
- },
28
- "openpyxl": {
29
- "version": "3.1.5",
30
- "imports": [
31
- "openpyxl"
32
- ],
33
- "pypi": [
34
- "et_xmlfile-2.0.0-py3-none-any.whl",
35
- "openpyxl-3.1.5-py2.py3-none-any.whl"
36
- ],
37
- "pyodide": []
38
- },
39
- "python-docx": {
40
- "version": "1.2.0",
41
- "imports": [
42
- "docx"
43
- ],
44
- "pypi": [
45
- "python_docx-1.2.0-py3-none-any.whl"
46
- ],
47
- "pyodide": [
48
- "typing-extensions",
49
- "lxml"
50
- ]
51
- },
52
- "python-pptx": {
53
- "version": "1.0.2",
54
- "imports": [
55
- "pptx"
56
- ],
57
- "pypi": [
58
- "xlsxwriter-3.2.9-py3-none-any.whl",
59
- "python_pptx-1.0.2-py3-none-any.whl"
60
- ],
61
- "pyodide": [
62
- "typing-extensions",
63
- "pillow",
64
- "lxml"
65
- ]
66
- },
67
- "cryptography": {
68
- "pypi": [],
69
- "pyodide": [
70
- "six",
71
- "pycparser",
72
- "cffi",
73
- "cryptography"
74
- ]
75
- },
76
- "pillow": {
77
- "pypi": [],
78
- "pyodide": [
79
- "pillow"
80
- ]
81
- }
82
- },
3
+ "packages": {},
83
4
  "files": {
84
5
  "micropip-0.11.1-py3-none-any.whl": {
85
6
  "url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/micropip-0.11.1-py3-none-any.whl",
86
7
  "sha256": "668cd1270f480ac9f93da4a2b4873a252743e4ad3da723d37a7e4c637c4f2314",
87
8
  "channel": "pyodide"
88
- },
89
- "defusedxml-0.7.1-py2.py3-none-any.whl": {
90
- "url": "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl",
91
- "sha256": "a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61",
92
- "channel": "pypi"
93
- },
94
- "fpdf2-2.8.8-py3-none-any.whl": {
95
- "url": "https://files.pythonhosted.org/packages/f5/be/af012eda9507494f28b99b077423806c43a11573eb6225dd46f19ae2d263/fpdf2-2.8.8-py3-none-any.whl",
96
- "sha256": "3557a478fc577a929c94aace9666aed4dcc432b5ab6764232e6a59f1ccd75f17",
97
- "channel": "pypi"
98
- },
99
- "pillow-12.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl": {
100
- "url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/pillow-12.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl",
101
- "sha256": "a50d83a8a76d646768e99e93ed5bf365d0b83563fd8b597e77562e75c8f575a9",
102
- "channel": "pyodide"
103
- },
104
- "fonttools-4.62.1-py3-none-any.whl": {
105
- "url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/fonttools-4.62.1-py3-none-any.whl",
106
- "sha256": "6d8caa09c191f26c47403499d6cff62f3fb9685b978baa047bf791862bcb298a",
107
- "channel": "pyodide"
108
- },
109
- "pypdf-6.15.0-py3-none-any.whl": {
110
- "url": "https://files.pythonhosted.org/packages/af/72/ce3067ac31e214a66388159f8462ddb8c13dd00170f24d555a1f1ae8ee91/pypdf-6.15.0-py3-none-any.whl",
111
- "sha256": "14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee",
112
- "channel": "pypi"
113
- },
114
- "et_xmlfile-2.0.0-py3-none-any.whl": {
115
- "url": "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl",
116
- "sha256": "7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa",
117
- "channel": "pypi"
118
- },
119
- "openpyxl-3.1.5-py2.py3-none-any.whl": {
120
- "url": "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl",
121
- "sha256": "5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2",
122
- "channel": "pypi"
123
- },
124
- "python_docx-1.2.0-py3-none-any.whl": {
125
- "url": "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl",
126
- "sha256": "3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7",
127
- "channel": "pypi"
128
- },
129
- "typing_extensions-4.15.0-py3-none-any.whl": {
130
- "url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/typing_extensions-4.15.0-py3-none-any.whl",
131
- "sha256": "213fc049fb8bbea5f8369b06c7270cdddd634c2c000f721ce445e22473d66fd5",
132
- "channel": "pyodide"
133
- },
134
- "lxml-6.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl": {
135
- "url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/lxml-6.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl",
136
- "sha256": "f37f82e09f6d7957aa167440db7ef683439cc3ce1befac1d2ca27e35bd7c95d4",
137
- "channel": "pyodide"
138
- },
139
- "xlsxwriter-3.2.9-py3-none-any.whl": {
140
- "url": "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl",
141
- "sha256": "9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3",
142
- "channel": "pypi"
143
- },
144
- "python_pptx-1.0.2-py3-none-any.whl": {
145
- "url": "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl",
146
- "sha256": "160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba",
147
- "channel": "pypi"
148
- },
149
- "six-1.17.0-py2.py3-none-any.whl": {
150
- "url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/six-1.17.0-py2.py3-none-any.whl",
151
- "sha256": "c6681057bd87fdec57e811b8e4007bad38deea284052817fa400d542c6423e2d",
152
- "channel": "pyodide"
153
- },
154
- "pycparser-3.0-py3-none-any.whl": {
155
- "url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/pycparser-3.0-py3-none-any.whl",
156
- "sha256": "fab73d094251fa60eef1b744db416f4f1fae95b18741b207870b65aa2f3479ed",
157
- "channel": "pyodide"
158
- },
159
- "cffi-2.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl": {
160
- "url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/cffi-2.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl",
161
- "sha256": "757845812fc8d2c94b0f0fb664d542e7d6e098263fbb5cda3b89dca5d7eff6d3",
162
- "channel": "pyodide"
163
- },
164
- "cryptography-47.0.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl": {
165
- "url": "https://cdn.jsdelivr.net/pyodide/v314.0.2/full/cryptography-47.0.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl",
166
- "sha256": "4e0e7e8e47613dd96110c640b15ee525db72a47946b8b06f8bcd0377c403fc5a",
167
- "channel": "pyodide"
168
9
  }
169
10
  }
170
11
  }
@@ -20,7 +20,9 @@ import { deniedRootsFor } from '../dist-lib/lib/tools/fs/scope.js'
20
20
  import { createGrepTool } from '../dist-lib/lib/tools/grep.js'
21
21
  import { createHttpRequestTool } from '../dist-lib/lib/tools/http-request.js'
22
22
  import { createMcpCallTool } from '../dist-lib/lib/tools/mcp-call.js'
23
+ import { createOfficeTools } from '../dist-lib/lib/tools/office.js'
23
24
  import { compileOperationTools } from '../dist-lib/lib/tools/operations/compile.js'
25
+ import { createPdfTool } from '../dist-lib/lib/tools/pdf.js'
24
26
  import { createReadFilesTool } from '../dist-lib/lib/tools/read-files.js'
25
27
  import { createSkillTool } from '../dist-lib/lib/tools/skill.js'
26
28
  import { ensureBundledSkillsDir } from '../dist-lib/lib/tools/skills/bundled-dir.js'
@@ -132,7 +134,9 @@ export default async function start(ipc, ready) {
132
134
  createFileGlobTool({ deniedRoots }),
133
135
  createGrepTool({ deniedRoots }),
134
136
  createCreateDocumentsTool({ deniedRoots }),
135
- createEditDocumentsTool({ deniedRoots })
137
+ createEditDocumentsTool({ deniedRoots }),
138
+ createPdfTool(),
139
+ ...createOfficeTools()
136
140
  ]
137
141
  if (skillCatalog) {
138
142
  tools.push(...compileOperationTools(skillCatalog, { httpRequestTool, mcpCallTool }))
@@ -1 +0,0 @@
1
- export declare const PPTX_EXTRACT: string;
@@ -1,26 +0,0 @@
1
- export const PPTX_EXTRACT = `from pptx import Presentation
2
- from pptx.enum.shapes import MSO_SHAPE_TYPE
3
- from pathlib import Path
4
-
5
- def text(shapes):
6
- for shape in shapes:
7
- if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
8
- yield from text(shape.shapes)
9
- elif shape.has_text_frame:
10
- if shape.text.strip():
11
- yield shape.text
12
- elif shape.has_table:
13
- for row in shape.table.rows:
14
- cells = ["" if cell.is_spanned else cell.text.replace("\\n", " ") for cell in row.cells]
15
- if any(cell.strip() for cell in cells):
16
- yield " | ".join(cells)
17
-
18
- with Path("extracted.txt").open("w", encoding="utf-8") as output:
19
- for number, slide in enumerate(Presentation("input.pptx").slides, 1):
20
- heading = False
21
- for block in text(slide.shapes):
22
- if not heading:
23
- output.write("Slide " + str(number) + "\\n")
24
- heading = True
25
- output.write(block + "\\n")
26
- `;
@@ -1,8 +0,0 @@
1
- import type { AbortSignal } from 'bare-abort-controller';
2
- export interface PythonExtractOptions {
3
- readonly format: string;
4
- readonly packages: readonly string[];
5
- readonly source: string;
6
- readonly signal?: AbortSignal | undefined;
7
- }
8
- export declare function extractWithPython(bytes: Uint8Array, fileName: string, { format, packages, source, signal }: PythonExtractOptions): Promise<string>;