Skip to content

Commit 87ee07a

Browse files
feat: user-facing CLI, Beta classifier, own-repo URLs (0.6.0)
- New attune-help console script with lookup / list / search / simpler subcommands over the HelpEngine API. python -m attune_help also works. Smoke tests cover exit codes, missing-topic suggestions, and arg parsing. - Promote Development Status to Beta (was Alpha). attune-help is now a core dep of attune-ai (Production/Stable), so the Alpha classifier understated the package's actual maturity. - Point PyPI Homepage/Repository URLs at the extracted Smart-AI-Memory/attune-help repo (was the parent attune-ai monorepo). Added Changelog and Issues URLs. CHANGELOG entry is 0.6.0 — Unreleased. Consumers in attune-ai and attune-author pin attune-help>=0.5.1,<0.6, so those caps need coordinated bumps to <0.7 when 0.6.0 is released. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1d1d366 commit 87ee07a

5 files changed

Lines changed: 279 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,37 @@
22

33
All notable changes to `attune-help` are documented here.
44

5+
## 0.6.0 — Unreleased
6+
7+
### Added
8+
9+
- **User-facing CLI** — new `attune-help` console script
10+
exposes `lookup`, `list`, `search`, and `simpler`
11+
subcommands over the same `HelpEngine` API the MCP
12+
server uses. Terminal users no longer need an MCP
13+
client to access the help content. `python -m
14+
attune_help` also works.
15+
16+
### Changed
17+
18+
- **Development Status promoted to Beta** (was Alpha).
19+
attune-help is now a core dependency of attune-ai
20+
(Production/Stable), so the Alpha classifier understated
21+
the package's actual maturity. Version jumps to `0.6.0`
22+
rather than `0.5.2` to mark the shift and give
23+
downstream consumers a deliberate upgrade point.
24+
- **PyPI project URLs point to the extracted repo**
25+
(`Smart-AI-Memory/attune-help`) instead of the parent
26+
`attune-ai` monorepo. Also added `Changelog` and
27+
`Issues` URLs.
28+
29+
### Consumer impact
30+
31+
- attune-ai and attune-author both now pin
32+
`attune-help>=0.5.1,<0.6`. Those caps will need to be
33+
bumped to `<0.7` at release time, coordinated across
34+
the two consumer repos.
35+
536
## 0.5.1 — 2026-04-12
637

738
### Fixed

pyproject.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ authors = [
1414
]
1515
keywords = ["help", "documentation", "progressive-depth", "templates"]
1616
classifiers = [
17-
"Development Status :: 3 - Alpha",
17+
"Development Status :: 4 - Beta",
1818
"Intended Audience :: Developers",
1919
"License :: OSI Approved :: Apache Software License",
2020
"Programming Language :: Python :: 3",
@@ -34,11 +34,14 @@ rich = ["rich>=13.0.0"]
3434
plugin = ["mcp>=0.9.0"]
3535

3636
[project.scripts]
37+
attune-help = "attune_help.cli:main"
3738
attune-help-mcp = "attune_help.mcp.server:main"
3839

3940
[project.urls]
40-
Homepage = "https://github.com/Smart-AI-Memory/attune-ai"
41-
Repository = "https://github.com/Smart-AI-Memory/attune-ai"
41+
Homepage = "https://github.com/Smart-AI-Memory/attune-help"
42+
Repository = "https://github.com/Smart-AI-Memory/attune-help"
43+
Changelog = "https://github.com/Smart-AI-Memory/attune-help/blob/main/CHANGELOG.md"
44+
Issues = "https://github.com/Smart-AI-Memory/attune-help/issues"
4245

4346
[tool.setuptools.packages.find]
4447
where = ["src"]

src/attune_help/__main__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Allow ``python -m attune_help`` as an alias for the CLI."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
7+
from attune_help.cli import main
8+
9+
if __name__ == "__main__":
10+
sys.exit(main())

src/attune_help/cli.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""attune-help CLI.
2+
3+
Minimal user-facing CLI wrapped around :class:`HelpEngine`.
4+
Exposes the handful of operations most useful from a
5+
terminal:
6+
7+
attune-help lookup <topic>
8+
attune-help list [--type <kind>] [--limit N]
9+
attune-help search <query> [--limit N]
10+
attune-help simpler <topic>
11+
12+
The MCP server (``attune-help-mcp``) is the equivalent
13+
surface for agent/IDE clients. Both wrap the same public
14+
``HelpEngine`` API — the CLI exists so terminal users have
15+
a first-class entry point without standing up an MCP
16+
client.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
import argparse
22+
import sys
23+
from collections.abc import Sequence
24+
25+
from attune_help import HelpEngine, __version__
26+
27+
_RENDERERS = ("plain", "cli", "claude_code", "marketplace", "json")
28+
29+
30+
def _build_parser() -> argparse.ArgumentParser:
31+
"""Build the top-level argument parser."""
32+
parser = argparse.ArgumentParser(
33+
prog="attune-help",
34+
description=("Look up, list, and search progressive-depth " "help templates."),
35+
)
36+
parser.add_argument(
37+
"--version",
38+
action="version",
39+
version=f"attune-help {__version__}",
40+
)
41+
parser.add_argument(
42+
"--template-dir",
43+
help=("Override template directory. Defaults to " "bundled templates."),
44+
)
45+
parser.add_argument(
46+
"--renderer",
47+
default="plain",
48+
choices=_RENDERERS,
49+
help="Output renderer. Default: plain.",
50+
)
51+
52+
sub = parser.add_subparsers(dest="command", required=True)
53+
54+
p_lookup = sub.add_parser(
55+
"lookup",
56+
help="Look up a topic with progressive depth.",
57+
)
58+
p_lookup.add_argument("topic")
59+
60+
p_list = sub.add_parser(
61+
"list",
62+
help="List available topic slugs.",
63+
)
64+
p_list.add_argument(
65+
"--type",
66+
dest="type_filter",
67+
help=("Filter by template type (e.g. concept, task, " "reference)."),
68+
)
69+
p_list.add_argument(
70+
"--limit",
71+
type=int,
72+
help="Maximum number of topics to show.",
73+
)
74+
75+
p_search = sub.add_parser(
76+
"search",
77+
help="Fuzzy-search topic slugs.",
78+
)
79+
p_search.add_argument("query")
80+
p_search.add_argument(
81+
"--limit",
82+
type=int,
83+
default=10,
84+
help="Maximum results. Default: 10.",
85+
)
86+
87+
p_simpler = sub.add_parser(
88+
"simpler",
89+
help=("Step the topic's progressive depth back one " "level."),
90+
)
91+
p_simpler.add_argument("topic")
92+
93+
return parser
94+
95+
96+
def _engine(args: argparse.Namespace) -> HelpEngine:
97+
"""Build a HelpEngine from parsed CLI args."""
98+
return HelpEngine(
99+
template_dir=args.template_dir,
100+
renderer=args.renderer,
101+
)
102+
103+
104+
def _cmd_lookup(args: argparse.Namespace) -> int:
105+
engine = _engine(args)
106+
result = engine.lookup(args.topic)
107+
if result is None:
108+
# Miss: show "did you mean" suggestions on stderr and
109+
# return nonzero so scripts can react.
110+
suggestions = engine.suggest(args.topic)
111+
if suggestions:
112+
joined = ", ".join(suggestions)
113+
print(
114+
f"No help for {args.topic!r}. Did you mean: {joined}?",
115+
file=sys.stderr,
116+
)
117+
else:
118+
print(f"No help for {args.topic!r}.", file=sys.stderr)
119+
return 1
120+
print(result)
121+
return 0
122+
123+
124+
def _cmd_list(args: argparse.Namespace) -> int:
125+
engine = _engine(args)
126+
topics = engine.list_topics(
127+
type_filter=args.type_filter,
128+
limit=args.limit,
129+
)
130+
if not topics:
131+
print("No topics found.", file=sys.stderr)
132+
return 1
133+
for topic in topics:
134+
print(topic)
135+
return 0
136+
137+
138+
def _cmd_search(args: argparse.Namespace) -> int:
139+
engine = _engine(args)
140+
hits = engine.search(args.query, limit=args.limit)
141+
if not hits:
142+
print(f"No matches for {args.query!r}.", file=sys.stderr)
143+
return 1
144+
for slug, score in hits:
145+
print(f"{slug}\t{score:.2f}")
146+
return 0
147+
148+
149+
def _cmd_simpler(args: argparse.Namespace) -> int:
150+
engine = _engine(args)
151+
result = engine.simpler(args.topic)
152+
if result is None:
153+
print(f"No help for {args.topic!r}.", file=sys.stderr)
154+
return 1
155+
print(result)
156+
return 0
157+
158+
159+
_DISPATCH = {
160+
"lookup": _cmd_lookup,
161+
"list": _cmd_list,
162+
"search": _cmd_search,
163+
"simpler": _cmd_simpler,
164+
}
165+
166+
167+
def main(argv: Sequence[str] | None = None) -> int:
168+
"""Entry point for the ``attune-help`` console script."""
169+
parser = _build_parser()
170+
args = parser.parse_args(argv)
171+
handler = _DISPATCH[args.command]
172+
try:
173+
return handler(args)
174+
except ValueError as e:
175+
print(f"Error: {e}", file=sys.stderr)
176+
return 2
177+
178+
179+
if __name__ == "__main__":
180+
sys.exit(main())

tests/test_cli.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Smoke tests for the attune-help CLI."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
from attune_help.cli import main
7+
8+
9+
def test_version_flag_exits_zero(capsys: pytest.CaptureFixture[str]) -> None:
10+
"""``--version`` prints the version and exits 0."""
11+
with pytest.raises(SystemExit) as exc_info:
12+
main(["--version"])
13+
assert exc_info.value.code == 0
14+
captured = capsys.readouterr()
15+
assert "attune-help" in captured.out
16+
17+
18+
def test_missing_command_exits_nonzero(capsys: pytest.CaptureFixture[str]) -> None:
19+
"""Running with no subcommand prints usage and exits nonzero."""
20+
with pytest.raises(SystemExit) as exc_info:
21+
main([])
22+
assert exc_info.value.code != 0
23+
24+
25+
def test_lookup_missing_topic_returns_1(capsys: pytest.CaptureFixture[str]) -> None:
26+
"""Looking up a nonexistent topic returns exit code 1."""
27+
exit_code = main(["lookup", "this-topic-definitely-does-not-exist-xyz"])
28+
assert exit_code == 1
29+
captured = capsys.readouterr()
30+
assert "No help for" in captured.err
31+
32+
33+
def test_list_runs(capsys: pytest.CaptureFixture[str]) -> None:
34+
"""``list`` exits cleanly (0 or 1 depending on bundled content)."""
35+
exit_code = main(["list", "--limit", "3"])
36+
# Either topics exist (0) or not (1) — both are valid smoke outcomes.
37+
assert exit_code in (0, 1)
38+
39+
40+
def test_search_empty_match_returns_1(capsys: pytest.CaptureFixture[str]) -> None:
41+
"""Searching for something guaranteed absent returns exit code 1."""
42+
exit_code = main(["search", "zzz-nonexistent-query-xyz"])
43+
assert exit_code == 1
44+
captured = capsys.readouterr()
45+
assert "No matches" in captured.err
46+
47+
48+
def test_invalid_renderer_rejected(capsys: pytest.CaptureFixture[str]) -> None:
49+
"""argparse rejects an unknown --renderer value."""
50+
with pytest.raises(SystemExit) as exc_info:
51+
main(["--renderer", "bogus", "lookup", "anything"])
52+
assert exc_info.value.code != 0

0 commit comments

Comments
 (0)