@superdoc/sdk 2.25.0-next.1 → 2.25.0-next.10
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/dist/agent/action-change-mode.cjs +6 -0
- package/dist/agent/action-change-mode.d.ts +1 -0
- package/dist/agent/action-change-mode.js +5 -0
- package/dist/agent/action-descriptors.json +2 -2
- package/dist/agent/actions.cjs +886 -530
- package/dist/agent/actions.d.ts +2 -0
- package/dist/agent/actions.js +887 -531
- package/dist/agent/catalog.cjs +5 -0
- package/dist/agent/catalog.js +5 -0
- package/dist/agent/doc-snapshot.cjs +10 -6
- package/dist/agent/doc-snapshot.d.ts +4 -1
- package/dist/agent/doc-snapshot.js +10 -6
- package/dist/agent/ir.d.ts +3 -0
- package/dist/embedded-prompts.generated.cjs +2 -1
- package/dist/embedded-prompts.generated.js +2 -1
- package/dist/embedded-tools.generated.cjs +5 -5
- package/dist/embedded-tools.generated.js +5 -5
- package/dist/generated/client.d.ts +534 -0
- package/dist/generated/contract.cjs +1 -1
- package/dist/generated/contract.js +1 -1
- package/dist/presets/core.cjs +2 -1
- package/dist/presets/core.d.ts +1 -0
- package/dist/presets/core.js +2 -2
- package/dist/presets/full.cjs +1804 -0
- package/dist/presets/full.d.ts +39 -0
- package/dist/presets/full.js +1795 -0
- package/dist/presets.cjs +2 -0
- package/dist/presets.js +2 -0
- package/dist/prompts/full-addendum.md +40 -0
- package/dist/prompts/system-prompt.md +3 -2
- package/dist/runtime/document-rpc.cjs +2 -8
- package/dist/runtime/document-rpc.js +2 -8
- package/dist/runtime/sdk-version.generated.cjs +1 -1
- package/dist/runtime/sdk-version.generated.d.ts +1 -1
- package/dist/runtime/sdk-version.generated.js +1 -1
- package/dist/runtime/tracked-support.cjs +11 -0
- package/dist/runtime/tracked-support.d.ts +6 -0
- package/dist/runtime/tracked-support.js +7 -0
- package/package.json +6 -6
- package/tools/catalog.json +9 -0
- package/tools/generation-receipt.json +20 -19
- package/tools/tools-policy.json +1 -1
- package/tools/tools.anthropic.json +9 -0
- package/tools/tools.generic.json +9 -0
- package/tools/tools.openai.json +9 -0
- package/tools/tools.vercel.json +9 -0
|
@@ -0,0 +1,1795 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { CONTRACT } from '../generated/contract.js';
|
|
6
|
+
import { isRecord } from '../guards.js';
|
|
7
|
+
import { SuperDocCliError } from '../runtime/errors.js';
|
|
8
|
+
import { operationAcceptsTrackedMode } from '../runtime/tracked-support.js';
|
|
9
|
+
import { ACTION_ARG_SCHEMA, AGENT_TOOL_DEFINITIONS, ALLOW_ALL_DIRECT_ACTIONS, listAgentTools, } from '../agent/catalog.js';
|
|
10
|
+
import { getOperationCatalogEntry } from '../agent/operation-catalog.js';
|
|
11
|
+
import { corePreset, readPromptFile, stripCorruptedToolArgKeys } from './core.js';
|
|
12
|
+
export const HIDDEN_PARAMS = new Set(['doc', 'sessionId', 'out', 'expectedRevision', 'force']);
|
|
13
|
+
const RESERVED_ARG_KEYS = new Set(['doc', 'sessionId', 'out', 'expectedRevision']);
|
|
14
|
+
const RESULT_CHAR_CAP = 8_000;
|
|
15
|
+
const SCHEMA_CHAR_CAP = 1_800;
|
|
16
|
+
const NOT_CATALOGUED = new Set(['doc.save', 'doc.close', 'doc.executeCode']);
|
|
17
|
+
const NOT_CATALOGUED_DOMAINS = new Set(['preset', 'session']);
|
|
18
|
+
const OPAQUE_RESULT_OPS = new Set([
|
|
19
|
+
'doc.diff.capture',
|
|
20
|
+
'doc.diff.compare',
|
|
21
|
+
'doc.markdownToFragment',
|
|
22
|
+
'doc.htmlToFragment',
|
|
23
|
+
'doc.getMarkdown',
|
|
24
|
+
'doc.getHtml',
|
|
25
|
+
'doc.extract',
|
|
26
|
+
'doc.clipboard.parse',
|
|
27
|
+
'doc.mutations.preview',
|
|
28
|
+
]);
|
|
29
|
+
function domainOf(operationId) {
|
|
30
|
+
const parts = operationId.split('.');
|
|
31
|
+
return parts.length >= 3 ? parts[1] : 'document';
|
|
32
|
+
}
|
|
33
|
+
function isDocumentOp(op) {
|
|
34
|
+
return (op.sdkSurface === 'document' &&
|
|
35
|
+
!NOT_CATALOGUED.has(op.operationId) &&
|
|
36
|
+
!NOT_CATALOGUED_DOMAINS.has(domainOf(op.operationId)));
|
|
37
|
+
}
|
|
38
|
+
function isCallableOp(op) {
|
|
39
|
+
return isDocumentOp(op) && op.skipAsATool !== true;
|
|
40
|
+
}
|
|
41
|
+
function contractOps() {
|
|
42
|
+
return Object.values(CONTRACT.operations).filter(isCallableOp);
|
|
43
|
+
}
|
|
44
|
+
const CATALOG_SIZE = contractOps().length;
|
|
45
|
+
const DEFAULT_OPTS = 'leanschema,leanaddendum,searchschemas,compactsearch,compactinspect,trustcalls,quickref';
|
|
46
|
+
const FULL_OPTS = new Set((process.env.SUPERDOC_FULL_OPTS ?? DEFAULT_OPTS)
|
|
47
|
+
.split(',')
|
|
48
|
+
.map((s) => s.trim())
|
|
49
|
+
.filter(Boolean));
|
|
50
|
+
const opt = (name) => FULL_OPTS.has(name);
|
|
51
|
+
const LEAN_ADDENDUM = `============================================================
|
|
52
|
+
EXTENDED SURFACE — the whole Document API (full preset)
|
|
53
|
+
============================================================
|
|
54
|
+
|
|
55
|
+
superdoc_perform_action covers the common verbs; every other engine operation is one step away. Escalation ladder, always in this order:
|
|
56
|
+
|
|
57
|
+
1. superdoc_perform_action — when a named action fits, use it. It matches surrounding formatting and numbering for you. Helper actions for the frequent gaps: insert_image {src, position?, anchorText?, caption?}, insert_footnote {anchorText, occurrence?, content, noteType?}, shade_cells {tableOrdinal?, rowIndex?, columnIndex?, color}, compare_with_file {path} (differences land here as tracked changes).
|
|
58
|
+
2. superdoc_api_call — {operationId, args}, or several in ONE round trip: {calls:[{operationId, args}, …]} for repetitive work (shade eight cells, insert twelve paragraphs). Runs in order, stops at the first failure, returns per-call receipts. The operations under QUICK REFERENCE need no lookup: call them straight away; an argument the operation does not accept is refused with its parameter list and changes nothing.
|
|
59
|
+
3. superdoc_api_search — only for an operation that is neither an action nor in QUICK REFERENCE: {query:"page margins"} or {domain:"sections"}. Hits include their parameter schema; look up the rest with ONE {operationIds:[…]} call (not one call per id). One search, then act.
|
|
60
|
+
{{QUICK_REFERENCE}}
|
|
61
|
+
|
|
62
|
+
Where things live (ids for superdoc_api_call):
|
|
63
|
+
- Page layout: doc.sections.list → sectionId, then doc.sections.setPageMargins / setPageSetup / setColumns / setPageNumbering / setTitlePage / setPageBorders; doc.create.sectionBreak.
|
|
64
|
+
- Header / footer text: doc.sections.list → doc.headerFooters.list → (if the slot has no explicit part) doc.headerFooters.parts.create {kind} → doc.headerFooters.refs.set {target:{kind:"headerFooterSlot", section:{kind:"section", sectionId}, headerFooterKind, variant}, refId}. A new part already holds ONE empty paragraph: write into it (doc.blocks.list {in:<story>} for its blockId, then doc.insert {value, type:"text", in:<story>, blockId, offset:0}); do not add a second paragraph. Header/footer doc.insert and doc.replace refuse block targets: use target:{kind:"selection", start:{kind:"text", blockId, offset}, end:{kind:"text", blockId, offset}} plus in:<story>. Story = {kind:"story", storyType:"headerFooterSlot", section:{kind:"section", sectionId}, headerFooterKind, variant}. Page number: doc.fields.insert {mode:"raw", instruction:"PAGE", at:{kind:"text", story:<same story>, segments:[{blockId, range:{start:N, end:N}}]}} after the text "Page " (N = its length); "Page N of M" adds " of " and a NUMPAGES field. Alignment: doc.format.paragraph.setAlignment with the story on the target. Odd/even variants need doc.sections.setOddEvenHeadersFooters {enabled:true} first, then one part per variant.
|
|
65
|
+
- Section breaks: doc.create.sectionBreak {at:{kind:"after", target:{kind:"block", nodeType, nodeId:<last paragraph of the closing section>}}, representation:"attachToPreviousParagraph", breakType:"nextPage"} closes the section on that paragraph, as Word does; {at:{kind:"before", target:<first paragraph of the new section>}} is the same boundary; {kind:"documentEnd"} appends a new final section. Then doc.sections.list for the new sectionId and doc.sections.setPageNumbering {start:1} to restart numbering. Add one only when the task needs a new section; never delete the paragraph hosting a break to remove it.
|
|
66
|
+
- Tables: doc.tables.setBorders / applyBorderPreset / setShading (table or cell background) / setCellProperties / setColumnWidth / setRowHeight / mergeCells / splitCell / sort / setStyle / setLayout; cell text formatting via doc.format.apply on the cell paragraph (new cells must match their siblings' run formatting — read a sibling with superdoc_inspect includeTableCellRuns:true). A NEW table (create_table, markdown) gets 1pt black borders: when the document already has tables, copy their convention (width, border colour and weight: superdoc_inspect / doc.tables.get a sibling) with doc.tables.setLayout and setBorders. A border weight the request leaves open is Word's 0.5pt. Table background = setShading on the table node, not shade_cells per cell.
|
|
67
|
+
- Formatting: doc.format.apply {target|ref, inline:{bold, italic, underline, color, fontSize, fontFamily, highlight}}; paragraph: doc.format.paragraph.setAlignment / setIndentation / setSpacing / setBorder / setShading / setKeepOptions / setTabStop / setNumbering. Units: colours are bare hex ("0000FF"), fontSize and letterSpacing in points, paragraph indentation and spacing in twips (20 = 1pt, 1440 = 1 inch), setBorder size in eighths of a point (1pt = 8) and space in points (Word's default is 1), table widths in twips where the schema says dxa. Box border: ONE batch of four setBorder {target, side:top|bottom|left|right, style:"single", size, color}; no paragraph setBorders exists.
|
|
68
|
+
- Whole-body font/size: ONE doc.format.apply with target {kind:"selection", start:{kind:"text", blockId:<first paragraph>, offset:0}, end:{kind:"text", blockId:<last paragraph>, offset:<length>}}, not set_font_family or per-paragraph calls; headers, footers and notes are separate story calls.
|
|
69
|
+
- Style definitions: doc.styles.create {id, name, type, conflictPolicy:"replace", custom:false, paragraph:{…}, run:{fontFamily:{ascii, hAnsi}, fontSize:<HALF-points: 11pt = 22>, color:{val:"RRGGBB"}, bold…}} redefines an existing style. Replace keeps every property you do not send (spacing, indentation, keep options, outlineLvl, basedOn, next), so send only what changes; null removes a property. Body-text changes "through the style" go here, not to each paragraph.
|
|
70
|
+
- Headings: insert_heading / doc.create.heading use the document's Heading N style. When superdoc_inspect shows the headings carry another style id (H1, Title2 …), create the paragraph and apply_style {styleId} (or doc.create.paragraph {styleId}) so no second heading style is minted. Promote an existing paragraph with apply_style {styleId} rather than headingLevel when such a style exists.
|
|
71
|
+
- Reference documents: doc.templates.apply {source:{kind:"path", path:"reference.docx"}} copies styles, numbering, page setup and header/footer substrate from a file next to the open document (relative paths resolve there); compare_with_file {path} lands the text differences as tracked changes.
|
|
72
|
+
- Lists: doc.lists.create / attach / detach / indent / outdent / setLevel / setValue / restartAt / setType / setLevelNumbering / setLevelBullet / split / join / merge.
|
|
73
|
+
- Fields, bookmarks, cross-references, hyperlinks, TOC: doc.fields.insert {mode:"raw", instruction}, doc.bookmarks.insert, doc.crossRefs.insert, doc.hyperlinks.insert / wrap / patch, doc.create.tableOfContents, doc.toc.configure / update / remove.
|
|
74
|
+
- Comments & tracked changes beyond the actions: doc.trackChanges.decide target is {kind:"id", id} | {kind:"all"}. Comment, footnote and user-quoted text go in exactly as written unless the task asks to rephrase.
|
|
75
|
+
- Long or structured drafting (memos, multi-section drafts, a document from blank): ONE doc.insert {value:"# Title\\n\\nParagraph…\\n\\n- bullet\\n\\n| a | b |\\n|---|---|", type:"markdown", target:{kind:"block", nodeType, nodeId}, placement:"after"|"before"} — headings, lists and tables become real structures. Read back with doc.getMarkdown.
|
|
76
|
+
- Content controls, protection, metadata, citations, index, captions: browse the matching domain.
|
|
77
|
+
|
|
78
|
+
Targets for superdoc_api_call:
|
|
79
|
+
- Block: {kind:"block", nodeType:"paragraph"|"heading"|"listItem"|"table"|"tableRow"|"tableCell"|"image"|"tableOfContents"|"sdt", nodeId} (nodeIds from superdoc_inspect).
|
|
80
|
+
- Position (\`at\`): {kind:"documentEnd"} | {kind:"documentStart"} | {kind:"before"|"after", target:<block>}.
|
|
81
|
+
- Text span: {kind:"text", segments:[{blockId, range:{start, end}}]} — character offsets inside that block's text. Add story:{…} inside headers/footers.
|
|
82
|
+
- One locator per call: target OR nodeId, never both.
|
|
83
|
+
- changeMode:"tracked" only where supportsTrackedMode ([tracked]; some refuse it for certain inputs, the error says why), dryRun only where supportsDryRun; otherwise the call is refused. Table and section property changes are direct-only.
|
|
84
|
+
|
|
85
|
+
Rules for the extended surface:
|
|
86
|
+
- The receipt is the truth. success:true (or a batch with completed === total) IS the verification: do not spend a superdoc_inspect call to confirm it. {success:false, failure} means nothing changed for that call — fix the args (usually a wrong target shape or missing nodeId), retry once. A thrown VALIDATION error also changed nothing. After a TIMEOUT, re-inspect before retrying an insert.
|
|
87
|
+
- Plan the whole edit from one inspect, then send the mutations as ONE batch per phase; repetition is a batch, not a call per item. Save once, at the end.
|
|
88
|
+
- Never claim a capability is missing without a superdoc_api_search first.
|
|
89
|
+
`;
|
|
90
|
+
const QUICK_REFERENCE_IDS = [
|
|
91
|
+
'doc.insert',
|
|
92
|
+
'doc.replace',
|
|
93
|
+
'doc.delete',
|
|
94
|
+
'doc.blocks.list',
|
|
95
|
+
'doc.create.paragraph',
|
|
96
|
+
'doc.create.sectionBreak',
|
|
97
|
+
'doc.format.apply',
|
|
98
|
+
'doc.format.paragraph.setAlignment',
|
|
99
|
+
'doc.format.paragraph.setIndentation',
|
|
100
|
+
'doc.format.paragraph.setSpacing',
|
|
101
|
+
'doc.format.paragraph.setKeepOptions',
|
|
102
|
+
'doc.styles.getCatalog',
|
|
103
|
+
'doc.styles.create',
|
|
104
|
+
'doc.comments.list',
|
|
105
|
+
'doc.comments.get',
|
|
106
|
+
'doc.comments.create',
|
|
107
|
+
'doc.comments.patch',
|
|
108
|
+
'doc.comments.delete',
|
|
109
|
+
'doc.trackChanges.list',
|
|
110
|
+
'doc.trackChanges.get',
|
|
111
|
+
'doc.trackChanges.decide',
|
|
112
|
+
'doc.tables.get',
|
|
113
|
+
'doc.tables.getProperties',
|
|
114
|
+
'doc.tables.getCells',
|
|
115
|
+
'doc.tables.setCellText',
|
|
116
|
+
'doc.tables.setColumnWidth',
|
|
117
|
+
'doc.tables.distributeColumns',
|
|
118
|
+
'doc.tables.setLayout',
|
|
119
|
+
'doc.tables.setBorders',
|
|
120
|
+
'doc.tables.setShading',
|
|
121
|
+
'doc.tables.mergeCells',
|
|
122
|
+
'doc.tables.insertRow',
|
|
123
|
+
'doc.tables.setRowHeight',
|
|
124
|
+
'doc.tables.setCellProperties',
|
|
125
|
+
'doc.sections.list',
|
|
126
|
+
'doc.headerFooters.list',
|
|
127
|
+
'doc.headerFooters.parts.list',
|
|
128
|
+
'doc.headerFooters.parts.create',
|
|
129
|
+
'doc.headerFooters.refs.set',
|
|
130
|
+
'doc.footnotes.list',
|
|
131
|
+
'doc.footnotes.get',
|
|
132
|
+
'doc.footnotes.insert',
|
|
133
|
+
'doc.footnotes.remove',
|
|
134
|
+
'doc.fields.insert',
|
|
135
|
+
'doc.bookmarks.insert',
|
|
136
|
+
'doc.hyperlinks.list',
|
|
137
|
+
'doc.hyperlinks.wrap',
|
|
138
|
+
'doc.lists.setLevelNumbering',
|
|
139
|
+
'doc.mutations.apply',
|
|
140
|
+
];
|
|
141
|
+
export const QUICK_REFERENCE_OMITTED = new Set([
|
|
142
|
+
'changeMode',
|
|
143
|
+
'changeTag',
|
|
144
|
+
'date',
|
|
145
|
+
'dryRun',
|
|
146
|
+
'supportCheck',
|
|
147
|
+
'namespaces',
|
|
148
|
+
'sourcePartUri',
|
|
149
|
+
'dependencies',
|
|
150
|
+
'nestingPolicy',
|
|
151
|
+
'capture',
|
|
152
|
+
'content',
|
|
153
|
+
'input',
|
|
154
|
+
'replaceMode',
|
|
155
|
+
'snapshotId',
|
|
156
|
+
'expectedSnapshotId',
|
|
157
|
+
'expectedText',
|
|
158
|
+
'aliases',
|
|
159
|
+
'priority',
|
|
160
|
+
'qFormat',
|
|
161
|
+
'hidden',
|
|
162
|
+
'semiHidden',
|
|
163
|
+
'unhideWhenUsed',
|
|
164
|
+
'locked',
|
|
165
|
+
'next',
|
|
166
|
+
'includeHidden',
|
|
167
|
+
'includeLatent',
|
|
168
|
+
'includeDiagnostics',
|
|
169
|
+
'reviewMode',
|
|
170
|
+
'commentId',
|
|
171
|
+
'externalId',
|
|
172
|
+
'authorId',
|
|
173
|
+
'authorEmail',
|
|
174
|
+
'authorImage',
|
|
175
|
+
'metadata',
|
|
176
|
+
]);
|
|
177
|
+
function shapeHint(schema) {
|
|
178
|
+
if (schema == null || typeof schema !== 'object')
|
|
179
|
+
return '';
|
|
180
|
+
const s = schema;
|
|
181
|
+
if (s.const !== undefined)
|
|
182
|
+
return JSON.stringify(s.const);
|
|
183
|
+
if (Array.isArray(s.oneOf)) {
|
|
184
|
+
const alts = s.oneOf;
|
|
185
|
+
const kinds = [
|
|
186
|
+
...new Set(alts
|
|
187
|
+
.map((alt) => alt.properties?.kind?.const)
|
|
188
|
+
.filter((k) => typeof k === 'string')),
|
|
189
|
+
];
|
|
190
|
+
if (kinds.length > 0 && alts.every((alt) => alt.properties?.kind))
|
|
191
|
+
return `{kind:${kinds.join('|')}}`;
|
|
192
|
+
const types = [
|
|
193
|
+
...new Set(alts.map((alt) => (typeof alt.type === 'string' ? alt.type : alt.properties ? 'object' : '')).filter(Boolean)),
|
|
194
|
+
];
|
|
195
|
+
return types.length ? types.map((t) => (t === 'object' ? '{…}' : t)).join('|') : '';
|
|
196
|
+
}
|
|
197
|
+
if (s.properties && typeof s.properties === 'object') {
|
|
198
|
+
const keys = Object.keys(s.properties);
|
|
199
|
+
return `{${keys.slice(0, 6).join(',')}${keys.length > 6 ? ',…' : ''}}`;
|
|
200
|
+
}
|
|
201
|
+
if (s.type === 'array')
|
|
202
|
+
return '[]';
|
|
203
|
+
return typeof s.type === 'string' && s.type !== 'json' && s.type !== 'object' ? s.type : '';
|
|
204
|
+
}
|
|
205
|
+
function unitHint(p) {
|
|
206
|
+
if (p.type !== 'number' && p.type !== 'integer')
|
|
207
|
+
return '';
|
|
208
|
+
const m = /\b(twips|half-points|points|eighths of a point|EMU|inches|percent)\b/i.exec(p.description ?? '');
|
|
209
|
+
return m ? m[1].toLowerCase() : '';
|
|
210
|
+
}
|
|
211
|
+
export function quickReference() {
|
|
212
|
+
const lines = QUICK_REFERENCE_IDS.flatMap((id) => {
|
|
213
|
+
const op = lookupOp(id);
|
|
214
|
+
if (!op)
|
|
215
|
+
return [];
|
|
216
|
+
const flags = [op.mutates === true ? '' : 'ro', operationAcceptsTrackedMode(op) ? 'tracked' : '']
|
|
217
|
+
.filter(Boolean)
|
|
218
|
+
.join(',');
|
|
219
|
+
const params = visibleParams(op)
|
|
220
|
+
.filter((p) => !QUICK_REFERENCE_OMITTED.has(p.name))
|
|
221
|
+
.map((p) => {
|
|
222
|
+
const hint = p.type && p.type !== 'json' ? unitHint(p) || p.type : shapeHint(p.schema);
|
|
223
|
+
return `${p.name}${p.required ? '*' : ''}${hint ? `:${hint}` : ''}`;
|
|
224
|
+
})
|
|
225
|
+
.join(', ');
|
|
226
|
+
return [`- ${op.operationId}${flags ? ` [${flags}]` : ''} {${params}}`];
|
|
227
|
+
});
|
|
228
|
+
return [
|
|
229
|
+
'QUICK REFERENCE (superdoc_api_call without a search; * = required; {a,b} = object with those keys; {kind:x|y} = one of the Targets shapes below; [ro] reads only; [tracked] accepts changeMode:"tracked"):',
|
|
230
|
+
...lines,
|
|
231
|
+
].join('\n');
|
|
232
|
+
}
|
|
233
|
+
function lookupOp(operationId) {
|
|
234
|
+
const op = CONTRACT.operations[operationId];
|
|
235
|
+
return op && isCallableOp(op) ? op : undefined;
|
|
236
|
+
}
|
|
237
|
+
function visibleParams(op) {
|
|
238
|
+
return op.params.filter((p) => !HIDDEN_PARAMS.has(p.name) && p.agentVisible !== false);
|
|
239
|
+
}
|
|
240
|
+
function normalizeId(id) {
|
|
241
|
+
const s = typeof id === 'string' ? id.trim() : '';
|
|
242
|
+
return s.startsWith('doc.') ? s : `doc.${s}`;
|
|
243
|
+
}
|
|
244
|
+
function forwardOptions(invokeOptions) {
|
|
245
|
+
if (!invokeOptions)
|
|
246
|
+
return undefined;
|
|
247
|
+
const { excludeActions: _ignored, ...rest } = invokeOptions;
|
|
248
|
+
return rest;
|
|
249
|
+
}
|
|
250
|
+
function resolveMethod(doc, operationId) {
|
|
251
|
+
const tokens = operationId.split('.').slice(1);
|
|
252
|
+
let cursor = doc;
|
|
253
|
+
for (const token of tokens) {
|
|
254
|
+
if (cursor == null ||
|
|
255
|
+
(typeof cursor !== 'object' && typeof cursor !== 'function') ||
|
|
256
|
+
!(token in cursor)) {
|
|
257
|
+
throw new SuperDocCliError(`No bound method found for operation ${operationId}.`, {
|
|
258
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
259
|
+
details: { operationId, token },
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
cursor = cursor[token];
|
|
263
|
+
}
|
|
264
|
+
if (typeof cursor !== 'function') {
|
|
265
|
+
throw new SuperDocCliError(`Resolved member for ${operationId} is not callable.`, {
|
|
266
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
267
|
+
details: { operationId },
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const method = cursor;
|
|
271
|
+
return (args, options) => method(enforceTrackedChangeMode(operationId, args, trackedEnforcementOf(options)).args, options);
|
|
272
|
+
}
|
|
273
|
+
function trackedEnforcementOf(options) {
|
|
274
|
+
if (!isRecord(options) || options.enforceChangeMode !== 'tracked')
|
|
275
|
+
return null;
|
|
276
|
+
const allowDirect = options.allowDirectActions;
|
|
277
|
+
return {
|
|
278
|
+
directOperationsAllowed: Array.isArray(allowDirect) && allowDirect.includes(ALLOW_ALL_DIRECT_ACTIONS),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function withoutTrackedEnforcement(invokeOptions) {
|
|
282
|
+
if (!invokeOptions)
|
|
283
|
+
return undefined;
|
|
284
|
+
const { enforceChangeMode: _enforced, allowDirectActions: _allowed, ...rest } = invokeOptions;
|
|
285
|
+
return rest;
|
|
286
|
+
}
|
|
287
|
+
function enforceTrackedChangeMode(operationId, args, enforcement) {
|
|
288
|
+
if (!enforcement)
|
|
289
|
+
return { args, changeModeForced: false };
|
|
290
|
+
const op = CONTRACT.operations[operationId];
|
|
291
|
+
if (op?.mutates === false)
|
|
292
|
+
return { args, changeModeForced: false };
|
|
293
|
+
if (op && operationAcceptsTrackedMode(op)) {
|
|
294
|
+
return { args: { ...args, changeMode: 'tracked' }, changeModeForced: args.changeMode !== 'tracked' };
|
|
295
|
+
}
|
|
296
|
+
if (enforcement.directOperationsAllowed)
|
|
297
|
+
return { args, changeModeForced: false };
|
|
298
|
+
throw new SuperDocCliError(`${operationId} edits the document directly and is refused under enforceChangeMode: "tracked". Nothing was changed. Use an operation that supports tracked mode, or have the host permit direct-only edits with allowDirectActions: ["*"].`, {
|
|
299
|
+
code: 'INVALID_ARGUMENT',
|
|
300
|
+
details: { operationId, supportsTrackedMode: false, reason: 'enforce-change-mode' },
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function changeModeReceipt(operationId, governed, enforcement) {
|
|
304
|
+
if (!enforcement || CONTRACT.operations[operationId]?.mutates !== true)
|
|
305
|
+
return {};
|
|
306
|
+
return {
|
|
307
|
+
changeMode: governed.args.changeMode === 'tracked' ? 'tracked' : 'direct',
|
|
308
|
+
changeModeForced: governed.changeModeForced,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function isFailureReceipt(value) {
|
|
312
|
+
return value != null && typeof value === 'object' && value.success === false;
|
|
313
|
+
}
|
|
314
|
+
function scoreOp(op, terms) {
|
|
315
|
+
const id = op.operationId.toLowerCase();
|
|
316
|
+
const desc = (op.description ?? '').toLowerCase();
|
|
317
|
+
const paramNames = op.params.map((p) => p.name.toLowerCase()).join(' ');
|
|
318
|
+
let score = 0;
|
|
319
|
+
for (const term of terms) {
|
|
320
|
+
if (id.includes(term))
|
|
321
|
+
score += 5;
|
|
322
|
+
if (desc.includes(term))
|
|
323
|
+
score += 2;
|
|
324
|
+
if (paramNames.includes(term))
|
|
325
|
+
score += 1;
|
|
326
|
+
}
|
|
327
|
+
return score;
|
|
328
|
+
}
|
|
329
|
+
function summarizeOp(op) {
|
|
330
|
+
return {
|
|
331
|
+
operationId: op.operationId,
|
|
332
|
+
description: (op.description ?? '').slice(0, opt('compactsearch') ? 110 : 180),
|
|
333
|
+
mutates: op.mutates === true,
|
|
334
|
+
tracked: operationAcceptsTrackedMode(op),
|
|
335
|
+
params: visibleParams(op).map((p) => (p.required ? `${p.name}*` : p.name)),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function summarizeOpWithSchema(op) {
|
|
339
|
+
const d = describeOp(op);
|
|
340
|
+
return {
|
|
341
|
+
operationId: d.operationId,
|
|
342
|
+
description: (op.description ?? '').slice(0, opt('compactsearch') ? 110 : 180),
|
|
343
|
+
mutates: d.mutates,
|
|
344
|
+
supportsTrackedMode: d.supportsTrackedMode,
|
|
345
|
+
supportsDryRun: d.supportsDryRun,
|
|
346
|
+
targetShape: d.targetShape,
|
|
347
|
+
params: d.params,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const SCHEMA_HITS = 3;
|
|
351
|
+
function compactSchema(schema, depth = 0) {
|
|
352
|
+
if (schema == null || typeof schema !== 'object')
|
|
353
|
+
return schema;
|
|
354
|
+
const text = JSON.stringify(schema);
|
|
355
|
+
if (text.length <= SCHEMA_CHAR_CAP)
|
|
356
|
+
return schema;
|
|
357
|
+
const s = schema;
|
|
358
|
+
if (Array.isArray(s.oneOf)) {
|
|
359
|
+
return {
|
|
360
|
+
oneOf: s.oneOf
|
|
361
|
+
.slice(0, 12)
|
|
362
|
+
.map((alt) => (depth < 2 ? compactSchema(alt, depth + 1) : shapeOf(alt))),
|
|
363
|
+
...(s.description ? { description: s.description } : {}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return shapeOf(schema);
|
|
367
|
+
}
|
|
368
|
+
function shapeOf(schema) {
|
|
369
|
+
if (schema == null || typeof schema !== 'object')
|
|
370
|
+
return schema;
|
|
371
|
+
const s = schema;
|
|
372
|
+
if (s.const !== undefined)
|
|
373
|
+
return { const: s.const };
|
|
374
|
+
if (s.type === 'array')
|
|
375
|
+
return { type: 'array', items: shapeOf(s.items) };
|
|
376
|
+
if (s.properties && typeof s.properties === 'object') {
|
|
377
|
+
const props = {};
|
|
378
|
+
for (const [k, v] of Object.entries(s.properties)) {
|
|
379
|
+
const vv = v;
|
|
380
|
+
props[k] =
|
|
381
|
+
vv?.const !== undefined ? { const: vv.const } : (vv?.type ?? (Array.isArray(vv?.oneOf) ? 'oneOf' : 'object'));
|
|
382
|
+
}
|
|
383
|
+
return { type: 'object', properties: props, ...(s.required ? { required: s.required } : {}) };
|
|
384
|
+
}
|
|
385
|
+
if (s.type)
|
|
386
|
+
return { type: s.type };
|
|
387
|
+
return '[object]';
|
|
388
|
+
}
|
|
389
|
+
function describeOp(op) {
|
|
390
|
+
const params = visibleParams(op).map((p) => ({
|
|
391
|
+
name: p.name,
|
|
392
|
+
type: p.type,
|
|
393
|
+
required: p.required === true,
|
|
394
|
+
description: p.description,
|
|
395
|
+
schema: compactSchema(p.schema),
|
|
396
|
+
}));
|
|
397
|
+
const entry = getOperationCatalogEntry(op.operationId);
|
|
398
|
+
return {
|
|
399
|
+
operationId: op.operationId,
|
|
400
|
+
description: op.description,
|
|
401
|
+
mutates: op.mutates === true,
|
|
402
|
+
supportsTrackedMode: operationAcceptsTrackedMode(op),
|
|
403
|
+
supportsDryRun: op.supportsDryRun === true,
|
|
404
|
+
domain: entry?.domain ?? domainOf(op.operationId),
|
|
405
|
+
targetShape: entry?.targetShape,
|
|
406
|
+
params,
|
|
407
|
+
usage: 'Call superdoc_api_call with {operationId, args:{…params…}}. Block targets are {kind:"block", nodeType, nodeId} using nodeIds from superdoc_inspect / doc.blocks.list. Positions are {kind:"documentEnd"} | {kind:"documentStart"} | {kind:"before"|"after", target:{kind:"block", nodeType, nodeId}}. Text spans are {kind:"text", segments:[{blockId, range:{start,end}}]}. Pass changeMode:"tracked" only when supportsTrackedMode is true.',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function describeOne(ops, rawId) {
|
|
411
|
+
const id = normalizeId(rawId);
|
|
412
|
+
const op = ops.find((o) => o.operationId === id);
|
|
413
|
+
if (!op) {
|
|
414
|
+
const near = ops
|
|
415
|
+
.map((o) => ({
|
|
416
|
+
o,
|
|
417
|
+
s: scoreOp(o, id
|
|
418
|
+
.replace(/^doc\./, '')
|
|
419
|
+
.toLowerCase()
|
|
420
|
+
.split('.')),
|
|
421
|
+
}))
|
|
422
|
+
.filter((x) => x.s > 0)
|
|
423
|
+
.sort((a, b) => b.s - a.s)
|
|
424
|
+
.slice(0, 8)
|
|
425
|
+
.map((x) => x.o.operationId);
|
|
426
|
+
return { found: false, operationId: id, didYouMean: near };
|
|
427
|
+
}
|
|
428
|
+
return { found: true, operation: describeOp(op) };
|
|
429
|
+
}
|
|
430
|
+
function runCatalog(args) {
|
|
431
|
+
const ops = contractOps();
|
|
432
|
+
if (Array.isArray(args.operationIds) && args.operationIds.length > 0) {
|
|
433
|
+
const operations = args.operationIds
|
|
434
|
+
.slice(0, 8)
|
|
435
|
+
.filter((id) => typeof id === 'string' && id.trim().length > 0)
|
|
436
|
+
.map((id) => describeOne(ops, id))
|
|
437
|
+
.map((r) => (r.found ? r.operation : r));
|
|
438
|
+
return { operations };
|
|
439
|
+
}
|
|
440
|
+
if (typeof args.operationId === 'string' && args.operationId.trim().length > 0) {
|
|
441
|
+
return describeOne(ops, args.operationId);
|
|
442
|
+
}
|
|
443
|
+
const limit = Math.min(Math.max(Number(args.limit ?? (opt('compactsearch') ? 12 : 25)), 1), 24);
|
|
444
|
+
const domain = typeof args.domain === 'string' && args.domain.trim() ? args.domain.trim().toLowerCase() : undefined;
|
|
445
|
+
const terms = (args.query ?? '')
|
|
446
|
+
.toLowerCase()
|
|
447
|
+
.split(/[^a-z0-9]+/)
|
|
448
|
+
.filter((t) => t.length > 1);
|
|
449
|
+
let candidates = ops;
|
|
450
|
+
if (domain)
|
|
451
|
+
candidates = candidates.filter((o) => domainOf(o.operationId).toLowerCase() === domain);
|
|
452
|
+
if (args.mutatingOnly === true)
|
|
453
|
+
candidates = candidates.filter((o) => o.mutates === true);
|
|
454
|
+
let ranked = candidates.map((o) => ({ o, s: terms.length ? scoreOp(o, terms) : 1 }));
|
|
455
|
+
if (terms.length)
|
|
456
|
+
ranked = ranked.filter((x) => x.s > 0);
|
|
457
|
+
ranked.sort((a, b) => b.s - a.s || a.o.operationId.localeCompare(b.o.operationId));
|
|
458
|
+
const domains = [...new Set(ops.map((o) => domainOf(o.operationId)))].sort();
|
|
459
|
+
const page = ranked.slice(0, limit);
|
|
460
|
+
if (opt('searchschemas') && (terms.length || domain)) {
|
|
461
|
+
const withSchema = page.slice(0, SCHEMA_HITS).map((x) => summarizeOpWithSchema(x.o));
|
|
462
|
+
const rest = page.slice(SCHEMA_HITS).map((x) => summarizeOp(x.o));
|
|
463
|
+
return {
|
|
464
|
+
total: ranked.length,
|
|
465
|
+
showing: page.length,
|
|
466
|
+
operations: [...withSchema, ...rest],
|
|
467
|
+
usage: 'The first hits include their params; call superdoc_api_call with {operationId, args} directly. Block targets are {kind:"block", nodeType, nodeId}; positions {kind:"documentEnd"|"documentStart"} or {kind:"before"|"after", target:<block>}; text spans {kind:"text", segments:[{blockId, range:{start,end}}]}. Pass changeMode:"tracked" only when supportsTrackedMode is true.',
|
|
468
|
+
next: rest.length
|
|
469
|
+
? 'For an operation listed without params, call superdoc_api_search with {operationId}.'
|
|
470
|
+
: undefined,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
total: ranked.length,
|
|
475
|
+
showing: page.length,
|
|
476
|
+
domains: terms.length || domain ? undefined : domains,
|
|
477
|
+
operations: page.map((x) => summarizeOp(x.o)),
|
|
478
|
+
next: 'Call superdoc_api_search with {operationId} for the full parameter schema of one operation.',
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const MIME_BY_EXT = {
|
|
482
|
+
'.png': 'image/png',
|
|
483
|
+
'.jpg': 'image/jpeg',
|
|
484
|
+
'.jpeg': 'image/jpeg',
|
|
485
|
+
'.gif': 'image/gif',
|
|
486
|
+
'.bmp': 'image/bmp',
|
|
487
|
+
'.webp': 'image/webp',
|
|
488
|
+
'.svg': 'image/svg+xml',
|
|
489
|
+
'.tif': 'image/tiff',
|
|
490
|
+
'.tiff': 'image/tiff',
|
|
491
|
+
};
|
|
492
|
+
function documentDir(doc) {
|
|
493
|
+
const p = doc.openResult?.document?.path;
|
|
494
|
+
return typeof p === 'string' && p.length > 0 ? path.dirname(path.resolve(p)) : undefined;
|
|
495
|
+
}
|
|
496
|
+
async function toDataUri(src, baseDir) {
|
|
497
|
+
if (/^data:/i.test(src))
|
|
498
|
+
return src;
|
|
499
|
+
if (/^https?:\/\//i.test(src)) {
|
|
500
|
+
const res = await fetch(src);
|
|
501
|
+
if (!res.ok) {
|
|
502
|
+
throw new SuperDocCliError(`Image URL fetch failed: ${res.status} ${res.statusText}`, {
|
|
503
|
+
code: 'INVALID_ARGUMENT',
|
|
504
|
+
details: { src },
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
const mime = res.headers.get('content-type')?.split(';')[0] || 'image/png';
|
|
508
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
509
|
+
return `data:${mime};base64,${bytes.toString('base64')}`;
|
|
510
|
+
}
|
|
511
|
+
const raw = /^file:\/\//i.test(src) ? fileURLToPath(src) : src;
|
|
512
|
+
const filePath = resolveDocumentRelativePath(raw, baseDir);
|
|
513
|
+
if (!existsSync(filePath)) {
|
|
514
|
+
throw new SuperDocCliError(`Image file not found: ${src} (looked at ${filePath})`, {
|
|
515
|
+
code: 'INVALID_ARGUMENT',
|
|
516
|
+
details: { src, path: filePath },
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
const mime = MIME_BY_EXT[path.extname(filePath).toLowerCase()] ?? 'image/png';
|
|
520
|
+
const bytes = await readFile(filePath);
|
|
521
|
+
return `data:${mime};base64,${bytes.toString('base64')}`;
|
|
522
|
+
}
|
|
523
|
+
function resolveDocumentRelativePath(raw, baseDir) {
|
|
524
|
+
if (path.isAbsolute(raw) || /^(data:|https?:\/\/|file:)/i.test(raw))
|
|
525
|
+
return raw;
|
|
526
|
+
if (!baseDir) {
|
|
527
|
+
throw new SuperDocCliError(`Relative path "${raw}" resolves from the open document's folder, and this document was not opened from a file. Pass an absolute path.`, { code: 'INVALID_ARGUMENT', details: { path: raw } });
|
|
528
|
+
}
|
|
529
|
+
const resolved = path.resolve(baseDir, raw);
|
|
530
|
+
const inside = path.relative(baseDir, resolved);
|
|
531
|
+
if (inside === '..' || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside)) {
|
|
532
|
+
throw new SuperDocCliError(`Relative path "${raw}" leaves the open document's folder ${baseDir}. Pass an absolute path for a file elsewhere.`, { code: 'INVALID_ARGUMENT', details: { path: raw, documentFolder: baseDir } });
|
|
533
|
+
}
|
|
534
|
+
return resolved;
|
|
535
|
+
}
|
|
536
|
+
const COLOR_KEY = /(^|[a-z])(color|fill)$/i;
|
|
537
|
+
function normalizeColors(value) {
|
|
538
|
+
if (Array.isArray(value))
|
|
539
|
+
return value.map(normalizeColors);
|
|
540
|
+
if (value && typeof value === 'object') {
|
|
541
|
+
const out = {};
|
|
542
|
+
for (const [k, v] of Object.entries(value)) {
|
|
543
|
+
out[k] =
|
|
544
|
+
typeof v === 'string' && COLOR_KEY.test(k) && /^#[0-9a-f]{6}$/i.test(v) ? v.slice(1) : normalizeColors(v);
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
return value;
|
|
549
|
+
}
|
|
550
|
+
function styleCreateSchema(channel) {
|
|
551
|
+
return lookupOp('doc.styles.create')?.params.find((p) => p.name === channel)?.schema;
|
|
552
|
+
}
|
|
553
|
+
function probeValue(schema) {
|
|
554
|
+
if (!schema)
|
|
555
|
+
return undefined;
|
|
556
|
+
if (schema.const !== undefined)
|
|
557
|
+
return schema.const;
|
|
558
|
+
if (Array.isArray(schema.oneOf))
|
|
559
|
+
return probeValue(schema.oneOf[0]);
|
|
560
|
+
if (schema.type === 'boolean')
|
|
561
|
+
return true;
|
|
562
|
+
if (schema.type === 'number' || schema.type === 'integer')
|
|
563
|
+
return 1;
|
|
564
|
+
if (schema.type === 'string')
|
|
565
|
+
return 'auto';
|
|
566
|
+
if (schema.type === 'array') {
|
|
567
|
+
const item = probeValue(schema.items);
|
|
568
|
+
return item === undefined ? undefined : [item];
|
|
569
|
+
}
|
|
570
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
571
|
+
const value = probeValue(child);
|
|
572
|
+
if (value !== undefined)
|
|
573
|
+
return { [key]: value };
|
|
574
|
+
}
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
577
|
+
const RECEIPT_KEY_ALIASES = { sz: 'size' };
|
|
578
|
+
const isSet = (state) => state != null && state !== 'inherit';
|
|
579
|
+
function patchValue(schema, state, lost, at) {
|
|
580
|
+
if (!schema || !isSet(state))
|
|
581
|
+
return undefined;
|
|
582
|
+
if (Array.isArray(schema.oneOf))
|
|
583
|
+
return schema.oneOf.some((alt) => alt.const === state) ? state : undefined;
|
|
584
|
+
switch (schema.type) {
|
|
585
|
+
case 'boolean':
|
|
586
|
+
if (state === true || state === 'on' || state === '1' || state === 'true')
|
|
587
|
+
return true;
|
|
588
|
+
if (state === false || state === 'off' || state === '0' || state === 'false')
|
|
589
|
+
return false;
|
|
590
|
+
return undefined;
|
|
591
|
+
case 'number':
|
|
592
|
+
case 'integer': {
|
|
593
|
+
const n = typeof state === 'object' ? NaN : Number(state);
|
|
594
|
+
return Number.isFinite(n) ? n : undefined;
|
|
595
|
+
}
|
|
596
|
+
case 'string':
|
|
597
|
+
if (typeof state === 'object')
|
|
598
|
+
return undefined;
|
|
599
|
+
return typeof state === 'boolean' ? (state ? '1' : '0') : String(state);
|
|
600
|
+
case 'array': {
|
|
601
|
+
if (!Array.isArray(state))
|
|
602
|
+
return undefined;
|
|
603
|
+
const items = state.map((item, i) => {
|
|
604
|
+
const before = lost.length;
|
|
605
|
+
const value = patchValue(schema.items, item, lost, `${at}[${i}]`);
|
|
606
|
+
if (value === undefined && isSet(item) && lost.length === before)
|
|
607
|
+
lost.push(`${at}[${i}]`);
|
|
608
|
+
return value;
|
|
609
|
+
});
|
|
610
|
+
return items.filter((item) => item !== undefined);
|
|
611
|
+
}
|
|
612
|
+
case 'object': {
|
|
613
|
+
if (typeof state !== 'object' || Array.isArray(state))
|
|
614
|
+
return undefined;
|
|
615
|
+
const properties = schema.properties ?? {};
|
|
616
|
+
const out = {};
|
|
617
|
+
for (const [rawKey, child] of Object.entries(state)) {
|
|
618
|
+
const key = rawKey in properties ? rawKey : RECEIPT_KEY_ALIASES[rawKey];
|
|
619
|
+
const path = at ? `${at}.${rawKey}` : rawKey;
|
|
620
|
+
const before = lost.length;
|
|
621
|
+
const value = key && key in properties ? patchValue(properties[key], child, lost, path) : undefined;
|
|
622
|
+
if (value !== undefined)
|
|
623
|
+
out[key] = value;
|
|
624
|
+
else if (isSet(child) && lost.length === before)
|
|
625
|
+
lost.push(path);
|
|
626
|
+
}
|
|
627
|
+
return Object.keys(out).length ? out : undefined;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
async function readStyleDefinition(doc, item, invokeOptions) {
|
|
633
|
+
const paragraphSchema = styleCreateSchema('paragraph');
|
|
634
|
+
const runSchema = styleCreateSchema('run');
|
|
635
|
+
if (!paragraphSchema?.properties || !runSchema?.properties)
|
|
636
|
+
return null;
|
|
637
|
+
const probe = (schema) => Object.fromEntries(Object.entries(schema.properties ?? {}).map(([key, child]) => [key, probeValue(child)]));
|
|
638
|
+
const receipt = (await resolveMethod(doc, 'doc.styles.create')({
|
|
639
|
+
id: item.id,
|
|
640
|
+
name: item.name ?? item.id,
|
|
641
|
+
type: item.type,
|
|
642
|
+
conflictPolicy: 'replace',
|
|
643
|
+
dryRun: true,
|
|
644
|
+
...(item.type === 'paragraph' ? { paragraph: probe(paragraphSchema) } : {}),
|
|
645
|
+
run: probe(runSchema),
|
|
646
|
+
}, forwardOptions(invokeOptions)));
|
|
647
|
+
if (receipt?.success !== true || !receipt.before)
|
|
648
|
+
return null;
|
|
649
|
+
const lost = [];
|
|
650
|
+
const channel = (name, schema) => patchValue({ type: 'object', properties: schema.properties }, receipt.before[name], lost, name);
|
|
651
|
+
return { paragraph: channel('paragraph', paragraphSchema), run: channel('run', runSchema), lost };
|
|
652
|
+
}
|
|
653
|
+
const THEME_SIBLINGS = {
|
|
654
|
+
color: ['themeColor', 'themeTint', 'themeShade'],
|
|
655
|
+
fill: ['themeFill', 'themeFillShade', 'themeFillTint'],
|
|
656
|
+
ascii: ['asciiTheme'],
|
|
657
|
+
hAnsi: ['hAnsiTheme'],
|
|
658
|
+
eastAsia: ['eastAsiaTheme'],
|
|
659
|
+
cs: ['cstheme'],
|
|
660
|
+
};
|
|
661
|
+
function mergeStylePatch(existing, patch, parent) {
|
|
662
|
+
if (patch === null)
|
|
663
|
+
return undefined;
|
|
664
|
+
const out = { ...(isRecord(existing) ? existing : {}) };
|
|
665
|
+
if (isRecord(patch)) {
|
|
666
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
667
|
+
if (parent) {
|
|
668
|
+
const siblings = THEME_SIBLINGS[parent === 'color' && key === 'val' ? 'color' : key] ?? [];
|
|
669
|
+
for (const sibling of siblings)
|
|
670
|
+
delete out[sibling];
|
|
671
|
+
}
|
|
672
|
+
if (value === null)
|
|
673
|
+
delete out[key];
|
|
674
|
+
else if (isRecord(value)) {
|
|
675
|
+
const merged = mergeStylePatch(isRecord(out[key]) ? out[key] : undefined, value, key);
|
|
676
|
+
if (merged)
|
|
677
|
+
out[key] = merged;
|
|
678
|
+
else
|
|
679
|
+
delete out[key];
|
|
680
|
+
}
|
|
681
|
+
else
|
|
682
|
+
out[key] = value;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return Object.keys(out).length ? out : undefined;
|
|
686
|
+
}
|
|
687
|
+
const KEPT_STYLE_FIELDS = [
|
|
688
|
+
'basedOn',
|
|
689
|
+
'next',
|
|
690
|
+
'aliases',
|
|
691
|
+
'priority',
|
|
692
|
+
'qFormat',
|
|
693
|
+
'hidden',
|
|
694
|
+
'semiHidden',
|
|
695
|
+
'unhideWhenUsed',
|
|
696
|
+
'locked',
|
|
697
|
+
];
|
|
698
|
+
async function keepUnsentStyleProperties(doc, args, invokeOptions) {
|
|
699
|
+
if (args.conflictPolicy !== 'replace' || typeof args.id !== 'string')
|
|
700
|
+
return args;
|
|
701
|
+
const refuse = (message, details = {}) => new SuperDocCliError(message, {
|
|
702
|
+
code: 'INVALID_ARGUMENT',
|
|
703
|
+
details: { operationId: 'doc.styles.create', styleId: args.id, ...details },
|
|
704
|
+
});
|
|
705
|
+
const unreadable = (reason) => refuse(`doc.styles.create could not read the current definition of style "${args.id}" (${reason}), so a replace could drop properties it cannot see. Nothing was changed. Create the style under a new id with conflictPolicy "fail", or ask the user how to proceed.`);
|
|
706
|
+
let item;
|
|
707
|
+
let existing;
|
|
708
|
+
try {
|
|
709
|
+
const catalog = (await resolveMethod(doc, 'doc.styles.getCatalog')({ view: 'currentDocument', includeHidden: true }, forwardOptions(invokeOptions)));
|
|
710
|
+
item = catalog?.items?.find((s) => s.id === args.id);
|
|
711
|
+
if (!item || item.link || item.type !== args.type || (item.type !== 'paragraph' && item.type !== 'character'))
|
|
712
|
+
return args;
|
|
713
|
+
existing = await readStyleDefinition(doc, item, invokeOptions);
|
|
714
|
+
}
|
|
715
|
+
catch (e) {
|
|
716
|
+
throw unreadable(e.message);
|
|
717
|
+
}
|
|
718
|
+
if (!existing)
|
|
719
|
+
throw unreadable('the definition read was refused');
|
|
720
|
+
const unsent = existing.lost.filter((path) => {
|
|
721
|
+
const [channel, key] = path.split('.');
|
|
722
|
+
return args[channel]?.[key.replace(/\[\d+\]$/, '')] === undefined;
|
|
723
|
+
});
|
|
724
|
+
if (unsent.length) {
|
|
725
|
+
const properties = [
|
|
726
|
+
...new Set(unsent.map((path) => path
|
|
727
|
+
.split('.')
|
|
728
|
+
.slice(0, 2)
|
|
729
|
+
.join('.')
|
|
730
|
+
.replace(/\[\d+\]$/, ''))),
|
|
731
|
+
];
|
|
732
|
+
throw refuse(`doc.styles.create could not read ${unsent.join(', ')} of style "${args.id}", so a replace would drop it. Nothing was changed. Resend the complete value of ${properties.join(', ')} in this call.`, { unreadable: unsent, resend: properties });
|
|
733
|
+
}
|
|
734
|
+
const out = { ...args };
|
|
735
|
+
for (const field of KEPT_STYLE_FIELDS) {
|
|
736
|
+
const value = item[field];
|
|
737
|
+
if (out[field] === undefined && value != null && value !== false && !(Array.isArray(value) && !value.length))
|
|
738
|
+
out[field] = value;
|
|
739
|
+
}
|
|
740
|
+
if (out.custom === undefined && typeof item.custom === 'boolean')
|
|
741
|
+
out.custom = item.custom;
|
|
742
|
+
for (const channel of item.type === 'paragraph' ? ['paragraph', 'run'] : ['run']) {
|
|
743
|
+
const merged = mergeStylePatch(existing[channel], args[channel]);
|
|
744
|
+
if (merged)
|
|
745
|
+
out[channel] = merged;
|
|
746
|
+
else
|
|
747
|
+
delete out[channel];
|
|
748
|
+
}
|
|
749
|
+
return out;
|
|
750
|
+
}
|
|
751
|
+
async function preprocessArgs(doc, operationId, args, invokeOptions) {
|
|
752
|
+
let out = normalizeColors(args);
|
|
753
|
+
if (operationId === 'doc.styles.create')
|
|
754
|
+
out = await keepUnsentStyleProperties(doc, out, invokeOptions);
|
|
755
|
+
if ((operationId === 'doc.create.image' || operationId === 'doc.images.replaceSource') &&
|
|
756
|
+
typeof out.src === 'string') {
|
|
757
|
+
out = { ...out, src: await toDataUri(out.src, documentDir(doc)) };
|
|
758
|
+
}
|
|
759
|
+
const source = out.source;
|
|
760
|
+
if (source && source.kind === 'path' && typeof source.path === 'string') {
|
|
761
|
+
out = { ...out, source: { ...source, path: resolveDocumentRelativePath(source.path, documentDir(doc)) } };
|
|
762
|
+
}
|
|
763
|
+
if (typeof out.path === 'string' && /\.(docx|png|jpe?g|gif|bmp|svg)$/i.test(out.path)) {
|
|
764
|
+
out = { ...out, path: resolveDocumentRelativePath(out.path, documentDir(doc)) };
|
|
765
|
+
}
|
|
766
|
+
return out;
|
|
767
|
+
}
|
|
768
|
+
function prepareCall(call) {
|
|
769
|
+
const operationId = normalizeId(call.operationId);
|
|
770
|
+
const op = lookupOp(operationId);
|
|
771
|
+
if (!op) {
|
|
772
|
+
throw new SuperDocCliError(`Unknown or non-callable operation: ${operationId}. Use superdoc_api_search to find the exact id.`, {
|
|
773
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
774
|
+
details: { operationId },
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
const args = { ...call.args };
|
|
778
|
+
for (const key of Object.keys(args))
|
|
779
|
+
if (RESERVED_ARG_KEYS.has(key))
|
|
780
|
+
delete args[key];
|
|
781
|
+
const paramNames = new Set(op.params.map((p) => p.name));
|
|
782
|
+
if (call.changeMode !== undefined) {
|
|
783
|
+
if (!paramNames.has('changeMode')) {
|
|
784
|
+
throw new SuperDocCliError(`${operationId} has no changeMode; it always applies directly.`, {
|
|
785
|
+
code: 'INVALID_ARGUMENT',
|
|
786
|
+
details: { operationId },
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
args.changeMode = call.changeMode;
|
|
790
|
+
}
|
|
791
|
+
if (call.dryRun !== undefined) {
|
|
792
|
+
if (!paramNames.has('dryRun') || op.supportsDryRun === false) {
|
|
793
|
+
throw new SuperDocCliError(`${operationId} does not support dryRun; it would apply for real. Call without dryRun when you mean it.`, {
|
|
794
|
+
code: 'INVALID_ARGUMENT',
|
|
795
|
+
details: { operationId, supportsDryRun: false },
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
args.dryRun = call.dryRun;
|
|
799
|
+
}
|
|
800
|
+
if (args.changeMode === 'tracked' && op.mutates === true && !operationAcceptsTrackedMode(op)) {
|
|
801
|
+
throw new SuperDocCliError(`${operationId} does not support tracked mode; call it without changeMode or pick an operation that does.`, {
|
|
802
|
+
code: 'INVALID_ARGUMENT',
|
|
803
|
+
details: { operationId, supportsTrackedMode: false },
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
const unknown = Object.keys(args).filter((k) => !paramNames.has(k));
|
|
807
|
+
if (unknown.length) {
|
|
808
|
+
throw new SuperDocCliError(`${operationId} does not accept: ${unknown.join(', ')}. Accepted: ${visibleParams(op)
|
|
809
|
+
.map((p) => p.name)
|
|
810
|
+
.join(', ')}.`, {
|
|
811
|
+
code: 'INVALID_ARGUMENT',
|
|
812
|
+
details: { operationId, unknown },
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
const missing = visibleParams(op)
|
|
816
|
+
.filter((p) => p.required && args[p.name] === undefined)
|
|
817
|
+
.map((p) => p.name);
|
|
818
|
+
if (missing.length) {
|
|
819
|
+
throw new SuperDocCliError(`${operationId} requires: ${missing.join(', ')}.`, {
|
|
820
|
+
code: 'INVALID_ARGUMENT',
|
|
821
|
+
details: { operationId, missing },
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
return { op, operationId, args };
|
|
825
|
+
}
|
|
826
|
+
function shapeResult(operationId, result) {
|
|
827
|
+
if (OPAQUE_RESULT_OPS.has(operationId))
|
|
828
|
+
return result;
|
|
829
|
+
const text = JSON.stringify(result);
|
|
830
|
+
if (text === undefined || text.length <= RESULT_CHAR_CAP)
|
|
831
|
+
return result;
|
|
832
|
+
return { truncated: true, chars: text.length, preview: text.slice(0, RESULT_CHAR_CAP) };
|
|
833
|
+
}
|
|
834
|
+
async function invokeCall(doc, call, invokeOptions) {
|
|
835
|
+
const { operationId, args } = prepareCall(call);
|
|
836
|
+
const enforcement = trackedEnforcementOf(invokeOptions);
|
|
837
|
+
const governed = enforceTrackedChangeMode(operationId, args, enforcement);
|
|
838
|
+
const prepared = await preprocessArgs(doc, operationId, governed.args, invokeOptions);
|
|
839
|
+
const result = shapeResult(operationId, await resolveMethod(doc, operationId)(prepared, forwardOptions(invokeOptions)));
|
|
840
|
+
const receipt = changeModeReceipt(operationId, governed, enforcement);
|
|
841
|
+
return isRecord(result) && receipt.changeMode !== undefined ? { ...result, ...receipt } : result;
|
|
842
|
+
}
|
|
843
|
+
async function runOperation(doc, raw, invokeOptions) {
|
|
844
|
+
if (Array.isArray(raw.calls) && raw.calls.length > 0) {
|
|
845
|
+
if (raw.calls.length > 40) {
|
|
846
|
+
throw new SuperDocCliError('superdoc_api_call batch is capped at 40 calls.', { code: 'INVALID_ARGUMENT' });
|
|
847
|
+
}
|
|
848
|
+
const batchLevel = ['dryRun', 'changeMode'].filter((key) => raw[key] !== undefined);
|
|
849
|
+
if (batchLevel.length) {
|
|
850
|
+
throw new SuperDocCliError(`superdoc_api_call applies ${batchLevel.join(' and ')} per call, not to a batch. Nothing was run. Put ${batchLevel.join(' and ')} on each entry of calls.`, { code: 'INVALID_ARGUMENT', details: { batchLevel } });
|
|
851
|
+
}
|
|
852
|
+
const prepared = raw.calls.map((c, i) => {
|
|
853
|
+
try {
|
|
854
|
+
return prepareCall(c);
|
|
855
|
+
}
|
|
856
|
+
catch (e) {
|
|
857
|
+
throw new SuperDocCliError(`calls[${i}]: ${e.message}`, {
|
|
858
|
+
code: 'INVALID_ARGUMENT',
|
|
859
|
+
details: { index: i },
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
const enforcement = trackedEnforcementOf(invokeOptions);
|
|
864
|
+
const governed = prepared.map(({ operationId, args }, i) => {
|
|
865
|
+
try {
|
|
866
|
+
return enforceTrackedChangeMode(operationId, args, enforcement);
|
|
867
|
+
}
|
|
868
|
+
catch (e) {
|
|
869
|
+
const refusal = e;
|
|
870
|
+
throw new SuperDocCliError(`calls[${i}]: ${refusal.message}`, {
|
|
871
|
+
code: refusal.code,
|
|
872
|
+
details: { index: i, ...(isRecord(refusal.details) ? refusal.details : {}) },
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
const receipts = [];
|
|
877
|
+
for (let i = 0; i < prepared.length; i++) {
|
|
878
|
+
const { operationId } = prepared[i];
|
|
879
|
+
try {
|
|
880
|
+
const args2 = await preprocessArgs(doc, operationId, governed[i].args, invokeOptions);
|
|
881
|
+
const result = await resolveMethod(doc, operationId)(args2, forwardOptions(invokeOptions));
|
|
882
|
+
receipts.push({
|
|
883
|
+
index: i,
|
|
884
|
+
operationId,
|
|
885
|
+
ok: !isFailureReceipt(result),
|
|
886
|
+
...changeModeReceipt(operationId, governed[i], enforcement),
|
|
887
|
+
result: shapeResult(operationId, result),
|
|
888
|
+
});
|
|
889
|
+
if (isFailureReceipt(result)) {
|
|
890
|
+
return {
|
|
891
|
+
completed: i,
|
|
892
|
+
total: prepared.length,
|
|
893
|
+
stoppedAt: i,
|
|
894
|
+
reason: 'operation reported success:false',
|
|
895
|
+
receipts,
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
catch (e) {
|
|
900
|
+
receipts.push({ index: i, operationId, ok: false, error: e.message });
|
|
901
|
+
return { completed: i, total: prepared.length, stoppedAt: i, reason: 'operation threw', receipts };
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return { completed: prepared.length, total: prepared.length, receipts };
|
|
905
|
+
}
|
|
906
|
+
if (typeof raw.operationId !== 'string' || raw.operationId.trim().length === 0) {
|
|
907
|
+
throw new SuperDocCliError('superdoc_api_call needs operationId (or calls:[…] for a batch).', {
|
|
908
|
+
code: 'INVALID_ARGUMENT',
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
return invokeCall(doc, raw, invokeOptions);
|
|
912
|
+
}
|
|
913
|
+
async function listBlocks(doc, args, invokeOptions) {
|
|
914
|
+
const res = (await resolveMethod(doc, 'doc.blocks.list')(args, forwardOptions(invokeOptions)));
|
|
915
|
+
return res.blocks ?? [];
|
|
916
|
+
}
|
|
917
|
+
const ANCHOR_PAGE_SIZE = 50;
|
|
918
|
+
const AMBIGUOUS_CANDIDATES_SHOWN = 6;
|
|
919
|
+
async function listAnchorMatches(doc, anchorText, wanted, invokeOptions) {
|
|
920
|
+
const list = resolveMethod(doc, 'doc.blocks.list');
|
|
921
|
+
const matches = [];
|
|
922
|
+
const limit = Math.max(6, Math.min(wanted, ANCHOR_PAGE_SIZE));
|
|
923
|
+
for (let offset = 0; matches.length < wanted;) {
|
|
924
|
+
const page = (await list({ textSearch: { terms: [anchorText], match: 'all' }, includeText: true, offset, limit }, forwardOptions(invokeOptions)));
|
|
925
|
+
const rows = page.blocks ?? [];
|
|
926
|
+
matches.push(...rows);
|
|
927
|
+
offset += rows.length;
|
|
928
|
+
if (rows.length === 0 || typeof page.total !== 'number' || offset >= page.total)
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
return matches;
|
|
932
|
+
}
|
|
933
|
+
async function findBlock(doc, args, invokeOptions) {
|
|
934
|
+
if (args.nodeId) {
|
|
935
|
+
const [block] = await listBlocks(doc, { nodeIds: [args.nodeId], limit: 1, includeText: true }, invokeOptions);
|
|
936
|
+
if (block)
|
|
937
|
+
return block;
|
|
938
|
+
throw new SuperDocCliError(`No block with nodeId ${args.nodeId}.`, {
|
|
939
|
+
code: 'INVALID_ARGUMENT',
|
|
940
|
+
details: { nodeId: args.nodeId },
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
if (args.anchorText && args.anchorText.trim()) {
|
|
944
|
+
const wanted = args.occurrence != null ? Math.max(1, Math.floor(args.occurrence)) : Number.POSITIVE_INFINITY;
|
|
945
|
+
const matches = await listAnchorMatches(doc, args.anchorText, wanted, invokeOptions);
|
|
946
|
+
if (matches.length === 0) {
|
|
947
|
+
throw new SuperDocCliError(`No block contains "${args.anchorText}".`, {
|
|
948
|
+
code: 'INVALID_ARGUMENT',
|
|
949
|
+
details: { anchorText: args.anchorText },
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
if (args.occurrence != null) {
|
|
953
|
+
const block = matches[Math.max(0, Math.floor(args.occurrence) - 1)];
|
|
954
|
+
if (block)
|
|
955
|
+
return block;
|
|
956
|
+
throw new SuperDocCliError(`Only ${matches.length} block(s) contain "${args.anchorText}"; occurrence ${args.occurrence} does not exist.`, {
|
|
957
|
+
code: 'INVALID_ARGUMENT',
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
if (matches.length > 1) {
|
|
961
|
+
const textOf = (m) => m.text ?? m.textPreview ?? '';
|
|
962
|
+
const exact = matches.filter((m) => textOf(m).trim() === args.anchorText.trim());
|
|
963
|
+
if (exact.length === 1)
|
|
964
|
+
return exact[0];
|
|
965
|
+
const caseSensitive = matches.filter((m) => textOf(m).includes(args.anchorText));
|
|
966
|
+
if (caseSensitive.length === 1)
|
|
967
|
+
return caseSensitive[0];
|
|
968
|
+
throw new SuperDocCliError(`"${args.anchorText}" matches ${matches.length} blocks; pass occurrence (1-based) or nodeId. Candidates: ${matches
|
|
969
|
+
.slice(0, AMBIGUOUS_CANDIDATES_SHOWN)
|
|
970
|
+
.map((m) => `${m.nodeId} "${(m.textPreview ?? m.text ?? '').slice(0, 50)}"`)
|
|
971
|
+
.join(' | ')}`, {
|
|
972
|
+
code: 'INVALID_ARGUMENT',
|
|
973
|
+
details: { matchCount: matches.length, candidates: matches.slice(0, ANCHOR_PAGE_SIZE).map((m) => m.nodeId) },
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
return matches[0];
|
|
977
|
+
}
|
|
978
|
+
throw new SuperDocCliError('Need anchorText or nodeId to locate the block.', { code: 'INVALID_ARGUMENT' });
|
|
979
|
+
}
|
|
980
|
+
async function runInsertImage(doc, args, invokeOptions) {
|
|
981
|
+
if (typeof args.src !== 'string' || args.src.length === 0) {
|
|
982
|
+
throw new SuperDocCliError('insert_image requires src (file path, URL, or data URI).', {
|
|
983
|
+
code: 'INVALID_ARGUMENT',
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
const opts = forwardOptions(invokeOptions);
|
|
987
|
+
const placement = args.placement ?? 'document_end';
|
|
988
|
+
let at = { kind: placement === 'document_start' ? 'documentStart' : 'documentEnd' };
|
|
989
|
+
if (placement === 'after' || placement === 'before') {
|
|
990
|
+
const block = await findBlock(doc, args, invokeOptions);
|
|
991
|
+
at = { kind: placement, target: { kind: 'block', nodeType: block.nodeType, nodeId: block.nodeId } };
|
|
992
|
+
}
|
|
993
|
+
const before = new Set((await listBlocks(doc, {}, invokeOptions)).map((b) => b.nodeId));
|
|
994
|
+
const imageArgs = { src: await toDataUri(args.src, documentDir(doc)), at };
|
|
995
|
+
if (args.alt)
|
|
996
|
+
imageArgs.alt = args.alt;
|
|
997
|
+
if (args.width || args.height)
|
|
998
|
+
imageArgs.size = { ...(args.width ? { width: args.width } : {}), ...(args.height ? { height: args.height } : {}) };
|
|
999
|
+
if (args.changeMode)
|
|
1000
|
+
imageArgs.changeMode = args.changeMode;
|
|
1001
|
+
const imageResult = (await resolveMethod(doc, 'doc.create.image')(imageArgs, opts));
|
|
1002
|
+
if (isFailureReceipt(imageResult))
|
|
1003
|
+
return imageResult;
|
|
1004
|
+
if (!args.caption)
|
|
1005
|
+
return { success: true, image: imageResult.image };
|
|
1006
|
+
const after = await listBlocks(doc, {}, invokeOptions);
|
|
1007
|
+
const imageBlock = after.find((b) => !before.has(b.nodeId)) ?? (placement === 'document_start' ? after[0] : after[after.length - 1]);
|
|
1008
|
+
const captionArgs = {
|
|
1009
|
+
text: args.caption,
|
|
1010
|
+
at: { kind: 'after', target: { kind: 'block', nodeType: imageBlock.nodeType, nodeId: imageBlock.nodeId } },
|
|
1011
|
+
};
|
|
1012
|
+
if (args.changeMode)
|
|
1013
|
+
captionArgs.changeMode = args.changeMode;
|
|
1014
|
+
try {
|
|
1015
|
+
const captionResult = await resolveMethod(doc, 'doc.create.paragraph')(captionArgs, opts);
|
|
1016
|
+
if (isFailureReceipt(captionResult)) {
|
|
1017
|
+
return {
|
|
1018
|
+
success: false,
|
|
1019
|
+
image: imageResult.image,
|
|
1020
|
+
imageInserted: true,
|
|
1021
|
+
caption: 'failed',
|
|
1022
|
+
captionFailure: captionResult,
|
|
1023
|
+
hint: 'The picture IS in the document — do not insert it again. Add the caption with doc.create.paragraph after the image block.',
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
return { success: true, image: imageResult.image, imageBlockId: imageBlock.nodeId, caption: 'inserted' };
|
|
1027
|
+
}
|
|
1028
|
+
catch (e) {
|
|
1029
|
+
return {
|
|
1030
|
+
success: false,
|
|
1031
|
+
image: imageResult.image,
|
|
1032
|
+
imageInserted: true,
|
|
1033
|
+
caption: 'failed',
|
|
1034
|
+
captionError: e.message,
|
|
1035
|
+
hint: 'The picture IS in the document — do not insert it again. Add the caption with doc.create.paragraph after the image block.',
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
async function runInsertFootnote(doc, args, invokeOptions) {
|
|
1040
|
+
if (typeof args.anchorText !== 'string' || !args.anchorText.trim()) {
|
|
1041
|
+
throw new SuperDocCliError('insert_footnote requires anchorText (the exact phrase the note mark follows).', {
|
|
1042
|
+
code: 'INVALID_ARGUMENT',
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
if (typeof args.content !== 'string' || !args.content.trim()) {
|
|
1046
|
+
throw new SuperDocCliError('insert_footnote requires content (the note text).', { code: 'INVALID_ARGUMENT' });
|
|
1047
|
+
}
|
|
1048
|
+
const opts = forwardOptions(invokeOptions);
|
|
1049
|
+
const type = args.type ?? 'footnote';
|
|
1050
|
+
const block = await findBlock(doc, args, invokeOptions);
|
|
1051
|
+
const text = block.text ?? '';
|
|
1052
|
+
const start = text.indexOf(args.anchorText);
|
|
1053
|
+
if (start < 0) {
|
|
1054
|
+
throw new SuperDocCliError(`Phrase "${args.anchorText}" not found inside block ${block.nodeId} text.`, {
|
|
1055
|
+
code: 'INVALID_ARGUMENT',
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
const list = resolveMethod(doc, 'doc.footnotes.list');
|
|
1059
|
+
const countBefore = (await list({}, opts)) ?? {};
|
|
1060
|
+
const end = start + args.anchorText.length;
|
|
1061
|
+
const noteArgs = {
|
|
1062
|
+
type,
|
|
1063
|
+
content: args.content,
|
|
1064
|
+
at: { kind: 'text', segments: [{ blockId: block.nodeId, range: { start: end, end } }] },
|
|
1065
|
+
};
|
|
1066
|
+
if (args.changeMode)
|
|
1067
|
+
noteArgs.changeMode = args.changeMode;
|
|
1068
|
+
const result = (await resolveMethod(doc, 'doc.footnotes.insert')(noteArgs, opts));
|
|
1069
|
+
if (isFailureReceipt(result))
|
|
1070
|
+
return result;
|
|
1071
|
+
const countAfter = (await list({}, opts)) ?? {};
|
|
1072
|
+
const n = (x) => x.total ?? x.items?.length ?? x.footnotes?.length;
|
|
1073
|
+
return {
|
|
1074
|
+
success: true,
|
|
1075
|
+
type,
|
|
1076
|
+
anchoredOn: `${block.nodeId}[${start}..${end}]`,
|
|
1077
|
+
note: result.footnote ?? result,
|
|
1078
|
+
notesBefore: n(countBefore),
|
|
1079
|
+
notesAfter: n(countAfter),
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
async function runShadeCells(doc, args, invokeOptions) {
|
|
1083
|
+
if (typeof args.color !== 'string' || !args.color.trim()) {
|
|
1084
|
+
throw new SuperDocCliError('shade_cells requires color (hex like "D9D9D9" or a name).', {
|
|
1085
|
+
code: 'INVALID_ARGUMENT',
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
const opts = forwardOptions(invokeOptions);
|
|
1089
|
+
let tableNodeId = args.tableNodeId;
|
|
1090
|
+
if (!tableNodeId) {
|
|
1091
|
+
const tables = await listBlocks(doc, { nodeTypes: ['table'] }, invokeOptions);
|
|
1092
|
+
const ordinal = Math.max(1, Math.floor(args.tableOrdinal ?? 1));
|
|
1093
|
+
const table = tables[ordinal - 1];
|
|
1094
|
+
if (!table) {
|
|
1095
|
+
throw new SuperDocCliError(`Document has ${tables.length} table(s); tableOrdinal ${ordinal} does not exist.`, {
|
|
1096
|
+
code: 'INVALID_ARGUMENT',
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
tableNodeId = table.nodeId;
|
|
1100
|
+
}
|
|
1101
|
+
const cellsArgs = { target: { kind: 'block', nodeType: 'table', nodeId: tableNodeId } };
|
|
1102
|
+
if (args.rowIndex != null)
|
|
1103
|
+
cellsArgs.rowIndex = args.rowIndex;
|
|
1104
|
+
if (args.columnIndex != null)
|
|
1105
|
+
cellsArgs.columnIndex = args.columnIndex;
|
|
1106
|
+
const cellsResult = (await resolveMethod(doc, 'doc.tables.getCells')(cellsArgs, opts));
|
|
1107
|
+
const cells = (cellsResult.cells ?? []).filter((c) => (args.rowIndex == null || c.rowIndex === args.rowIndex) &&
|
|
1108
|
+
(args.columnIndex == null || c.columnIndex === args.columnIndex));
|
|
1109
|
+
if (cells.length === 0) {
|
|
1110
|
+
throw new SuperDocCliError('No cells matched the requested row/column.', {
|
|
1111
|
+
code: 'INVALID_ARGUMENT',
|
|
1112
|
+
details: { tableNodeId, rowIndex: args.rowIndex, columnIndex: args.columnIndex },
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
const setShading = resolveMethod(doc, 'doc.tables.setShading');
|
|
1116
|
+
const shaded = [];
|
|
1117
|
+
const failures = [];
|
|
1118
|
+
for (const cell of cells) {
|
|
1119
|
+
try {
|
|
1120
|
+
const r = await setShading({ target: { kind: 'block', nodeType: 'tableCell', nodeId: cell.nodeId }, color: args.color }, opts);
|
|
1121
|
+
if (isFailureReceipt(r))
|
|
1122
|
+
failures.push({
|
|
1123
|
+
nodeId: cell.nodeId,
|
|
1124
|
+
error: JSON.stringify(r.failure).slice(0, 160),
|
|
1125
|
+
});
|
|
1126
|
+
else
|
|
1127
|
+
shaded.push(cell.nodeId);
|
|
1128
|
+
}
|
|
1129
|
+
catch (e) {
|
|
1130
|
+
failures.push({ nodeId: cell.nodeId, error: e.message.slice(0, 160) });
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
success: failures.length === 0,
|
|
1135
|
+
tableNodeId,
|
|
1136
|
+
color: args.color,
|
|
1137
|
+
cellsShaded: shaded.length,
|
|
1138
|
+
cellsTotal: cells.length,
|
|
1139
|
+
failures,
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
const COMPONENTS_APPLIED_DIRECTLY = new Set(['comments', 'styles', 'numbering', 'parts']);
|
|
1143
|
+
function allowsDirectHelper(options, action) {
|
|
1144
|
+
const allowDirect = isRecord(options) ? options.allowDirectActions : undefined;
|
|
1145
|
+
return Array.isArray(allowDirect) && (allowDirect.includes(ALLOW_ALL_DIRECT_ACTIONS) || allowDirect.includes(action));
|
|
1146
|
+
}
|
|
1147
|
+
async function runCompareWithFile(doc, args, invokeOptions) {
|
|
1148
|
+
if (typeof args.path === 'string')
|
|
1149
|
+
args = { ...args, path: resolveDocumentRelativePath(args.path, documentDir(doc)) };
|
|
1150
|
+
if (typeof args.path !== 'string' || !existsSync(args.path)) {
|
|
1151
|
+
throw new SuperDocCliError(`Comparison file not found: ${args.path}`, {
|
|
1152
|
+
code: 'INVALID_ARGUMENT',
|
|
1153
|
+
details: { path: args.path },
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
const opts = forwardOptions(invokeOptions);
|
|
1157
|
+
const client = doc.client;
|
|
1158
|
+
if (!client || typeof client.open !== 'function') {
|
|
1159
|
+
throw new SuperDocCliError('compare_with_file needs a handle from client.open() (no client reference on this handle).', {
|
|
1160
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
const other = await client.open({ doc: args.path });
|
|
1164
|
+
let snapshot;
|
|
1165
|
+
try {
|
|
1166
|
+
snapshot = await resolveMethod(other, 'doc.diff.capture')({}, opts);
|
|
1167
|
+
}
|
|
1168
|
+
finally {
|
|
1169
|
+
try {
|
|
1170
|
+
await other.close();
|
|
1171
|
+
}
|
|
1172
|
+
catch { }
|
|
1173
|
+
}
|
|
1174
|
+
const changeMode = args.changeMode ?? 'tracked';
|
|
1175
|
+
const styleOptions = changeMode === 'tracked' &&
|
|
1176
|
+
(!trackedEnforcementOf(invokeOptions) || allowsDirectHelper(invokeOptions, 'compare_with_file'))
|
|
1177
|
+
? { styleChangeMode: 'direct' }
|
|
1178
|
+
: {};
|
|
1179
|
+
const diff = (await resolveMethod(doc, 'doc.diff.compare')({ targetSnapshot: snapshot, ...styleOptions }, opts));
|
|
1180
|
+
if (diff?.summary?.hasChanges === false) {
|
|
1181
|
+
return {
|
|
1182
|
+
changeMode: null,
|
|
1183
|
+
applied: false,
|
|
1184
|
+
reason: 'The two documents have no differences in the compared components.',
|
|
1185
|
+
summary: diff.summary,
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
const directComponents = (diff?.summary?.changedComponents ?? []).filter((component) => COMPONENTS_APPLIED_DIRECTLY.has(component));
|
|
1189
|
+
if (directComponents.length &&
|
|
1190
|
+
trackedEnforcementOf(invokeOptions) &&
|
|
1191
|
+
!allowsDirectHelper(invokeOptions, 'compare_with_file')) {
|
|
1192
|
+
throw new SuperDocCliError(`compare_with_file would apply the ${directComponents.join(', ')} differences directly, and enforceChangeMode: "tracked" refuses direct edits. Nothing was changed. Have the host permit it with allowDirectActions: ["compare_with_file"], or carry over only the text differences by hand.`, {
|
|
1193
|
+
code: 'INVALID_ARGUMENT',
|
|
1194
|
+
details: {
|
|
1195
|
+
toolName: 'superdoc_perform_action',
|
|
1196
|
+
action: 'compare_with_file',
|
|
1197
|
+
directComponents,
|
|
1198
|
+
reason: 'enforce-change-mode',
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
const eligibility = diff?.applyEligibility?.[changeMode];
|
|
1203
|
+
if (eligibility?.status === 'blocked') {
|
|
1204
|
+
return {
|
|
1205
|
+
applied: false,
|
|
1206
|
+
changeMode,
|
|
1207
|
+
reason: `The diff cannot be applied in ${changeMode} mode.`,
|
|
1208
|
+
blockers: eligibility.blockers,
|
|
1209
|
+
summary: diff.summary,
|
|
1210
|
+
hint: changeMode === 'tracked'
|
|
1211
|
+
? 'Retry with changeMode:"direct" if a plain merge is acceptable, or apply the blocked parts by hand.'
|
|
1212
|
+
: undefined,
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
const applied = (await resolveMethod(doc, 'doc.diff.apply')({ diff, changeMode, ...styleOptions }, opts));
|
|
1216
|
+
const receipts = Array.isArray(applied?.operationReceipts)
|
|
1217
|
+
? applied.operationReceipts
|
|
1218
|
+
: [];
|
|
1219
|
+
return {
|
|
1220
|
+
applied: true,
|
|
1221
|
+
changeMode,
|
|
1222
|
+
changedComponents: diff?.summary?.changedComponents,
|
|
1223
|
+
appliedOperations: applied?.appliedOperations,
|
|
1224
|
+
reviewCreated: receipts.filter((r) => r.disposition === 'review-created').length,
|
|
1225
|
+
appliedDirectly: receipts.filter((r) => r.disposition === 'applied-directly').length,
|
|
1226
|
+
note: 'Comments, styles and numbering differences apply directly even in tracked mode; body and header/footer text differences become tracked changes.',
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
const HELPER_ARG_SCHEMA = {
|
|
1230
|
+
src: {
|
|
1231
|
+
type: 'string',
|
|
1232
|
+
description: 'Image: file path (absolute or relative to the working folder), http(s) URL, or data:image/... URI.',
|
|
1233
|
+
},
|
|
1234
|
+
caption: { type: 'string', description: 'Caption paragraph inserted right after the image.' },
|
|
1235
|
+
alt: { type: 'string' },
|
|
1236
|
+
width: { type: 'number', description: 'Pixels.' },
|
|
1237
|
+
height: { type: 'number', description: 'Pixels.' },
|
|
1238
|
+
occurrence: {
|
|
1239
|
+
type: 'integer',
|
|
1240
|
+
minimum: 1,
|
|
1241
|
+
description: '1-based pick when anchorText matches several blocks (1 = first mention).',
|
|
1242
|
+
},
|
|
1243
|
+
content: { type: 'string', description: 'Footnote / endnote text.' },
|
|
1244
|
+
noteType: { type: 'string', enum: ['footnote', 'endnote'] },
|
|
1245
|
+
path: { type: 'string', description: 'Path to the other .docx to compare against.' },
|
|
1246
|
+
};
|
|
1247
|
+
const str = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
|
|
1248
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);
|
|
1249
|
+
const mode = (v) => (v === 'direct' || v === 'tracked' ? v : undefined);
|
|
1250
|
+
function imagePlacement(a) {
|
|
1251
|
+
if (a.position === 'before' || a.position === 'after')
|
|
1252
|
+
return a.position;
|
|
1253
|
+
const at = a.placement && typeof a.placement === 'object' ? a.placement.at : undefined;
|
|
1254
|
+
if (at === 'document_end' || at === 'document_start' || at === 'before' || at === 'after')
|
|
1255
|
+
return at;
|
|
1256
|
+
return str(a.anchorText) || str(a.nodeId) ? 'after' : 'document_end';
|
|
1257
|
+
}
|
|
1258
|
+
const HELPER_ACTIONS = {
|
|
1259
|
+
insert_image: {
|
|
1260
|
+
hint: 'src (file path/URL/data URI), caption?, alt?, width?, height?; place with position "after"|"before" + anchorText (or nodeId), default document end; changeMode?',
|
|
1261
|
+
args: ['src', 'caption', 'alt', 'width', 'height', 'position', 'anchorText', 'occurrence', 'nodeId', 'changeMode'],
|
|
1262
|
+
disposition: 'tracked',
|
|
1263
|
+
run: (doc, a, o) => runInsertImage(doc, {
|
|
1264
|
+
src: str(a.src) ?? '',
|
|
1265
|
+
alt: str(a.alt),
|
|
1266
|
+
caption: str(a.caption),
|
|
1267
|
+
width: num(a.width),
|
|
1268
|
+
height: num(a.height),
|
|
1269
|
+
occurrence: num(a.occurrence),
|
|
1270
|
+
anchorText: str(a.anchorText),
|
|
1271
|
+
nodeId: str(a.nodeId),
|
|
1272
|
+
changeMode: mode(a.changeMode),
|
|
1273
|
+
placement: imagePlacement(a),
|
|
1274
|
+
}, o),
|
|
1275
|
+
},
|
|
1276
|
+
insert_footnote: {
|
|
1277
|
+
hint: 'anchorText (exact phrase the note mark follows; unique, or occurrence/nodeId), content, noteType? footnote|endnote, changeMode?',
|
|
1278
|
+
args: ['anchorText', 'occurrence', 'nodeId', 'content', 'noteType', 'changeMode'],
|
|
1279
|
+
disposition: 'tracked',
|
|
1280
|
+
run: (doc, a, o) => runInsertFootnote(doc, {
|
|
1281
|
+
anchorText: str(a.anchorText) ?? '',
|
|
1282
|
+
occurrence: num(a.occurrence),
|
|
1283
|
+
nodeId: str(a.nodeId),
|
|
1284
|
+
content: str(a.content) ?? '',
|
|
1285
|
+
type: a.noteType === 'endnote' ? 'endnote' : 'footnote',
|
|
1286
|
+
changeMode: mode(a.changeMode),
|
|
1287
|
+
}, o),
|
|
1288
|
+
},
|
|
1289
|
+
shade_cells: {
|
|
1290
|
+
hint: 'color (hex like "D9D9D9"), tableOrdinal? (default 1) or nodeId (table), rowIndex? (0 = header row), columnIndex?; whole table / one row / one column / one cell in one call',
|
|
1291
|
+
args: ['tableOrdinal', 'nodeId', 'rowIndex', 'columnIndex', 'color'],
|
|
1292
|
+
disposition: 'direct',
|
|
1293
|
+
run: (doc, a, o) => runShadeCells(doc, {
|
|
1294
|
+
tableOrdinal: num(a.tableOrdinal),
|
|
1295
|
+
tableNodeId: str(a.nodeId),
|
|
1296
|
+
rowIndex: num(a.rowIndex),
|
|
1297
|
+
columnIndex: num(a.columnIndex),
|
|
1298
|
+
color: str(a.color) ?? '',
|
|
1299
|
+
}, o),
|
|
1300
|
+
},
|
|
1301
|
+
compare_with_file: {
|
|
1302
|
+
hint: 'path to another .docx; its differences land HERE as tracked changes (changeMode tracked, default) or apply directly',
|
|
1303
|
+
args: ['path', 'changeMode'],
|
|
1304
|
+
disposition: 'tracked',
|
|
1305
|
+
run: (doc, a, o) => runCompareWithFile(doc, { path: str(a.path) ?? '', changeMode: mode(a.changeMode) }, o),
|
|
1306
|
+
},
|
|
1307
|
+
};
|
|
1308
|
+
const HELPER_ACTION_NAMES = Object.keys(HELPER_ACTIONS);
|
|
1309
|
+
function resolveHelperPolicy(options) {
|
|
1310
|
+
const excluded = new Set(options?.excludeActions ?? []);
|
|
1311
|
+
const allowDirect = new Set(options?.allowDirectActions ?? []);
|
|
1312
|
+
const tracked = options?.enforceChangeMode === 'tracked';
|
|
1313
|
+
const helpers = HELPER_ACTION_NAMES.filter((name) => {
|
|
1314
|
+
if (excluded.has(name))
|
|
1315
|
+
return false;
|
|
1316
|
+
if (tracked && HELPER_ACTIONS[name].disposition === 'direct')
|
|
1317
|
+
return allowDirect.has('*') || allowDirect.has(name);
|
|
1318
|
+
return true;
|
|
1319
|
+
});
|
|
1320
|
+
const coreOptions = {
|
|
1321
|
+
...options,
|
|
1322
|
+
excludeActions: [...excluded].filter((name) => !(name in HELPER_ACTIONS)),
|
|
1323
|
+
...(options?.allowDirectActions
|
|
1324
|
+
? { allowDirectActions: [...allowDirect].filter((name) => !(name in HELPER_ACTIONS)) }
|
|
1325
|
+
: {}),
|
|
1326
|
+
};
|
|
1327
|
+
return { helpers, coreOptions };
|
|
1328
|
+
}
|
|
1329
|
+
const LEAN_ACTION_DESCRIPTION = 'High-level deterministic document edit. Pick an action and pass flat product-facing arguments. Actions wrap the most common doc.* operations and return real pre/post evidence and verification. Each action, its arguments and the selector shapes are documented in the ACTIONS section of the system prompt.';
|
|
1330
|
+
function withHelperActions(provider, tool, helpers) {
|
|
1331
|
+
if (helpers.length === 0 && !opt('leanschema'))
|
|
1332
|
+
return tool;
|
|
1333
|
+
const outer = tool;
|
|
1334
|
+
const fn = (provider === 'openai' ? outer.function : outer);
|
|
1335
|
+
const schemaKey = provider === 'anthropic' ? 'input_schema' : provider === 'vercel' ? 'inputSchema' : 'parameters';
|
|
1336
|
+
const schema = fn[schemaKey];
|
|
1337
|
+
const action = schema.properties.action;
|
|
1338
|
+
const properties = { action: { ...action, enum: [...action.enum, ...helpers] } };
|
|
1339
|
+
for (const [key, value] of Object.entries(schema.properties))
|
|
1340
|
+
if (key !== 'action')
|
|
1341
|
+
properties[key] = value;
|
|
1342
|
+
for (const name of helpers) {
|
|
1343
|
+
for (const arg of HELPER_ACTIONS[name].args) {
|
|
1344
|
+
if (arg in properties)
|
|
1345
|
+
continue;
|
|
1346
|
+
const argSchema = HELPER_ARG_SCHEMA[arg] ?? ACTION_ARG_SCHEMA[arg];
|
|
1347
|
+
if (!argSchema)
|
|
1348
|
+
throw new Error(`full preset: helper action ${name} declares arg "${arg}" with no schema.`);
|
|
1349
|
+
properties[arg] = argSchema;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const base = opt('leanschema') ? LEAN_ACTION_DESCRIPTION : fn.description;
|
|
1353
|
+
const description = helpers.length
|
|
1354
|
+
? `${base} Full-preset helpers: ${helpers.map((name) => `${name} (${HELPER_ACTIONS[name].hint})`).join(', ')}.`
|
|
1355
|
+
: base;
|
|
1356
|
+
const patched = { ...fn, description, [schemaKey]: { ...schema, properties } };
|
|
1357
|
+
return provider === 'openai' ? { ...outer, function: patched } : patched;
|
|
1358
|
+
}
|
|
1359
|
+
const OBJ = (properties, required = []) => ({
|
|
1360
|
+
type: 'object',
|
|
1361
|
+
additionalProperties: false,
|
|
1362
|
+
...(required.length ? { required } : {}),
|
|
1363
|
+
properties,
|
|
1364
|
+
});
|
|
1365
|
+
const CALL_SCHEMA = OBJ({
|
|
1366
|
+
operationId: { type: 'string', description: 'Exact operation id, e.g. "doc.tables.setShading".' },
|
|
1367
|
+
args: {
|
|
1368
|
+
type: 'object',
|
|
1369
|
+
additionalProperties: true,
|
|
1370
|
+
properties: {},
|
|
1371
|
+
description: 'Operation parameters as documented by superdoc_api_search.',
|
|
1372
|
+
},
|
|
1373
|
+
changeMode: {
|
|
1374
|
+
type: 'string',
|
|
1375
|
+
enum: ['direct', 'tracked'],
|
|
1376
|
+
description: 'Only for operations whose catalog entry says supportsTrackedMode.',
|
|
1377
|
+
},
|
|
1378
|
+
dryRun: { type: 'boolean', description: 'Only for operations whose catalog entry says supportsDryRun.' },
|
|
1379
|
+
}, ['operationId']);
|
|
1380
|
+
export const FULL_TOOL_DEFINITIONS = [
|
|
1381
|
+
{
|
|
1382
|
+
name: 'superdoc_api_search',
|
|
1383
|
+
description: `Search the complete SuperDoc Document API catalog (${CATALOG_SIZE} operations: footnotes, images, table shading/borders, sections & page layout, headers/footers, TOC, fields, bookmarks, hyperlinks, content controls, comments, tracked changes, diff …). Use only when neither a superdoc_perform_action verb nor an operation under QUICK REFERENCE in the system prompt fits. {query} keyword search or {domain} browse returns operation ids with one-line descriptions; {operationId} returns the full parameter schema for one operation. Read-only.`,
|
|
1384
|
+
inputSchema: OBJ({
|
|
1385
|
+
query: {
|
|
1386
|
+
type: 'string',
|
|
1387
|
+
description: 'Keywords, e.g. "footnote insert", "cell shading", "page margins", "header text".',
|
|
1388
|
+
},
|
|
1389
|
+
domain: {
|
|
1390
|
+
type: 'string',
|
|
1391
|
+
description: 'Browse one namespace: tables, lists, format, sections, headerFooters, footnotes, images, toc, fields, bookmarks, hyperlinks, comments, trackChanges, contentControls, styles, blocks, diff, create, document (root ops like insert/replace/delete/getMarkdown).',
|
|
1392
|
+
},
|
|
1393
|
+
operationId: { type: 'string', description: 'Exact id (e.g. "doc.footnotes.insert") to get its parameters.' },
|
|
1394
|
+
operationIds: {
|
|
1395
|
+
type: 'array',
|
|
1396
|
+
items: { type: 'string' },
|
|
1397
|
+
description: 'Up to 8 exact ids to describe in ONE call when a recipe needs several operations.',
|
|
1398
|
+
},
|
|
1399
|
+
mutatingOnly: { type: 'boolean' },
|
|
1400
|
+
limit: { type: 'integer', minimum: 1, maximum: 24 },
|
|
1401
|
+
}),
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
name: 'superdoc_api_call',
|
|
1405
|
+
description: 'Invoke ANY Document API operation by id with its native arguments — the general-purpose edit surface behind superdoc_perform_action. Operations under QUICK REFERENCE in the system prompt are called directly; look any other up with superdoc_api_search first (exact params, mutates?, supportsTrackedMode?, supportsDryRun?). Pass ONE call as {operationId, args} or SEVERAL as {calls:[{operationId,args},…]} — a batch runs in order in one round trip and stops at the first failure, returning per-call receipts. Block targets: {kind:"block", nodeType, nodeId} (nodeIds from superdoc_inspect). Positions: {kind:"documentEnd"|"documentStart"} or {kind:"before"|"after", target:<block>}. Text spans: {kind:"text", segments:[{blockId, range:{start,end}}]}. Image src may be a file path, URL, or data URI. Never pass doc or sessionId.',
|
|
1406
|
+
inputSchema: OBJ({
|
|
1407
|
+
operationId: { type: 'string', description: 'Exact operation id for a single call.' },
|
|
1408
|
+
args: {
|
|
1409
|
+
type: 'object',
|
|
1410
|
+
additionalProperties: true,
|
|
1411
|
+
properties: {},
|
|
1412
|
+
description: 'Parameters for the single call.',
|
|
1413
|
+
},
|
|
1414
|
+
changeMode: { type: 'string', enum: ['direct', 'tracked'] },
|
|
1415
|
+
dryRun: { type: 'boolean' },
|
|
1416
|
+
calls: {
|
|
1417
|
+
type: 'array',
|
|
1418
|
+
minItems: 1,
|
|
1419
|
+
maxItems: 40,
|
|
1420
|
+
items: CALL_SCHEMA,
|
|
1421
|
+
description: 'Batch: several operations executed in order in one round trip.',
|
|
1422
|
+
},
|
|
1423
|
+
}),
|
|
1424
|
+
},
|
|
1425
|
+
];
|
|
1426
|
+
const EXECUTE_CODE_DEFINITION = AGENT_TOOL_DEFINITIONS.find((d) => d.name === 'superdoc_execute_code');
|
|
1427
|
+
if (!EXECUTE_CODE_DEFINITION)
|
|
1428
|
+
throw new Error('full preset: superdoc_execute_code definition missing from agent catalog');
|
|
1429
|
+
const FULL_ONLY_TOOL_NAMES = new Set(FULL_TOOL_DEFINITIONS.map((d) => d.name));
|
|
1430
|
+
function extraDefinitions(includeExec) {
|
|
1431
|
+
return includeExec
|
|
1432
|
+
? [...FULL_TOOL_DEFINITIONS, EXECUTE_CODE_DEFINITION]
|
|
1433
|
+
: [...FULL_TOOL_DEFINITIONS];
|
|
1434
|
+
}
|
|
1435
|
+
function toProviderTool(provider, def) {
|
|
1436
|
+
if (provider === 'anthropic')
|
|
1437
|
+
return { name: def.name, description: def.description, input_schema: def.inputSchema };
|
|
1438
|
+
if (provider === 'generic')
|
|
1439
|
+
return { name: def.name, description: def.description, parameters: def.inputSchema };
|
|
1440
|
+
if (provider === 'vercel')
|
|
1441
|
+
return { name: def.name, description: def.description, inputSchema: def.inputSchema };
|
|
1442
|
+
return { type: 'function', function: { name: def.name, description: def.description, parameters: def.inputSchema } };
|
|
1443
|
+
}
|
|
1444
|
+
async function fullGetTools(includeExec, provider, options) {
|
|
1445
|
+
const { helpers, coreOptions } = resolveHelperPolicy(options);
|
|
1446
|
+
const coreTools = listAgentTools(provider, coreOptions).map((tool) => {
|
|
1447
|
+
const name = provider === 'openai'
|
|
1448
|
+
? tool.function?.name
|
|
1449
|
+
: tool.name;
|
|
1450
|
+
return name === 'superdoc_perform_action' ? withHelperActions(provider, tool, helpers) : tool;
|
|
1451
|
+
});
|
|
1452
|
+
const extra = extraDefinitions(includeExec).map((d) => toProviderTool(provider, d));
|
|
1453
|
+
const tools = [...coreTools, ...extra];
|
|
1454
|
+
if (options?.cache !== true)
|
|
1455
|
+
return { tools, cacheStrategy: 'disabled' };
|
|
1456
|
+
if (provider === 'anthropic') {
|
|
1457
|
+
const last = { ...tools[tools.length - 1], cache_control: { type: 'ephemeral' } };
|
|
1458
|
+
return { tools: [...tools.slice(0, -1), last], cacheStrategy: 'explicit' };
|
|
1459
|
+
}
|
|
1460
|
+
if (provider === 'openai')
|
|
1461
|
+
return { tools, cacheStrategy: 'automatic' };
|
|
1462
|
+
return { tools, cacheStrategy: 'unsupported' };
|
|
1463
|
+
}
|
|
1464
|
+
async function fullGetCatalog(includeExec) {
|
|
1465
|
+
const core = await corePreset.getCatalog();
|
|
1466
|
+
const extra = extraDefinitions(includeExec).map((d) => ({
|
|
1467
|
+
toolName: d.name,
|
|
1468
|
+
description: d.description,
|
|
1469
|
+
inputSchema: d.inputSchema,
|
|
1470
|
+
mutates: d.name !== 'superdoc_api_search',
|
|
1471
|
+
operations: [],
|
|
1472
|
+
}));
|
|
1473
|
+
const coreTools = core.tools.map((tool) => tool.toolName === 'superdoc_perform_action'
|
|
1474
|
+
? withHelperActions('vercel', tool, resolveHelperPolicy().helpers)
|
|
1475
|
+
: tool);
|
|
1476
|
+
const tools = [...coreTools, ...extra];
|
|
1477
|
+
return {
|
|
1478
|
+
contractVersion: includeExec ? 'full-exec/v3' : 'full/v3',
|
|
1479
|
+
generatedAt: null,
|
|
1480
|
+
toolCount: tools.length,
|
|
1481
|
+
tools,
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
let _addendumCache = null;
|
|
1485
|
+
const EXEC_ADDENDUM = [
|
|
1486
|
+
'',
|
|
1487
|
+
'CODE EXECUTION (full-exec only)',
|
|
1488
|
+
'',
|
|
1489
|
+
'- superdoc_execute_code runs a JavaScript body IN-HOST against a SYNCHRONOUS `doc` (never `await` doc.* calls) — use it ONLY when a loop with per-item decisions cannot be expressed as a superdoc_api_call batch. Return a short summary. Do NOT call doc.save/close.',
|
|
1490
|
+
'- Never re-implement with superdoc_execute_code what one operation or one batch does, and never use it to bypass a refused tracked mode.',
|
|
1491
|
+
].join('\n');
|
|
1492
|
+
const HELPER_PROMPT_MENTIONS = {
|
|
1493
|
+
compare_with_file: [
|
|
1494
|
+
', compare_with_file {path} (differences land here as tracked changes)',
|
|
1495
|
+
'; compare_with_file {path} lands the text differences as tracked changes',
|
|
1496
|
+
', compare_with_file',
|
|
1497
|
+
'- Compare two documents: superdoc_perform_action {action:"compare_with_file", path:"…/other.docx"} — differences land in THIS document as tracked changes; comments/styles/numbering differences apply directly.\n',
|
|
1498
|
+
],
|
|
1499
|
+
};
|
|
1500
|
+
function withoutUnofferedHelpers(addendum, helpers) {
|
|
1501
|
+
let text = addendum;
|
|
1502
|
+
for (const [name, mentions] of Object.entries(HELPER_PROMPT_MENTIONS)) {
|
|
1503
|
+
if (helpers.includes(name))
|
|
1504
|
+
continue;
|
|
1505
|
+
for (const mention of mentions)
|
|
1506
|
+
text = text.replace(mention, '');
|
|
1507
|
+
}
|
|
1508
|
+
return text;
|
|
1509
|
+
}
|
|
1510
|
+
async function fullGetSystemPrompt(includeExec, options) {
|
|
1511
|
+
const { helpers, coreOptions } = resolveHelperPolicy(options);
|
|
1512
|
+
const base = await corePreset.getSystemPrompt(coreOptions);
|
|
1513
|
+
if (_addendumCache == null) {
|
|
1514
|
+
_addendumCache = await readPromptFile('full-addendum.md', 'Full preset addendum');
|
|
1515
|
+
}
|
|
1516
|
+
const helperNote = helpers.length
|
|
1517
|
+
? ` This preset adds the actions ${helpers.join(', ')} (see EXTENDED SURFACE).`
|
|
1518
|
+
: '';
|
|
1519
|
+
const toolLines = [
|
|
1520
|
+
` superdoc_perform_action — named edit verbs we authored, tested, and validate statically. Pass {action:"name", ...flat args}.${helperNote}`,
|
|
1521
|
+
` superdoc_api_search — search / describe the ${CATALOG_SIZE}-operation Document API when no action fits.`,
|
|
1522
|
+
' superdoc_api_call — invoke any Document API operation by id, singly or as a batch.',
|
|
1523
|
+
];
|
|
1524
|
+
if (includeExec)
|
|
1525
|
+
toolLines.push(' superdoc_execute_code — in-host JavaScript against the synchronous doc API for multi-step work.');
|
|
1526
|
+
const count = includeExec ? 'five' : 'four';
|
|
1527
|
+
const pruned = opt('leanprompt') ? withoutActionLines(base) : base;
|
|
1528
|
+
const addendum = withoutUnofferedHelpers(opt('leanaddendum') ? LEAN_ADDENDUM : _addendumCache, helpers);
|
|
1529
|
+
const rewritten = pruned
|
|
1530
|
+
.replace('TOOLS — two of them', `TOOLS — ${count} of them`)
|
|
1531
|
+
.replace(' superdoc_perform_action — named edit verbs we authored, tested, and validate statically. Pass {action:"name", ...flat args}.', toolLines.join('\n'))
|
|
1532
|
+
.replace('If no action expresses the request, say what is missing rather than faking it.', 'If no action expresses the request, escalate: superdoc_api_search → superdoc_api_call (see EXTENDED SURFACE below). Say something is impossible only after the catalog has no operation for it.');
|
|
1533
|
+
const extended = (opt('trustcalls')
|
|
1534
|
+
? addendum.replace(/- After a structural superdoc_api_call[^\n]*\n/, '- A superdoc_api_call receipt with success:true (or a batch with completed === total) IS the verification; do not spend a superdoc_inspect call to confirm what the receipt already proves.\n')
|
|
1535
|
+
: addendum).replace('{{QUICK_REFERENCE}}\n', opt('quickref') ? `\n${quickReference()}\n` : '');
|
|
1536
|
+
const rules = opt('trustreceipts') ? withReceiptRule(rewritten) : rewritten;
|
|
1537
|
+
return `${rules}\n\n${extended}${includeExec ? `\n${EXEC_ADDENDUM}` : ''}`;
|
|
1538
|
+
}
|
|
1539
|
+
const RECEIPT_RULE = '- A receipt with status "ok" IS the verification of that edit: do not call superdoc_inspect only to confirm it. Reads the task itself needs (answering a question, matching formatting) and the STALENESS rule below still apply.';
|
|
1540
|
+
function withReceiptRule(prompt) {
|
|
1541
|
+
const anchor = prompt.indexOf('- RECEIPTS ARE THE TRUTH.');
|
|
1542
|
+
if (anchor < 0)
|
|
1543
|
+
return prompt;
|
|
1544
|
+
const lineEnd = prompt.indexOf('\n', anchor);
|
|
1545
|
+
if (lineEnd < 0)
|
|
1546
|
+
return prompt;
|
|
1547
|
+
return `${prompt.slice(0, lineEnd + 1)}${RECEIPT_RULE}\n${prompt.slice(lineEnd + 1)}`;
|
|
1548
|
+
}
|
|
1549
|
+
function withoutActionLines(prompt) {
|
|
1550
|
+
const start = prompt.indexOf('ACTIONS (superdoc_perform_action with flat args)');
|
|
1551
|
+
const end = prompt.indexOf('Selector shapes:');
|
|
1552
|
+
if (start < 0 || end < 0 || end < start)
|
|
1553
|
+
return prompt;
|
|
1554
|
+
const headerEnd = prompt.indexOf('\n', prompt.indexOf('====', start + 10)) + 1;
|
|
1555
|
+
if (headerEnd <= 0 || headerEnd > end)
|
|
1556
|
+
return prompt;
|
|
1557
|
+
return (prompt.slice(0, headerEnd) +
|
|
1558
|
+
'\nEvery action, with its arguments and when to use it, is documented in the superdoc_perform_action tool description. Pass flat args; selector-taking args use the shapes below.\n\n' +
|
|
1559
|
+
prompt.slice(end));
|
|
1560
|
+
}
|
|
1561
|
+
function compactInspect(result) {
|
|
1562
|
+
if (result == null || typeof result !== 'object' || Array.isArray(result))
|
|
1563
|
+
return result;
|
|
1564
|
+
const out = {};
|
|
1565
|
+
for (const [key, value] of Object.entries(result)) {
|
|
1566
|
+
if (Array.isArray(value)) {
|
|
1567
|
+
if (value.length === 0 && key !== 'blocks')
|
|
1568
|
+
continue;
|
|
1569
|
+
if (key === 'headerFooters') {
|
|
1570
|
+
const kept = value.filter((slot) => !(slot &&
|
|
1571
|
+
typeof slot === 'object' &&
|
|
1572
|
+
Array.isArray(slot.blocks) &&
|
|
1573
|
+
slot.blocks.length === 0));
|
|
1574
|
+
if (kept.length === 0)
|
|
1575
|
+
continue;
|
|
1576
|
+
out[key] = kept;
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
out[key] = value.map((item) => compactRow(item));
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
out[key] = value;
|
|
1583
|
+
}
|
|
1584
|
+
return out;
|
|
1585
|
+
}
|
|
1586
|
+
function compactRow(row) {
|
|
1587
|
+
if (row == null || typeof row !== 'object' || Array.isArray(row))
|
|
1588
|
+
return row;
|
|
1589
|
+
const r = row;
|
|
1590
|
+
const out = {};
|
|
1591
|
+
for (const [key, value] of Object.entries(r)) {
|
|
1592
|
+
if (value === null || value === undefined)
|
|
1593
|
+
continue;
|
|
1594
|
+
if (key === 'tabCount' && value === 0)
|
|
1595
|
+
continue;
|
|
1596
|
+
if (key === 'textPreview' && typeof value === 'string' && typeof r.text === 'string') {
|
|
1597
|
+
const stem = value.endsWith('…') ? value.slice(0, -1) : value;
|
|
1598
|
+
if (r.text.startsWith(stem))
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
out[key] = Array.isArray(value) ? value.map((item) => compactRow(item)) : value;
|
|
1602
|
+
}
|
|
1603
|
+
return out;
|
|
1604
|
+
}
|
|
1605
|
+
async function runHelperAction(documentHandle, action, args, invokeOptions) {
|
|
1606
|
+
const helper = HELPER_ACTIONS[action];
|
|
1607
|
+
if (!trackedEnforcementOf(invokeOptions))
|
|
1608
|
+
return helper.run(documentHandle, args, invokeOptions);
|
|
1609
|
+
if (helper.disposition === 'direct') {
|
|
1610
|
+
if (!resolveHelperPolicy(invokeOptions).helpers.includes(action)) {
|
|
1611
|
+
throw new SuperDocCliError(`Action ${action} edits the document directly and is refused under enforceChangeMode: "tracked". Pass it in allowDirectActions to permit it.`, {
|
|
1612
|
+
code: 'INVALID_ARGUMENT',
|
|
1613
|
+
details: { toolName: 'superdoc_perform_action', action, excluded: true, reason: 'direct-only' },
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
const result = await helper.run(documentHandle, args, withoutTrackedEnforcement(invokeOptions));
|
|
1617
|
+
return isRecord(result)
|
|
1618
|
+
? { ...result, changeMode: 'direct', changeModeForced: args.changeMode === 'tracked' }
|
|
1619
|
+
: result;
|
|
1620
|
+
}
|
|
1621
|
+
const result = await helper.run(documentHandle, { ...args, changeMode: 'tracked' }, invokeOptions);
|
|
1622
|
+
return isRecord(result)
|
|
1623
|
+
? { ...result, changeMode: 'tracked', changeModeForced: args.changeMode !== 'tracked' }
|
|
1624
|
+
: result;
|
|
1625
|
+
}
|
|
1626
|
+
async function fullDispatch(includeExec, documentHandle, toolName, args, invokeOptions) {
|
|
1627
|
+
if (args == null || typeof args !== 'object' || Array.isArray(args)) {
|
|
1628
|
+
throw new SuperDocCliError(`Tool arguments for ${toolName} must be an object.`, {
|
|
1629
|
+
code: 'INVALID_ARGUMENT',
|
|
1630
|
+
details: { toolName },
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
if (!includeExec && toolName === 'superdoc_execute_code') {
|
|
1634
|
+
throw new SuperDocCliError('superdoc_execute_code is disabled in the full preset. Use superdoc_api_call (a {calls:[…]} batch covers multi-step edits).', { code: 'TOOL_DISPATCH_NOT_FOUND', details: { toolName, preset: 'full' } });
|
|
1635
|
+
}
|
|
1636
|
+
refuseOperationsOutsideTheDocument(toolName, args);
|
|
1637
|
+
if (toolName === 'superdoc_perform_action' && typeof args.action === 'string' && args.action in HELPER_ACTIONS) {
|
|
1638
|
+
const excluded = invokeOptions?.excludeActions;
|
|
1639
|
+
if (excluded?.includes(args.action)) {
|
|
1640
|
+
throw new SuperDocCliError(`Action "${args.action}" is excluded on this surface.`, {
|
|
1641
|
+
code: 'INVALID_ARGUMENT',
|
|
1642
|
+
details: { action: args.action },
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
if (args.dryRun !== undefined && args.dryRun !== false) {
|
|
1646
|
+
throw new SuperDocCliError(`Action ${args.action} has no dry run and would edit the document. Nothing was changed. Describe the planned change instead, or call it without dryRun.`, { code: 'INVALID_ARGUMENT', details: { action: args.action, supportsDryRun: false } });
|
|
1647
|
+
}
|
|
1648
|
+
return runHelperAction(documentHandle, args.action, args, invokeOptions);
|
|
1649
|
+
}
|
|
1650
|
+
if (!FULL_ONLY_TOOL_NAMES.has(toolName)) {
|
|
1651
|
+
const level = toolName === 'superdoc_perform_action' ? headingLevelOf(args) : null;
|
|
1652
|
+
const before = level && (await authoredHeadingStyle(documentHandle, level, invokeOptions).then(Boolean, () => true))
|
|
1653
|
+
? await headingIds(documentHandle, invokeOptions)
|
|
1654
|
+
: null;
|
|
1655
|
+
const result = await corePreset.dispatch(documentHandle, toolName, args, invokeOptions);
|
|
1656
|
+
if (level && before)
|
|
1657
|
+
await adoptAuthoredHeadingStyle(documentHandle, level, before, args.action !== 'apply_style', invokeOptions);
|
|
1658
|
+
return toolName === 'superdoc_inspect' && opt('compactinspect') ? compactInspect(result) : result;
|
|
1659
|
+
}
|
|
1660
|
+
switch (toolName) {
|
|
1661
|
+
case 'superdoc_api_search':
|
|
1662
|
+
return runCatalog(args);
|
|
1663
|
+
case 'superdoc_api_call':
|
|
1664
|
+
return runOperation(documentHandle, args, invokeOptions);
|
|
1665
|
+
}
|
|
1666
|
+
throw new SuperDocCliError(`Unknown tool: ${toolName}`, {
|
|
1667
|
+
code: 'TOOL_DISPATCH_NOT_FOUND',
|
|
1668
|
+
details: { toolName, preset: 'full' },
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
function operationIdsNamedBy(toolName, args) {
|
|
1672
|
+
if (!isRecord(args))
|
|
1673
|
+
return [];
|
|
1674
|
+
if (toolName === 'agent_operation')
|
|
1675
|
+
return [args.operationId];
|
|
1676
|
+
if (toolName !== 'agent_apply')
|
|
1677
|
+
return [];
|
|
1678
|
+
const steps = isRecord(args.plan) && Array.isArray(args.plan.steps) ? args.plan.steps : [];
|
|
1679
|
+
return steps.flatMap((step) => isRecord(step) && (step.kind === 'apply' || step.kind === 'inspect' || step.operationId !== undefined)
|
|
1680
|
+
? [step.operationId]
|
|
1681
|
+
: []);
|
|
1682
|
+
}
|
|
1683
|
+
function refuseOperationsOutsideTheDocument(toolName, args) {
|
|
1684
|
+
for (const operationId of operationIdsNamedBy(toolName, stripCorruptedToolArgKeys(args))) {
|
|
1685
|
+
const op = typeof operationId === 'string'
|
|
1686
|
+
? CONTRACT.operations[operationId]
|
|
1687
|
+
: undefined;
|
|
1688
|
+
if (op && isDocumentOp(op))
|
|
1689
|
+
continue;
|
|
1690
|
+
throw new SuperDocCliError(`${toolName} cannot run ${String(operationId)}: only document operations are callable here. Nothing was run. Call superdoc_api_call with an operation from superdoc_api_search.`, { code: 'TOOL_DISPATCH_NOT_FOUND', details: { toolName, operationId, reason: 'not-a-document-operation' } });
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
function headingLevelOf(args) {
|
|
1694
|
+
const raw = args.action === 'insert_heading'
|
|
1695
|
+
? args.level
|
|
1696
|
+
: args.action === 'insert_paragraphs' || args.action === 'apply_style'
|
|
1697
|
+
? args.headingLevel
|
|
1698
|
+
: undefined;
|
|
1699
|
+
const level = Number(raw);
|
|
1700
|
+
return Number.isInteger(level) && level >= 1 && level <= 6 ? level : null;
|
|
1701
|
+
}
|
|
1702
|
+
async function headingIds(doc, invokeOptions) {
|
|
1703
|
+
try {
|
|
1704
|
+
const inspect = (await corePreset.dispatch(doc, 'superdoc_inspect', { blockOffset: 0, blockLimit: 400, omitEmptyBlocks: true, dropTextPreview: true }, invokeOptions));
|
|
1705
|
+
return new Map((inspect?.blocks ?? []).filter((b) => b.headingLevel != null).map((b) => [b.nodeId, b]));
|
|
1706
|
+
}
|
|
1707
|
+
catch {
|
|
1708
|
+
return null;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
async function authoredHeadingStyle(doc, level, invokeOptions) {
|
|
1712
|
+
const catalog = (await resolveMethod(doc, 'doc.styles.getCatalog')({ view: 'currentDocument', types: ['paragraph'] }, forwardOptions(invokeOptions)));
|
|
1713
|
+
const items = catalog?.items ?? [];
|
|
1714
|
+
const own = new RegExp(`^(?:h|heading[ _-]?)${level}$`, 'i');
|
|
1715
|
+
const reserved = new RegExp(`^heading ?${level}$`, 'i');
|
|
1716
|
+
const authored = items.filter((s) => s.provenance === 'authored' && (own.test(s.id) || own.test(s.name ?? '')) && !reserved.test(s.name ?? ''));
|
|
1717
|
+
return authored.length === 1 ? { style: authored[0], items, reserved } : null;
|
|
1718
|
+
}
|
|
1719
|
+
async function adoptAuthoredHeadingStyle(doc, level, before, inserted, invokeOptions) {
|
|
1720
|
+
try {
|
|
1721
|
+
const found = await authoredHeadingStyle(doc, level, invokeOptions);
|
|
1722
|
+
if (!found)
|
|
1723
|
+
return;
|
|
1724
|
+
const { items, reserved } = found;
|
|
1725
|
+
const target = found.style.id;
|
|
1726
|
+
const after = await headingIds(doc, invokeOptions);
|
|
1727
|
+
if (!after)
|
|
1728
|
+
return;
|
|
1729
|
+
const strays = [...after.values()].filter((b) => b.headingLevel === level &&
|
|
1730
|
+
b.styleId &&
|
|
1731
|
+
b.styleId !== target &&
|
|
1732
|
+
reserved.test(items.find((s) => s.id === b.styleId)?.name ?? '') &&
|
|
1733
|
+
before.get(b.nodeId)?.styleId !== b.styleId);
|
|
1734
|
+
const setStyle = resolveMethod(doc, 'doc.styles.paragraph.setStyle');
|
|
1735
|
+
for (const block of strays) {
|
|
1736
|
+
await setStyle({ target: { kind: 'block', nodeType: block.nodeType, nodeId: block.nodeId }, styleId: target }, forwardOptions(invokeOptions));
|
|
1737
|
+
}
|
|
1738
|
+
if (!inserted)
|
|
1739
|
+
return;
|
|
1740
|
+
const adopted = [...after.values()].filter((b) => b.headingLevel === level && !before.has(b.nodeId) && (b.styleId === target || strays.includes(b)));
|
|
1741
|
+
await clearRedundantHeadingFormatting(doc, found.style, level, adopted, invokeOptions);
|
|
1742
|
+
}
|
|
1743
|
+
catch { }
|
|
1744
|
+
}
|
|
1745
|
+
export async function clearRedundantHeadingFormatting(doc, style, level, headings, invokeOptions) {
|
|
1746
|
+
if (headings.length === 0)
|
|
1747
|
+
return;
|
|
1748
|
+
const definition = await readStyleDefinition(doc, style, invokeOptions);
|
|
1749
|
+
if (!definition)
|
|
1750
|
+
return;
|
|
1751
|
+
const opts = forwardOptions(invokeOptions);
|
|
1752
|
+
if (definition.paragraph?.outlineLvl === level - 1) {
|
|
1753
|
+
const setOutlineLevel = resolveMethod(doc, 'doc.format.paragraph.setOutlineLevel');
|
|
1754
|
+
for (const heading of headings) {
|
|
1755
|
+
await setOutlineLevel({ target: { kind: 'block', nodeType: heading.nodeType, nodeId: heading.nodeId }, outlineLevel: null }, opts);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (definition.run?.bold === true) {
|
|
1759
|
+
const rows = await listBlocks(doc, { nodeIds: headings.map((h) => h.nodeId), includeText: true }, invokeOptions);
|
|
1760
|
+
const match = resolveMethod(doc, 'doc.query.match');
|
|
1761
|
+
const formatApply = resolveMethod(doc, 'doc.format.apply');
|
|
1762
|
+
for (const row of rows) {
|
|
1763
|
+
if (!row.text)
|
|
1764
|
+
continue;
|
|
1765
|
+
const found = (await match({
|
|
1766
|
+
select: { type: 'text', pattern: row.text },
|
|
1767
|
+
within: { kind: 'block', nodeType: row.nodeType, nodeId: row.nodeId },
|
|
1768
|
+
require: 'first',
|
|
1769
|
+
}, opts));
|
|
1770
|
+
const direct = (found?.items ?? [])
|
|
1771
|
+
.flatMap((item) => item.blocks ?? [])
|
|
1772
|
+
.filter((block) => block.blockId === row.nodeId)
|
|
1773
|
+
.flatMap((block) => block.runs ?? [])
|
|
1774
|
+
.map((run) => run.styles?.direct?.bold);
|
|
1775
|
+
if (!direct.includes('on') || direct.includes('off'))
|
|
1776
|
+
continue;
|
|
1777
|
+
const point = (offset) => ({ kind: 'text', blockId: row.nodeId, offset });
|
|
1778
|
+
await formatApply({ target: { kind: 'selection', start: point(0), end: point(row.text.length) }, inline: { bold: null } }, opts);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
function buildFullPreset(id, includeExec, description) {
|
|
1783
|
+
return {
|
|
1784
|
+
id,
|
|
1785
|
+
description,
|
|
1786
|
+
supportsCacheControl: true,
|
|
1787
|
+
getTools: (provider, options) => fullGetTools(includeExec, provider, options),
|
|
1788
|
+
getCatalog: () => fullGetCatalog(includeExec),
|
|
1789
|
+
getSystemPrompt: (options) => fullGetSystemPrompt(includeExec, options),
|
|
1790
|
+
getMcpPrompt: () => corePreset.getMcpPrompt(),
|
|
1791
|
+
dispatch: (documentHandle, toolName, args, invokeOptions) => fullDispatch(includeExec, documentHandle, toolName, args, invokeOptions),
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
export const fullPreset = buildFullPreset('full', false, 'Experimental: core preset plus progressive disclosure of the entire Document API — superdoc_api_search (search the whole operation catalog), superdoc_api_call (call any op, batchable), and four helper actions on superdoc_perform_action (insert_image, insert_footnote, shade_cells, compare_with_file). No code execution.');
|
|
1795
|
+
export const fullExecPreset = buildFullPreset('full-exec', true, 'Internal A/B variant of `full` that also advertises superdoc_execute_code (unsandboxed in-host JavaScript). Not registered, not for production.');
|