The discovery envelope is remote data, but four of its shapes were
forwarded to pflag registration calls that panic:
- a flag named 'params' or 'json' collides with the reserved payload
flags registered at the end of ApplyBindings (the original pre-1.0.32
lockout: "chat_permission_grant flag redefined: params")
- two bindings resolving to the same long flag name (cross-binding
duplicate primary/alias; the existing dedup map was per-binding only)
- two flags claiming the same shorthand
- a multi-character shorthand
Because the command tree is built from the cached envelope before Cobra
dispatches anything, any of these aborted every CLI invocation.
Add canRegisterFlag (skip duplicate/reserved long names; CollectBindings
already tolerates missing flags via Lookup→nil→continue, and the value
stays reachable through --params) and safeShorthand (drop invalid or
taken shorthands, keep the long flag) and apply them at every
envelope-driven registration site in ApplyBindings and
registerPositionalAliasFlags. The trailing --json/--params registration
is also made idempotent.
Complements #447: that PR adds the escape hatch when the build panics;
this removes the known panic vectors so the escape hatch should never
be needed for them.
- printExecutionError: in JSON mode emit the structured error to stdout
(not stderr) so machine consumers parsing stdout get a parseable result;
update the 3 root_execute tests to assert the new contract.
- requireYesForDelete prompt: when stdin is not a TTY (piped/scripted/JSON
pipelines), do not show an interactive confirm that emits non-JSON and
blocks on stdin; return a structured validation error requiring --yes.
NewDirectCommand was hard-coding cobra.NoArgs for envelope-generated
leaf commands that have no positional bindings (totalMax == 0). This
is stricter than cobra's own default — legacyArgs (args.go:30-32)
returns nil for any command without subcommands.
The strict behavior surfaced as "unknown command \"<word>\" for
\"dws aisearch person\"" whenever an AI agent passed trailing
positional words after a leaf, e.g.
dws aisearch person search --keyword "张"
dws aisearch person user search --keyword "张"
Switching to cobra.ArbitraryArgs restores cobra's natural leaf
behavior: trailing positional args are silently ignored. Existing
positional-binding paths (MinimumNArgs / RangeArgs / MaximumNArgs)
are unchanged.
Verified against dws-wukong/auto-test/cli_to_mcp/testcases on
aiapp / live / aisearch: 50/50 pass (was 48/50; the 2 remaining
failures were the F-class extra-positional-args tolerance cases
this commit fixes).
* feat(transform): add file_read transform for UTF-8 file / stdin content (#277)
Introduces a new ApplyTransform case "file_read":
- Input: a string flag value treated as a UTF-8 file path
- Special case: the path "-" reads from stdin
- Output: the file contents as a string
- Errors surface as validation errors (exit 2) — non-UTF-8, missing
file, empty path, and non-string input all reject cleanly
This is a foundational primitive intended to compose with envelope
schema features so a CLI flag like --content-file can carry a path
that ultimately feeds a string-typed MCP parameter with the file's
contents.
## Scope note — MapsTo intentionally NOT included
The original proposal in #277 paired this transform with a new
CLIFlagOverride.MapsTo field that retargets a flag's value to a
different MCP parameter (e.g. --content / --content-file both feed
the upstream `markdown` param). Pre-production end-to-end validation
(see #282) showed that the MCP registry server-side schema does not
currently recognise `mapsTo` and strips it on serialisation. Every
other new envelope field (hidden / required / transform /
mutuallyExclusive / requireOneOf) survives — only mapsTo is dropped.
Shipping MapsTo without server-side support would land dead client
code. The MapsTo field + sourceFlag transform guard were therefore
removed from this PR; they will return in a follow-up PR once #282
(server-side schema acknowledgement of mapsTo) is resolved. The
file_read transform stays here because it composes with multiple
mechanisms beyond MapsTo and is independently testable.
## Tests
Six new cases in internal/compat/transform_test.go cover the
contract: literal file, stdin via "-", missing file, non-UTF-8
input, empty path, non-string input. All pass under both `go test`
and `go test -coverprofile`.
Refs #277, blocked-by #282
* feat(schema): add CLIFlagOverride.MapsTo for sibling-flag routing (#277#282)
Adds the `MapsTo` field on `CLIFlagOverride` and wires the dispatch loop
in `compat.buildOverrideBindings` so a flag's final value (post-transform
or literal) is routed into a different MCP parameter slot than its own
property name. This lets two sibling CLI flags feed a single upstream
parameter — the canonical case being `--content` (literal) + `--content-file`
(transform: file_read) both mapping to `markdown`.
Tool-level `CLIToolOverride.MutuallyExclusive` (cobra MarkFlagsMutuallyExclusive)
is the right partner for guarding "set one, not both" at parse time; no
new exclusion machinery is added.
Closes the client-side gap previously misattributed to a server-side
mapsTo strip in #282. Once a doc envelope with mapsTo lands in pre-prod,
end-to-end `--content-file` becomes shippable, finishing #277 Step 1b.
Test coverage (internal/compat/dynamic_commands_test.go):
- MapsTo without transform: literal --content → params[markdown]
- MapsTo with file_read transform: --content-file path → params[markdown]
- Sibling flags both mapsTo same target, only one set: clean routing
- Sibling flags both set: rejected by tool-level MutuallyExclusive (regression)
Backward-compat: empty MapsTo preserves existing params[propertyName] write
semantics for every existing envelope.
* chore: update coverage badge [skip ci]
* chore: update coverage badge [skip ci]
* chore: update coverage badge [skip ci]
* chore: update coverage badge [skip ci]
* chore: update coverage badge [skip ci]
* 1.0.19 changelog
* stick opt
* fix(cli): utf-8 safe sticky suffix guard + changelog (#272)
SuffixLooksLikeValue used to read suffix[0] (a single byte) for both the
uuid format branch and the fallback "is the first rune a letter?" check.
For multi-byte UTF-8 leading runes — common in dws because value text is
often Chinese — this picked up only the first byte (0xE0..0xF4 lead),
which is not a letter and not a hex digit, so the function silently
returned true and let glued tokens like --name<CJK> get split into
--name <CJK>... This switches both branches to utf8.DecodeRuneInString
and adds a utf8.RuneError guard so invalid UTF-8 input is rejected too.
Also locks down the new behaviour in CHANGELOG ## [Unreleased]:
- Changed: schema-aware sticky flag splitting
- Added: available_flags field on unknown-flag errors
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Two generic CLI envelope schema enhancements that close gaps surfaced by
the cli_to_mcp test suite without resorting to product-specific helpers:
1. CLIToolOverride.CLIAliases (registry.go + dynamic_commands.go)
- Lets a single MCP tool register additional cobra command aliases via
envelope JSON (e.g. `range read` accepts `range get`, `member list`
accepts `member ls`). Plumbs through the existing Route.Aliases ->
cobra.Command.Aliases path; conflicts with siblings are silently
skipped by cobra.
2. json_parse_strict transform (transform.go)
- Strict JSON variant of json_parse that does NOT fall back to YAML.
Use when the upstream tool requires a structured array/object value
and silently coercing malformed input to a scalar string would mask
a real user error (observed: filter-view --criteria 'NOT_VALID_JSON'
was being accepted and quietly creating an empty-criteria view).
A generic envelope schema feature that lets a single CLI command orchestrate
an ordered sequence of MCP tool calls plus optional HTTP-download sinks,
declared entirely in the envelope JSON. The motivating use case is the
"submit-job + poll-status + download-result" pattern (e.g. sheet export),
which previously required hardcoded helper commands per product.
## Schema additions
- `CLIToolOverride.Pipeline []PipelineStep` — when non-empty, dispatch
ignores the parent map key (no single "primary tool") and walks the
steps in order. CLI surface (CLIName / Group / Flags) still applies.
- `PipelineStep` struct — supports two step types:
- `type:"call"` (default) invokes Tool with templated Args. Optional
PollUntilField/Value turn it into a polling loop with configurable
PollIntervalSec / PollTimeoutSec.
- `type:"download"` resolves DownloadURLField, fetches via HTTP GET,
and writes the body to the path supplied by OutputFlag's value
(with directory-path filename inference).
- `CLIFlagOverride.PipelineLocal bool` — marks a flag as CLI-side only;
CollectBindings skips it so the value never reaches MCP params, but
the pipeline executor reads it via extractFlagValuesByAlias.
## Template language
Two prefixes supported in PipelineStep.Args / DownloadURLField:
$flag.<name> — value of the user's CLI flag whose alias is <name>
$step.<idx>.<dotPath> — field from a prior step's response (idx 0-based)
literal — passed through
dotPath walks nested map[string]any so "$step.1.content.downloadUrl"
resolves through wrapped MCP envelopes correctly.
## Stdout contract
The download step always emits machine-parseable plain-text lines
("jobId: <id>\\n", "downloadUrl: <url>\\n") in addition to the standard
JSON output, so shell pipelines and regex-based test suites can extract
key values without parsing JSON. Structured callers continue to consume
the output.WriteCommandPayload JSON envelope.
## Why this is schema enhancement, not product hardcode
- The executor is product-agnostic: any envelope can declare a Pipeline
and benefit (sheet export today; doc/drive/aitable async patterns
tomorrow).
- The template language is the only product-coupling, and it lives in
the envelope JSON — not in Go code.
- No sheet/wiki/aitable-specific code in dingtalk-workspace-cli.
Files: 1 new + 3 modified (~250 LOC of executor + ~30 LOC of schema
plumbing). Existing tests pass; new pipeline tests TBA in a follow-up
once the schema fields are in upstream.
* fix(compat): accept YAML flow as a fallback for json_parse transform
Users who run `dws aitable table create --fields [{fieldName: 标题, ...}]`
hit a shell glob error (`zsh: bad pattern: [{fieldName:`) because the
unquoted brackets are interpreted as a zsh pattern. Even when the shell
is placated with outer quotes, the strict-JSON parser still rejected the
natural form that drops quotes around keys and string values.
transformJSONParse now tries strict JSON first (unchanged fast path) and,
on failure, falls back to yaml.Unmarshal. YAML's flow syntax is a
superset of JSON that accepts `{key: value}` without surrounding quotes,
so a single set of outer quotes is all the user needs:
dws aitable table create --fields '[{fieldName: 标题, type: text}]'
Already-valid strict JSON is unaffected. The @file syntax (`--fields
@path.json`) continues to work through the same transform.
The terminal error message, if both parsers fail, now points users at
the two working forms (quoted YAML flow or @file).
* fix(helpers): route chat send-by-bot to bot product
`chat message send-by-bot` previously stamped its invocation with
CanonicalProduct="chat", routing it through the chat product's auth /
server dispatch. The command is semantically a bot operation — switch
it to "bot" so auth client selection and downstream routing match the
actual MCP server. Extends chat_test.go with single-chat and group-chat
routing assertions.
* feat(compat): merge same-name subcommands under shared parent
When multiple server entries attach to the same parent and their cli.command
collides with a subcommand already in the parent tree, the incoming subcommand's
children are merged recursively into the existing one instead of producing a
duplicate sibling. Leaf-name collisions resolve first-wins.
Fixes the duplicate `group` / `message` rows in `dws chat --help` that surfaced
when bot capabilities were distributed across chat.group.members and
chat.message subtrees.
* feat(ir): carry FlagOverlay and ToolAnnotations into canonical catalog
FlagOverlay mirrors the per-parameter CLI overlay (alias, transform,
transformArgs, env default, default value, hidden) sourced from
market.CLIToolOverride.Flags.
ToolAnnotations mirrors MCP 2025+ tool annotations with nullable hints
(destructive / read-only / idempotent / open-world) so absence means
"unknown" rather than "false".
BuildCatalog now propagates both through the canonical layer, with tests
covering group metadata and flag-overlay passthrough.
* feat(output): add -f pretty for schema-aware colored output
dws schema now supports -f pretty alongside json / raw. Pretty mode
partitions output by product / tool / parameter / enum and applies ANSI
color so humans can browse the catalog without piping through jq.
json / raw behaviour is unchanged.
docs/reference.md gains a Schema Introspection section documenting
dws schema and the new pretty format.
* feat(skills): refresh 13-product references; add devdoc / drive / oa
SKILL.md cli_version bumped to >=1.0.15; product index reordered to match
the v1.0.15 command surface.
New references:
- devdoc.md: Open Platform documentation search
- drive.md: DingTalk drive (promoted out of Coming soon)
- oa.md: OA approval end-to-end flow
Major rewrites for aitable (dashboard / chart share, import/export,
attachment flow), chat (bot capabilities merged into chat.message and
chat.group.members), calendar (event suggest / attachments), doc (comment
subtree, file create, upload/download flow), and minutes (list
mine/shared/all unified, record subcommands split).
Minor alignment touch-ups for contact / report / todo flag names.
* docs(release): sync v1.0.15 notes — 159 commands across 13 products
README Key Services fully refreshed: chat 23, calendar 14, aitable 41,
doc 21, minutes 19; drive promoted out of Coming soon; workbench and
standalone bot rows removed; Quick Start expanded to 7 examples spanning
doc / minutes / drive; Coming soon trimmed to mail / conference / aiapp /
live / wiki.
Adds docs/command-index.md, an auto-generated English listing of all 159
runtime commands with description and when-to-use guidance aimed at AI
agents. Replaces the ad-hoc command-index.pre.* / command-index.full.*
snapshots used during development.
CHANGELOG 1.0.15 records the compat subcommand-merging feature, the new
command index, and a flag-naming cleanup across chat / calendar / drive /
minutes / contact / devdoc.
- Support file path reference in plugin.json cli field (e.g. "cli": "overlay.json")
in addition to inline JSON objects, resolving path relative to plugin root
- Add description field to CLIToolOverride for static command descriptions
as fallback when MCP tools/list is unavailable (e.g. upstream server offline)
- Fix plugin install on Windows: use cmd /C instead of sh -c for build commands
- Skip copying identical files during plugin install to avoid overwriting
locked executables (running stdio plugin processes on Windows)
- Add symlink skip and path traversal guard in copyDir for security
- Clean up stale files in destination during plugin upgrade via removeStaleFiles
🤖 Generated with [Qoder][https://qoder.com]
- Fix schema-derived flags not being collected into tool call params
- Add registerHTTPServer for plugin streamable-http tool discovery
- Include dev plugins in ListInstalled output
- Isolate settings path for test environments
- Add stdio and HTTP MCP end-to-end integration tests
- Build tool → endpoint mapping from CLI tools and overrides
- directRuntimeEndpoint resolves by tool name before product fallback
- Support dynamic command routing with compat layer updates