feat: get_categories tool, author resolution in get_activities, update CLAUDE.md (v0.4.8)

- get_categories(list_id): new tool filtering taskcategorysync by
  sortingIndexByTaskList, returns {id, name, emoji} ordered by sort index
- get_activities: author IDs now resolved to display names (firstName) via
  famlistfamily members; raw author_id preserved as separate field; member
  lookup is non-fatal (falls back to raw ID on error)
- CLAUDE.md: tools table updated to reflect all implemented tools (v0.4.8)
- SPEC.md: full accgetallfamily response structure documented including
  categories[] on tasks and category parameter investigation findings
- Category assignment in create/update task: all tested parameter names
  ignored by API; still to verify (Service Worker blocks web inspection)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 17:04:35 +02:00
parent 70b1f73079
commit ffb8b062c8
5 changed files with 118 additions and 19 deletions
+77 -1
View File
@@ -331,6 +331,67 @@ def get_tasks(list_id: str, only_open: bool = True):
return json.dumps(result, ensure_ascii=False, indent=2)
# ---------------------------------------------------------------------------
# Tool: get_categories
# ---------------------------------------------------------------------------
@mcp.tool()
def get_categories(list_id: str) -> str:
"""Return the task categories available for a list as JSON.
Categories are loaded from taskcategorysync (part of accgetallfamily) and
filtered to those assigned to the given list via sortingIndexByTaskList.
Shopping lists have predefined system categories (aisles); TODO lists
typically only carry a single default system category.
Note: the parameter name for assigning a category when creating or
updating a task is not yet verified via FW_DEBUG=1. See SPEC.md.
Args:
list_id: List ID from get_lists (e.g. ``taskList/23431854_29740942``).
Returns:
JSON list of {id, name, emoji} objects ordered by sortingIndex.
"""
try:
data = _accgetallfamily()
except RuntimeError as exc:
return f"Error: {exc}"
try:
raw_cats = data["a01"]["r"]["r"]["updatedCreated"]
if not isinstance(raw_cats, list):
raise TypeError("a01.r.r.updatedCreated is not a list")
except (KeyError, TypeError):
return json.dumps(
{"warning": "Unexpected taskcategorysync response structure", "raw": data.get("a01")},
ensure_ascii=False,
indent=2,
)
# Collect categories assigned to this list, ordered by their sorting index.
matched: list[tuple[int, dict[str, Any]]] = []
seen: set[str] = set()
for cat in raw_cats:
sorting = cat.get("sortingIndexByTaskList") or {}
if list_id not in sorting:
continue
cat_id: str = cat.get("metaId", "")
if cat_id in seen:
continue
seen.add(cat_id)
sort_val = int(sorting.get(list_id, cat.get("initialSortingIndex", 0)) or 0)
matched.append((sort_val, cat))
matched.sort(key=lambda t: t[0])
result = [
{"id": cat.get("metaId"), "name": cat.get("name"), "emoji": cat.get("emoji")}
for _, cat in matched
]
return json.dumps(result, ensure_ascii=False, indent=2)
# ---------------------------------------------------------------------------
# Tool: get_activities
# ---------------------------------------------------------------------------
@@ -344,6 +405,19 @@ def get_activities(limit: int = 20):
except RuntimeError as exc:
return f"Error: {exc}"
# Load member data to resolve author IDs to display names.
# Non-fatal: fall back to raw account IDs when member lookup fails.
author_map: dict[str, str] = {}
try:
for circle in _famlistfamily():
for member in circle.get("members") or []:
acc_id: str = member.get("accountId", "")
display = member.get("firstName") or member.get("name") or acc_id
if acc_id:
author_map[acc_id] = display
except RuntimeError:
pass
try:
with FamilyWallClient() as client:
client.login(email, password)
@@ -375,13 +449,15 @@ def get_activities(limit: int = 20):
result = []
for item in raw_activities:
raw_author: str = item.get("accountId", "")
result.append(
{
"id": item.get("metaId"),
"type": item.get("refType"),
"text": item.get("text"),
"date": item.get("creationDate"),
"author": item.get("accountId"),
"author": author_map.get(raw_author, raw_author),
"author_id": raw_author,
}
)