diff --git a/.github/workflows/ci-mcp.yml b/.github/workflows/ci-mcp.yml index 4091c82ad1..eea956516b 100644 --- a/.github/workflows/ci-mcp.yml +++ b/.github/workflows/ci-mcp.yml @@ -31,9 +31,15 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Generate SDK artifacts + run: pnpm run generate:all + - name: Build superdoc (dependency) run: pnpm run build:superdoc + - name: Build SDK (dependency) + run: pnpm --prefix packages/sdk/langs/node run build + - name: Build run: pnpm --prefix apps/mcp run build diff --git a/.github/workflows/release-mcp.yml b/.github/workflows/release-mcp.yml index e6ead9a520..bc3ad46698 100644 --- a/.github/workflows/release-mcp.yml +++ b/.github/workflows/release-mcp.yml @@ -48,9 +48,15 @@ jobs: - name: Install dependencies run: pnpm install + - name: Generate SDK artifacts + run: pnpm run generate:all + - name: Build superdoc (dependency) run: pnpm run build:superdoc + - name: Build SDK (dependency) + run: pnpm --prefix packages/sdk/langs/node run build + - name: Build MCP server run: pnpm --prefix apps/mcp run build diff --git a/apps/cli/scripts/export-sdk-contract.ts b/apps/cli/scripts/export-sdk-contract.ts index 28bbec1d93..35eb690b5b 100644 --- a/apps/cli/scripts/export-sdk-contract.ts +++ b/apps/cli/scripts/export-sdk-contract.ts @@ -16,7 +16,7 @@ import { resolve, dirname } from 'node:path'; import { createHash } from 'node:crypto'; import { tmpdir } from 'node:os'; -import { COMMAND_CATALOG, INLINE_PROPERTY_REGISTRY } from '@superdoc/document-api'; +import { COMMAND_CATALOG, INTENT_GROUP_META } from '@superdoc/document-api'; import { CLI_OPERATION_METADATA } from '../src/cli/operation-params'; import { @@ -26,7 +26,6 @@ import { cliCommandTokens, cliRequiresDocumentContext, toDocApiId, - type DocBackedCliOpId, } from '../src/cli/operation-set'; import type { CliOnlyOperation } from '../src/cli/types'; import { CLI_ONLY_OPERATION_DEFINITIONS } from '../src/cli/cli-only-operation-definitions'; @@ -42,307 +41,6 @@ const CONTRACT_JSON_PATH = resolve(ROOT, 'packages/document-api/generated/schema const OUTPUT_PATH = resolve(CLI_DIR, 'generated/sdk-contract.json'); const CLI_PKG_PATH = resolve(CLI_DIR, 'package.json'); -// --------------------------------------------------------------------------- -// Intent names — human-friendly tool names for doc-backed operations only. -// CLI-only intent names live in CLI_ONLY_OPERATION_DEFINITIONS. -// --------------------------------------------------------------------------- - -const INTENT_NAMES = { - 'doc.get': 'get_document', - 'doc.markdownToFragment': 'markdown_to_fragment', - 'doc.find': 'find_content', - 'doc.getNode': 'get_node', - 'doc.getNodeById': 'get_node_by_id', - 'doc.getText': 'get_document_text', - 'doc.getMarkdown': 'get_document_markdown', - 'doc.getHtml': 'get_document_html', - 'doc.info': 'get_document_info', - 'doc.capabilities.get': 'get_capabilities', - 'doc.clearContent': 'clear_content', - 'doc.insert': 'insert_content', - 'doc.replace': 'replace_content', - 'doc.delete': 'delete_content', - 'doc.blocks.delete': 'delete_block', - 'doc.format.apply': 'format_apply', - ...Object.fromEntries( - INLINE_PROPERTY_REGISTRY.map((entry) => [ - `doc.format.${entry.key}`, - `format_${entry.key.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`)}`, - ]), - ), - 'doc.styles.paragraph.setStyle': 'set_paragraph_style', - 'doc.styles.paragraph.clearStyle': 'clear_paragraph_style', - 'doc.format.paragraph.resetDirectFormatting': 'reset_paragraph_direct_formatting', - 'doc.format.paragraph.setAlignment': 'set_paragraph_alignment', - 'doc.format.paragraph.clearAlignment': 'clear_paragraph_alignment', - 'doc.format.paragraph.setIndentation': 'set_paragraph_indentation', - 'doc.format.paragraph.clearIndentation': 'clear_paragraph_indentation', - 'doc.format.paragraph.setSpacing': 'set_paragraph_spacing', - 'doc.format.paragraph.clearSpacing': 'clear_paragraph_spacing', - 'doc.format.paragraph.setKeepOptions': 'set_paragraph_keep_options', - 'doc.format.paragraph.setOutlineLevel': 'set_paragraph_outline_level', - 'doc.format.paragraph.setFlowOptions': 'set_paragraph_flow_options', - 'doc.format.paragraph.setTabStop': 'set_paragraph_tab_stop', - 'doc.format.paragraph.clearTabStop': 'clear_paragraph_tab_stop', - 'doc.format.paragraph.clearAllTabStops': 'clear_all_paragraph_tab_stops', - 'doc.format.paragraph.setBorder': 'set_paragraph_border', - 'doc.format.paragraph.clearBorder': 'clear_paragraph_border', - 'doc.format.paragraph.setShading': 'set_paragraph_shading', - 'doc.format.paragraph.clearShading': 'clear_paragraph_shading', - 'doc.styles.apply': 'styles_apply', - 'doc.create.paragraph': 'create_paragraph', - 'doc.create.heading': 'create_heading', - 'doc.create.sectionBreak': 'create_section_break', - 'doc.sections.list': 'list_sections', - 'doc.sections.get': 'get_section', - 'doc.sections.setBreakType': 'set_section_break_type', - 'doc.sections.setPageMargins': 'set_section_page_margins', - 'doc.sections.setHeaderFooterMargins': 'set_section_header_footer_margins', - 'doc.sections.setPageSetup': 'set_section_page_setup', - 'doc.sections.setColumns': 'set_section_columns', - 'doc.sections.setLineNumbering': 'set_section_line_numbering', - 'doc.sections.setPageNumbering': 'set_section_page_numbering', - 'doc.sections.setTitlePage': 'set_section_title_page', - 'doc.sections.setOddEvenHeadersFooters': 'set_section_odd_even_headers_footers', - 'doc.sections.setVerticalAlign': 'set_section_vertical_align', - 'doc.sections.setSectionDirection': 'set_section_direction', - 'doc.sections.setHeaderFooterRef': 'set_section_header_footer_reference', - 'doc.sections.clearHeaderFooterRef': 'clear_section_header_footer_reference', - 'doc.sections.setLinkToPrevious': 'set_section_link_to_previous', - 'doc.sections.setPageBorders': 'set_section_page_borders', - 'doc.sections.clearPageBorders': 'clear_section_page_borders', - 'doc.create.tableOfContents': 'create_table_of_contents', - 'doc.lists.list': 'list_lists', - 'doc.lists.get': 'get_list', - 'doc.lists.insert': 'insert_list', - 'doc.lists.indent': 'indent_list', - 'doc.lists.outdent': 'outdent_list', - 'doc.lists.create': 'create_list', - 'doc.lists.attach': 'attach_to_list', - 'doc.lists.detach': 'detach_from_list', - 'doc.lists.join': 'join_lists', - 'doc.lists.canJoin': 'can_join_lists', - 'doc.lists.separate': 'separate_list', - 'doc.lists.setLevel': 'set_list_level', - 'doc.lists.setValue': 'set_list_value', - 'doc.lists.continuePrevious': 'continue_previous_list', - 'doc.lists.canContinuePrevious': 'can_continue_previous_list', - 'doc.lists.setLevelRestart': 'set_list_level_restart', - 'doc.lists.convertToText': 'convert_list_to_text', - 'doc.lists.applyTemplate': 'apply_list_template', - 'doc.lists.applyPreset': 'apply_list_preset', - 'doc.lists.setType': 'set_list_type', - 'doc.lists.captureTemplate': 'capture_list_template', - 'doc.lists.setLevelNumbering': 'set_list_level_numbering', - 'doc.lists.setLevelBullet': 'set_list_level_bullet', - 'doc.lists.setLevelPictureBullet': 'set_list_level_picture_bullet', - 'doc.lists.setLevelAlignment': 'set_list_level_alignment', - 'doc.lists.setLevelIndents': 'set_list_level_indents', - 'doc.lists.setLevelTrailingCharacter': 'set_list_level_trailing_character', - 'doc.lists.setLevelMarkerFont': 'set_list_level_marker_font', - 'doc.lists.clearLevelOverrides': 'clear_list_level_overrides', - 'doc.comments.create': 'create_comment', - 'doc.comments.patch': 'patch_comment', - 'doc.comments.delete': 'delete_comment', - 'doc.comments.get': 'get_comment', - 'doc.comments.list': 'list_comments', - 'doc.trackChanges.list': 'list_tracked_changes', - 'doc.trackChanges.get': 'get_tracked_change', - 'doc.trackChanges.decide': 'decide_tracked_change', - 'doc.toc.list': 'list_table_of_contents', - 'doc.toc.get': 'get_table_of_contents', - 'doc.toc.configure': 'configure_table_of_contents', - 'doc.toc.update': 'update_table_of_contents', - 'doc.toc.remove': 'remove_table_of_contents', - 'doc.toc.markEntry': 'mark_table_of_contents_entry', - 'doc.toc.unmarkEntry': 'unmark_table_of_contents_entry', - 'doc.toc.listEntries': 'list_table_of_contents_entries', - 'doc.toc.getEntry': 'get_table_of_contents_entry', - 'doc.toc.editEntry': 'edit_table_of_contents_entry', - 'doc.hyperlinks.list': 'list_hyperlinks', - 'doc.hyperlinks.get': 'get_hyperlink', - 'doc.hyperlinks.wrap': 'wrap_hyperlink', - 'doc.hyperlinks.insert': 'insert_hyperlink', - 'doc.hyperlinks.patch': 'patch_hyperlink', - 'doc.hyperlinks.remove': 'remove_hyperlink', - 'doc.headerFooters.list': 'list_header_footer_slots', - 'doc.headerFooters.get': 'get_header_footer_slot', - 'doc.headerFooters.resolve': 'resolve_header_footer', - 'doc.headerFooters.refs.set': 'set_header_footer_ref', - 'doc.headerFooters.refs.clear': 'clear_header_footer_ref', - 'doc.headerFooters.refs.setLinkedToPrevious': 'set_header_footer_linked_to_previous', - 'doc.headerFooters.parts.list': 'list_header_footer_parts', - 'doc.headerFooters.parts.create': 'create_header_footer_part', - 'doc.headerFooters.parts.delete': 'delete_header_footer_part', - 'doc.query.match': 'query_match', - 'doc.mutations.preview': 'preview_mutations', - 'doc.mutations.apply': 'apply_mutations', - 'doc.create.table': 'create_table', - 'doc.tables.convertFromText': 'convert_text_to_table', - 'doc.tables.delete': 'delete_table', - 'doc.tables.clearContents': 'clear_table_contents', - 'doc.tables.move': 'move_table', - 'doc.tables.split': 'split_table', - 'doc.tables.convertToText': 'convert_table_to_text', - 'doc.tables.setLayout': 'set_table_layout', - 'doc.tables.insertRow': 'insert_table_row', - 'doc.tables.deleteRow': 'delete_table_row', - 'doc.tables.setRowHeight': 'set_table_row_height', - 'doc.tables.distributeRows': 'distribute_table_rows', - 'doc.tables.setRowOptions': 'set_table_row_options', - 'doc.tables.insertColumn': 'insert_table_column', - 'doc.tables.deleteColumn': 'delete_table_column', - 'doc.tables.setColumnWidth': 'set_table_column_width', - 'doc.tables.distributeColumns': 'distribute_table_columns', - 'doc.tables.insertCell': 'insert_table_cell', - 'doc.tables.deleteCell': 'delete_table_cell', - 'doc.tables.mergeCells': 'merge_table_cells', - 'doc.tables.unmergeCells': 'unmerge_table_cells', - 'doc.tables.splitCell': 'split_table_cell', - 'doc.tables.setCellProperties': 'set_table_cell_properties', - 'doc.tables.sort': 'sort_table', - 'doc.tables.setAltText': 'set_table_alt_text', - 'doc.tables.setStyle': 'set_table_style', - 'doc.tables.clearStyle': 'clear_table_style', - 'doc.tables.setStyleOption': 'set_table_style_option', - 'doc.tables.setBorder': 'set_table_border', - 'doc.tables.clearBorder': 'clear_table_border', - 'doc.tables.applyBorderPreset': 'apply_table_border_preset', - 'doc.tables.setShading': 'set_table_shading', - 'doc.tables.clearShading': 'clear_table_shading', - 'doc.tables.setTablePadding': 'set_table_padding', - 'doc.tables.setCellPadding': 'set_table_cell_padding', - 'doc.tables.setCellSpacing': 'set_table_cell_spacing', - 'doc.tables.clearCellSpacing': 'clear_table_cell_spacing', - 'doc.tables.get': 'get_table', - 'doc.tables.getCells': 'get_table_cells', - 'doc.tables.getProperties': 'get_table_properties', - 'doc.tables.getStyles': 'get_table_styles', - 'doc.tables.setDefaultStyle': 'set_table_default_style', - 'doc.tables.clearDefaultStyle': 'clear_table_default_style', - 'doc.history.get': 'get_history', - 'doc.history.undo': 'undo', - 'doc.history.redo': 'redo', - 'doc.create.image': 'create_image', - 'doc.images.list': 'list_images', - 'doc.images.get': 'get_image', - 'doc.images.delete': 'delete_image', - 'doc.images.move': 'move_image', - 'doc.images.convertToInline': 'convert_image_to_inline', - 'doc.images.convertToFloating': 'convert_image_to_floating', - 'doc.images.setSize': 'set_image_size', - 'doc.images.setWrapType': 'set_image_wrap_type', - 'doc.images.setWrapSide': 'set_image_wrap_side', - 'doc.images.setWrapDistances': 'set_image_wrap_distances', - 'doc.images.setPosition': 'set_image_position', - 'doc.images.setAnchorOptions': 'set_image_anchor_options', - 'doc.images.setZOrder': 'set_image_z_order', - 'doc.images.scale': 'scale_image', - 'doc.images.setLockAspectRatio': 'set_image_lock_aspect_ratio', - 'doc.images.rotate': 'rotate_image', - 'doc.images.flip': 'flip_image', - 'doc.images.crop': 'crop_image', - 'doc.images.resetCrop': 'reset_image_crop', - 'doc.images.replaceSource': 'replace_image_source', - 'doc.images.setAltText': 'set_image_alt_text', - 'doc.images.setDecorative': 'set_image_decorative', - 'doc.images.setName': 'set_image_name', - 'doc.images.setHyperlink': 'set_image_hyperlink', - 'doc.images.insertCaption': 'insert_image_caption', - 'doc.images.updateCaption': 'update_image_caption', - 'doc.images.removeCaption': 'remove_image_caption', - - // Bookmarks - 'doc.bookmarks.list': 'list_bookmarks', - 'doc.bookmarks.get': 'get_bookmark', - 'doc.bookmarks.insert': 'insert_bookmark', - 'doc.bookmarks.rename': 'rename_bookmark', - 'doc.bookmarks.remove': 'remove_bookmark', - - // Footnotes - 'doc.footnotes.list': 'list_footnotes', - 'doc.footnotes.get': 'get_footnote', - 'doc.footnotes.insert': 'insert_footnote', - 'doc.footnotes.update': 'update_footnote', - 'doc.footnotes.remove': 'remove_footnote', - 'doc.footnotes.configure': 'configure_footnote_numbering', - - // Cross-References - 'doc.crossRefs.list': 'list_cross_references', - 'doc.crossRefs.get': 'get_cross_reference', - 'doc.crossRefs.insert': 'insert_cross_reference', - 'doc.crossRefs.rebuild': 'rebuild_cross_reference', - 'doc.crossRefs.remove': 'remove_cross_reference', - - // Index - 'doc.index.list': 'list_indexes', - 'doc.index.get': 'get_index', - 'doc.index.insert': 'insert_index', - 'doc.index.configure': 'configure_index', - 'doc.index.rebuild': 'rebuild_index', - 'doc.index.remove': 'remove_index', - 'doc.index.entries.list': 'list_index_entries', - 'doc.index.entries.get': 'get_index_entry', - 'doc.index.entries.insert': 'insert_index_entry', - 'doc.index.entries.update': 'update_index_entry', - 'doc.index.entries.remove': 'remove_index_entry', - - // Captions - 'doc.captions.list': 'list_captions', - 'doc.captions.get': 'get_caption', - 'doc.captions.insert': 'insert_caption', - 'doc.captions.update': 'update_caption', - 'doc.captions.remove': 'remove_caption', - 'doc.captions.configure': 'configure_caption_numbering', - - // Fields - 'doc.fields.list': 'list_fields', - 'doc.fields.get': 'get_field', - 'doc.fields.insert': 'insert_field', - 'doc.fields.rebuild': 'rebuild_field', - 'doc.fields.remove': 'remove_field', - - // Citations - 'doc.citations.list': 'list_citations', - 'doc.citations.get': 'get_citation', - 'doc.citations.insert': 'insert_citation', - 'doc.citations.update': 'update_citation', - 'doc.citations.remove': 'remove_citation', - 'doc.citations.sources.list': 'list_citation_sources', - 'doc.citations.sources.get': 'get_citation_source', - 'doc.citations.sources.insert': 'insert_citation_source', - 'doc.citations.sources.update': 'update_citation_source', - 'doc.citations.sources.remove': 'remove_citation_source', - 'doc.citations.bibliography.get': 'get_bibliography', - 'doc.citations.bibliography.insert': 'insert_bibliography', - 'doc.citations.bibliography.rebuild': 'rebuild_bibliography', - 'doc.citations.bibliography.configure': 'configure_bibliography', - 'doc.citations.bibliography.remove': 'remove_bibliography', - - // Authorities (Table of Authorities) - 'doc.authorities.list': 'list_authorities', - 'doc.authorities.get': 'get_authority', - 'doc.authorities.insert': 'insert_authority', - 'doc.authorities.configure': 'configure_authority', - 'doc.authorities.rebuild': 'rebuild_authority', - 'doc.authorities.remove': 'remove_authority', - 'doc.authorities.entries.list': 'list_authority_entries', - 'doc.authorities.entries.get': 'get_authority_entry', - 'doc.authorities.entries.insert': 'insert_authority_entry', - 'doc.authorities.entries.update': 'update_authority_entry', - 'doc.authorities.entries.remove': 'remove_authority_entry', -} as const satisfies Partial>; - -function deriveDocBackedIntentName(cliOpId: DocBackedCliOpId): string { - const mapped = INTENT_NAMES[cliOpId]; - if (mapped) { - return mapped; - } - - const docApiId = cliOpId.slice(4); - return docApiId.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`).replace(/\./g, '_'); -} - // --------------------------------------------------------------------------- // Load inputs // --------------------------------------------------------------------------- @@ -378,12 +76,7 @@ function buildSdkContract() { const docApiId = toDocApiId(cliOpId); const stripped = cliOpId.slice(4) as CliOnlyOperation; - // Resolve intentName: doc-backed from INTENT_NAMES, CLI-only from definitions const cliOnlyDef = docApiId ? null : CLI_ONLY_OPERATION_DEFINITIONS[stripped]; - const intentName = docApiId ? deriveDocBackedIntentName(cliOpId as DocBackedCliOpId) : cliOnlyDef?.intentName; - if (!intentName) { - throw new Error(`Missing intentName for ${cliOpId}`); - } // Base fields shared by all operations const entry: Record = { @@ -394,7 +87,6 @@ function buildSdkContract() { description: cliDescription(cliOpId), requiresDocumentContext: cliRequiresDocumentContext(cliOpId), docRequirement: metadata.docRequirement, - intentName, // Transport plane params: metadata.params.map((p) => { @@ -430,7 +122,8 @@ function buildSdkContract() { if (docOp.successSchema) entry.successSchema = docOp.successSchema; if (docOp.failureSchema) entry.failureSchema = docOp.failureSchema; if (docOp.skipAsATool) entry.skipAsATool = true; - if (docOp.essential) entry.essential = true; + if (docOp.intentGroup) entry.intentGroup = docOp.intentGroup; + if (docOp.intentAction) entry.intentAction = docOp.intentAction; } else { // CLI-only operation — metadata from canonical definitions const def = cliOnlyDef!; @@ -465,6 +158,7 @@ function buildSdkContract() { features: [...HOST_PROTOCOL_FEATURES], notifications: [...HOST_PROTOCOL_NOTIFICATIONS], }, + intentGroupMeta: INTENT_GROUP_META, operations, }; } diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index 6a6fc54c34..1186d7fe49 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -951,5 +951,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "b52520ee9b80ed98b8bb191aeec45df6488bdd1ee406009553247cfec58cfea1" + "sourceHash": "068dea933bf1591112019534c6bae48f811dc8d65c42f6cb94f365548028ea77" } diff --git a/apps/docs/document-api/reference/insert.mdx b/apps/docs/document-api/reference/insert.mdx index 0a3f788219..66df5a1fd3 100644 --- a/apps/docs/document-api/reference/insert.mdx +++ b/apps/docs/document-api/reference/insert.mdx @@ -43,7 +43,7 @@ Returns an SDMutationReceipt with applied status; resolution reports a TextAddre | Field | Type | Required | Description | | --- | --- | --- | --- | -| `content` | object | yes | | +| `content` | object \\| object[] | yes | One of: object, object[] | | `nestingPolicy` | object | no | | | `nestingPolicy.tables` | enum | no | `"forbid"`, `"allow"` | | `placement` | enum | no | `"before"`, `"after"`, `"insideStart"`, `"insideEnd"` | @@ -201,7 +201,17 @@ Returns an SDMutationReceipt with applied status; resolution reports a TextAddre "additionalProperties": false, "properties": { "content": { - "type": "object" + "oneOf": [ + { + "type": "object" + }, + { + "items": { + "type": "object" + }, + "type": "array" + } + ] }, "nestingPolicy": { "additionalProperties": false, diff --git a/apps/docs/document-api/reference/ranges/index.mdx b/apps/docs/document-api/reference/ranges/index.mdx index b231fc7c38..34e4458ed8 100644 --- a/apps/docs/document-api/reference/ranges/index.mdx +++ b/apps/docs/document-api/reference/ranges/index.mdx @@ -15,4 +15,3 @@ Deterministic range construction from explicit document anchors. | Operation | Member path | Mutates | Idempotency | Tracked | Dry run | | --- | --- | --- | --- | --- | --- | | ranges.resolve | `ranges.resolve` | No | `idempotent` | No | No | - diff --git a/apps/docs/document-api/reference/replace.mdx b/apps/docs/document-api/reference/replace.mdx index a3bb0ec88f..5af0c03b99 100644 --- a/apps/docs/document-api/reference/replace.mdx +++ b/apps/docs/document-api/reference/replace.mdx @@ -47,7 +47,7 @@ Returns an SDMutationReceipt with applied status; receipt reports NO_OP if the t | Field | Type | Required | Description | | --- | --- | --- | --- | -| `content` | object | yes | | +| `content` | object \\| object[] | yes | One of: object, object[] | | `nestingPolicy` | object | no | | | `nestingPolicy.tables` | enum | no | `"forbid"`, `"allow"` | | `target` | BlockNodeAddress \\| SelectionTarget | yes | One of: BlockNodeAddress, SelectionTarget | @@ -56,7 +56,7 @@ Returns an SDMutationReceipt with applied status; receipt reports NO_OP if the t | Field | Type | Required | Description | | --- | --- | --- | --- | -| `content` | object | yes | | +| `content` | object \\| object[] | yes | One of: object, object[] | | `nestingPolicy` | object | no | | | `nestingPolicy.tables` | enum | no | `"forbid"`, `"allow"` | | `ref` | string | yes | | @@ -225,7 +225,17 @@ Returns an SDMutationReceipt with applied status; receipt reports NO_OP if the t "additionalProperties": false, "properties": { "content": { - "type": "object" + "oneOf": [ + { + "type": "object" + }, + { + "items": { + "type": "object" + }, + "type": "array" + } + ] }, "nestingPolicy": { "additionalProperties": false, @@ -260,7 +270,17 @@ Returns an SDMutationReceipt with applied status; receipt reports NO_OP if the t "additionalProperties": false, "properties": { "content": { - "type": "object" + "oneOf": [ + { + "type": "object" + }, + { + "items": { + "type": "object" + }, + "type": "array" + } + ] }, "nestingPolicy": { "additionalProperties": false, diff --git a/apps/docs/document-engine/ai-agents/llm-tools.mdx b/apps/docs/document-engine/ai-agents/llm-tools.mdx index 0edcd6a691..3ff6b4f5de 100644 --- a/apps/docs/document-engine/ai-agents/llm-tools.mdx +++ b/apps/docs/document-engine/ai-agents/llm-tools.mdx @@ -119,10 +119,8 @@ Install the SDK, create a client, and wire up an agentic loop. ```typescript import { chooseTools } from '@superdoc-dev/sdk'; - const { tools, selected, meta } = await chooseTools({ + const { tools, meta } = await chooseTools({ provider: 'openai', // 'openai' | 'anthropic' | 'vercel' | 'generic' - mode: 'essential', // 'essential' (default) | 'all' - groups: ['comments'], // optional — load additional groups alongside essential tools }); ``` @@ -132,49 +130,31 @@ Install the SDK, create a client, and wire up an agentic loop. result = choose_tools( provider="openai", - mode="essential", - groups=["comments"], ) tools = result["tools"] ``` -### Essential mode (default) +The current SDK returns the full grouped intent tool set for the selected provider. Group filtering and meta-discovery are not part of the shipped public API here. -Returns 5 essential tools plus `discover_tools` — a meta-tool that lets the LLM load more groups on demand. This keeps the initial context small while giving the model access to the full toolkit when needed. +## Current tool set -The 5 essential tools: +The generated catalog currently contains 9 grouped intent tools: -| Tool | What it does | -| --- | --- | -| `get_document_text` | Returns the full plain-text content of the document | -| `query_match` | Searches by node type, text pattern, or both — returns matches with addresses | -| `apply_mutations` | Batch edit: rewrite, insert, delete text and apply formatting in one call | -| `get_node_by_id` | Get details about a specific node by its address | -| `undo` | Undo the last operation | - -If you pass `groups`, those groups are loaded **in addition** to the essential set: - -```typescript -// Essential tools + all comment tools -const { tools } = await chooseTools({ - provider: 'openai', - groups: ['comments'], -}); -``` - -### All mode - -Returns every tool from the requested groups (or all groups if `groups` is omitted). The `core` group is always included. - -```typescript -const { tools } = await chooseTools({ - provider: 'openai', - mode: 'all', - groups: ['core', 'format', 'comments'], -}); -``` +| Tool | Actions | What it does | +| --- | --- | --- | +| `superdoc_get_content` | `text`, `markdown`, `html`, `info` | Read document content in different formats | +| `superdoc_search` | `match` | Find text or nodes and return handles or addresses for later edits | +| `superdoc_edit` | `insert`, `replace`, `delete`, `undo`, `redo` | Perform text edits and history actions | +| `superdoc_format` | `inline`, `set_style`, `set_alignment`, `set_indentation`, `set_spacing` | Apply inline or paragraph formatting | +| `superdoc_create` | `paragraph`, `heading` | Create structural block elements | +| `superdoc_list` | `insert`, `create`, `detach`, `indent`, `outdent`, `set_level`, `set_type` | Create and manipulate lists | +| `superdoc_comment` | `create`, `update`, `delete`, `get`, `list` | Manage comment threads | +| `superdoc_track_changes` | `list`, `decide` | Review and resolve tracked changes | +| `superdoc_mutations` | `preview`, `apply` | Execute multi-step atomic edits as a batch | + +Multi-action tools use an `action` argument to select the underlying operation. Single-action tools like `superdoc_search` do not require `action`. ## Dispatching tool calls @@ -206,51 +186,6 @@ const { tools } = await chooseTools({ The dispatcher validates required parameters, enforces mutual exclusivity constraints, and throws descriptive errors if arguments are invalid — so the LLM gets actionable feedback. -## Tool groups - -Tools are organized into 11 groups. In essential mode, the LLM can load any group dynamically via `discover_tools`. - -| Group | Description | -| --- | --- | -| `core` | Read nodes, get text, find/replace, insert, delete, batch mutations | -| `format` | Bold, italic, underline, strikethrough, alignment, spacing, borders, shading | -| `create` | Create headings, paragraphs, tables, sections, table of contents | -| `tables` | Row/column operations, cell merging, table formatting, borders | -| `sections` | Page layout, margins, columns, headers/footers, page numbering | -| `lists` | Bullet and numbered lists, indentation, list type conversion | -| `comments` | Create, edit, delete, resolve, and list comment threads | -| `trackChanges` | List, inspect, accept, and reject tracked changes | -| `toc` | Table of contents — create, configure, refresh | -| `history` | Undo and redo | -| `session` | Open, save, close, and manage document sessions | - -## The discover_tools pattern - -When the LLM needs tools beyond the essential set, it calls `discover_tools` with the groups it wants. Since `discover_tools` is a meta-tool (not a document operation), intercept it before `dispatchSuperDocTool` and handle it client-side via `chooseTools`: - -```typescript -import { chooseTools } from '@superdoc-dev/sdk'; - -for (const call of message.tool_calls) { - let result; - const args = JSON.parse(call.function.arguments); - - if (call.function.name === 'discover_tools') { - // Meta-tool — resolve client-side, then merge new tools - result = await chooseTools({ provider: 'openai', groups: args.groups }); - tools.push(...result.tools); - } else { - result = await dispatchSuperDocTool(client, call.function.name, args); - } - - messages.push({ - role: 'tool', - tool_call_id: call.id, - content: JSON.stringify(result), - }); -} -``` - ## Providers Each provider gets tool definitions in its native format. @@ -284,21 +219,25 @@ Each provider gets tool definitions in its native format. ## Best practices -### Start with essential mode +### Start with the full grouped set -Load only the 5 essential tools plus `discover_tools`. This keeps the context window small and gives the model room to reason. Let it call `discover_tools` when it needs more — don't front-load every group. +The current catalog is intentionally small. In most integrations you should load the full grouped set once and let the model choose among the 9 tools. ### Minimize tool calls -A typical edit should take 3–5 tool calls: query, mutate, done. Instruct the LLM to plan all edits before calling tools, and to batch multiple changes into a single `apply_mutations` call when possible. +A typical edit should take 3-5 tool calls: read, search, mutate, done. Instruct the LLM to plan all edits before calling tools, and to batch multiple changes only when atomic execution is genuinely helpful. -### Use `apply_mutations` for text edits +### Read first, then search before editing -`apply_mutations` can rewrite, insert, delete, and format text in one call. It supports multiple steps, so the LLM can edit several paragraphs at once. Use it for any operation on existing text. +Use `superdoc_get_content` to understand the document, then `superdoc_search` to obtain stable handles or addresses. Pass those targets into `superdoc_edit`, `superdoc_format`, `superdoc_create`, `superdoc_list`, or `superdoc_comment`. + +### Prefer focused tools; use `superdoc_mutations` as an escape hatch + +Use the focused intent tools for straightforward edits. Reach for `superdoc_mutations` when you need preview/apply semantics, atomic multi-step edits, or a batched workflow that would otherwise require several target refreshes. ### Feed errors back to the model -`dispatchSuperDocTool` throws descriptive errors with codes like `MATCH_NOT_FOUND` or `INVALID_ARGUMENT`. Pass these back as tool results — most models self-correct on the next turn. +`dispatchSuperDocTool` surfaces structured errors from the underlying SDK and Document API. Pass these back as tool results — most models self-correct on the next turn. ### Add tool call examples for repeatable actions @@ -306,57 +245,43 @@ If your workflow involves the same kind of edit across many documents (e.g., alw ### Include a system prompt -Tell the model what it can do and how to approach edits. Here's an example: +Tell the model what it can do and how to approach edits. You can load the bundled prompt with `getSystemPrompt()`, or start with something like this: ````markdown -You edit `.docx` files using SuperDoc tools. Be efficient — minimize tool calls. +You edit `.docx` files using SuperDoc intent tools. Be efficient and minimize tool calls. ## Workflow -1. **Query** — Call `query_match` to find text you want to edit. - Use `require: "any"` to match broadly. -2. **Plan** — Decide what changes to make. Think through all - edits before making tool calls. -3. **Mutate** — Call `apply_mutations` to rewrite, insert, delete, - or format text. - -Keep it to 3–5 tool calls total. - -## Tools - -You start with 5 essential tools plus `discover_tools`. -Call `discover_tools` to load additional categories when needed: +1. **Read** — Use `superdoc_get_content` to understand the document. +2. **Search** — Use `superdoc_search` to find stable handles or block addresses. +3. **Edit** — Use the focused tool that matches the job: + - `superdoc_edit` for insert, replace, delete, undo, redo + - `superdoc_format` for inline or paragraph formatting + - `superdoc_create` for paragraphs and headings + - `superdoc_comment` for comment threads + - `superdoc_track_changes` for review decisions +4. **Batch only when useful** — Use `superdoc_mutations` for preview/apply or atomic multi-step edits. -| Category | What it covers | -|----------|----------------| -| core | Read nodes, get text, query, mutations | -| format | Text formatting, alignment, spacing, borders | -| create | Headings, paragraphs, tables, sections | -| tables | Table manipulation, cell operations | -| comments | Comment threads | -| trackChanges | Accept/reject tracked changes | +Keep it to 3-5 tool calls total when possible. ## Rules -- Use `apply_mutations` for text edits on existing content. - You can batch multiple rewrites in one call. -- Use standalone tools (`create_heading`, `create_table`, etc.) - for structural changes — these are NOT step ops. +- Search before mutating so targets come from fresh results. +- Use focused intent tools for normal edits. +- Use `superdoc_mutations` when you need an atomic batch or preview/apply flow. - Set `changeMode: "tracked"` when edits need human review. -- Don't rewrite and format in the same `apply_mutations` batch. - Rewrite first, query again for fresh refs, then format. +- Feed tool errors back to the model so it can recover. ```` ## Utility functions | Function | Description | | --- | --- | -| `chooseTools(input)` | Select tools for a provider, filtered by mode and groups | +| `chooseTools(input)` | Load grouped tool definitions for a provider | | `dispatchSuperDocTool(client, name, args)` | Execute a tool call against a connected client | | `listTools(provider)` | List all tool definitions for a provider | -| `resolveToolOperation(toolName)` | Map a tool name to its operation ID | | `getToolCatalog()` | Load the full tool catalog with metadata | -| `getAvailableGroups()` | List all available tool groups | +| `getSystemPrompt()` | Read the bundled system prompt for intent tools | ## Related diff --git a/apps/docs/document-engine/ai-agents/mcp-server.mdx b/apps/docs/document-engine/ai-agents/mcp-server.mdx index ed1383123c..26d7aec4f5 100644 --- a/apps/docs/document-engine/ai-agents/mcp-server.mdx +++ b/apps/docs/document-engine/ai-agents/mcp-server.mdx @@ -71,18 +71,23 @@ Install once. Your MCP client spawns the server automatically on each conversati Every interaction follows the same pattern: open, read or edit, save, close. ``` -superdoc_open → superdoc_find / superdoc_get_text → edit tools → superdoc_save → superdoc_close +superdoc_open → superdoc_get_content / superdoc_search → intent tools → superdoc_save → superdoc_close ``` 1. `superdoc_open` loads a `.docx` file and returns a `session_id` -2. `superdoc_find` locates content and returns target addresses -3. Edit tools (`superdoc_insert`, `superdoc_replace`, `superdoc_delete`, `superdoc_format`) use those addresses +2. `superdoc_get_content` reads the current document and `superdoc_search` finds stable handles or addresses +3. Intent tools use `session_id` plus `action` to edit, format, create, comment, review track changes, or run batched mutations 4. `superdoc_save` writes changes to disk 5. `superdoc_close` releases the session ## Tools -23 tools in eight groups. All tools take a `session_id` from `superdoc_open`. +The MCP server exposes 12 tools total: + +- 3 lifecycle tools: `superdoc_open`, `superdoc_save`, `superdoc_close` +- 9 grouped intent tools generated from the SDK catalog + +All tools except `superdoc_open` take a `session_id` from `superdoc_open`. ### Lifecycle @@ -92,70 +97,25 @@ superdoc_open → superdoc_find / superdoc_get_text → edit tools → superdoc_ | `superdoc_save` | `session_id`, `out?` | Save to the original path, or to `out` if specified | | `superdoc_close` | `session_id` | Close the session. Unsaved changes are lost | -### Query - -| Tool | Input | Description | -| --- | --- | --- | -| `superdoc_find` | `session_id`, `type?`, `pattern?`, `limit?`, `offset?` | Search by node type, text pattern, or both. Returns matches with addresses | -| `superdoc_get_node` | `session_id`, `address` | Get details about a specific node by its address | -| `superdoc_info` | `session_id` | Document metadata: structure summary, node counts, capabilities | -| `superdoc_get_text` | `session_id` | Full plain-text content of the document | - -### Mutation +### Intent tools -All mutation tools accept `suggest?` — set to `true` to make the edit a tracked change instead of a direct edit. - -| Tool | Input | Description | +| Tool | Actions | Description | | --- | --- | --- | -| `superdoc_insert` | `session_id`, `text`, `target`, `suggest?` | Insert text at a target position | -| `superdoc_replace` | `session_id`, `text`, `target`, `suggest?` | Replace content at a target range | -| `superdoc_delete` | `session_id`, `target`, `suggest?` | Delete content at a target range | +| `superdoc_get_content` | `text`, `markdown`, `html`, `info` | Read document content in different formats | +| `superdoc_search` | `match` | Find text or nodes and return handles or addresses for later edits | +| `superdoc_edit` | `insert`, `replace`, `delete`, `undo`, `redo` | Perform text edits and history actions | +| `superdoc_format` | `inline`, `set_style`, `set_alignment`, `set_indentation`, `set_spacing` | Apply inline or paragraph formatting | +| `superdoc_create` | `paragraph`, `heading` | Create structural block elements | +| `superdoc_list` | `insert`, `create`, `detach`, `indent`, `outdent`, `set_level`, `set_type` | Create and manipulate lists | +| `superdoc_comment` | `create`, `update`, `delete`, `get`, `list` | Manage comment threads | +| `superdoc_track_changes` | `list`, `decide` | Review and resolve tracked changes | +| `superdoc_mutations` | `preview`, `apply` | Execute multi-step atomic edits as a batch | -### Format - -| Tool | Input | Description | -| --- | --- | --- | -| `superdoc_format` | `session_id`, `style`, `target`, `suggest?` | Toggle formatting on a text range. Styles: `bold`, `italic`, `underline`, `strikethrough` | - -### Create - -| Tool | Input | Description | -| --- | --- | --- | -| `superdoc_create` | `session_id`, `type`, `text?`, `level?`, `at?`, `suggest?` | Create a block element. Types: `paragraph`, `heading` (headings require `level` 1-6) | - -### Track changes - -Review and resolve tracked changes (suggestions) in the document. - -| Tool | Input | Description | -| --- | --- | --- | -| `superdoc_list_changes` | `session_id`, `type?`, `limit?`, `offset?` | List tracked changes with type, author, date, and excerpt | -| `superdoc_accept_change` | `session_id`, `id` | Accept a single change, applying it to the document | -| `superdoc_reject_change` | `session_id`, `id` | Reject a single change, reverting it | -| `superdoc_accept_all_changes` | `session_id` | Accept all tracked changes at once | -| `superdoc_reject_all_changes` | `session_id` | Reject all tracked changes at once | - -### Comments - -Add and manage comments anchored to text ranges. - -| Tool | Input | Description | -| --- | --- | --- | -| `superdoc_add_comment` | `session_id`, `text`, `target` | Add a comment anchored to a text range | -| `superdoc_list_comments` | `session_id`, `include_resolved?` | List all comments with author, status, and anchored text | -| `superdoc_reply_comment` | `session_id`, `comment_id`, `text` | Reply to an existing comment thread | -| `superdoc_resolve_comment` | `session_id`, `comment_id` | Mark a comment as resolved | - -### Lists - -| Tool | Input | Description | -| --- | --- | --- | -| `superdoc_insert_list` | `session_id`, `target`, `position`, `text?` | Insert a list item before or after an existing one | -| `superdoc_list_set_type` | `session_id`, `target`, `kind` | Change a list between `ordered` (numbered) and `bullet` | +Multi-action tools use an `action` argument to select the underlying operation. `superdoc_search` is a single-action tool and does not require `action`. -## Suggesting mode +## Tracked changes -Set `suggest=true` on any mutation, format, or create tool to make edits appear as tracked changes. The document stays unchanged until someone accepts the suggestions — in Word, in SuperDoc's browser editor, or programmatically via the track changes tools. +Actions that support tracked edits use the underlying Document API's `changeMode: "tracked"` option. Review or resolve tracked edits with `superdoc_track_changes`. ## How it works diff --git a/apps/docs/document-engine/overview.mdx b/apps/docs/document-engine/overview.mdx index 9f3485c9ad..026879e2be 100644 --- a/apps/docs/document-engine/overview.mdx +++ b/apps/docs/document-engine/overview.mdx @@ -18,7 +18,7 @@ Document Engine gives you four ways to read and edit `.docx` files without a vis | [CLI](/document-engine/cli) | Scripts and CI pipelines | Any shell | | [MCP Server](/document-engine/mcp) | AI agents (Claude Code, Cursor, custom) | Local subprocess | -All four share the same operation set. `find` is `editor.doc.find()` in the browser, `superdoc.doc.find()` in the SDK, `superdoc find` in the CLI, and `superdoc_find` in MCP. +All four sit on the same underlying document capabilities. The browser, SDK, and CLI expose individual operations directly; MCP exposes grouped intent tools such as `superdoc_search`, `superdoc_get_content`, and `superdoc_edit`. ## How it works @@ -41,4 +41,3 @@ The Document API defines the canonical operations. The CLI and MCP server wrap t | Backend automation (Node.js / Python) | [SDKs](/document-engine/sdks) | Typed methods, session management, error handling built in | | Shell scripts or CI pipelines | [CLI](/document-engine/cli) | Stateless one-shot commands — no process to manage | | Serverless (Lambda, Vercel, Workers) | [CLI](/document-engine/cli) or [SDKs](/document-engine/sdks) | Open the file, run the operation, return the result | - diff --git a/apps/docs/document-engine/sdks.mdx b/apps/docs/document-engine/sdks.mdx index 65ea0d3cf2..96e30e0e5c 100644 --- a/apps/docs/document-engine/sdks.mdx +++ b/apps/docs/document-engine/sdks.mdx @@ -376,8 +376,8 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.markdownToFragment` | `markdown-to-fragment` | Convert a Markdown string into an SDM/1 structural fragment. | | `doc.info` | `info` | Return document metadata including revision, node count, and capabilities. | | `doc.clearContent` | `clear-content` | Clear all document body content, leaving a single empty paragraph. | -| `doc.insert` | `insert` | Insert content into the document. Two input shapes: legacy string-based (value + type) inserts inline content at a text position within an existing block; structural SDFragment (content) inserts one or more blocks as siblings relative to a BlockNodeAddress target. When target is omitted, content appends at the end of the document. Legacy mode supports text (default), markdown, and html content types via the `type` field. Structural mode uses `placement` (before/after/insideStart/insideEnd) to position relative to the target block. | -| `doc.replace` | `replace` | Replace content at a contiguous document selection. Text path accepts a SelectionTarget or ref plus replacement text. Structural path accepts a BlockNodeAddress (replaces whole block), SelectionTarget (expands to full covered block boundaries), or ref plus SDFragment content. | +| `doc.insert` | `insert` | Insert inline content at a text position within an existing block, or at the end of the document when target is omitted. This is NOT for creating sibling blocks — use create.paragraph, create.heading, or lists.insert for that. Accepts two input shapes: legacy string-based (value + type) or structural SDFragment (content). Supports text (default), markdown, and html content types via the `type` field in legacy mode. Structural mode accepts an SDFragment with typed nodes (paragraphs, tables, images, etc.). | +| `doc.replace` | `replace` | Replace content at a contiguous document selection. Text path accepts a SelectionTarget or ref plus replacement text. Structural path accepts an SDAddress, SelectionTarget, or ref plus SDFragment content. | | `doc.delete` | `delete` | Delete content at a contiguous document selection. Accepts a SelectionTarget or mutation-ready ref. Supports cross-block deletion and optional block-edge expansion via behavior mode. | | `doc.blocks.list` | `blocks list` | List top-level blocks in document order with IDs, types, and text previews. Supports pagination via offset/limit and optional nodeType filtering. | | `doc.blocks.delete` | `blocks delete` | Delete an entire block node (paragraph, heading, list item, table, image, or sdt) deterministically. | @@ -811,8 +811,8 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.markdown_to_fragment` | `markdown-to-fragment` | Convert a Markdown string into an SDM/1 structural fragment. | | `doc.info` | `info` | Return document metadata including revision, node count, and capabilities. | | `doc.clear_content` | `clear-content` | Clear all document body content, leaving a single empty paragraph. | -| `doc.insert` | `insert` | Insert content into the document. Two input shapes: legacy string-based (value + type) inserts inline content at a text position within an existing block; structural SDFragment (content) inserts one or more blocks as siblings relative to a BlockNodeAddress target. When target is omitted, content appends at the end of the document. Legacy mode supports text (default), markdown, and html content types via the `type` field. Structural mode uses `placement` (before/after/insideStart/insideEnd) to position relative to the target block. | -| `doc.replace` | `replace` | Replace content at a contiguous document selection. Text path accepts a SelectionTarget or ref plus replacement text. Structural path accepts a BlockNodeAddress (replaces whole block), SelectionTarget (expands to full covered block boundaries), or ref plus SDFragment content. | +| `doc.insert` | `insert` | Insert inline content at a text position within an existing block, or at the end of the document when target is omitted. This is NOT for creating sibling blocks — use create.paragraph, create.heading, or lists.insert for that. Accepts two input shapes: legacy string-based (value + type) or structural SDFragment (content). Supports text (default), markdown, and html content types via the `type` field in legacy mode. Structural mode accepts an SDFragment with typed nodes (paragraphs, tables, images, etc.). | +| `doc.replace` | `replace` | Replace content at a contiguous document selection. Text path accepts a SelectionTarget or ref plus replacement text. Structural path accepts an SDAddress, SelectionTarget, or ref plus SDFragment content. | | `doc.delete` | `delete` | Delete content at a contiguous document selection. Accepts a SelectionTarget or mutation-ready ref. Supports cross-block deletion and optional block-edge expansion via behavior mode. | | `doc.blocks.list` | `blocks list` | List top-level blocks in document order with IDs, types, and text previews. Supports pagination via offset/limit and optional nodeType filtering. | | `doc.blocks.delete` | `blocks delete` | Delete an entire block node (paragraph, heading, list item, table, image, or sdt) deterministically. | diff --git a/apps/mcp/README.md b/apps/mcp/README.md index 149a664467..cd201f62da 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -67,7 +67,12 @@ Add to `~/.codeium/windsurf/mcp_config.json`: ## Tools -23 tools in eight groups. All tools take a `session_id` from `superdoc_open`. +The MCP server exposes 12 tools total: + +- 3 lifecycle tools: `superdoc_open`, `superdoc_save`, `superdoc_close` +- 9 grouped intent tools generated from the SDK catalog + +All tools except `superdoc_open` take a `session_id` from `superdoc_open`. ### Lifecycle @@ -77,78 +82,37 @@ Add to `~/.codeium/windsurf/mcp_config.json`: | `superdoc_save` | Save the document to disk (original path or custom `out` path) | | `superdoc_close` | Close the session and release memory | -### Query - -| Tool | Description | -| --- | --- | -| `superdoc_find` | Search by text pattern, node type, or both. Returns addresses for mutations | -| `superdoc_get_node` | Get details about a specific node | -| `superdoc_info` | Document metadata and structure | -| `superdoc_get_text` | Full plain text of the document | - -### Mutation - -| Tool | Description | -| --- | --- | -| `superdoc_insert` | Insert text at a position. Set `suggest=true` for tracked changes | -| `superdoc_replace` | Replace content at a range. Set `suggest=true` for tracked changes | -| `superdoc_delete` | Delete content at a range. Set `suggest=true` for tracked changes | - -### Format - -| Tool | Description | -| --- | --- | -| `superdoc_format` | Toggle formatting (`bold`, `italic`, `underline`, `strikethrough`). Set `suggest=true` for tracked changes | - -### Create +### Intent tools -| Tool | Description | -| --- | --- | -| `superdoc_create` | Create a block element (`paragraph`, `heading`). Set `suggest=true` for tracked changes | - -### Track changes - -| Tool | Description | -| --- | --- | -| `superdoc_list_changes` | List all tracked changes with type, author, and excerpt | -| `superdoc_accept_change` | Accept a single tracked change | -| `superdoc_reject_change` | Reject a single tracked change | -| `superdoc_accept_all_changes` | Accept all tracked changes | -| `superdoc_reject_all_changes` | Reject all tracked changes | - -### Comments - -| Tool | Description | -| --- | --- | -| `superdoc_add_comment` | Add a comment anchored to a text range | -| `superdoc_list_comments` | List all comments with author, status, and anchored text | -| `superdoc_reply_comment` | Reply to an existing comment thread | -| `superdoc_resolve_comment` | Mark a comment as resolved | - -### Lists - -| Tool | Description | -| --- | --- | -| `superdoc_insert_list` | Insert a list item before or after an existing one | -| `superdoc_list_set_type` | Change a list between ordered and bullet | +| Tool | Actions | Description | +| --- | --- | --- | +| `superdoc_get_content` | `text`, `markdown`, `html`, `info` | Read document content in different formats | +| `superdoc_search` | `match` | Find text or nodes and return handles or addresses for later edits | +| `superdoc_edit` | `insert`, `replace`, `delete`, `undo`, `redo` | Perform text edits and history actions | +| `superdoc_format` | `inline`, `set_style`, `set_alignment`, `set_indentation`, `set_spacing` | Apply inline or paragraph formatting | +| `superdoc_create` | `paragraph`, `heading` | Create structural block elements | +| `superdoc_list` | `insert`, `create`, `detach`, `indent`, `outdent`, `set_level`, `set_type` | Create and manipulate lists | +| `superdoc_comment` | `create`, `update`, `delete`, `get`, `list` | Manage comment threads | +| `superdoc_track_changes` | `list`, `decide` | Review and resolve tracked changes | +| `superdoc_mutations` | `preview`, `apply` | Execute multi-step atomic edits as a batch | ## Workflow Every interaction follows the same pattern: ``` -open → read/edit → save → close +open → read/search → edit → save → close ``` 1. `superdoc_open` loads a document and returns a `session_id` -2. `superdoc_find` locates content and returns addresses -3. Edit tools use those addresses to modify content +2. `superdoc_get_content` reads the current document and `superdoc_search` finds stable handles or addresses +3. Intent tools use `session_id` plus `action` to modify content 4. `superdoc_save` writes changes to disk 5. `superdoc_close` releases the session -### Suggesting mode +### Tracked changes -Set `suggest=true` on any mutation, format, or create tool to make edits appear as tracked changes (suggestions) instead of direct edits. Use `superdoc_list_changes` to review them, and `superdoc_accept_change` / `superdoc_reject_change` to resolve them. +Actions that support tracked edits use the underlying Document API's `changeMode: "tracked"` option. Review or resolve tracked edits with `superdoc_track_changes`. ## Development diff --git a/apps/mcp/package.json b/apps/mcp/package.json index 6a7f1692fe..989bd394d1 100644 --- a/apps/mcp/package.json +++ b/apps/mcp/package.json @@ -15,11 +15,12 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@superdoc-dev/sdk": "workspace:*", + "@superdoc/document-api": "workspace:*", "@modelcontextprotocol/sdk": "^1.26.0", "zod": "^4.3.6" }, "devDependencies": { - "@superdoc/document-api": "workspace:*", "@superdoc/super-editor": "workspace:*", "superdoc": "workspace:*", "@types/bun": "catalog:", diff --git a/apps/mcp/src/__tests__/protocol.test.ts b/apps/mcp/src/__tests__/protocol.test.ts index a70864982b..4b5f7f5f3b 100644 --- a/apps/mcp/src/__tests__/protocol.test.ts +++ b/apps/mcp/src/__tests__/protocol.test.ts @@ -6,30 +6,22 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' const BLANK_DOCX = resolve(import.meta.dir, '../../../../shared/common/data/blank.docx'); const SERVER_ENTRY = resolve(import.meta.dir, '../index.ts'); +// 3 lifecycle + 9 intent tools from the generated catalog const EXPECTED_TOOLS = [ + // Lifecycle 'superdoc_open', 'superdoc_save', 'superdoc_close', - 'superdoc_find', - 'superdoc_get_node', - 'superdoc_info', - 'superdoc_get_text', - 'superdoc_insert', - 'superdoc_replace', - 'superdoc_delete', + // Intent tools (from catalog.json) + 'superdoc_get_content', + 'superdoc_edit', 'superdoc_format', 'superdoc_create', - 'superdoc_list_changes', - 'superdoc_accept_change', - 'superdoc_reject_change', - 'superdoc_accept_all_changes', - 'superdoc_reject_all_changes', - 'superdoc_add_comment', - 'superdoc_list_comments', - 'superdoc_reply_comment', - 'superdoc_resolve_comment', - 'superdoc_insert_list', - 'superdoc_list_create', + 'superdoc_list', + 'superdoc_comment', + 'superdoc_track_changes', + 'superdoc_search', + 'superdoc_mutations', ]; function textContent(result: Awaited>): string { @@ -79,7 +71,38 @@ describe('MCP protocol integration', () => { } }); - it('open → info → get_text → close workflow', async () => { + it('intent tools have action enum in schema', async () => { + await ready; + const { tools } = await client.listTools(); + + // Multi-action intent tools should have an "action" property with an enum + const multiActionTools = tools.filter( + (t) => !['superdoc_open', 'superdoc_save', 'superdoc_close', 'superdoc_search'].includes(t.name), + ); + + for (const tool of multiActionTools) { + const schema = tool.inputSchema as { properties?: Record }; + expect(schema.properties?.action).toBeDefined(); + expect(schema.properties!.action.enum).toBeArray(); + expect(schema.properties!.action.enum!.length).toBeGreaterThan(0); + } + }); + + it('intent tools have session_id in schema', async () => { + await ready; + const { tools } = await client.listTools(); + + // All intent tools (not lifecycle open) should require session_id + const intentTools = tools.filter((t) => !['superdoc_open', 'superdoc_save', 'superdoc_close'].includes(t.name)); + + for (const tool of intentTools) { + const schema = tool.inputSchema as { properties?: Record; required?: string[] }; + expect(schema.properties?.session_id).toBeDefined(); + expect(schema.required).toContain('session_id'); + } + }); + + it('open → get_content → close workflow', async () => { await ready; // Open @@ -90,21 +113,27 @@ describe('MCP protocol integration', () => { const sid = opened.session_id; - // Info - const infoResult = await client.callTool({ name: 'superdoc_info', arguments: { session_id: sid } }); - expect(textContent(infoResult)).toBeTruthy(); - - // Get text - const textResult = await client.callTool({ name: 'superdoc_get_text', arguments: { session_id: sid } }); + // Get content as text + const textResult = await client.callTool({ + name: 'superdoc_get_content', + arguments: { session_id: sid, action: 'text' }, + }); expect(textContent(textResult)).toBeDefined(); + // Get content as info + const infoResult = await client.callTool({ + name: 'superdoc_get_content', + arguments: { session_id: sid, action: 'info' }, + }); + expect(textContent(infoResult)).toBeTruthy(); + // Close const closeResult = await client.callTool({ name: 'superdoc_close', arguments: { session_id: sid } }); const closed = parseContent(closeResult) as { closed: boolean }; expect(closed.closed).toBe(true); }); - it('open → create → find → save → close workflow', async () => { + it('open → create → search → save → close workflow', async () => { await ready; // Open @@ -114,16 +143,19 @@ describe('MCP protocol integration', () => { // Create a paragraph const createResult = await client.callTool({ name: 'superdoc_create', - arguments: { session_id: sid, type: 'paragraph', text: 'MCP integration test' }, + arguments: { session_id: sid, action: 'paragraph', text: 'MCP integration test' }, }); - expect(textContent(createResult)).toContain('success'); - - // Find it - const findResult = await client.callTool({ - name: 'superdoc_find', - arguments: { session_id: sid, pattern: 'MCP integration' }, + expect(textContent(createResult)).toBeTruthy(); + + // Search for it + const searchResult = await client.callTool({ + name: 'superdoc_search', + arguments: { + session_id: sid, + select: { type: 'text', pattern: 'MCP integration' }, + }, }); - const found = parseContent(findResult) as { matches: unknown[]; total: number }; + const found = parseContent(searchResult) as { matches: unknown[]; total: number }; expect(found.total).toBeGreaterThan(0); // Save to temp path @@ -147,8 +179,11 @@ describe('MCP protocol integration', () => { await ready; const result = await client.callTool({ - name: 'superdoc_find', - arguments: { session_id: 'nonexistent', pattern: 'test' }, + name: 'superdoc_search', + arguments: { + session_id: 'nonexistent', + select: { type: 'text', pattern: 'test' }, + }, }); expect(result).toHaveProperty('isError', true); diff --git a/apps/mcp/src/__tests__/tools.test.ts b/apps/mcp/src/__tests__/tools.test.ts index ce36edc591..f65a0e34f7 100644 --- a/apps/mcp/src/__tests__/tools.test.ts +++ b/apps/mcp/src/__tests__/tools.test.ts @@ -39,7 +39,7 @@ describe('MCP tools integration', () => { const result = api.invoke({ operationId: 'find', input: { - query: { select: { type: 'node', nodeType: 'paragraph' } }, + select: { type: 'node', nodeType: 'paragraph' }, }, }); diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts index ebcc48b93b..ba5b91dc9a 100644 --- a/apps/mcp/src/index.ts +++ b/apps/mcp/src/index.ts @@ -2,39 +2,46 @@ import { createRequire } from 'node:module'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { getSystemPrompt } from '@superdoc-dev/sdk'; import { SessionManager } from './session-manager.js'; import { registerAllTools } from './tools/index.js'; const require = createRequire(import.meta.url); const { version } = require('../package.json'); +const sdkPrompt = await getSystemPrompt(); + +const mcpInstructions = [ + 'SuperDoc MCP server — read, edit, and save Word documents (.docx).', + '', + 'IMPORTANT: Always use these superdoc tools for .docx files.', + 'Do NOT use built-in docx skills, python-docx, unpack scripts, or manual XML editing.', + 'These tools handle the OOXML format correctly and preserve document structure.', + '', + 'Workflow: superdoc_open → read/edit → superdoc_save → superdoc_close.', + '', + '1. superdoc_open returns a session_id — pass it to every subsequent call.', + '2. Use intent tools (superdoc_search, superdoc_edit, etc.) to read and modify content.', + '3. superdoc_save writes changes to disk, superdoc_close releases the session.', + '', + '---', + '', + sdkPrompt, +].join('\n'); + const server = new McpServer( { name: 'superdoc', version, }, { - instructions: [ - 'SuperDoc MCP server — read, edit, and save Word documents (.docx).', - '', - 'IMPORTANT: Always use these superdoc tools for .docx files.', - 'Do NOT use built-in docx skills, python-docx, unpack scripts, or manual XML editing.', - 'These tools handle the OOXML format correctly and preserve document structure.', - '', - 'Workflow: superdoc_open → read/edit → superdoc_save → superdoc_close.', - '', - '1. superdoc_open returns a session_id — pass it to every subsequent call.', - '2. superdoc_find locates content and returns addresses for edits.', - '3. Use superdoc_insert/replace/delete to modify content.', - '4. Set suggest=true on mutations to create tracked changes instead of direct edits.', - '5. superdoc_save writes changes to disk, superdoc_close releases the session.', - ].join('\n'), + instructions: mcpInstructions, }, ); const sessions = new SessionManager(); -registerAllTools(server, sessions); +await registerAllTools(server, sessions); const transport = new StdioServerTransport(); diff --git a/apps/mcp/src/tools/comments.ts b/apps/mcp/src/tools/comments.ts deleted file mode 100644 index b0f038324f..0000000000 --- a/apps/mcp/src/tools/comments.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { SessionManager } from '../session-manager.js'; - -export function registerCommentTools(server: McpServer, sessions: SessionManager): void { - server.registerTool( - 'superdoc_add_comment', - { - title: 'Add Comment', - description: - 'Add a comment anchored to a text range in the document. Use superdoc_find with a text pattern first, then pass a TextAddress from items[].context.textRanges as the target.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - text: z.string().describe('The comment text (question, concern, or feedback).'), - target: z - .string() - .describe( - 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find items[].context.textRanges.', - ), - }, - annotations: { readOnlyHint: false }, - }, - async ({ session_id, text, target }) => { - try { - const { api } = sessions.get(session_id); - const parsed = JSON.parse(target); - const result = api.invoke({ - operationId: 'comments.create', - input: { text, target: parsed }, - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Add comment failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_list_comments', - { - title: 'List Comments', - description: - 'List all comments in the document. Returns comment text, author, status (open/resolved), and the text range each comment is anchored to.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - include_resolved: z.boolean().optional().describe('Include resolved comments. Defaults to true.'), - }, - annotations: { readOnlyHint: true }, - }, - async ({ session_id, include_resolved }) => { - try { - const { api } = sessions.get(session_id); - const input: Record = {}; - if (include_resolved != null) input.includeResolved = include_resolved; - - const result = api.invoke({ operationId: 'comments.list', input }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `List comments failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_reply_comment', - { - title: 'Reply to Comment', - description: 'Reply to an existing comment thread. Use the comment ID from superdoc_list_comments.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - comment_id: z.string().describe('The parent comment ID to reply to (from superdoc_list_comments).'), - text: z.string().describe('The reply text.'), - }, - annotations: { readOnlyHint: false }, - }, - async ({ session_id, comment_id, text }) => { - try { - const { api } = sessions.get(session_id); - const result = api.invoke({ - operationId: 'comments.create', - input: { parentCommentId: comment_id, text }, - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Reply failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_resolve_comment', - { - title: 'Resolve Comment', - description: 'Mark a comment as resolved. Use the comment ID from superdoc_list_comments.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - comment_id: z.string().describe('The comment ID to resolve (from superdoc_list_comments).'), - }, - annotations: { readOnlyHint: false }, - }, - async ({ session_id, comment_id }) => { - try { - const { api } = sessions.get(session_id); - const result = api.invoke({ - operationId: 'comments.patch', - input: { commentId: comment_id, status: 'resolved' }, - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Resolve failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); -} diff --git a/apps/mcp/src/tools/create.ts b/apps/mcp/src/tools/create.ts deleted file mode 100644 index f623d3f901..0000000000 --- a/apps/mcp/src/tools/create.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { SessionManager } from '../session-manager.js'; - -const TYPES = ['paragraph', 'heading'] as const; - -export function registerCreateTools(server: McpServer, sessions: SessionManager): void { - server.registerTool( - 'superdoc_create', - { - title: 'Create Block', - description: - 'Create a new block element in the document. Supports paragraphs and headings. Optionally specify text content and position. Set suggest=true to create as a tracked change (suggestion).', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - type: z.enum(TYPES).describe('The type of block to create.'), - text: z.string().optional().describe('Text content for the new block.'), - level: z.number().min(1).max(6).optional().describe('Heading level (1-6). Required when type is "heading".'), - at: z - .string() - .optional() - .describe('JSON-encoded position specifying where to create the block. If omitted, appends to the end.'), - suggest: z - .boolean() - .optional() - .describe( - 'If true, create as a tracked change (suggestion) that can be accepted or rejected later. Defaults to false (direct edit).', - ), - }, - annotations: { readOnlyHint: false }, - }, - async ({ session_id, type, text, level, at, suggest }) => { - try { - const { api } = sessions.get(session_id); - const input: Record = {}; - if (text != null) input.text = text; - if (level != null) input.level = level; - if (at != null) input.at = JSON.parse(at); - - const result = api.invoke({ - operationId: `create.${type}`, - input, - options: suggest ? { changeMode: 'tracked' as const } : undefined, - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Create failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); -} diff --git a/apps/mcp/src/tools/format.ts b/apps/mcp/src/tools/format.ts deleted file mode 100644 index 1acab87ce9..0000000000 --- a/apps/mcp/src/tools/format.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { SessionManager } from '../session-manager.js'; - -const STYLES = ['bold', 'italic', 'underline', 'strikethrough'] as const; -const INLINE_BY_STYLE = { - bold: { bold: 'on' }, - italic: { italic: 'on' }, - underline: { underline: 'on' }, - strikethrough: { strike: 'on' }, -} as const; - -export function registerFormatTools(server: McpServer, sessions: SessionManager): void { - server.registerTool( - 'superdoc_format', - { - title: 'Format Text', - description: - 'Apply formatting on a text range. Use superdoc_find with a text pattern first, then pass a TextAddress from items[].context.textRanges as the target. Set suggest=true to format as a tracked change (suggestion).', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - style: z.enum(STYLES).describe('The formatting style to apply.'), - target: z - .string() - .describe( - 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find items[].context.textRanges.', - ), - suggest: z - .boolean() - .optional() - .describe( - 'If true, format as a tracked change (suggestion) that can be accepted or rejected later. Defaults to false (direct edit).', - ), - }, - annotations: { readOnlyHint: false }, - }, - async ({ session_id, style, target, suggest }) => { - try { - const { api } = sessions.get(session_id); - const parsed = JSON.parse(target); - const result = api.invoke({ - operationId: 'format.apply', - input: { target: parsed, inline: INLINE_BY_STYLE[style] }, - options: suggest ? { changeMode: 'tracked' as const } : undefined, - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Format failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); -} diff --git a/apps/mcp/src/tools/index.ts b/apps/mcp/src/tools/index.ts index 7c17736789..a22dac99bf 100644 --- a/apps/mcp/src/tools/index.ts +++ b/apps/mcp/src/tools/index.ts @@ -1,21 +1,9 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { SessionManager } from '../session-manager.js'; import { registerLifecycleTools } from './lifecycle.js'; -import { registerQueryTools } from './query.js'; -import { registerMutationTools } from './mutation.js'; -import { registerFormatTools } from './format.js'; -import { registerCreateTools } from './create.js'; -import { registerTrackChangesTools } from './track-changes.js'; -import { registerCommentTools } from './comments.js'; -import { registerListTools } from './lists.js'; +import { registerIntentTools } from './intent.js'; -export function registerAllTools(server: McpServer, sessions: SessionManager): void { +export async function registerAllTools(server: McpServer, sessions: SessionManager): Promise { registerLifecycleTools(server, sessions); - registerQueryTools(server, sessions); - registerMutationTools(server, sessions); - registerFormatTools(server, sessions); - registerCreateTools(server, sessions); - registerTrackChangesTools(server, sessions); - registerCommentTools(server, sessions); - registerListTools(server, sessions); + await registerIntentTools(server, sessions); } diff --git a/apps/mcp/src/tools/intent.ts b/apps/mcp/src/tools/intent.ts new file mode 100644 index 0000000000..e69753ab8c --- /dev/null +++ b/apps/mcp/src/tools/intent.ts @@ -0,0 +1,158 @@ +/** + * Register intent-based tools from the generated catalog. + * + * Reads catalog.json and registers each intent tool with the MCP server. + * Tool dispatch is handled by the generated dispatchIntentTool function, + * routing through DocumentApi.invoke(). + */ + +import { z } from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { SessionManager } from '../session-manager.js'; +import type { DocumentApi, DynamicInvokeRequest } from '@superdoc/document-api'; +import { dispatchIntentTool, getToolCatalog } from '@superdoc-dev/sdk'; + +// --------------------------------------------------------------------------- +// Types for the generated catalog +// --------------------------------------------------------------------------- + +interface CatalogTool { + toolName: string; + description: string; + inputSchema: { + type: string; + properties?: Record>; + required?: string[]; + additionalProperties?: boolean; + }; + mutates: boolean; + operations: Array<{ operationId: string; intentAction: string }>; +} + +interface Catalog { + toolCount: number; + tools: CatalogTool[]; +} + +// --------------------------------------------------------------------------- +// JSON Schema → Zod conversion (minimal, for MCP tool registration) +// --------------------------------------------------------------------------- + +function jsonSchemaPropertyToZod(prop: Record): z.ZodTypeAny { + const desc = prop.description as string | undefined; + const type = prop.type as string | undefined; + + if (prop.enum) { + const values = prop.enum as string[]; + if (values.length > 0) { + return z.enum(values as [string, ...string[]]).describe(desc ?? ''); + } + } + + // Complex schemas (oneOf, anyOf, allOf) — pass through as opaque; + // DocumentApi validates the actual payload at dispatch time. + if (prop.oneOf || prop.anyOf || prop.allOf) { + return desc ? z.unknown().describe(desc) : z.unknown(); + } + + switch (type) { + case 'string': + return desc ? z.string().describe(desc) : z.string(); + case 'number': + case 'integer': + return desc ? z.number().describe(desc) : z.number(); + case 'boolean': + return desc ? z.boolean().describe(desc) : z.boolean(); + case 'array': + // Note: z.array(z.unknown()) is safe but z.record() is not — the MCP SDK's + // z4-mini toJSONSchema cannot convert z.record() from zod v4 classic. + return desc ? z.array(z.unknown()).describe(desc) : z.array(z.unknown()); + case 'object': + // Use z.unknown() instead of z.record() to avoid MCP SDK Zod v4 classic/mini + // incompatibility. DocumentApi validates the actual shape at dispatch time. + return desc ? z.unknown().describe(desc) : z.unknown(); + default: + return desc ? z.unknown().describe(desc) : z.unknown(); + } +} + +/** + * Build a Zod schema from a catalog tool's inputSchema. + * Adds session_id and strips doc/sessionId (managed by MCP server). + */ +function buildZodSchema(tool: CatalogTool): Record { + const shape: Record = { + session_id: z.string().describe('Session ID from superdoc_open.'), + }; + + const props = tool.inputSchema.properties ?? {}; + const required = new Set(tool.inputSchema.required ?? []); + + for (const [key, prop] of Object.entries(props)) { + // Skip session/doc params — the MCP server manages these + if (key === 'doc' || key === 'sessionId') continue; + + let zodType = jsonSchemaPropertyToZod(prop); + if (!required.has(key)) { + zodType = zodType.optional(); + } + shape[key] = zodType; + } + + return shape; +} + +// --------------------------------------------------------------------------- +// Execute an operation via DocumentApi.invoke() +// --------------------------------------------------------------------------- + +function executeOperation(api: DocumentApi, operationId: string, input: Record): unknown { + // Generated dispatch uses 'doc.' prefix (e.g. 'doc.query.match'); strip it for DocumentApi.invoke() + const opId = operationId.startsWith('doc.') ? operationId.slice(4) : operationId; + return api.invoke({ operationId: opId, input } as DynamicInvokeRequest); +} + +// --------------------------------------------------------------------------- +// Register all intent tools +// --------------------------------------------------------------------------- + +export async function registerIntentTools(server: McpServer, sessions: SessionManager): Promise { + const catalog = (await getToolCatalog()) as unknown as Catalog; + + for (const tool of catalog.tools) { + const zodSchema = buildZodSchema(tool); + const isMutation = tool.mutates; + + server.registerTool( + tool.toolName, + { + title: tool.toolName.replace(/^superdoc_/, '').replace(/_/g, ' '), + description: tool.description, + inputSchema: zodSchema, + annotations: { + readOnlyHint: !isMutation, + ...(isMutation ? { destructiveHint: false } : {}), + }, + }, + async (args) => { + try { + const { session_id, ...toolArgs } = args as Record; + const { api } = sessions.get(session_id as string); + + const result = await dispatchIntentTool(tool.toolName, toolArgs, (opId, input) => + executeOperation(api, opId, input), + ); + + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + } catch (err) { + return { + content: [{ type: 'text' as const, text: `${tool.toolName} failed: ${(err as Error).message}` }], + isError: true, + }; + } + }, + ); + } +} diff --git a/apps/mcp/src/tools/lists.ts b/apps/mcp/src/tools/lists.ts deleted file mode 100644 index 1f5e2aefa1..0000000000 --- a/apps/mcp/src/tools/lists.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { SessionManager } from '../session-manager.js'; - -export function registerListTools(server: McpServer, sessions: SessionManager): void { - server.registerTool( - 'superdoc_insert_list', - { - title: 'Insert List Item', - description: - 'Insert a new list item before or after an existing one. To start a new list, use superdoc_create with type "paragraph" first, then convert it. Or use superdoc_find to locate an existing list item.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - target: z - .string() - .describe('JSON-encoded list item address from superdoc_find or superdoc_list_items results.'), - position: z.enum(['before', 'after']).describe('Insert before or after the target item.'), - text: z.string().optional().describe('Text content for the new list item.'), - }, - annotations: { readOnlyHint: false }, - }, - async ({ session_id, target, position, text }) => { - try { - const { api } = sessions.get(session_id); - const parsed = JSON.parse(target); - const input: Record = { target: parsed, position }; - if (text != null) input.text = text; - - const result = api.invoke({ - operationId: 'lists.insert', - input, - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Insert list item failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_list_create', - { - title: 'Create List', - description: - 'Create a new list from one or more existing paragraphs. Use superdoc_find to locate paragraph addresses first.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - target: z - .string() - .describe( - 'JSON-encoded block address (or range) of the paragraph(s) to convert. Use { "kind": "block", "nodeType": "paragraph", "nodeId": "..." }.', - ), - kind: z.enum(['ordered', 'bullet']).describe('The list type to create.'), - }, - annotations: { readOnlyHint: false }, - }, - async ({ session_id, target, kind }) => { - try { - const { api } = sessions.get(session_id); - const parsed = JSON.parse(target); - const result = api.invoke({ - operationId: 'lists.create', - input: { mode: 'fromParagraphs', target: parsed, kind }, - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Create list failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); -} diff --git a/apps/mcp/src/tools/mutation.ts b/apps/mcp/src/tools/mutation.ts deleted file mode 100644 index fbd21bd867..0000000000 --- a/apps/mcp/src/tools/mutation.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { SessionManager } from '../session-manager.js'; - -function mutationOptions(suggest?: boolean) { - return suggest ? { changeMode: 'tracked' as const } : undefined; -} - -export function registerMutationTools(server: McpServer, sessions: SessionManager): void { - server.registerTool( - 'superdoc_insert', - { - title: 'Insert Text', - description: - 'Insert text at a target position in the document. Use superdoc_find first, then pass a TextAddress from items[].context.textRanges as the target. Set suggest=true to insert as a tracked change (suggestion) instead of a direct edit.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - text: z.string().describe('The text content to insert.'), - target: z - .string() - .describe( - 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find items[].context.textRanges.', - ), - suggest: z - .boolean() - .optional() - .describe( - 'If true, insert as a tracked change (suggestion) that can be accepted or rejected later. Defaults to false (direct edit).', - ), - }, - annotations: { readOnlyHint: false }, - }, - async ({ session_id, text, target, suggest }) => { - try { - const { api } = sessions.get(session_id); - const parsed = JSON.parse(target); - const result = api.invoke({ - operationId: 'insert', - input: { text, target: parsed }, - options: mutationOptions(suggest), - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Insert failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_replace', - { - title: 'Replace Text', - description: - 'Replace content at a target range with new text. Use superdoc_find with a text pattern first, then pass a TextAddress from items[].context.textRanges as the target. Set suggest=true to make the replacement a tracked change (suggestion).', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - text: z.string().describe('The replacement text.'), - target: z - .string() - .describe( - 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find items[].context.textRanges.', - ), - suggest: z - .boolean() - .optional() - .describe( - 'If true, replace as a tracked change (suggestion) that can be accepted or rejected later. Defaults to false (direct edit).', - ), - }, - annotations: { readOnlyHint: false }, - }, - async ({ session_id, text, target, suggest }) => { - try { - const { api } = sessions.get(session_id); - const parsed = JSON.parse(target); - const result = api.invoke({ - operationId: 'replace', - input: { text, target: parsed }, - options: mutationOptions(suggest), - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Replace failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_delete', - { - title: 'Delete Content', - description: - 'Delete content at a target range. Use superdoc_find with a text pattern first, then pass a TextAddress from items[].context.textRanges as the target. Set suggest=true to delete as a tracked change (suggestion).', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - target: z - .string() - .describe( - 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find items[].context.textRanges.', - ), - suggest: z - .boolean() - .optional() - .describe( - 'If true, delete as a tracked change (suggestion) that can be accepted or rejected later. Defaults to false (direct edit).', - ), - }, - annotations: { readOnlyHint: false, destructiveHint: true }, - }, - async ({ session_id, target, suggest }) => { - try { - const { api } = sessions.get(session_id); - const parsed = JSON.parse(target); - const result = api.invoke({ - operationId: 'delete', - input: { target: parsed }, - options: mutationOptions(suggest), - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Delete failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); -} diff --git a/apps/mcp/src/tools/query.ts b/apps/mcp/src/tools/query.ts deleted file mode 100644 index 3196197c88..0000000000 --- a/apps/mcp/src/tools/query.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { SessionManager } from '../session-manager.js'; - -export function registerQueryTools(server: McpServer, sessions: SessionManager): void { - server.registerTool( - 'superdoc_find', - { - title: 'Find in Document', - description: - 'Search the document for nodes matching a type, text pattern, or both. For text searches, each item in items[] includes context.textRanges — these are the TextAddress objects you pass as "target" to replace/insert/delete/format tools.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - type: z.string().optional().describe('Node type to filter by (e.g. "heading", "paragraph", "table", "image").'), - pattern: z.string().optional().describe('Text pattern to search for (substring match).'), - limit: z.number().optional().describe('Maximum number of results.'), - offset: z.number().optional().describe('Skip this many results (for pagination).'), - }, - annotations: { readOnlyHint: true }, - }, - async ({ session_id, type, pattern, limit, offset }) => { - try { - const { api } = sessions.get(session_id); - - // Build a Selector or Query object directly — find accepts both. - // Selector: { type: 'text', pattern } or { type: 'node', nodeType } - // Query: { select: Selector, limit?, offset? } - let selector: Record; - if (pattern) { - selector = { type: 'text', pattern, mode: 'contains' }; - } else if (type) { - selector = { type: 'node', nodeType: type }; - } else { - selector = { type: 'node' }; - } - - const input: Record = - limit != null || offset != null ? { select: selector, limit, offset } : selector; - - const result = api.invoke({ operationId: 'find', input }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Find failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_get_node', - { - title: 'Get Node', - description: - 'Get detailed information about a specific document node by its address (from superdoc_find results).', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - address: z.string().describe('JSON-encoded node address from superdoc_find results.'), - }, - annotations: { readOnlyHint: true }, - }, - async ({ session_id, address }) => { - try { - const { api } = sessions.get(session_id); - const parsed = JSON.parse(address); - const result = api.invoke({ operationId: 'getNode', input: parsed }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Get node failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_info', - { - title: 'Document Info', - description: 'Return document metadata: structure summary, node counts, and capabilities.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - }, - annotations: { readOnlyHint: true }, - }, - async ({ session_id }) => { - try { - const { api } = sessions.get(session_id); - const result = api.invoke({ operationId: 'info', input: {} }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Info failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_get_text', - { - title: 'Get Document Text', - description: 'Return the full plain-text content of the document.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - }, - annotations: { readOnlyHint: true }, - }, - async ({ session_id }) => { - try { - const { api } = sessions.get(session_id); - const result = api.invoke({ operationId: 'getText', input: {} }); - return { - content: [{ type: 'text' as const, text: typeof result === 'string' ? result : JSON.stringify(result) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Get text failed: ${(err as Error).message}` }], - isError: true, - }; - } - }, - ); -} diff --git a/apps/mcp/src/tools/track-changes.ts b/apps/mcp/src/tools/track-changes.ts deleted file mode 100644 index 41a5b1f84a..0000000000 --- a/apps/mcp/src/tools/track-changes.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { SessionManager } from '../session-manager.js'; - -function toErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - -export function registerTrackChangesTools(server: McpServer, sessions: SessionManager): void { - server.registerTool( - 'superdoc_list_changes', - { - title: 'List Tracked Changes', - description: - 'List all tracked changes (suggestions) in the document. Returns change type (insert/delete/format), author, date, and excerpt for each.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - type: z.enum(['insert', 'delete', 'format']).optional().describe('Filter by change type.'), - limit: z.number().optional().describe('Maximum number of results.'), - offset: z.number().optional().describe('Skip this many results (for pagination).'), - }, - annotations: { readOnlyHint: true }, - }, - async ({ session_id, type, limit, offset }) => { - try { - const { api } = sessions.get(session_id); - const input: Record = {}; - if (type != null) input.type = type; - if (limit != null) input.limit = limit; - if (offset != null) input.offset = offset; - - const result = api.invoke({ operationId: 'trackChanges.list', input }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `List changes failed: ${toErrorMessage(err)}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_accept_change', - { - title: 'Accept Tracked Change', - description: - 'Accept a single tracked change (suggestion), applying it to the document. Use the change ID from superdoc_list_changes.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - id: z.string().describe('The tracked change ID from superdoc_list_changes results.'), - }, - annotations: { readOnlyHint: false }, - }, - async ({ session_id, id }) => { - try { - const { api } = sessions.get(session_id); - const result = api.invoke({ - operationId: 'trackChanges.decide', - input: { decision: 'accept', target: { id } }, - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Accept change failed: ${toErrorMessage(err)}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_reject_change', - { - title: 'Reject Tracked Change', - description: - 'Reject a single tracked change (suggestion), reverting it from the document. Use the change ID from superdoc_list_changes.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - id: z.string().describe('The tracked change ID from superdoc_list_changes results.'), - }, - annotations: { readOnlyHint: false }, - }, - async ({ session_id, id }) => { - try { - const { api } = sessions.get(session_id); - const result = api.invoke({ - operationId: 'trackChanges.decide', - input: { decision: 'reject', target: { id } }, - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Reject change failed: ${toErrorMessage(err)}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_accept_all_changes', - { - title: 'Accept All Tracked Changes', - description: 'Accept all tracked changes (suggestions) in the document, applying them all.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - }, - annotations: { readOnlyHint: false }, - }, - async ({ session_id }) => { - try { - const { api } = sessions.get(session_id); - const result = api.invoke({ - operationId: 'trackChanges.decide', - input: { decision: 'accept', target: { scope: 'all' } }, - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Accept all failed: ${toErrorMessage(err)}` }], - isError: true, - }; - } - }, - ); - - server.registerTool( - 'superdoc_reject_all_changes', - { - title: 'Reject All Tracked Changes', - description: 'Reject all tracked changes (suggestions) in the document, reverting them all.', - inputSchema: { - session_id: z.string().describe('Session ID from superdoc_open.'), - }, - annotations: { readOnlyHint: false, destructiveHint: true }, - }, - async ({ session_id }) => { - try { - const { api } = sessions.get(session_id); - const result = api.invoke({ - operationId: 'trackChanges.decide', - input: { decision: 'reject', target: { scope: 'all' } }, - }); - return { - content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], - }; - } catch (err) { - return { - content: [{ type: 'text' as const, text: `Reject all failed: ${toErrorMessage(err)}` }], - isError: true, - }; - } - }, - ); -} diff --git a/packages/document-api/scripts/lib/contract-output-artifacts.ts b/packages/document-api/scripts/lib/contract-output-artifacts.ts index e5ef87ba57..03f3be3ac4 100644 --- a/packages/document-api/scripts/lib/contract-output-artifacts.ts +++ b/packages/document-api/scripts/lib/contract-output-artifacts.ts @@ -22,7 +22,8 @@ function buildOperationContractMap() { successSchema: operation.schemas.success, failureSchema: operation.schemas.failure, ...(operation.skipAsATool ? { skipAsATool: true } : {}), - ...(operation.essential ? { essential: true } : {}), + ...(operation.intentGroup ? { intentGroup: operation.intentGroup } : {}), + ...(operation.intentAction ? { intentAction: operation.intentAction } : {}), }, ]), ); diff --git a/packages/document-api/scripts/lib/contract-snapshot.ts b/packages/document-api/scripts/lib/contract-snapshot.ts index 9a267548e7..eaf4d92b42 100644 --- a/packages/document-api/scripts/lib/contract-snapshot.ts +++ b/packages/document-api/scripts/lib/contract-snapshot.ts @@ -20,7 +20,8 @@ export interface ContractOperationSnapshot { typeof buildInternalContractSchemas >['operations']]; skipAsATool?: boolean; - essential?: boolean; + intentGroup?: string; + intentAction?: string; } export interface ContractSnapshot { @@ -43,7 +44,12 @@ export function buildContractSnapshot(): ContractSnapshot { metadata: COMMAND_CATALOG[operationId], schemas: internalSchemas.operations[operationId], ...(OPERATION_DEFINITIONS[operationId]?.skipAsATool ? { skipAsATool: true } : {}), - ...(OPERATION_DEFINITIONS[operationId]?.essential ? { essential: true } : {}), + ...(OPERATION_DEFINITIONS[operationId]?.intentGroup + ? { intentGroup: OPERATION_DEFINITIONS[operationId]!.intentGroup } + : {}), + ...(OPERATION_DEFINITIONS[operationId]?.intentAction + ? { intentAction: OPERATION_DEFINITIONS[operationId]!.intentAction } + : {}), })); const sourcePayload = { diff --git a/packages/document-api/src/contract/index.ts b/packages/document-api/src/contract/index.ts index 11e8e2827b..a6191dc54c 100644 --- a/packages/document-api/src/contract/index.ts +++ b/packages/document-api/src/contract/index.ts @@ -6,3 +6,4 @@ export * from './reference-doc-map.js'; export * from './reference-aliases.js'; export * from './operation-registry.js'; export * from './step-op-catalog.js'; +export { INTENT_GROUP_META, type IntentGroupMeta } from './operation-definitions.js'; diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index c2d5430a70..87e9819f89 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -77,10 +77,34 @@ export interface OperationDefinitionEntry { referenceDocPath: string; referenceGroup: ReferenceGroupKey; skipAsATool?: boolean; - /** When true, this tool is included in the default "essential" tool set. */ - essential?: boolean; + /** Which intent tool this operation belongs to (e.g. 'edit' → superdoc_edit). */ + intentGroup?: string; + /** Action enum value within the intent group (e.g. 'insert', 'replace'). */ + intentAction?: string; } +// --------------------------------------------------------------------------- +// Intent group metadata — tool-level names and descriptions +// --------------------------------------------------------------------------- + +export type IntentGroupMeta = { toolName: string; description: string }; + +export const INTENT_GROUP_META: Record = { + search: { toolName: 'superdoc_search', description: 'Find text or nodes in the document' }, + get_content: { toolName: 'superdoc_get_content', description: 'Read document content in various formats' }, + edit: { toolName: 'superdoc_edit', description: 'Insert, replace, delete text, or undo/redo' }, + create: { toolName: 'superdoc_create', description: 'Create structural block elements' }, + format: { toolName: 'superdoc_format', description: 'Change text and paragraph formatting' }, + table: { toolName: 'superdoc_table', description: 'Table structure and cell operations' }, + list: { toolName: 'superdoc_list', description: 'Create and manipulate lists' }, + comment: { toolName: 'superdoc_comment', description: 'Comment threads — create, edit, delete' }, + track_changes: { toolName: 'superdoc_track_changes', description: 'Review and resolve tracked changes' }, + link: { toolName: 'superdoc_link', description: 'Manage hyperlinks' }, + image: { toolName: 'superdoc_image', description: 'Image placement and properties' }, + section: { toolName: 'superdoc_section', description: 'Page layout, margins, columns' }, + mutations: { toolName: 'superdoc_mutations', description: 'Atomic multi-step batch edits (escape hatch)' }, +}; + // --------------------------------------------------------------------------- // Metadata helpers (moved from command-catalog.ts) // --------------------------------------------------------------------------- @@ -308,7 +332,6 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'get-node-by-id.mdx', referenceGroup: 'core', - essential: true, }, getText: { memberPath: 'getText', @@ -318,7 +341,9 @@ export const OPERATION_DEFINITIONS = { metadata: readOperation(), referenceDocPath: 'get-text.mdx', referenceGroup: 'core', - essential: true, + + intentGroup: 'get_content', + intentAction: 'text', }, getMarkdown: { memberPath: 'getMarkdown', @@ -328,6 +353,8 @@ export const OPERATION_DEFINITIONS = { metadata: readOperation(), referenceDocPath: 'get-markdown.mdx', referenceGroup: 'core', + intentGroup: 'get_content', + intentAction: 'markdown', }, getHtml: { memberPath: 'getHtml', @@ -337,6 +364,8 @@ export const OPERATION_DEFINITIONS = { metadata: readOperation(), referenceDocPath: 'get-html.mdx', referenceGroup: 'core', + intentGroup: 'get_content', + intentAction: 'html', }, markdownToFragment: { memberPath: 'markdownToFragment', @@ -355,6 +384,8 @@ export const OPERATION_DEFINITIONS = { metadata: readOperation(), referenceDocPath: 'info.mdx', referenceGroup: 'core', + intentGroup: 'get_content', + intentAction: 'info', }, clearContent: { @@ -418,6 +449,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'insert.mdx', referenceGroup: 'core', + intentGroup: 'edit', + intentAction: 'insert', }, replace: { memberPath: 'replace', @@ -459,6 +492,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'replace.mdx', referenceGroup: 'core', + intentGroup: 'edit', + intentAction: 'replace', }, delete: { memberPath: 'delete', @@ -476,6 +511,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'delete.mdx', referenceGroup: 'core', + intentGroup: 'edit', + intentAction: 'delete', }, 'blocks.list': { @@ -490,7 +527,6 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'blocks/list.mdx', referenceGroup: 'blocks', - essential: true, }, 'blocks.delete': { @@ -556,6 +592,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'format/apply.mdx', referenceGroup: 'format', + intentGroup: 'format', + intentAction: 'inline', }, ...FORMAT_INLINE_ALIAS_OPERATION_DEFINITIONS, @@ -591,6 +629,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'create/paragraph.mdx', referenceGroup: 'create', + intentGroup: 'create', + intentAction: 'paragraph', }, 'create.heading': { memberPath: 'create.heading', @@ -606,6 +646,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'create/heading.mdx', referenceGroup: 'create', + intentGroup: 'create', + intentAction: 'heading', }, 'create.sectionBreak': { memberPath: 'create.sectionBreak', @@ -930,6 +972,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'styles/paragraph/set-style.mdx', referenceGroup: 'styles.paragraph', + intentGroup: 'format', + intentAction: 'set_style', }, 'styles.paragraph.clearStyle': { memberPath: 'styles.paragraph.clearStyle', @@ -979,6 +1023,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'format/paragraph/set-alignment.mdx', referenceGroup: 'format.paragraph', + intentGroup: 'format', + intentAction: 'set_alignment', }, 'format.paragraph.clearAlignment': { memberPath: 'format.paragraph.clearAlignment', @@ -1009,6 +1055,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'format/paragraph/set-indentation.mdx', referenceGroup: 'format.paragraph', + intentGroup: 'format', + intentAction: 'set_indentation', }, 'format.paragraph.clearIndentation': { memberPath: 'format.paragraph.clearIndentation', @@ -1039,6 +1087,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'format/paragraph/set-spacing.mdx', referenceGroup: 'format.paragraph', + intentGroup: 'format', + intentAction: 'set_spacing', }, 'format.paragraph.clearSpacing': { memberPath: 'format.paragraph.clearSpacing', @@ -1245,6 +1295,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'lists/insert.mdx', referenceGroup: 'lists', + intentGroup: 'list', + intentAction: 'insert', }, 'lists.create': { memberPath: 'lists.create', @@ -1260,6 +1312,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'lists/create.mdx', referenceGroup: 'lists', + intentGroup: 'list', + intentAction: 'create', }, 'lists.attach': { memberPath: 'lists.attach', @@ -1290,6 +1344,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'lists/detach.mdx', referenceGroup: 'lists', + intentGroup: 'list', + intentAction: 'detach', }, 'lists.indent': { memberPath: 'lists.indent', @@ -1306,6 +1362,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'lists/indent.mdx', referenceGroup: 'lists', + intentGroup: 'list', + intentAction: 'indent', }, 'lists.outdent': { memberPath: 'lists.outdent', @@ -1321,6 +1379,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'lists/outdent.mdx', referenceGroup: 'lists', + intentGroup: 'list', + intentAction: 'outdent', }, 'lists.join': { memberPath: 'lists.join', @@ -1383,6 +1443,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'lists/set-level.mdx', referenceGroup: 'lists', + intentGroup: 'list', + intentAction: 'set_level', }, 'lists.setValue': { memberPath: 'lists.setValue', @@ -1505,6 +1567,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'lists/set-type.mdx', referenceGroup: 'lists', + intentGroup: 'list', + intentAction: 'set_type', }, 'lists.captureTemplate': { memberPath: 'lists.captureTemplate', @@ -1662,6 +1726,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'comments/create.mdx', referenceGroup: 'comments', + intentGroup: 'comment', + intentAction: 'create', }, 'comments.patch': { memberPath: 'comments.patch', @@ -1677,6 +1743,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'comments/patch.mdx', referenceGroup: 'comments', + intentGroup: 'comment', + intentAction: 'update', }, 'comments.delete': { memberPath: 'comments.delete', @@ -1693,6 +1761,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'comments/delete.mdx', referenceGroup: 'comments', + intentGroup: 'comment', + intentAction: 'delete', }, 'comments.get': { memberPath: 'comments.get', @@ -1705,6 +1775,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'comments/get.mdx', referenceGroup: 'comments', + intentGroup: 'comment', + intentAction: 'get', }, 'comments.list': { memberPath: 'comments.list', @@ -1717,6 +1789,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'comments/list.mdx', referenceGroup: 'comments', + intentGroup: 'comment', + intentAction: 'list', }, 'trackChanges.list': { @@ -1730,6 +1804,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'track-changes/list.mdx', referenceGroup: 'trackChanges', + intentGroup: 'track_changes', + intentAction: 'list', }, 'trackChanges.get': { memberPath: 'trackChanges.get', @@ -1758,6 +1834,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'track-changes/decide.mdx', referenceGroup: 'trackChanges', + intentGroup: 'track_changes', + intentAction: 'decide', }, 'query.match': { @@ -1773,7 +1851,9 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'query/match.mdx', referenceGroup: 'query', - essential: true, + + intentGroup: 'search', + intentAction: 'match', }, 'ranges.resolve': { @@ -1790,7 +1870,6 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'ranges/resolve.mdx', referenceGroup: 'ranges', - essential: true, }, 'mutations.preview': { @@ -1805,6 +1884,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'mutations/preview.mdx', referenceGroup: 'mutations', + intentGroup: 'mutations', + intentAction: 'preview', }, 'mutations.apply': { @@ -1828,7 +1909,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'mutations/apply.mdx', referenceGroup: 'mutations', - essential: true, + intentGroup: 'mutations', + intentAction: 'apply', }, 'capabilities.get': { @@ -2723,7 +2805,9 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'history/undo.mdx', referenceGroup: 'history', - essential: true, + + intentGroup: 'edit', + intentAction: 'undo', }, 'history.redo': { @@ -2741,6 +2825,8 @@ export const OPERATION_DEFINITIONS = { }), referenceDocPath: 'history/redo.mdx', referenceGroup: 'history', + intentGroup: 'edit', + intentAction: 'redo', }, // ------------------------------------------------------------------------- diff --git a/packages/sdk/codegen/src/__tests__/contract-integrity.test.ts b/packages/sdk/codegen/src/__tests__/contract-integrity.test.ts index 91c4150af4..e393392254 100644 --- a/packages/sdk/codegen/src/__tests__/contract-integrity.test.ts +++ b/packages/sdk/codegen/src/__tests__/contract-integrity.test.ts @@ -5,7 +5,6 @@ import path from 'node:path'; const REPO_ROOT = path.resolve(import.meta.dir, '../../../../../'); const CONTRACT_PATH = path.join(REPO_ROOT, 'apps/cli/generated/sdk-contract.json'); const CATALOG_PATH = path.join(REPO_ROOT, 'packages/sdk/tools/catalog.json'); -const NAME_MAP_PATH = path.join(REPO_ROOT, 'packages/sdk/tools/tool-name-map.json'); async function loadJson(filePath: string): Promise { return JSON.parse(await readFile(filePath, 'utf8')) as T; @@ -16,6 +15,7 @@ type Contract = { sourceHash: string; cli: { package: string; minVersion: string }; protocol: { version: string; transport: string; features: string[] }; + intentGroupMeta?: Record; operations: Record< string, { @@ -33,25 +33,26 @@ type Contract = { agentVisible?: boolean; }>; mutates: boolean; - intentName: string | null; outputSchema: Record; inputSchema?: Record; successSchema?: Record; failureSchema?: Record; + skipAsATool?: boolean; + intentGroup?: string; + intentAction?: string; } >; }; -type Catalog = { +type IntentCatalog = { contractVersion: string; toolCount: number; tools: Array<{ - operationId: string; toolName: string; - category?: string; - essential?: boolean; - requiredCapabilities?: string[]; - inputSchema?: Record; + description: string; + inputSchema: Record; + mutates: boolean; + operations: Array<{ operationId: string; intentAction: string }>; }>; }; @@ -91,7 +92,6 @@ describe('Contract integrity', () => { contract = await loadJson(CONTRACT_PATH); for (const [id, op] of Object.entries(contract.operations)) { if (op.mutates && op.inputSchema) { - // Doc-backed mutations should have success/failure schemas expect(op.successSchema).toBeTruthy(); expect(op.failureSchema).toBeTruthy(); } @@ -139,61 +139,82 @@ describe('Contract integrity', () => { }); }); -describe('Tool catalog integrity', () => { - test('tool counts match non-skipped contract operation count', async () => { +describe('Intent tool catalog integrity', () => { + test('catalog has correct number of intent tools', async () => { const contract = await loadJson(CONTRACT_PATH); - const catalog = await loadJson(CATALOG_PATH); - const nonSkippedOps = Object.values(contract.operations).filter( - (op) => !(op as Record).skipAsATool, - ); + const catalog = await loadJson(CATALOG_PATH); + + // Count unique intentGroups with at least one annotated operation + const intentGroups = new Set(); + for (const op of Object.values(contract.operations)) { + if (op.skipAsATool) continue; + if (op.intentGroup) intentGroups.add(op.intentGroup); + } - expect(catalog.tools.length).toBe(nonSkippedOps.length); - expect(catalog.toolCount).toBe(nonSkippedOps.length); + expect(catalog.tools.length).toBe(intentGroups.size); + expect(catalog.toolCount).toBe(intentGroups.size); }); - test('tool name map covers all non-skipped operations', async () => { - const contract = await loadJson(CONTRACT_PATH); - const nameMap = await loadJson>(NAME_MAP_PATH); - const nonSkippedOps = new Set( - Object.entries(contract.operations) - .filter(([, op]) => !(op as Record).skipAsATool) - .map(([id]) => id), - ); - const mappedOps = new Set(Object.values(nameMap)); + test('each provider bundle has same tool count as catalog', async () => { + const catalog = await loadJson(CATALOG_PATH); + const providers = ['openai', 'anthropic', 'vercel', 'generic']; - for (const opId of nonSkippedOps) { - expect(mappedOps.has(opId)).toBe(true); + for (const provider of providers) { + const bundle = await loadJson<{ tools: unknown[] }>( + path.join(REPO_ROOT, `packages/sdk/tools/tools.${provider}.json`), + ); + expect(Array.isArray(bundle.tools)).toBe(true); + expect(bundle.tools.length).toBe(catalog.tools.length); } }); - test('all catalog entries have required fields', async () => { - const catalog = await loadJson(CATALOG_PATH); + test('all tool names match superdoc_* pattern', async () => { + const catalog = await loadJson(CATALOG_PATH); + for (const tool of catalog.tools) { + expect(tool.toolName).toMatch(/^superdoc_[a-z_]+$/); + } + }); + test('tool schemas are valid JSON Schema', async () => { + const catalog = await loadJson(CATALOG_PATH); for (const tool of catalog.tools) { - expect(tool.operationId).toBeTruthy(); - expect(tool.toolName).toBeTruthy(); + expect(tool.inputSchema).toBeTruthy(); + expect(tool.inputSchema.type).toBe('object'); + expect(typeof tool.inputSchema.properties).toBe('object'); } }); - test('provider bundles have correct structure', async () => { + test('each tool action enum matches intentAction values of grouped operations', async () => { const contract = await loadJson(CONTRACT_PATH); - const opCount = Object.keys(contract.operations).length; - const providers = ['openai', 'anthropic', 'vercel', 'generic']; + const catalog = await loadJson(CATALOG_PATH); - const nonSkippedCount = Object.values(contract.operations).filter( - (op) => !(op as Record).skipAsATool, - ).length; + for (const tool of catalog.tools) { + const catalogActions = tool.operations.map((op) => op.intentAction).sort(); - for (const provider of providers) { - const bundle = await loadJson<{ tools: unknown[] }>( - path.join(REPO_ROOT, `packages/sdk/tools/tools.${provider}.json`), - ); - expect(Array.isArray(bundle.tools)).toBe(true); - // nonSkippedCount tools + discover_tools - expect(bundle.tools.length).toBe(nonSkippedCount + 1); + // Verify against contract + for (const op of tool.operations) { + const contractOp = contract.operations[op.operationId]; + expect(contractOp).toBeDefined(); + expect(contractOp.intentAction).toBe(op.intentAction); + } + + // For multi-op tools, verify action enum exists + if (tool.operations.length > 1) { + const actionProp = tool.inputSchema.properties as Record>; + expect(actionProp.action).toBeDefined(); + expect(actionProp.action.enum).toBeTruthy(); + const schemaActions = [...(actionProp.action.enum as string[])].sort(); + expect(schemaActions).toEqual(catalogActions); + } } }); + test('system prompt file exists and is non-empty', async () => { + const promptPath = path.join(REPO_ROOT, 'packages/sdk/tools/system-prompt.md'); + const content = await readFile(promptPath, 'utf8'); + expect(content.length).toBeGreaterThan(100); + }); + test('OpenAI tools have required function shape', async () => { const bundle = await loadJson<{ tools: Array> }>( path.join(REPO_ROOT, 'packages/sdk/tools/tools.openai.json'), @@ -221,22 +242,53 @@ describe('Tool catalog integrity', () => { }); }); +describe('Intent annotation integrity', () => { + test('intentGroup + intentAction consistency: no duplicate intentAction within a group', async () => { + const contract = await loadJson(CONTRACT_PATH); + + const groupActions = new Map>(); + for (const [id, op] of Object.entries(contract.operations)) { + if (!op.intentGroup || !op.intentAction) continue; + if (!groupActions.has(op.intentGroup)) { + groupActions.set(op.intentGroup, new Set()); + } + const actions = groupActions.get(op.intentGroup)!; + expect(actions.has(op.intentAction)).toBe(false); + actions.add(op.intentAction); + } + }); + + test('all annotated operations have valid intentGroup in intentGroupMeta', async () => { + const contract = await loadJson(CONTRACT_PATH); + const meta = contract.intentGroupMeta ?? {}; + + for (const [id, op] of Object.entries(contract.operations)) { + if (op.intentGroup) { + expect(meta[op.intentGroup]).toBeDefined(); + } + } + }); + + test('annotated operations always have both intentGroup and intentAction', async () => { + const contract = await loadJson(CONTRACT_PATH); + for (const [id, op] of Object.entries(contract.operations)) { + if (op.intentGroup) { + expect(op.intentAction).toBeTruthy(); + } + if (op.intentAction) { + expect(op.intentGroup).toBeTruthy(); + } + } + }); +}); + const POLICY_PATH = path.join(REPO_ROOT, 'packages/sdk/tools/tools-policy.json'); type ToolsPolicy = { policyVersion: string; contractHash: string; - groups: string[]; - groupDescriptions?: Record; - essentialTools?: string[]; - discoverTool?: { name: string; description: string; schema: Record }; - defaults: { - mode?: string; - maxTools: number; - alwaysInclude: string[]; - foundationalOperationIds: string[]; - }; - capabilityFeatures: Record; + toolCount: number; + tools: Array<{ toolName: string; mutates: boolean }>; }; describe('Tools policy integrity', () => { @@ -244,46 +296,8 @@ describe('Tools policy integrity', () => { const policy = await loadJson(POLICY_PATH); expect(policy.policyVersion).toBeTruthy(); expect(policy.contractHash).toBeTruthy(); - expect(Array.isArray(policy.groups)).toBe(true); - expect(typeof policy.defaults).toBe('object'); - expect(typeof policy.capabilityFeatures).toBe('object'); - }); - - test('has essential tools list', async () => { - const policy = await loadJson(POLICY_PATH); - expect(Array.isArray(policy.essentialTools)).toBe(true); - expect(policy.essentialTools!.length).toBeGreaterThan(0); - }); - - test('has discover_tools definition', async () => { - const policy = await loadJson(POLICY_PATH); - expect(policy.discoverTool).toBeDefined(); - expect(policy.discoverTool!.name).toBe('discover_tools'); - expect(typeof policy.discoverTool!.description).toBe('string'); - expect(typeof policy.discoverTool!.schema).toBe('object'); - }); - - test('default mode is essential', async () => { - const policy = await loadJson(POLICY_PATH); - expect(policy.defaults.mode).toBe('essential'); - }); - - test('group categories exist in catalog entries', async () => { - const policy = await loadJson(POLICY_PATH); - const catalog = await loadJson(CATALOG_PATH); - const catalogCategories = new Set(catalog.tools.map((t) => t.category)); - - for (const group of policy.groups) { - expect(catalogCategories.has(group)).toBe(true); - } - }); - - test('foundational operation IDs exist in contract', async () => { - const policy = await loadJson(POLICY_PATH); - const contract = await loadJson(CONTRACT_PATH); - for (const opId of policy.defaults.foundationalOperationIds) { - expect(contract.operations[opId]).toBeDefined(); - } + expect(typeof policy.toolCount).toBe('number'); + expect(Array.isArray(policy.tools)).toBe(true); }); test('contractHash matches contract sourceHash', async () => { @@ -292,58 +306,10 @@ describe('Tools policy integrity', () => { expect(policy.contractHash).toBe(contract.sourceHash); }); - test('capabilityFeatures consistent with catalog entries', async () => { - const policy = await loadJson(POLICY_PATH); - const catalog = await loadJson(CATALOG_PATH); - - for (const [category, expectedFeatures] of Object.entries(policy.capabilityFeatures)) { - const categoryTools = catalog.tools.filter((t) => t.category === category); - for (const tool of categoryTools) { - expect(tool.requiredCapabilities).toEqual(expectedFeatures); - } - } - }); - - test('essential tools exist in catalog', async () => { + test('policy tool count matches catalog', async () => { const policy = await loadJson(POLICY_PATH); - const catalog = await loadJson(CATALOG_PATH); - const catalogToolNames = new Set(catalog.tools.map((t) => t.toolName)); - - for (const toolName of policy.essentialTools ?? []) { - expect(catalogToolNames.has(toolName)).toBe(true); - } - }); -}); - -describe('Intent name integrity', () => { - test('all operations have intentName in contract', async () => { - const contract = await loadJson(CONTRACT_PATH); - for (const [id, op] of Object.entries(contract.operations)) { - expect(op.intentName).toBeTruthy(); - } - }); - - test('contract intentNames match catalog toolNames', async () => { - const contract = await loadJson(CONTRACT_PATH); - const catalog = await loadJson(CATALOG_PATH); - - const catalogIntentNames = new Map(catalog.tools.map((t) => [t.operationId, t.toolName])); - - for (const [id, op] of Object.entries(contract.operations)) { - if ((op as Record).skipAsATool) continue; - const catalogName = catalogIntentNames.get(id); - expect(catalogName).toBe(op.intentName); - } - }); - - test('all intentNames are unique and match snake_case naming policy', async () => { - const contract = await loadJson(CONTRACT_PATH); - const seen = new Set(); - for (const [id, op] of Object.entries(contract.operations)) { - expect(op.intentName).toMatch(/^[a-z][a-z0-9_]*$/); - expect(seen.has(op.intentName!)).toBe(false); - seen.add(op.intentName!); - } + const catalog = await loadJson(CATALOG_PATH); + expect(policy.toolCount).toBe(catalog.toolCount); }); }); @@ -361,23 +327,6 @@ describe('agentVisible param annotation integrity', () => { } }); - test('agentVisible: false params are excluded from catalog inputSchema', async () => { - const contract = await loadJson(CONTRACT_PATH); - const catalog = await loadJson(CATALOG_PATH); - - for (const tool of catalog.tools) { - const inputSchema = tool.inputSchema as { properties?: Record } | undefined; - if (!inputSchema?.properties) continue; - const op = contract.operations[tool.operationId]; - if (!op) continue; - - const hiddenParams = op.params.filter((p) => p.agentVisible === false).map((p) => p.name); - for (const hidden of hiddenParams) { - expect(inputSchema.properties[hidden]).toBeUndefined(); - } - } - }); - test('no unexpected params are marked agentVisible: false', async () => { const contract = await loadJson(CONTRACT_PATH); for (const [, op] of Object.entries(contract.operations)) { diff --git a/packages/sdk/codegen/src/__tests__/cross-lang-parity.test.ts b/packages/sdk/codegen/src/__tests__/cross-lang-parity.test.ts index af091ef813..203d7ef5dc 100644 --- a/packages/sdk/codegen/src/__tests__/cross-lang-parity.test.ts +++ b/packages/sdk/codegen/src/__tests__/cross-lang-parity.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from 'bun:test'; import { spawn } from 'node:child_process'; -import { readFileSync } from 'node:fs'; import path from 'node:path'; const REPO_ROOT = path.resolve(import.meta.dir, '../../../../../'); @@ -10,10 +9,8 @@ const PYTHON_SDK = path.join(REPO_ROOT, 'packages/sdk/langs/python'); // Helpers // -------------------------------------------------------------------------- -type SelectionEntry = { operationId: string; toolName: string; category: string; mutates: boolean }; type ChooseResult = { - selected: SelectionEntry[]; - meta: { provider: string; mode: string; groups: string[]; selectedCount: number }; + meta: { provider: string; toolCount: number }; }; /** Call the Python parity helper with a JSON command and parse the result. */ @@ -65,11 +62,11 @@ async function nodeTools() { } // -------------------------------------------------------------------------- -// chooseTools parity — group-based selection +// chooseTools parity // -------------------------------------------------------------------------- -describe('chooseTools parity — essential mode (default)', () => { - test('default mode returns only essential tools + discover_tools', async () => { +describe('chooseTools parity', () => { + test('returns same tool count for generic provider', async () => { const input = { provider: 'generic' as const }; const { chooseTools } = await nodeTools(); @@ -77,198 +74,46 @@ describe('chooseTools parity — essential mode (default)', () => { const pyResult = (await callPython({ action: 'chooseTools', input })) as ChooseResult; - // Both should return same essential tools - const nodeIds = nodeResult.selected.map((s) => s.operationId).sort(); - const pyIds = pyResult.selected.map((s) => s.operationId).sort(); - expect(pyIds).toEqual(nodeIds); - expect(nodeIds.length).toBeGreaterThan(0); - - // Should be a small set (essential only) - expect(nodeIds.length).toBeLessThan(20); - - // Meta should report essential mode - expect(nodeResult.meta.mode).toBe('essential'); - expect(pyResult.meta.mode).toBe('essential'); - }); - - test('essential + groups union: loads essential plus requested category', async () => { - const input = { provider: 'generic' as const, groups: ['comments' as const] }; - - const { chooseTools } = await nodeTools(); - const nodeResult = await chooseTools(input); - - const pyResult = (await callPython({ action: 'chooseTools', input })) as ChooseResult; - - const nodeIds = nodeResult.selected.map((s) => s.operationId).sort(); - const pyIds = pyResult.selected.map((s) => s.operationId).sort(); - expect(pyIds).toEqual(nodeIds); - - // Should include comment tools - const nodeCategories = new Set(nodeResult.selected.map((s) => s.category)); - expect(nodeCategories.has('comments')).toBe(true); - - // Should also include essential tools (which are from core/history) - expect(nodeIds.length).toBeGreaterThan(5); - }); - - test('includeDiscoverTool=false omits discover_tools', async () => { - const input = { provider: 'generic' as const, includeDiscoverTool: false }; - - const { chooseTools } = await nodeTools(); - const nodeResult = await chooseTools(input); - - // discover_tools should NOT appear in the tools array - const toolNames = nodeResult.tools - .filter((t): t is Record => typeof t === 'object' && t !== null) - .map((t) => (t as Record).name as string); - expect(toolNames).not.toContain('discover_tools'); - }); -}); - -describe('chooseTools parity — all mode (group-based selection)', () => { - test('mode=all with no groups: identical selected operationIds', async () => { - const input = { provider: 'generic' as const, mode: 'all' as const }; - - const { chooseTools } = await nodeTools(); - const nodeResult = await chooseTools(input); - const nodeIds = nodeResult.selected.map((s) => s.operationId).sort(); - - const pyResult = (await callPython({ action: 'chooseTools', input })) as ChooseResult; - const pyIds = pyResult.selected.map((s) => s.operationId).sort(); - - expect(pyIds).toEqual(nodeIds); - expect(nodeIds.length).toBeGreaterThan(0); - expect(nodeResult.meta.mode).toBe('all'); - }); - - test('mode=all: core group always auto-included', async () => { - const input = { provider: 'generic' as const, mode: 'all' as const, groups: ['format' as const] }; - - const { chooseTools } = await nodeTools(); - const nodeResult = await chooseTools(input); - const nodeCategories = new Set(nodeResult.selected.map((s) => s.category)); - - const pyResult = (await callPython({ action: 'chooseTools', input })) as ChooseResult; - const pyCategories = new Set(pyResult.selected.map((s) => s.category)); - - // Core should be auto-included even though only 'format' was requested - expect(nodeCategories.has('core')).toBe(true); - expect(nodeCategories.has('format')).toBe(true); - expect(pyCategories.has('core')).toBe(true); - expect(pyCategories.has('format')).toBe(true); - }); - - test('mode=all: specific groups only', async () => { - const input = { - provider: 'generic' as const, - mode: 'all' as const, - groups: ['core' as const, 'comments' as const], - }; - - const { chooseTools } = await nodeTools(); - const nodeResult = await chooseTools(input); - const nodeCategories = new Set(nodeResult.selected.map((s) => s.category)); - - const pyResult = (await callPython({ action: 'chooseTools', input })) as ChooseResult; - const pyCategories = new Set(pyResult.selected.map((s) => s.category)); - - // Should only have core and comments - for (const cat of nodeCategories) { - expect(['core', 'comments']).toContain(cat); - } - expect(pyCategories).toEqual(nodeCategories); - }); - - test('mode=all: meta matches between runtimes', async () => { - const input = { provider: 'generic' as const, mode: 'all' as const, groups: ['core' as const, 'tables' as const] }; - - const { chooseTools } = await nodeTools(); - const nodeResult = await chooseTools(input); - - const pyResult = (await callPython({ action: 'chooseTools', input })) as ChooseResult; - expect(pyResult.meta.provider).toBe(nodeResult.meta.provider); - expect(pyResult.meta.mode).toBe('all'); - expect(pyResult.meta.selectedCount).toBe(nodeResult.meta.selectedCount); - expect(pyResult.meta.groups.sort()).toEqual(nodeResult.meta.groups.sort()); + expect(pyResult.meta.toolCount).toBe(nodeResult.meta.toolCount); + expect(nodeResult.meta.toolCount).toBeGreaterThan(0); }); }); // -------------------------------------------------------------------------- -// Constraint validation parity +// Intent dispatch parity // -------------------------------------------------------------------------- -describe('Constraint validation parity', () => { - test('mutuallyExclusive rejects in both runtimes', async () => { - // doc.lists.list has mutuallyExclusive: [['query', 'within'], ...] - const args = { query: 'test', within: 'some-id' }; - - const { dispatchSuperDocTool } = await nodeTools(); - let nodeError: { code?: string } | null = null; - try { - await dispatchSuperDocTool({ doc: {} }, 'list_lists', args); - } catch (error: unknown) { - nodeError = error as { code?: string }; - } - - const pyResult = (await callPython({ - action: 'validateDispatchArgs', - operationId: 'doc.lists.list', - args, - })) as { rejected?: boolean; code?: string }; - - expect(nodeError).not.toBeNull(); - expect(nodeError!.code).toBe('INVALID_ARGUMENT'); - expect(pyResult.rejected).toBe(true); - expect(pyResult.code).toBe('INVALID_ARGUMENT'); - }); - - test('type mismatches pass through to CLI: both runtimes accept true for a number param', async () => { - // doc.lists.list has a 'limit' number param - const args = { limit: true }; +describe('Intent dispatch parity', () => { + test('Node and Python dispatch same tool+action to same operation', async () => { + // Test that both runtimes map superdoc_edit + action=insert to doc.insert + const nodeResult = (await callPython({ + action: 'resolveIntentDispatch', + toolName: 'superdoc_edit', + args: { action: 'insert' }, + })) as { operationId: string }; - const pyResult = await callPython({ - action: 'validateDispatchArgs', - operationId: 'doc.lists.list', - args, - }); - - expect(pyResult).toBe('passed'); + expect(nodeResult.operationId).toBe('doc.insert'); }); - test('unknown param rejected in both runtimes', async () => { - const args = { unknownParam: 'value' }; - - const { dispatchSuperDocTool } = await nodeTools(); - let nodeError: { code?: string } | null = null; - try { - await dispatchSuperDocTool({ doc: {} }, 'get_document_info', args); - } catch (error: unknown) { - nodeError = error as { code?: string }; - } - - const pyResult = (await callPython({ - action: 'validateDispatchArgs', - operationId: 'doc.info', - args, - })) as { rejected?: boolean; code?: string }; + test('single-op tool dispatches correctly', async () => { + const result = (await callPython({ + action: 'resolveIntentDispatch', + toolName: 'superdoc_search', + args: {}, + })) as { operationId: string }; - expect(nodeError).not.toBeNull(); - expect(nodeError!.code).toBe('INVALID_ARGUMENT'); - expect(pyResult.rejected).toBe(true); - expect(pyResult.code).toBe('INVALID_ARGUMENT'); + expect(result.operationId).toBe('doc.query.match'); }); - test('valid args pass in both runtimes', async () => { - const args = { query: 'test' }; - - const pyResult = await callPython({ - action: 'validateDispatchArgs', - operationId: 'doc.lists.list', - args, - }); + test('unknown tool raises error', async () => { + const result = (await callPython({ + action: 'resolveIntentDispatch', + toolName: 'superdoc_nonexistent', + args: {}, + })) as { error: string }; - expect(pyResult).toBe('passed'); + expect(result.error).toBeTruthy(); }); }); diff --git a/packages/sdk/codegen/src/generate-all.mjs b/packages/sdk/codegen/src/generate-all.mjs index ac56f7d728..40c59a7e35 100644 --- a/packages/sdk/codegen/src/generate-all.mjs +++ b/packages/sdk/codegen/src/generate-all.mjs @@ -3,7 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises'; import { loadContract, REPO_ROOT } from './shared.mjs'; import { generateNodeSdk } from './generate-node.mjs'; import { generatePythonSdk } from './generate-python.mjs'; -import { generateToolCatalogs } from './generate-tool-catalogs.mjs'; +import { generateIntentTools } from './generate-intent-tools.mjs'; /** * When SDK_CODEGEN_OUTPUT_ROOT is set (for --check mode), redirect outputs @@ -21,6 +21,9 @@ function redirectedWriteGeneratedFile(filePath, content) { } else if (relToRepo.startsWith(path.join('packages', 'sdk', 'langs', 'python', 'superdoc', 'generated'))) { const relPart = path.relative(path.join(REPO_ROOT, 'packages/sdk/langs/python/superdoc/generated'), filePath); destPath = path.join(outputRoot, 'python-generated', relPart); + } else if (relToRepo.startsWith(path.join('packages', 'sdk', 'langs', 'python', 'superdoc', 'tools'))) { + const relPart = path.relative(path.join(REPO_ROOT, 'packages/sdk/langs/python/superdoc/tools'), filePath); + destPath = path.join(outputRoot, 'python-tools', relPart); } else if (relToRepo.startsWith(path.join('packages', 'sdk', 'tools'))) { const relPart = path.relative(path.join(REPO_ROOT, 'packages/sdk/tools'), filePath); destPath = path.join(outputRoot, 'tools', relPart); @@ -43,10 +46,10 @@ async function main() { await Promise.all([ generateNodeSdk(contract), generatePythonSdk(contract), - generateToolCatalogs(contract), + generateIntentTools(contract), ]); - console.log('Generated Node + Python SDKs + tool catalogs from contract.'); + console.log('Generated Node + Python SDKs + tools from contract.'); } main().catch((error) => { diff --git a/packages/sdk/codegen/src/generate-intent-tools.mjs b/packages/sdk/codegen/src/generate-intent-tools.mjs new file mode 100644 index 0000000000..2cbe55be1c --- /dev/null +++ b/packages/sdk/codegen/src/generate-intent-tools.mjs @@ -0,0 +1,430 @@ +import path from 'node:path'; +import { loadContract, REPO_ROOT, writeGeneratedFile } from './shared.mjs'; + +const TOOLS_OUTPUT_DIR = path.join(REPO_ROOT, 'packages/sdk/tools'); + +// --------------------------------------------------------------------------- +// Schema sanitization — ensure JSON Schema 2020-12 compliance +// --------------------------------------------------------------------------- + +/** + * Recursively fix bare `{ const: value }` nodes to include `type`. + * Anthropic requires `const` to be accompanied by a `type` field. + */ +function sanitizeSchema(schema) { + if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return schema; + + const result = { ...schema }; + + // "type": "json" is a SuperDoc contract sentinel for "any JSON value". + if (result.type === 'json') { + delete result.type; + return result; + } + + // Fix bare const: add type based on the const value + if ('const' in result && !result.type) { + const val = result.const; + if (typeof val === 'string') result.type = 'string'; + else if (typeof val === 'number') result.type = 'number'; + else if (typeof val === 'boolean') result.type = 'boolean'; + } + + // Recurse into nested structures + if (result.properties) { + result.properties = Object.fromEntries( + Object.entries(result.properties).map(([k, v]) => [k, sanitizeSchema(v)]), + ); + } + if (Array.isArray(result.oneOf)) { + const allConst = result.oneOf.every((v) => v && typeof v === 'object' && 'const' in v && Object.keys(v).length <= 2); + if (allConst && result.oneOf.length > 0) { + const values = result.oneOf.map((v) => v.const); + delete result.oneOf; + result.enum = values; + } else { + result.oneOf = result.oneOf.map(sanitizeSchema); + } + } + if (Array.isArray(result.anyOf)) { + result.anyOf = result.anyOf.map(sanitizeSchema); + } + if (Array.isArray(result.allOf)) { + result.allOf = result.allOf.map(sanitizeSchema); + } + if (result.items) { + result.items = sanitizeSchema(result.items); + } + if (result.additionalProperties && typeof result.additionalProperties === 'object') { + result.additionalProperties = sanitizeSchema(result.additionalProperties); + } + + return result; +} + +// --------------------------------------------------------------------------- +// Build input schema from CLI params (for CLI-only ops or as fallback) +// --------------------------------------------------------------------------- + +function buildInputSchemaFromParams(operation) { + const properties = {}; + const required = []; + + for (const param of operation.params ?? []) { + if (param.agentVisible === false) continue; + + let schema; + if (param.type === 'string' && param.schema) schema = { type: 'string', ...param.schema }; + else if (param.type === 'string') schema = { type: 'string' }; + else if (param.type === 'number') schema = { type: 'number' }; + else if (param.type === 'boolean') schema = { type: 'boolean' }; + else if (param.type === 'string[]') schema = { type: 'array', items: { type: 'string' } }; + else if (param.type === 'json' && param.schema && param.schema.type !== 'json') schema = param.schema; + else schema = { type: 'object' }; + + schema = sanitizeSchema(schema); + if (param.description) schema.description = param.description; + properties[param.name] = schema; + if (param.required) required.push(param.name); + } + + const result = { type: 'object', properties }; + if (required.length > 0) result.required = required; + result.additionalProperties = false; + return result; +} + +// --------------------------------------------------------------------------- +// Build intent tools from grouped operations +// --------------------------------------------------------------------------- + +function buildIntentTools(contract) { + const intentGroupMeta = contract.intentGroupMeta ?? {}; + + // Group operations by intentGroup + const groups = new Map(); + for (const [operationId, operation] of Object.entries(contract.operations)) { + if (operation.skipAsATool) continue; + if (!operation.intentGroup) continue; + + const group = operation.intentGroup; + if (!groups.has(group)) groups.set(group, []); + groups.get(group).push({ operationId, operation }); + } + + const tools = []; + + for (const [groupKey, ops] of groups) { + const meta = intentGroupMeta[groupKey]; + if (!meta) { + console.warn(`No INTENT_GROUP_META for group "${groupKey}", skipping.`); + continue; + } + + const isSingleOp = ops.length === 1; + const mutates = ops.some(({ operation }) => operation.mutates); + + if (isSingleOp) { + // Single-op tool — no action enum, input schema = operation schema + const { operationId, operation } = ops[0]; + const inputSchema = buildInputSchemaFromParams(operation); + + tools.push({ + toolName: meta.toolName, + description: meta.description, + inputSchema, + mutates, + operations: [{ operationId, intentAction: operation.intentAction }], + }); + } else { + // Multi-op tool — add action discriminator + const actionEnum = ops.map(({ operation }) => operation.intentAction).sort(); + + // Build properties: action + union of all operation properties + const actionProperty = { + type: 'string', + enum: actionEnum, + description: `The action to perform. One of: ${actionEnum.join(', ')}.`, + }; + + // Collect all properties across all operations (excluding action). + // A property is marked required only if every operation that defines it + // also marks it required — otherwise it's conditionally required per-action + // and must stay optional in the merged schema. + const allProperties = { action: actionProperty }; + /** @type {Map} */ + const propPresence = new Map(); + + for (const { operation } of ops) { + const opSchema = buildInputSchemaFromParams(operation); + const opRequired = new Set(opSchema.required ?? []); + + for (const [propName, propSchema] of Object.entries(opSchema.properties ?? {})) { + if (propName === 'action') continue; + + if (!allProperties[propName]) { + allProperties[propName] = { ...propSchema }; + } + + const entry = propPresence.get(propName) ?? { total: 0, requiredCount: 0 }; + entry.total++; + if (opRequired.has(propName)) entry.requiredCount++; + propPresence.set(propName, entry); + } + } + + // 'action' is always required; other props are required only if they + // appear in every operation AND every operation marks them required. + // If a param only exists in some actions, it's conditionally required + // and must stay optional in the merged schema. + const opCount = ops.length; + const allRequired = ['action']; + for (const [propName, { total, requiredCount }] of propPresence) { + if (total === opCount && requiredCount === opCount) { + allRequired.push(propName); + } + } + + const inputSchema = { + type: 'object', + properties: allProperties, + required: allRequired, + additionalProperties: false, + }; + + tools.push({ + toolName: meta.toolName, + description: meta.description, + inputSchema, + mutates, + operations: ops.map(({ operationId, operation }) => ({ + operationId, + intentAction: operation.intentAction, + })), + }); + } + } + + return tools; +} + +// --------------------------------------------------------------------------- +// Generate dispatch code +// --------------------------------------------------------------------------- + +function generateDispatchCode(tools) { + const lines = [ + '// Auto-generated by generate-intent-tools.mjs — do not edit', + '', + 'export function dispatchIntentTool(', + ' toolName: string,', + ' args: Record,', + ' execute: (operationId: string, input: Record) => unknown,', + '): unknown {', + ' switch (toolName) {', + ]; + + for (const tool of tools) { + const isSingleOp = tool.operations.length === 1; + + if (isSingleOp) { + const { operationId } = tool.operations[0]; + lines.push(` case '${tool.toolName}':`); + lines.push(` return execute('${operationId}', args);`); + } else { + lines.push(` case '${tool.toolName}': {`); + lines.push(' const { action, ...rest } = args;'); + lines.push(' switch (action) {'); + for (const { operationId, intentAction } of tool.operations) { + lines.push(` case '${intentAction}': return execute('${operationId}', rest);`); + } + lines.push(` default: throw new Error(\`Unknown action for ${tool.toolName}: \${action}\`);`); + lines.push(' }'); + lines.push(' }'); + } + } + + lines.push(' default:'); + lines.push(' throw new Error(`Unknown intent tool: ${toolName}`);'); + lines.push(' }'); + lines.push('}'); + lines.push(''); + + return lines.join('\n'); +} + +// --------------------------------------------------------------------------- +// Generate Python dispatch code +// --------------------------------------------------------------------------- + +function generatePythonDispatchCode(tools) { + const lines = [ + '# Auto-generated by generate-intent-tools.mjs — do not edit', + '', + 'from typing import Any, Callable, Dict', + '', + 'from ..errors import SuperDocError', + '', + '', + 'def dispatch_intent_tool(', + ' tool_name: str,', + ' args: Dict[str, Any],', + ' execute: Callable[[str, Dict[str, Any]], Any],', + ') -> Any:', + ]; + + // Build if/elif chain + let first = true; + for (const tool of tools) { + const isSingleOp = tool.operations.length === 1; + const prefix = first ? ' if' : ' elif'; + first = false; + + if (isSingleOp) { + const { operationId } = tool.operations[0]; + lines.push(`${prefix} tool_name == '${tool.toolName}':`); + lines.push(` return execute('${operationId}', args)`); + } else { + lines.push(`${prefix} tool_name == '${tool.toolName}':`); + lines.push(" action = args.get('action')"); + lines.push(' rest = {k: v for k, v in args.items() if k != \'action\'}'); + let firstAction = true; + for (const { operationId, intentAction } of tool.operations) { + const actionPrefix = firstAction ? ' if' : ' elif'; + firstAction = false; + lines.push(`${actionPrefix} action == '${intentAction}':`); + lines.push(` return execute('${operationId}', rest)`); + } + lines.push(` else:`); + lines.push(` raise SuperDocError(f'Unknown action for ${tool.toolName}: {action}', code='TOOL_DISPATCH_NOT_FOUND', details={'toolName': '${tool.toolName}', 'action': action})`); + } + } + + lines.push(' else:'); + lines.push(" raise SuperDocError(f'Unknown intent tool: {tool_name}', code='TOOL_DISPATCH_NOT_FOUND', details={'toolName': tool_name})"); + lines.push(''); + + return lines.join('\n'); +} + +// --------------------------------------------------------------------------- +// Provider formatters +// --------------------------------------------------------------------------- + +function toOpenAiTool(entry) { + return { + type: 'function', + function: { + name: entry.toolName, + description: entry.description, + parameters: entry.inputSchema, + }, + }; +} + +function toAnthropicTool(entry) { + return { + name: entry.toolName, + description: entry.description, + input_schema: entry.inputSchema, + }; +} + +function toVercelTool(entry) { + return { + type: 'function', + function: { + name: entry.toolName, + description: entry.description, + parameters: entry.inputSchema, + }, + }; +} + +function toGenericTool(entry) { + return { + name: entry.toolName, + description: entry.description, + parameters: entry.inputSchema, + metadata: { + mutates: entry.mutates, + operationCount: entry.operations.length, + operations: entry.operations.map((op) => op.operationId), + }, + }; +} + +// --------------------------------------------------------------------------- +// Main generation +// --------------------------------------------------------------------------- + +export async function generateIntentTools(contract) { + const tools = buildIntentTools(contract); + + // Full catalog + const catalog = { + contractVersion: contract.contractVersion, + generatedAt: null, + toolCount: tools.length, + tools: tools.map((t) => ({ + toolName: t.toolName, + description: t.description, + inputSchema: t.inputSchema, + mutates: t.mutates, + operations: t.operations, + })), + }; + + // Tools policy (simplified for intent tools) + const policy = { + policyVersion: 'v4', + toolCount: tools.length, + tools: tools.map((t) => ({ + toolName: t.toolName, + mutates: t.mutates, + })), + contractHash: contract.sourceHash, + }; + + // Provider bundles + const providers = { + openai: { formatter: toOpenAiTool, file: 'tools.openai.json' }, + anthropic: { formatter: toAnthropicTool, file: 'tools.anthropic.json' }, + vercel: { formatter: toVercelTool, file: 'tools.vercel.json' }, + generic: { formatter: toGenericTool, file: 'tools.generic.json' }, + }; + + // Generated dispatch code + const dispatchTs = generateDispatchCode(tools); + const dispatchPy = generatePythonDispatchCode(tools); + + const writes = [ + writeGeneratedFile(path.join(TOOLS_OUTPUT_DIR, 'catalog.json'), JSON.stringify(catalog, null, 2) + '\n'), + writeGeneratedFile(path.join(TOOLS_OUTPUT_DIR, 'tools-policy.json'), JSON.stringify(policy, null, 2) + '\n'), + writeGeneratedFile( + path.join(REPO_ROOT, 'packages/sdk/langs/node/src/generated/intent-dispatch.generated.ts'), + dispatchTs, + ), + writeGeneratedFile( + path.join(REPO_ROOT, 'packages/sdk/langs/python/superdoc/tools/intent_dispatch_generated.py'), + dispatchPy, + ), + ]; + + for (const { formatter, file } of Object.values(providers)) { + const providerTools = tools.map(formatter); + const bundle = { + contractVersion: contract.contractVersion, + tools: providerTools, + }; + writes.push(writeGeneratedFile(path.join(TOOLS_OUTPUT_DIR, file), JSON.stringify(bundle, null, 2) + '\n')); + } + + await Promise.all(writes); +} + +if (import.meta.url.endsWith(process.argv[1]?.replace(/^file:\/\//, '') ?? '')) { + const contract = await loadContract(); + await generateIntentTools(contract); + console.log('Generated intent tool files.'); +} diff --git a/packages/sdk/codegen/src/generate-tool-catalogs.mjs b/packages/sdk/codegen/src/generate-tool-catalogs.mjs deleted file mode 100644 index 8765769c40..0000000000 --- a/packages/sdk/codegen/src/generate-tool-catalogs.mjs +++ /dev/null @@ -1,449 +0,0 @@ -import { readFile } from 'node:fs/promises'; -import path from 'node:path'; -import { loadContract, REPO_ROOT, sanitizeOperationId, writeGeneratedFile } from './shared.mjs'; - -const TOOLS_OUTPUT_DIR = path.join(REPO_ROOT, 'packages/sdk/tools'); -const DOCAPI_TOOLS_PATH = path.join( - REPO_ROOT, - 'packages/document-api/generated/manifests/document-api-tools.json', -); - -const NAME_POLICY_VERSION = 'v1'; -const EXPOSURE_VERSION = 'v1'; - -// --------------------------------------------------------------------------- -// Intent naming — read from contract's intentName field, fallback to derivation -// --------------------------------------------------------------------------- - -function toIntentName(operationId, operation) { - if (operation.intentName) { - return operation.intentName; - } - // Fallback: strip 'doc.' prefix and convert dots/camelCase to snake_case - return sanitizeOperationId(operationId) - .replace(/\./g, '_') - .replace(/([a-z])([A-Z])/g, '$1_$2') - .toLowerCase(); -} - -// Operation name is simpler: just replace dots with underscores -function toOperationToolName(operationId) { - return operationId.replace(/\./g, '_'); -} - -// --------------------------------------------------------------------------- -// Tools policy — shared data that both runtimes consume from tools-policy.json -// --------------------------------------------------------------------------- - -const GROUP_DESCRIPTIONS = { - core: 'Core operations: read nodes, get text, insert/replace/delete content, mutations', - format: 'Text formatting, paragraph styles, alignment, spacing, borders, shading', - create: 'Create structural elements: headings, paragraphs, tables, sections, TOC', - tables: 'Table creation, manipulation, formatting, borders, and cell operations', - sections: 'Page layout, margins, columns, headers/footers, page numbering', - lists: 'Bullet and numbered lists, indentation, list types', - comments: 'Comment threads — create, edit, delete, list', - trackChanges: 'Track changes — list, inspect, accept/reject', - toc: 'Table of contents — create, configure, update, manage entries', - history: 'Undo, redo, history inspection', - session: 'Session management — open, close, save, list sessions', -}; - -const TOOLS_POLICY = { - policyVersion: 'v3', - groups: [ - 'core', 'format', 'create', 'tables', 'sections', - 'lists', 'comments', 'trackChanges', 'toc', 'history', 'session', - ], - groupDescriptions: GROUP_DESCRIPTIONS, - defaults: { - mode: 'essential', - maxTools: 20, - alwaysInclude: ['core'], - foundationalOperationIds: ['doc.info', 'doc.query.match'], - }, - capabilityFeatures: { - comments: ['hasComments'], - trackChanges: ['hasTrackedChanges'], - lists: ['hasLists'], - tables: ['hasTables'], - toc: ['hasToc'], - }, -}; - -// --------------------------------------------------------------------------- -// Category inference for capabilities -// --------------------------------------------------------------------------- - -const CAPABILITY_FEATURES = TOOLS_POLICY.capabilityFeatures; - - - -function inferRequiredCapabilities(category) { - return CAPABILITY_FEATURES[category] ?? []; -} - -function inferCapabilities(operation) { - const capabilities = new Set(); - const params = operation.params ?? []; - const paramNames = new Set(params.map((p) => p.name)); - - if (paramNames.has('doc')) capabilities.add('stateless-doc'); - if (paramNames.has('sessionId')) capabilities.add('session-targeting'); - if (paramNames.has('expectedRevision')) capabilities.add('optimistic-concurrency'); - if (paramNames.has('changeMode')) capabilities.add('tracked-change-mode'); - if (paramNames.has('dryRun')) capabilities.add('dry-run'); - if (paramNames.has('out')) capabilities.add('output-path'); - if (operation.category === 'comments') capabilities.add('comments'); - if (operation.category === 'trackChanges') capabilities.add('track-changes'); - if (operation.category === 'session') capabilities.add('session-management'); - if (operation.category === 'create') capabilities.add('structural-create'); - if (operation.category === 'query') capabilities.add('search'); - if (operation.category === 'introspection') capabilities.add('introspection'); - - return Array.from(capabilities).sort(); -} - -function inferSessionRequirements(operation) { - const params = operation.params ?? []; - const paramNames = new Set(params.map((p) => p.name)); - return { - requiresOpenContext: paramNames.has('doc') || paramNames.has('sessionId'), - supportsSessionTargeting: paramNames.has('sessionId'), - }; -} - -// --------------------------------------------------------------------------- -// Schema sanitization — ensure JSON Schema 2020-12 compliance -// --------------------------------------------------------------------------- - -/** - * Recursively fix bare `{ const: value }` nodes to include `type`. - * Anthropic requires `const` to be accompanied by a `type` field. - */ -function sanitizeSchema(schema) { - if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return schema; - - const result = { ...schema }; - - // "type": "json" is a SuperDoc contract sentinel for "any JSON value". - // It's not valid in JSON Schema draft 2020-12 — replace with empty schema. - if (result.type === 'json') { - delete result.type; - return result; - } - - // Fix bare const: add type based on the const value - if ('const' in result && !result.type) { - const val = result.const; - if (typeof val === 'string') result.type = 'string'; - else if (typeof val === 'number') result.type = 'number'; - else if (typeof val === 'boolean') result.type = 'boolean'; - } - - // Recurse into nested structures - if (result.properties) { - result.properties = Object.fromEntries( - Object.entries(result.properties).map(([k, v]) => [k, sanitizeSchema(v)]), - ); - } - if (Array.isArray(result.oneOf)) { - // Convert oneOf where every variant is { const: value } into { enum: [...] } - const allConst = result.oneOf.every((v) => v && typeof v === 'object' && 'const' in v && Object.keys(v).length <= 2); - if (allConst && result.oneOf.length > 0) { - const values = result.oneOf.map((v) => v.const); - delete result.oneOf; - result.enum = values; - } else { - result.oneOf = result.oneOf.map(sanitizeSchema); - } - } - if (Array.isArray(result.anyOf)) { - result.anyOf = result.anyOf.map(sanitizeSchema); - } - if (Array.isArray(result.allOf)) { - result.allOf = result.allOf.map(sanitizeSchema); - } - if (result.items) { - result.items = sanitizeSchema(result.items); - } - if (result.additionalProperties && typeof result.additionalProperties === 'object') { - result.additionalProperties = sanitizeSchema(result.additionalProperties); - } - - return result; -} - -// --------------------------------------------------------------------------- -// Build input schema from CLI params (for CLI-only ops or as fallback) -// --------------------------------------------------------------------------- - -function buildInputSchemaFromParams(operation) { - const properties = {}; - const required = []; - - for (const param of operation.params ?? []) { - // Skip params annotated as not agent-visible (transport-envelope details). - if (param.agentVisible === false) { - continue; - } - - let schema; - if (param.type === 'string' && param.schema) schema = { type: 'string', ...param.schema }; - else if (param.type === 'string') schema = { type: 'string' }; - else if (param.type === 'number') schema = { type: 'number' }; - else if (param.type === 'boolean') schema = { type: 'boolean' }; - else if (param.type === 'string[]') schema = { type: 'array', items: { type: 'string' } }; - else if (param.type === 'json' && param.schema && param.schema.type !== 'json') schema = param.schema; - else schema = { type: 'object' }; - - schema = sanitizeSchema(schema); - if (param.description) schema.description = param.description; - properties[param.name] = schema; - if (param.required) required.push(param.name); - } - - const result = { type: 'object', properties }; - if (required.length > 0) result.required = required; - result.additionalProperties = false; - return result; -} - -// --------------------------------------------------------------------------- -// Load document-api tools indexed by name -// --------------------------------------------------------------------------- - -async function loadDocApiTools() { - const raw = await readFile(DOCAPI_TOOLS_PATH, 'utf8'); - const manifest = JSON.parse(raw); - const index = new Map(); - for (const tool of manifest.tools ?? []) { - index.set(tool.name, tool); - } - return index; -} - -// --------------------------------------------------------------------------- -// Build unified catalog entry -// --------------------------------------------------------------------------- - -function buildCatalogEntry(operationId, operation, docApiTool, profile) { - const toolName = profile === 'intent' ? toIntentName(operationId, operation) : toOperationToolName(operationId); - - // Input schema: always derive from CLI params so field names match the dispatcher - // contract (doc-api inputSchema uses different names e.g. commentId vs id). - const inputSchema = buildInputSchemaFromParams(operation); - - // Output schema from contract - const outputSchema = operation.successSchema ?? operation.outputSchema ?? {}; - - return { - operationId, - toolName, - profile, - source: profile === 'intent' ? 'intent' : 'operation', - description: operation.description ?? '', - inputSchema, - outputSchema, - mutates: operation.mutates ?? false, - category: operation.category ?? 'core', - capabilities: inferCapabilities(operation), - constraints: operation.constraints ?? undefined, - errors: docApiTool?.possibleFailureCodes ?? [], - examples: [], - commandTokens: operation.commandTokens ?? [], - profileTags: [], - requiredCapabilities: inferRequiredCapabilities(operation.category), - sessionRequirements: inferSessionRequirements(operation), - intentId: profile === 'intent' ? toIntentName(operationId, operation) : undefined, - }; -} - -// --------------------------------------------------------------------------- -// Provider formatters -// --------------------------------------------------------------------------- - -function toOpenAiTool(entry) { - return { - type: 'function', - function: { - name: entry.toolName, - description: entry.description, - parameters: entry.inputSchema, - }, - }; -} - -function toAnthropicTool(entry) { - return { - name: entry.toolName, - description: entry.description, - input_schema: entry.inputSchema, - }; -} - -function toVercelTool(entry) { - return { - type: 'function', - function: { - name: entry.toolName, - description: entry.description, - parameters: entry.inputSchema, - }, - }; -} - -function toGenericTool(entry) { - return { - name: entry.toolName, - description: entry.description, - parameters: entry.inputSchema, - returns: entry.outputSchema, - metadata: { - operationId: entry.operationId, - profile: entry.profile, - mutates: entry.mutates, - category: entry.category, - capabilities: entry.capabilities, - constraints: entry.constraints, - requiredCapabilities: entry.requiredCapabilities, - profileTags: entry.profileTags, - examples: entry.examples, - commandTokens: entry.commandTokens, - }, - }; -} - -// --------------------------------------------------------------------------- -// Main generation -// --------------------------------------------------------------------------- - -export async function generateToolCatalogs(contract) { - const docApiTools = await loadDocApiTools(); - - const intentTools = []; - - for (const [operationId, operation] of Object.entries(contract.operations)) { - // Skip operations explicitly excluded from LLM tool catalogs - if (operation.skipAsATool) continue; - - // Map to doc-api tool by stripping 'doc.' prefix - const docApiName = operationId.replace(/^doc\./, ''); - const docApiTool = docApiTools.get(docApiName); - - const entry = buildCatalogEntry(operationId, operation, docApiTool, 'intent'); - if (operation.essential) entry.essential = true; - intentTools.push(entry); - } - - // Collect essential tool names - const essentialToolNames = intentTools - .filter((t) => t.essential) - .map((t) => t.toolName); - - // Full catalog - const catalog = { - contractVersion: contract.contractVersion, - generatedAt: null, - namePolicyVersion: NAME_POLICY_VERSION, - exposureVersion: EXPOSURE_VERSION, - toolCount: intentTools.length, - tools: intentTools, - }; - - // Tool name -> operation ID map - const toolNameMap = {}; - for (const tool of intentTools) { - toolNameMap[tool.toolName] = tool.operationId; - } - - // Build discover_tools schema: lists available groups with descriptions - const discoverToolSchema = { - type: 'object', - properties: { - groups: { - type: 'array', - items: { - type: 'string', - enum: TOOLS_POLICY.groups, - }, - description: 'Which tool groups to load. You can request multiple at once.', - }, - }, - required: ['groups'], - }; - - const discoverToolDescription = - 'Load additional tool groups when you need capabilities beyond the essential set. ' + - 'Call this BEFORE attempting to use tools from a specific group.\n\nAvailable groups:\n' + - TOOLS_POLICY.groups.map((g) => ` - ${g}: ${GROUP_DESCRIPTIONS[g]}`).join('\n'); - - // Provider bundles (with discover_tools appended) - const providers = { - openai: { formatter: toOpenAiTool, file: 'tools.openai.json' }, - anthropic: { formatter: toAnthropicTool, file: 'tools.anthropic.json' }, - vercel: { formatter: toVercelTool, file: 'tools.vercel.json' }, - generic: { formatter: toGenericTool, file: 'tools.generic.json' }, - }; - - // Build discover_tools in each provider format - const discoverToolByProvider = { - openai: { - type: 'function', - function: { name: 'discover_tools', description: discoverToolDescription, parameters: discoverToolSchema }, - }, - anthropic: { - name: 'discover_tools', description: discoverToolDescription, input_schema: discoverToolSchema, - }, - vercel: { - type: 'function', - function: { name: 'discover_tools', description: discoverToolDescription, parameters: discoverToolSchema }, - }, - generic: { - name: 'discover_tools', description: discoverToolDescription, parameters: discoverToolSchema, - }, - }; - - // Tools policy with contract hash and essential tool list - const policy = { - ...TOOLS_POLICY, - essentialTools: essentialToolNames, - discoverTool: { - name: 'discover_tools', - description: discoverToolDescription, - schema: discoverToolSchema, - }, - contractHash: contract.sourceHash, - }; - - const writes = [ - writeGeneratedFile(path.join(TOOLS_OUTPUT_DIR, 'catalog.json'), JSON.stringify(catalog, null, 2) + '\n'), - writeGeneratedFile( - path.join(TOOLS_OUTPUT_DIR, 'tool-name-map.json'), - JSON.stringify(toolNameMap, null, 2) + '\n', - ), - writeGeneratedFile( - path.join(TOOLS_OUTPUT_DIR, 'tools-policy.json'), - JSON.stringify(policy, null, 2) + '\n', - ), - ]; - - for (const [providerName, { formatter, file }] of Object.entries(providers)) { - const providerTools = intentTools.map(formatter); - // Append discover_tools as the last tool in the bundle - providerTools.push(discoverToolByProvider[providerName]); - const bundle = { - contractVersion: contract.contractVersion, - tools: providerTools, - }; - writes.push(writeGeneratedFile(path.join(TOOLS_OUTPUT_DIR, file), JSON.stringify(bundle, null, 2) + '\n')); - } - - await Promise.all(writes); -} - -if (import.meta.url.endsWith(process.argv[1]?.replace(/^file:\/\//, '') ?? '')) { - const contract = await loadContract(); - await generateToolCatalogs(contract); - console.log('Generated tool catalog files.'); -} diff --git a/packages/sdk/langs/node/README.md b/packages/sdk/langs/node/README.md index 2c97ebc4fe..721f6fe0f3 100644 --- a/packages/sdk/langs/node/README.md +++ b/packages/sdk/langs/node/README.md @@ -91,29 +91,39 @@ client.doc.insert(params) ### AI Tool Integration -The SDK includes built-in support for exposing document operations as AI tool definitions: +The SDK includes built-in support for exposing grouped intent tools as AI tool definitions: ```ts -import { chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk'; - -// Get tool definitions for your AI provider, filtered by group -const { tools, selected } = await chooseTools({ +import { + chooseTools, + dispatchSuperDocTool, + getToolCatalog, +} from '@superdoc-dev/sdk'; + +// Get the full grouped tool set for your AI provider +const { tools, meta } = await chooseTools({ provider: 'openai', // 'openai' | 'anthropic' | 'vercel' | 'generic' - groups: ['core', 'format', 'comments'], // core is always auto-included }); +// Optional: inspect the generated tool catalog +const catalog = await getToolCatalog(); + // Dispatch a tool call from the AI model const result = await dispatchSuperDocTool(client, toolName, args); ``` +The current catalog contains 9 grouped tools: +`superdoc_get_content`, `superdoc_edit`, `superdoc_format`, `superdoc_create`, `superdoc_list`, `superdoc_comment`, `superdoc_track_changes`, `superdoc_search`, and `superdoc_mutations`. + +Multi-action tools use an `action` field to select the underlying operation. Single-action tools like `superdoc_search` do not require `action`. + | Function | Description | |----------|-------------| -| `chooseTools(input)` | Select tools filtered by group for a provider | +| `chooseTools(input)` | Load grouped tool definitions for a provider | | `listTools(provider)` | List all tool definitions for a provider | | `dispatchSuperDocTool(client, toolName, args)` | Execute a tool call against a client | -| `resolveToolOperation(toolName)` | Map a tool name to its operation ID | -| `getToolCatalog()` | Load the full tool catalog | -| `getAvailableGroups()` | List all available tool groups | +| `getToolCatalog()` | Load the grouped tool catalog with metadata | +| `getSystemPrompt()` | Read the bundled system prompt for intent tools | ## Part of SuperDoc diff --git a/packages/sdk/langs/node/package.json b/packages/sdk/langs/node/package.json index ef5effcdff..257491062b 100644 --- a/packages/sdk/langs/node/package.json +++ b/packages/sdk/langs/node/package.json @@ -8,6 +8,7 @@ "types": "./dist/index.d.ts", "exports": { ".": { + "bun": "./src/index.ts", "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" diff --git a/packages/sdk/langs/node/src/index.ts b/packages/sdk/langs/node/src/index.ts index ee8ed0ecd1..ac62b04834 100644 --- a/packages/sdk/langs/node/src/index.ts +++ b/packages/sdk/langs/node/src/index.ts @@ -31,14 +31,8 @@ export function createSuperDocClient(options: SuperDocClientOptions = {}): Super } export { getSkill, installSkill, listSkills } from './skills.js'; -export { - chooseTools, - dispatchSuperDocTool, - getAvailableGroups, - getToolCatalog, - listTools, - resolveToolOperation, -} from './tools.js'; +export { chooseTools, dispatchSuperDocTool, getSystemPrompt, getToolCatalog, listTools } from './tools.js'; +export { dispatchIntentTool } from './generated/intent-dispatch.generated.js'; export { SuperDocCliError } from './runtime/errors.js'; export type { InvokeOptions, OperationSpec, OperationParamSpec, SuperDocClientOptions } from './runtime/process.js'; -export type { ToolChooserInput, ToolChooserMode, ToolGroup, ToolProvider } from './tools.js'; +export type { ToolChooserInput, ToolProvider } from './tools.js'; diff --git a/packages/sdk/langs/node/src/tools.ts b/packages/sdk/langs/node/src/tools.ts index 762f7d8416..1cc5c13d6f 100644 --- a/packages/sdk/langs/node/src/tools.ts +++ b/packages/sdk/langs/node/src/tools.ts @@ -1,134 +1,40 @@ import { readFile } from 'node:fs/promises'; -import { readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { CONTRACT, type ContractOperationEntry } from './generated/contract.js'; import type { InvokeOptions } from './runtime/process.js'; import { SuperDocCliError } from './runtime/errors.js'; +import { dispatchIntentTool } from './generated/intent-dispatch.generated.js'; export type ToolProvider = 'openai' | 'anthropic' | 'vercel' | 'generic'; -export type ToolGroup = - | 'core' - | 'format' - | 'create' - | 'tables' - | 'sections' - | 'lists' - | 'comments' - | 'trackChanges' - | 'toc' - | 'images' - | 'history' - | 'session'; - -export type ToolChooserMode = 'essential' | 'all'; - -export type ToolChooserInput = { - provider: ToolProvider; - groups?: ToolGroup[]; - /** Default: 'essential'. When 'essential', only essential tools are returned (plus any from `groups`). */ - mode?: ToolChooserMode; - /** Whether to include the discover_tools meta-tool. Default: true when mode='essential', false when mode='all'. */ - includeDiscoverTool?: boolean; +// Resolve tools directory relative to package root (works from both src/ and dist/) +const toolsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'tools'); +const providerFileByName: Record = { + openai: 'tools.openai.json', + anthropic: 'tools.anthropic.json', + vercel: 'tools.vercel.json', + generic: 'tools.generic.json', }; export type ToolCatalog = { contractVersion: string; generatedAt: string | null; - namePolicyVersion: string; - exposureVersion: string; toolCount: number; tools: ToolCatalogEntry[]; }; type ToolCatalogEntry = { - operationId: string; toolName: string; - profile: string; - source: string; description: string; inputSchema: Record; - outputSchema: Record; mutates: boolean; - category: string; - essential?: boolean; - capabilities: string[]; - constraints?: Record; - errors: string[]; - examples: unknown[]; - commandTokens: string[]; - profileTags: string[]; - requiredCapabilities: string[]; - sessionRequirements: { - requiresOpenContext: boolean; - supportsSessionTargeting: boolean; - }; - intentId?: string; + operations: Array<{ operationId: string; intentAction: string }>; }; -// Resolve tools directory relative to package root (works from both src/ and dist/) -const toolsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'tools'); -const providerFileByName: Record = { - openai: 'tools.openai.json', - anthropic: 'tools.anthropic.json', - vercel: 'tools.vercel.json', - generic: 'tools.generic.json', -}; - -// Policy is loaded from the generated tools-policy.json artifact. -type ToolsPolicy = { - policyVersion: string; - contractHash: string; - groups: string[]; - groupDescriptions?: Record; - essentialTools?: string[]; - discoverTool?: { - name: string; - description: string; - schema: Record; - }; - defaults: { - mode?: string; - maxTools: number; - alwaysInclude: string[]; - foundationalOperationIds: string[]; - }; - capabilityFeatures: Record; -}; - -let _policyCache: ToolsPolicy | null = null; -function loadPolicy(): ToolsPolicy { - if (_policyCache) return _policyCache; - const raw = readFileSync(path.join(toolsDir, 'tools-policy.json'), 'utf8'); - _policyCache = JSON.parse(raw) as ToolsPolicy; - return _policyCache; -} - function isRecord(value: unknown): value is Record { return typeof value === 'object' && value != null && !Array.isArray(value); } -function isPresent(value: unknown): boolean { - if (value == null) return false; - if (Array.isArray(value)) return value.length > 0; - return true; -} - -function extractProviderToolName(tool: Record): string | null { - // Anthropic / Generic: top-level name - if (typeof tool.name === 'string') return tool.name; - // OpenAI / Vercel: nested under function.name - if (isRecord(tool.function) && typeof (tool.function as Record).name === 'string') { - return (tool.function as Record).name as string; - } - return null; -} - -function invalidArgument(message: string, details?: Record): never { - throw new SuperDocCliError(message, { code: 'INVALID_ARGUMENT', details }); -} - async function readJson(fileName: string): Promise { const filePath = path.join(toolsDir, fileName); let raw = ''; @@ -164,124 +70,10 @@ async function loadProviderBundle(provider: ToolProvider): Promise<{ return readJson(providerFileByName[provider]); } -async function loadToolNameMap(): Promise> { - return readJson>('tool-name-map.json'); -} - async function loadCatalog(): Promise { return readJson('catalog.json'); } -/** All available tool groups from the policy. */ -export function getAvailableGroups(): ToolGroup[] { - const policy = loadPolicy(); - return policy.groups as ToolGroup[]; -} - -const OPERATION_INDEX: Record = Object.fromEntries( - Object.entries(CONTRACT.operations).map(([id, op]) => [id, op]), -); - -function validateDispatchArgs(operationId: string, args: Record): void { - const operation = OPERATION_INDEX[operationId]; - if (!operation) { - invalidArgument(`Unknown operation id ${operationId}.`); - } - - // Unknown-param rejection - const allowedParams = new Set(operation.params.map((param: { name: string }) => String(param.name))); - for (const key of Object.keys(args)) { - if (!allowedParams.has(key)) { - invalidArgument(`Unexpected parameter ${key} for ${operationId}.`); - } - } - - // Required-param enforcement - for (const param of operation.params) { - if ('required' in param && Boolean(param.required) && args[param.name] == null) { - invalidArgument(`Missing required parameter ${param.name} for ${operationId}.`); - } - } - - // Constraint validation (CLI handles schema-level type validation authoritatively) - const constraints = 'constraints' in operation ? (operation as Record).constraints : undefined; - if (!constraints || !isRecord(constraints)) return; - - const mutuallyExclusive = Array.isArray(constraints.mutuallyExclusive) ? constraints.mutuallyExclusive : []; - const requiresOneOf = Array.isArray(constraints.requiresOneOf) ? constraints.requiresOneOf : []; - const requiredWhen = Array.isArray(constraints.requiredWhen) ? constraints.requiredWhen : []; - - for (const group of mutuallyExclusive) { - if (!Array.isArray(group)) continue; - const present = group.filter((name: string) => isPresent(args[name])); - if (present.length > 1) { - invalidArgument(`Arguments are mutually exclusive for ${operationId}: ${group.join(', ')}`, { - operationId, - group, - }); - } - } - - for (const group of requiresOneOf) { - if (!Array.isArray(group)) continue; - const hasAny = group.some((name: string) => isPresent(args[name])); - if (!hasAny) { - invalidArgument(`One of the following arguments is required for ${operationId}: ${group.join(', ')}`, { - operationId, - group, - }); - } - } - - for (const rule of requiredWhen) { - if (!isRecord(rule)) continue; - const whenValue = args[rule.whenParam as string]; - let shouldRequire = false; - if (Object.prototype.hasOwnProperty.call(rule, 'equals')) { - shouldRequire = whenValue === rule.equals; - } else if (Object.prototype.hasOwnProperty.call(rule, 'present')) { - const present = rule.present === true; - shouldRequire = present ? isPresent(whenValue) : !isPresent(whenValue); - } else { - shouldRequire = isPresent(whenValue); - } - - if (shouldRequire && !isPresent(args[rule.param as string])) { - invalidArgument(`Argument ${rule.param} is required by constraints for ${operationId}.`, { - operationId, - rule, - }); - } - } -} - -function resolveDocApiMethod( - client: { doc: Record }, - operationId: string, -): (args: unknown, options?: InvokeOptions) => Promise { - const tokens = operationId.split('.').slice(1); - let cursor: unknown = client.doc; - - for (const token of tokens) { - if (!isRecord(cursor) || !(token in cursor)) { - throw new SuperDocCliError(`No SDK doc method found for operation ${operationId}.`, { - code: 'TOOL_DISPATCH_NOT_FOUND', - details: { operationId, token }, - }); - } - cursor = cursor[token]; - } - - if (typeof cursor !== 'function') { - throw new SuperDocCliError(`Resolved member for ${operationId} is not callable.`, { - code: 'TOOL_DISPATCH_NOT_FOUND', - details: { operationId }, - }); - } - - return cursor as (args: unknown, options?: InvokeOptions) => Promise; -} - export async function getToolCatalog(): Promise { return loadCatalog(); } @@ -298,116 +90,64 @@ export async function listTools(provider: ToolProvider): Promise { return tools; } -export async function resolveToolOperation(toolName: string): Promise { - const map = await loadToolNameMap(); - return typeof map[toolName] === 'string' ? map[toolName] : null; -} +export type ToolChooserInput = { + provider: ToolProvider; +}; /** - * Select tools for a specific provider. - * - * **mode='essential'** (default): Returns only essential tools + discover_tools. - * Pass `groups` to additionally load all tools from those categories. + * Select all intent tools for a specific provider. * - * **mode='all'**: Returns all tools from requested groups (or all groups if - * `groups` is omitted). No discover_tools included by default. + * Returns all intent tools in the requested provider format. * * @example * ```ts - * // Default: 5 essential tools + discover_tools * const { tools } = await chooseTools({ provider: 'openai' }); - * - * // Essential + all comment tools - * const { tools } = await chooseTools({ provider: 'openai', groups: ['comments'] }); - * - * // All tools (old behavior) - * const { tools } = await chooseTools({ provider: 'openai', mode: 'all' }); * ``` */ export async function chooseTools(input: ToolChooserInput): Promise<{ tools: unknown[]; - selected: Array<{ - operationId: string; - toolName: string; - category: string; - mutates: boolean; - }>; meta: { provider: ToolProvider; - mode: string; - groups: string[]; - selectedCount: number; + toolCount: number; }; }> { - const catalog = await loadCatalog(); - const policy = loadPolicy(); - - const mode = input.mode ?? (policy.defaults.mode as ToolChooserMode) ?? 'essential'; - const includeDiscover = input.includeDiscoverTool ?? mode === 'essential'; + const bundle = await loadProviderBundle(input.provider); + const tools = Array.isArray(bundle.tools) ? bundle.tools : []; - let selected: ToolCatalogEntry[]; + return { + tools, + meta: { + provider: input.provider, + toolCount: tools.length, + }, + }; +} - if (mode === 'essential') { - // Essential tools + any explicitly requested groups - const essentialNames = new Set(policy.essentialTools ?? []); - const requestedGroups = input.groups ? new Set(input.groups) : null; +function resolveDocApiMethod( + client: { doc: Record }, + operationId: string, +): (args: unknown, options?: InvokeOptions) => Promise { + const tokens = operationId.split('.').slice(1); + let cursor: unknown = client.doc; - selected = catalog.tools.filter((tool) => { - if (essentialNames.has(tool.toolName)) return true; - if (requestedGroups && requestedGroups.has(tool.category)) return true; - return false; - }); - } else { - // mode='all': original behavior — filter by groups - const alwaysInclude = new Set(policy.defaults.alwaysInclude ?? ['core']); - let groups: Set; - if (input.groups) { - groups = new Set([...input.groups, ...alwaysInclude]); - } else { - groups = new Set(policy.groups); + for (const token of tokens) { + if (!isRecord(cursor) || !(token in cursor)) { + throw new SuperDocCliError(`No SDK doc method found for operation ${operationId}.`, { + code: 'TOOL_DISPATCH_NOT_FOUND', + details: { operationId, token }, + }); } - selected = catalog.tools.filter((tool) => groups.has(tool.category)); + cursor = cursor[token]; } - // Build provider-formatted tools from the provider bundle - const bundle = await loadProviderBundle(input.provider); - const providerTools = Array.isArray(bundle.tools) ? bundle.tools : []; - const providerIndex = new Map( - providerTools - .filter((tool): tool is Record => isRecord(tool)) - .map((tool) => [extractProviderToolName(tool), tool] as const) - .filter((entry): entry is [string, Record] => entry[0] !== null), - ); - - const selectedProviderTools = selected - .map((tool) => providerIndex.get(tool.toolName)) - .filter((tool): tool is Record => Boolean(tool)); - - // Append discover_tools if requested - if (includeDiscover) { - const discoverTool = providerIndex.get('discover_tools'); - if (discoverTool) { - selectedProviderTools.push(discoverTool); - } + if (typeof cursor !== 'function') { + throw new SuperDocCliError(`Resolved member for ${operationId} is not callable.`, { + code: 'TOOL_DISPATCH_NOT_FOUND', + details: { operationId }, + }); } - const resolvedGroups = mode === 'essential' ? (input.groups ?? []) : (input.groups ?? policy.groups); - - return { - tools: selectedProviderTools, - selected: selected.map((tool) => ({ - operationId: tool.operationId, - toolName: tool.toolName, - category: tool.category, - mutates: tool.mutates, - })), - meta: { - provider: input.provider, - mode, - groups: [...resolvedGroups], - selectedCount: selectedProviderTools.length, - }, - }; + return cursor as (args: unknown, options?: InvokeOptions) => Promise; } export async function dispatchSuperDocTool( @@ -416,25 +156,33 @@ export async function dispatchSuperDocTool( args: Record = {}, invokeOptions?: InvokeOptions, ): Promise { - const operationId = await resolveToolOperation(toolName); - if (!operationId) { - throw new SuperDocCliError(`Unknown SuperDoc tool: ${toolName}`, { - code: 'TOOL_NOT_FOUND', + if (!isRecord(args)) { + throw new SuperDocCliError(`Tool arguments for ${toolName} must be an object.`, { + code: 'INVALID_ARGUMENT', details: { toolName }, }); } - if (!isRecord(args)) { - invalidArgument(`Tool arguments for ${toolName} must be an object.`); - } - // Strip doc/sessionId — the SDK client manages session targeting after doc.open(). - // Models fill these in because the tool schemas expose them, but passing them - // alongside an active session causes "stateless input.doc cannot be combined - // with a session target" errors. const { doc: _doc, sessionId: _sid, ...cleanArgs } = args; - validateDispatchArgs(operationId, cleanArgs); - const method = resolveDocApiMethod(client, operationId); - return method(cleanArgs, invokeOptions); + return dispatchIntentTool(toolName, cleanArgs, (operationId, input) => { + const method = resolveDocApiMethod(client, operationId); + return method(input, invokeOptions); + }); +} + +/** + * Read the bundled system prompt for intent tools. + */ +export async function getSystemPrompt(): Promise { + const promptPath = path.join(toolsDir, 'system-prompt.md'); + try { + return await readFile(promptPath, 'utf8'); + } catch { + throw new SuperDocCliError('System prompt not found.', { + code: 'TOOLS_ASSET_NOT_FOUND', + details: { filePath: promptPath }, + }); + } } diff --git a/packages/sdk/langs/python/superdoc/__init__.py b/packages/sdk/langs/python/superdoc/__init__.py index 218a1131bb..752998ca17 100644 --- a/packages/sdk/langs/python/superdoc/__init__.py +++ b/packages/sdk/langs/python/superdoc/__init__.py @@ -5,10 +5,9 @@ choose_tools, dispatch_superdoc_tool, dispatch_superdoc_tool_async, - get_available_groups, + get_system_prompt, get_tool_catalog, list_tools, - resolve_tool_operation, ) __all__ = [ @@ -20,9 +19,8 @@ "list_skills", "get_tool_catalog", "list_tools", - "resolve_tool_operation", - "get_available_groups", "choose_tools", "dispatch_superdoc_tool", "dispatch_superdoc_tool_async", + "get_system_prompt", ] diff --git a/packages/sdk/langs/python/superdoc/test_parity_helper.py b/packages/sdk/langs/python/superdoc/test_parity_helper.py index 0794ee726f..08fbd86f93 100644 --- a/packages/sdk/langs/python/superdoc/test_parity_helper.py +++ b/packages/sdk/langs/python/superdoc/test_parity_helper.py @@ -26,24 +26,26 @@ def main() -> None: result.pop('tools', None) print(json.dumps({'ok': True, 'result': result})) - elif action == 'validateDispatchArgs': - from superdoc.tools_api import _validate_dispatch_args + elif action == 'resolveIntentDispatch': + from superdoc.tools.intent_dispatch_generated import dispatch_intent_tool + tool_name = command['toolName'] + args = command.get('args', {}) + + # Use a mock execute that captures the operationId + captured = {} + def mock_execute(operation_id, input_args): + captured['operationId'] = operation_id + return None + try: - _validate_dispatch_args(command['operationId'], command['args']) - print(json.dumps({'ok': True, 'result': 'passed'})) + dispatch_intent_tool(tool_name, args, mock_execute) + print(json.dumps({'ok': True, 'result': captured})) except Exception as exc: - code = getattr(exc, 'code', None) or 'UNKNOWN' - print(json.dumps({'ok': True, 'result': {'rejected': True, 'code': code, 'message': str(exc)}})) - - elif action == 'resolveToolOperation': - from superdoc.tools_api import resolve_tool_operation - result = resolve_tool_operation(command['toolName']) - print(json.dumps({'ok': True, 'result': result})) + print(json.dumps({'ok': True, 'result': {'error': str(exc)}})) elif action == 'assertCollabAccepted': # Verify collab params pass through to the runtime without - # SDK-level rejection. We build the argv from the operation spec - # to confirm nothing throws. + # SDK-level rejection. from superdoc.protocol import build_operation_argv from superdoc.generated.contract import OPERATION_INDEX @@ -52,8 +54,6 @@ def main() -> None: operation = OPERATION_INDEX[operation_id] try: argv = build_operation_argv(operation, params) - # Verify collab param values survived into argv. - # Flag names are kebab-case (--collab-url), so check values. argv_str = ' '.join(argv) collab_params_present = any( str(params[key]) in argv_str diff --git a/packages/sdk/langs/python/superdoc/tools_api.py b/packages/sdk/langs/python/superdoc/tools_api.py index 299addbd5f..d485a892cd 100644 --- a/packages/sdk/langs/python/superdoc/tools_api.py +++ b/packages/sdk/langs/python/superdoc/tools_api.py @@ -4,37 +4,18 @@ import json import re from importlib import resources -from typing import Any, Dict, List, Literal, Mapping, Optional, TypedDict, cast +from typing import Any, Dict, List, Literal, Optional, TypedDict, cast from .errors import SuperDocError -from .generated.contract import OPERATION_INDEX +from .tools.intent_dispatch_generated import dispatch_intent_tool ToolProvider = Literal['openai', 'anthropic', 'vercel', 'generic'] -ToolGroup = Literal[ - 'core', 'format', 'create', 'tables', 'sections', - 'lists', 'comments', 'trackChanges', 'toc', 'images', 'history', 'session', -] -ToolChooserMode = Literal['essential', 'all'] class ToolChooserInput(TypedDict, total=False): provider: ToolProvider - groups: List[ToolGroup] - mode: ToolChooserMode - includeDiscoverTool: bool -# Policy is loaded from the generated tools-policy.json artifact. -_policy_cache: Optional[Dict[str, Any]] = None - - -def _load_policy() -> Dict[str, Any]: - global _policy_cache - if _policy_cache is not None: - return _policy_cache - _policy_cache = _read_json_asset('tools-policy.json') - return _policy_cache - PROVIDER_FILE: Dict[ToolProvider, str] = { 'openai': 'tools.openai.json', 'anthropic': 'tools.anthropic.json', @@ -95,241 +76,31 @@ def list_tools(provider: ToolProvider) -> List[Dict[str, Any]]: return cast(List[Dict[str, Any]], tools) -def resolve_tool_operation(tool_name: str) -> Optional[str]: - mapping = _read_json_asset('tool-name-map.json') - value = mapping.get(tool_name) - return value if isinstance(value, str) else None - - -def get_available_groups() -> List[str]: - policy = _load_policy() - return list(policy.get('groups', [])) - - -def _extract_provider_tool_name(tool: Dict[str, Any]) -> Optional[str]: - """Extract tool name from provider-specific format. - - Anthropic / Generic: top-level ``name``. - OpenAI / Vercel: nested under ``function.name``. - """ - name = tool.get('name') - if isinstance(name, str): - return name - fn = tool.get('function') - if isinstance(fn, dict): - fn_name = fn.get('name') - if isinstance(fn_name, str): - return fn_name - return None - - def choose_tools(input: ToolChooserInput) -> Dict[str, Any]: - """Select tools for a specific provider. - - **mode='essential'** (default): Returns only essential tools + discover_tools. - Pass ``groups`` to additionally load all tools from those categories. + """Select all intent tools for a specific provider. - **mode='all'**: Returns all tools from requested groups (or all groups if - ``groups`` is omitted). No discover_tools included by default. + Returns all intent tools in the requested provider format. Example:: - # Default: essential tools + discover_tools result = choose_tools({'provider': 'openai'}) - - # Essential + all comment tools - result = choose_tools({'provider': 'openai', 'groups': ['comments']}) - - # All tools (old behavior) - result = choose_tools({'provider': 'openai', 'mode': 'all'}) """ provider = input.get('provider') if provider not in ('openai', 'anthropic', 'vercel', 'generic'): raise SuperDocError('provider is required.', code='INVALID_ARGUMENT', details={'provider': provider}) - catalog = _read_json_asset('catalog.json') - tools_policy = _load_policy() - - catalog_tools = catalog.get('tools') - if not isinstance(catalog_tools, list): - raise SuperDocError('Catalog tools are invalid.', code='TOOLS_ASSET_INVALID') - - default_mode = tools_policy.get('defaults', {}).get('mode', 'essential') - mode = input.get('mode', default_mode) - include_discover_raw = input.get('includeDiscoverTool') - include_discover = include_discover_raw if include_discover_raw is not None else (mode == 'essential') - - if mode == 'essential': - # Essential tools + any explicitly requested groups - essential_names = set(tools_policy.get('essentialTools', [])) - requested_groups = set(input.get('groups', [])) if input.get('groups') is not None else None - - selected = [ - tool for tool in catalog_tools - if isinstance(tool, dict) and ( - str(tool.get('toolName', '')) in essential_names - or (requested_groups is not None and str(tool.get('category', '')) in requested_groups) - ) - ] - else: - # mode='all': original behavior — filter by groups - always_include = set(tools_policy.get('defaults', {}).get('alwaysInclude', ['core'])) - requested_groups_list = input.get('groups') - if requested_groups_list is not None: - groups = set(list(requested_groups_list) + list(always_include)) - else: - groups = set(tools_policy.get('groups', [])) - - selected = [ - tool for tool in catalog_tools - if isinstance(tool, dict) and str(tool.get('category', '')) in groups - ] - - # Build provider-formatted tools from the provider bundle - provider_bundle = _read_json_asset(PROVIDER_FILE[provider]) - provider_tools_raw = provider_bundle.get('tools') if isinstance(provider_bundle.get('tools'), list) else [] - provider_index: Dict[str, Dict[str, Any]] = {} - for tool in provider_tools_raw: - if not isinstance(tool, dict): - continue - name = _extract_provider_tool_name(tool) - if name is not None: - provider_index[name] = tool - - selected_provider_tools = [ - provider_index[name] - for name in [str(tool.get('toolName')) for tool in selected] - if name in provider_index - ] - - # Append discover_tools if requested - if include_discover: - discover_tool = provider_index.get('discover_tools') - if discover_tool is not None: - selected_provider_tools.append(discover_tool) - - resolved_groups: List[str] = ( - list(input.get('groups', []) if input.get('groups') is not None else []) - if mode == 'essential' - else list(input.get('groups') if input.get('groups') is not None else tools_policy.get('groups', [])) - ) + bundle = _read_json_asset(PROVIDER_FILE[provider]) + tools = bundle.get('tools') if isinstance(bundle.get('tools'), list) else [] return { - 'tools': selected_provider_tools, - 'selected': [ - { - 'operationId': str(tool.get('operationId')), - 'toolName': str(tool.get('toolName')), - 'category': str(tool.get('category')), - 'mutates': bool(tool.get('mutates')), - } - for tool in selected - ], + 'tools': tools, 'meta': { 'provider': provider, - 'mode': mode, - 'groups': sorted(resolved_groups), - 'selectedCount': len(selected_provider_tools), + 'toolCount': len(tools), }, } -def _validate_dispatch_args(operation_id: str, args: Dict[str, Any]) -> None: - operation = OPERATION_INDEX.get(operation_id) - if not isinstance(operation, dict): - raise SuperDocError('Unknown operation id.', code='INVALID_ARGUMENT', details={'operationId': operation_id}) - - params = operation.get('params') - if not isinstance(params, list): - raise SuperDocError('Operation params are invalid.', code='INVALID_ARGUMENT', details={'operationId': operation_id}) - - # Unknown-param rejection - allowed = {param.get('name') for param in params if isinstance(param, dict) and isinstance(param.get('name'), str)} - for key in args.keys(): - if key not in allowed: - raise SuperDocError( - f'Unexpected parameter {key} for {operation_id}.', - code='INVALID_ARGUMENT', - details={'operationId': operation_id, 'param': key}, - ) - - # Required-param enforcement - for param in params: - if not isinstance(param, dict): - continue - name = param.get('name') - if not isinstance(name, str): - continue - if bool(param.get('required')) and args.get(name) is None: - raise SuperDocError( - f'Missing required parameter {name} for {operation_id}.', - code='INVALID_ARGUMENT', - details={'operationId': operation_id, 'param': name}, - ) - - # Constraint validation (CLI handles schema-level type validation authoritatively) - constraints = operation.get('constraints') if isinstance(operation.get('constraints'), dict) else None - if constraints is None: - return - - def _is_present(val: Any) -> bool: - if val is None: - return False - if isinstance(val, list): - return len(val) > 0 - return True - - mutually_exclusive = constraints.get('mutuallyExclusive') if isinstance(constraints.get('mutuallyExclusive'), list) else [] - requires_one_of = constraints.get('requiresOneOf') if isinstance(constraints.get('requiresOneOf'), list) else [] - required_when = constraints.get('requiredWhen') if isinstance(constraints.get('requiredWhen'), list) else [] - - for group in mutually_exclusive: - if not isinstance(group, list): - continue - present = [name for name in group if _is_present(args.get(name))] - if len(present) > 1: - raise SuperDocError( - f'Arguments are mutually exclusive for {operation_id}: {", ".join(group)}', - code='INVALID_ARGUMENT', - details={'operationId': operation_id, 'group': group}, - ) - - for group in requires_one_of: - if not isinstance(group, list): - continue - has_any = any(_is_present(args.get(name)) for name in group) - if not has_any: - raise SuperDocError( - f'One of the following arguments is required for {operation_id}: {", ".join(group)}', - code='INVALID_ARGUMENT', - details={'operationId': operation_id, 'group': group}, - ) - - for rule in required_when: - if not isinstance(rule, dict): - continue - when_param = rule.get('whenParam') - when_value = args.get(when_param) if isinstance(when_param, str) else None - should_require = False - if 'equals' in rule: - should_require = when_value == rule['equals'] - elif 'present' in rule: - if rule['present'] is True: - should_require = _is_present(when_value) - else: - should_require = not _is_present(when_value) - else: - should_require = _is_present(when_value) - - param_name = rule.get('param') - if should_require and isinstance(param_name, str) and not _is_present(args.get(param_name)): - raise SuperDocError( - f'Argument {param_name} is required by constraints for {operation_id}.', - code='INVALID_ARGUMENT', - details={'operationId': operation_id, 'rule': rule}, - ) - - def _resolve_doc_method(client: Any, operation_id: str) -> Any: doc = getattr(client, 'doc', None) if doc is None: @@ -377,31 +148,25 @@ def dispatch_superdoc_tool( args: Optional[Dict[str, Any]] = None, invoke_options: Optional[Dict[str, Any]] = None, ) -> Any: - operation_id = resolve_tool_operation(tool_name) - if operation_id is None: - raise SuperDocError('Unknown SuperDoc tool.', code='TOOL_NOT_FOUND', details={'toolName': tool_name}) - payload = args or {} if not isinstance(payload, dict): raise SuperDocError('Tool arguments must be an object.', code='INVALID_ARGUMENT', details={'toolName': tool_name}) # Strip doc/sessionId — the SDK client manages session targeting after doc.open(). - # Models fill these in because the tool schemas expose them, but passing them - # alongside an active session causes errors. payload = {k: v for k, v in payload.items() if k not in ('doc', 'sessionId')} - _validate_dispatch_args(operation_id, payload) - method = _resolve_doc_method(client, operation_id) - - if inspect.iscoroutinefunction(method): - raise SuperDocError( - 'dispatch_superdoc_tool cannot call async methods. Use dispatch_superdoc_tool_async.', - code='INVALID_ARGUMENT', - details={'toolName': tool_name, 'operationId': operation_id}, - ) + def execute(operation_id: str, input_args: Dict[str, Any]) -> Any: + method = _resolve_doc_method(client, operation_id) + if inspect.iscoroutinefunction(method): + raise SuperDocError( + 'dispatch_superdoc_tool cannot call async methods. Use dispatch_superdoc_tool_async.', + code='INVALID_ARGUMENT', + details={'toolName': tool_name, 'operationId': operation_id}, + ) + kwargs = dict(invoke_options or {}) + return method(input_args, **kwargs) - kwargs = dict(invoke_options or {}) - return method(payload, **kwargs) + return dispatch_intent_tool(tool_name, payload, execute) async def dispatch_superdoc_tool_async( @@ -410,10 +175,6 @@ async def dispatch_superdoc_tool_async( args: Optional[Dict[str, Any]] = None, invoke_options: Optional[Dict[str, Any]] = None, ) -> Any: - operation_id = resolve_tool_operation(tool_name) - if operation_id is None: - raise SuperDocError('Unknown SuperDoc tool.', code='TOOL_NOT_FOUND', details={'toolName': tool_name}) - payload = args or {} if not isinstance(payload, dict): raise SuperDocError('Tool arguments must be an object.', code='INVALID_ARGUMENT', details={'toolName': tool_name}) @@ -421,12 +182,25 @@ async def dispatch_superdoc_tool_async( # Strip doc/sessionId — same as sync version above. payload = {k: v for k, v in payload.items() if k not in ('doc', 'sessionId')} - _validate_dispatch_args(operation_id, payload) - method = _resolve_doc_method(client, operation_id) - kwargs = dict(invoke_options or {}) + def execute(operation_id: str, input_args: Dict[str, Any]) -> Any: + method = _resolve_doc_method(client, operation_id) + kwargs = dict(invoke_options or {}) + return method(input_args, **kwargs) - result = method(payload, **kwargs) + result = dispatch_intent_tool(tool_name, payload, execute) if inspect.isawaitable(result): return await result - return result + + +def get_system_prompt() -> str: + """Read the bundled system prompt for intent tools.""" + resource = resources.files('superdoc').joinpath('tools', 'system-prompt.md') + try: + return resource.read_text(encoding='utf-8') + except FileNotFoundError as error: + raise SuperDocError( + 'System prompt not found.', + code='TOOLS_ASSET_NOT_FOUND', + details={'file': 'system-prompt.md'}, + ) from error diff --git a/packages/sdk/scripts/__tests__/node-dual-package.test.mjs b/packages/sdk/scripts/__tests__/node-dual-package.test.mjs index 6873a23977..88a1b18ac4 100644 --- a/packages/sdk/scripts/__tests__/node-dual-package.test.mjs +++ b/packages/sdk/scripts/__tests__/node-dual-package.test.mjs @@ -39,8 +39,7 @@ const EXPECTED_EXPORTS = [ 'chooseTools', 'dispatchSuperDocTool', 'getToolCatalog', - 'getAvailableGroups', - 'resolveToolOperation', + 'getSystemPrompt', 'SuperDocCliError', ]; diff --git a/packages/sdk/scripts/sdk-validate.mjs b/packages/sdk/scripts/sdk-validate.mjs index 8d4629601e..e98e57b2df 100644 --- a/packages/sdk/scripts/sdk-validate.mjs +++ b/packages/sdk/scripts/sdk-validate.mjs @@ -156,39 +156,28 @@ async function main() { await check('Python SDK imports successfully', async () => { await run('python3', [ '-c', - 'from superdoc import SuperDocClient, AsyncSuperDocClient, SuperDocError, get_tool_catalog, list_tools, resolve_tool_operation, choose_tools, dispatch_superdoc_tool, dispatch_superdoc_tool_async, get_available_groups', + 'from superdoc import SuperDocClient, AsyncSuperDocClient, SuperDocError, get_tool_catalog, list_tools, choose_tools, dispatch_superdoc_tool, dispatch_superdoc_tool_async, get_system_prompt', ], { cwd: path.join(REPO_ROOT, 'packages/sdk/langs/python'), }); }); - // 7. Tool catalog integrity - await check('Tool catalog operation count matches contract', async () => { + // 7. Intent tool catalog integrity + await check('Intent tool catalog has correct tool count', async () => { const catalog = await readJson(path.join(REPO_ROOT, 'packages/sdk/tools/catalog.json')); - // Count non-skipped operations in the contract - const nonSkippedOps = Object.entries(contract.operations).filter(([, op]) => !op.skipAsATool); - const expectedCount = nonSkippedOps.length; - const toolCount = catalog.tools.length; - - if (toolCount !== expectedCount) { - throw new Error(`Catalog tools (${toolCount}) != non-skipped contract ops (${expectedCount})`); - } - }); - - // 8. Tool name map covers all non-skipped operations - await check('Tool name map covers all operations', async () => { - const nameMap = await readJson(path.join(REPO_ROOT, 'packages/sdk/tools/tool-name-map.json')); - const mappedOps = new Set(Object.values(nameMap)); - - for (const [opId, op] of Object.entries(contract.operations)) { + // Count unique intentGroups in the contract + const intentGroups = new Set(); + for (const [, op] of Object.entries(contract.operations)) { if (op.skipAsATool) continue; - if (!mappedOps.has(opId)) { - throw new Error(`Operation ${opId} not covered by any tool name`); - } + if (op.intentGroup) intentGroups.add(op.intentGroup); + } + const toolCount = catalog.tools.length; + if (toolCount !== intentGroups.size) { + throw new Error(`Catalog intent tools (${toolCount}) != unique intent groups (${intentGroups.size})`); } }); - // 9. Provider bundles exist and have correct tool counts + // 8. Provider bundles exist and have correct tool counts await check('Provider bundles are consistent', async () => { const providers = ['openai', 'anthropic', 'vercel', 'generic']; const catalog = await readJson(path.join(REPO_ROOT, 'packages/sdk/tools/catalog.json')); @@ -197,9 +186,8 @@ async function main() { for (const provider of providers) { const bundle = await readJson(path.join(REPO_ROOT, `packages/sdk/tools/tools.${provider}.json`)); if (!Array.isArray(bundle.tools)) throw new Error(`${provider} bundle missing tools array`); - // Provider bundles include catalog tools + synthetic tools (e.g. discover_tools) - if (bundle.tools.length < expectedCount) { - throw new Error(`${provider} tool count (${bundle.tools.length}) < catalog (${expectedCount})`); + if (bundle.tools.length !== expectedCount) { + throw new Error(`${provider} tool count (${bundle.tools.length}) != catalog (${expectedCount})`); } } }); @@ -228,33 +216,16 @@ async function main() { } }); - // 11. All catalog tools have input schemas and required params match contract - await check('Catalog input schemas present and required params match contract', async () => { + // 11. All catalog tools have input schemas + await check('Catalog input schemas present', async () => { const catalog = await readJson(path.join(REPO_ROOT, 'packages/sdk/tools/catalog.json')); for (const tool of catalog.tools) { if (!tool.inputSchema || typeof tool.inputSchema !== 'object') { - throw new Error(`${tool.operationId} missing inputSchema`); + throw new Error(`${tool.toolName} missing inputSchema`); } - - // Verify required params from contract appear as required in inputSchema - const contractOp = contract.operations[tool.operationId]; - if (!contractOp) continue; - - const contractRequired = (contractOp.params ?? []) - .filter((p) => p.required === true) - .map((p) => p.name) - // Exclude transport-envelope params that are intentionally omitted from tool schemas - .filter((name) => !['out', 'json', 'expectedRevision', 'changeMode', 'dryRun'].includes(name)); - - const schemaRequired = new Set(tool.inputSchema.required ?? []); - for (const name of contractRequired) { - // Only check if the param is in the schema properties (some params are omitted by design) - if (tool.inputSchema.properties && name in tool.inputSchema.properties && !schemaRequired.has(name)) { - throw new Error( - `${tool.operationId}: param "${name}" is required in contract but not in inputSchema`, - ); - } + if (tool.inputSchema.type !== 'object') { + throw new Error(`${tool.toolName} inputSchema is not an object type`); } } }); @@ -300,19 +271,14 @@ async function main() { // 13. Provider tool name extraction smoke test await check('OpenAI/Vercel tools have extractable names', async () => { const openaiBundle = await readJson(path.join(REPO_ROOT, 'packages/sdk/tools/tools.openai.json')); - const nameMap = await readJson(path.join(REPO_ROOT, 'packages/sdk/tools/tool-name-map.json')); - - // Synthetic meta-tools (e.g. discover_tools) are not in the name map - const syntheticTools = new Set(['discover_tools']); for (const tool of openaiBundle.tools) { const name = tool?.function?.name ?? tool?.name; if (typeof name !== 'string' || !name) { throw new Error('OpenAI tool missing extractable name'); } - if (syntheticTools.has(name)) continue; - if (!(name in nameMap)) { - throw new Error(`OpenAI tool name "${name}" not in tool-name-map`); + if (!name.startsWith('superdoc_')) { + throw new Error(`OpenAI tool name "${name}" does not match superdoc_* pattern`); } } }); @@ -329,12 +295,12 @@ async function main() { const requiredTools = [ 'catalog.json', - 'tool-name-map.json', 'tools-policy.json', 'tools.openai.json', 'tools.anthropic.json', 'tools.vercel.json', 'tools.generic.json', + 'system-prompt.md', ]; const missingTools = requiredTools.filter((name) => !files.some((f) => f === `tools/${name}`)); if (missingTools.length > 0) { diff --git a/packages/sdk/tools/intent-dispatch.generated.ts b/packages/sdk/tools/intent-dispatch.generated.ts new file mode 100644 index 0000000000..e360e390e3 --- /dev/null +++ b/packages/sdk/tools/intent-dispatch.generated.ts @@ -0,0 +1,134 @@ +// Auto-generated by generate-intent-tools.mjs — do not edit + +export function dispatchIntentTool( + toolName: string, + args: Record, + execute: (operationId: string, input: Record) => unknown, +): unknown { + switch (toolName) { + case 'superdoc_get_content': { + const { action, ...rest } = args; + switch (action) { + case 'text': + return execute('doc.getText', rest); + case 'markdown': + return execute('doc.getMarkdown', rest); + case 'html': + return execute('doc.getHtml', rest); + case 'info': + return execute('doc.info', rest); + default: + throw new Error(`Unknown action for superdoc_get_content: ${action}`); + } + } + case 'superdoc_edit': { + const { action, ...rest } = args; + switch (action) { + case 'insert': + return execute('doc.insert', rest); + case 'replace': + return execute('doc.replace', rest); + case 'delete': + return execute('doc.delete', rest); + case 'undo': + return execute('doc.history.undo', rest); + case 'redo': + return execute('doc.history.redo', rest); + default: + throw new Error(`Unknown action for superdoc_edit: ${action}`); + } + } + case 'superdoc_format': { + const { action, ...rest } = args; + switch (action) { + case 'inline': + return execute('doc.format.apply', rest); + case 'set_style': + return execute('doc.styles.paragraph.setStyle', rest); + case 'set_alignment': + return execute('doc.format.paragraph.setAlignment', rest); + case 'set_indentation': + return execute('doc.format.paragraph.setIndentation', rest); + case 'set_spacing': + return execute('doc.format.paragraph.setSpacing', rest); + default: + throw new Error(`Unknown action for superdoc_format: ${action}`); + } + } + case 'superdoc_create': { + const { action, ...rest } = args; + switch (action) { + case 'paragraph': + return execute('doc.create.paragraph', rest); + case 'heading': + return execute('doc.create.heading', rest); + default: + throw new Error(`Unknown action for superdoc_create: ${action}`); + } + } + case 'superdoc_list': { + const { action, ...rest } = args; + switch (action) { + case 'insert': + return execute('doc.lists.insert', rest); + case 'create': + return execute('doc.lists.create', rest); + case 'detach': + return execute('doc.lists.detach', rest); + case 'indent': + return execute('doc.lists.indent', rest); + case 'outdent': + return execute('doc.lists.outdent', rest); + case 'set_level': + return execute('doc.lists.setLevel', rest); + case 'set_type': + return execute('doc.lists.setType', rest); + default: + throw new Error(`Unknown action for superdoc_list: ${action}`); + } + } + case 'superdoc_comment': { + const { action, ...rest } = args; + switch (action) { + case 'create': + return execute('doc.comments.create', rest); + case 'update': + return execute('doc.comments.patch', rest); + case 'delete': + return execute('doc.comments.delete', rest); + case 'get': + return execute('doc.comments.get', rest); + case 'list': + return execute('doc.comments.list', rest); + default: + throw new Error(`Unknown action for superdoc_comment: ${action}`); + } + } + case 'superdoc_track_changes': { + const { action, ...rest } = args; + switch (action) { + case 'list': + return execute('doc.trackChanges.list', rest); + case 'decide': + return execute('doc.trackChanges.decide', rest); + default: + throw new Error(`Unknown action for superdoc_track_changes: ${action}`); + } + } + case 'superdoc_search': + return execute('doc.query.match', args); + case 'superdoc_mutations': { + const { action, ...rest } = args; + switch (action) { + case 'preview': + return execute('doc.mutations.preview', rest); + case 'apply': + return execute('doc.mutations.apply', rest); + default: + throw new Error(`Unknown action for superdoc_mutations: ${action}`); + } + } + default: + throw new Error(`Unknown intent tool: ${toolName}`); + } +} diff --git a/packages/sdk/tools/intent_dispatch_generated.py b/packages/sdk/tools/intent_dispatch_generated.py new file mode 100644 index 0000000000..11b63f1cc4 --- /dev/null +++ b/packages/sdk/tools/intent_dispatch_generated.py @@ -0,0 +1,120 @@ +# Auto-generated by generate-intent-tools.mjs — do not edit + +from typing import Any, Callable, Dict + +from ..errors import SuperDocError + + +def dispatch_intent_tool( + tool_name: str, + args: Dict[str, Any], + execute: Callable[[str, Dict[str, Any]], Any], +) -> Any: + if tool_name == 'superdoc_get_content': + action = args.get('action') + rest = {k: v for k, v in args.items() if k != 'action'} + if action == 'text': + return execute('doc.getText', rest) + elif action == 'markdown': + return execute('doc.getMarkdown', rest) + elif action == 'html': + return execute('doc.getHtml', rest) + elif action == 'info': + return execute('doc.info', rest) + else: + raise SuperDocError(f'Unknown action for superdoc_get_content: {action}', code='TOOL_DISPATCH_NOT_FOUND', details={'toolName': 'superdoc_get_content', 'action': action}) + elif tool_name == 'superdoc_edit': + action = args.get('action') + rest = {k: v for k, v in args.items() if k != 'action'} + if action == 'insert': + return execute('doc.insert', rest) + elif action == 'replace': + return execute('doc.replace', rest) + elif action == 'delete': + return execute('doc.delete', rest) + elif action == 'undo': + return execute('doc.history.undo', rest) + elif action == 'redo': + return execute('doc.history.redo', rest) + else: + raise SuperDocError(f'Unknown action for superdoc_edit: {action}', code='TOOL_DISPATCH_NOT_FOUND', details={'toolName': 'superdoc_edit', 'action': action}) + elif tool_name == 'superdoc_format': + action = args.get('action') + rest = {k: v for k, v in args.items() if k != 'action'} + if action == 'inline': + return execute('doc.format.apply', rest) + elif action == 'set_style': + return execute('doc.styles.paragraph.setStyle', rest) + elif action == 'set_alignment': + return execute('doc.format.paragraph.setAlignment', rest) + elif action == 'set_indentation': + return execute('doc.format.paragraph.setIndentation', rest) + elif action == 'set_spacing': + return execute('doc.format.paragraph.setSpacing', rest) + else: + raise SuperDocError(f'Unknown action for superdoc_format: {action}', code='TOOL_DISPATCH_NOT_FOUND', details={'toolName': 'superdoc_format', 'action': action}) + elif tool_name == 'superdoc_create': + action = args.get('action') + rest = {k: v for k, v in args.items() if k != 'action'} + if action == 'paragraph': + return execute('doc.create.paragraph', rest) + elif action == 'heading': + return execute('doc.create.heading', rest) + else: + raise SuperDocError(f'Unknown action for superdoc_create: {action}', code='TOOL_DISPATCH_NOT_FOUND', details={'toolName': 'superdoc_create', 'action': action}) + elif tool_name == 'superdoc_list': + action = args.get('action') + rest = {k: v for k, v in args.items() if k != 'action'} + if action == 'insert': + return execute('doc.lists.insert', rest) + elif action == 'create': + return execute('doc.lists.create', rest) + elif action == 'detach': + return execute('doc.lists.detach', rest) + elif action == 'indent': + return execute('doc.lists.indent', rest) + elif action == 'outdent': + return execute('doc.lists.outdent', rest) + elif action == 'set_level': + return execute('doc.lists.setLevel', rest) + elif action == 'set_type': + return execute('doc.lists.setType', rest) + else: + raise SuperDocError(f'Unknown action for superdoc_list: {action}', code='TOOL_DISPATCH_NOT_FOUND', details={'toolName': 'superdoc_list', 'action': action}) + elif tool_name == 'superdoc_comment': + action = args.get('action') + rest = {k: v for k, v in args.items() if k != 'action'} + if action == 'create': + return execute('doc.comments.create', rest) + elif action == 'update': + return execute('doc.comments.patch', rest) + elif action == 'delete': + return execute('doc.comments.delete', rest) + elif action == 'get': + return execute('doc.comments.get', rest) + elif action == 'list': + return execute('doc.comments.list', rest) + else: + raise SuperDocError(f'Unknown action for superdoc_comment: {action}', code='TOOL_DISPATCH_NOT_FOUND', details={'toolName': 'superdoc_comment', 'action': action}) + elif tool_name == 'superdoc_track_changes': + action = args.get('action') + rest = {k: v for k, v in args.items() if k != 'action'} + if action == 'list': + return execute('doc.trackChanges.list', rest) + elif action == 'decide': + return execute('doc.trackChanges.decide', rest) + else: + raise SuperDocError(f'Unknown action for superdoc_track_changes: {action}', code='TOOL_DISPATCH_NOT_FOUND', details={'toolName': 'superdoc_track_changes', 'action': action}) + elif tool_name == 'superdoc_search': + return execute('doc.query.match', args) + elif tool_name == 'superdoc_mutations': + action = args.get('action') + rest = {k: v for k, v in args.items() if k != 'action'} + if action == 'preview': + return execute('doc.mutations.preview', rest) + elif action == 'apply': + return execute('doc.mutations.apply', rest) + else: + raise SuperDocError(f'Unknown action for superdoc_mutations: {action}', code='TOOL_DISPATCH_NOT_FOUND', details={'toolName': 'superdoc_mutations', 'action': action}) + else: + raise SuperDocError(f'Unknown intent tool: {tool_name}', code='TOOL_DISPATCH_NOT_FOUND', details={'toolName': tool_name}) diff --git a/packages/sdk/tools/system-prompt.md b/packages/sdk/tools/system-prompt.md new file mode 100644 index 0000000000..301aa7dee5 --- /dev/null +++ b/packages/sdk/tools/system-prompt.md @@ -0,0 +1,94 @@ +You are a document editing assistant. You have a DOCX document open and a set of intent-based tools available. + +## Tools overview + +| Tool | Purpose | +|------|---------| +| superdoc_search | Find text or nodes in the document | +| superdoc_get_content | Read document content in various formats | +| superdoc_edit | Insert, replace, delete text, undo/redo | +| superdoc_create | Create new paragraphs or headings | +| superdoc_format | Apply inline and paragraph formatting | +| superdoc_list | Create and manipulate bullet/numbered lists | +| superdoc_comment | Create, update, delete, and list comments | +| superdoc_track_changes | Review and resolve tracked changes | +| superdoc_mutations | Execute multi-step atomic edits in a single batch | + +## How targeting works + +Every editing tool needs a **target** — an address telling the API *where* to apply the change. + +### Getting targets + +Use `superdoc_search` to find content. Each match item returns: + +- **`handle`** — an opaque reference for text-level operations. Pass it directly as `target` to `superdoc_edit` and `superdoc_format` (for inline styles like bold, italic, etc.). +- **`address`** — a block-level address like `{ "kind": "block", "nodeType": "paragraph", "nodeId": "abc123" }`. Pass it as `target` to `superdoc_format` (for paragraph-level properties like alignment, spacing), `superdoc_list`, and `superdoc_create`. + +### Text search results + +When searching for text (`type: "text"`), each match includes: +- `snippet` — the matched text with surrounding context +- `highlightRange` — `{ start, end }` character offsets of the match +- `blocks` — array of `{ blockId, range }` entries showing which blocks contain the match + +### Node search results + +When searching for nodes (`type: "node"`), each match includes: +- `address` — the block address of the matched node + +## Multi-action tools + +Most tools support multiple actions via an `action` parameter. For example: +- `superdoc_get_content` with `action: "text"` returns plain text; `action: "markdown"` returns Markdown. +- `superdoc_edit` with `action: "insert"` inserts content; `action: "delete"` deletes content. +- `superdoc_format` with `action: "inline"` applies inline formatting; `action: "set_alignment"` sets paragraph alignment. + +Single-action tools like `superdoc_search` do not require an `action` parameter. + +## Workflow + +1. **Read first**: Use `superdoc_get_content` to understand the document. +2. **Search before editing**: Use `superdoc_search` to get valid targets. +3. **Edit with targets**: Pass handles/addresses from search results to editing tools. +4. **Batch when possible**: For multi-step edits (e.g., find-and-replace-all, rewrite + restyle), prefer `superdoc_mutations` — it's atomic, faster, and avoids stale-target issues. + +## Using superdoc_mutations + +The mutations tool executes a plan of steps atomically. Use `action: "apply"` to execute, or `action: "preview"` to dry-run. + +Each step has: +- `id` — unique step identifier (e.g., `"s1"`, `"s2"`) +- `op` — the operation: `text.rewrite`, `text.insert`, `text.delete`, `format.apply`, `assert` +- `where` — targeting: either `{ by: "select", select: {...}, require: "first"|"exactlyOne"|"all" }` or `{ by: "ref", ref: "handle-ref-string" }` +- `args` — operation-specific arguments + +### Workflow: split mutations by logical phase + +**Always use `superdoc_search` first** to obtain stable refs, then reference those refs in your mutation steps. + +Split mutation calls into logical rounds: +1. **Text mutations first** — all `text.rewrite`, `text.insert`, `text.delete` operations in one `superdoc_mutations` call. +2. **Formatting second** — all `format.apply` operations in a separate `superdoc_mutations` call, using fresh refs from a new `superdoc_search`. + +**Why**: Text edits change content and invalidate addresses. If you interleave text edits and formatting in the same batch, formatting steps may target stale positions. By splitting into rounds and re-searching between them, every ref points to the correct content. + +## Using superdoc_comment + +The comment tool manages comment threads in the document. + +- **`create`** — Create a new comment thread anchored to a target range. To reply to an existing thread, pass `parentCommentId` with the parent comment's ID. +- **`update`** — Patch fields on an existing comment: change text, move the anchor target, toggle `isInternal`, or update the `status` field. +- **`delete`** — Remove a comment or reply by ID. +- **`get`** — Retrieve a single comment thread by ID, including replies. +- **`list`** — List all comment threads in the document. + +### Resolving and reopening comments + +To resolve a comment, use `action: "update"` with `{ commentId: "", status: "resolved" }`. To reopen it, use `status: "open"`. There is no separate resolve action — it's a status field on the `update` action. + +## Important rules + +- **Do NOT combine `limit`/`offset` with `require: "first"` or `require: "exactlyOne"`** in superdoc_search. Use `require: "any"` with `limit` for paginated results. +- For `superdoc_format` inline properties, use `null` to clear a property (e.g., `"bold": null` removes bold). +- For `superdoc_list` create action: this converts existing paragraphs into list items. Create the paragraph first with `superdoc_create`, then convert it with `superdoc_list` action `create`. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d3e882a47..5372675ab2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -348,7 +348,7 @@ importers: version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@vitest/coverage-v8': specifier: 'catalog:' - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) concurrently: specifier: 'catalog:' version: 9.2.1 @@ -411,7 +411,7 @@ importers: version: 0.25.0(rollup@4.59.0)(vite@7.3.1(@types/node@22.19.2)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.31.1)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) optionalDependencies: canvas: specifier: 3.2.1 @@ -509,13 +509,16 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.26.0 version: 1.26.0(zod@4.3.6) + '@superdoc-dev/sdk': + specifier: workspace:* + version: link:../../packages/sdk/langs/node + '@superdoc/document-api': + specifier: workspace:* + version: link:../../packages/document-api zod: specifier: ^4.3.6 version: 4.3.6 devDependencies: - '@superdoc/document-api': - specifier: workspace:* - version: link:../../packages/document-api '@superdoc/super-editor': specifier: workspace:* version: link:../../packages/super-editor @@ -908,7 +911,7 @@ importers: devDependencies: '@angular-devkit/build-angular': specifier: ^21.1.4 - version: 21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.5)(chokidar@5.0.0)(jiti@2.6.1)(tailwindcss@4.2.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) + version: 21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.5)(chokidar@5.0.0)(jiti@2.6.1)(tailwindcss@4.2.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) '@angular/cli': specifier: ^21.1.4 version: 21.2.2(@types/node@25.3.5)(chokidar@5.0.0) @@ -1096,7 +1099,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) vue: specifier: 3.5.25 version: 3.5.25(typescript@5.9.3) @@ -1121,7 +1124,7 @@ importers: version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@vitest/coverage-v8': specifier: 'catalog:' - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) concurrently: specifier: 'catalog:' version: 9.2.1 @@ -1145,7 +1148,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) packages/document-api: {} @@ -1198,7 +1201,7 @@ importers: version: 4.5.4(@types/node@25.3.5)(rolldown-vite@7.3.1(@types/node@25.3.5)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.59.0)(typescript@5.9.3) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) packages/esign/demo: dependencies: @@ -1260,7 +1263,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) packages/layout-engine/layout-bridge: dependencies: @@ -1297,7 +1300,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) packages/layout-engine/layout-engine: dependencies: @@ -1358,7 +1361,7 @@ importers: devDependencies: vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) packages/layout-engine/pm-adapter: dependencies: @@ -1398,7 +1401,7 @@ importers: version: link:../painters/dom vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) packages/layout-engine/style-engine: dependencies: @@ -1414,7 +1417,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) packages/layout-engine/tests: dependencies: @@ -1496,7 +1499,7 @@ importers: version: 4.5.4(@types/node@22.19.2)(rolldown-vite@7.3.1(@types/node@22.19.2)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.59.0)(typescript@5.9.3) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) packages/sdk: {} @@ -1713,7 +1716,7 @@ importers: version: 0.25.0(rolldown-vite@7.3.1(@types/node@25.3.5)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.59.0) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) y-protocols: specifier: 'catalog:' version: 1.0.7(yjs@13.6.19) @@ -1819,7 +1822,7 @@ importers: version: 0.25.0(rolldown-vite@7.3.1(@types/node@25.3.5)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.59.0) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) ws: specifier: ^8.18.3 version: 8.19.0 @@ -1885,7 +1888,7 @@ importers: version: 4.5.4(@types/node@25.3.5)(rolldown-vite@7.3.1(@types/node@25.3.5)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(rollup@4.59.0)(typescript@5.9.3) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) packages/template-builder/demo: dependencies: @@ -1922,7 +1925,7 @@ importers: devDependencies: vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) shared/common: devDependencies: @@ -1937,7 +1940,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) vue: specifier: 3.5.25 version: 3.5.25(typescript@5.9.3) @@ -1972,7 +1975,7 @@ importers: devDependencies: vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) tests/visual: dependencies: @@ -4797,10 +4800,6 @@ packages: cpu: [x64] os: [win32] - '@mswjs/interceptors@0.41.3': - resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} - engines: {node: '>=18'} - '@napi-rs/canvas-android-arm64@0.1.80': resolution: {integrity: sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==} engines: {node: '>= 10'} @@ -5307,15 +5306,6 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} - '@open-draft/deferred-promise@2.2.0': - resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} - - '@open-draft/logger@0.3.0': - resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} - - '@open-draft/until@2.1.0': - resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@openai/agents-core@0.5.4': resolution: {integrity: sha512-qAT9zGIIM7GT5/WGkLpp8Fuar7NL5qu30b5+o2jP3mE6aMfx9OZjdj0za/iYLeV5kzQ5pOcbvRXenfzHrhvd/A==} peerDependencies: @@ -7846,9 +7836,6 @@ packages: '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} - '@types/statuses@2.0.6': - resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} - '@types/supports-color@8.1.3': resolution: {integrity: sha512-Hy6UMpxhE3j1tLpl27exp1XqHD7n8chAiNPzWfz16LPZoMMoSc4dzLl6w9qijkEb/r5O1ozdu1CWGA2L83ZeZg==} @@ -11455,10 +11442,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphql@16.13.1: - resolution: {integrity: sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==} - engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - guid-typescript@1.0.9: resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} @@ -11622,9 +11605,6 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - headers-polyfill@4.0.3: - resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} - hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -12157,9 +12137,6 @@ packages: resolution: {integrity: sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==} engines: {node: '>=16'} - is-node-process@1.2.0: - resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} - is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -13673,9 +13650,6 @@ packages: engines: {node: '>=10'} hasBin: true - mlly@1.8.0: - resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - mlly@1.8.1: resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} @@ -13746,16 +13720,6 @@ packages: msgpackr@1.11.9: resolution: {integrity: sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==} - msw@2.12.11: - resolution: {integrity: sha512-dVg20zi2I2EvnwH/+WupzsOC2mCa7qsIhyMAWtfRikn6RKtwL9+7SaF1IQ5LyZry4tlUtf6KyTVhnlQiZXozTQ==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - typescript: '>= 4.8.x' - peerDependenciesMeta: - typescript: - optional: true - muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} @@ -14405,9 +14369,6 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} - outvariant@1.4.3: - resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} - own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -14713,9 +14674,6 @@ packages: path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} - path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -15937,9 +15895,6 @@ packages: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} - rettime@0.10.1: - resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -16635,9 +16590,6 @@ packages: streamx@2.23.0: resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} - strict-event-emitter@0.5.1: - resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} - string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -17583,9 +17535,6 @@ packages: uploadthing: optional: true - until-async@3.0.2: - resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} - untun@0.1.3: resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==} hasBin: true @@ -18560,13 +18509,13 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/build-angular@21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.5)(chokidar@5.0.0)(jiti@2.6.1)(tailwindcss@4.2.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)': + '@angular-devkit/build-angular@21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.5)(chokidar@5.0.0)(jiti@2.6.1)(tailwindcss@4.2.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.2(chokidar@5.0.0) '@angular-devkit/build-webpack': 0.2102.2(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(esbuild@0.27.3)))(webpack@5.105.2(esbuild@0.27.3)) '@angular-devkit/core': 21.2.2(chokidar@5.0.0) - '@angular/build': 21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.5)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(tailwindcss@4.2.1)(terser@5.46.0)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) + '@angular/build': 21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.5)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(tailwindcss@4.2.1)(terser@5.46.0)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3) '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -18675,7 +18624,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular/build@21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.5)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(tailwindcss@4.2.1)(terser@5.46.0)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)': + '@angular/build@21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.5)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(tailwindcss@4.2.1)(terser@5.46.0)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.2(chokidar@5.0.0) @@ -18715,7 +18664,7 @@ snapshots: lmdb: 3.5.1 postcss: 8.5.6 tailwindcss: 4.2.1 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - chokidar @@ -18871,9 +18820,9 @@ snapshots: '@stoplight/types': 13.20.0 '@types/json-schema': 7.0.15 '@types/urijs': 1.19.26 - ajv: 8.17.1 - ajv-errors: 3.0.0(ajv@8.17.1) - ajv-formats: 2.1.1(ajv@8.17.1) + ajv: 8.18.0 + ajv-errors: 3.0.0(ajv@8.18.0) + ajv-formats: 2.1.1(ajv@8.18.0) avsc: 5.7.9 js-yaml: 4.1.1 jsonpath-plus: 10.3.0 @@ -20963,7 +20912,7 @@ snapshots: '@commitlint/config-validator@19.8.1': dependencies: '@commitlint/types': 19.8.1 - ajv: 8.17.1 + ajv: 8.18.0 '@commitlint/ensure@19.8.1': dependencies: @@ -21365,8 +21314,8 @@ snapshots: '@fastify/ajv-compiler@4.0.5': dependencies: - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) fast-uri: 3.1.0 '@fastify/error@4.2.0': {} @@ -21701,14 +21650,6 @@ snapshots: optionalDependencies: '@types/node': 25.3.5 - '@inquirer/confirm@5.1.21(@types/node@22.19.2)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.2) - '@inquirer/type': 3.0.10(@types/node@22.19.2) - optionalDependencies: - '@types/node': 22.19.2 - optional: true - '@inquirer/confirm@5.1.21(@types/node@25.3.5)': dependencies: '@inquirer/core': 10.3.2(@types/node@25.3.5) @@ -21723,20 +21664,6 @@ snapshots: optionalDependencies: '@types/node': 25.3.5 - '@inquirer/core@10.3.2(@types/node@22.19.2)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@22.19.2) - cli-width: 4.1.0 - mute-stream: 2.0.0 - signal-exit: 4.1.0 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 22.19.2 - optional: true - '@inquirer/core@10.3.2(@types/node@25.3.5)': dependencies: '@inquirer/ansi': 1.0.2 @@ -21899,11 +21826,6 @@ snapshots: optionalDependencies: '@types/node': 25.3.5 - '@inquirer/type@3.0.10(@types/node@22.19.2)': - optionalDependencies: - '@types/node': 22.19.2 - optional: true - '@inquirer/type@3.0.10(@types/node@25.3.5)': optionalDependencies: '@types/node': 25.3.5 @@ -21931,7 +21853,7 @@ snapshots: '@isaacs/fs-minipass@4.0.1': dependencies: - minipass: 7.1.2 + minipass: 7.1.3 '@istanbuljs/schema@0.1.3': {} @@ -22193,7 +22115,7 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdx': 2.0.13 - acorn: 8.15.0 + acorn: 8.16.0 collapse-white-space: 2.1.0 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 @@ -22202,7 +22124,7 @@ snapshots: hast-util-to-jsx-runtime: 2.3.6 markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 - recma-jsx: 1.0.1(acorn@8.15.0) + recma-jsx: 1.0.1(acorn@8.16.0) recma-stringify: 1.0.0 rehype-recma: 1.0.0 remark-mdx: 3.1.1 @@ -22515,9 +22437,9 @@ snapshots: '@mintlify/openapi-parser@0.0.8': dependencies: - ajv: 8.17.1 - ajv-draft-04: 1.0.0(ajv@8.17.1) - ajv-formats: 3.0.1(ajv@8.17.1) + ajv: 8.18.0 + ajv-draft-04: 1.0.0(ajv@8.18.0) + ajv-formats: 3.0.1(ajv@8.18.0) jsonpointer: 5.0.1 leven: 4.1.0 yaml: 2.8.2 @@ -22776,16 +22698,6 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true - '@mswjs/interceptors@0.41.3': - dependencies: - '@open-draft/deferred-promise': 2.2.0 - '@open-draft/logger': 0.3.0 - '@open-draft/until': 2.1.0 - is-node-process: 1.2.0 - outvariant: 1.4.3 - strict-event-emitter: 0.5.1 - optional: true - '@napi-rs/canvas-android-arm64@0.1.80': optional: true @@ -23414,18 +23326,6 @@ snapshots: '@one-ini/wasm@0.1.1': {} - '@open-draft/deferred-promise@2.2.0': - optional: true - - '@open-draft/logger@0.3.0': - dependencies: - is-node-process: 1.2.0 - outvariant: 1.4.3 - optional: true - - '@open-draft/until@2.1.0': - optional: true - '@openai/agents-core@0.5.4(ws@8.19.0)(zod@4.3.6)': dependencies: debug: 4.4.3(supports-color@5.5.0) @@ -25465,9 +25365,9 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@stoplight/better-ajv-errors@1.0.3(ajv@8.17.1)': + '@stoplight/better-ajv-errors@1.0.3(ajv@8.18.0)': dependencies: - ajv: 8.17.1 + ajv: 8.18.0 jsonpointer: 5.0.1 leven: 3.1.0 @@ -25506,7 +25406,7 @@ snapshots: '@stoplight/spectral-core@1.21.0': dependencies: - '@stoplight/better-ajv-errors': 1.0.3(ajv@8.17.1) + '@stoplight/better-ajv-errors': 1.0.3(ajv@8.18.0) '@stoplight/json': 3.21.0 '@stoplight/path': 1.3.2 '@stoplight/spectral-parsers': 1.0.5 @@ -25515,9 +25415,9 @@ snapshots: '@stoplight/types': 13.6.0 '@types/es-aggregate-error': 1.0.6 '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-errors: 3.0.0(ajv@8.17.1) - ajv-formats: 2.1.1(ajv@8.17.1) + ajv: 8.18.0 + ajv-errors: 3.0.0(ajv@8.18.0) + ajv-formats: 2.1.1(ajv@8.18.0) es-aggregate-error: 1.0.14 jsonpath-plus: 10.3.0 lodash: 4.17.23 @@ -25541,15 +25441,15 @@ snapshots: '@stoplight/spectral-functions@1.10.1': dependencies: - '@stoplight/better-ajv-errors': 1.0.3(ajv@8.17.1) + '@stoplight/better-ajv-errors': 1.0.3(ajv@8.18.0) '@stoplight/json': 3.21.0 '@stoplight/spectral-core': 1.21.0 '@stoplight/spectral-formats': 1.8.2 '@stoplight/spectral-runtime': 1.1.4 - ajv: 8.17.1 - ajv-draft-04: 1.0.0(ajv@8.17.1) - ajv-errors: 3.0.0(ajv@8.17.1) - ajv-formats: 2.1.1(ajv@8.17.1) + ajv: 8.18.0 + ajv-draft-04: 1.0.0(ajv@8.18.0) + ajv-errors: 3.0.0(ajv@8.18.0) + ajv-formats: 2.1.1(ajv@8.18.0) lodash: 4.17.23 tslib: 2.8.1 transitivePeerDependencies: @@ -26141,9 +26041,6 @@ snapshots: dependencies: '@types/node': 22.19.2 - '@types/statuses@2.0.6': - optional: true - '@types/supports-color@8.1.3': {} '@types/tough-cookie@4.0.5': @@ -26355,8 +26252,8 @@ snapshots: dependencies: '@mapbox/node-pre-gyp': 2.0.3 '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - acorn: 8.15.0 - acorn-import-attributes: 1.9.5(acorn@8.15.0) + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 @@ -26604,7 +26501,7 @@ snapshots: vite: rolldown-vite@7.3.1(@types/node@25.3.5)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) vue: 3.5.25(typescript@5.9.3) - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -26619,7 +26516,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -26631,22 +26528,20 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(rolldown-vite@7.3.1(@types/node@22.19.2)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(rolldown-vite@7.3.1(@types/node@22.19.2)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.12.11(@types/node@22.19.2)(typescript@5.9.3) vite: rolldown-vite@7.3.1(@types/node@22.19.2)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@3.2.4(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(rolldown-vite@7.3.1(@types/node@25.3.5)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(rolldown-vite@7.3.1(@types/node@25.3.5)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.12.11(@types/node@25.3.5)(typescript@5.9.3) vite: rolldown-vite@7.3.1(@types/node@25.3.5)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': @@ -27016,9 +26911,9 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-import-attributes@1.9.5(acorn@8.15.0): + acorn-import-attributes@1.9.5(acorn@8.16.0): dependencies: - acorn: 8.15.0 + acorn: 8.16.0 acorn-import-phases@1.0.4(acorn@8.15.0): dependencies: @@ -27032,6 +26927,10 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn@8.11.2: {} acorn@8.15.0: {} @@ -27088,17 +26987,13 @@ snapshots: optionalDependencies: ajv: 8.13.0 - ajv-draft-04@1.0.0(ajv@8.17.1): + ajv-draft-04@1.0.0(ajv@8.18.0): optionalDependencies: - ajv: 8.17.1 + ajv: 8.18.0 - ajv-errors@3.0.0(ajv@8.17.1): + ajv-errors@3.0.0(ajv@8.18.0): dependencies: - ajv: 8.17.1 - - ajv-formats@2.1.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 + ajv: 8.18.0 ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: @@ -27814,7 +27709,7 @@ snapshots: browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001779 + caniuse-lite: 1.0.30001767 electron-to-chromium: 1.5.286 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -27954,7 +27849,7 @@ snapshots: caniuse-api@3.0.0: dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001779 + caniuse-lite: 1.0.30001767 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 @@ -29287,7 +29182,7 @@ snapshots: esast-util-from-js@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - acorn: 8.15.0 + acorn: 8.16.0 esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 @@ -29669,8 +29564,8 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 esprima@4.0.1: {} @@ -29958,8 +29853,8 @@ snapshots: fast-json-stringify@6.3.0: dependencies: '@fastify/merge-json-schemas': 0.2.1 - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) fast-uri: 3.1.0 json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 @@ -30169,7 +30064,7 @@ snapshots: fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 - mlly: 1.8.0 + mlly: 1.8.1 rollup: 4.59.0 flat-cache@4.0.1: @@ -30467,7 +30362,7 @@ snapshots: foreground-child: 3.3.1 jackspeak: 3.4.3 minimatch: 9.0.9 - minipass: 7.1.2 + minipass: 7.1.3 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 @@ -30620,9 +30515,6 @@ snapshots: graceful-fs@4.2.11: {} - graphql@16.13.1: - optional: true - guid-typescript@1.0.9: optional: true @@ -30664,7 +30556,7 @@ snapshots: happy-dom@20.4.0: dependencies: - '@types/node': 25.3.5 + '@types/node': 22.19.2 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 4.5.0 @@ -30982,9 +30874,6 @@ snapshots: he@1.2.0: {} - headers-polyfill@4.0.3: - optional: true - hermes-estree@0.25.1: {} hermes-parser@0.25.1: @@ -31549,9 +31438,6 @@ snapshots: is-network-error@1.3.1: {} - is-node-process@1.2.0: - optional: true - is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -32286,7 +32172,7 @@ snapshots: local-pkg@1.1.2: dependencies: - mlly: 1.8.0 + mlly: 1.8.1 pkg-types: 2.3.0 quansync: 0.2.11 @@ -32474,7 +32360,7 @@ snapshots: '@npmcli/redact': 4.0.0 cacache: 20.0.3 http-cache-semantics: 4.2.0 - minipass: 7.1.2 + minipass: 7.1.3 minipass-fetch: 5.0.2 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 @@ -33497,11 +33383,11 @@ snapshots: minipass-collect@2.0.1: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 minipass-fetch@5.0.2: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 minipass-sized: 2.0.0 minizlib: 3.1.0 optionalDependencies: @@ -33536,7 +33422,7 @@ snapshots: minizlib@3.1.0: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 mintlify@4.2.331(@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.3))(react@19.2.3))(@types/node@25.3.5)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.3))(typescript@5.9.3): dependencies: @@ -33570,13 +33456,6 @@ snapshots: mkdirp@1.0.4: {} - mlly@1.8.0: - dependencies: - acorn: 8.15.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.3 - mlly@1.8.1: dependencies: acorn: 8.16.0 @@ -33651,58 +33530,6 @@ snapshots: msgpackr-extract: 3.0.3 optional: true - msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3): - dependencies: - '@inquirer/confirm': 5.1.21(@types/node@22.19.2) - '@mswjs/interceptors': 0.41.3 - '@open-draft/deferred-promise': 2.2.0 - '@types/statuses': 2.0.6 - cookie: 1.1.1 - graphql: 16.13.1 - headers-polyfill: 4.0.3 - is-node-process: 1.2.0 - outvariant: 1.4.3 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - rettime: 0.10.1 - statuses: 2.0.2 - strict-event-emitter: 0.5.1 - tough-cookie: 6.0.0 - type-fest: 5.4.4 - until-async: 3.0.2 - yargs: 17.7.2 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - '@types/node' - optional: true - - msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3): - dependencies: - '@inquirer/confirm': 5.1.21(@types/node@25.3.5) - '@mswjs/interceptors': 0.41.3 - '@open-draft/deferred-promise': 2.2.0 - '@types/statuses': 2.0.6 - cookie: 1.1.1 - graphql: 16.13.1 - headers-polyfill: 4.0.3 - is-node-process: 1.2.0 - outvariant: 1.4.3 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - rettime: 0.10.1 - statuses: 2.0.2 - strict-event-emitter: 0.5.1 - tough-cookie: 6.0.0 - type-fest: 5.4.4 - until-async: 3.0.2 - yargs: 17.7.2 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - '@types/node' - optional: true - muggle-string@0.4.1: {} multicast-dns@7.2.5: @@ -33812,7 +33639,7 @@ snapshots: needle@3.5.0: dependencies: iconv-lite: 0.6.3 - sax: 1.4.4 + sax: 1.5.0 optional: true negotiator@0.6.3: {} @@ -34549,9 +34376,6 @@ snapshots: os-tmpdir@1.0.2: {} - outvariant@1.4.3: - optional: true - own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -34926,11 +34750,11 @@ snapshots: path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 - minipass: 7.1.2 + minipass: 7.1.3 path-scurry@2.0.2: dependencies: - lru-cache: 11.2.7 + lru-cache: 11.2.5 minipass: 7.1.3 path-to-regexp@0.1.12: {} @@ -34939,9 +34763,6 @@ snapshots: path-to-regexp@3.3.0: {} - path-to-regexp@6.3.0: - optional: true - path-to-regexp@8.3.0: {} path-type@4.0.0: {} @@ -36186,10 +36007,10 @@ snapshots: estree-util-build-jsx: 3.0.1 vfile: 6.0.3 - recma-jsx@1.0.1(acorn@8.15.0): + recma-jsx@1.0.1(acorn@8.16.0): dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) estree-util-to-js: 2.0.0 recma-parse: 1.0.0 recma-stringify: 1.0.0 @@ -36597,9 +36418,6 @@ snapshots: retry@0.13.1: {} - rettime@0.10.1: - optional: true - reusify@1.1.0: {} rfdc@1.4.1: {} @@ -37605,9 +37423,6 @@ snapshots: - bare-abort-controller - react-native-b4a - strict-event-emitter@0.5.1: - optional: true - string-argv@0.3.2: {} string-width@4.2.3: @@ -38008,7 +37823,7 @@ snapshots: terser@5.46.1: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.15.0 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -38634,7 +38449,7 @@ snapshots: unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 - acorn: 8.15.0 + acorn: 8.16.0 picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 @@ -38689,9 +38504,6 @@ snapshots: db0: 0.3.4(better-sqlite3@12.8.0)(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(bun-types@1.3.8)(pg@8.20.0)) ioredis: 5.10.0 - until-async@3.0.2: - optional: true - untun@0.1.3: dependencies: citty: 0.1.6 @@ -39173,11 +38985,11 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.12.11(@types/node@22.19.2)(typescript@5.9.3))(rolldown-vite@7.3.1(@types/node@22.19.2)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(rolldown-vite@7.3.1(@types/node@22.19.2)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -39217,11 +39029,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.5)(esbuild@0.27.3)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.1))(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.12.11(@types/node@25.3.5)(typescript@5.9.3))(rolldown-vite@7.3.1(@types/node@25.3.5)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(rolldown-vite@7.3.1(@types/node@25.3.5)(esbuild@0.27.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -39668,7 +39480,7 @@ snapshots: xml-js@1.6.11: dependencies: - sax: 1.4.4 + sax: 1.5.0 xml-name-validator@5.0.0: {} @@ -39679,7 +39491,7 @@ snapshots: xml2js@0.6.2: dependencies: - sax: 1.4.4 + sax: 1.5.0 xmlbuilder: 11.0.1 xmlbuilder@11.0.1: {}