Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/charmbracelet/harmonica v0.2.0
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654
github.com/charmbracelet/x/ansi v0.11.7
github.com/extend-hq/extend-go-sdk v0.6.0
github.com/extend-hq/extend-go-sdk v0.10.0
github.com/itchyny/gojq v0.12.19
github.com/mattn/go-isatty v0.0.21
github.com/spf13/cobra v1.10.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/extend-hq/extend-go-sdk v0.6.0 h1:ZP0TLd77YtFyD8bMxYdpWIsymuJpYdpiFcH+QTtN6vQ=
github.com/extend-hq/extend-go-sdk v0.6.0/go.mod h1:OGwNG071bADLZ/BicWu/TLYn+QGELr/r051eECkfcn0=
github.com/extend-hq/extend-go-sdk v0.10.0 h1:Hv7RlnN0adxWsrncZW9yKrEx2VSxVBH5f+zFQW7uDa4=
github.com/extend-hq/extend-go-sdk v0.10.0/go.mod h1:OGwNG071bADLZ/BicWu/TLYn+QGELr/r051eECkfcn0=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
Expand Down
28 changes: 22 additions & 6 deletions internal/cli/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

extend "github.com/extend-hq/extend-go-sdk"
sdkclient "github.com/extend-hq/extend-go-sdk/client"
"github.com/extend-hq/extend-go-sdk/option"

"github.com/extend-hq/extend-cli/internal/extendx"
"github.com/extend-hq/extend-cli/internal/output"
Expand Down Expand Up @@ -269,10 +270,13 @@ contained runs with ` + "`extend extract runs list --batch <id>`" + `.`,
}
extractor.OverrideConfig = &override
}
br, err := prep.Client.ExtractRuns.CreateBatch(cmd.Context(), &extend.ExtractRunsCreateBatchRequest{
req := &extend.ExtractRunsCreateBatchRequest{
Extractor: extractor,
Inputs: items,
Priority: priorityPtr(f.priority),
}
br, err := extendx.StartOnce(cmd.Context(), func(opts ...option.RequestOption) (*extend.BatchRun, error) {
return prep.Client.ExtractRuns.CreateBatch(cmd.Context(), req, opts...)
})
if err != nil {
return fmt.Errorf("submit batch: %w", err)
Expand Down Expand Up @@ -347,10 +351,13 @@ contained runs with ` + "`extend classify runs list --batch <id>`" + `.`,
}
classifier.OverrideConfig = &override
}
br, err := prep.Client.ClassifyRuns.CreateBatch(cmd.Context(), &extend.ClassifyRunsCreateBatchRequest{
req := &extend.ClassifyRunsCreateBatchRequest{
Classifier: classifier,
Inputs: items,
Priority: priorityPtr(f.priority),
}
br, err := extendx.StartOnce(cmd.Context(), func(opts ...option.RequestOption) (*extend.BatchRun, error) {
return prep.Client.ClassifyRuns.CreateBatch(cmd.Context(), req, opts...)
})
if err != nil {
return fmt.Errorf("submit batch: %w", err)
Expand Down Expand Up @@ -423,10 +430,13 @@ contained runs with ` + "`extend split runs list --batch <id>`" + `.`,
}
splitter.OverrideConfig = &override
}
br, err := prep.Client.SplitRuns.CreateBatch(cmd.Context(), &extend.SplitRunsCreateBatchRequest{
req := &extend.SplitRunsCreateBatchRequest{
Splitter: splitter,
Inputs: items,
Priority: priorityPtr(f.priority),
}
br, err := extendx.StartOnce(cmd.Context(), func(opts ...option.RequestOption) (*extend.BatchRun, error) {
return prep.Client.SplitRuns.CreateBatch(cmd.Context(), req, opts...)
})
if err != nil {
return fmt.Errorf("submit batch: %w", err)
Expand Down Expand Up @@ -523,10 +533,13 @@ contained runs with ` + "`extend parse runs list --batch <id>`" + `.`,
Metadata: metadataPtr(prep.Metadata),
}
}
br, err := prep.Client.ParseRuns.CreateBatch(cmd.Context(), &extend.ParseRunsCreateBatchRequest{
req := &extend.ParseRunsCreateBatchRequest{
Inputs: items,
Config: cfg,
Priority: priorityPtr(priority),
}
br, err := extendx.StartOnce(cmd.Context(), func(opts ...option.RequestOption) (*extend.BatchRun, error) {
return prep.Client.ParseRuns.CreateBatch(cmd.Context(), req, opts...)
})
if err != nil {
return fmt.Errorf("submit batch: %w", err)
Expand All @@ -536,7 +549,7 @@ contained runs with ` + "`extend parse runs list --batch <id>`" + `.`,
Configure: func(cmd *cobra.Command) {
cmd.Flags().StringVar(&filesFrom, "files-from", "", "Path to a file with one input per line (- for stdin)")
cmd.Flags().StringVar(&target, "target", "markdown", "Parse target: markdown or spatial")
cmd.Flags().StringVar(&engine, "engine", "", "Engine: parse_performance or parse_light (default: server default)")
cmd.Flags().StringVar(&engine, "engine", "", "Engine: parse_auto, parse_performance, or parse_light (default: server default)")
cmd.Flags().StringVar(&engineVersion, "engine-version", "", "Engine version (e.g. latest, 1.0.1, 2.0.0-beta)")
cmd.Flags().StringVar(&chunkStrategy, "chunk-strategy", "", "Chunking strategy: page|document|section (none omits chunkingStrategy)")
cmd.Flags().IntVar(&chunkMinChars, "chunk-min-chars", 0, "Minimum characters per chunk (server default if 0)")
Expand Down Expand Up @@ -630,12 +643,15 @@ no 'workflows batches' commands. Track progress with:
}
items[i] = &extend.WorkflowRunsCreateBatchRequestInputsItem{File: file, Secrets: secretsPtr}
}
resp, err := prep.Client.WorkflowRuns.CreateBatch(cmd.Context(), &extend.WorkflowRunsCreateBatchRequest{
req := &extend.WorkflowRunsCreateBatchRequest{
Workflow: &extend.WorkflowReference{
ID: f.using,
Version: versionPtr(f.version),
},
Inputs: items,
}
resp, err := extendx.StartOnce(cmd.Context(), func(opts ...option.RequestOption) (*extend.WorkflowRunsCreateBatchResponse, error) {
return prep.Client.WorkflowRuns.CreateBatch(cmd.Context(), req, opts...)
})
if err != nil {
return fmt.Errorf("submit batch: %w", err)
Expand Down
5 changes: 4 additions & 1 deletion internal/cli/classify.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

extend "github.com/extend-hq/extend-go-sdk"
sdkclient "github.com/extend-hq/extend-go-sdk/client"
"github.com/extend-hq/extend-go-sdk/option"

"github.com/extend-hq/extend-cli/internal/extendx"
"github.com/extend-hq/extend-cli/internal/output"
Expand Down Expand Up @@ -208,7 +209,9 @@ func runClassify(ctx context.Context, app *App, p classifyParams) error {
req.Priority = &pr
}

run, err := cli.ClassifyRuns.Create(ctx, req)
run, err := extendx.StartOnce(ctx, func(opts ...option.RequestOption) (*extend.ClassifyRun, error) {
return cli.ClassifyRuns.Create(ctx, req, opts...)
})
if err != nil {
return fmt.Errorf("create run: %w", err)
}
Expand Down
75 changes: 71 additions & 4 deletions internal/cli/configdocs.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ package cli
//
// Source of truth is the pinned extend-go-sdk structs: SplitConfig,
// ExtractConfigJSON, ClassifyConfig, WorkflowsCreateRequest.Steps,
// ParseConfigBlockOptions, ParseConfigAdvancedOptions, and EditJSON. When
// ParseConfigBlockOptions, ParseConfigAdvancedOptions,
// EditConfigAdvancedOptions, EditSchemaGenerationConfigAdvancedOptions,
// and EditJSON. When
// the SDK is regenerated and a field is added, removed, or renamed, update
// the matching catalog here in the same change — drift is a defect, not a
// follow-up. The drift guards in configdocs_test.go enforce this.
Expand Down Expand Up @@ -155,7 +157,9 @@ const workflowStepsFields = ` - steps (array) - the workflow's processing graph
- CLASSIFY and SPLIT route with classificationId values. These must match
classification IDs from the classifier/splitter config, not type labels.
- CONDITIONAL routes with conditionId values matching config.conditions[].id.
- RULE_VALIDATION routes with result values: "pass" or "fail".
- RULE_VALIDATION and VALIDATION route with result values: "pass" or "fail".
- ROUTER routes with branchId values matching config.branches[].id, plus the
reserved "otherwise" route taken when no branch matches.
- Conditional and formula fields can reference prior processor outputs as
"{{stepName.output.*}}", workflow run metadata as "{{metadata.*}}", and
external validation responses as
Expand All @@ -178,6 +182,11 @@ const workflowStepsFields = ` - steps (array) - the workflow's processing graph
{ "step": "review", "result": "fail" }
]

"next": [
{ "step": "extract_invoice", "branchId": "is_invoice" },
{ "step": "review", "branchId": "otherwise" }
]

Processor references and versions:
- EXTRACT config references { "extractor": { "id": "ex_xxx", "version": "latest" } }.
- CLASSIFY config references { "classifier": { "id": "cl_xxx", "version": "0.1" } }.
Expand Down Expand Up @@ -220,6 +229,17 @@ const workflowStepsFields = ` - steps (array) - the workflow's processing graph
the formula must evaluate to a boolean and may reference any earlier step
output, including data fetched with EXTERNAL_DATA_VALIDATION. A common
topology routes pass to WEBHOOK_RESPONSE and fail to HUMAN_REVIEW.
- VALIDATION - checks workflow data with config.blocks, all of which must pass;
route on result "pass" or "fail". A block is code or natural language:
{ "name": "totals_match", "blockType": "code",
"code": { "language": "javascript", "code": "..." } }
{ "name": "looks_legit", "blockType": "semantic",
"semantic": { "promptText": "..." } }
Only JavaScript code runs today; python blocks fail when evaluated.
- ROUTER - evaluates config.branches in order and takes the first branch whose
block returns true, or the "otherwise" route when none does. Each branch is
{ "id", "name", "block" }, with a code or semantic block as in VALIDATION;
code blocks must return a boolean.
- EXTERNAL_DATA_VALIDATION - posts extraction data to an external HTTP endpoint;
config.requestOptions sets url, method, headers, and contentType, and
failureBehavior controls failures. Extend also signs requests with the
Expand Down Expand Up @@ -305,7 +325,10 @@ const parseBlockOptionsFields = ` - figures (object) — enabled; figureImageCl
(carry headers across page breaks); cellBlocksEnabled (emit a block per cell);
agentic (object: enabled, customInstructions).
- text (object) — signatureDetectionEnabled; agentic (object: enabled, customInstructions).
- keyValue (object) — blankFieldFormattingEnabled (emit detected blank fields).
- keyValue (object) — blankFieldFormattingEnabled (emit detected blank fields);
structuredOutputEnabled (return each form region as a typed tree of keys, values,
checkboxes, signatures, and tables under details.form; needs parse_performance
>= 2.0.0, parse_light >= 1.0.0, or parse_auto).
- barcodes (object) — readingEnabled (decode barcode values); imageClippingEnabled.
- formulas (object) — enabled (detect formulas and include their LaTeX representation).`

Expand All @@ -320,8 +343,20 @@ const parseAdvancedOptionsFields = ` - pageRotationEnabled (bool) — auto-dete
- excelIncludeCellMetadata (bool) — include cell provenance (source refs/formulas, data-cell/data-formula in HTML); advanced mode only.
- excelIncludeCellFormatting (bool) — include cell formatting (bold, italic, font/background color, inline HTML styles); advanced mode only.
- verticalGroupingThreshold (number 0.1–5.0, default 1.0) — line-grouping sensitivity; --target spatial only.
- returnOcr (object) — words (bool): include word-level raw OCR data in the response.
- returnOcr (object) — words (bool): include word-level raw OCR data in the response;
lines (bool): include line-level OCR data at output.ocr.lines. Independent opt-ins.
- textExtractionMethod ("auto"|"ocr_only") — how PDF text is read: "auto" uses the PDF's
embedded text where it is reliable and OCRs the rest; "ocr_only" OCRs every page.
parse_auto and parse_light >= 1.0.0 only (default "auto"); other engines always OCR.
- embedOcrInPdf (bool) — add the recognized words to the processed PDF as an invisible
text layer, so it can be searched and copied; pages that already have text are
skipped. Download the PDF afterwards with ` + "`extend files download <run's file.id>`" + `.
- alwaysConvertToPdf (bool) — convert images/Office/HTML to PDF first (enables spatial bboxes).
- htmlSourceAnchors (bool) — HTML input only: give each block a source with the byte range it
came from in the file, instead of polygon/boundingBox. The file must be UTF-8 or
Windows-1252; can't be combined with alwaysConvertToPdf.
- returnSourceNodes (bool) — HTML input only: also return word-level source anchors at
output.source.nodes, like returnOcr.words for PDFs. Requires htmlSourceAnchors.
- enrichmentFormat ("xml"|"bracket") — annotation style, e.g. <page_number>1</page_number> vs [page_number: 1].
- imageConversionQuality ("high"|"medium"|"low") — quality when converting to PDF.
- formattingDetection (array) — change-tracking detection; each entry { "type": "change_tracking" }.
Expand Down Expand Up @@ -366,13 +401,45 @@ each field. Populate the value keys; leave detected structural keys as generated
fontSize, fontColor (RGB 0-255), font.
- extend_edit:column_width (number) — column width percentage for table fields.
- extend_edit:row_heights (array) — row height percentages for array/table fields.
- extend_edit:source_acroform (object) — the PDF's own form field behind this property:
fieldName, fieldType, and optionMap/optionValue for choice fields. Generated for
native fields; keep it unchanged. With advancedOptions.preserveSignatureValidity it
names the existing field to fill.
- extend:descriptions (array) — human-readable labels for the enum values, in the same
order. Generated for choice fields; keep it unchanged.

Standard JSON Schema keys carry through: description, properties, items, enum,
required, additionalProperties, and maxItems (row cap for array/table fields).
Root-level conditional form logic is supported with dependentRequired,
if/then/else, allOf, oneOf, anyOf, and not. Conditional clauses do not accept
extend_edit:* keys.`

// editAdvancedOptionsFields documents the EditConfigAdvancedOptions object
// (edit --advanced-options).
const editAdvancedOptionsFields = ` flattenPdf bool Make the filled form non-editable (server default: true).
nativeFieldsOnly bool Only use embedded AcroForm fields; set false to also detect fields via vision.
tableParsingEnabled bool Parse table regions as arrays of objects so their cells can be filled.
radioEnumsEnabled bool Model a radio-button group as a single-choice enum so only one option fills.
conditionalGenerationEnabled bool When the server generates the schema, add the form's stated requirements
as conditional rules; values that break them fail the run.
advancedSchemaRefinementEnabled bool When the server generates the schema, organize field names and descriptions;
instructions can nest fields or merge options into one choice (adds latency).
Edit 1.0.0 or later; omitted uses the organization's default (off).
preserveSignatureValidity bool Try to keep existing digital signatures valid: fill only existing fields, matched
by extend_edit:source_acroform.fieldName; flattenPdf then defaults to false, and
flattenPdf true fails on signed PDFs. Edit 1.0.0-beta or later.`

// formDetectionAdvancedOptionsFields documents the
// EditSchemaGenerationConfigAdvancedOptions object (detect-form
// --advanced-options).
const formDetectionAdvancedOptionsFields = ` nativeFieldsOnly bool Only use embedded AcroForm fields; set false to also detect fields via vision.
tableParsingEnabled bool Parse table regions as arrays of objects.
radioEnumsEnabled bool Model a radio-button group as a single-choice enum.
conditionalGenerationEnabled bool Add the form's stated requirements as root-level conditional rules.
advancedSchemaRefinementEnabled bool Organize field names and descriptions; instructions can nest fields or
merge checkbox/radio options into one choice (adds latency). Edit 1.0.0 or
later; omitted uses the organization's default (off).`

const editOutputDoc = `Edit output:

- output.editedFile.id — Extend file ID for the completed PDF; reusable as input
Expand Down
2 changes: 2 additions & 0 deletions internal/cli/configdocs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ func TestConfigCatalogsCoverSDKFields(t *testing.T) {
// Parse --advanced-options: top-level plus the returnOcr leaf.
{"ParseConfigAdvancedOptions", reflect.TypeOf(extend.ParseConfigAdvancedOptions{}), parseAdvancedOptionsFields},
{"ParseConfigAdvancedOptionsReturnOcr", reflect.TypeOf(extend.ParseConfigAdvancedOptionsReturnOcr{}), parseAdvancedOptionsFields},
{"EditConfigAdvancedOptions", reflect.TypeOf(extend.EditConfigAdvancedOptions{}), editAdvancedOptionsFields},
{"EditSchemaGenerationConfigAdvancedOptions", reflect.TypeOf(extend.EditSchemaGenerationConfigAdvancedOptions{}), formDetectionAdvancedOptionsFields},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
12 changes: 7 additions & 5 deletions internal/cli/detectform.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

extend "github.com/extend-hq/extend-go-sdk"
sdkclient "github.com/extend-hq/extend-go-sdk/client"
"github.com/extend-hq/extend-go-sdk/option"

"github.com/extend-hq/extend-cli/internal/extendx"
"github.com/extend-hq/extend-cli/internal/output"
Expand Down Expand Up @@ -63,9 +64,7 @@ run object reports the resolved exact version under config.
Detection options ride in --advanced-options as a JSON object (omitted
fields use the server default):

nativeFieldsOnly bool Only use embedded AcroForm fields; set false to also detect fields via vision.
tableParsingEnabled bool Parse table regions as arrays of objects.
radioEnumsEnabled bool Model a radio-button group as a single-choice enum.
` + formDetectionAdvancedOptionsFields + `

` + formDetectionOutputDoc + `

Expand Down Expand Up @@ -129,9 +128,12 @@ fields use the server default):
}
cfg.InputSchema = &schema
}
run, err := cli.FormDetectionRuns.Create(cmd.Context(), &extend.FormDetectionRunsCreateRequest{
req := &extend.FormDetectionRunsCreateRequest{
File: file,
Config: cfg,
}
run, err := extendx.StartOnce(cmd.Context(), func(opts ...option.RequestOption) (*extend.FormDetectionRun, error) {
return cli.FormDetectionRuns.Create(cmd.Context(), req, opts...)
})
if err != nil {
return fmt.Errorf("create run: %w", err)
Expand All @@ -156,7 +158,7 @@ fields use the server default):
return renderWithDefault(app, final, output.FormatJSON)
},
Configure: func(cmd *cobra.Command) {
cmd.Flags().StringVar(&advancedOptionsPath, "advanced-options", "", "Detection options as a JSON object: nativeFieldsOnly, tableParsingEnabled, radioEnumsEnabled. Source: inline JSON, path, file:// URI, or '-' for stdin. Omitted fields use the server default.")
cmd.Flags().StringVar(&advancedOptionsPath, "advanced-options", "", "Detection options as a JSON object: nativeFieldsOnly, tableParsingEnabled, radioEnumsEnabled, conditionalGenerationEnabled, advancedSchemaRefinementEnabled. Source: inline JSON, path, file:// URI, or '-' for stdin. Omitted fields use the server default.")
cmd.Flags().StringVar(&engineVersion, "engine-version", "", "Edit engine version (e.g. latest, 0.0.1, 1.0.0-beta). Omit for the server default; the run reports the resolved exact version.")
cmd.Flags().StringVar(&instructions, "instructions", "", "Free-form instructions to guide schema generation")
cmd.Flags().StringVar(&inputSchemaPath, "input-schema", "", "Starting-point JSON Schema (overlaid by detection). Source: inline JSON, path, file:// URI, or '-' for stdin.")
Expand Down
Loading
Loading