kd-cli 2.6.6__tar.gz → 2.7.1__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.
- {kd_cli-2.6.6/kd_cli.egg-info → kd_cli-2.7.1}/PKG-INFO +1 -1
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/api/articles.py +16 -2
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/article/__init__.py +60 -6
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/client.py +79 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/resources/CHANGELOG.md +31 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/resources/operating-guide.md +5 -5
- {kd_cli-2.6.6 → kd_cli-2.7.1/kd_cli.egg-info}/PKG-INFO +1 -1
- {kd_cli-2.6.6 → kd_cli-2.7.1}/pyproject.toml +1 -1
- {kd_cli-2.6.6 → kd_cli-2.7.1}/LICENSE +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/MANIFEST.in +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/README.md +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/api/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/api/http.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/api/hub.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/api/saved_queries.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/api/tags.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/api/youtrack.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/article_marker.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/auth.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cache.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/core/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/core/command_base.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/core/command_parser.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/core/confirm.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/core/namespaces.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/core/option_registry.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/input.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/main.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/alias/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/cache/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/init.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/issue/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/primer/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/project/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/query/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/release_notes.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/search/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/system/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/tag/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/time/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/namespaces/user/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/output.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/cli/tree.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/config.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/config_read.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/config_schema.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/connection.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/debug.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/exceptions.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/history.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/locking.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/article.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/assist.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/attachment.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/comment.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/common.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/issue.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/project.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/saved_query.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/search.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/tag.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/tag_entities.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/user.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/models/work_item.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/operating_guide.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/primer.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/query_parser.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/release_notes.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/resources/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/resources/command-synonyms.yaml +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/resources/config-template.yaml +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/resources/error-messages.yaml +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/resources/primers/__init__.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/resources/primers/hitl.yaml +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/resources/sdlc-doctrine.md +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd/sdlc.py +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd_cli.egg-info/SOURCES.txt +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd_cli.egg-info/dependency_links.txt +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd_cli.egg-info/entry_points.txt +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd_cli.egg-info/requires.txt +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/kd_cli.egg-info/top_level.txt +0 -0
- {kd_cli-2.6.6 → kd_cli-2.7.1}/setup.cfg +0 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
"""Articles REST API wrapper.
|
|
2
2
|
|
|
3
3
|
Covers ``/api/articles`` endpoints:
|
|
4
|
-
- Articles: list (global + project-scoped), get, create, update, delete
|
|
4
|
+
- Articles: list (global + project-scoped), search, get, create, update, delete
|
|
5
5
|
- Child articles: list, attach, detach parent
|
|
6
6
|
- Comments: list, create
|
|
7
7
|
- Attachments: list, get, create, delete
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The ``query`` parameter on ``/api/articles`` is undocumented in the
|
|
10
|
+
public REST reference but probed-and-supported on the YT tiers kd
|
|
11
|
+
targets (current cloud + on-prem 2023.2); ``search_articles`` is the
|
|
12
|
+
canonical full-text path. Servers that reject it surface as a 400 with
|
|
13
|
+
"unknown parameter" wording, classified at the SDK boundary.
|
|
10
14
|
"""
|
|
11
15
|
|
|
12
16
|
from __future__ import annotations
|
|
@@ -103,6 +107,16 @@ class ArticlesAPI:
|
|
|
103
107
|
)
|
|
104
108
|
return [Article.model_validate(a) for a in data]
|
|
105
109
|
|
|
110
|
+
# --- Search ---
|
|
111
|
+
|
|
112
|
+
def search_articles(self, query: str, top: int = 50, skip: int = 0) -> list[Article]:
|
|
113
|
+
data = self._http.get(
|
|
114
|
+
"/articles",
|
|
115
|
+
fields=_ARTICLE_LIST_FIELDS,
|
|
116
|
+
params={"query": query, "$top": top, "$skip": skip},
|
|
117
|
+
)
|
|
118
|
+
return [Article.model_validate(a) for a in data]
|
|
119
|
+
|
|
106
120
|
# --- Tag scan (articles with tags for client-side filtering) ---
|
|
107
121
|
|
|
108
122
|
def list_articles_with_tags(self, top: int = 50, skip: int = 0) -> list[Article]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Article namespace: /article
|
|
2
2
|
|
|
3
|
-
Commands: list, show, children, create, update, delete, tree, move
|
|
3
|
+
Commands: list, search, show, pull, push, children, create, update, delete, tree, move
|
|
4
4
|
Sub-namespaces: /article/comment (list, add), /article/attachment (list, add, show, download, delete)
|
|
5
5
|
API layer: YouTrack REST API (/api/articles)
|
|
6
6
|
"""
|
|
@@ -81,17 +81,19 @@ class ArticleListCommand(Command):
|
|
|
81
81
|
requires_client=True,
|
|
82
82
|
usage="kd /article list [--project P] [--parent ID] [--limit N]",
|
|
83
83
|
long_description=(
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
84
|
+
"List articles structurally — by project (--project) or by "
|
|
85
|
+
"parent (--parent ARTICLE_ID). For full-text / body / "
|
|
86
|
+
"field-bearing queries use `/article search`; this command "
|
|
87
|
+
"does not interpret query syntax. When --parent is given, "
|
|
88
|
+
"--project is ignored because child listing is already "
|
|
89
|
+
"scoped to the parent's project."
|
|
88
90
|
),
|
|
89
91
|
examples=[
|
|
90
92
|
"kd /article list --project FB",
|
|
91
93
|
"kd /article list --project FB --limit 10",
|
|
92
94
|
"kd /article list --parent FB-A-1",
|
|
93
95
|
],
|
|
94
|
-
see_also=["/article show", "/article children"],
|
|
96
|
+
see_also=["/article show", "/article children", "/article search"],
|
|
95
97
|
)
|
|
96
98
|
|
|
97
99
|
def _register_options(self) -> None:
|
|
@@ -118,6 +120,57 @@ class ArticleListCommand(Command):
|
|
|
118
120
|
return [_article_to_row(a) for a in articles]
|
|
119
121
|
|
|
120
122
|
|
|
123
|
+
class ArticleSearchCommand(Command):
|
|
124
|
+
"""Server-side article full-text search via the YT article query DSL."""
|
|
125
|
+
|
|
126
|
+
def __init__(self) -> None:
|
|
127
|
+
super().__init__(
|
|
128
|
+
"search",
|
|
129
|
+
"Search articles by full-text query",
|
|
130
|
+
requires_client=True,
|
|
131
|
+
usage="kd /article search QUERY [--limit N] [--skip N]",
|
|
132
|
+
long_description=(
|
|
133
|
+
"Run a YouTrack article query against /api/articles?query=. "
|
|
134
|
+
"Matches body, summary, tags, and other YT-indexed fields; "
|
|
135
|
+
"results are server-side relevance-ranked. The upstream "
|
|
136
|
+
"query parameter is undocumented; servers that reject it "
|
|
137
|
+
"surface a specific capability error rather than silently "
|
|
138
|
+
"falling back to local substring search. See the article "
|
|
139
|
+
"search how-to for query syntax and field filters."
|
|
140
|
+
),
|
|
141
|
+
examples=[
|
|
142
|
+
'kd /article search "project: {Code OnTime} {wix}"',
|
|
143
|
+
'kd /article search "project: {Intellect Solutions} content: HKEY_LOCAL_MACHINE"',
|
|
144
|
+
'kd /article search "project: {COT} tag: {kind:proposal}"',
|
|
145
|
+
'kd /article search "summary: timeout" --limit 10',
|
|
146
|
+
],
|
|
147
|
+
see_also=["/article list", "/search", "/tag articles"],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def _register_options(self) -> None:
|
|
151
|
+
self.register_option(limit_option())
|
|
152
|
+
self.register_option(
|
|
153
|
+
OptionDeclaration(
|
|
154
|
+
name="skip",
|
|
155
|
+
layer=OptionLayer.COMMAND,
|
|
156
|
+
type=int,
|
|
157
|
+
default=0,
|
|
158
|
+
help_text="Skip the first N results (pagination).",
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def execute(
|
|
163
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
164
|
+
) -> CommandResult:
|
|
165
|
+
query = " ".join(args).strip()
|
|
166
|
+
if not query:
|
|
167
|
+
raise ValidationError("Usage: kd /article search QUERY")
|
|
168
|
+
limit = options.command.get("limit", 50)
|
|
169
|
+
skip = options.command.get("skip", 0)
|
|
170
|
+
articles = client.articles.search(query, top=limit, skip=skip)
|
|
171
|
+
return [_article_to_row(a) for a in articles]
|
|
172
|
+
|
|
173
|
+
|
|
121
174
|
class ArticleShowCommand(Command):
|
|
122
175
|
"""Show article details."""
|
|
123
176
|
|
|
@@ -1058,6 +1111,7 @@ def create_article_namespace() -> Namespace:
|
|
|
1058
1111
|
"""Build the /article namespace."""
|
|
1059
1112
|
ns = Namespace("article", "Knowledge base articles", path="article")
|
|
1060
1113
|
ns.register_command("list", ArticleListCommand())
|
|
1114
|
+
ns.register_command("search", ArticleSearchCommand())
|
|
1061
1115
|
ns.register_command("show", ArticleShowCommand())
|
|
1062
1116
|
ns.register_command("pull", ArticlePullCommand())
|
|
1063
1117
|
ns.register_command("push", ArticlePushCommand())
|
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import builtins
|
|
22
22
|
import difflib
|
|
23
|
+
import re
|
|
23
24
|
import shlex
|
|
24
25
|
from collections import OrderedDict
|
|
25
26
|
from collections.abc import Callable
|
|
@@ -1325,6 +1326,56 @@ def _splice_body(old_body: str, block: str, position: str) -> str:
|
|
|
1325
1326
|
return combined
|
|
1326
1327
|
|
|
1327
1328
|
|
|
1329
|
+
_CAPABILITY_MISSING_MSG = (
|
|
1330
|
+
"Article full-text search is not available on this YouTrack "
|
|
1331
|
+
"instance. The REST article query parameter is undocumented and "
|
|
1332
|
+
"this server rejected it. Use `/article list`, `/tag articles`, "
|
|
1333
|
+
"or `/cache search` for local cached content."
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
def _is_unknown_query_param(exc: APIError) -> bool:
|
|
1338
|
+
"""Heuristic: did YT reject the ``query`` parameter as unknown?
|
|
1339
|
+
|
|
1340
|
+
YT versions that surface this path emit phrases like "unknown
|
|
1341
|
+
parameter" or "unrecognized parameter" in the 400 body. Refined
|
|
1342
|
+
once an instance that exhibits it surfaces during dogfooding;
|
|
1343
|
+
until then the heuristic is intentionally loose.
|
|
1344
|
+
"""
|
|
1345
|
+
body = (exc.response_body or "").lower()
|
|
1346
|
+
return ("parameter" in body) and ("unknown" in body or "unrecognized" in body)
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
_TAG_BARE_COLON_LITERAL_RE = re.compile(r"\btag\s*:\s*(?!\{)\S*:\S*")
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
def _is_tag_literal_misparse(exc: APIError, query: str) -> bool:
|
|
1353
|
+
"""Heuristic: did YT's article query DSL parse a colon inside a
|
|
1354
|
+
bare ``tag:`` literal as a field separator?
|
|
1355
|
+
|
|
1356
|
+
Two gates, both required:
|
|
1357
|
+
|
|
1358
|
+
1. The 400 body matches the observed phrasing — *"... isn't used
|
|
1359
|
+
for the tag field"* or its non-curly variant.
|
|
1360
|
+
2. The user's *input query* contains a ``tag:`` clause followed
|
|
1361
|
+
by a non-brace token that itself contains a colon. Without
|
|
1362
|
+
this the same upstream phrase fires for legitimate unknown-tag
|
|
1363
|
+
errors (e.g. ``tag: NOPE``), where the brace-wrapping hint
|
|
1364
|
+
would be misleading.
|
|
1365
|
+
|
|
1366
|
+
The regex is intentionally conservative — it does not understand
|
|
1367
|
+
quoted strings or full YT-DSL grammar; that is what the M012
|
|
1368
|
+
verbatim-passthrough contract trades away. False negatives prefer
|
|
1369
|
+
the no-hint path (raw YT message still surfaces) over the noisy
|
|
1370
|
+
false positive of suggesting braces when no colon was typed.
|
|
1371
|
+
"""
|
|
1372
|
+
body = (exc.response_body or "").lower()
|
|
1373
|
+
body_match = "isn't used for the tag field" in body or "is not used for the tag field" in body
|
|
1374
|
+
if not body_match:
|
|
1375
|
+
return False
|
|
1376
|
+
return _TAG_BARE_COLON_LITERAL_RE.search(query) is not None
|
|
1377
|
+
|
|
1378
|
+
|
|
1328
1379
|
class ArticleOperations:
|
|
1329
1380
|
"""High-level knowledge base article operations."""
|
|
1330
1381
|
|
|
@@ -1355,6 +1406,34 @@ class ArticleOperations:
|
|
|
1355
1406
|
return self._articles.list_project_articles(project_obj.id, top=top, skip=skip)
|
|
1356
1407
|
return self._articles.list_articles(top=top, skip=skip)
|
|
1357
1408
|
|
|
1409
|
+
def search(
|
|
1410
|
+
self,
|
|
1411
|
+
query: str,
|
|
1412
|
+
top: int = 50,
|
|
1413
|
+
skip: int = 0,
|
|
1414
|
+
) -> builtins.list[Article]:
|
|
1415
|
+
"""Server-side article full-text search via the YT article query
|
|
1416
|
+
engine. Wraps ``/api/articles?query=`` (undocumented but supported
|
|
1417
|
+
on current cloud + on-prem 2023.2). Server relevance order is
|
|
1418
|
+
preserved — no client-side re-sort, no local-substring fallback.
|
|
1419
|
+
"""
|
|
1420
|
+
self._debug.info("api", f"searching articles query={query!r}")
|
|
1421
|
+
try:
|
|
1422
|
+
return self._articles.search_articles(query, top=top, skip=skip)
|
|
1423
|
+
except APIError as exc:
|
|
1424
|
+
if exc.status_code == 400 and _is_unknown_query_param(exc):
|
|
1425
|
+
raise ValidationError(_CAPABILITY_MISSING_MSG) from exc
|
|
1426
|
+
if exc.status_code == 400 and _is_tag_literal_misparse(exc, query):
|
|
1427
|
+
raise ValidationError(
|
|
1428
|
+
f"{exc}\n"
|
|
1429
|
+
"Hint: tag names with colons need brace wrapping in YouTrack "
|
|
1430
|
+
"query syntax — use `tag: {kind:proposal}`, not "
|
|
1431
|
+
"`tag: kind:proposal`. See `kd --guide` §YouTrack Gotchas."
|
|
1432
|
+
) from exc
|
|
1433
|
+
if exc.status_code == 400:
|
|
1434
|
+
raise ValidationError(str(exc)) from exc
|
|
1435
|
+
raise
|
|
1436
|
+
|
|
1358
1437
|
def get(self, article_id: str) -> Article:
|
|
1359
1438
|
return self._articles.get_article(article_id)
|
|
1360
1439
|
|
|
@@ -4,6 +4,37 @@ All notable changes to kd-cli are recorded here. The project follows [Semantic V
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.7.1] — 2026-05-02
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `kd /article search` now appends a brace-wrapping hint when YouTrack returns
|
|
11
|
+
the *"… isn't used for the tag field"* 400 caused by a bare colon-bearing
|
|
12
|
+
`tag:` literal (e.g. `tag: kind:proposal`). The upstream message stays
|
|
13
|
+
visible; kd points to the fix — `tag: {kind:proposal}` — and references
|
|
14
|
+
`kd --guide` §YouTrack Gotchas. Docs and `kd /article search --help` now
|
|
15
|
+
show the brace form (KDCLI-186).
|
|
16
|
+
|
|
17
|
+
## [2.7.0] — 2026-05-02
|
|
18
|
+
|
|
19
|
+
This release adds server-backed full-text search for knowledge-base
|
|
20
|
+
articles. Until now `/article list --project P` was the only way to
|
|
21
|
+
find articles without knowing their id; body and field queries
|
|
22
|
+
required a manual scan. `kd /article search QUERY` closes that gap
|
|
23
|
+
with relevance ranking.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- `kd /article search QUERY [--limit N] [--skip N]` and
|
|
27
|
+
`client.articles.search()` — server-backed article full-text search
|
|
28
|
+
via the YouTrack article query DSL (same syntax family as issue
|
|
29
|
+
search; supports `project: {…}`, `content:`, `summary:`, `tag:`,
|
|
30
|
+
`comments:`, etc., relevance-ranked by YouTrack). Output columns
|
|
31
|
+
match `/article list`. The upstream REST `query` parameter is
|
|
32
|
+
undocumented but supported on YouTrack cloud and on-prem 2023.2;
|
|
33
|
+
instances that reject it surface a specific capability error
|
|
34
|
+
rather than silently falling back to local substring matching, so
|
|
35
|
+
scripts cannot accidentally trade ranked server results for
|
|
36
|
+
unranked summary scans (KDCLI-183).
|
|
37
|
+
|
|
7
38
|
## [2.6.6] — 2026-05-01
|
|
8
39
|
|
|
9
40
|
This release adds pinned issues as a first-class primer section
|
|
@@ -76,9 +76,9 @@ Not every read has the same cost.
|
|
|
76
76
|
|
|
77
77
|
**Direct reads** fetch one entity or one server-side page. Examples: `/issue show`, `/article show`, `/project show`, `/tag show`, `/query show`.
|
|
78
78
|
|
|
79
|
-
**Query-backed reads** rely on an upstream query or filter. Examples: `/issue list --query`, `/query issues`, `/tag issues`, `/tag list --prefix`, `/tag list --namespace`. They are usually cheap but still depend on YouTrack query semantics.
|
|
79
|
+
**Query-backed reads** rely on an upstream query or filter. Examples: `/issue list --query`, `/query issues`, `/tag issues`, `/tag list --prefix`, `/tag list --namespace`, `/article search`. They are usually cheap but still depend on YouTrack query semantics.
|
|
80
80
|
|
|
81
|
-
**Scan-backed reads** walk pages and filter client-side because YouTrack has no matching endpoint. Examples: `/tag articles`, the article half of `/tag entities`, article
|
|
81
|
+
**Scan-backed reads** walk pages and filter client-side because YouTrack has no matching endpoint. Examples: `/tag articles`, the article half of `/tag entities`, the article half of `/search --types article` (client-side substring), and deep `/article tree` traversal. Use limits and project scoping.
|
|
82
82
|
|
|
83
83
|
Write-side commands do not prompt for confirmation unless their help says so. Pair destructive or broad operations with a read first:
|
|
84
84
|
|
|
@@ -108,11 +108,11 @@ The cache namespace (`kd /cache refresh|status|search|clear`) is read-side by co
|
|
|
108
108
|
|
|
109
109
|
## YouTrack Gotchas
|
|
110
110
|
|
|
111
|
-
YouTrack
|
|
111
|
+
YouTrack's article query API is undocumented but supported on the YT tiers kd targets (cloud + on-prem 2023.2). `kd /article search QUERY` calls `GET /api/articles?query=…` and returns relevance-ranked results matching across body, summary, tags, and comments. Instances that reject the parameter surface a specific capability error rather than falling back silently. Server-side article-by-tag is still missing — `/tag articles` scans article pages that include `tags(id,name)` and filters locally.
|
|
112
112
|
|
|
113
113
|
Saved queries are YouTrack issue-search objects. `/query issues` executes them directly. `/query articles` only translates supported `project:` and `tag:` fields into article scans; issue-specific fields such as state or assignee are rejected for articles.
|
|
114
114
|
|
|
115
|
-
Tag names with colons or spaces need YouTrack query literal handling. `tag: type:case` is wrong; YouTrack reads `type` as the tag value and `case` as another field. Use `tag: {type:case}` in hand-written queries. kd command paths that compose tag queries use the shared literal helper.
|
|
115
|
+
Tag names with colons or spaces need YouTrack query literal handling in both issue and article queries (the article query DSL is the same syntax family). `tag: type:case` is wrong; YouTrack reads `type` as the tag value and `case` as another field. Use `tag: {type:case}` in hand-written queries — for `/issue list --query` and `/article search` alike. kd command paths that compose tag queries use the shared literal helper; `/article search` preserves the YouTrack 400 and appends a brace-wrapping hint when a bare colon-bearing `tag:` literal trips the misparse.
|
|
116
116
|
|
|
117
117
|
Repeated same-field clauses in YouTrack query syntax default to OR. `tag: A tag: B` matches either tag. kd multi-tag issue operations join tag clauses with explicit `and` when intersection is required.
|
|
118
118
|
|
|
@@ -357,4 +357,4 @@ Use these sources together:
|
|
|
357
357
|
- `docs/design/architecture.md` and ADRs for architectural constraints.
|
|
358
358
|
|
|
359
359
|
---
|
|
360
|
-
Updated: 2026-
|
|
360
|
+
Updated: 2026-05-02
|
|
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
|
|
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
|