One module per API area (base, files, activity, skills, conversations, notifications, forms, search, plans, projects, services, misc, diff, cron, agents) on a shared base.Schema; __init__.py re-exports every model so handlers keep writing schemas.<Name>. OpenAPI output is byte-identical. PlanSummary lives in conversations.py to keep the module graph a DAG. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
263 lines
7.3 KiB
Python
263 lines
7.3 KiB
Python
"""Projects, their GOAL.md (+ the goals board at ``/api/goals``) and the
|
|
scaffold templates."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Literal, Optional
|
|
|
|
from .base import AccountSplit, Schema
|
|
|
|
__all__ = [
|
|
"ProjectCostSummary",
|
|
"TokenTypeSpend",
|
|
"ProjectByType",
|
|
"ProjectCostDetail",
|
|
"GoalSummary",
|
|
"GoalChecklistItem",
|
|
"GoalClaim",
|
|
"GoalDetail",
|
|
"GoalMilestone",
|
|
"GoalTheme",
|
|
"RunningConversation",
|
|
"GoalBoardEntry",
|
|
"GoalsResponse",
|
|
"ProjectSummary",
|
|
"IndexMeta",
|
|
"OgImage",
|
|
"KeyFile",
|
|
"ProjectCommit",
|
|
"ProjectDetail",
|
|
"ProjectsResponse",
|
|
"ProjectEnvVar",
|
|
"ProjectEnvResponse",
|
|
"TemplateSummary",
|
|
"TemplateTreeNode",
|
|
"TemplateDetail",
|
|
"TemplatesResponse",
|
|
]
|
|
|
|
|
|
# ── projects ──────────────────────────────────────────────────────────────────
|
|
class ProjectCostSummary(Schema):
|
|
cost: float
|
|
tokens: int
|
|
conversations: int
|
|
# The Claude account owning the project (orus-monorepo → "work") and
|
|
# whether that account is left out of aggregated totals. For such a project
|
|
# the figures above are that account's spend even in the default view.
|
|
account: Optional[str] = None
|
|
excludeFromTotals: Optional[bool] = None
|
|
byAccount: Optional[dict[str, AccountSplit]] = None
|
|
|
|
|
|
class TokenTypeSpend(Schema):
|
|
tokens: int
|
|
cost: float
|
|
|
|
|
|
class ProjectByType(Schema):
|
|
input: TokenTypeSpend
|
|
output: TokenTypeSpend
|
|
cacheRead: TokenTypeSpend
|
|
cacheWrite: TokenTypeSpend
|
|
|
|
|
|
class ProjectCostDetail(ProjectCostSummary):
|
|
meanCost: float
|
|
medianCost: float
|
|
meanTokens: float
|
|
medianTokens: float
|
|
byType: ProjectByType
|
|
loc: int
|
|
meanCostPerLoc: float
|
|
|
|
|
|
# A directory's GOAL.md: the checklist rollup that tags a card, and — on a
|
|
# detail response — the markdown itself. Absent file ⇒ the field is None.
|
|
class GoalSummary(Schema):
|
|
done: int
|
|
total: int
|
|
|
|
|
|
# One checklist item on a detail page's Goal checklist widget: the raw markdown
|
|
# after its checkbox (rendered), a plain single-line summary (the Work prompt +
|
|
# tooltip), and whether it's ticked.
|
|
class GoalChecklistItem(Schema):
|
|
text: str
|
|
plain: str
|
|
checked: bool
|
|
|
|
|
|
# A goal-keeper's in-flight claim under "## Being worked on" — an item some agent
|
|
# is on right now. Surfaced on the detail (so the checklist can flag a claimed
|
|
# item) and on the board. (Defined here so GoalDetail can reference it.)
|
|
class GoalClaim(Schema):
|
|
text: str
|
|
since: Optional[str] = None # ISO — parsed off a trailing "@<iso>"
|
|
stale: bool # older than the goal-keeper's 5h cadence
|
|
|
|
|
|
class GoalDetail(GoalSummary):
|
|
content: str
|
|
items: list[GoalChecklistItem]
|
|
claims: list[GoalClaim]
|
|
|
|
|
|
# ── the goals board (/api/goals) ─────────────────────────────────────────────
|
|
# One card per project/service that keeps a GOAL.md: its progress, the version
|
|
# milestones off its "## Horizons", the themed wishlist groups, the goal-keeper
|
|
# claims in "## Being worked on", and how many agents are on it right now.
|
|
class GoalMilestone(Schema):
|
|
title: str
|
|
version: Optional[str] = None # "0.3" — absent on an unversioned horizon
|
|
current: bool # the horizon marked "(now)"
|
|
done: int
|
|
total: int
|
|
hasTasks: bool # False ⇒ prose horizon, don't render "0/0"
|
|
|
|
|
|
class GoalTheme(Schema):
|
|
title: str
|
|
done: int
|
|
total: int
|
|
|
|
|
|
class RunningConversation(Schema):
|
|
id: str
|
|
title: str
|
|
startedAt: Optional[str] = None
|
|
|
|
|
|
class GoalBoardEntry(GoalSummary):
|
|
kind: Literal["project", "service"]
|
|
dir: str
|
|
name: str
|
|
description: str
|
|
url: Optional[str] = None
|
|
updatedAt: Optional[str] = None
|
|
milestones: list[GoalMilestone]
|
|
themes: list[GoalTheme]
|
|
claims: list[GoalClaim]
|
|
# The goal's checklist, so a card can expand into its individual items (and
|
|
# put a Work button on each) without a round-trip to the detail page. Capped
|
|
# server-side — see `goal.BOARD_ITEMS` — since the board renders many goals.
|
|
items: list[GoalChecklistItem]
|
|
itemsTruncated: int # items beyond the cap, for a "+N more" hint
|
|
agents: int # sessions running on this unit right now
|
|
runningConversations: list[RunningConversation]
|
|
conversations: int
|
|
lastConversationAt: Optional[str] = None
|
|
|
|
|
|
class GoalsResponse(Schema):
|
|
goals: list[GoalBoardEntry]
|
|
|
|
|
|
class ProjectSummary(Schema):
|
|
dir: str
|
|
name: str
|
|
description: str
|
|
version: Optional[str] = None
|
|
url: Optional[str] = None
|
|
stack: list[str]
|
|
keywords: list[str]
|
|
hasPackageJson: bool
|
|
goal: Optional[GoalSummary] = None
|
|
updatedAt: Optional[str] = None
|
|
costs: Optional[ProjectCostSummary] = None
|
|
# True for the root project — the homelab repo itself, which collects every
|
|
# conversation that didn't work on a project of its own. Rendered distinctly
|
|
# (it isn't a `projects/<slug>` directory like the others).
|
|
isRoot: Optional[bool] = None
|
|
# The Claude account whose sessions work on this project (accounts.py).
|
|
account: Optional[str] = None
|
|
|
|
|
|
class IndexMeta(Schema):
|
|
title: Optional[str] = None
|
|
description: Optional[str] = None
|
|
ogImage: Optional[str] = None
|
|
ogUrl: Optional[str] = None
|
|
themeColor: Optional[str] = None
|
|
|
|
|
|
class OgImage(Schema):
|
|
kind: Literal["url", "asset"]
|
|
src: str
|
|
|
|
|
|
class KeyFile(Schema):
|
|
name: str
|
|
bytes: int
|
|
|
|
|
|
class ProjectCommit(Schema):
|
|
hash: str
|
|
date: str
|
|
subject: str
|
|
|
|
|
|
class ProjectDetail(ProjectSummary):
|
|
readme: Optional[str] = None
|
|
claudeMd: Optional[str] = None
|
|
goal: Optional[GoalDetail] = None
|
|
indexMeta: Optional[IndexMeta] = None
|
|
ogImage: Optional[OgImage] = None
|
|
scripts: list[str]
|
|
keyFiles: list[KeyFile]
|
|
commits: list[ProjectCommit]
|
|
costs: Optional[ProjectCostDetail] = None
|
|
|
|
|
|
class ProjectsResponse(Schema):
|
|
projects: list[ProjectSummary]
|
|
|
|
|
|
class ProjectEnvVar(Schema):
|
|
key: str
|
|
# Secrets never travel to the UI: value is None for them, hasValue says
|
|
# whether a non-empty value is set on disk.
|
|
value: Optional[str] = None
|
|
secret: bool
|
|
hasValue: bool
|
|
|
|
|
|
class ProjectEnvResponse(Schema):
|
|
slug: str
|
|
exists: bool
|
|
vars: list[ProjectEnvVar]
|
|
|
|
|
|
# ── templates ─────────────────────────────────────────────────────────────────
|
|
class TemplateSummary(Schema):
|
|
name: str
|
|
description: str
|
|
stack: list[str]
|
|
fileCount: int
|
|
sizeBytes: int
|
|
hasReadme: bool
|
|
|
|
|
|
class TemplateTreeNode(Schema):
|
|
name: str
|
|
path: str
|
|
type: Literal["dir", "file"]
|
|
size: Optional[int] = None
|
|
text: Optional[bool] = None
|
|
children: Optional[list["TemplateTreeNode"]] = None
|
|
|
|
|
|
class TemplateDetail(Schema):
|
|
name: str
|
|
description: str
|
|
stack: list[str]
|
|
fileCount: int
|
|
sizeBytes: int
|
|
readme: Optional[str] = None
|
|
scripts: dict[str, str]
|
|
dependencies: list[str]
|
|
tree: list[TemplateTreeNode]
|
|
|
|
|
|
class TemplatesResponse(Schema):
|
|
templates: list[TemplateSummary]
|