kd-cli 2.6.5__tar.gz → 2.7.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.
- {kd_cli-2.6.5/kd_cli.egg-info → kd_cli-2.7.0}/PKG-INFO +1 -1
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/articles.py +16 -2
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cache.py +91 -6
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/article/__init__.py +59 -6
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/cache/__init__.py +5 -2
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/primer/__init__.py +397 -13
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/output.py +34 -3
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/client.py +41 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/primer.py +176 -2
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/CHANGELOG.md +50 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/operating-guide.md +37 -7
- {kd_cli-2.6.5 → kd_cli-2.7.0/kd_cli.egg-info}/PKG-INFO +1 -1
- {kd_cli-2.6.5 → kd_cli-2.7.0}/pyproject.toml +1 -1
- {kd_cli-2.6.5 → kd_cli-2.7.0}/LICENSE +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/MANIFEST.in +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/README.md +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/http.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/hub.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/saved_queries.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/tags.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/api/youtrack.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/article_marker.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/auth.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/core/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/core/command_base.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/core/command_parser.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/core/confirm.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/core/namespaces.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/core/option_registry.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/input.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/main.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/alias/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/init.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/issue/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/project/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/query/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/release_notes.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/search/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/system/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/tag/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/time/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/namespaces/user/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/cli/tree.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/config.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/config_read.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/config_schema.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/connection.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/debug.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/exceptions.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/history.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/locking.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/article.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/assist.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/attachment.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/comment.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/common.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/issue.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/project.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/saved_query.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/search.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/tag.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/tag_entities.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/user.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/models/work_item.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/operating_guide.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/query_parser.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/release_notes.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/command-synonyms.yaml +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/config-template.yaml +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/error-messages.yaml +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/primers/__init__.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/primers/hitl.yaml +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/resources/sdlc-doctrine.md +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd/sdlc.py +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd_cli.egg-info/SOURCES.txt +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd_cli.egg-info/dependency_links.txt +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd_cli.egg-info/entry_points.txt +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd_cli.egg-info/requires.txt +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/kd_cli.egg-info/top_level.txt +0 -0
- {kd_cli-2.6.5 → kd_cli-2.7.0}/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]:
|
|
@@ -30,7 +30,14 @@ from kd.connection import ConnectionContext
|
|
|
30
30
|
from kd.exceptions import APIError, ConfigError, ValidationError
|
|
31
31
|
from kd.history import atomic_write_text
|
|
32
32
|
from kd.models.tag_entities import TagEntitiesResult, tag_entities_result_to_dict
|
|
33
|
-
from kd.primer import
|
|
33
|
+
from kd.primer import (
|
|
34
|
+
PRIMER_FILENAME,
|
|
35
|
+
PrimerArticle,
|
|
36
|
+
PrimerIssue,
|
|
37
|
+
PrimerTag,
|
|
38
|
+
load_primer,
|
|
39
|
+
require_workspace_dir,
|
|
40
|
+
)
|
|
34
41
|
from kd.query_parser import youtrack_query_literal
|
|
35
42
|
|
|
36
43
|
# Public surface: names imported by external callers (CLI, client) and the
|
|
@@ -90,6 +97,7 @@ __all__ = [
|
|
|
90
97
|
"HydratedQuery",
|
|
91
98
|
"HydratedQueryRun",
|
|
92
99
|
"IssueRefreshReport",
|
|
100
|
+
"IssueScopeRefreshReport",
|
|
93
101
|
"MilestoneRefreshReport",
|
|
94
102
|
"QueryNameRefreshReport",
|
|
95
103
|
"QueryRefreshReport",
|
|
@@ -285,11 +293,26 @@ class QueryRefreshReport:
|
|
|
285
293
|
failed: list[FailedQuery]
|
|
286
294
|
|
|
287
295
|
|
|
296
|
+
@dataclass
|
|
297
|
+
class IssueScopeRefreshReport:
|
|
298
|
+
"""Per-pinned-issue half of :class:`RefreshReport`.
|
|
299
|
+
|
|
300
|
+
Parallels :class:`ArticleRefreshReport`. Reuses :class:`HydratedIssue`
|
|
301
|
+
and :class:`FailedIssue` from the single-issue refresh path, with the
|
|
302
|
+
same per-id isolation contract.
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
skipped_no_pinned: bool
|
|
306
|
+
hydrated: list[HydratedIssue]
|
|
307
|
+
failed: list[FailedIssue]
|
|
308
|
+
|
|
309
|
+
|
|
288
310
|
@dataclass
|
|
289
311
|
class RefreshReport:
|
|
290
312
|
"""Result of a single ``refresh_primer_scope`` invocation."""
|
|
291
313
|
|
|
292
314
|
articles: ArticleRefreshReport
|
|
315
|
+
issues: IssueScopeRefreshReport
|
|
293
316
|
queries: QueryRefreshReport
|
|
294
317
|
|
|
295
318
|
|
|
@@ -1881,6 +1904,57 @@ def _refresh_primer_articles(
|
|
|
1881
1904
|
return ArticleRefreshReport(skipped_no_pinned=False, hydrated=hydrated, failed=failed)
|
|
1882
1905
|
|
|
1883
1906
|
|
|
1907
|
+
def _refresh_primer_issues(
|
|
1908
|
+
client: Any,
|
|
1909
|
+
pinned: list[PrimerIssue],
|
|
1910
|
+
fetched_at: datetime,
|
|
1911
|
+
cache_root: Path,
|
|
1912
|
+
) -> IssueScopeRefreshReport:
|
|
1913
|
+
"""Hydrate each pinned issue into ``cache_root/issues/``.
|
|
1914
|
+
|
|
1915
|
+
Mirrors :func:`_refresh_primer_articles`. Per-id ``APIError`` (and
|
|
1916
|
+
subclasses) are isolated into ``failed``; ``AuthError`` and
|
|
1917
|
+
``ConfigError`` propagate as command-level failures and abort the
|
|
1918
|
+
run. Issues are fetched with ``detail="rich"`` so the cache payload
|
|
1919
|
+
matches the single-issue refresh shape (proposal-39 §Issue Cache
|
|
1920
|
+
Shape); ``include_comments`` stays off — the primer-scope contract
|
|
1921
|
+
is "what does the agent need to start work," not "full comment log."
|
|
1922
|
+
"""
|
|
1923
|
+
if not pinned:
|
|
1924
|
+
return IssueScopeRefreshReport(skipped_no_pinned=True, hydrated=[], failed=[])
|
|
1925
|
+
|
|
1926
|
+
hydrated: list[HydratedIssue] = []
|
|
1927
|
+
failed: list[FailedIssue] = []
|
|
1928
|
+
for entry in pinned:
|
|
1929
|
+
try:
|
|
1930
|
+
issue = client.issues.get(entry.id, detail="rich")
|
|
1931
|
+
payload = _issue_to_cache_payload(
|
|
1932
|
+
issue,
|
|
1933
|
+
fetched_at=fetched_at,
|
|
1934
|
+
include_comments=False,
|
|
1935
|
+
command=PRIMER_SCOPE_REFRESH_COMMAND,
|
|
1936
|
+
reason="primer pinned issue",
|
|
1937
|
+
)
|
|
1938
|
+
write_cached_issue(
|
|
1939
|
+
issue.id_readable,
|
|
1940
|
+
payload,
|
|
1941
|
+
fetched_at=fetched_at,
|
|
1942
|
+
cache_root=cache_root,
|
|
1943
|
+
)
|
|
1944
|
+
hydrated.append(
|
|
1945
|
+
HydratedIssue(
|
|
1946
|
+
id=issue.id_readable,
|
|
1947
|
+
summary=issue.summary,
|
|
1948
|
+
upstream_updated=_ms_to_iso(issue.updated),
|
|
1949
|
+
path=_report_path(cache_root / ISSUES_DIRNAME / f"{issue.id_readable}.yaml"),
|
|
1950
|
+
comments_count=None,
|
|
1951
|
+
)
|
|
1952
|
+
)
|
|
1953
|
+
except APIError as e:
|
|
1954
|
+
failed.append(FailedIssue(id=entry.id, reason=f"{type(e).__name__}: {e}"))
|
|
1955
|
+
return IssueScopeRefreshReport(skipped_no_pinned=False, hydrated=hydrated, failed=failed)
|
|
1956
|
+
|
|
1957
|
+
|
|
1884
1958
|
def _refresh_primer_query(
|
|
1885
1959
|
client: Any,
|
|
1886
1960
|
pinned_tags: list[PrimerTag],
|
|
@@ -1962,17 +2036,23 @@ def refresh_primer_scope(
|
|
|
1962
2036
|
root = connection_cache_dir(connection, workspace_dir.parent)
|
|
1963
2037
|
when = fetched_at or datetime.now(timezone.utc)
|
|
1964
2038
|
|
|
1965
|
-
if not primer.articles and not primer.tags:
|
|
2039
|
+
if not primer.articles and not primer.tags and not primer.issues:
|
|
1966
2040
|
raise ValidationError(
|
|
1967
|
-
"--primer-scope set but .kd/primer.yaml has no pinned articles
|
|
1968
|
-
"Pin a tag with: kd /primer/tag add TAG_NAME,
|
|
2041
|
+
"--primer-scope set but .kd/primer.yaml has no pinned articles, tags, "
|
|
2042
|
+
"or issues. Pin a tag with: kd /primer/tag add TAG_NAME, an issue "
|
|
2043
|
+
"with: kd /primer/issue add ISSUE-ID, or an article with: "
|
|
1969
2044
|
"kd /primer/article add ARTICLE-ID"
|
|
1970
2045
|
)
|
|
1971
2046
|
|
|
1972
2047
|
write_scope_manifest(connection, cache_root=root, fetched_at=when)
|
|
1973
2048
|
article_report = _refresh_primer_articles(client, primer.articles, when, root)
|
|
2049
|
+
issue_report = _refresh_primer_issues(client, primer.issues, when, root)
|
|
1974
2050
|
query_report = _refresh_primer_query(client, primer.tags, when, root)
|
|
1975
|
-
return RefreshReport(
|
|
2051
|
+
return RefreshReport(
|
|
2052
|
+
articles=article_report,
|
|
2053
|
+
issues=issue_report,
|
|
2054
|
+
queries=query_report,
|
|
2055
|
+
)
|
|
1976
2056
|
|
|
1977
2057
|
|
|
1978
2058
|
# ---------------------------------------------------------------------------
|
|
@@ -2027,6 +2107,7 @@ def _issue_to_cache_payload(
|
|
|
2027
2107
|
fetched_at: datetime,
|
|
2028
2108
|
include_comments: bool,
|
|
2029
2109
|
command: str,
|
|
2110
|
+
reason: str = "single-entity refresh",
|
|
2030
2111
|
) -> dict[str, Any]:
|
|
2031
2112
|
"""Build the proposal-39 §Issue Cache Shape dict from an SDK ``Issue``.
|
|
2032
2113
|
|
|
@@ -2037,6 +2118,10 @@ def _issue_to_cache_payload(
|
|
|
2037
2118
|
:func:`_project_key_from_id`. ``links`` and ``attachments`` are
|
|
2038
2119
|
deliberately not projected (deferred to S7+).
|
|
2039
2120
|
|
|
2121
|
+
``reason`` defaults to ``"single-entity refresh"`` for the existing
|
|
2122
|
+
single-issue refresh path; primer-scope refresh passes
|
|
2123
|
+
``"primer pinned issue"`` to record why the entry was hydrated.
|
|
2124
|
+
|
|
2040
2125
|
Not shared with ``kd/cli/namespaces/issue/__init__.py:_issue_to_detail``
|
|
2041
2126
|
— that helper truncates comments to the last 5 (display logic),
|
|
2042
2127
|
while cache projection must include all comments when
|
|
@@ -2053,7 +2138,7 @@ def _issue_to_cache_payload(
|
|
|
2053
2138
|
"fetched_at": _format_iso(fetched_at),
|
|
2054
2139
|
"source": {
|
|
2055
2140
|
"command": command,
|
|
2056
|
-
"reason":
|
|
2141
|
+
"reason": reason,
|
|
2057
2142
|
},
|
|
2058
2143
|
}
|
|
2059
2144
|
for cf in issue.custom_fields:
|
|
@@ -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,56 @@ 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 "summary: timeout" --limit 10',
|
|
145
|
+
],
|
|
146
|
+
see_also=["/article list", "/search", "/tag articles"],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _register_options(self) -> None:
|
|
150
|
+
self.register_option(limit_option())
|
|
151
|
+
self.register_option(
|
|
152
|
+
OptionDeclaration(
|
|
153
|
+
name="skip",
|
|
154
|
+
layer=OptionLayer.COMMAND,
|
|
155
|
+
type=int,
|
|
156
|
+
default=0,
|
|
157
|
+
help_text="Skip the first N results (pagination).",
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def execute(
|
|
162
|
+
self, args: list[str], options: ResolvedOptions, client: Any = None
|
|
163
|
+
) -> CommandResult:
|
|
164
|
+
query = " ".join(args).strip()
|
|
165
|
+
if not query:
|
|
166
|
+
raise ValidationError("Usage: kd /article search QUERY")
|
|
167
|
+
limit = options.command.get("limit", 50)
|
|
168
|
+
skip = options.command.get("skip", 0)
|
|
169
|
+
articles = client.articles.search(query, top=limit, skip=skip)
|
|
170
|
+
return [_article_to_row(a) for a in articles]
|
|
171
|
+
|
|
172
|
+
|
|
121
173
|
class ArticleShowCommand(Command):
|
|
122
174
|
"""Show article details."""
|
|
123
175
|
|
|
@@ -1058,6 +1110,7 @@ def create_article_namespace() -> Namespace:
|
|
|
1058
1110
|
"""Build the /article namespace."""
|
|
1059
1111
|
ns = Namespace("article", "Knowledge base articles", path="article")
|
|
1060
1112
|
ns.register_command("list", ArticleListCommand())
|
|
1113
|
+
ns.register_command("search", ArticleSearchCommand())
|
|
1061
1114
|
ns.register_command("show", ArticleShowCommand())
|
|
1062
1115
|
ns.register_command("pull", ArticlePullCommand())
|
|
1063
1116
|
ns.register_command("push", ArticlePushCommand())
|
|
@@ -131,8 +131,11 @@ class CacheRefreshCommand(Command):
|
|
|
131
131
|
"hydration verb — the cache is rehydratable upstream-"
|
|
132
132
|
"mirror data and no command silently reads through it. "
|
|
133
133
|
"Selectors are mutually exclusive: --primer-scope "
|
|
134
|
-
"hydrates primer-pinned articles
|
|
135
|
-
"
|
|
134
|
+
"hydrates primer-pinned articles, primer-pinned issues "
|
|
135
|
+
"(via the same single-issue projection at "
|
|
136
|
+
".kd/cache/issues/<id>.yaml; comments not projected), "
|
|
137
|
+
"plus the primer-scope tag-entities query; --issue ID "
|
|
138
|
+
"hydrates a single issue "
|
|
136
139
|
"into .kd/cache/issues/<id>.yaml; --article ID hydrates "
|
|
137
140
|
"a single article; --query NAME runs a saved query and "
|
|
138
141
|
"writes the issue list to .kd/cache/queries/<slug>.yaml "
|