feat: fetch_issue script for trimmed issue reads
Raw `tea issues -o json` / `tea api` dumps the full payload (avatars, nested users, every comment) into the model context. The script writes trimmed markdown to tmp/issue/<n>/ and prints only a compact index; login comes from the operator pin, never an argument. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,39 @@ The pin takes effect immediately — no restart. Only `tea logins list` and
|
|||||||
per-project by the operator (see `/tea:auth`) and injected by the guard.
|
per-project by the operator (see `/tea:auth`) and injected by the guard.
|
||||||
Config lives in `$XDG_CONFIG_HOME/tea`.
|
Config lives in `$XDG_CONFIG_HOME/tea`.
|
||||||
|
|
||||||
|
## Reading an issue: use the fetch script, not raw tea calls
|
||||||
|
|
||||||
|
To read an existing issue (its body, its discussion), do NOT run
|
||||||
|
`tea issues <n> -o json` or `tea api .../issues/<n>` directly — the full JSON
|
||||||
|
payload (avatars, nested user objects, every comment body) lands in your
|
||||||
|
context whether you need it or not. Instead run the bundled script; the only
|
||||||
|
input it needs is the issue key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 <skill-base-dir>/scripts/fetch_issue.py 42
|
||||||
|
```
|
||||||
|
|
||||||
|
Key forms: `42`, `#42`, `owner/repo#42`, or a full issue URL. Repo defaults
|
||||||
|
to the current directory's git remote (add `--repo owner/repo` outside one).
|
||||||
|
|
||||||
|
It writes trimmed markdown files locally and prints only a compact index:
|
||||||
|
|
||||||
|
```
|
||||||
|
tmp/issue/42/data issue: metadata header + body
|
||||||
|
tmp/issue/42/comments/ one file per comment: NNN-<comment-id>.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Then Read just the files the task needs — often the `data` file alone, or a
|
||||||
|
single comment picked from the index (author + date per line). Each comment
|
||||||
|
file carries its `comment-id`, ready for a `PATCH` via `tea api`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- No `--login` on the script call: the script resolves the operator's pinned
|
||||||
|
login itself from `.claude/settings.local.json` — same source as the
|
||||||
|
tea-guard hook. No pin → it exits with a pointer to `/tea:auth`.
|
||||||
|
- Every run refetches fresh and wipes the issue's `comments/` dir, so stale
|
||||||
|
files never survive.
|
||||||
|
|
||||||
## Index
|
## Index
|
||||||
|
|
||||||
- [tea CLI overview](references/tea/index.md) — global flags, common options, output formats
|
- [tea CLI overview](references/tea/index.md) — global flags, common options, output formats
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
fetch_issue.py — pull one Gitea issue (+ all comments) to local files.
|
||||||
|
|
||||||
|
Token-saving fetcher for Claude sessions: instead of dumping raw API JSON
|
||||||
|
into the conversation, it writes trimmed markdown files under tmp/issue/
|
||||||
|
and prints only a compact index. Read the files you actually need.
|
||||||
|
|
||||||
|
tmp/issue/<n>/data issue itself (metadata header + body)
|
||||||
|
tmp/issue/<n>/comments/ one file per comment: NNN-<comment-id>.md
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
fetch_issue.py <key> [--repo owner/repo] [--out DIR]
|
||||||
|
|
||||||
|
<key> 42 | #42 | owner/repo#42 | https://host/owner/repo/issues/42
|
||||||
|
|
||||||
|
Login: resolved from .claude/settings.local.json (env.GITEA_LOGIN), walking
|
||||||
|
up from CWD — the same file /tea:auth writes and the tea-guard hook reads.
|
||||||
|
The script never accepts a login argument: the operator's pin is the only
|
||||||
|
identity it will use. No pin -> exit with a pointer to /tea:auth.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def die(msg, code=1):
|
||||||
|
sys.stderr.write("fetch_issue: " + msg + "\n")
|
||||||
|
sys.exit(code)
|
||||||
|
|
||||||
|
|
||||||
|
def find_pin(start_dir):
|
||||||
|
"""Walk up from start_dir; return login from the first
|
||||||
|
.claude/settings.local.json carrying a non-empty env.GITEA_LOGIN."""
|
||||||
|
d = os.path.abspath(start_dir or ".")
|
||||||
|
while True:
|
||||||
|
p = os.path.join(d, ".claude", "settings.local.json")
|
||||||
|
if os.path.isfile(p):
|
||||||
|
try:
|
||||||
|
with open(p) as f:
|
||||||
|
v = (json.load(f).get("env") or {}).get("GITEA_LOGIN")
|
||||||
|
if isinstance(v, str) and v.strip():
|
||||||
|
return v.strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def parse_key(key):
|
||||||
|
"""Return (number, repo-or-None) from 42 / #42 / owner/repo#42 / URL."""
|
||||||
|
key = key.strip()
|
||||||
|
m = re.match(r'^https?://[^/]+/([^/]+)/([^/]+)/issues/(\d+)/?$', key)
|
||||||
|
if m:
|
||||||
|
return int(m.group(3)), "%s/%s" % (m.group(1), m.group(2))
|
||||||
|
m = re.match(r'^([\w.-]+/[\w.-]+)#(\d+)$', key)
|
||||||
|
if m:
|
||||||
|
return int(m.group(2)), m.group(1)
|
||||||
|
m = re.match(r'^#?(\d+)$', key)
|
||||||
|
if m:
|
||||||
|
return int(m.group(1)), None
|
||||||
|
die("cannot parse issue key %r (want 42, #42, owner/repo#42, or an issue URL)" % key)
|
||||||
|
|
||||||
|
|
||||||
|
def tea_api(login, endpoint):
|
||||||
|
"""GET via `tea api`, return parsed JSON."""
|
||||||
|
cmd = ["tea", "api", "--login", login, endpoint]
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if r.returncode != 0:
|
||||||
|
die("`tea api %s` failed:\n%s" % (endpoint, (r.stderr or r.stdout).strip()))
|
||||||
|
try:
|
||||||
|
return json.loads(r.stdout)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
die("`tea api %s` returned non-JSON:\n%s" % (endpoint, r.stdout[:500]))
|
||||||
|
|
||||||
|
|
||||||
|
def day(iso):
|
||||||
|
return (iso or "")[:10]
|
||||||
|
|
||||||
|
|
||||||
|
def issue_markdown(iss):
|
||||||
|
labels = ", ".join(l.get("name", "") for l in iss.get("labels") or []) or "none"
|
||||||
|
assignees = ", ".join(a.get("login", "") for a in iss.get("assignees") or []) or "none"
|
||||||
|
milestone = (iss.get("milestone") or {}).get("title") or "none"
|
||||||
|
lines = [
|
||||||
|
"#%d %s" % (iss["number"], iss.get("title", "")),
|
||||||
|
"state: %s" % iss.get("state", ""),
|
||||||
|
"labels: %s" % labels,
|
||||||
|
"author: %s" % (iss.get("user") or {}).get("login", ""),
|
||||||
|
"assignees: %s" % assignees,
|
||||||
|
"milestone: %s" % milestone,
|
||||||
|
"created: %s" % iss.get("created_at", ""),
|
||||||
|
"updated: %s" % iss.get("updated_at", ""),
|
||||||
|
"url: %s" % iss.get("html_url", ""),
|
||||||
|
"comments: %d" % iss.get("comments", 0),
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
iss.get("body") or "(no body)",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def comment_markdown(c):
|
||||||
|
lines = [
|
||||||
|
"comment-id: %d" % c["id"],
|
||||||
|
"author: %s" % (c.get("user") or {}).get("login", ""),
|
||||||
|
"created: %s" % c.get("created_at", ""),
|
||||||
|
"updated: %s" % c.get("updated_at", ""),
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
c.get("body") or "(empty)",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="Fetch a Gitea issue + comments to tmp/issue/<n>/")
|
||||||
|
ap.add_argument("key", help="issue key: 42, #42, owner/repo#42, or issue URL")
|
||||||
|
ap.add_argument("--repo", help="owner/repo (default: auto-detect from CWD git remote)")
|
||||||
|
ap.add_argument("--out", default=os.path.join("tmp", "issue"),
|
||||||
|
help="output root (default: tmp/issue)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
number, key_repo = parse_key(args.key)
|
||||||
|
repo = args.repo or key_repo # None -> let tea fill {owner}/{repo} from CWD
|
||||||
|
base = "repos/%s" % repo if repo else "repos/{owner}/{repo}"
|
||||||
|
|
||||||
|
login = find_pin(os.getcwd())
|
||||||
|
if not login:
|
||||||
|
die("no login pinned (.claude/settings.local.json env.GITEA_LOGIN). Run /tea:auth.")
|
||||||
|
|
||||||
|
iss = tea_api(login, "%s/issues/%d" % (base, number))
|
||||||
|
|
||||||
|
comments = []
|
||||||
|
page = 1
|
||||||
|
while page <= 40:
|
||||||
|
batch = tea_api(login, "%s/issues/%d/comments?page=%d&limit=50" % (base, number, page))
|
||||||
|
if not isinstance(batch, list) or not batch:
|
||||||
|
break
|
||||||
|
comments.extend(batch)
|
||||||
|
if len(batch) < 50:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
root = os.path.join(args.out, str(number))
|
||||||
|
cdir = os.path.join(root, "comments")
|
||||||
|
shutil.rmtree(cdir, ignore_errors=True) # drop stale comments from earlier fetches
|
||||||
|
os.makedirs(cdir, exist_ok=True)
|
||||||
|
|
||||||
|
data_path = os.path.join(root, "data")
|
||||||
|
with open(data_path, "w") as f:
|
||||||
|
f.write(issue_markdown(iss))
|
||||||
|
|
||||||
|
index = []
|
||||||
|
for i, c in enumerate(comments, 1):
|
||||||
|
name = "%03d-%d.md" % (i, c["id"])
|
||||||
|
with open(os.path.join(cdir, name), "w") as f:
|
||||||
|
f.write(comment_markdown(c))
|
||||||
|
index.append((name, (c.get("user") or {}).get("login", ""), day(c.get("created_at"))))
|
||||||
|
|
||||||
|
# Compact index — the only thing that lands in the model's context.
|
||||||
|
labels = ", ".join(l.get("name", "") for l in iss.get("labels") or []) or "no labels"
|
||||||
|
print("#%d %s [%s] %s — %s, updated %s" % (
|
||||||
|
iss["number"], iss.get("title", ""), iss.get("state", ""), labels,
|
||||||
|
(iss.get("user") or {}).get("login", ""), day(iss.get("updated_at"))))
|
||||||
|
print(data_path)
|
||||||
|
print("comments: %d" % len(comments))
|
||||||
|
for name, author, created in index:
|
||||||
|
print("%s %s %s" % (os.path.join(cdir, name), author, created))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user