kd-cli 2.2.1__tar.gz → 2.3.0__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.1/kd_cli.egg-info → kd_cli-2.3.0}/PKG-INFO +1 -1
  2. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/youtrack.py +25 -2
  3. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/article/__init__.py +12 -1
  4. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/client.py +112 -11
  5. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/resources/CHANGELOG.md +13 -0
  6. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/resources/operating-guide.md +5 -0
  7. {kd_cli-2.2.1 → kd_cli-2.3.0/kd_cli.egg-info}/PKG-INFO +1 -1
  8. {kd_cli-2.2.1 → kd_cli-2.3.0}/pyproject.toml +1 -1
  9. {kd_cli-2.2.1 → kd_cli-2.3.0}/LICENSE +0 -0
  10. {kd_cli-2.2.1 → kd_cli-2.3.0}/MANIFEST.in +0 -0
  11. {kd_cli-2.2.1 → kd_cli-2.3.0}/README.md +0 -0
  12. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/__init__.py +0 -0
  13. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/__init__.py +0 -0
  14. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/articles.py +0 -0
  15. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/http.py +0 -0
  16. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/hub.py +0 -0
  17. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/saved_queries.py +0 -0
  18. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/api/tags.py +0 -0
  19. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/article_marker.py +0 -0
  20. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/auth.py +0 -0
  21. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/__init__.py +0 -0
  22. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/core/__init__.py +0 -0
  23. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/core/command_base.py +0 -0
  24. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/core/command_parser.py +0 -0
  25. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/core/namespaces.py +0 -0
  26. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/core/option_registry.py +0 -0
  27. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/input.py +0 -0
  28. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/main.py +0 -0
  29. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/__init__.py +0 -0
  30. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/alias/__init__.py +0 -0
  31. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/init.py +0 -0
  32. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/issue/__init__.py +0 -0
  33. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/primer/__init__.py +0 -0
  34. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/project/__init__.py +0 -0
  35. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/query/__init__.py +0 -0
  36. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/release_notes.py +0 -0
  37. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/search/__init__.py +0 -0
  38. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/system/__init__.py +0 -0
  39. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/tag/__init__.py +0 -0
  40. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/time/__init__.py +0 -0
  41. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/namespaces/user/__init__.py +0 -0
  42. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/output.py +0 -0
  43. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/cli/tree.py +0 -0
  44. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/config.py +0 -0
  45. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/config_read.py +0 -0
  46. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/config_schema.py +0 -0
  47. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/connection.py +0 -0
  48. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/debug.py +0 -0
  49. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/exceptions.py +0 -0
  50. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/history.py +0 -0
  51. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/__init__.py +0 -0
  52. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/article.py +0 -0
  53. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/assist.py +0 -0
  54. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/attachment.py +0 -0
  55. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/comment.py +0 -0
  56. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/common.py +0 -0
  57. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/issue.py +0 -0
  58. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/project.py +0 -0
  59. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/saved_query.py +0 -0
  60. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/search.py +0 -0
  61. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/tag.py +0 -0
  62. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/tag_entities.py +0 -0
  63. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/user.py +0 -0
  64. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/models/work_item.py +0 -0
  65. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/operating_guide.py +0 -0
  66. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/primer.py +0 -0
  67. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/query_parser.py +0 -0
  68. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/release_notes.py +0 -0
  69. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/resources/__init__.py +0 -0
  70. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/resources/command-synonyms.yaml +0 -0
  71. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/resources/config-template.yaml +0 -0
  72. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd/resources/error-messages.yaml +0 -0
  73. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd_cli.egg-info/SOURCES.txt +0 -0
  74. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd_cli.egg-info/dependency_links.txt +0 -0
  75. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd_cli.egg-info/entry_points.txt +0 -0
  76. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd_cli.egg-info/requires.txt +0 -0
  77. {kd_cli-2.2.1 → kd_cli-2.3.0}/kd_cli.egg-info/top_level.txt +0 -0
  78. {kd_cli-2.2.1 → kd_cli-2.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kd-cli
3
- Version: 2.2.1
3
+ Version: 2.3.0
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)
@@ -181,7 +181,7 @@ class ArticlePushCommand(Command):
181
181
  requires_client=True,
182
182
  usage=(
183
183
  "kd /article push FILE --project P --title T "
184
- "[--slug S] [--parent ID] [--tag T] [--force] [--dry-run]"
184
+ "[--slug S] [--parent ID] [--tag T] [--create-tags] [--force] [--dry-run]"
185
185
  ),
186
186
  long_description=(
187
187
  "Push a local markdown file to YouTrack as a knowledge base "
@@ -201,6 +201,7 @@ class ArticlePushCommand(Command):
201
201
  'kd /article push guide.md --project FB --title "User Guide" --parent FB-A-1',
202
202
  'kd /article push guide.md --project FB --title "Guide" --tag "type:guide"',
203
203
  'kd /article push guide.md --project FB --title "Guide" --tag "type:guide" --tag "platform:rdk-e"',
204
+ 'kd /article push guide.md --project FB --title "Guide" --tag "type:guide" --create-tags',
204
205
  'kd /article push guide.md --project FB --title "Guide" --dry-run',
205
206
  'kd /article push guide.md --project FB --title "Guide" --force',
206
207
  ],
@@ -264,6 +265,15 @@ class ArticlePushCommand(Command):
264
265
  help_text="Resolve and validate, but do not write anywhere",
265
266
  )
266
267
  )
268
+ self.register_option(
269
+ OptionDeclaration(
270
+ name="create-tags",
271
+ layer=OptionLayer.COMMAND,
272
+ type=bool,
273
+ default=False,
274
+ help_text="Create missing tags before pushing",
275
+ )
276
+ )
267
277
 
268
278
  def execute(
269
279
  self, args: list[str], options: ResolvedOptions, client: Any = None
@@ -289,6 +299,7 @@ class ArticlePushCommand(Command):
289
299
  force=bool(options.command.get("force")),
290
300
  dry_run=bool(options.command.get("dry-run")),
291
301
  tags=tags or None,
302
+ create_tags=bool(options.command.get("create-tags")),
292
303
  )
293
304
  return {
294
305
  "ok": True,
@@ -20,6 +20,7 @@ from __future__ import annotations
20
20
 
21
21
  import builtins
22
22
  import difflib
23
+ import shlex
23
24
  from collections import OrderedDict
24
25
  from dataclasses import dataclass, field as dataclass_field
25
26
  from datetime import datetime, timezone
@@ -43,7 +44,7 @@ from kd.article_marker import (
43
44
  from kd.config import ResolvedConfig
44
45
  from kd.connection import ConnectionContext
45
46
  from kd.debug import DebugLog
46
- from kd.exceptions import APIError, ConfigError, NotFoundError, ValidationError, KdError
47
+ from kd.exceptions import APIError, ConfigError, KdError, NotFoundError, ValidationError
47
48
  from kd.query_parser import youtrack_query_literal
48
49
  from kd.models import (
49
50
  Article,
@@ -279,11 +280,7 @@ class FieldSchema:
279
280
  )
280
281
 
281
282
  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
- )
283
+ field_value = self._field_value_payload(field, validated_value)
287
284
  self._debug.debug(
288
285
  "fields",
289
286
  f"resolved '{slot}' -> field '{field.name}' ($type={issue_type})",
@@ -350,9 +347,7 @@ class FieldSchema:
350
347
  f"Unsupported field type '{field.project_field_type}' for field '{field.name}'"
351
348
  )
352
349
  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
- )
350
+ field_value = self._field_value_payload(field, validated_value)
356
351
  self._debug.debug(
357
352
  "fields",
358
353
  f"resolved generic '{field_name}' -> field '{field.name}' "
@@ -392,16 +387,36 @@ class FieldSchema:
392
387
  """Return True if this field is a user-type field (user[1])."""
393
388
  return field.field_type_id.startswith("user")
394
389
 
390
+ def _field_value_payload(
391
+ self,
392
+ field: ProjectCustomField,
393
+ value: str | None,
394
+ ) -> dict[str, str] | None:
395
+ """Build the YouTrack issue-field value payload.
396
+
397
+ ``None`` is a deliberate clear operation and must serialize as
398
+ ``"value": null`` in the final issue mutation payload.
399
+ """
400
+ if value is None:
401
+ return None
402
+ return {"login": value} if self._is_user_field(field) else {"name": value}
403
+
395
404
  def _validate_field_value(
396
405
  self,
397
406
  field: ProjectCustomField,
398
407
  value: str,
399
- ) -> str:
408
+ ) -> str | None:
400
409
  """Validate a value against the field's bundle.
401
410
 
402
- Returns the canonical value name (case-corrected).
411
+ Returns the canonical value name (case-corrected), or ``None``
412
+ when an optional field should be cleared.
403
413
  Raises ValidationError with allowed values on mismatch.
404
414
  """
415
+ if value == "":
416
+ if field.can_be_empty:
417
+ return None
418
+ raise ValidationError(f"Field '{field.name}' is required and cannot be cleared.")
419
+
405
420
  if self._is_user_field(field):
406
421
  return value # User fields: server validates against group members
407
422
  if not field.bundle_values:
@@ -999,6 +1014,44 @@ class TimeOperations:
999
1014
  raise ValidationError(f"Unknown work item type '{name}'. Available: {available}")
1000
1015
 
1001
1016
 
1017
+ def _tag_prefix_for_list(tag: str) -> str:
1018
+ """Return the smallest useful prefix for missing-tag discovery."""
1019
+ if ":" in tag:
1020
+ return tag.split(":", 1)[0] + ":"
1021
+ return tag
1022
+
1023
+
1024
+ def _extract_tag_suggestions(error: NotFoundError) -> builtins.list[str]:
1025
+ """Extract close-match names from the existing tag resolver message."""
1026
+ message = str(error)
1027
+ marker = "Did you mean: "
1028
+ if marker not in message:
1029
+ return []
1030
+ raw = message.split(marker, 1)[1].rstrip(".?")
1031
+ return [part.strip() for part in raw.split(",") if part.strip()]
1032
+
1033
+
1034
+ def _missing_tag_resolution(tag: str, error: NotFoundError) -> str:
1035
+ """Format an actionable missing-tag instruction for article push."""
1036
+ lines = [f"Tag '{tag}' not found."]
1037
+ suggestions = _extract_tag_suggestions(error)
1038
+ if suggestions:
1039
+ lines.append(f" Near matches: {', '.join(suggestions)}")
1040
+ prefix = _tag_prefix_for_list(tag)
1041
+ lines.extend(
1042
+ [
1043
+ "",
1044
+ f"To use an existing tag: kd /tag list --prefix {shlex.quote(prefix)}",
1045
+ f"To create a new tag: kd /tag create {shlex.quote(tag)}",
1046
+ f"To retry push without this tag: omit --tag {shlex.quote(tag)}",
1047
+ "",
1048
+ "If you are still curating the tag set, push without --tag and apply tags",
1049
+ f"separately with kd /tag add ARTICLE-ID {shlex.quote(tag)}.",
1050
+ ]
1051
+ )
1052
+ return "\n".join(lines)
1053
+
1054
+
1002
1055
  class ArticleOperations:
1003
1056
  """High-level knowledge base article operations."""
1004
1057
 
@@ -1123,6 +1176,41 @@ class ArticleOperations:
1123
1176
  failed.append(f"{tag} ({exc})")
1124
1177
  return applied, failed
1125
1178
 
1179
+ def _preflight_tags(
1180
+ self,
1181
+ tags: builtins.list[str],
1182
+ *,
1183
+ create_missing: bool = False,
1184
+ dry_run: bool = False,
1185
+ ) -> builtins.list[str]:
1186
+ """Resolve tags before mutating an article.
1187
+
1188
+ Returns actionable failure blocks. When ``create_missing`` is true,
1189
+ missing tags are created before the article write; ``dry_run`` never
1190
+ writes tags and treats missing tags as would-create.
1191
+ """
1192
+ if self._tag_ops is None:
1193
+ raise KdError("Tag operations not available")
1194
+ failed: builtins.list[str] = []
1195
+ for tag in tags:
1196
+ try:
1197
+ self._tag_ops.get(tag)
1198
+ except NotFoundError as exc:
1199
+ if create_missing:
1200
+ if dry_run:
1201
+ continue
1202
+ try:
1203
+ self._tag_ops.create(tag)
1204
+ except KdError as create_exc:
1205
+ failed.append(
1206
+ f"Tag '{tag}' not found and could not be created. {create_exc}"
1207
+ )
1208
+ continue
1209
+ failed.append(_missing_tag_resolution(tag, exc))
1210
+ except KdError as exc:
1211
+ failed.append(f"Tag '{tag}' could not be resolved. {exc}")
1212
+ return failed
1213
+
1126
1214
  def update(
1127
1215
  self,
1128
1216
  article_id: str,
@@ -1146,6 +1234,7 @@ class ArticleOperations:
1146
1234
  force: bool = False,
1147
1235
  dry_run: bool = False,
1148
1236
  tags: builtins.list[str] | None = None,
1237
+ create_tags: bool = False,
1149
1238
  ) -> PushResult:
1150
1239
  """Idempotently upsert a local markdown file as a YouTrack article.
1151
1240
 
@@ -1264,6 +1353,18 @@ class ArticleOperations:
1264
1353
  f"move {existing_remote.id_readable} --parent {parent}."
1265
1354
  )
1266
1355
 
1356
+ if tags:
1357
+ preflight_failed = self._preflight_tags(
1358
+ tags,
1359
+ create_missing=create_tags,
1360
+ dry_run=dry_run,
1361
+ )
1362
+ if preflight_failed:
1363
+ raise KdError(
1364
+ f"Article {'update' if is_update else 'create'} aborted because "
1365
+ "tagging failed.\n\n" + "\n\n".join(preflight_failed)
1366
+ )
1367
+
1267
1368
  # Dry-run bails out here with all validation done but no writes.
1268
1369
  if dry_run:
1269
1370
  existing_sec = 0
@@ -4,6 +4,19 @@ All notable changes to kd-cli are recorded here. The project follows [Semantic V
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.3.0] — 2026-04-18
8
+
9
+ ### Added
10
+ - `/article push --create-tags` explicitly creates missing tags before pushing and attaching them, for scripted flows that own their tag namespace (KDCLI-85).
11
+
12
+ ### Fixed
13
+ - `/article push --tag` missing-tag failures now print actionable recovery commands instead of a bare not-found / close-match message (KDCLI-85).
14
+
15
+ ## [2.2.2] — 2026-04-18
16
+
17
+ ### Fixed
18
+ - `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).
19
+
7
20
  ## [2.2.1] — 2026-04-18
8
21
 
9
22
  ### Added
@@ -80,6 +80,7 @@ kd /issue update KDCLI-42 --state Fixed -o yaml
80
80
  ```
81
81
 
82
82
  For article push, use `--dry-run` before a risky write and inspect conflicts with `/article pull`.
83
+ `/article push --tag` rejects missing tags by default and prints the concrete recovery commands. Use `--create-tags` only for scripted flows that already own the tag namespace.
83
84
 
84
85
  ## YouTrack Gotchas
85
86
 
@@ -97,6 +98,8 @@ Issue custom fields are project-specific and need `$type` discriminators on muta
97
98
  kd /project/field list KDCLI
98
99
  ```
99
100
 
101
+ 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.
102
+
100
103
  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
104
 
102
105
  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.
@@ -141,6 +144,8 @@ Use `/article show` for metadata plus rendered content. Use `/article pull` when
141
144
 
142
145
  Do not use push for one-off web edits or broad tree migration. It is single-file by design. If the remote changed, pull, diff, merge by hand, then push again. Use `--force` only after inspecting the remote state.
143
146
 
147
+ When pushing with `--tag`, kd validates all tags before the article write. Missing tags abort with commands to list the namespace, create the tag, retry without it, or apply the tag separately after push. `--create-tags` is the explicit opt-in for automation that should create missing tags first.
148
+
144
149
  ## Tags And Discovery
145
150
 
146
151
  Tags are the main YouTrack-native cross-entity discovery primitive. A namespaced taxonomy such as `kind:*`, `component:*`, and `outcome:*` gives both humans and LLMs a stable way to move across issues and articles.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kd-cli
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: Knowledge Dispatcher for issue trackers and knowledge bases
5
5
  Author: Sergiy Gladkyy
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kd-cli"
7
- version = "2.2.1"
7
+ version = "2.3.0"
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
File without changes
File without changes