# retoor from __future__ import annotations from .spec import Action, Catalog, Param def path(name: str, description: str, required: bool = True) -> Param: return Param(name=name, location="path", description=description, required=required) def query(name: str, description: str, required: bool = False) -> Param: return Param( name=name, location="query", description=description, required=required ) def body(name: str, description: str, required: bool = False) -> Param: return Param(name=name, location="body", description=description, required=required) def upload(name: str, description: str) -> Param: return Param(name=name, location="file", description=description, required=True) def confirm() -> Param: return Param( name="confirm", location="body", description=( "Set to true ONLY after the user has explicitly confirmed this irreversible action. " "Leave it unset on the first call: the tool refuses and tells you to confirm with the " "user, and only a repeat call with confirm=true actually performs it." ), required=False, type="boolean", ) ATTACHMENTS = "Comma separated attachment uids returned by upload_file." TARGET_TYPE = "Target type, e.g. post, comment, project, gist." ACTIONS: tuple[Action, ...] = ( Action( name="auth_status", method="GET", path="", summary="Report whether the current session is authenticated", handler="status", requires_auth=False, ), Action( name="login", method="POST", path="/auth/login", summary="Authenticate with email and password and start a session", handler="login", requires_auth=False, params=( body("email", "Account email address.", required=True), body("password", "Account password.", required=True), body("remember_me", "Keep the session alive longer ('on' or '')."), ), ), Action( name="logout", method="GET", path="/auth/logout", summary="End the current session", handler="logout", requires_auth=True, ), Action( name="signup", method="POST", path="/auth/signup", summary="Create a new account", requires_auth=False, params=( body("username", "Desired username.", required=True), body("email", "Email address.", required=True), body("password", "Password (minimum six characters).", required=True), body("confirm_password", "Password confirmation.", required=True), ), ), Action( name="forgot_password", method="POST", path="/auth/forgot-password", summary="Request a password reset email", requires_auth=False, params=(body("email", "Account email address.", required=True),), ), Action( name="reset_password", method="POST", path="/auth/reset-password/{token}", summary="Reset a password using a reset token", requires_auth=False, params=( path("token", "Reset token from the email link."), body("password", "New password.", required=True), body("confirm_password", "New password confirmation.", required=True), ), ), Action( name="view_feed", method="GET", path="/feed", summary="View the activity feed", requires_auth=False, params=( query("tab", "Feed tab to view."), query("topic", "Filter by topic."), query("search", "Search post title and content."), query("before", "Pagination cursor."), ), ), Action( name="create_post", method="POST", path="/posts/create", summary="Create a new post", params=( body("content", "Post body text.", required=True), body("title", "Optional post title."), body("topic", "Optional topic."), body("project_uid", "Attach the post to a project uid."), body("attachment_uids", ATTACHMENTS), body("poll_question", "Optional poll question."), body( "poll_options", "Poll options as a JSON array of strings, or one option per line, or comma separated. At least two are required for the poll to be created.", ), ), ), Action( name="view_post", method="GET", path="/posts/{post_slug}", summary="View a single post by slug", requires_auth=False, params=( path( "post_slug", "Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title.", ), ), ), Action( name="edit_post", method="POST", path="/posts/edit/{post_slug}", summary="Edit an existing post", params=( path( "post_slug", "Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title.", ), body("content", "Updated post body.", required=True), body("title", "Updated title."), body("topic", "Updated topic."), body( "poll_question", "Optional poll question. Adds a poll to a post that does not already have one.", ), body( "poll_options", "Poll options as a JSON array of strings, or one option per line, or comma separated. At least two are required for the poll to be created.", ), ), ), Action( name="delete_post", method="POST", path="/posts/delete/{post_slug}", summary="Delete a post (your own, or any post when you are an administrator). Soft delete, confirmation required", params=( path( "post_slug", "Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title.", ), confirm(), ), ), Action( name="create_comment", method="POST", path="/comments/create", summary="Create a comment on a post or other target", params=( body("content", "Comment body.", required=True), body("post_uid", "Uid of the post being commented on."), body("target_uid", "Uid of the target when not a post."), body("target_type", TARGET_TYPE), body("parent_uid", "Parent comment uid for replies."), body("attachment_uids", ATTACHMENTS), ), ), Action( name="delete_comment", method="POST", path="/comments/delete/{comment_uid}", summary="Delete a comment (your own, or any comment when you are an administrator). Soft delete, confirmation required", params=(path("comment_uid", "Uid of the comment."), confirm()), ), Action( name="list_projects", method="GET", path="/projects", summary="List projects", requires_auth=False, params=( query("tab", "Projects tab."), query("search", "Search query."), query("user_uid", "Filter by owner uid."), query("project_type", "Filter by project type."), query("before", "Pagination cursor."), ), ), Action( name="view_project", method="GET", path="/projects/{project_slug}", summary="View a project by slug", requires_auth=False, params=( path( "project_slug", "Exact project slug copied from a /projects/... link in a listing response; do not build it from the title.", ), ), ), Action( name="create_project", method="POST", path="/projects/create", summary="Create a new project", params=( body("title", "Project title.", required=True), body("description", "Project description.", required=True), body("release_date", "Release date."), body("demo_date", "Demo date."), body("project_type", "Project type."), body("platforms", "Supported platforms."), body("status", "Project status."), body("attachment_uids", ATTACHMENTS), ), ), Action( name="edit_project", method="POST", path="/projects/edit/{project_slug}", summary="Edit an existing project", params=( path( "project_slug", "Exact project slug copied from a /projects/... link in a listing response; do not build it from the title.", ), body("title", "Updated project title.", required=True), body("description", "Updated project description.", required=True), body("release_date", "Updated release date."), body("demo_date", "Updated demo date."), body("project_type", "Updated project type."), body("platforms", "Updated supported platforms."), body("status", "Updated project status."), ), ), Action( name="delete_project", method="POST", path="/projects/delete/{project_slug}", summary="Delete a project and all its files (your own, or any project when you are an administrator). Soft delete, confirmation required", params=( path( "project_slug", "Exact project slug copied from a /projects/... link in a listing response; do not build it from the title.", ), confirm(), ), ), Action( name="project_set_private", method="POST", path="/projects/{project_slug}/private", summary="Mark a project private (owner-only) or public", description=( "Set value=true to hide the project from everyone except its owner (and administrators), " "or value=false to make it public again. This is a significant change: you MUST ask the " "user for explicit confirmation BEFORE calling it, and only pass confirm=true once they " "have agreed. Only the project owner may change this." ), params=( path("project_slug", "Project slug or uid."), body( "value", "true to make the project private, false to make it public.", required=True, ), body( "confirm", "Must be true, set only after the user has explicitly confirmed.", required=True, ), ), ), Action( name="project_set_readonly", method="POST", path="/projects/{project_slug}/readonly", summary="Mark a project read-only (immutable files) or writable again", description=( "Set value=true to make every file in the project immutable - no writes, edits, line " "edits, moves, deletes, or uploads succeed afterwards, from anyone including you - or " "value=false to allow changes again. This is a significant change: you MUST ask the user " "for explicit confirmation BEFORE calling it, and only pass confirm=true once they have " "agreed. Only the project owner may change this." ), params=( path("project_slug", "Project slug or uid."), body( "value", "true to make the project read-only, false to make it writable.", required=True, ), body( "confirm", "Must be true, set only after the user has explicitly confirmed.", required=True, ), ), ), Action( name="project_list_files", method="GET", path="/projects/{project_slug}/files", summary="List every file and directory in a project filesystem", description="Returns the flat list of nodes (path, type, size, mime). Use it to inspect the project tree before reading or writing files.", params=(path("project_slug", "Project slug or uid."),), requires_auth=False, ), Action( name="project_read_file", method="GET", path="/projects/{project_slug}/files/raw", summary="Read one file from a project filesystem", description="Returns the file metadata plus the text content. Binary files return a url instead of content.", params=( path("project_slug", "Project slug or uid."), query( "path", "Relative file path inside the project, e.g. src/main.py.", required=True, ), ), requires_auth=False, ), Action( name="project_write_file", method="POST", path="/projects/{project_slug}/files/write", summary="Create or overwrite a whole text file in a project (parent directories are created automatically)", description=( "Replaces the ENTIRE file with the content you send. Creating a new file needs no prior " "read, but overwriting an existing one requires reading it first (project_read_file). " "For an existing file prefer the surgical line tools (project_replace_lines, " "project_insert_lines, project_delete_lines, project_append_file) instead of rewriting it, " "and never batch several writes in one turn. Use this only to create a new file or fully " "rewrite a small one. Missing parent directories are created recursively." ), params=( path("project_slug", "Project slug or uid."), body("path", "Relative file path, e.g. src/app/main.py.", required=True), body("content", "Full file content.", required=True), ), ), Action( name="project_read_lines", method="GET", path="/projects/{project_slug}/files/lines", summary="Read a 1-indexed line range of a text file in a project", description=( "Returns {path, start, end, total_lines, lines, content} for the requested range. Use it " "to inspect part of a large file before editing, and to learn total_lines so you can target " "the right range with the line-edit tools." ), params=( path("project_slug", "Project slug or uid."), query("path", "Relative file path inside the project.", required=True), query("start", "First line to read (1-indexed, default 1)."), query("end", "Last line to read (inclusive); omit for end of file."), ), requires_auth=False, ), Action( name="project_replace_lines", method="POST", path="/projects/{project_slug}/files/replace-lines", summary="Replace an inclusive 1-indexed line range of a text file with new content", description=( "Surgically rewrites lines start..end (inclusive) with content (which may be any number of " "lines, or empty to delete the range). The preferred way to edit an existing file: it leaves " "the rest of the file untouched and never overflows the model output limit." ), params=( path("project_slug", "Project slug or uid."), body("path", "Relative file path.", required=True), body("start", "First line to replace (1-indexed).", required=True), body("end", "Last line to replace (inclusive).", required=True), body("content", "Replacement text for those lines (empty deletes them)."), ), ), Action( name="project_insert_lines", method="POST", path="/projects/{project_slug}/files/insert-lines", summary="Insert content before a 1-indexed line of a text file", description=( "Inserts content before line 'at' without touching existing lines. Use at=1 to prepend and " "at=total_lines+1 to insert at the end." ), params=( path("project_slug", "Project slug or uid."), body("path", "Relative file path.", required=True), body( "at", "Insert before this 1-indexed line (1 prepends, total+1 appends).", required=True, ), body("content", "Text to insert.", required=True), ), ), Action( name="project_delete_lines", method="POST", path="/projects/{project_slug}/files/delete-lines", summary="Delete an inclusive 1-indexed line range from a text file", params=( path("project_slug", "Project slug or uid."), body("path", "Relative file path.", required=True), body("start", "First line to delete (1-indexed).", required=True), body("end", "Last line to delete (inclusive).", required=True), ), ), Action( name="project_append_file", method="POST", path="/projects/{project_slug}/files/append", summary="Append content to the end of a text file in a project", description="Adds content as new lines at the end of the file. Use it to grow a large file across turns without resending the whole thing.", params=( path("project_slug", "Project slug or uid."), body("path", "Relative file path.", required=True), body("content", "Text to append.", required=True), ), ), Action( name="project_upload_file", method="POST", path="/projects/{project_slug}/files/upload", summary="Upload a local file into a project directory (parents created automatically)", params=( path("project_slug", "Project slug or uid."), upload("file", "Local filesystem path of the file to upload."), body("path", "Target directory inside the project, empty for the root."), ), ), Action( name="project_make_dir", method="POST", path="/projects/{project_slug}/files/mkdir", summary="Create a directory (and parents) in a project filesystem", params=( path("project_slug", "Project slug or uid."), body( "path", "Relative directory path, e.g. src/components.", required=True ), ), ), Action( name="project_move_file", method="POST", path="/projects/{project_slug}/files/move", summary="Move or rename a file or directory within a project", params=( path("project_slug", "Project slug or uid."), body("from_path", "Existing path.", required=True), body("to_path", "New path.", required=True), ), ), Action( name="project_delete_file", method="POST", path="/projects/{project_slug}/files/delete", summary="Delete a file or directory (recursive) from a project filesystem (project owner, or any project when you are an administrator). Soft delete, confirmation required", params=( path("project_slug", "Project slug or uid."), body("path", "Relative path to delete.", required=True), confirm(), ), ), Action( name="zip_project", method="POST", path="/projects/{project_slug}/zip", summary="Build a downloadable zip archive of a whole project", description=( "Queues a background zip job and returns {uid, status_url}. Poll the status_url with " "zip_status until status is 'done', then give the user the download_url." ), params=(path("project_slug", "Project slug or uid."),), requires_auth=False, ), Action( name="project_files_zip", method="POST", path="/projects/{project_slug}/files/zip", summary="Build a downloadable zip archive of a project subdirectory", description=( "Queues a background zip job for a subpath inside the project and returns " "{uid, status_url}. Poll the status_url with zip_status until status is 'done', " "then give the user the download_url. When path is empty the whole project is zipped." ), params=( path("project_slug", "Project slug or uid."), query("path", "Relative subpath inside the project to zip; empty for the whole project."), ), requires_auth=False, ), Action( name="zip_status", method="GET", path="/zips/{uid}", summary="Check a zip job and obtain its download link once finished", description=( "Returns the job status and stats. When status is 'done', download_url points at the " "ready archive; while 'pending' or 'running', poll again shortly." ), params=(path("uid", "Zip job uid returned by zip_project."),), requires_auth=False, ), Action( name="fork_project", method="POST", path="/projects/{project_slug}/fork", summary="Fork a project into a new project owned by the current user", description=( "Queues a background fork job and returns {uid, status_url}. Poll the status_url with " "fork_status until status is 'done', then give the user the project_url of the new fork." ), params=( path("project_slug", "Slug or uid of the project to fork."), body("title", "Title for the new forked project.", required=True), ), requires_auth=True, ), Action( name="fork_status", method="GET", path="/forks/{uid}", summary="Check a fork job and obtain the new project once finished", description=( "Returns the job status. When status is 'done', project_url points at the new forked " "project; while 'pending' or 'running', poll again shortly." ), params=(path("uid", "Fork job uid returned by fork_project."),), requires_auth=False, ), Action( name="seo_diagnostics", method="POST", path="/tools/seo/run", summary="Run an SEO audit on a URL or sitemap", description=( "Queues a background SEO diagnostics job and returns {uid, status_url}. Poll the " "status_url with seo_status until status is 'done', then share the score, grade and " "report_url. mode='url' audits a single page; mode='sitemap' crawls up to max_pages." ), params=( body("url", "The page URL or sitemap.xml URL to audit.", required=True), body("mode", "'url' for a single page or 'sitemap' to crawl a sitemap."), body("max_pages", "Maximum pages to crawl in sitemap mode (1-50)."), ), requires_auth=False, ), Action( name="seo_status", method="GET", path="/tools/seo/{uid}", summary="Check an SEO audit and obtain its score once finished", description=( "Returns the audit status. When status is 'done', score, grade and report_url are " "populated; while 'pending' or 'running', poll again shortly." ), params=(path("uid", "SEO job uid returned by seo_diagnostics."),), requires_auth=False, ), Action( name="deepsearch", method="POST", path="/tools/deepsearch/run", summary="Start a deep multi-agent web research job", description=( "Queues a background DeepSearch job and returns {uid, status_url}. Poll the " "status_url with deepsearch_status until status is 'done', then share the " "score, confidence and session_url. depth (1-4) and max_pages (1-30) control " "how widely it crawls." ), params=( body("query", "The research question to investigate.", required=True), body("depth", "Research depth 1-4 (default 2)."), body("max_pages", "Maximum sources to crawl 1-30 (default 12)."), ), requires_auth=False, ), Action( name="deepsearch_status", method="GET", path="/tools/deepsearch/{uid}", summary="Check a DeepSearch job and obtain its metrics once finished", description=( "Returns the research status. When status is 'done', score, confidence, " "source_diversity and session_url are populated; otherwise poll again shortly." ), params=(path("uid", "DeepSearch job uid returned by deepsearch."),), requires_auth=False, ), Action( name="deepsearch_session", method="GET", path="/tools/deepsearch/{uid}/session", summary="Read a finished DeepSearch report", description=( "Returns the full cited report for a finished DeepSearch: summary, findings, " "gaps, sources and metrics." ), params=(path("uid", "DeepSearch job uid returned by deepsearch."),), requires_auth=False, ), Action( name="search_users", method="GET", path="/profile/search", summary="Search for users by name", params=(query("q", "Search query."),), ), Action( name="view_profile", method="GET", path="/profile/{username}", summary="View a user profile", requires_auth=False, params=( path("username", "Username to view."), query("tab", "Profile tab."), ), ), Action( name="update_profile", method="POST", path="/profile/update", summary="Update the current user's profile", params=( body("bio", "Profile biography."), body("location", "Location."), body("git_link", "Git profile link."), body("website", "Personal website."), ), ), Action( name="regenerate_api_key", method="POST", path="/profile/regenerate-api-key", summary="Issue a new API key and invalidate the current one", description=( "Returns the new api_key. Warning: this immediately invalidates any key currently " "used for authentication, so confirm with the user before calling it." ), params=( body( "confirm", "Must be true, set only after the user has explicitly confirmed.", required=True, ), ), ), Action( name="list_messages", method="GET", path="/messages", summary="View direct message conversations", params=( query("with_uid", "Open a conversation with a user uid."), query("search", "Search conversations."), ), ), Action( name="search_message_users", method="GET", path="/messages/search", summary="Search users to message", params=(query("q", "Search query."),), ), Action( name="send_message", method="POST", path="/messages/send", summary="Send a direct message", params=( body("content", "Message body.", required=True), body("receiver_uid", "Recipient user uid.", required=True), body("attachment_uids", ATTACHMENTS), ), ), Action( name="list_notifications", method="GET", path="/notifications", summary="List notifications", params=(query("before", "Pagination cursor."),), ), Action( name="notification_counts", method="GET", path="/notifications/counts", summary="Get unread notification and message counts", requires_auth=False, ), Action( name="open_notification", method="GET", path="/notifications/open/{notification_uid}", summary="Open a notification and follow it", params=(path("notification_uid", "Notification uid."),), ), Action( name="mark_notification_read", method="POST", path="/notifications/mark-read/{notification_uid}", summary="Mark a single notification read", params=(path("notification_uid", "Notification uid."),), ), Action( name="mark_all_notifications_read", method="POST", path="/notifications/mark-all-read", summary="Mark all notifications read", ), Action( name="vote", method="POST", path="/votes/{target_type}/{target_uid}", summary="Cast or toggle a vote on a target", description="Re-sending the same value removes the vote. Returns the net/up/down tally.", ajax=True, params=( path("target_type", TARGET_TYPE), path( "target_uid", "Uid of the target, copied from a listing response; do not invent it.", ), body( "value", "Vote value: 1 to upvote, -1 to downvote (re-send to remove).", required=True, ), ), ), Action( name="react", method="POST", path="/reactions/{target_type}/{target_uid}", summary="Toggle an emoji reaction on a target", description="Re-sending the same emoji removes it. Returns reaction counts.", ajax=True, params=( path("target_type", TARGET_TYPE), path( "target_uid", "Uid of the target, copied from a listing response; do not invent it.", ), body("emoji", "One of the allowed reaction emoji.", required=True), ), ), Action( name="list_bookmarks", method="GET", path="/bookmarks/saved", summary="List saved bookmarks", params=(query("before", "Pagination cursor."),), ), Action( name="toggle_bookmark", method="POST", path="/bookmarks/{target_type}/{target_uid}", summary="Toggle a bookmark on a target", description="Re-sending removes the bookmark. Returns {saved: true|false}.", ajax=True, params=( path("target_type", TARGET_TYPE), path( "target_uid", "Uid of the target, copied from a listing response; do not invent it.", ), ), ), Action( name="vote_poll", method="POST", path="/polls/{poll_uid}/vote", summary="Vote in a poll", description="Casts or changes your vote on a poll option. Returns the option tallies.", ajax=True, params=( path("poll_uid", "Poll uid."), body("option_uid", "Chosen option uid.", required=True), ), ), Action( name="follow_user", method="POST", path="/follow/{username}", summary="Follow a user", params=(path("username", "Username to follow."),), ), Action( name="unfollow_user", method="POST", path="/follow/unfollow/{username}", summary="Unfollow a user", params=(path("username", "Username to unfollow."),), ), Action( name="list_followers", method="GET", path="/profile/{username}/followers", summary="List the users who follow a profile", requires_auth=False, params=( path("username", "Username whose followers to list."), query("page", "Page number, 25 per page."), ), ), Action( name="list_following", method="GET", path="/profile/{username}/following", summary="List the users a profile follows", requires_auth=False, params=( path("username", "Username whose following to list."), query("page", "Page number, 25 per page."), ), ), Action( name="list_media", method="GET", path="/profile/{username}", summary="List a user's uploaded media, newest first", requires_auth=False, params=( path("username", "Username whose media to list."), query("tab", "Profile tab (defaults to media for this action)."), query("page", "Page number, 24 per page."), ), ), Action( name="delete_media", method="POST", path="/media/{uid}/delete", summary="Delete an uploaded media attachment (your own, or anyone's when you are an administrator)", description=( "Removes the attachment from the user's Media tab and from any post, project, or " "gist where it was attached. Soft delete (restorable from admin trash). Show the user " "the exact media, get explicit confirmation, then call again with confirm=true." ), requires_auth=True, params=(path("uid", "Attachment uid to delete."), confirm()), ), Action( name="view_leaderboard", method="GET", path="/leaderboard", summary="View the leaderboard", requires_auth=False, ), Action( name="list_issues", method="GET", path="/issues", summary="List issue tickets from the Gitea tracker", requires_auth=False, params=( query("state", "Filter by state: open, closed, or all."), query("page", "1-based page number."), ), ), Action( name="create_issue", method="POST", path="/issues/create", summary="Report an issue. It is rewritten by AI and filed on the tracker.", description=( "Queues a background issue creation job and returns {uid, status_url}. Poll the " "status_url with issue_job_status until status is 'done', then show the user the " "issue_url pointing at the filed ticket." ), params=( body("title", "Issue title.", required=True), body("description", "Issue description.", required=True), ), ), Action( name="issue_job_status", method="GET", path="/issues/jobs/{uid}", summary="Poll an issue creation job and obtain the filed issue ticket URL once finished", description=( "Returns the job status. When status is 'done', issue_url and number are populated; " "while 'pending' or 'running', poll again shortly." ), params=(path("uid", "Issue job uid returned by create_issue."),), requires_auth=False, ), Action( name="view_issue", method="GET", path="/issues/{number}", summary="View an issue ticket and its developer updates", requires_auth=False, params=(path("number", "The issue ticket number from a /issues listing."),), ), Action( name="comment_issue", method="POST", path="/issues/{number}/comment", summary="Post a comment on an issue ticket, attributed to the current user", params=( path("number", "The issue ticket number."), body("body", "Comment text.", required=True), ), ), Action( name="set_issue_status", method="POST", path="/issues/{number}/status", summary="Change an issue ticket status (admin only)", requires_admin=True, params=( path("number", "The issue ticket number."), body("status", "New status: open or closed.", required=True), ), ), Action( name="list_gists", method="GET", path="/gists", summary="List gists", requires_auth=False, params=( query("language", "Filter by language."), query("user_uid", "Filter by owner uid."), query("search", "Search gist title and description."), query("before", "Pagination cursor."), ), ), Action( name="view_gist", method="GET", path="/gists/{gist_slug}", summary="View a gist by slug", requires_auth=False, params=( path( "gist_slug", "Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.", ), ), ), Action( name="create_gist", method="POST", path="/gists/create", summary="Create a gist", params=( body("title", "Gist title.", required=True), body("source_code", "Gist source code.", required=True), body("description", "Gist description."), body("language", "Programming language."), body("attachment_uids", ATTACHMENTS), ), ), Action( name="edit_gist", method="POST", path="/gists/edit/{gist_slug}", summary="Edit a gist", params=( path( "gist_slug", "Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.", ), body("title", "Gist title.", required=True), body("source_code", "Gist source code.", required=True), body("description", "Gist description."), body("language", "Programming language."), ), ), Action( name="delete_gist", method="POST", path="/gists/delete/{gist_slug}", summary="Delete a gist (your own, or any gist when you are an administrator). Soft delete, confirmation required", params=( path( "gist_slug", "Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.", ), confirm(), ), ), Action( name="list_news", method="GET", path="/news", summary="List news articles", requires_auth=False, params=(query("before", "Pagination cursor."),), ), Action( name="view_news", method="GET", path="/news/{news_slug}", summary="View a news article by slug", requires_auth=False, params=( path( "news_slug", "Exact news slug copied from a /news/... link in a listing response; do not build it from the title.", ), ), ), Action( name="upload_file", method="POST", path="/uploads/upload", summary="Upload a local file and obtain its attachment uid", params=(upload("file", "Local filesystem path of the file to upload."),), ), Action( name="attach_url", method="POST", path="/uploads/upload-url", summary="Download a file from a public URL and store it as an attachment, returning its uid", description=( "Fetches the file at the given URL on the server (size-capped, SSRF-guarded) and stores it " "as an attachment exactly like an upload, returning {uid, url, mime_type, ...}. Pass the " "returned uid in attachment_uids when you create_post, create_project, create_gist, " "create_comment, create_issue, or send_message to attach it. The file type is taken from the " "URL or its Content-Type; supply 'filename' (with an allowed extension) when the URL has no " "usable name. Use this to attach an image or file straight from the internet." ), params=( body("url", "Public http(s) URL of the file to download and attach.", required=True), body( "filename", "Optional filename with an allowed extension, used when the URL has no clear name.", ), ), ), Action( name="delete_attachment", method="DELETE", path="/uploads/delete/{attachment_uid}", summary="Delete an uploaded attachment (your own, or anyone's when you are an administrator). Soft delete, confirmation required", params=(path("attachment_uid", "Uid of the attachment."), confirm()), ), Action( name="admin_overview", method="GET", path="/admin", summary="View the admin overview", requires_admin=True, ), Action( name="site_analytics", method="GET", path="/admin/analytics", summary="Site-wide aggregate analytics in one call (admin only)", description=( "Returns JSON: total members, active users in the last 24h/7d/30d, signed_in_now, " "new signups (24h/7d/30d), content totals (posts, comments, gists, projects, " "news), and top authors. Use this for any 'how many'/'how active'/count question " "instead of paging through admin_list_users. signed_in_now counts members holding an " "unexpired session (logged in within the session lifetime, default 7-30 days), NOT a " "real-time online count - see signed_in_definition in the response and never report it " "as 'currently online' or 'logged in right now'." ), params=(query("top_n", "How many top authors to include (1-50)."),), requires_admin=True, ), Action( name="ai_usage", method="GET", path="/admin/ai-usage/data", summary="AI gateway usage, cost, latency, and reliability metrics (admin only)", description=( "Returns JSON for a bounded window: request volume and throughput, token usage with " "averages and percentiles, latency (upstream, gateway overhead, queue wait, connect), " "error rates by category, cost in USD (per model, per caller, input vs output, projected " "monthly burn, caching savings), caller behavior, and an hourly breakdown. Use this for " "any question about AI spend, token consumption, gateway performance, or errors." ), params=( query("hours", "Lookback window in hours (1-168, default 48)."), query("top_n", "How many rows in each top-N breakdown (default 10)."), ), requires_admin=True, ), Action( name="audit_log", method="GET", path="/admin/audit-log", summary="Query the platform audit trail with filters (admin only)", description=( "Returns JSON: a paginated, newest-first list of audit events (every state-changing " "action on the platform, with actor, origin, target, old/new value, and result), plus " "pagination, the active filters, and an 'options' object listing every available value " "for each filter (category, event_key, actor_role, origin, result) so you can both " "filter and discover valid filter values from a single call. Use this for any audit, " "history, 'who did X', 'what changed', or 'show denied/failed actions' question. Filter " "by event_key (exact), category, actor_role, actor_uid, origin (web, api, devii, cli, " "service, scheduler), or result (success, failure, denied); narrow by date_from/date_to " "(ISO date bounds, inclusive) or free-text q over summary, event key, and target." ), params=( query("page", "Page number (1-based)."), query("event_key", "Filter by exact event key (e.g. admin.setting.update)."), query("category", "Filter by category (auth, content, admin, container, ...)."), query("actor_role", "Filter by actor role at action time."), query("actor_uid", "Filter by acting user uid."), query("origin", "Filter by origin (web, api, devii, cli, service, scheduler)."), query("result", "Filter by result (success, failure, denied)."), query("q", "Free-text search over summary, event key, and target."), query("date_from", "ISO date lower bound (inclusive)."), query("date_to", "ISO date upper bound (inclusive)."), ), requires_admin=True, ), Action( name="audit_event", method="GET", path="/admin/audit-log/{uid}", summary="Get one audit event with its related-object links (admin only)", description=( "Returns JSON: the full audit event row plus every related-object link (actor, target, " "parent, project, instance, setting, ...). Use after audit_log to inspect a single event " "in detail." ), params=(path("uid", "Audit event uid."),), requires_admin=True, ), Action( name="admin_list_users", method="GET", path="/admin/users", summary="List users for administration", params=(query("page", "Page number."),), requires_admin=True, ), Action( name="admin_set_user_role", method="POST", path="/admin/users/{uid}/role", summary="Set a user's role", params=( path("uid", "User uid."), body("role", "New role.", required=True), ), requires_admin=True, ), Action( name="admin_set_user_password", method="POST", path="/admin/users/{uid}/password", summary="Set a user's password", params=( path("uid", "User uid."), body("password", "New password.", required=True), ), requires_admin=True, ), Action( name="admin_toggle_user", method="POST", path="/admin/users/{uid}/toggle", summary="Toggle a user's active state", params=(path("uid", "User uid."),), requires_admin=True, ), Action( name="admin_get_settings", method="GET", path="/admin/settings", summary="View site settings", requires_admin=True, ), Action( name="admin_save_settings", method="POST", path="/admin/settings", summary="Save site settings", params=( body("site_name", "Site name."), body("site_description", "Site description."), body("site_tagline", "Site tagline."), body("max_upload_size_mb", "Maximum upload size in megabytes."), body("allowed_file_types", "Allowed file types."), body("max_attachments_per_resource", "Maximum attachments per resource."), body("rate_limit_per_minute", "Rate limit per minute."), body("rate_limit_window_seconds", "Rate limit window in seconds."), body("session_max_age_days", "Session maximum age in days."), body("session_remember_days", "Remember-me duration in days."), body("registration_open", "Whether registration is open."), body("maintenance_mode", "Whether maintenance mode is on."), body("maintenance_message", "Maintenance message."), ), requires_admin=True, ), Action( name="admin_list_news", method="GET", path="/admin/news", summary="List news for administration", params=(query("page", "Page number."),), requires_admin=True, ), Action( name="admin_toggle_news", method="POST", path="/admin/news/{uid}/toggle", summary="Toggle a news article", params=(path("uid", "News uid."),), requires_admin=True, ), Action( name="admin_publish_news", method="POST", path="/admin/news/{uid}/publish", summary="Publish a news article", params=(path("uid", "News uid."),), requires_admin=True, ), Action( name="admin_landing_news", method="POST", path="/admin/news/{uid}/landing", summary="Set a news article as landing content", params=(path("uid", "News uid."),), requires_admin=True, ), Action( name="admin_delete_news", method="POST", path="/admin/news/{uid}/delete", summary="Delete a news article (admin only). Soft delete, confirmation required", params=(path("uid", "News uid."), confirm()), requires_admin=True, ), Action( name="admin_list_services", method="GET", path="/admin/services", summary="View managed services", requires_admin=True, ), Action( name="admin_services_data", method="GET", path="/admin/services/data", summary="Get live status, metrics, and log tail for every background service", requires_admin=True, ), Action( name="admin_service_status", method="GET", path="/admin/services/{name}/data", summary="Get live status, metrics, and log tail for one background service", params=(path("name", "Service name (e.g. news, bots, openai)."),), requires_admin=True, ), Action( name="admin_start_service", method="POST", path="/admin/services/{name}/start", summary="Start a managed service", params=(path("name", "Service name."),), requires_admin=True, ), Action( name="admin_stop_service", method="POST", path="/admin/services/{name}/stop", summary="Stop a managed service", params=(path("name", "Service name."),), requires_admin=True, ), Action( name="admin_run_service", method="POST", path="/admin/services/{name}/run", summary="Run a managed service once", params=(path("name", "Service name."),), requires_admin=True, ), Action( name="admin_clear_service_logs", method="POST", path="/admin/services/{name}/clear-logs", summary="Clear a managed service's logs", params=(path("name", "Service name."),), requires_admin=True, ), Action( name="admin_config_service", method="POST", path="/admin/services/{name}/config", summary="Update a managed service's configuration", params=(path("name", "Service name."),), freeform_body=True, requires_admin=True, ), Action( name="admin_user_ai_usage", method="GET", path="/admin/users/{uid}/ai-usage", summary="Get a user's AI gateway usage (admin only)", description="Returns per-user AI usage data including token counts, cost, and request volume for the given lookback window.", params=( path("uid", "User uid."), query("hours", "Lookback window in hours (default 24)."), ), requires_admin=True, ), Action( name="admin_reset_user_ai_quota", method="POST", path="/admin/users/{uid}/reset-ai-quota", summary="Reset a user's AI quota (admin only)", description="Clears the AI quota ledger for a specific user, allowing them to use AI features again.", params=(path("uid", "User uid."), confirm()), requires_admin=True, ), Action( name="admin_media_purge", method="POST", path="/admin/media/{uid}/purge", summary="Permanently remove soft-deleted media (admin only)", description="Permanently deletes a soft-deleted attachment from the database and filesystem.", params=(path("uid", "Attachment uid to purge."), confirm()), requires_admin=True, ), Action( name="admin_reset_guest_ai_quota", method="POST", path="/admin/ai-quota/reset-guests", summary="Reset all guest AI quotas (admin only)", description="Clears the AI quota ledger for all guest sessions.", params=(confirm(),), requires_admin=True, ), Action( name="admin_reset_all_ai_quota", method="POST", path="/admin/ai-quota/reset-all", summary="Reset ALL AI quotas including member quotas (admin only)", description="Clears the AI quota ledger for every user and guest. Use with caution.", params=(confirm(),), requires_admin=True, ), Action( name="restore_media", method="POST", path="/media/{uid}/restore", summary="Restore a soft-deleted media attachment (admin only)", description="Restores a previously soft-deleted attachment, making it visible again.", params=(path("uid", "Attachment uid to restore."),), requires_admin=True, ), ) PLATFORM_CATALOG = Catalog(actions=ACTIONS)