From c2014fa7f8d748b87b3d19c9d2d1e19c970b623d Mon Sep 17 00:00:00 2001 From: xarmian Date: Wed, 13 May 2026 03:54:50 +0000 Subject: [PATCH] fix(cli): schema-aware --field parsing for non-string typed fields (BUG-1125) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pad item create/update --field key=value previously stored every value as a string, so json / number / checkbox / multi_select fields were rejected by the server-side validator. The new parseFieldFlag helper fetches the collection schema once per command and parses each value according to its declared field type: - json / multi_select → json.Unmarshal - number → strconv.ParseFloat - checkbox → strconv.ParseBool - text / url / select / date / relation / unknown → raw string Schema-fetch failure degrades gracefully to pre-fix string-only behavior. pad item list --field is unchanged (URL query param, not validator). Verified against both repros: --field reading_time=3 on blogs (the original number case) and --field 'arguments=[{...}]' on playbooks (the json case that surfaced authoring the ship playbook). String fields show no regression. Skill update folded in: the "Authoring slug-invocable playbooks with arguments" section in skills/pad/SKILL.md previously routed users to the web editor as the only path for structured arguments. With this fix the CLI handles it in one command, so the section now leads with the CLI flow and demotes the web editor to an alternative. --- cmd/pad/main.go | 62 ++++++++++++++++++++++++++++++++++++++++++--- skills/pad/SKILL.md | 42 +++++++++++++++--------------- 2 files changed, 79 insertions(+), 25 deletions(-) diff --git a/cmd/pad/main.go b/cmd/pad/main.go index 27bf3fde..0763d7ea 100644 --- a/cmd/pad/main.go +++ b/cmd/pad/main.go @@ -2799,6 +2799,42 @@ Usage: // v2 Commands: create, list, show, update, delete, search, status, next, collections // ============================================================================= +// parseFieldFlag parses a --field key=value flag value according to the +// field's declared schema type. JSON-typed and multi_select fields receive +// parsed JSON values, number-typed fields receive numbers, and checkbox +// fields receive booleans. Unknown fields (not in the schema) and string- +// typed fields (text, url, select, date, relation) fall back to the raw +// string. On parse failure, falls back to the raw string so the server +// validator returns a useful error rather than the CLI silently dropping +// data. See BUG-1125. +func parseFieldFlag(schema models.CollectionSchema, key, raw string) any { + for i := range schema.Fields { + def := schema.Fields[i] + if def.Key != key { + continue + } + switch def.Type { + case "json", "multi_select": + var v any + if err := json.Unmarshal([]byte(raw), &v); err == nil { + return v + } + case "number": + if f, err := strconv.ParseFloat(raw, 64); err == nil { + return f + } + case "checkbox": + if b, err := strconv.ParseBool(raw); err == nil { + return b + } + } + // All other types (text, url, select, date, relation) — string is correct. + return raw + } + // Unknown field — let the server decide. + return raw +} + // --- create --- func createCmd() *cobra.Command { @@ -2857,10 +2893,18 @@ Run with --help-collections to see available collections and their status values fields["category"] = category } - // Apply arbitrary --field key=value flags + // Apply arbitrary --field key=value flags. Fetch the collection + // schema so JSON / number / checkbox / multi_select fields parse + // to their declared types (BUG-1125). Schema-fetch failure + // degrades gracefully: all values stay as strings, matching + // pre-fix behavior. + var collSchema models.CollectionSchema + if coll, err := client.GetCollection(ws, collSlug); err == nil { + _ = json.Unmarshal([]byte(coll.Schema), &collSchema) + } for _, kv := range fieldFlags { if idx := strings.Index(kv, "="); idx > 0 { - fields[kv[:idx]] = kv[idx+1:] + fields[kv[:idx]] = parseFieldFlag(collSchema, kv[:idx], kv[idx+1:]) } } @@ -3424,10 +3468,20 @@ Examples: existingFields["category"] = category } - // Apply arbitrary --field key=value flags + // Apply arbitrary --field key=value flags. Fetch the + // collection schema (using the item's own collection slug) + // so JSON / number / checkbox / multi_select fields parse + // to their declared types (BUG-1125). Schema-fetch failure + // degrades gracefully: all values stay as strings. + var collSchema models.CollectionSchema + if item.CollectionSlug != "" { + if coll, err := client.GetCollection(ws, item.CollectionSlug); err == nil { + _ = json.Unmarshal([]byte(coll.Schema), &collSchema) + } + } for _, kv := range fieldFlags { if idx := strings.Index(kv, "="); idx > 0 { - existingFields[kv[:idx]] = kv[idx+1:] + existingFields[kv[:idx]] = parseFieldFlag(collSchema, kv[:idx], kv[idx+1:]) } } diff --git a/skills/pad/SKILL.md b/skills/pad/SKILL.md index b7f70469..653180a8 100644 --- a/skills/pad/SKILL.md +++ b/skills/pad/SKILL.md @@ -242,34 +242,34 @@ pad item create playbook "Release checklist" \ EOF ``` -**Authoring slug-invocable playbooks with arguments.** The `arguments` field on the playbooks collection is a `json` type, and `pad item create --field` only sets string values — so you can't set a structured `arguments` array directly from the CLI. Two working paths: +**Authoring slug-invocable playbooks with arguments.** As of BUG-1125's fix, `pad item create --field` is schema-aware — pass the structured `arguments` array directly as a JSON literal and the CLI parses it into the json-typed field. The full playbook (slug + arguments + body with `## Arguments` mirror) lands in one command: -- **Web UI playbook editor (recommended)** at `/{username}/{workspace}/playbooks` (click "+ New Playbook"). The editor lets the user (or the agent talking the user through it) declare each argument's `name / type / required / default / description / enum` in a structured form, validates the kebab-case slug + workspace uniqueness, and round-trips the spec into the body's `## Arguments` section. Open the URL with `pad server open` if the user isn't already there. This is the canonical path — the editor exists specifically for this case. +```bash +pad item create playbook "Cut a release" \ + --field invocation_slug=release \ + --field trigger=manual \ + --field status=active \ + --field 'arguments=[{"name":"version","type":"string","required":true,"description":"semver, e.g. 0.5.0"},{"name":"dry-run","type":"flag","default":false,"description":"Print what would happen, don'\''t push"}]' \ + --stdin <<'EOF' +Cut a Pad release. -- **CLI then web edit.** If the user wants the CLI flow, create the playbook with the body containing a `## Arguments` section and set everything BUT `arguments`: +## Arguments - ```bash - pad item create playbook "Cut a release" \ - --field invocation_slug=release \ - --field trigger=manual \ - --field status=active \ - --stdin <<'EOF' - Cut a Pad release. +- `version` (string, required) — semver, e.g. 0.5.0 +- `dry-run` (flag, default=false) — print what would happen, don't push - ## Arguments +## Steps - - `version` (string, required) — semver, e.g. 0.5.0 +1. Verify the tree is clean and on main +2. Run `make test` +3. Tag with `git tag v$VERSION && git push --tags` +4. Verify CI release workflow succeeded +EOF +``` - ## Steps +The `arguments` JSON and the body's `## Arguments` section are the same contract expressed two ways — the structured field is what the strict CLI/MCP arg parser reads; the markdown is the human-readable mirror. Keep them in sync. For long argument specs, write the JSON to a file and inline it: `--field "arguments=$(cat /tmp/args.json)"`. - 1. Verify the tree is clean and on main - 2. Run `make test` - 3. Tag with `git tag v$VERSION && git push --tags` - 4. Verify CI release workflow succeeded - EOF - ``` - - Then open the new playbook in the web editor's structured arguments builder to declare each argument (the markdown `## Arguments` section is the human-readable mirror; the structured field is what the strict CLI parser reads, so both need to be populated). The editor's two-way binding will keep them in sync from there. +**Web UI playbook editor** at `/{username}/{workspace}/playbooks` (click "+ New Playbook") is the alternative if the user prefers a form-based flow — kebab-case slug input with debounced uniqueness check, structured arguments builder, and two-way binding with the body's `## Arguments` section. Open with `pad server open`. Equally valid; pick whichever surface the user is already in. After creation, point the user at `/pad ` for the new invocation or, for trigger-only playbooks, the action that will auto-load it ("This will fire on the next `on-release` action").