@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.
- package/CHANGELOG.md +87 -1
- package/NOTICE.md +16 -0
- package/dist-lib/lib/capabilities/completion.d.ts +1 -0
- package/dist-lib/lib/capabilities/completion.js +28 -8
- package/dist-lib/lib/capabilities/tool-turns.d.ts +3 -0
- package/dist-lib/lib/capabilities/tool-turns.js +7 -0
- package/dist-lib/lib/context-window.js +2 -2
- package/dist-lib/lib/docx/index.d.ts +1 -1
- package/dist-lib/lib/docx/index.js +16 -24
- package/dist-lib/lib/helpers/run-outcome.d.ts +4 -1
- package/dist-lib/lib/helpers/run-outcome.js +14 -8
- package/dist-lib/lib/helpers/tool-call-markup.js +4 -4
- package/dist-lib/lib/hooks/built-in/arg-repair.js +7 -0
- package/dist-lib/lib/office/engine/codes.d.ts +62 -0
- package/dist-lib/lib/office/engine/codes.js +61 -0
- package/dist-lib/lib/office/engine/docx/document.d.ts +216 -0
- package/dist-lib/lib/office/engine/docx/document.js +1152 -0
- package/dist-lib/lib/office/engine/docx/index.d.ts +7 -0
- package/dist-lib/lib/office/engine/docx/index.js +8 -0
- package/dist-lib/lib/office/engine/docx/styles.d.ts +31 -0
- package/dist-lib/lib/office/engine/docx/styles.js +173 -0
- package/dist-lib/lib/office/engine/docx/template.d.ts +18 -0
- package/dist-lib/lib/office/engine/docx/template.js +133 -0
- package/dist-lib/lib/office/engine/docx/text.d.ts +55 -0
- package/dist-lib/lib/office/engine/docx/text.js +538 -0
- package/dist-lib/lib/office/engine/drawingml/chart.d.ts +63 -0
- package/dist-lib/lib/office/engine/drawingml/chart.js +539 -0
- package/dist-lib/lib/office/engine/drawingml/text.d.ts +35 -0
- package/dist-lib/lib/office/engine/drawingml/text.js +352 -0
- package/dist-lib/lib/office/engine/errors.d.ts +6 -0
- package/dist-lib/lib/office/engine/errors.js +12 -0
- package/dist-lib/lib/office/engine/image.d.ts +8 -0
- package/dist-lib/lib/office/engine/image.js +48 -0
- package/dist-lib/lib/office/engine/index.d.ts +12 -0
- package/dist-lib/lib/office/engine/index.js +7 -0
- package/dist-lib/lib/office/engine/opc/content-types.d.ts +17 -0
- package/dist-lib/lib/office/engine/opc/content-types.js +74 -0
- package/dist-lib/lib/office/engine/opc/media.d.ts +13 -0
- package/dist-lib/lib/office/engine/opc/media.js +11 -0
- package/dist-lib/lib/office/engine/opc/package.d.ts +51 -0
- package/dist-lib/lib/office/engine/opc/package.js +324 -0
- package/dist-lib/lib/office/engine/opc/paths.d.ts +7 -0
- package/dist-lib/lib/office/engine/opc/paths.js +46 -0
- package/dist-lib/lib/office/engine/opc/properties.d.ts +7 -0
- package/dist-lib/lib/office/engine/opc/properties.js +75 -0
- package/dist-lib/lib/office/engine/opc/relationships.d.ts +23 -0
- package/dist-lib/lib/office/engine/opc/relationships.js +65 -0
- package/dist-lib/lib/office/engine/opc/zip.d.ts +15 -0
- package/dist-lib/lib/office/engine/opc/zip.js +264 -0
- package/dist-lib/lib/office/engine/pptx/fit.d.ts +12 -0
- package/dist-lib/lib/office/engine/pptx/fit.js +37 -0
- package/dist-lib/lib/office/engine/pptx/index.d.ts +8 -0
- package/dist-lib/lib/office/engine/pptx/index.js +8 -0
- package/dist-lib/lib/office/engine/pptx/inherit.d.ts +29 -0
- package/dist-lib/lib/office/engine/pptx/inherit.js +155 -0
- package/dist-lib/lib/office/engine/pptx/notes.d.ts +5 -0
- package/dist-lib/lib/office/engine/pptx/notes.js +85 -0
- package/dist-lib/lib/office/engine/pptx/presentation.d.ts +54 -0
- package/dist-lib/lib/office/engine/pptx/presentation.js +749 -0
- package/dist-lib/lib/office/engine/pptx/read.d.ts +9 -0
- package/dist-lib/lib/office/engine/pptx/read.js +66 -0
- package/dist-lib/lib/office/engine/pptx/shapes.d.ts +52 -0
- package/dist-lib/lib/office/engine/pptx/shapes.js +276 -0
- package/dist-lib/lib/office/engine/pptx/slides.d.ts +27 -0
- package/dist-lib/lib/office/engine/pptx/slides.js +123 -0
- package/dist-lib/lib/office/engine/pptx/template.d.ts +20 -0
- package/dist-lib/lib/office/engine/pptx/template.js +391 -0
- package/dist-lib/lib/office/engine/pptx/types.d.ts +120 -0
- package/dist-lib/lib/office/engine/pptx/types.js +1 -0
- package/dist-lib/lib/office/engine/units.d.ts +3 -0
- package/dist-lib/lib/office/engine/units.js +12 -0
- package/dist-lib/lib/office/engine/utf8.d.ts +2 -0
- package/dist-lib/lib/office/engine/utf8.js +103 -0
- package/dist-lib/lib/office/engine/xlsx/formula.d.ts +49 -0
- package/dist-lib/lib/office/engine/xlsx/formula.js +381 -0
- package/dist-lib/lib/office/engine/xlsx/index.d.ts +8 -0
- package/dist-lib/lib/office/engine/xlsx/index.js +8 -0
- package/dist-lib/lib/office/engine/xlsx/refs.d.ts +22 -0
- package/dist-lib/lib/office/engine/xlsx/refs.js +82 -0
- package/dist-lib/lib/office/engine/xlsx/reshape.d.ts +21 -0
- package/dist-lib/lib/office/engine/xlsx/reshape.js +424 -0
- package/dist-lib/lib/office/engine/xlsx/sheet.d.ts +108 -0
- package/dist-lib/lib/office/engine/xlsx/sheet.js +662 -0
- package/dist-lib/lib/office/engine/xlsx/strings.d.ts +15 -0
- package/dist-lib/lib/office/engine/xlsx/strings.js +93 -0
- package/dist-lib/lib/office/engine/xlsx/styles.d.ts +38 -0
- package/dist-lib/lib/office/engine/xlsx/styles.js +369 -0
- package/dist-lib/lib/office/engine/xlsx/template.d.ts +7 -0
- package/dist-lib/lib/office/engine/xlsx/template.js +54 -0
- package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +161 -0
- package/dist-lib/lib/office/engine/xlsx/workbook.js +850 -0
- package/dist-lib/lib/office/engine/xlsx/write.d.ts +2 -0
- package/dist-lib/lib/office/engine/xlsx/write.js +42 -0
- package/dist-lib/lib/office/engine/xml.d.ts +45 -0
- package/dist-lib/lib/office/engine/xml.js +258 -0
- package/dist-lib/lib/office/extract.d.ts +9 -0
- package/dist-lib/lib/office/extract.js +19 -0
- package/dist-lib/lib/office/index.d.ts +1 -1
- package/dist-lib/lib/office/index.js +74 -19
- package/dist-lib/lib/tools/exec/adapters/python/module-distributions.js +0 -3
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +7 -50
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +38 -695
- package/dist-lib/lib/tools/exec/adapters/python/run-session.js +1 -85
- package/dist-lib/lib/tools/exec/scope.d.ts +2 -0
- package/dist-lib/lib/tools/exec/scope.js +33 -2
- package/dist-lib/lib/tools/exec.js +9 -20
- package/dist-lib/lib/tools/office/ops.d.ts +13 -0
- package/dist-lib/lib/tools/office/ops.js +100 -0
- package/dist-lib/lib/tools/office.d.ts +12 -0
- package/dist-lib/lib/tools/office.js +486 -0
- package/dist-lib/lib/tools/pdf/build.d.ts +43 -0
- package/dist-lib/lib/tools/pdf/build.js +150 -0
- package/dist-lib/lib/tools/pdf/images.d.ts +7 -0
- package/dist-lib/lib/tools/pdf/images.js +82 -0
- package/dist-lib/lib/tools/pdf/request.d.ts +63 -0
- package/dist-lib/lib/tools/pdf/request.js +335 -0
- package/dist-lib/lib/tools/pdf.d.ts +6 -0
- package/dist-lib/lib/tools/pdf.js +302 -0
- package/dist-lib/lib/tools/skill.js +14 -0
- package/package.json +34 -6
- package/wheels/manifest.json +1 -160
- package/worker/harness-sidecar.mjs +5 -1
- package/dist-lib/lib/office/pptx.d.ts +0 -1
- package/dist-lib/lib/office/pptx.js +0 -26
- package/dist-lib/lib/office/python.d.ts +0 -8
- package/dist-lib/lib/office/python.js +0 -32
- package/dist-lib/lib/office/xlsx.d.ts +0 -1
- package/dist-lib/lib/office/xlsx.js +0 -26
- package/dist-lib/lib/tools/exec/adapters/python/create-effect.d.ts +0 -7
- package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +0 -318
- package/wheels/cffi-2.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/cryptography-47.0.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/defusedxml-0.7.1-py2.py3-none-any.whl +0 -0
- package/wheels/et_xmlfile-2.0.0-py3-none-any.whl +0 -0
- package/wheels/fonttools-4.62.1-py3-none-any.whl +0 -0
- package/wheels/fpdf2-2.8.8-py3-none-any.whl +0 -0
- package/wheels/lxml-6.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/openpyxl-3.1.5-py2.py3-none-any.whl +0 -0
- package/wheels/pillow-12.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/pycparser-3.0-py3-none-any.whl +0 -0
- package/wheels/pypdf-6.15.0-py3-none-any.whl +0 -0
- package/wheels/python_docx-1.2.0-py3-none-any.whl +0 -0
- package/wheels/python_pptx-1.0.2-py3-none-any.whl +0 -0
- package/wheels/six-1.17.0-py2.py3-none-any.whl +0 -0
- package/wheels/typing_extensions-4.15.0-py3-none-any.whl +0 -0
- package/wheels/xlsxwriter-3.2.9-py3-none-any.whl +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import b4a from 'b4a';
|
|
2
|
+
import { errorMessage, isRecord } from '../guards.js';
|
|
3
|
+
import { docx, pptx, units, xlsx } from '../office/engine/index.js';
|
|
4
|
+
import { stageInputs } from './exec/adapters/python/attachments.js';
|
|
5
|
+
import { checkOutputBytes, RUN_OUTPUT_BUDGET_BYTES } from './exec/adapters/python/output-policy.js';
|
|
6
|
+
import { DOCX_OPS, opArguments, opKeys, PPTX_OPS, withAliases, XLSX_OPS } from './office/ops.js';
|
|
7
|
+
const READ_CELL_LIMIT = 10_000;
|
|
8
|
+
const attachedThisTurn = new WeakMap();
|
|
9
|
+
const FIRST_SHEET = /^sheet ?1$/i;
|
|
10
|
+
const SHAPE_ROLES = new Set(['title', 'subtitle', 'body']);
|
|
11
|
+
const HOISTED = ['attachmentId', 'output'];
|
|
12
|
+
const WHERE_IDS_ARE = 'copy the id from the [Attached file "…" — attachmentId: …] line or an earlier tool result';
|
|
13
|
+
function isObject(value) {
|
|
14
|
+
return isRecord(value) && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
export function shapeFor(deck, op) {
|
|
17
|
+
const { shape, slide } = op;
|
|
18
|
+
if (typeof shape !== 'string' || SHAPE_ROLES.has(shape) || typeof slide !== 'number') {
|
|
19
|
+
return op;
|
|
20
|
+
}
|
|
21
|
+
if (/^\d+$/.test(shape)) {
|
|
22
|
+
return { ...op, shape: Number(shape) };
|
|
23
|
+
}
|
|
24
|
+
const wanted = shape.trim().toLowerCase();
|
|
25
|
+
const found = deck.read()[slide]?.shapes.find((entry) => entry.name.toLowerCase() === wanted);
|
|
26
|
+
return found && found.id !== null ? { ...op, shape: found.id } : op;
|
|
27
|
+
}
|
|
28
|
+
export function sheetFor(book, op, created) {
|
|
29
|
+
if (typeof op.sheet !== 'string')
|
|
30
|
+
return op;
|
|
31
|
+
const sheets = book.sheets();
|
|
32
|
+
if (sheets.some((sheet) => sheet.name === op.sheet)) {
|
|
33
|
+
return op;
|
|
34
|
+
}
|
|
35
|
+
const only = sheets.length === 1 ? sheets[0] : undefined;
|
|
36
|
+
if (!only) {
|
|
37
|
+
return op;
|
|
38
|
+
}
|
|
39
|
+
if (FIRST_SHEET.test(op.sheet)) {
|
|
40
|
+
return { ...op, sheet: only.name };
|
|
41
|
+
}
|
|
42
|
+
if (created && FIRST_SHEET.test(only.name) && book.read(only.index).rows.length === 0) {
|
|
43
|
+
book.renameSheet(only.index, op.sheet);
|
|
44
|
+
}
|
|
45
|
+
return op;
|
|
46
|
+
}
|
|
47
|
+
function readWorkbook(book) {
|
|
48
|
+
const sheets = book.sheets();
|
|
49
|
+
const read = [];
|
|
50
|
+
let budget = READ_CELL_LIMIT;
|
|
51
|
+
for (const sheet of sheets) {
|
|
52
|
+
if (sheet.kind !== 'worksheet')
|
|
53
|
+
continue;
|
|
54
|
+
if (budget <= 0) {
|
|
55
|
+
read.push({ name: sheet.name, range: sheet.range ?? null, truncated: true });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const contents = book.read(sheet.index, { limit: budget });
|
|
59
|
+
budget -= contents.rows.reduce((cells, row) => cells + row.length, 0);
|
|
60
|
+
read.push(contents);
|
|
61
|
+
}
|
|
62
|
+
return { sheets, read };
|
|
63
|
+
}
|
|
64
|
+
const PPTX_FORMAT = {
|
|
65
|
+
name: 'pptx',
|
|
66
|
+
noun: 'PowerPoint deck',
|
|
67
|
+
ops: PPTX_OPS,
|
|
68
|
+
createKeys: ['size', 'title', 'fonts', 'colors'],
|
|
69
|
+
open: (bytes) => pptx.open(bytes),
|
|
70
|
+
create: (options) => pptx.create(options),
|
|
71
|
+
read: (deck) => deck.read(),
|
|
72
|
+
count: (deck) => ({ slides: deck.slideCount }),
|
|
73
|
+
prepare: (deck, op) => shapeFor(deck, op)
|
|
74
|
+
};
|
|
75
|
+
const DOCX_FORMAT = {
|
|
76
|
+
name: 'docx',
|
|
77
|
+
noun: 'Word document',
|
|
78
|
+
ops: DOCX_OPS,
|
|
79
|
+
createKeys: ['title', 'page', 'size', 'fonts'],
|
|
80
|
+
open: (bytes) => docx.open(bytes),
|
|
81
|
+
create: (options) => docx.create(options),
|
|
82
|
+
read: (doc) => doc.read(),
|
|
83
|
+
count: (doc) => ({ blocks: doc.blockCount })
|
|
84
|
+
};
|
|
85
|
+
const XLSX_FORMAT = {
|
|
86
|
+
name: 'xlsx',
|
|
87
|
+
noun: 'Excel workbook',
|
|
88
|
+
ops: XLSX_OPS,
|
|
89
|
+
createKeys: ['sheets', 'title'],
|
|
90
|
+
open: (bytes) => xlsx.open(bytes),
|
|
91
|
+
create: (options) => xlsx.create(options),
|
|
92
|
+
read: readWorkbook,
|
|
93
|
+
count: (book) => ({ sheets: book.sheets().length }),
|
|
94
|
+
prepare: (book, op, created) => sheetFor(book, op, created),
|
|
95
|
+
beforeSave: (book) => book.hasMacros
|
|
96
|
+
? 'the workbook has macros, which cannot be saved as .xlsx — add a removeMacros op'
|
|
97
|
+
: null
|
|
98
|
+
};
|
|
99
|
+
export function normalizeArgs(args) {
|
|
100
|
+
if (!Array.isArray(args.ops))
|
|
101
|
+
return args;
|
|
102
|
+
const lifted = { ...args };
|
|
103
|
+
const ops = [];
|
|
104
|
+
for (const entry of args.ops) {
|
|
105
|
+
const op = typeof entry === 'string' ? { op: entry } : entry;
|
|
106
|
+
if (!isObject(op)) {
|
|
107
|
+
ops.push(op);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const rest = { ...op };
|
|
111
|
+
for (const key of HOISTED) {
|
|
112
|
+
if (key in rest && lifted[key] === undefined) {
|
|
113
|
+
lifted[key] = rest[key];
|
|
114
|
+
delete rest[key];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (rest.op === 'create' && lifted.create === undefined && lifted.attachmentId === undefined) {
|
|
118
|
+
const { op: _create, ...options } = rest;
|
|
119
|
+
lifted.create = options;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
ops.push(rest);
|
|
123
|
+
}
|
|
124
|
+
lifted.ops = ops;
|
|
125
|
+
return lifted;
|
|
126
|
+
}
|
|
127
|
+
function parseOp(format, op, i) {
|
|
128
|
+
if (!isObject(op) || typeof op.op !== 'string') {
|
|
129
|
+
return `ops[${i}] must be { op, ...args }`;
|
|
130
|
+
}
|
|
131
|
+
const spec = format.ops[op.op];
|
|
132
|
+
if (!spec) {
|
|
133
|
+
return `ops[${i}]: unknown op "${op.op}"; ops are ${Object.keys(format.ops).join(', ')}`;
|
|
134
|
+
}
|
|
135
|
+
const keys = opKeys(spec);
|
|
136
|
+
const unknown = Object.keys(op).filter((key) => key !== 'op' && !keys.includes(key));
|
|
137
|
+
if (!unknown.length) {
|
|
138
|
+
return { name: op.op, spec, args: withAliases(spec, op) };
|
|
139
|
+
}
|
|
140
|
+
if (op.op === 'sheets' && 'name' in op && format.ops.addSheet) {
|
|
141
|
+
return `ops[${i}] sheets lists the sheets and takes no arguments; to add a sheet use { op: "addSheet", name }`;
|
|
142
|
+
}
|
|
143
|
+
const takes = keys.length ? `it takes ${keys.join(', ')}` : 'it takes no arguments';
|
|
144
|
+
return `ops[${i}] ${op.op} does not take ${unknown.join(', ')}; ${takes}`;
|
|
145
|
+
}
|
|
146
|
+
function parseOps(format, ops) {
|
|
147
|
+
if (!Array.isArray(ops)) {
|
|
148
|
+
return 'ops must be an array of { op, ...args }';
|
|
149
|
+
}
|
|
150
|
+
const parsed = [];
|
|
151
|
+
for (const [i, op] of ops.entries()) {
|
|
152
|
+
const result = parseOp(format, op, i);
|
|
153
|
+
if (typeof result === 'string') {
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
parsed.push(result);
|
|
157
|
+
}
|
|
158
|
+
return parsed;
|
|
159
|
+
}
|
|
160
|
+
function argumentError(format, args) {
|
|
161
|
+
const { attachmentId, create, output } = args;
|
|
162
|
+
if (attachmentId !== undefined && (typeof attachmentId !== 'string' || !attachmentId)) {
|
|
163
|
+
return 'attachmentId must be a non-empty string';
|
|
164
|
+
}
|
|
165
|
+
if (create !== undefined && !isObject(create)) {
|
|
166
|
+
return 'create must be an object';
|
|
167
|
+
}
|
|
168
|
+
if (attachmentId !== undefined && create !== undefined) {
|
|
169
|
+
return `pass attachmentId to open a ${format.noun}, or create to start one — not both`;
|
|
170
|
+
}
|
|
171
|
+
const unknownCreate = Object.keys(create ?? {}).filter((key) => !format.createKeys.includes(key));
|
|
172
|
+
if (unknownCreate.length) {
|
|
173
|
+
return `create does not take ${unknownCreate.join(', ')}; it takes ${format.createKeys.join(', ')}`;
|
|
174
|
+
}
|
|
175
|
+
if (output !== undefined && (typeof output !== 'string' || !output)) {
|
|
176
|
+
return 'output must be a file name';
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function parseRequest(format, raw) {
|
|
181
|
+
const args = normalizeArgs(raw);
|
|
182
|
+
const error = argumentError(format, args);
|
|
183
|
+
if (error) {
|
|
184
|
+
return error;
|
|
185
|
+
}
|
|
186
|
+
const ops = parseOps(format, args.ops ?? []);
|
|
187
|
+
if (typeof ops === 'string') {
|
|
188
|
+
return ops;
|
|
189
|
+
}
|
|
190
|
+
const attachmentId = typeof args.attachmentId === 'string' ? args.attachmentId : undefined;
|
|
191
|
+
if (attachmentId === undefined && !ops.length) {
|
|
192
|
+
return `nothing to read: pass the attachmentId of a ${format.noun} (${WHERE_IDS_ARE}), or ops to build one`;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
attachmentId,
|
|
196
|
+
create: isObject(args.create) ? args.create : undefined,
|
|
197
|
+
ops,
|
|
198
|
+
output: typeof args.output === 'string' ? args.output : undefined
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function sourceDocument(format, request, ctx) {
|
|
202
|
+
if (request.attachmentId === undefined) {
|
|
203
|
+
return { document: format.create(request.create ?? {}), fileName: null };
|
|
204
|
+
}
|
|
205
|
+
const found = await ctx.fetchAttachment?.({
|
|
206
|
+
attachmentId: request.attachmentId,
|
|
207
|
+
signal: ctx.signal
|
|
208
|
+
});
|
|
209
|
+
if (!found || !('bytes' in found)) {
|
|
210
|
+
return {
|
|
211
|
+
error: `attachment ${request.attachmentId} not found in this chat; ${WHERE_IDS_ARE}, not a file name`
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return { document: format.open(found.bytes), fileName: found.fileName };
|
|
215
|
+
}
|
|
216
|
+
export function revisedName(format, fileName) {
|
|
217
|
+
const dot = fileName.lastIndexOf('.');
|
|
218
|
+
const stem = dot > 0 ? fileName.slice(0, dot) : fileName;
|
|
219
|
+
if (format === 'pptx') {
|
|
220
|
+
const version = /^(.*)-v(\d+)$/.exec(stem);
|
|
221
|
+
return version ? `${version[1]}-v${Number(version[2]) + 1}.pptx` : `${stem}-v2.pptx`;
|
|
222
|
+
}
|
|
223
|
+
const revised = /^(.*)_revised(?:-(\d+))?$/.exec(stem);
|
|
224
|
+
if (!revised) {
|
|
225
|
+
return `${stem}_revised.${format}`;
|
|
226
|
+
}
|
|
227
|
+
return `${revised[1]}_revised-${Number(revised[2] ?? 1) + 1}.${format}`;
|
|
228
|
+
}
|
|
229
|
+
function outputFor(format, output, input) {
|
|
230
|
+
if (!input)
|
|
231
|
+
return output;
|
|
232
|
+
if (output && output.toLowerCase() !== input.toLowerCase())
|
|
233
|
+
return output;
|
|
234
|
+
return revisedName(format, input);
|
|
235
|
+
}
|
|
236
|
+
async function imageId(ctx, id) {
|
|
237
|
+
const found = await ctx.fetchAttachment?.({ attachmentId: id, meta: true, signal: ctx.signal });
|
|
238
|
+
if (found) {
|
|
239
|
+
return id;
|
|
240
|
+
}
|
|
241
|
+
const latest = await ctx.fetchAttachment?.({ meta: true, signal: ctx.signal });
|
|
242
|
+
return latest && latest.fileName.toLowerCase() === id.toLowerCase() ? undefined : id;
|
|
243
|
+
}
|
|
244
|
+
async function imageInput(op, i, ctx) {
|
|
245
|
+
const image = op.args.image ?? {};
|
|
246
|
+
if (!isObject(image)) {
|
|
247
|
+
return { error: `ops[${i}]: image must be { attachmentId } or {}` };
|
|
248
|
+
}
|
|
249
|
+
const id = image.attachmentId;
|
|
250
|
+
if (id !== undefined && (typeof id !== 'string' || !id)) {
|
|
251
|
+
return { error: `ops[${i}]: image.attachmentId must be a non-empty string` };
|
|
252
|
+
}
|
|
253
|
+
return { attachmentId: id ? await imageId(ctx, id) : undefined, path: `image-${i}.png` };
|
|
254
|
+
}
|
|
255
|
+
async function stagedImages(request, ctx) {
|
|
256
|
+
const wanted = request.ops.flatMap((op, i) => (opKeys(op.spec).includes('image') ? [i] : []));
|
|
257
|
+
const images = new Map();
|
|
258
|
+
if (!wanted.length) {
|
|
259
|
+
return images;
|
|
260
|
+
}
|
|
261
|
+
if (!ctx.fetchAttachment) {
|
|
262
|
+
return { error: 'this device cannot read chat images' };
|
|
263
|
+
}
|
|
264
|
+
const inputs = [];
|
|
265
|
+
for (const i of wanted) {
|
|
266
|
+
const op = request.ops[i];
|
|
267
|
+
const input = op ? await imageInput(op, i, ctx) : { error: `ops[${i}] is missing` };
|
|
268
|
+
if ('error' in input) {
|
|
269
|
+
return input;
|
|
270
|
+
}
|
|
271
|
+
inputs.push(input);
|
|
272
|
+
}
|
|
273
|
+
const staged = await stageInputs(ctx.fetchAttachment, inputs, {
|
|
274
|
+
turnImages: ctx.turnImages,
|
|
275
|
+
signal: ctx.signal
|
|
276
|
+
});
|
|
277
|
+
if ('error' in staged) {
|
|
278
|
+
return staged;
|
|
279
|
+
}
|
|
280
|
+
for (const [n, file] of staged.files.entries()) {
|
|
281
|
+
const at = wanted[n];
|
|
282
|
+
if (at !== undefined) {
|
|
283
|
+
images.set(at, file.bytes);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return images;
|
|
287
|
+
}
|
|
288
|
+
function saveError({ format, document, output }, ctx) {
|
|
289
|
+
if (!output) {
|
|
290
|
+
return `the ops changed the ${format.noun}; name the file in output`;
|
|
291
|
+
}
|
|
292
|
+
if (!output.toLowerCase().endsWith(`.${format.name}`)) {
|
|
293
|
+
return `output must end in .${format.name}, got "${output}"`;
|
|
294
|
+
}
|
|
295
|
+
const refusal = format.beforeSave?.(document);
|
|
296
|
+
if (refusal) {
|
|
297
|
+
return refusal;
|
|
298
|
+
}
|
|
299
|
+
return ctx.attach ? null : `this device cannot attach a ${format.noun} to the chat`;
|
|
300
|
+
}
|
|
301
|
+
async function attachOutput(job, ctx) {
|
|
302
|
+
const refused = saveError(job, ctx);
|
|
303
|
+
if (refused || !job.output || !ctx.attach) {
|
|
304
|
+
return { error: refused ?? 'nothing to save' };
|
|
305
|
+
}
|
|
306
|
+
const { output, document } = job;
|
|
307
|
+
const delivered = attachedThisTurn.get(ctx.attach) ?? new Map();
|
|
308
|
+
const earlier = delivered.get(output.toLowerCase());
|
|
309
|
+
const bytes = document.save();
|
|
310
|
+
if (earlier && b4a.equals(earlier.bytes, bytes)) {
|
|
311
|
+
return { attachment: earlier.attachment, repeated: true };
|
|
312
|
+
}
|
|
313
|
+
if (earlier) {
|
|
314
|
+
return {
|
|
315
|
+
error: `${earlier.attachment.fileName} is already attached with different content; save this version under a new name in output`
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const verdict = checkOutputBytes(output, bytes, 0);
|
|
319
|
+
if ('error' in verdict) {
|
|
320
|
+
return verdict;
|
|
321
|
+
}
|
|
322
|
+
const budget = ctx.runBudget;
|
|
323
|
+
if (budget && budget.outputBytesUsed + bytes.byteLength > RUN_OUTPUT_BUDGET_BYTES) {
|
|
324
|
+
return { error: 'run output budget exceeded' };
|
|
325
|
+
}
|
|
326
|
+
const record = await ctx.attach({
|
|
327
|
+
fileName: output,
|
|
328
|
+
mimeType: verdict.mimeType,
|
|
329
|
+
bytes,
|
|
330
|
+
raw: true
|
|
331
|
+
});
|
|
332
|
+
if (budget) {
|
|
333
|
+
budget.outputBytesUsed += bytes.byteLength;
|
|
334
|
+
}
|
|
335
|
+
const attachment = {
|
|
336
|
+
attachmentId: record.id,
|
|
337
|
+
fileName: record.fileName,
|
|
338
|
+
byteLength: record.byteLength
|
|
339
|
+
};
|
|
340
|
+
delivered.set(output.toLowerCase(), { attachment, bytes });
|
|
341
|
+
attachedThisTurn.set(ctx.attach, delivered);
|
|
342
|
+
return { attachment, repeated: false };
|
|
343
|
+
}
|
|
344
|
+
function runOp(document, op, input) {
|
|
345
|
+
const method = Reflect.get(document, op.name);
|
|
346
|
+
if (typeof method !== 'function') {
|
|
347
|
+
throw new Error(`${op.name} is not available on this file`);
|
|
348
|
+
}
|
|
349
|
+
return Reflect.apply(method, document, opArguments(op.spec, input, units.inches));
|
|
350
|
+
}
|
|
351
|
+
function runOps({ format, document, request, images }) {
|
|
352
|
+
const results = [];
|
|
353
|
+
const created = request.attachmentId === undefined;
|
|
354
|
+
for (const [i, op] of request.ops.entries()) {
|
|
355
|
+
const placed = format.prepare?.(document, op.args, created) ?? op.args;
|
|
356
|
+
const input = images.has(i) ? { ...placed, image: images.get(i) } : placed;
|
|
357
|
+
try {
|
|
358
|
+
results.push(runOp(document, op, input));
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
return { error: `ops[${i}] ${op.name}: ${errorMessage(err)}` };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return results;
|
|
365
|
+
}
|
|
366
|
+
function countLine(counts) {
|
|
367
|
+
return Object.entries(counts)
|
|
368
|
+
.map(([noun, n]) => `${n} ${n === 1 ? noun.replace(/s$/, '') : noun}`)
|
|
369
|
+
.join(', ');
|
|
370
|
+
}
|
|
371
|
+
function describe(format) {
|
|
372
|
+
const queries = Object.keys(format.ops).filter((name) => format.ops[name]?.query);
|
|
373
|
+
const edits = Object.keys(format.ops).filter((name) => !format.ops[name]?.query);
|
|
374
|
+
const inches = Object.values(format.ops).some((spec) => spec.inches)
|
|
375
|
+
? ' Positions and sizes (x, y, width, height) are inches.'
|
|
376
|
+
: '';
|
|
377
|
+
return (`Read, edit or create a ${format.noun} (.${format.name}). With attachmentId alone it ` +
|
|
378
|
+
`returns the ${format.noun}'s contents. ops run office engine methods in order, each ` +
|
|
379
|
+
`{ op, ...named args }: ${queries.join(', ')} return data; ${edits.join(', ')} change the ` +
|
|
380
|
+
'file. When anything changed, the result is saved as a chat attachment named output and ' +
|
|
381
|
+
'shown to the user automatically. A failing op saves nothing and its error names what ' +
|
|
382
|
+
`exists, so fix the call from it.${inches}`);
|
|
383
|
+
}
|
|
384
|
+
function parameters(format) {
|
|
385
|
+
return {
|
|
386
|
+
type: 'object',
|
|
387
|
+
properties: {
|
|
388
|
+
attachmentId: {
|
|
389
|
+
type: 'string',
|
|
390
|
+
description: `The ${format.noun} in this chat; omit it to create a new one`
|
|
391
|
+
},
|
|
392
|
+
create: {
|
|
393
|
+
type: 'object',
|
|
394
|
+
description: `Options for a new ${format.noun}: ${format.createKeys.join(', ')}`
|
|
395
|
+
},
|
|
396
|
+
ops: {
|
|
397
|
+
type: 'array',
|
|
398
|
+
description: 'Operations to run in order, each { op, ...named args }',
|
|
399
|
+
items: {
|
|
400
|
+
type: 'object',
|
|
401
|
+
properties: { op: { type: 'string', enum: Object.keys(format.ops) } },
|
|
402
|
+
required: ['op']
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
output: {
|
|
406
|
+
type: 'string',
|
|
407
|
+
description: `File name for the saved ${format.noun}, ending in .${format.name}`
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
async function execute(format, args, ctx) {
|
|
413
|
+
const request = parseRequest(format, args);
|
|
414
|
+
if (typeof request === 'string') {
|
|
415
|
+
return { error: request };
|
|
416
|
+
}
|
|
417
|
+
if (request.attachmentId !== undefined && !ctx.fetchAttachment) {
|
|
418
|
+
return { error: 'this device cannot read chat attachments' };
|
|
419
|
+
}
|
|
420
|
+
const images = await stagedImages(request, ctx);
|
|
421
|
+
if ('error' in images) {
|
|
422
|
+
return images;
|
|
423
|
+
}
|
|
424
|
+
let source;
|
|
425
|
+
try {
|
|
426
|
+
source = await sourceDocument(format, request, ctx);
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
return { error: errorMessage(err) };
|
|
430
|
+
}
|
|
431
|
+
if ('error' in source) {
|
|
432
|
+
return source;
|
|
433
|
+
}
|
|
434
|
+
const { document, fileName } = source;
|
|
435
|
+
if (!request.ops.length) {
|
|
436
|
+
return { document: format.read(document) };
|
|
437
|
+
}
|
|
438
|
+
const results = runOps({ format, document, request, images });
|
|
439
|
+
if ('error' in results) {
|
|
440
|
+
return results;
|
|
441
|
+
}
|
|
442
|
+
if (!document.changed && request.attachmentId !== undefined) {
|
|
443
|
+
return { results };
|
|
444
|
+
}
|
|
445
|
+
const output = outputFor(format.name, request.output, fileName);
|
|
446
|
+
const saved = await attachOutput({ format, document, output }, ctx);
|
|
447
|
+
if ('error' in saved) {
|
|
448
|
+
return saved;
|
|
449
|
+
}
|
|
450
|
+
const { attachment, repeated } = saved;
|
|
451
|
+
const counts = format.count(document);
|
|
452
|
+
const renamed = output !== request.output
|
|
453
|
+
? `Saved as ${output}, so the attached ${fileName} keeps its own name. `
|
|
454
|
+
: '';
|
|
455
|
+
return {
|
|
456
|
+
results,
|
|
457
|
+
attachments: [attachment],
|
|
458
|
+
...counts,
|
|
459
|
+
reply: `${attachment.fileName} — ${countLine(counts)}`,
|
|
460
|
+
next: renamed +
|
|
461
|
+
`Done: ${attachment.fileName} ${repeated ? 'was already' : 'is'} attached to the chat and shown to the user. ` +
|
|
462
|
+
`Do not call ${format.name} again for it. Your answer is the reply value alone, word for word.`
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function createOfficeTool(format) {
|
|
466
|
+
return {
|
|
467
|
+
name: format.name,
|
|
468
|
+
toolset: null,
|
|
469
|
+
skillGated: true,
|
|
470
|
+
description: describe(format),
|
|
471
|
+
parameters: parameters(format),
|
|
472
|
+
effect: () => 'read',
|
|
473
|
+
validate(args) {
|
|
474
|
+
const request = parseRequest(format, args);
|
|
475
|
+
return typeof request === 'string' ? request : null;
|
|
476
|
+
},
|
|
477
|
+
execute: (args, ctx = {}) => execute(format, args, ctx)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
export function createOfficeTools() {
|
|
481
|
+
return [
|
|
482
|
+
createOfficeTool(PPTX_FORMAT),
|
|
483
|
+
createOfficeTool(DOCX_FORMAT),
|
|
484
|
+
createOfficeTool(XLSX_FORMAT)
|
|
485
|
+
];
|
|
486
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PdfRequest } from './request.ts';
|
|
2
|
+
export type Pdfium = typeof import('bare-pdfium');
|
|
3
|
+
export type Doc = ReturnType<Pdfium['open']>;
|
|
4
|
+
type Field = ReturnType<Doc['fields']>[number];
|
|
5
|
+
export interface Source {
|
|
6
|
+
readonly attachmentId: string;
|
|
7
|
+
readonly fileName: string;
|
|
8
|
+
readonly bytes: Uint8Array;
|
|
9
|
+
}
|
|
10
|
+
export interface Opened {
|
|
11
|
+
readonly source: Source;
|
|
12
|
+
readonly doc: Doc;
|
|
13
|
+
}
|
|
14
|
+
export interface Built {
|
|
15
|
+
readonly bytes: Uint8Array;
|
|
16
|
+
readonly pageCount: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ReadField {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly type: string;
|
|
21
|
+
readonly value: Field['value'];
|
|
22
|
+
readonly options: readonly string[] | undefined;
|
|
23
|
+
readonly readOnly: true | undefined;
|
|
24
|
+
}
|
|
25
|
+
export interface ReadDocument {
|
|
26
|
+
readonly attachmentId: string;
|
|
27
|
+
readonly fileName: string;
|
|
28
|
+
readonly pageCount: number;
|
|
29
|
+
readonly pages: readonly {
|
|
30
|
+
page: number;
|
|
31
|
+
text: string;
|
|
32
|
+
}[];
|
|
33
|
+
readonly fields: readonly ReadField[];
|
|
34
|
+
readonly note: string | undefined;
|
|
35
|
+
readonly truncated: true | undefined;
|
|
36
|
+
}
|
|
37
|
+
export interface ReadDocuments {
|
|
38
|
+
readonly documents: readonly ReadDocument[];
|
|
39
|
+
}
|
|
40
|
+
export declare function pagesLabel(count: number): string;
|
|
41
|
+
export declare function readDocs(opened: readonly Opened[]): ReadDocument | ReadDocuments;
|
|
42
|
+
export declare function build(pdfium: Pdfium, request: PdfRequest, opened: readonly Opened[]): Built | string;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import b4a from 'b4a';
|
|
2
|
+
const READ_TEXT_LIMIT = 30_000;
|
|
3
|
+
function plural(count, noun) {
|
|
4
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
5
|
+
}
|
|
6
|
+
export function pagesLabel(count) {
|
|
7
|
+
return plural(count, 'page');
|
|
8
|
+
}
|
|
9
|
+
function zeroBased(opened, pages) {
|
|
10
|
+
if (!pages) {
|
|
11
|
+
return Array.from({ length: opened.doc.pageCount() }, (_, page) => page);
|
|
12
|
+
}
|
|
13
|
+
const count = opened.doc.pageCount();
|
|
14
|
+
const outside = pages.filter((page) => page < 1 || page > count);
|
|
15
|
+
if (outside.length) {
|
|
16
|
+
return `${opened.source.fileName} has ${pagesLabel(count)}, numbered 1 to ${count}; there is no page ${outside.join(', ')}`;
|
|
17
|
+
}
|
|
18
|
+
return pages.map((page) => page - 1);
|
|
19
|
+
}
|
|
20
|
+
function describeField(field) {
|
|
21
|
+
return {
|
|
22
|
+
name: field.name,
|
|
23
|
+
type: field.type,
|
|
24
|
+
value: field.value,
|
|
25
|
+
options: field.options,
|
|
26
|
+
readOnly: field.readOnly || undefined
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function scannedNote(scanned) {
|
|
30
|
+
if (!scanned.length)
|
|
31
|
+
return undefined;
|
|
32
|
+
const one = scanned.length === 1;
|
|
33
|
+
return `${one ? 'page' : 'pages'} ${scanned.join(', ')} ${one ? 'has' : 'have'} no text layer: ${one ? 'it is a scanned image' : 'they are scanned images'}, and there is no OCR here`;
|
|
34
|
+
}
|
|
35
|
+
function readOne({ source, doc }, budget) {
|
|
36
|
+
const pages = [];
|
|
37
|
+
const scanned = [];
|
|
38
|
+
let truncated = false;
|
|
39
|
+
for (const { page, text } of doc.textPages()) {
|
|
40
|
+
const clean = text.replace(/\r\n?/g, '\n').trim();
|
|
41
|
+
if (!clean) {
|
|
42
|
+
scanned.push(page + 1);
|
|
43
|
+
}
|
|
44
|
+
const kept = clean.slice(0, Math.max(0, budget.chars));
|
|
45
|
+
budget.chars -= kept.length;
|
|
46
|
+
truncated ||= kept.length < clean.length;
|
|
47
|
+
if (budget.chars > 0 || kept) {
|
|
48
|
+
pages.push({ page: page + 1, text: kept });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
attachmentId: source.attachmentId,
|
|
53
|
+
fileName: source.fileName,
|
|
54
|
+
pageCount: doc.pageCount(),
|
|
55
|
+
pages,
|
|
56
|
+
fields: doc.fields().map(describeField),
|
|
57
|
+
note: scannedNote(scanned),
|
|
58
|
+
truncated: truncated || undefined
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function readDocs(opened) {
|
|
62
|
+
const budget = { chars: READ_TEXT_LIMIT };
|
|
63
|
+
const documents = opened.map((one) => readOne(one, budget));
|
|
64
|
+
const [only] = documents;
|
|
65
|
+
return documents.length === 1 && only ? only : { documents };
|
|
66
|
+
}
|
|
67
|
+
function fromDoc(doc, bytes) {
|
|
68
|
+
return { bytes, pageCount: doc.pageCount() };
|
|
69
|
+
}
|
|
70
|
+
function joinPages(pdfium, parts, pages) {
|
|
71
|
+
const out = pdfium.create();
|
|
72
|
+
try {
|
|
73
|
+
for (const part of parts) {
|
|
74
|
+
out.importPages(part.doc, { pages });
|
|
75
|
+
}
|
|
76
|
+
return fromDoc(out, out.save());
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
out.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function textFile({ source, doc }) {
|
|
83
|
+
const text = [...doc.textPages()]
|
|
84
|
+
.map((page) => page.text.replace(/\r\n?/g, '\n').trim())
|
|
85
|
+
.join('\n\n');
|
|
86
|
+
if (!text.trim()) {
|
|
87
|
+
return `${source.fileName} has no text layer: its pages are scanned images, and there is no OCR here`;
|
|
88
|
+
}
|
|
89
|
+
return { bytes: b4a.from(text + '\n', 'utf8'), pageCount: doc.pageCount() };
|
|
90
|
+
}
|
|
91
|
+
function create(pdfium, request) {
|
|
92
|
+
const encrypt = request.password ? { password: request.password } : undefined;
|
|
93
|
+
const bytes = pdfium.compose(request.markdown, {
|
|
94
|
+
pageSize: request.pageSize,
|
|
95
|
+
landscape: request.landscape,
|
|
96
|
+
pageNumbers: request.pageNumbers,
|
|
97
|
+
encrypt
|
|
98
|
+
});
|
|
99
|
+
return { bytes, pageCount: pdfium.pageCount(bytes, { password: request.password }) };
|
|
100
|
+
}
|
|
101
|
+
function editOne(opened, request) {
|
|
102
|
+
const { doc } = opened;
|
|
103
|
+
switch (request.action) {
|
|
104
|
+
case 'rotate':
|
|
105
|
+
case 'watermark': {
|
|
106
|
+
const pages = zeroBased(opened, request.pages);
|
|
107
|
+
if (typeof pages === 'string') {
|
|
108
|
+
return pages;
|
|
109
|
+
}
|
|
110
|
+
if (request.action === 'rotate') {
|
|
111
|
+
doc.rotate(request.degrees, { pages });
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
doc.watermark(request.text, { pages, opacity: request.opacity, position: request.position });
|
|
115
|
+
}
|
|
116
|
+
return fromDoc(doc, doc.save());
|
|
117
|
+
}
|
|
118
|
+
case 'fill':
|
|
119
|
+
doc.fillFields(request.values);
|
|
120
|
+
return fromDoc(doc, doc.save());
|
|
121
|
+
case 'encrypt': {
|
|
122
|
+
const password = request.password ?? '';
|
|
123
|
+
const { ownerPassword, permissions } = request;
|
|
124
|
+
return fromDoc(doc, doc.save({ encrypt: { password, ownerPassword, permissions } }));
|
|
125
|
+
}
|
|
126
|
+
case 'decrypt':
|
|
127
|
+
return fromDoc(doc, doc.save({ removeSecurity: true }));
|
|
128
|
+
case 'text':
|
|
129
|
+
return textFile(opened);
|
|
130
|
+
default:
|
|
131
|
+
return `${request.action} is not an edit`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function build(pdfium, request, opened) {
|
|
135
|
+
const [first] = opened;
|
|
136
|
+
if (request.action === 'create') {
|
|
137
|
+
return create(pdfium, request);
|
|
138
|
+
}
|
|
139
|
+
if (!first) {
|
|
140
|
+
return 'no PDF to work on';
|
|
141
|
+
}
|
|
142
|
+
if (request.action === 'merge') {
|
|
143
|
+
return joinPages(pdfium, opened);
|
|
144
|
+
}
|
|
145
|
+
if (request.action !== 'split') {
|
|
146
|
+
return editOne(first, request);
|
|
147
|
+
}
|
|
148
|
+
const pages = zeroBased(first, request.pages);
|
|
149
|
+
return typeof pages === 'string' ? pages : joinPages(pdfium, [first], pages);
|
|
150
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ToolExecuteContext } from '../../capabilities/completion.ts';
|
|
2
|
+
import { type CreateRequest } from './request.ts';
|
|
3
|
+
interface ToolError {
|
|
4
|
+
readonly error: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function placeImages(request: CreateRequest, ctx: ToolExecuteContext): Promise<CreateRequest | ToolError>;
|
|
7
|
+
export {};
|