Skip to content

Commit 97b1ca1

Browse files
feat(cli): add openkb feedback to file a prefilled GitHub issue (#53)
* feat(cli): add 'openkb feedback' to file a prefilled GitHub issue Submitting feedback from a CLI tool with no backend is awkward — auto-creating issues requires a maintainer-owned token (security nightmare to ship in source), running an OpenKB-owned API server is overkill for an OSS CLI, and asking users to authenticate with their own gh CLI excludes anyone who hasn't installed it. Surface shrinks to one flag (--type) plus the positional message. Help text and README updated. 19 feedback tests still pass (329 total).
1 parent 62153db commit 97b1ca1

3 files changed

Lines changed: 394 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ A single source might touch 10-15 wiki pages. Knowledge accumulates: each docume
156156
| `openkb lint` | Run structural + knowledge health checks |
157157
| `openkb list` | List indexed documents and concepts |
158158
| `openkb status` | Show knowledge base stats |
159+
| <code>openkb&nbsp;feedback&nbsp;["msg"]</code> | File feedback by opening a prefilled GitHub issue (use `--type bug/feature/question` to tag the issue) |
159160

160161
<!-- | `openkb lint --fix` | Auto-fix what it can | -->
161162

openkb/cli.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,3 +1133,154 @@ def status(ctx):
11331133
click.echo("No knowledge base found. Run `openkb init` first.")
11341134
return
11351135
print_status(kb_dir)
1136+
1137+
1138+
# ---------------------------------------------------------------------------
1139+
# feedback
1140+
# ---------------------------------------------------------------------------
1141+
1142+
_FEEDBACK_REPO = "VectifyAI/OpenKB"
1143+
_FEEDBACK_TYPES = ("bug", "feature", "question", "other")
1144+
_FEEDBACK_LABEL_MAP = {
1145+
"bug": "bug",
1146+
"feature": "enhancement",
1147+
"question": "question",
1148+
"other": "",
1149+
}
1150+
1151+
1152+
def _openkb_version() -> str:
1153+
"""Return the installed openkb package version.
1154+
1155+
Delegates to ``openkb.__version__`` so the chat REPL, feedback issue
1156+
body, and any future caller all surface the same fallback string
1157+
(``0.0.0+unknown`` from ``openkb/__init__.py``). Mirrors
1158+
``openkb.agent.chat._openkb_version``.
1159+
"""
1160+
from openkb import __version__
1161+
return __version__
1162+
1163+
1164+
def _collect_feedback_diagnostics(ctx) -> dict[str, str]:
1165+
"""Auto-collect non-sensitive environment info to attach to a feedback
1166+
issue. Kept deliberately small — no paths, no API keys, no usernames.
1167+
"""
1168+
import platform
1169+
1170+
kb_dir = _find_kb_dir(ctx.obj.get("kb_dir_override") if ctx.obj else None)
1171+
return {
1172+
"openkb": _openkb_version(),
1173+
"python": platform.python_version(),
1174+
"platform": f"{platform.system()} {platform.release()}",
1175+
"kb_initialised": "yes" if kb_dir else "no",
1176+
}
1177+
1178+
1179+
def _build_feedback_url(
1180+
message: str, feedback_type: str, diagnostics: dict[str, str],
1181+
) -> str:
1182+
"""Build a GitHub issue URL with title / body / labels prefilled."""
1183+
from urllib.parse import urlencode
1184+
1185+
first_line = message.splitlines()[0] if message else ""
1186+
truncated = first_line[:60] + ("…" if len(first_line) > 60 else "")
1187+
title_prefix = f"[{feedback_type}] " if feedback_type != "other" else ""
1188+
title = f"{title_prefix}{truncated}" if truncated else f"{title_prefix}Feedback from CLI"
1189+
1190+
if diagnostics:
1191+
diag_block = "\n".join(f"- **{k}**: {v}" for k, v in diagnostics.items())
1192+
body = (
1193+
f"{message}\n\n"
1194+
"---\n\n"
1195+
"<details>\n"
1196+
"<summary>Diagnostics (auto-collected by <code>openkb feedback</code>)</summary>\n\n"
1197+
f"{diag_block}\n"
1198+
"</details>\n"
1199+
)
1200+
else:
1201+
body = message
1202+
1203+
params = {"title": title, "body": body}
1204+
label = _FEEDBACK_LABEL_MAP.get(feedback_type, "")
1205+
if label:
1206+
params["labels"] = label
1207+
1208+
return f"https://github.com/{_FEEDBACK_REPO}/issues/new?{urlencode(params)}"
1209+
1210+
1211+
@cli.command()
1212+
@click.argument("message", required=False)
1213+
@click.option(
1214+
"--type", "feedback_type",
1215+
type=click.Choice(_FEEDBACK_TYPES),
1216+
default=None,
1217+
help="Feedback type — sets the GitHub issue label.",
1218+
)
1219+
@click.pass_context
1220+
def feedback(ctx, message, feedback_type):
1221+
"""Submit feedback by opening a prefilled GitHub issue.
1222+
1223+
Examples:
1224+
1225+
\b
1226+
openkb feedback # interactive
1227+
openkb feedback "openkb add hangs on .docx" # one-line bug report
1228+
openkb feedback --type feature "..." # tags the issue 'enhancement'
1229+
1230+
The command does not send anything to OpenKB maintainers directly —
1231+
it opens GitHub in your browser with title, body, and label prefilled.
1232+
You log in with your own GitHub account and submit the issue.
1233+
"""
1234+
if not message:
1235+
click.echo(
1236+
"What's your feedback? End with an empty line + Ctrl-D "
1237+
"(Unix) or Ctrl-Z+Enter (Windows). Ctrl-C cancels."
1238+
)
1239+
message = sys.stdin.read().strip()
1240+
1241+
if not message:
1242+
click.echo("No feedback provided. Aborted.")
1243+
ctx.exit(1)
1244+
return
1245+
1246+
if feedback_type is None:
1247+
# Skip the prompt in non-TTY contexts (CI / piped stdin) so
1248+
# ``echo "msg" | openkb feedback`` doesn't hang on the second
1249+
# prompt after consuming all piped input for the message body.
1250+
# Mirrors the ``_stdin_is_tty()`` gate added in PR #48.
1251+
if _stdin_is_tty():
1252+
feedback_type = click.prompt(
1253+
"Type",
1254+
default="other",
1255+
type=click.Choice(_FEEDBACK_TYPES),
1256+
show_default=True,
1257+
show_choices=True,
1258+
)
1259+
else:
1260+
feedback_type = "other"
1261+
1262+
diagnostics = _collect_feedback_diagnostics(ctx)
1263+
url = _build_feedback_url(message, feedback_type, diagnostics)
1264+
1265+
click.echo("Copy this URL into a browser if the auto-open below fails:")
1266+
click.echo(f" {url}")
1267+
1268+
import webbrowser
1269+
try:
1270+
opened = webbrowser.open(url)
1271+
except Exception as exc:
1272+
# webbrowser.open rarely raises but be defensive — the printed URL
1273+
# above is the fallback path.
1274+
click.echo(f" (browser auto-open failed: {exc})", err=True)
1275+
return
1276+
1277+
# ``webbrowser.open`` returns False on headless boxes (no GUI, no
1278+
# ``BROWSER`` env) without raising. Without this check we'd silently
1279+
# print "Opened" and the user would think the issue was filed.
1280+
if opened:
1281+
click.echo("Opened GitHub in your browser.")
1282+
else:
1283+
click.echo(
1284+
" (no browser available — copy the URL above to file the issue)",
1285+
err=True,
1286+
)

0 commit comments

Comments
 (0)