kd-cli 2.2.0__tar.gz → 2.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {kd_cli-2.2.0/kd_cli.egg-info → kd_cli-2.2.2}/PKG-INFO +1 -1
  2. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/youtrack.py +25 -2
  3. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/main.py +12 -0
  4. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/client.py +24 -10
  5. kd_cli-2.2.2/kd/operating_guide.py +20 -0
  6. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/resources/CHANGELOG.md +10 -0
  7. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/resources/operating-guide.md +2 -0
  8. {kd_cli-2.2.0 → kd_cli-2.2.2/kd_cli.egg-info}/PKG-INFO +1 -1
  9. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd_cli.egg-info/SOURCES.txt +1 -0
  10. {kd_cli-2.2.0 → kd_cli-2.2.2}/pyproject.toml +1 -1
  11. {kd_cli-2.2.0 → kd_cli-2.2.2}/LICENSE +0 -0
  12. {kd_cli-2.2.0 → kd_cli-2.2.2}/MANIFEST.in +0 -0
  13. {kd_cli-2.2.0 → kd_cli-2.2.2}/README.md +0 -0
  14. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/__init__.py +0 -0
  15. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/__init__.py +0 -0
  16. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/articles.py +0 -0
  17. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/http.py +0 -0
  18. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/hub.py +0 -0
  19. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/saved_queries.py +0 -0
  20. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/api/tags.py +0 -0
  21. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/article_marker.py +0 -0
  22. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/auth.py +0 -0
  23. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/__init__.py +0 -0
  24. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/core/__init__.py +0 -0
  25. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/core/command_base.py +0 -0
  26. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/core/command_parser.py +0 -0
  27. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/core/namespaces.py +0 -0
  28. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/core/option_registry.py +0 -0
  29. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/input.py +0 -0
  30. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/__init__.py +0 -0
  31. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/alias/__init__.py +0 -0
  32. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/article/__init__.py +0 -0
  33. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/init.py +0 -0
  34. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/issue/__init__.py +0 -0
  35. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/primer/__init__.py +0 -0
  36. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/project/__init__.py +0 -0
  37. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/query/__init__.py +0 -0
  38. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/release_notes.py +0 -0
  39. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/search/__init__.py +0 -0
  40. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/system/__init__.py +0 -0
  41. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/tag/__init__.py +0 -0
  42. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/time/__init__.py +0 -0
  43. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/namespaces/user/__init__.py +0 -0
  44. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/output.py +0 -0
  45. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/cli/tree.py +0 -0
  46. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/config.py +0 -0
  47. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/config_read.py +0 -0
  48. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/config_schema.py +0 -0
  49. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/connection.py +0 -0
  50. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/debug.py +0 -0
  51. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/exceptions.py +0 -0
  52. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/history.py +0 -0
  53. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/__init__.py +0 -0
  54. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/article.py +0 -0
  55. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/assist.py +0 -0
  56. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/attachment.py +0 -0
  57. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/comment.py +0 -0
  58. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/common.py +0 -0
  59. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/issue.py +0 -0
  60. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/project.py +0 -0
  61. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/saved_query.py +0 -0
  62. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/search.py +0 -0
  63. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/tag.py +0 -0
  64. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/tag_entities.py +0 -0
  65. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/user.py +0 -0
  66. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/models/work_item.py +0 -0
  67. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/primer.py +0 -0
  68. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/query_parser.py +0 -0
  69. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/release_notes.py +0 -0
  70. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/resources/__init__.py +0 -0
  71. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/resources/command-synonyms.yaml +0 -0
  72. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/resources/config-template.yaml +0 -0
  73. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd/resources/error-messages.yaml +0 -0
  74. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd_cli.egg-info/dependency_links.txt +0 -0
  75. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd_cli.egg-info/entry_points.txt +0 -0
  76. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd_cli.egg-info/requires.txt +0 -0
  77. {kd_cli-2.2.0 → kd_cli-2.2.2}/kd_cli.egg-info/top_level.txt +0 -0
  78. {kd_cli-2.2.0 → kd_cli-2.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kd-cli
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: Knowledge Dispatcher for issue trackers and knowledge bases
5
5
  Author: Sergiy Gladkyy
6
6
  License-Expression: MIT
@@ -24,6 +24,7 @@ from kd.models import (
24
24
  Attachment,
25
25
  Comment,
26
26
  CommentCreate,
27
+ CustomField,
27
28
  FieldSet,
28
29
  Issue,
29
30
  IssueCreate,
@@ -38,6 +39,28 @@ from kd.models import (
38
39
  WorkItemType,
39
40
  )
40
41
 
42
+
43
+ def _custom_field_payload(field: CustomField) -> dict[str, Any]:
44
+ payload = field.model_dump(by_alias=True, exclude_none=True)
45
+ if field.value is None:
46
+ payload["value"] = None
47
+ return payload
48
+
49
+
50
+ def _issue_create_payload(issue: IssueCreate) -> dict[str, Any]:
51
+ payload = issue.model_dump(by_alias=True, exclude_none=True)
52
+ if issue.custom_fields is not None:
53
+ payload["customFields"] = [_custom_field_payload(field) for field in issue.custom_fields]
54
+ return payload
55
+
56
+
57
+ def _issue_update_payload(update: IssueUpdate) -> dict[str, Any]:
58
+ payload = update.model_dump(by_alias=True, exclude_none=True)
59
+ if update.custom_fields is not None:
60
+ payload["customFields"] = [_custom_field_payload(field) for field in update.custom_fields]
61
+ return payload
62
+
63
+
41
64
  # Default field selections for common queries
42
65
  _PROJECT_LIST_FIELDS = str(
43
66
  FieldSet(
@@ -226,7 +249,7 @@ class YouTrackAPI:
226
249
  def create_issue(self, issue: IssueCreate) -> Issue:
227
250
  data = self._http.post(
228
251
  "/issues",
229
- json=issue.model_dump(by_alias=True, exclude_none=True),
252
+ json=_issue_create_payload(issue),
230
253
  fields=_ISSUE_FIELDS,
231
254
  )
232
255
  return Issue.model_validate(data)
@@ -234,7 +257,7 @@ class YouTrackAPI:
234
257
  def update_issue(self, issue_id: str, update: IssueUpdate) -> Issue:
235
258
  data = self._http.post(
236
259
  f"/issues/{issue_id}",
237
- json=update.model_dump(by_alias=True, exclude_none=True),
260
+ json=_issue_update_payload(update),
238
261
  fields=_ISSUE_FIELDS,
239
262
  )
240
263
  return Issue.model_validate(data)
@@ -30,6 +30,7 @@ from kd.config import ResolvedConfig, resolve_command_defaults, resolve_config
30
30
  from kd.connection import ConnectionContext, resolve_connection
31
31
  from kd.debug import DebugLevel, DebugLog, init_debug_log
32
32
  from kd.exceptions import KdError
33
+ from kd.operating_guide import load_guide
33
34
 
34
35
  # Global options — single source of truth for extraction, help, and reference.
35
36
  GLOBAL_OPTIONS: list[OptionDeclaration] = [
@@ -54,6 +55,13 @@ GLOBAL_OPTIONS: list[OptionDeclaration] = [
54
55
  type=bool,
55
56
  help_text="Print full command reference (LLM-friendly)",
56
57
  ),
58
+ OptionDeclaration(
59
+ name="guide",
60
+ short_name="G",
61
+ layer=OptionLayer.GLOBAL,
62
+ type=bool,
63
+ help_text="Print embedded operating guide (LLM-friendly)",
64
+ ),
57
65
  OptionDeclaration(
58
66
  name="debug",
59
67
  short_name="D",
@@ -158,6 +166,10 @@ def main(argv: list[str] | None = None) -> None:
158
166
  print(f"kd {__version__}")
159
167
  return
160
168
 
169
+ if global_opts.get("guide"):
170
+ print(load_guide(), end="")
171
+ return
172
+
161
173
  # Alias expansion: detect `alias <name>` and expand before parsing.
162
174
  # Config is loaded early only when needed; stored for reuse later.
163
175
  resolved: ResolvedConfig | None = None
@@ -279,11 +279,7 @@ class FieldSchema:
279
279
  )
280
280
 
281
281
  validated_value = self._validate_field_value(field, value)
282
- field_value = (
283
- {"login": validated_value}
284
- if self._is_user_field(field)
285
- else {"name": validated_value}
286
- )
282
+ field_value = self._field_value_payload(field, validated_value)
287
283
  self._debug.debug(
288
284
  "fields",
289
285
  f"resolved '{slot}' -> field '{field.name}' ($type={issue_type})",
@@ -350,9 +346,7 @@ class FieldSchema:
350
346
  f"Unsupported field type '{field.project_field_type}' for field '{field.name}'"
351
347
  )
352
348
  validated_value = self._validate_field_value(field, value)
353
- field_value = (
354
- {"login": validated_value} if self._is_user_field(field) else {"name": validated_value}
355
- )
349
+ field_value = self._field_value_payload(field, validated_value)
356
350
  self._debug.debug(
357
351
  "fields",
358
352
  f"resolved generic '{field_name}' -> field '{field.name}' "
@@ -392,16 +386,36 @@ class FieldSchema:
392
386
  """Return True if this field is a user-type field (user[1])."""
393
387
  return field.field_type_id.startswith("user")
394
388
 
389
+ def _field_value_payload(
390
+ self,
391
+ field: ProjectCustomField,
392
+ value: str | None,
393
+ ) -> dict[str, str] | None:
394
+ """Build the YouTrack issue-field value payload.
395
+
396
+ ``None`` is a deliberate clear operation and must serialize as
397
+ ``"value": null`` in the final issue mutation payload.
398
+ """
399
+ if value is None:
400
+ return None
401
+ return {"login": value} if self._is_user_field(field) else {"name": value}
402
+
395
403
  def _validate_field_value(
396
404
  self,
397
405
  field: ProjectCustomField,
398
406
  value: str,
399
- ) -> str:
407
+ ) -> str | None:
400
408
  """Validate a value against the field's bundle.
401
409
 
402
- Returns the canonical value name (case-corrected).
410
+ Returns the canonical value name (case-corrected), or ``None``
411
+ when an optional field should be cleared.
403
412
  Raises ValidationError with allowed values on mismatch.
404
413
  """
414
+ if value == "":
415
+ if field.can_be_empty:
416
+ return None
417
+ raise ValidationError(f"Field '{field.name}' is required and cannot be cleared.")
418
+
405
419
  if self._is_user_field(field):
406
420
  return value # User fields: server validates against group members
407
421
  if not field.bundle_values:
@@ -0,0 +1,20 @@
1
+ """Operating guide loader.
2
+
3
+ Reads the bundled ``kd/resources/operating-guide.md`` for ``kd --guide``.
4
+ The guide carries durable operating doctrine (product boundary, primer
5
+ workflow, cost classes, YouTrack gotchas) — a complement to the
6
+ syntax-focused ``kd --reference``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.resources
12
+
13
+
14
+ def load_guide() -> str:
15
+ """Read the bundled operating guide as text."""
16
+ return (
17
+ importlib.resources.files("kd.resources")
18
+ .joinpath("operating-guide.md")
19
+ .read_text(encoding="utf-8")
20
+ )
@@ -4,6 +4,16 @@ All notable changes to kd-cli are recorded here. The project follows [Semantic V
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.2.2] — 2026-04-18
8
+
9
+ ### Fixed
10
+ - `kd /issue update ID --field "NAME="` now clears optional custom fields by sending `value: null` to YouTrack. Required fields reject empty values with a clear client-side error instead of the generic "invalid value" bundle message (KDCLI-81).
11
+
12
+ ## [2.2.1] — 2026-04-18
13
+
14
+ ### Added
15
+ - `kd --guide` / `-G` prints an embedded operating guide covering kd's product boundary, primer workflow, cost classes, and YouTrack gotchas — the doctrine complement to the syntax-focused `kd --reference`. Runs without a connection or working config, so it's safe in fresh or broken-config environments (KDCLI-94).
16
+
7
17
  ## [2.2.0] — 2026-04-18
8
18
 
9
19
  Milestone 006 — tag discovery and taxonomy hygiene v1. Eight sequenced sessions that turn tags into a load-bearing cross-entity discovery primitive: `/issue` tag parity with `/article`, multi-tag intersection on `/tag issues`, the `/tag entities` cross-kind verb, `/tag list --prefix|--namespace` taxonomy introspection, and workspace-pinned tags on the primer consumed as an implicit OR scope.
@@ -97,6 +97,8 @@ Issue custom fields are project-specific and need `$type` discriminators on muta
97
97
  kd /project/field list KDCLI
98
98
  ```
99
99
 
100
+ Clear optional custom fields with an empty value, for example `kd /issue update KDCLI-42 --field "Milestone="`. Required fields reject empty values before any upstream write.
101
+
100
102
  Attachment metadata download URLs may start with `/api/`. kd handles this internally on download commands; do not assume the raw URL can be appended unchanged to an API base URL in custom scripts.
101
103
 
102
104
  Tag existence, tag visibility, and tag sharing rights are different concerns. A visible tag may not be usable for every operation, and sharing settings are provider-specific security posture, not just display metadata.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kd-cli
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: Knowledge Dispatcher for issue trackers and knowledge bases
5
5
  Author: Sergiy Gladkyy
6
6
  License-Expression: MIT
@@ -13,6 +13,7 @@ kd/connection.py
13
13
  kd/debug.py
14
14
  kd/exceptions.py
15
15
  kd/history.py
16
+ kd/operating_guide.py
16
17
  kd/primer.py
17
18
  kd/query_parser.py
18
19
  kd/release_notes.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kd-cli"
7
- version = "2.2.0"
7
+ version = "2.2.2"
8
8
  description = "Knowledge Dispatcher for issue trackers and knowledge bases"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes