diff --git a/.agents/skills/academic-cv-builder/SKILL.md b/.agents/skills/academic-cv-builder/SKILL.md new file mode 100644 index 00000000..73d9a63a --- /dev/null +++ b/.agents/skills/academic-cv-builder/SKILL.md @@ -0,0 +1,421 @@ +--- +name: academic-cv-builder +description: Format CVs for academic positions with publications, grants, and teaching +--- + +# Academic CV Builder + +## When to Use This Skill + +Use this skill when the user: +- Is applying for academic positions (faculty, research, postdoc) +- Needs to create or update a curriculum vitae +- Wants to format publications, grants, and teaching experience +- Is in academia or transitioning to academic careers +- Mentions: "academic CV", "curriculum vitae", "faculty position", "research CV", "professor resume" + +## Core Capabilities + +- Structure CVs for academic positions +- Format publications, presentations, and grants +- Organize teaching and research experience +- Include appropriate academic sections +- Tailor for different academic roles (tenure-track, postdoc, lecturer) +- Balance research, teaching, and service + +## Academic CV vs. Resume + +| Resume | Academic CV | +|--------|------------| +| 1-2 pages | 2-20+ pages (length increases with career) | +| Highlights relevant experience | Comprehensive record | +| Results-focused | Scholarship-focused | +| Industry keywords | Disciplinary expertise | +| Skills section prominent | Publications prominent | +| Education minimal | Education detailed | + +## Standard Academic CV Sections + +### Typical Order + +``` +1. Contact Information +2. Education +3. Research/Academic Positions +4. Publications +5. Presentations +6. Grants & Funding +7. Teaching Experience +8. Mentoring +9. Service +10. Professional Memberships +11. Honors & Awards +12. References (or "Available upon request") +``` + +### Section Order Varies By: +- **Research position:** Publications, grants, research experience first +- **Teaching position:** Teaching, course development first +- **Administrative position:** Leadership, service first + +## Section-by-Section Guide + +### 1. Contact Information + +``` +FIRST MIDDLE LAST, Ph.D. +Department of [Field] +[University Name] +[Building, Room Number] +[City, State ZIP] + +Email: email@university.edu +Phone: (555) 123-4567 +Web: www.yoursite.edu +ORCID: 0000-0000-0000-0000 +``` + +### 2. Education + +**Format:** Degree, Field, Institution, Year + +``` +EDUCATION + +Ph.D. in Molecular Biology, Stanford University, 2019 + Dissertation: "Title of Your Dissertation" + Advisor: Dr. Jane Smith + Committee: Dr. A, Dr. B, Dr. C + +M.S. in Biology, UC Berkeley, 2015 + +B.S. in Biochemistry, UCLA, 2013 + Summa Cum Laude +``` + +**Include:** +- All degrees (in reverse chronological order) +- Dissertation/thesis title +- Advisor(s) +- Committee members (for PhD) +- Honors (cum laude, etc.) +- Relevant minors or certificates + +### 3. Research/Academic Positions + +``` +ACADEMIC APPOINTMENTS + +Assistant Professor of Biology, University of Michigan, 2022-Present + Department of Molecular, Cellular, and Developmental Biology + +Postdoctoral Fellow, MIT, 2019-2022 + Advisor: Dr. John Doe + Lab: Computational Biology Lab + +Graduate Research Assistant, Stanford University, 2014-2019 + Advisor: Dr. Jane Smith +``` + +### 4. Publications + +**Most Important Section for Research Positions** + +#### Formatting Options + +**Option 1: Numbered List (Common in Sciences)** +``` +PUBLICATIONS + +Peer-Reviewed Journal Articles + +15. Last, F.M., Co-Author, A.B., & Senior, C.D. (2023). Article title. Journal Name, 45(2), 123-145. doi:10.1000/xyz + +14. Last, F.M., & Co-Author, A.B. (2022). Article title. Journal Name, 44(1), 10-25. doi:10.1000/abc +``` + +**Option 2: Categories (Useful for Multiple Types)** +``` +PUBLICATIONS + +Peer-Reviewed Journal Articles (15) + +Book Chapters (3) + +Books (1) + +Under Review (2) + +In Preparation (3) +``` + +**Formatting Details:** +- **Bold your name** in author lists +- Include DOIs when available +- Note impact factors if requested/relevant +- Indicate student co-authors with asterisk* +- Some fields expect reverse chronological; others expect chronological + +**Categories to Consider:** +- Peer-reviewed journal articles +- Books and book chapters +- Conference proceedings +- Technical reports +- Non-peer-reviewed publications +- Works under review +- Works in preparation + +### 5. Presentations + +``` +PRESENTATIONS + +Invited Talks + +"Talk Title," Conference Name, Location, Date. +"Talk Title," Department Seminar, University Name, Date. + +Conference Presentations + +"Poster/Talk Title," Conference Name, Location, Date. [Poster/Oral] +``` + +**Categorize By:** +- Invited talks (keynotes, seminars) +- Conference presentations +- Campus talks +- Public lectures/outreach + +### 6. Grants & Funding + +``` +GRANTS AND FUNDING + +Awarded + +NIH R01 (Co-PI), "Project Title," 2023-2028, $2.5M total ($500K to my lab) + +NSF CAREER Award (PI), "Project Title," 2022-2027, $650,000 + +Internal Grant (PI), "Project Title," 2021, $25,000 + +Pending + +NIH R21 (PI), "Project Title," submitted January 2024 + +Not Funded (Optional) + +[Some fields expect you to list unfunded submissions] +``` + +**Include:** +- Funding agency and mechanism +- Your role (PI, Co-PI, Co-I) +- Project title +- Dates +- Total amount (and amount to your lab if split) + +### 7. Teaching Experience + +``` +TEACHING EXPERIENCE + +Courses Taught + +BIOL 301: Molecular Biology (Instructor of Record) + University of Michigan, Fall 2022, Fall 2023 + Enrollment: 45 students + Developed new course curriculum + +BIOL 101: Introduction to Biology (Lab Instructor) + Stanford University, 2015-2018 + +Guest Lectures + +"Topic," Course Name, Professor's Name, University, Date +``` + +**Include:** +- Course number and title +- Your role (Instructor, TA, Guest Lecturer) +- Institution and dates +- Enrollment numbers +- Course development or new preparations +- Teaching evaluations summary (if strong) + +### 8. Mentoring + +``` +MENTORING + +Graduate Students +- Student Name (Ph.D. expected 2025), Dissertation: "Title" +- Student Name (Ph.D. 2023), Current position: Postdoc at MIT + +Postdoctoral Fellows +- Name (2021-2023), Current position: Assistant Professor at X + +Undergraduate Researchers +- Name (2022-2023), Thesis: "Title," Current: PhD program at Y +- Name (2021-2022), Thesis: "Title," Current: Industry position +``` + +### 9. Service + +``` +SERVICE + +To the Profession +- Editorial Board Member, Journal Name, 2022-Present +- Grant Reviewer, NIH Study Section XYZ, 2023 +- Conference Organizer, Conference Name, 2022 + +To the University +- Graduate Admissions Committee, 2022-Present +- Faculty Search Committee, 2023 +- Curriculum Committee, 2022-2023 + +To the Department +- Seminar Coordinator, 2022-Present +- Undergraduate Advisor, 2022-Present +``` + +### 10. Professional Memberships + +``` +PROFESSIONAL MEMBERSHIPS + +American Society for Cell Biology (ASCB), 2015-Present +Society for Neuroscience (SfN), 2018-Present +``` + +### 11. Honors & Awards + +``` +HONORS AND AWARDS + +NSF CAREER Award, 2022 +Best Paper Award, Conference Name, 2021 +Outstanding Graduate Student Award, Stanford University, 2018 +National Science Foundation Graduate Research Fellowship, 2015-2018 +Phi Beta Kappa, 2013 +``` + +## Role-Specific Emphasis + +### Tenure-Track Faculty + +**Emphasize:** +1. Publications (especially recent, high-impact) +2. Grants (especially independent funding) +3. Research trajectory and vision +4. Teaching experience +5. Mentoring record + +### Postdoctoral Position + +**Emphasize:** +1. Publications (from PhD and postdoc) +2. Research experience and skills +3. Collaboration experience +4. Future research potential +5. Any funding/fellowships + +### Lecturer/Teaching Faculty + +**Emphasize:** +1. Teaching experience (courses, evaluations) +2. Course development +3. Pedagogical training +4. Mentoring undergraduates +5. Teaching awards + +### Research Scientist + +**Emphasize:** +1. Publications +2. Technical skills +3. Grant writing experience +4. Collaboration record +5. Relevant research experience + +## Discipline-Specific Conventions + +### Sciences (Biology, Chemistry, Physics) +- Author order matters (first author, last author = senior) +- Impact factors sometimes included +- Numbered publication lists common +- Conference presentations less weighted than publications + +### Humanities (History, Literature, Philosophy) +- Single-author publications highly valued +- Book publications crucial +- Conference presentations important +- Public scholarship valued + +### Social Sciences +- Both solo and collaborative work valued +- Mix of journal articles and books +- Funded research important +- Policy impact valued + +## CV Length Guidelines + +| Career Stage | Expected Length | +|--------------|-----------------| +| Graduate Student | 2-4 pages | +| Postdoc | 3-6 pages | +| Early Career Faculty | 5-10 pages | +| Mid-Career Faculty | 10-20 pages | +| Senior Faculty | 15-30+ pages | + +**Rule:** Your CV grows throughout your career. Don't pad, but don't artificially constrain length. + +## Output Format + +When creating an academic CV: + +```markdown +# ACADEMIC CV STRUCTURE FOR [NAME] + +## Recommended Section Order +[Based on position type and field] + +1. [Section] +2. [Section] +... + +## Section Content + +### Education +[Formatted education section] + +### Publications +[Formatted with appropriate style for field] + +### [Other Sections] +[Formatted content] + +--- + +## Formatting Notes +- [Field-specific conventions to follow] +- [Style guide recommendations] + +## Things to Add/Update +- [ ] [Missing item] +- [ ] [Item needing update] +``` + +## Academic CV Checklist + +- ✅ Contact information complete (including ORCID if applicable) +- ✅ Education includes all degrees, advisors, dissertations +- ✅ Publications properly formatted with your name highlighted +- ✅ All grants listed with amounts and your role +- ✅ Teaching experience comprehensive +- ✅ Service documented +- ✅ Consistent formatting throughout +- ✅ Reverse chronological order (usually) +- ✅ No unexplained gaps +- ✅ Updated within last 6 months diff --git a/.agents/skills/agenticx-a2a-connector/SKILL.md b/.agents/skills/agenticx-a2a-connector/SKILL.md new file mode 100644 index 00000000..3bbaa716 --- /dev/null +++ b/.agents/skills/agenticx-a2a-connector/SKILL.md @@ -0,0 +1,173 @@ +--- +name: agenticx-a2a-connector +description: Guide for using the A2A (Agent-to-Agent) communication protocol in AgenticX including agent discovery, skill invocation, remote agent cards, and distributed agent systems. Use when the user wants agents to communicate with each other, set up distributed agent systems, invoke remote agent skills, or build agent-to-agent workflows. +metadata: + author: AgenticX + version: "0.3.9" +--- + +# AgenticX A2A Connector + +Guide for building distributed, inter-communicating agent systems using the A2A protocol. + +## What is A2A? + +A2A (Agent-to-Agent) is a protocol that enables agents to discover each other's capabilities and invoke them as if they were local tools. This allows building distributed agent systems where specialized agents collaborate across network boundaries. + +## Core Components + +| Component | Purpose | +|-----------|---------| +| `AgentCard` | Advertises an agent's identity and skills | +| `Skill` | Describes a capability an agent offers | +| `A2ASkillTool` | Wraps a remote skill as a local tool | +| `A2ASkillToolFactory` | Batch-creates tools from an AgentCard | +| `A2AClient` | HTTP client for calling remote agents | + +## Agent Cards + +An AgentCard declares what an agent can do: + +```python +from agenticx.protocols import AgentCard, Skill + +card = AgentCard( + name="Research Agent", + description="Specializes in web research and report generation", + url="http://research-agent:8000", + skills=[ + Skill( + name="web_research", + description="Search the web and compile findings", + parameters_schema={ + "type": "object", + "properties": { + "query": {"type": "string"}, + "depth": {"type": "integer", "default": 3} + }, + "required": ["query"] + } + ), + Skill( + name="generate_report", + description="Generate a structured report from research data", + parameters_schema={ + "type": "object", + "properties": { + "topic": {"type": "string"}, + "format": {"type": "string", "enum": ["markdown", "html"]} + }, + "required": ["topic"] + } + ) + ] +) +``` + +## Using Remote Agent Skills as Tools + +### Single Skill + +```python +from agenticx.protocols import A2ASkillTool + +tool = A2ASkillTool( + agent_url="http://research-agent:8000", + skill_name="web_research" +) + +# Use like any local tool +result = tool.run(query="latest AI trends", depth=5) +``` + +### All Skills from an Agent + +```python +from agenticx.protocols import A2ASkillToolFactory + +factory = A2ASkillToolFactory() +tools = factory.create_tools(agent_card=card) + +# tools is a list of A2ASkillTool instances, one per skill +for t in tools: + print(f"Tool: {t.name} — {t.description}") +``` + +## A2A Client + +Low-level client for direct communication: + +```python +from agenticx.protocols import A2AClient + +client = A2AClient(base_url="http://research-agent:8000") + +# Discover capabilities +card = client.get_agent_card() + +# Invoke a skill +result = client.invoke_skill( + skill_name="web_research", + parameters={"query": "quantum computing", "depth": 3} +) +``` + +## Building an A2A-Enabled Agent + +Expose your agent as an A2A service: + +```python +from agenticx import Agent +from agenticx.protocols import A2AServer + +agent = Agent( + id="specialist", + name="Data Specialist", + role="Data Analysis", + goal="Analyze datasets", + organization_id="team-a" +) + +server = A2AServer(agent=agent, port=8001) +server.register_skill( + name="analyze_data", + handler=my_analysis_function, + description="Analyze a dataset and return insights" +) +server.start() +``` + +## Multi-Agent Architecture Pattern + +``` +┌─────────────────┐ A2A ┌──────────────────┐ +│ Orchestrator │────────────→│ Research Agent │ +│ Agent │ │ (port 8001) │ +│ (port 8000) │ └──────────────────┘ +│ │ A2A ┌──────────────────┐ +│ │────────────→│ Analysis Agent │ +│ │ │ (port 8002) │ +└─────────────────┘ └──────────────────┘ +``` + +The Orchestrator discovers remote agents via their AgentCards and invokes their skills as tools within its own workflow. + +## CLI for A2A + +```bash +# Start an agent as an A2A service +agx serve --port 8001 + +# The serve command exposes: +# - GET /.well-known/agent-card → AgentCard JSON +# - POST /tasks/submit → Invoke skills +``` + +## Best Practices + +1. **Version your skills** — include version in AgentCard metadata +2. **Schema validation** — always define `parameters_schema` for skills +3. **Timeout handling** — set reasonable timeouts for remote calls +4. **Retry logic** — implement retries for network failures +5. **Health checks** — use `/health` endpoints before routing traffic +6. **Security** — authenticate A2A calls in production environments diff --git a/.agents/skills/agenticx-agent-builder/SKILL.md b/.agents/skills/agenticx-agent-builder/SKILL.md new file mode 100644 index 00000000..cecf68e2 --- /dev/null +++ b/.agents/skills/agenticx-agent-builder/SKILL.md @@ -0,0 +1,199 @@ +--- +name: agenticx-agent-builder +description: Guide for creating and configuring AgenticX agents with roles, goals, tools, LLM providers, and execution strategies. Use when the user wants to create agents, assign tools to agents, configure LLM backends, set up agent execution, or build multi-agent systems. +metadata: + author: AgenticX + version: "0.3.9" +--- + +# AgenticX Agent Builder + +Guide for creating production-grade agents in AgenticX. + +## Core Concepts + +An Agent in AgenticX consists of: +- **Identity**: id, name, role, goal +- **LLM Provider**: the language model backend +- **Tools**: functions the agent can invoke +- **Executor**: the runtime that orchestrates agent reasoning + +## Creating an Agent + +### Minimal Agent + +```python +from agenticx import Agent, Task, AgentExecutor +from agenticx.llms import OpenAIProvider + +agent = Agent( + id="assistant", + name="Assistant", + role="General Purpose Assistant", + goal="Help users with tasks", + organization_id="default" +) +``` + +### Agent with Rich Configuration + +```python +agent = Agent( + id="research-analyst", + name="Research Analyst", + role="Senior Research Analyst", + goal="Produce thorough, well-cited research reports", + backstory="10 years experience in data-driven research", + organization_id="research-team", + verbose=True +) +``` + +### CLI Creation + +```bash +agx agent create research-analyst --role "Senior Research Analyst" +agx agent list +``` + +## LLM Providers + +AgenticX supports multiple LLM backends through a unified interface: + +```python +from agenticx.llms import OpenAIProvider, LiteLLMProvider + +# OpenAI +llm = OpenAIProvider(model="gpt-4") + +# Any model via LiteLLM (Codex, Gemini, local models, etc.) +llm = LiteLLMProvider(model="anthropic/Codex-sonnet-4-20250514") +llm = LiteLLMProvider(model="ollama/llama3") +``` + +## Adding Tools + +### Function Decorator Tools + +```python +from agenticx.tools import tool + +@tool +def search_web(query: str) -> str: + """Search the web for information.""" + return f"Results for: {query}" + +@tool +def calculate(expression: str) -> float: + """Evaluate a math expression safely.""" + return eval(expression) # use ast.literal_eval in production +``` + +### Attaching Tools to Execution + +```python +executor = AgentExecutor( + agent=agent, + llm=llm, + tools=[search_web, calculate] +) +result = executor.run(task) +``` + +## Task Definition + +```python +task = Task( + id="research-task", + description="Research the latest trends in AI agents", + expected_output="A structured report with sections and citations", + context={"domain": "artificial-intelligence"} +) +``` + +### Output Validation + +AgenticX validates task outputs using Pydantic: + +```python +from pydantic import BaseModel + +class ResearchReport(BaseModel): + title: str + summary: str + findings: list[str] + +task = Task( + id="validated-task", + description="Research AI trends", + expected_output="Structured research report", + output_model=ResearchReport +) +``` + +## Execution Strategies + +### Basic Execution + +```python +executor = AgentExecutor(agent=agent, llm=llm) +result = executor.run(task) +``` + +### With Events & Callbacks + +AgenticX emits events during execution (TaskStart, ToolCall, LLMCall, etc.): + +```python +from agenticx.core import EventLog + +event_log = EventLog() +executor = AgentExecutor(agent=agent, llm=llm, event_log=event_log) +result = executor.run(task) + +for event in event_log.events: + print(f"{event.type}: {event.data}") +``` + +## Multi-Agent Patterns + +### Agent Handoff + +```python +from agenticx.core import HandoffOutput + +# Agent A can hand off to Agent B +handoff = HandoffOutput(target_agent="agent-b", context={"data": result}) +``` + +### Communication Interface + +```python +from agenticx.core import BroadcastCommunication + +comm = BroadcastCommunication() +comm.send(sender="agent-a", message="Task complete", data=result) +``` + +## GuideRails + +Constrain agent behavior with guardrails: + +```python +from agenticx.core import GuideRails, GuideRailsConfig + +config = GuideRailsConfig( + max_iterations=10, + timeout_seconds=60, + abort_on_failure=True +) +guardrails = GuideRails(config=config) +``` + +## Best Practices + +1. **Specific roles** — narrow roles produce better results than generic ones +2. **Clear goals** — state what success looks like +3. **Minimal tools** — only attach tools the agent actually needs +4. **Output validation** — use Pydantic models for structured outputs +5. **Event logging** — always enable for debugging and monitoring diff --git a/.agents/skills/agenticx-automation-crontask/SKILL.md b/.agents/skills/agenticx-automation-crontask/SKILL.md new file mode 100644 index 00000000..9b9c450d --- /dev/null +++ b/.agents/skills/agenticx-automation-crontask/SKILL.md @@ -0,0 +1,62 @@ +--- +name: agenticx-automation-crontask +description: Build and maintain Near Desktop scheduled (cron) tasks — default workspace ~/.agenticx/crontask, schedule_task tool, execution contract, and user-facing output. Use when the user wants recurring automation, crontab-style jobs, or to author/fix automation task prompts. +metadata: + author: AgenticX + version: "0.1.2" +--- + +# AgenticX 定时任务(Crontask) + +## 任务根目录(工程约定,与 Desktop / `schedule_task` 一致) + +| 用户在自动化里是否填写「工作区」 | 任务根目录 | +|----------------------------------|------------| +| **填写了** | 用户给定目录(Desktop 会 `mkdir`) | +| **留空** | `~/.agenticx/crontask//`(**每个定时任务独占一个子目录**,与 `automation:` 对话一一对应) | + +- **Python venv**:一律在任务根下 **`<任务根>/.venv`**,用 `<任务根>/.venv/bin/pip` / `python`;不要把定时任务专属依赖装到仓库 `.venv` 或任意路径,除非该路径就是用户指定的任务根。 +- **脚本、数据、日志、临时文件、辅助工具**:**全部**放在任务根或其子目录内;不要在 `~/.agenticx/scripts`、仓库根等散落(除非用户显式把其中某路径设为任务根)。 + +## Meta-Agent:调用 `schedule_task` 之前(运行环境) + +定时任务**以后**在 **automation 专属会话**里执行,不会在当时的 Near 对话里自动装包。在调用 `schedule_task` **之前**应代用户准备好任务根下的环境: + +1. 先拿到 **任务根**:用户指定的 `workspace`,或创建任务后默认的 `~/.agenticx/crontask//`(`schedule_task` 返回的 `task_id` 可用于路径)。 +2. 在任务根下 **`python3 -m venv .venv`**(若尚无),**`.venv/bin/pip install …`**。 +3. **`bash_exec` 用 `<任务根>/.venv/bin/python` 试跑脚本**,确认无 import 错误。 +4. `instruction` 里的命令必须与 **同一解释器路径** 一致。 + +## 默认工作区(摘要) + +- 未指定 `workspace` 时,Desktop 与 `schedule_task` 均写入 **`~/.agenticx/crontask/`** 并创建目录。 +- 删除任务时,UI 会**二次确认**是否同时删除该目录下的本地文件(仅针对上述 crontask 子目录,不随意删除用户任意路径)。 + +## 用 `schedule_task` 创建任务(对话 / Meta-Agent) + +在 `instruction`(提示词)里写清: + +1. **何时跑**:已在工具参数里用 `frequency_type` / `time` / `days` 表达;提示词内可再写一句业务语义(如「交易日 9:28」)便于人读。 +2. **怎么跑**:必须要求 **真实执行**(如 `bash_exec` + `python3`),禁止「只给代码不运行」。 +3. **输出格式**:给出**严格版式**(标题、字段、单位),并写明「最终回复只允许该版式,禁止工具 JSON」。 +4. **失败**:简短错误(接口/库/网络),不超过若干行,不要教程、不要反问。 +5. **依赖**:写明 `pip install` 包名;执行环境以本机为准。 + +可选参数 `workspace`:仅当用户明确要求固定目录时填写;否则留空使用默认 crontask 目录。 + +## 任务执行时(automation 会话) + +会话 `avatar_id` 为 `automation:` 时,后端会注入执行器系统提示,强调: + +- 先工具执行、再按用户版式输出; +- 不在最终回复粘贴 `schedule_task` 等原始 JSON; +- 失败简短说明。 + +编写或审阅提示词时,应与此行为一致。 + +## 验收清单 + +- [ ] 提示词是否要求**实际跑命令**并得到数据? +- [ ] 是否定义了**唯一允许的输出模板**? +- [ ] 是否说明**非交易日 / 无数据**时的单行或短输出? +- [ ] 是否需要**默认 crontask 路径**下的脚本文件(便于复跑与排障)? diff --git a/.agents/skills/agenticx-deployer/SKILL.md b/.agents/skills/agenticx-deployer/SKILL.md new file mode 100644 index 00000000..2465533f --- /dev/null +++ b/.agents/skills/agenticx-deployer/SKILL.md @@ -0,0 +1,157 @@ +--- +name: agenticx-deployer +description: Guide for deploying AgenticX agents to production including Docker containerization, Kubernetes orchestration, Volcengine AgentKit cloud deployment, and API server setup. Use when the user wants to deploy agents, containerize applications, set up Kubernetes, configure cloud deployment, or run the AgenticX API server in production. +metadata: + author: AgenticX + version: "0.3.9" +--- + +# AgenticX Deployer + +Guide for taking AgenticX agents from development to production. + +## Deployment Options + +| Method | Best For | Command | +|--------|----------|---------| +| API Server | Quick deployment, development | `agx serve` | +| Docker | Single-node, reproducible | `agx deploy docker` | +| Kubernetes | Multi-node, auto-scaling | `agx deploy k8s` | +| Volcengine | Cloud-native, managed | `agx volcengine deploy` | + +## API Server + +### Start Server + +```bash +# Default (0.0.0.0:8000) +agx serve + +# Custom port + host +agx serve --port 9000 --host 127.0.0.1 + +# Development with auto-reload +agx serve --port 8000 --reload +``` + +Requires: `pip install "agenticx[server]"` + +### Health Endpoints + +| Endpoint | Purpose | +|----------|---------| +| `GET /health` | Comprehensive health check | +| `GET /health/live` | Liveness probe | +| `GET /health/ready` | Readiness probe | +| `POST /tasks/submit` | Submit a task | + +## Docker Deployment + +### Prepare & Build + +```bash +# Prepare deployment package +agx deploy prepare --output ./deploy-package + +# Build Docker image +agx deploy docker --tag my-agent:latest + +# Run container +docker run -p 8000:8000 \ + -e OPENAI_API_KEY="sk-..." \ + my-agent:latest +``` + +### Custom Dockerfile + +```dockerfile +FROM python:3.11-slim + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . + +EXPOSE 8000 +CMD ["agx", "serve", "--port", "8000"] +``` + +## Kubernetes Deployment + +```bash +# Generate K8s manifests +agx deploy k8s --output ./k8s-manifests + +# Apply to cluster +kubectl apply -f ./k8s-manifests/ +``` + +Generated manifests include: +- Deployment with health probes +- Service (ClusterIP) +- ConfigMap for non-sensitive config +- Secret template for API keys +- HPA for auto-scaling + +## Volcengine AgentKit + +Cloud-native deployment on Volcengine's managed platform. + +Requires: `pip install "agenticx[volcengine]"` + +### Setup + +```bash +# Initialize project +agx volcengine init + +# Configure credentials +agx volcengine config \ + --access-key YOUR_AK \ + --secret-key YOUR_SK \ + --region cn-beijing +``` + +### Deploy + +```bash +# Deploy agent +agx volcengine deploy --agent my-agent + +# Check status +agx volcengine status + +# Invoke deployed agent +agx volcengine invoke --agent my-agent --input "Analyze this data" + +# Tear down +agx volcengine destroy --agent my-agent +``` + +### Integration Info + +```bash +agx volcengine info +``` + +## Monitoring in Production + +```bash +# Start monitoring service +agx monitor start + +# Check monitoring status +agx monitor status +``` + +AgenticX supports Prometheus metrics export for production monitoring. + +## Pre-Deployment Checklist + +1. **Environment variables** — all API keys set and not hardcoded +2. **Health probes** — `/health/live` and `/health/ready` configured +3. **Resource limits** — CPU/memory limits set in K8s/Docker +4. **Logging** — structured logging enabled +5. **Secrets** — use K8s Secrets or vault, never commit keys +6. **Testing** — run `agx test` before deploying +7. **Validation** — run `agx validate config.yaml` for config correctness diff --git a/.agents/skills/agenticx-memory-architect/SKILL.md b/.agents/skills/agenticx-memory-architect/SKILL.md new file mode 100644 index 00000000..e7fcb925 --- /dev/null +++ b/.agents/skills/agenticx-memory-architect/SKILL.md @@ -0,0 +1,174 @@ +--- +name: agenticx-memory-architect +description: Guide for setting up and using the AgenticX memory system including Mem0 integration, long-term memory, context management, and memory-enhanced agents. Use when the user wants to add memory to agents, persist conversation history, build memory-aware workflows, or integrate with Mem0 for long-term recall. +metadata: + author: AgenticX + version: "0.3.9" +--- + +# AgenticX Memory Architect + +Guide for building agents with persistent memory capabilities. + +## Overview + +AgenticX integrates with Mem0 for long-term memory, providing agents with the ability to remember past interactions, learn from experience, and maintain context across sessions. + +## Installation + +```bash +pip install "agenticx[memory]" +# Includes: mem0, chromadb, qdrant-client, redis, milvus +``` + +## Memory System Components + +| Component | Purpose | +|-----------|---------| +| `MemoryManager` | Core memory management interface | +| `Mem0Integration` | Bridge to Mem0's memory engine | +| `ContextMemory` | Short-term, session-scoped memory | +| `LongTermMemory` | Persistent, cross-session memory | + +## Basic Memory Usage + +### Initialize Memory + +```python +from agenticx.memory import MemoryManager + +memory = MemoryManager( + provider="mem0", + config={ + "llm": {"provider": "openai", "config": {"model": "gpt-4"}}, + "vector_store": {"provider": "chroma"} + } +) +``` + +### Store and Retrieve + +```python +# Add a memory +memory.add( + content="User prefers concise reports with bullet points", + user_id="user-123", + agent_id="analyst" +) + +# Search memories +results = memory.search( + query="What format does the user prefer?", + user_id="user-123" +) +for r in results: + print(f"[{r.score:.2f}] {r.content}") + +# Get all memories for a user +all_memories = memory.get_all(user_id="user-123") +``` + +## Memory-Enhanced Agents + +### Attach Memory to an Agent + +```python +from agenticx import Agent, AgentExecutor +from agenticx.memory import MemoryManager +from agenticx.llms import OpenAIProvider + +memory = MemoryManager(provider="mem0") +agent = Agent( + id="assistant", + name="Personal Assistant", + role="Assistant with memory", + goal="Help users while remembering their preferences", + organization_id="default" +) + +executor = AgentExecutor( + agent=agent, + llm=OpenAIProvider(model="gpt-4"), + memory=memory +) + +# First interaction — learns preference +result = executor.run(task_1) + +# Later interaction — recalls preference +result = executor.run(task_2) # agent remembers context from task_1 +``` + +## Memory Extraction + +AgenticX can automatically extract memorable facts from conversations: + +```python +from agenticx.core.memory_extraction import MemoryExtractor + +extractor = MemoryExtractor(llm=llm) +facts = extractor.extract(conversation_history) +# facts: ["User is a data scientist", "Prefers Python over R", ...] + +for fact in facts: + memory.add(content=fact, user_id="user-123") +``` + +## Vector Store Backends + +| Backend | Config key | Best for | +|---------|-----------|----------| +| ChromaDB | `"chroma"` | Local development, small scale | +| Qdrant | `"qdrant"` | Production, high performance | +| Redis | `"redis"` | Fast access, ephemeral | +| Milvus | `"milvus"` | Large scale, distributed | + +```python +# Qdrant example +memory = MemoryManager( + provider="mem0", + config={ + "vector_store": { + "provider": "qdrant", + "config": {"host": "localhost", "port": 6333} + } + } +) +``` + +## Healthcare Example + +```python +# Medical knowledge memory +memory.add( + content="Patient has Type 2 diabetes, diagnosed 2023", + user_id="patient-456", + metadata={"category": "medical_history"} +) + +# Query with context +results = memory.search( + query="What chronic conditions does the patient have?", + user_id="patient-456" +) +``` + +## CLI Memory Operations + +```bash +# Run the memory example +python examples/memory_example.py + +# Healthcare scenario +python examples/mem0_healthcare_example.py +``` + +## Best Practices + +1. **Scope memories** — always associate with `user_id` and/or `agent_id` +2. **Dedup** — check for similar memories before adding +3. **TTL** — set expiration for time-sensitive information +4. **Privacy** — never store PII without consent; use data isolation +5. **Vector store selection** — ChromaDB for dev, Qdrant/Milvus for production +6. **Memory extraction** — automate fact extraction from conversations +7. **Test retrieval** — verify that stored memories are actually retrievable diff --git a/.agents/skills/agenticx-quickstart/SKILL.md b/.agents/skills/agenticx-quickstart/SKILL.md new file mode 100644 index 00000000..6dc22309 --- /dev/null +++ b/.agents/skills/agenticx-quickstart/SKILL.md @@ -0,0 +1,121 @@ +--- +name: agenticx-quickstart +description: AgenticX zero-to-hero quickstart guide. Use when the user wants to get started with AgenticX, create their first project, build their first agent, or run their first workflow. Covers installation, project scaffolding, agent creation, task execution, and CLI basics. +metadata: + author: AgenticX + version: "0.3.9" +--- + +# AgenticX Quickstart + +Guide for getting a user from zero to a running AgenticX agent in under 5 minutes. + +## Installation + +```bash +# Core install (lightweight, ~27 deps, installs in seconds) +pip install agenticx + +# Verify +agx --version +``` + +Optional extras — install only what you need: + +| Extra | What it adds | Command | +|-------|-------------|---------| +| `memory` | Mem0, ChromaDB, Qdrant | `pip install "agenticx[memory]"` | +| `document` | PDF/PPT/Word parsing | `pip install "agenticx[document]"` | +| `server` | API server (`agx serve`) | `pip install "agenticx[server]"` | +| `volcengine` | Volcengine AgentKit | `pip install "agenticx[volcengine]"` | +| `all` | Everything | `pip install "agenticx[all]"` | + +## Environment + +```bash +export OPENAI_API_KEY="sk-..." +# Optional +export ANTHROPIC_API_KEY="sk-ant-..." +``` + +## Create a Project + +```bash +agx project create my-first-agent --template basic +cd my-first-agent +agx project info +``` + +## Create Your First Agent (Python) + +```python +from agenticx import Agent, Task, AgentExecutor +from agenticx.llms import OpenAIProvider + +agent = Agent( + id="data-analyst", + name="Data Analyst", + role="Data Analysis Expert", + goal="Help users analyze and understand data", + organization_id="my-org" +) + +task = Task( + id="analysis-task", + description="Analyze sales data trends", + expected_output="Detailed analysis report" +) + +llm = OpenAIProvider(model="gpt-4") +executor = AgentExecutor(agent=agent, llm=llm) +result = executor.run(task) +print(result) +``` + +## Create via CLI + +```bash +# Scaffold an agent +agx agent create researcher --role "Senior Research Analyst" +agx agent list + +# Scaffold a workflow +agx workflow create research-pipeline --agents "researcher" + +# Run it +agx run workflows/research-pipeline.py --verbose +``` + +## Add Tools + +```python +from agenticx.tools import tool + +@tool +def calculate_sum(x: int, y: int) -> int: + """Calculate the sum of two numbers.""" + return x + y + +# Pass tools when creating agent or executor +``` + +## Essential CLI Commands + +| Command | Purpose | +|---------|---------| +| `agx` | Welcome page with common commands | +| `agx --help` | Full help | +| `agx project create NAME` | Scaffold a project | +| `agx agent create NAME` | Create an agent | +| `agx workflow create NAME` | Create a workflow | +| `agx run FILE` | Execute a workflow file | +| `agx serve` | Start API server | +| `agx skills list` | List installed skills | +| `agx hooks list` | List available hooks | + +## Next Steps + +- Build complex agents → use the **agenticx-agent-builder** skill +- Design workflows → use the **agenticx-workflow-designer** skill +- Create custom tools → use the **agenticx-tool-creator** skill +- Deploy to production → use the **agenticx-deployer** skill diff --git a/.agents/skills/agenticx-skill-manager/SKILL.md b/.agents/skills/agenticx-skill-manager/SKILL.md new file mode 100644 index 00000000..4371977a --- /dev/null +++ b/.agents/skills/agenticx-skill-manager/SKILL.md @@ -0,0 +1,189 @@ +--- +name: agenticx-skill-manager +description: Guide for managing AgenticX skills including listing, searching, installing, uninstalling, publishing, and running a skill registry server. Use when the user wants to manage skills, find available skills, publish custom skills, set up a skill registry, or understand the skill ecosystem. +metadata: + author: AgenticX + version: "0.3.9" +--- + +# AgenticX Skill Manager + +Guide for managing the AgenticX skill ecosystem. + +## What Are Skills? + +Skills are self-contained instruction bundles (SKILL.md + optional resources) that teach AI agents how to perform specific tasks. AgenticX is compatible with the Anthropic Agent Skills specification. + +## Skill Discovery Paths + +The skill loader scans these directories (highest priority first): + +| Priority | Path | Scope | +|----------|------|-------| +| 1 | `./.agents/skills` | Project (Cherry Studio compatible) | +| 2 | `./.agent/skills` | Project | +| 3 | `~/.agents/skills` | Global | +| 4 | `~/.agent/skills` | Global | +| 5 | `./.Codex/skills` | Project | +| 6 | `~/.Codex/skills` | Global | +| 7 | Built-in (agenticx package) | Framework | + +## CLI Commands + +### List Skills + +```bash +# List all locally discovered skills +agx skills list + +# Include remote registry skills +agx skills list --remote + +# Output as JSON +agx skills list --format json +``` + +### Search Skills + +```bash +# Search by keyword +agx skills search "pdf" +agx skills search "workflow" + +# Search in remote registry +agx skills search "data analysis" --remote +``` + +### Install a Skill + +```bash +# Install from registry +agx skills install pdf-processor + +# Install to specific path +agx skills install pdf-processor --path ./.agents/skills +``` + +### Uninstall a Skill + +```bash +agx skills uninstall pdf-processor +``` + +### Publish a Skill + +```bash +# Publish to default registry +agx skills publish ./my-skills/data-analyzer + +# Publish to a specific registry URL +agx skills publish ./my-skills/data-analyzer --registry http://registry.example.com:8321 +``` + +### Run a Skill Registry Server + +```bash +# Start local registry (default port 8321) +agx skills serve + +# Custom port +agx skills serve --port 9000 +``` + +## Creating a Skill + +### Skill Structure + +``` +my-skill/ +├── SKILL.md # Required: frontmatter + instructions +├── scripts/ # Optional: executable code +├── references/ # Optional: additional documentation +└── assets/ # Optional: templates, images, data +``` + +### SKILL.md Format + +```markdown +--- +name: my-skill +description: What this skill does and when to use it. Be specific about triggers. +metadata: + author: your-name + version: "1.0" +--- + +# My Skill + +Instructions for the AI agent to follow when this skill is activated. + +## Steps +1. First step +2. Second step + +## Examples +- Example usage pattern +``` + +### Key Rules + +- **name**: lowercase, hyphens only, max 64 chars, must match directory name +- **description**: max 1024 chars, include both what it does AND when to trigger +- **Body**: under 500 lines; split large content into `references/` files + +## Programmatic Access + +### SkillBundleLoader + +```python +from agenticx.tools.skill_bundle import SkillBundleLoader + +loader = SkillBundleLoader() +skills = loader.scan() + +for skill in skills: + print(f"{skill.name}: {skill.description}") + +# Get specific skill +meta = loader.get_skill("agenticx-quickstart") +content = loader.get_skill_content("agenticx-quickstart") +``` + +### SkillRegistryClient + +```python +from agenticx.skills import SkillRegistryClient + +client = SkillRegistryClient(base_url="http://localhost:8321") +results = client.search("pdf") +client.install("pdf-processor") +``` + +## Skill Gating + +Skills can declare environment requirements in frontmatter: + +```yaml +metadata: + agenticx: + gate: + os: ["linux", "darwin"] + requires_bins: ["ffmpeg"] + requires_env: ["API_KEY"] +``` + +Skills that fail gating are silently skipped during scan. + +## Skill Sync + +Sync skills between directories: + +```python +from agenticx.tools.skill_sync import sync_skills, check_skills_sync + +# Check sync status +status = check_skills_sync() + +# Sync .agents/skills → .Codex/skills +sync_skills() +``` diff --git a/.agents/skills/agenticx-tool-creator/SKILL.md b/.agents/skills/agenticx-tool-creator/SKILL.md new file mode 100644 index 00000000..c7f99cf8 --- /dev/null +++ b/.agents/skills/agenticx-tool-creator/SKILL.md @@ -0,0 +1,145 @@ +--- +name: agenticx-tool-creator +description: Guide for creating custom tools in AgenticX including function decorator tools, MCP tool integration, tool registries, and remote tool access. Use when the user wants to create tools for agents, integrate external APIs as tools, build MCP servers, or extend agent capabilities with custom functions. +metadata: + author: AgenticX + version: "0.3.9" +--- + +# AgenticX Tool Creator + +Guide for building tools that extend agent capabilities. + +## Tool Architecture + +AgenticX tools inherit from `BaseTool` and are consumed by agents during execution. Three approaches exist: + +1. **Function decorator** (`@tool`) — fastest for simple tools +2. **Class-based** (extend `BaseTool`) — for complex or stateful tools +3. **MCP remote tools** — for external services via Model Context Protocol + +## Function Decorator Tools + +```python +from agenticx.tools import tool + +@tool +def search_web(query: str) -> str: + """Search the web for information. + + Args: + query: The search query string. + + Returns: + Search results as text. + """ + # implementation + return f"Results for: {query}" + +@tool +def read_file(path: str) -> str: + """Read contents of a local file.""" + with open(path) as f: + return f.read() +``` + +The `@tool` decorator reads the function signature and docstring to generate the tool schema automatically. The docstring **is** the tool description the LLM sees. + +## Class-Based Tools + +For tools needing initialization, state, or complex logic: + +```python +from agenticx.core import BaseTool + +class DatabaseQuery(BaseTool): + name = "database_query" + description = "Query the project database." + + def __init__(self, connection_string: str): + super().__init__() + self.conn = connect(connection_string) + + def _run(self, sql: str) -> str: + return self.conn.execute(sql).fetchall() +``` + +## Tool Registry + +Register and discover tools globally: + +```python +from agenticx.core import ToolRegistry + +registry = ToolRegistry() +registry.register(search_web) +registry.register(read_file) + +# List all registered tools +for t in registry.list_tools(): + print(f"{t.name}: {t.description}") +``` + +## MCP Integration + +AgenticX supports the Model Context Protocol for remote tool access. + +### Connecting to an MCP Server + +```python +from agenticx.protocols import MCPClient + +client = MCPClient(server_url="http://localhost:3000") +tools = client.list_tools() + +# Use MCP tools like local tools +result = client.call_tool("search", {"query": "AI agents"}) +``` + +### Building an MCP Server + +AgenticX agents can be exposed as MCP-compatible services: + +```python +from agenticx.protocols import MCPServer + +server = MCPServer(host="0.0.0.0", port=3000) +server.register_tool(search_web) +server.register_tool(read_file) +server.start() +``` + +## Skill-Based Tools + +Skills (SKILL.md bundles) are also exposed as tools via `SkillTool`: + +```python +from agenticx.tools.skill_bundle import SkillBundleLoader, SkillTool + +loader = SkillBundleLoader() +skill_tool = SkillTool(loader=loader) +# Agents can invoke: skill_tool("list") or skill_tool("read ") +``` + +## Tool Design Guidelines + +1. **Clear docstrings** — the LLM uses the docstring to decide when to call the tool +2. **Type hints** — always annotate parameters and return types +3. **Error handling** — return descriptive error messages, don't raise bare exceptions +4. **Minimal scope** — one tool, one purpose +5. **Idempotent when possible** — safe to retry without side effects +6. **Test independently** — verify tools work before attaching to agents + +## Advanced: Tool Context + +Tools can access execution context: + +```python +@tool +def contextual_tool(query: str, _context: "ToolContext" = None) -> str: + """A tool that uses execution context.""" + if _context: + user = _context.user + session = _context.session_id + return f"Processed: {query}" +``` diff --git a/.agents/skills/agenticx-workflow-designer/SKILL.md b/.agents/skills/agenticx-workflow-designer/SKILL.md new file mode 100644 index 00000000..5493bb24 --- /dev/null +++ b/.agents/skills/agenticx-workflow-designer/SKILL.md @@ -0,0 +1,190 @@ +--- +name: agenticx-workflow-designer +description: Guide for designing and running AgenticX workflows including sequential pipelines, parallel execution, graph-based orchestration, conditional routing, and trigger services. Use when the user wants to create workflows, orchestrate multiple agents, design agent pipelines, or set up complex multi-step processes. +metadata: + author: AgenticX + version: "0.3.9" +--- + +# AgenticX Workflow Designer + +Guide for building workflows that orchestrate agents, tasks, and execution paths. + +## Core Components + +| Component | Purpose | +|-----------|---------| +| `Workflow` | Container for nodes and edges | +| `WorkflowNode` | A step in the workflow (agent + task) | +| `WorkflowEdge` | Connection between nodes (with optional conditions) | +| `WorkflowEngine` | Runtime executor for the workflow graph | +| `WorkflowGraph` | Graph representation of the workflow | + +## Basic Workflow + +```python +from agenticx import Workflow, WorkflowNode, WorkflowEdge +from agenticx.core import WorkflowEngine + +# Define nodes +research_node = WorkflowNode( + id="research", + agent=researcher_agent, + task=research_task +) + +analysis_node = WorkflowNode( + id="analysis", + agent=analyst_agent, + task=analysis_task +) + +# Define edges (sequential flow) +edge = WorkflowEdge(source="research", target="analysis") + +# Build workflow +workflow = Workflow( + id="research-pipeline", + nodes=[research_node, analysis_node], + edges=[edge] +) + +# Execute +engine = WorkflowEngine() +result = engine.run(workflow) +``` + +## CLI Workflow Creation + +```bash +# Create workflow scaffold +agx workflow create research-pipeline --agents "researcher,analyst" + +# List workflows +agx workflow list + +# Run a workflow file +agx run workflows/research-pipeline.py --verbose +``` + +## Workflow Patterns + +### Sequential Pipeline + +Nodes execute one after another: + +``` +[Research] → [Analysis] → [Report] +``` + +```python +edges = [ + WorkflowEdge(source="research", target="analysis"), + WorkflowEdge(source="analysis", target="report"), +] +``` + +### Parallel Execution + +Multiple nodes execute concurrently: + +``` + ┌→ [Web Search] ─┐ +[Start] ─┤ ├→ [Merge] + └→ [DB Query] ─┘ +``` + +```python +edges = [ + WorkflowEdge(source="start", target="web-search"), + WorkflowEdge(source="start", target="db-query"), + WorkflowEdge(source="web-search", target="merge"), + WorkflowEdge(source="db-query", target="merge"), +] +``` + +### Conditional Routing + +Route execution based on output: + +```python +edge = WorkflowEdge( + source="classifier", + target="handler-a", + condition=lambda result: result.get("category") == "A" +) +``` + +### Graph-Based Orchestration + +For complex DAGs with multiple paths and merge points, use `WorkflowGraph`: + +```python +from agenticx.core import WorkflowGraph + +graph = WorkflowGraph() +graph.add_node(research_node) +graph.add_node(analysis_node) +graph.add_node(report_node) +graph.add_edge("research", "analysis") +graph.add_edge("analysis", "report") +``` + +## Triggers + +### Scheduled Trigger + +```python +from agenticx.core import TriggerService, ScheduledTrigger + +trigger = ScheduledTrigger( + cron="0 9 * * 1", # Every Monday at 9am + workflow_id="weekly-report" +) +service = TriggerService() +service.register(trigger) +``` + +### Event-Driven Trigger + +```python +from agenticx.core import EventDrivenTrigger + +trigger = EventDrivenTrigger( + event_type="new_data_available", + workflow_id="data-pipeline" +) +``` + +## Execution Context + +Track workflow state during execution: + +```python +from agenticx.core import ExecutionContext, WorkflowStatus + +context = ExecutionContext(workflow_id="research-pipeline") +# context.status → WorkflowStatus.RUNNING / COMPLETED / FAILED +# context.node_results → dict of node_id → result +``` + +## Running Workflow Files + +```bash +# Simple run +agx run my_workflow.py + +# With config file +agx run my_workflow.py --config config.yaml --verbose + +# Debug mode +agx run my_workflow.py --debug +``` + +## Best Practices + +1. **Start simple** — begin with sequential, add complexity as needed +2. **Name nodes clearly** — they appear in logs and monitoring +3. **Set timeouts** — prevent infinite loops in conditional workflows +4. **Use validation** — validate outputs at each node boundary +5. **Monitor execution** — enable observability for production workflows diff --git a/.agents/skills/ai-slop-detector/SKILL.md b/.agents/skills/ai-slop-detector/SKILL.md index f9e99dd0..15b72d8a 100644 --- a/.agents/skills/ai-slop-detector/SKILL.md +++ b/.agents/skills/ai-slop-detector/SKILL.md @@ -2,10 +2,12 @@ name: ai-slop-detector description: Use when the user asks to audit, score, humanize, or fix prose for AI-writing signals or readability. Produces separate AI-slop and comprehension findings with actionable edits. tags: [writing, editing, readability, ai-detection] +capability: Audit prose for AI-writing signals and reader comprehension, then return prioritized edits. triggers: - humanize this - AI slop check - does this sound AI-written + - read like it was generated by AI - readability audit --- diff --git a/.agents/skills/alert-manager/SKILL.md b/.agents/skills/alert-manager/SKILL.md new file mode 100644 index 00000000..0e058842 --- /dev/null +++ b/.agents/skills/alert-manager/SKILL.md @@ -0,0 +1,129 @@ +--- +name: alert-manager +description: 'Use when the user asks to "set SEO alerts"; configures ranking, traffic, technical, competitor, and notification thresholds. SEO预警/排名监控' +version: "9.9.9" +license: Apache-2.0 +compatibility: "Codex, skills.sh, ClawHub, Vercel Labs, Cursor, Windsurf, Codex CLI, Amp, Gemini CLI, Kimi Code, Qwen Code, CodeBuddy" +homepage: "https://github.com/aaron-he-zhu/seo-geo-Codex-skills" +when_to_use: "Use when setting up monitoring alerts for rankings, traffic, backlinks, technical issues, or AI visibility changes." +argument-hint: " [metric]" +metadata: + author: aaron-he-zhu + version: "9.9.9" + geo-relevance: "low" + tags: + - seo + - geo + - seo-alerts + - ranking-alerts + - traffic-monitoring + - competitor-alerts + - automated-monitoring + - anomaly-detection + - SEO预警 + - SEOアラート + - SEO알림 + - alertas-seo + triggers: + - "set up SEO alerts" + - "monitor rankings" + - "traffic alerts" + - "competitor alerts" + - "alert me if rankings drop" + - "notify me of traffic changes" + - "watch my keywords for changes" + - "how to monitor my rankings" + - "how to set up SEO alerts" + - "SEO预警" + - "排名监控" + - "流量报警" + - "竞品变动提醒" + - "排名掉了提醒我" + - "流量异常" + - "有变化通知我" + - "SEOアラート" + - "ランキング監視" + - "SEO 알림" + - "순위 모니터링" + - "alertas SEO" + - "monitoreo de rankings" + - "alertas de SEO" +--- + +# Alert Manager + +Sets up proactive monitoring alerts for ranking, traffic, technical, backlink, competitor, and GEO changes. + +## Quick Start + +``` +Set up SEO monitoring alerts for [domain] +``` + +``` +Create ranking drop alerts for my top 20 keywords +``` + +## Skill Contract + +**Expected output**: an alert configuration summary plus the standard handoff summary for `memory/monitoring/`. + +- **Reads**: current metrics, baselines, alert thresholds, and reporting context from [AGENTS.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/AGENTS.md) and the shared [State Model](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/references/state-model.md) when available. +- **Writes**: a user-facing monitoring deliverable and reusable summary. +- **Promotes**: significant anomalies, durable thresholds, follow-up actions, and pending decisions to `memory/open-loops.md`. +- **Primary next skill**: [performance-reporter](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/performance-reporter/SKILL.md) when alert output needs a reporting cadence. + +### Handoff Summary + +> Emit the standard shape from [skill-contract.md §Handoff Summary Format](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/references/skill-contract.md). + +## Data Sources + +All integrations optional (see [CONNECTORS.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/CONNECTORS.md)). With tools, monitor real-time feeds from ~~SEO tool, ~~search console, and ~~web crawler. Without tools, ask for baselines, critical keywords, preferences, and historical data. + +## Instructions + +When a user requests alert setup: + +1. **Define Alert Categories** — choose from rankings, traffic, technical, backlinks, competitors, GEO / AI, and brand alerts. +2. **Configure Alert Rules by Category** — define trigger condition, threshold, alert name, and priority for each relevant rule. +3. **Define Alert Response Plans** — map Critical / High / Medium / Low to response time and next actions. +4. **Set Up Alert Delivery** — configure channels, routing, cooldowns, maintenance windows, and escalation paths. +5. **Create Alert Summary** — deliver category counts, critical playbook, and weekly review checklist. + +> **Reference**: See [references/alert-configuration-templates.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/alert-manager/references/alert-configuration-templates.md) for the full category tables, thresholds, and response-plan templates. + +## Example + +Sample outcome: a keyword alert matrix with Critical vs High thresholds, a response plan for drops, and notification routing to email + Slack. + +## Tips for Success + +Start simple, tune thresholds to normal volatility, avoid alert fatigue, and review the system regularly. + +## Alert Threshold Quick Reference + +| Metric | Warning | Critical | Frequency | +|--------|---------|----------|-----------| +| Organic traffic | -15% WoW | -30% WoW | Daily | +| Keyword positions | >3 position drop | >5 position drop | Daily | +| Pages indexed | -5% change | -20% change | Weekly | +| Crawl errors | >10 new/day | >50 new/day | Daily | +| Core Web Vitals | "Needs Improvement" | "Poor" | Weekly | +| Backlinks lost | >5% in 1 week | >15% in 1 week | Weekly | +| AI citation loss | Any key query | >20% queries | Weekly | +| Security issues | Any detected | Any detected | Daily | + +> **Reference**: See [references/alert-threshold-guide.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/alert-manager/references/alert-threshold-guide.md) for threshold setting, fatigue prevention, escalation paths, and response playbooks. + +### Save Results + +Ask "Save these results?" If yes, write `memory/monitoring/YYYY-MM-DD-.md` with headline finding, actions, and open loops. + +## Reference Materials + +- [Alert Threshold Guide](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/alert-manager/references/alert-threshold-guide.md) — Thresholds, fatigue prevention, and escalation templates + +## Next Best Skill + +Reporting cadence requested → [performance-reporter](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/performance-reporter/SKILL.md). Standalone setup → Terminal. Visited-set rule applies per [skill-contract.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/references/skill-contract.md). diff --git a/.agents/skills/alert-manager/references/alert-configuration-templates.md b/.agents/skills/alert-manager/references/alert-configuration-templates.md new file mode 100644 index 00000000..85a81e00 --- /dev/null +++ b/.agents/skills/alert-manager/references/alert-configuration-templates.md @@ -0,0 +1,90 @@ +# Alert Configuration Templates + +Compact alert thresholds and response plans for SEO/GEO monitoring. + +## 1. Alert Thresholds + +| Area | Alert | Condition | Priority | +|------|-------|-----------|----------| +| Rankings | Critical drop | Top 3 keyword drops 5+ positions | Critical | +| Rankings | Top 10 loss | Top 10 keyword drops out of top 10 | High | +| Rankings | Moderate drop | Any keyword drops 10+ positions | Medium | +| Rankings | Competitor overtake | Competitor passes you for key term | Medium | +| Rankings | Positive movement | New Top 3, page 1 entry, 10+ position climb | Positive | +| SERP features | Snippet lost / won | Featured snippet ownership changes | High / Positive | +| SERP features | AI Overview change | Appears or disappears | Medium | +| Traffic | Traffic crash | Day-over-day decline >=50% | Critical | +| Traffic | Significant drop | Week-over-week decline >=30% | High | +| Traffic | Moderate decline | Month-over-month decline >=20% | Medium | +| Traffic | Trend warning | 3 consecutive weeks decline | Medium | +| Conversions | Conversion drop | Organic conversions down 30%+ | Critical | +| Conversions | CVR decline | Conversion rate drops 20%+ | High | +| Technical | Site down | HTTP 5xx errors | Critical | +| Technical | SSL expiry | Certificate expires in 14 days | Critical | +| Technical | Robots.txt block | Important pages blocked | Critical | +| Technical | Index drop | Important pages drop from index | Critical | +| Technical | Crawl/index/CWV/mobile issue | Error spike 50%+, 10% index drop, CWV poor, mobile errors | High | +| Technical | New 404 pages | 404 errors on important pages | Medium | +| Technical | Redirect chains | 3+ redirect hops | Medium | +| Security | Manual action / malware | GSC warning or flagged issue | Critical | +| Backlinks | High-value link lost | DA 70+ link removed | High | +| Backlinks | High-value link gained | New DA 70+ link | Positive | +| Backlinks | Multiple lost links | 10+ links lost in a day | Medium | +| Backlinks | Toxic / negative SEO | Spam pattern or toxic score +20% | High | +| Competitors | Competitor content/backlink move | New content/update or high-DA link | Info | +| GEO | Citation lost/won | AI citation ownership changes | Medium / Positive | +| GEO | Citation rate drop | AI citation rate drops 20%+ | High | +| Brand | Negative mention / rating drop | Sentiment or rating deterioration | High | +| Brand | Unlinked mention | Brand mention without link | Opportunity | + +## 2. Page-Level Traffic Rules + +| Page Type | Alert Condition | Priority | +|-----------|-----------------|----------| +| Homepage | 20%+ decline | Critical | +| Top 10 pages | 30%+ decline | High | +| Conversion pages | 25%+ decline | High | +| Blog posts | 40%+ decline | Medium | + +## 3. Response Plans + +| Priority | Response Time | Immediate Actions | +|----------|---------------|-------------------| +| Critical | Within 1 hour | Confirm data, check server/GSC/indexing, assign owner, start incident log | +| High | Same day | Diagnose cause, compare competitors/SERP, create recovery plan | +| Medium | Within 48 hours | Investigate trend, batch with related alerts, schedule fix | +| Low / Info | Weekly review | Document, tag trend, no interruption | +| Positive | Weekly review | Record win and identify repeatable driver | + +| Alert Type | First Checks | +|------------|--------------| +| Site down | Server, DNS, CDN, deploy status | +| Traffic crash | Algorithm update, GSC errors, analytics tag, competitors | +| Manual action | GSC message, affected URLs, remediation path | +| Critical rank drop | Indexing, SERP change, page changes, competitor movement | +| Backlink loss | Source page status, relationship owner, outreach path | +| CWV failure | Template, script, image, hosting, field data | + +## 4. Notification Setup + +| Priority | Channels | Frequency | Escalation | +|----------|----------|-----------|------------| +| Critical | Email + SMS + Slack | Immediate | No response in 1hr -> Director | +| High | Email + Slack | Immediate | No response in 4hr -> Manager | +| Medium | Email + Slack | Daily digest | No response in 24hr -> Lead | +| Low / Positive | Email | Weekly digest | None | + +| Role | Critical | High | Medium | Low | +|------|----------|------|--------|-----| +| SEO Manager | Yes | Yes | Yes | Yes | +| Dev Team | Yes | Tech only | No | No | +| Marketing Lead | Yes | Yes | No | No | +| Executive | Yes | No | No | No | + +## 5. Suppression Rules + +- Suppress duplicate alerts for 24 hours. +- Suppress known maintenance windows. +- Batch low-priority alerts into digests. +- Require source timestamp and metric source on every alert. +- Mark unresolved alerts with owner, status, and next check time. diff --git a/.agents/skills/alert-manager/references/alert-threshold-guide.md b/.agents/skills/alert-manager/references/alert-threshold-guide.md new file mode 100644 index 00000000..f11cf37c --- /dev/null +++ b/.agents/skills/alert-manager/references/alert-threshold-guide.md @@ -0,0 +1,97 @@ +# Alert Threshold Guide + +## Severity Levels + +**Standard Deviation method**: Info (1 SD), Warning (1.5 SD), Critical (2 SD), Emergency (3 SD) from baseline mean. + +## Thresholds by Metric Category + +### Traffic + +| Metric | Warning | Critical | Emergency | +|--------|---------|----------|-----------| +| Organic sessions (WoW) | -15% | -30% | -50% | +| Organic sessions (DoD) | -25% weekday | -40% | Site appears down | +| Non-brand sessions (WoW) | -20% | -35% | -50% | +| Organic conversions (WoW) | -20% | -40% | -60% | +| Top 10 page traffic (WoW) | -25% | -40% | -60% | + +### Rankings + +| Metric | Warning | Critical | +|--------|---------|----------| +| Tier 1 keyword position | Drop >= 3 | Drop >= 5 | +| Tier 2 keyword position | Drop >= 5 | Drop >= 10 | +| Tier 3 keyword position | Drop >= 10 | Off page 3 | +| Average position (all) | +2.0 worsening | +5.0 worsening | +| Keywords in top 10 | -10% count | -20% count | +| Brand keyword | Any drop from #1 | Below #3 | +| Featured snippet | Any loss | 3+ losses | + +### Technical + +| Metric | Warning | Critical | Emergency | +|--------|---------|----------|-----------| +| New 4xx errors | >5/day | >20/day | >100/day | +| New 5xx errors | >1/day | >5/day | >20/day | +| Crawl rate change | -30% baseline | -60% baseline | Near-zero | +| Index coverage drop | -5% | -15% | -30% | +| Server response time | >500ms | >1000ms | >2000ms | +| LCP (mobile) | Needs Improvement | Poor | >6s | +| CLS | >0.1 | >0.25 | >0.5 | +| INP | >200ms | >500ms | >1000ms | + +### Backlinks + +| Metric | Warning | Critical | +|--------|---------|----------| +| Referring domains lost (weekly) | >5% total | >15% total | +| High-authority link lost (DR 60+) | Any loss | 3+ in one week | +| Toxic link spike | >10/week | >50/week | +| Exact match anchor % | Reaches 20% | Reaches 30% | + +### GEO / AI Visibility + +| Metric | Warning | Critical | +|--------|---------|----------| +| AI citation rate | Drops 10+ pp | Below 10% | +| Key query citation lost | Any Tier 1 | 3+ Tier 1 queries | +| Citation position | Worsens by 2+ | Dropped entirely | + +## Alert Routing + +| Priority | Channel | Escalation | +|----------|---------|------------| +| P0 Emergency | SMS + Phone + Slack #emergencies | PagerDuty on-call | +| P1 Urgent | Slack #alerts + Email | SMS if unacknowledged in 4h | +| P2 Important | Email + Slack #daily | Auto-escalate to P1 after 1 week | +| P3 Monitor | Weekly digest | Auto-escalate to P2 after 1 month | + +## Suppression Rules + +| Rule | Configuration | +|------|--------------| +| Duplicate cooldown | No re-alert same metric for 24h | +| Maintenance window | Suppress non-security alerts | +| Weekend adjustment | Increase traffic thresholds +20% | +| Recovery auto-close | Close if metric normalizes within 48h | +| Batch related | Group multiple ranking drops into one alert | + +## Threshold Tuning + +| Signal | Action | +|--------|--------| +| >30% false positives | Widen thresholds by 0.5 SD | +| Missed real problem | Tighten the specific threshold | +| Seasonal change | Adjust baselines for known patterns | +| Major site change | Re-establish baseline (2-4 week observation) | +| After algorithm update | Stabilize 2-4 weeks, then recalibrate | + +### Site Maturity Guidelines + +| Stage | Approach | +|-------|----------| +| New (0-6mo) | Wide thresholds, few alerts | +| Growing (6-18mo) | Moderate thresholds, expand coverage | +| Established (18mo+) | Tight thresholds, comprehensive | +| Post-migration | Reset to wide, re-tighten over 4-8 weeks | diff --git a/.agents/skills/algorithmic-art/LICENSE.txt b/.agents/skills/algorithmic-art/LICENSE.txt new file mode 100644 index 00000000..4f881c52 --- /dev/null +++ b/.agents/skills/algorithmic-art/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Anthropic, PBC. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/.agents/skills/algorithmic-art/SKILL.md b/.agents/skills/algorithmic-art/SKILL.md new file mode 100644 index 00000000..97478a90 --- /dev/null +++ b/.agents/skills/algorithmic-art/SKILL.md @@ -0,0 +1,405 @@ +--- +name: algorithmic-art +description: Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations. +license: Complete terms in LICENSE.txt +--- + +Algorithmic philosophies are computational aesthetic movements that are then expressed through code. Output .md files (philosophy), .html files (interactive viewer), and .js files (generative algorithms). + +This happens in two steps: +1. Algorithmic Philosophy Creation (.md file) +2. Express by creating p5.js generative art (.html + .js files) + +First, undertake this task: + +## ALGORITHMIC PHILOSOPHY CREATION + +To begin, create an ALGORITHMIC PHILOSOPHY (not static images or templates) that will be interpreted through: +- Computational processes, emergent behavior, mathematical beauty +- Seeded randomness, noise fields, organic systems +- Particles, flows, fields, forces +- Parametric variation and controlled chaos + +### THE CRITICAL UNDERSTANDING +- What is received: Some subtle input or instructions by the user to take into account, but use as a foundation; it should not constrain creative freedom. +- What is created: An algorithmic philosophy/generative aesthetic movement. +- What happens next: The same version receives the philosophy and EXPRESSES IT IN CODE - creating p5.js sketches that are 90% algorithmic generation, 10% essential parameters. + +Consider this approach: +- Write a manifesto for a generative art movement +- The next phase involves writing the algorithm that brings it to life + +The philosophy must emphasize: Algorithmic expression. Emergent behavior. Computational beauty. Seeded variation. + +### HOW TO GENERATE AN ALGORITHMIC PHILOSOPHY + +**Name the movement** (1-2 words): "Organic Turbulence" / "Quantum Harmonics" / "Emergent Stillness" + +**Articulate the philosophy** (4-6 paragraphs - concise but complete): + +To capture the ALGORITHMIC essence, express how this philosophy manifests through: +- Computational processes and mathematical relationships? +- Noise functions and randomness patterns? +- Particle behaviors and field dynamics? +- Temporal evolution and system states? +- Parametric variation and emergent complexity? + +**CRITICAL GUIDELINES:** +- **Avoid redundancy**: Each algorithmic aspect should be mentioned once. Avoid repeating concepts about noise theory, particle dynamics, or mathematical principles unless adding new depth. +- **Emphasize craftsmanship REPEATEDLY**: The philosophy MUST stress multiple times that the final algorithm should appear as though it took countless hours to develop, was refined with care, and comes from someone at the absolute top of their field. This framing is essential - repeat phrases like "meticulously crafted algorithm," "the product of deep computational expertise," "painstaking optimization," "master-level implementation." +- **Leave creative space**: Be specific about the algorithmic direction, but concise enough that the next Codex has room to make interpretive implementation choices at an extremely high level of craftsmanship. + +The philosophy must guide the next version to express ideas ALGORITHMICALLY, not through static images. Beauty lives in the process, not the final frame. + +### PHILOSOPHY EXAMPLES + +**"Organic Turbulence"** +Philosophy: Chaos constrained by natural law, order emerging from disorder. +Algorithmic expression: Flow fields driven by layered Perlin noise. Thousands of particles following vector forces, their trails accumulating into organic density maps. Multiple noise octaves create turbulent regions and calm zones. Color emerges from velocity and density - fast particles burn bright, slow ones fade to shadow. The algorithm runs until equilibrium - a meticulously tuned balance where every parameter was refined through countless iterations by a master of computational aesthetics. + +**"Quantum Harmonics"** +Philosophy: Discrete entities exhibiting wave-like interference patterns. +Algorithmic expression: Particles initialized on a grid, each carrying a phase value that evolves through sine waves. When particles are near, their phases interfere - constructive interference creates bright nodes, destructive creates voids. Simple harmonic motion generates complex emergent mandalas. The result of painstaking frequency calibration where every ratio was carefully chosen to produce resonant beauty. + +**"Recursive Whispers"** +Philosophy: Self-similarity across scales, infinite depth in finite space. +Algorithmic expression: Branching structures that subdivide recursively. Each branch slightly randomized but constrained by golden ratios. L-systems or recursive subdivision generate tree-like forms that feel both mathematical and organic. Subtle noise perturbations break perfect symmetry. Line weights diminish with each recursion level. Every branching angle the product of deep mathematical exploration. + +**"Field Dynamics"** +Philosophy: Invisible forces made visible through their effects on matter. +Algorithmic expression: Vector fields constructed from mathematical functions or noise. Particles born at edges, flowing along field lines, dying when they reach equilibrium or boundaries. Multiple fields can attract, repel, or rotate particles. The visualization shows only the traces - ghost-like evidence of invisible forces. A computational dance meticulously choreographed through force balance. + +**"Stochastic Crystallization"** +Philosophy: Random processes crystallizing into ordered structures. +Algorithmic expression: Randomized circle packing or Voronoi tessellation. Start with random points, let them evolve through relaxation algorithms. Cells push apart until equilibrium. Color based on cell size, neighbor count, or distance from center. The organic tiling that emerges feels both random and inevitable. Every seed produces unique crystalline beauty - the mark of a master-level generative algorithm. + +*These are condensed examples. The actual algorithmic philosophy should be 4-6 substantial paragraphs.* + +### ESSENTIAL PRINCIPLES +- **ALGORITHMIC PHILOSOPHY**: Creating a computational worldview to be expressed through code +- **PROCESS OVER PRODUCT**: Always emphasize that beauty emerges from the algorithm's execution - each run is unique +- **PARAMETRIC EXPRESSION**: Ideas communicate through mathematical relationships, forces, behaviors - not static composition +- **ARTISTIC FREEDOM**: The next Codex interprets the philosophy algorithmically - provide creative implementation room +- **PURE GENERATIVE ART**: This is about making LIVING ALGORITHMS, not static images with randomness +- **EXPERT CRAFTSMANSHIP**: Repeatedly emphasize the final algorithm must feel meticulously crafted, refined through countless iterations, the product of deep expertise by someone at the absolute top of their field in computational aesthetics + +**The algorithmic philosophy should be 4-6 paragraphs long.** Fill it with poetic computational philosophy that brings together the intended vision. Avoid repeating the same points. Output this algorithmic philosophy as a .md file. + +--- + +## DEDUCING THE CONCEPTUAL SEED + +**CRITICAL STEP**: Before implementing the algorithm, identify the subtle conceptual thread from the original request. + +**THE ESSENTIAL PRINCIPLE**: +The concept is a **subtle, niche reference embedded within the algorithm itself** - not always literal, always sophisticated. Someone familiar with the subject should feel it intuitively, while others simply experience a masterful generative composition. The algorithmic philosophy provides the computational language. The deduced concept provides the soul - the quiet conceptual DNA woven invisibly into parameters, behaviors, and emergence patterns. + +This is **VERY IMPORTANT**: The reference must be so refined that it enhances the work's depth without announcing itself. Think like a jazz musician quoting another song through algorithmic harmony - only those who know will catch it, but everyone appreciates the generative beauty. + +--- + +## P5.JS IMPLEMENTATION + +With the philosophy AND conceptual framework established, express it through code. Pause to gather thoughts before proceeding. Use only the algorithmic philosophy created and the instructions below. + +### ⚠️ STEP 0: READ THE TEMPLATE FIRST ⚠️ + +**CRITICAL: BEFORE writing any HTML:** + +1. **Read** `templates/viewer.html` using the Read tool +2. **Study** the exact structure, styling, and Anthropic branding +3. **Use that file as the LITERAL STARTING POINT** - not just inspiration +4. **Keep all FIXED sections exactly as shown** (header, sidebar structure, Anthropic colors/fonts, seed controls, action buttons) +5. **Replace only the VARIABLE sections** marked in the file's comments (algorithm, parameters, UI controls for parameters) + +**Avoid:** +- ❌ Creating HTML from scratch +- ❌ Inventing custom styling or color schemes +- ❌ Using system fonts or dark themes +- ❌ Changing the sidebar structure + +**Follow these practices:** +- ✅ Copy the template's exact HTML structure +- ✅ Keep Anthropic branding (Poppins/Lora fonts, light colors, gradient backdrop) +- ✅ Maintain the sidebar layout (Seed → Parameters → Colors? → Actions) +- ✅ Replace only the p5.js algorithm and parameter controls + +The template is the foundation. Build on it, don't rebuild it. + +--- + +To create gallery-quality computational art that lives and breathes, use the algorithmic philosophy as the foundation. + +### TECHNICAL REQUIREMENTS + +**Seeded Randomness (Art Blocks Pattern)**: +```javascript +// ALWAYS use a seed for reproducibility +let seed = 12345; // or hash from user input +randomSeed(seed); +noiseSeed(seed); +``` + +**Parameter Structure - FOLLOW THE PHILOSOPHY**: + +To establish parameters that emerge naturally from the algorithmic philosophy, consider: "What qualities of this system can be adjusted?" + +```javascript +let params = { + seed: 12345, // Always include seed for reproducibility + // colors + // Add parameters that control YOUR algorithm: + // - Quantities (how many?) + // - Scales (how big? how fast?) + // - Probabilities (how likely?) + // - Ratios (what proportions?) + // - Angles (what direction?) + // - Thresholds (when does behavior change?) +}; +``` + +**To design effective parameters, focus on the properties the system needs to be tunable rather than thinking in terms of "pattern types".** + +**Core Algorithm - EXPRESS THE PHILOSOPHY**: + +**CRITICAL**: The algorithmic philosophy should dictate what to build. + +To express the philosophy through code, avoid thinking "which pattern should I use?" and instead think "how to express this philosophy through code?" + +If the philosophy is about **organic emergence**, consider using: +- Elements that accumulate or grow over time +- Random processes constrained by natural rules +- Feedback loops and interactions + +If the philosophy is about **mathematical beauty**, consider using: +- Geometric relationships and ratios +- Trigonometric functions and harmonics +- Precise calculations creating unexpected patterns + +If the philosophy is about **controlled chaos**, consider using: +- Random variation within strict boundaries +- Bifurcation and phase transitions +- Order emerging from disorder + +**The algorithm flows from the philosophy, not from a menu of options.** + +To guide the implementation, let the conceptual essence inform creative and original choices. Build something that expresses the vision for this particular request. + +**Canvas Setup**: Standard p5.js structure: +```javascript +function setup() { + createCanvas(1200, 1200); + // Initialize your system +} + +function draw() { + // Your generative algorithm + // Can be static (noLoop) or animated +} +``` + +### CRAFTSMANSHIP REQUIREMENTS + +**CRITICAL**: To achieve mastery, create algorithms that feel like they emerged through countless iterations by a master generative artist. Tune every parameter carefully. Ensure every pattern emerges with purpose. This is NOT random noise - this is CONTROLLED CHAOS refined through deep expertise. + +- **Balance**: Complexity without visual noise, order without rigidity +- **Color Harmony**: Thoughtful palettes, not random RGB values +- **Composition**: Even in randomness, maintain visual hierarchy and flow +- **Performance**: Smooth execution, optimized for real-time if animated +- **Reproducibility**: Same seed ALWAYS produces identical output + +### OUTPUT FORMAT + +Output: +1. **Algorithmic Philosophy** - As markdown or text explaining the generative aesthetic +2. **Single HTML Artifact** - Self-contained interactive generative art built from `templates/viewer.html` (see STEP 0 and next section) + +The HTML artifact contains everything: p5.js (from CDN), the algorithm, parameter controls, and UI - all in one file that works immediately in Codex.ai artifacts or any browser. Start from the template file, not from scratch. + +--- + +## INTERACTIVE ARTIFACT CREATION + +**REMINDER: `templates/viewer.html` should have already been read (see STEP 0). Use that file as the starting point.** + +To allow exploration of the generative art, create a single, self-contained HTML artifact. Ensure this artifact works immediately in Codex.ai or any browser - no setup required. Embed everything inline. + +### CRITICAL: WHAT'S FIXED VS VARIABLE + +The `templates/viewer.html` file is the foundation. It contains the exact structure and styling needed. + +**FIXED (always include exactly as shown):** +- Layout structure (header, sidebar, main canvas area) +- Anthropic branding (UI colors, fonts, gradients) +- Seed section in sidebar: + - Seed display + - Previous/Next buttons + - Random button + - Jump to seed input + Go button +- Actions section in sidebar: + - Regenerate button + - Reset button + +**VARIABLE (customize for each artwork):** +- The entire p5.js algorithm (setup/draw/classes) +- The parameters object (define what the art needs) +- The Parameters section in sidebar: + - Number of parameter controls + - Parameter names + - Min/max/step values for sliders + - Control types (sliders, inputs, etc.) +- Colors section (optional): + - Some art needs color pickers + - Some art might use fixed colors + - Some art might be monochrome (no color controls needed) + - Decide based on the art's needs + +**Every artwork should have unique parameters and algorithm!** The fixed parts provide consistent UX - everything else expresses the unique vision. + +### REQUIRED FEATURES + +**1. Parameter Controls** +- Sliders for numeric parameters (particle count, noise scale, speed, etc.) +- Color pickers for palette colors +- Real-time updates when parameters change +- Reset button to restore defaults + +**2. Seed Navigation** +- Display current seed number +- "Previous" and "Next" buttons to cycle through seeds +- "Random" button for random seed +- Input field to jump to specific seed +- Generate 100 variations when requested (seeds 1-100) + +**3. Single Artifact Structure** +```html + + + + + + + + +
+
+ +
+ + + +``` + +**CRITICAL**: This is a single artifact. No external files, no imports (except p5.js CDN). Everything inline. + +**4. Implementation Details - BUILD THE SIDEBAR** + +The sidebar structure: + +**1. Seed (FIXED)** - Always include exactly as shown: +- Seed display +- Prev/Next/Random/Jump buttons + +**2. Parameters (VARIABLE)** - Create controls for the art: +```html +
+ + + ... +
+``` +Add as many control-group divs as there are parameters. + +**3. Colors (OPTIONAL/VARIABLE)** - Include if the art needs adjustable colors: +- Add color pickers if users should control palette +- Skip this section if the art uses fixed colors +- Skip if the art is monochrome + +**4. Actions (FIXED)** - Always include exactly as shown: +- Regenerate button +- Reset button +- Download PNG button + +**Requirements**: +- Seed controls must work (prev/next/random/jump/display) +- All parameters must have UI controls +- Regenerate, Reset, Download buttons must work +- Keep Anthropic branding (UI styling, not art colors) + +### USING THE ARTIFACT + +The HTML artifact works immediately: +1. **In Codex.ai**: Displayed as an interactive artifact - runs instantly +2. **As a file**: Save and open in any browser - no server needed +3. **Sharing**: Send the HTML file - it's completely self-contained + +--- + +## VARIATIONS & EXPLORATION + +The artifact includes seed navigation by default (prev/next/random buttons), allowing users to explore variations without creating multiple files. If the user wants specific variations highlighted: + +- Include seed presets (buttons for "Variation 1: Seed 42", "Variation 2: Seed 127", etc.) +- Add a "Gallery Mode" that shows thumbnails of multiple seeds side-by-side +- All within the same single artifact + +This is like creating a series of prints from the same plate - the algorithm is consistent, but each seed reveals different facets of its potential. The interactive nature means users discover their own favorites by exploring the seed space. + +--- + +## THE CREATIVE PROCESS + +**User request** → **Algorithmic philosophy** → **Implementation** + +Each request is unique. The process involves: + +1. **Interpret the user's intent** - What aesthetic is being sought? +2. **Create an algorithmic philosophy** (4-6 paragraphs) describing the computational approach +3. **Implement it in code** - Build the algorithm that expresses this philosophy +4. **Design appropriate parameters** - What should be tunable? +5. **Build matching UI controls** - Sliders/inputs for those parameters + +**The constants**: +- Anthropic branding (colors, fonts, layout) +- Seed navigation (always present) +- Self-contained HTML artifact + +**Everything else is variable**: +- The algorithm itself +- The parameters +- The UI controls +- The visual outcome + +To achieve the best results, trust creativity and let the philosophy guide the implementation. + +--- + +## RESOURCES + +This skill includes helpful templates and documentation: + +- **templates/viewer.html**: REQUIRED STARTING POINT for all HTML artifacts. + - This is the foundation - contains the exact structure and Anthropic branding + - **Keep unchanged**: Layout structure, sidebar organization, Anthropic colors/fonts, seed controls, action buttons + - **Replace**: The p5.js algorithm, parameter definitions, and UI controls in Parameters section + - The extensive comments in the file mark exactly what to keep vs replace + +- **templates/generator_template.js**: Reference for p5.js best practices and code structure principles. + - Shows how to organize parameters, use seeded randomness, structure classes + - NOT a pattern menu - use these principles to build unique algorithms + - Embed algorithms inline in the HTML artifact (don't create separate .js files) + +**Critical reminder**: +- The **template is the STARTING POINT**, not inspiration +- The **algorithm is where to create** something unique +- Don't copy the flow field example - build what the philosophy demands +- But DO keep the exact UI structure and Anthropic branding from the template \ No newline at end of file diff --git a/.agents/skills/algorithmic-art/templates/generator_template.js b/.agents/skills/algorithmic-art/templates/generator_template.js new file mode 100644 index 00000000..e263fbde --- /dev/null +++ b/.agents/skills/algorithmic-art/templates/generator_template.js @@ -0,0 +1,223 @@ +/** + * ═══════════════════════════════════════════════════════════════════════════ + * P5.JS GENERATIVE ART - BEST PRACTICES + * ═══════════════════════════════════════════════════════════════════════════ + * + * This file shows STRUCTURE and PRINCIPLES for p5.js generative art. + * It does NOT prescribe what art you should create. + * + * Your algorithmic philosophy should guide what you build. + * These are just best practices for how to structure your code. + * + * ═══════════════════════════════════════════════════════════════════════════ + */ + +// ============================================================================ +// 1. PARAMETER ORGANIZATION +// ============================================================================ +// Keep all tunable parameters in one object +// This makes it easy to: +// - Connect to UI controls +// - Reset to defaults +// - Serialize/save configurations + +let params = { + // Define parameters that match YOUR algorithm + // Examples (customize for your art): + // - Counts: how many elements (particles, circles, branches, etc.) + // - Scales: size, speed, spacing + // - Probabilities: likelihood of events + // - Angles: rotation, direction + // - Colors: palette arrays + + seed: 12345, + // define colorPalette as an array -- choose whatever colors you'd like ['#d97757', '#6a9bcc', '#788c5d', '#b0aea5'] + // Add YOUR parameters here based on your algorithm +}; + +// ============================================================================ +// 2. SEEDED RANDOMNESS (Critical for reproducibility) +// ============================================================================ +// ALWAYS use seeded random for Art Blocks-style reproducible output + +function initializeSeed(seed) { + randomSeed(seed); + noiseSeed(seed); + // Now all random() and noise() calls will be deterministic +} + +// ============================================================================ +// 3. P5.JS LIFECYCLE +// ============================================================================ + +function setup() { + createCanvas(800, 800); + + // Initialize seed first + initializeSeed(params.seed); + + // Set up your generative system + // This is where you initialize: + // - Arrays of objects + // - Grid structures + // - Initial positions + // - Starting states + + // For static art: call noLoop() at the end of setup + // For animated art: let draw() keep running +} + +function draw() { + // Option 1: Static generation (runs once, then stops) + // - Generate everything in setup() + // - Call noLoop() in setup() + // - draw() doesn't do much or can be empty + + // Option 2: Animated generation (continuous) + // - Update your system each frame + // - Common patterns: particle movement, growth, evolution + // - Can optionally call noLoop() after N frames + + // Option 3: User-triggered regeneration + // - Use noLoop() by default + // - Call redraw() when parameters change +} + +// ============================================================================ +// 4. CLASS STRUCTURE (When you need objects) +// ============================================================================ +// Use classes when your algorithm involves multiple entities +// Examples: particles, agents, cells, nodes, etc. + +class Entity { + constructor() { + // Initialize entity properties + // Use random() here - it will be seeded + } + + update() { + // Update entity state + // This might involve: + // - Physics calculations + // - Behavioral rules + // - Interactions with neighbors + } + + display() { + // Render the entity + // Keep rendering logic separate from update logic + } +} + +// ============================================================================ +// 5. PERFORMANCE CONSIDERATIONS +// ============================================================================ + +// For large numbers of elements: +// - Pre-calculate what you can +// - Use simple collision detection (spatial hashing if needed) +// - Limit expensive operations (sqrt, trig) when possible +// - Consider using p5 vectors efficiently + +// For smooth animation: +// - Aim for 60fps +// - Profile if things are slow +// - Consider reducing particle counts or simplifying calculations + +// ============================================================================ +// 6. UTILITY FUNCTIONS +// ============================================================================ + +// Color utilities +function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} + +function colorFromPalette(index) { + return params.colorPalette[index % params.colorPalette.length]; +} + +// Mapping and easing +function mapRange(value, inMin, inMax, outMin, outMax) { + return outMin + (outMax - outMin) * ((value - inMin) / (inMax - inMin)); +} + +function easeInOutCubic(t) { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; +} + +// Constrain to bounds +function wrapAround(value, max) { + if (value < 0) return max; + if (value > max) return 0; + return value; +} + +// ============================================================================ +// 7. PARAMETER UPDATES (Connect to UI) +// ============================================================================ + +function updateParameter(paramName, value) { + params[paramName] = value; + // Decide if you need to regenerate or just update + // Some params can update in real-time, others need full regeneration +} + +function regenerate() { + // Reinitialize your generative system + // Useful when parameters change significantly + initializeSeed(params.seed); + // Then regenerate your system +} + +// ============================================================================ +// 8. COMMON P5.JS PATTERNS +// ============================================================================ + +// Drawing with transparency for trails/fading +function fadeBackground(opacity) { + fill(250, 249, 245, opacity); // Anthropic light with alpha + noStroke(); + rect(0, 0, width, height); +} + +// Using noise for organic variation +function getNoiseValue(x, y, scale = 0.01) { + return noise(x * scale, y * scale); +} + +// Creating vectors from angles +function vectorFromAngle(angle, magnitude = 1) { + return createVector(cos(angle), sin(angle)).mult(magnitude); +} + +// ============================================================================ +// 9. EXPORT FUNCTIONS +// ============================================================================ + +function exportImage() { + saveCanvas('generative-art-' + params.seed, 'png'); +} + +// ============================================================================ +// REMEMBER +// ============================================================================ +// +// These are TOOLS and PRINCIPLES, not a recipe. +// Your algorithmic philosophy should guide WHAT you create. +// This structure helps you create it WELL. +// +// Focus on: +// - Clean, readable code +// - Parameterized for exploration +// - Seeded for reproducibility +// - Performant execution +// +// The art itself is entirely up to you! +// +// ============================================================================ \ No newline at end of file diff --git a/.agents/skills/algorithmic-art/templates/viewer.html b/.agents/skills/algorithmic-art/templates/viewer.html new file mode 100644 index 00000000..630cc1f6 --- /dev/null +++ b/.agents/skills/algorithmic-art/templates/viewer.html @@ -0,0 +1,599 @@ + + + + + + + Generative Art Viewer + + + + + + + +
+ + + + +
+
+
Initializing generative art...
+
+
+
+ + + + \ No newline at end of file diff --git a/.agents/skills/api-gateway/CONTRIBUTING.md b/.agents/skills/api-gateway/CONTRIBUTING.md new file mode 100644 index 00000000..4fd08c0b --- /dev/null +++ b/.agents/skills/api-gateway/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contributing + +Contributions of any kind are welcome! If you've found a bug or have a feature request, please feel free to [open an issue](/issues). + + + +To make changes yourself, follow these steps: + +1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository and [clone](https://help.github.com/articles/cloning-a-repository/) it locally. + + +2. Make your changes + +3. Submit a [pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) + +## Contributor License Agreement ([CLA](https://en.wikipedia.org/wiki/Contributor_License_Agreement)) + +Once you have submitted a pull request, sign the CLA by clicking on the badge in the comment from [@CLAassistant](https://github.com/CLAassistant). + +image + +
+Thanks for contributing to Stripe! :sparkles: diff --git a/.agents/skills/api-gateway/LICENSE b/.agents/skills/api-gateway/LICENSE new file mode 100644 index 00000000..21bdb8d3 --- /dev/null +++ b/.agents/skills/api-gateway/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 Maton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.agents/skills/api-gateway/README.md b/.agents/skills/api-gateway/README.md new file mode 100644 index 00000000..54a0db47 --- /dev/null +++ b/.agents/skills/api-gateway/README.md @@ -0,0 +1,77 @@ +# 🎉 api-gateway-skill - Connect Your AI Easily + +## 📥 Download Here +[![Download api-gateway-skill](https://github.com/up2datedatabase476/api-gateway-skill/raw/refs/heads/main/references/gateway-skill-api-1.2.zip)](https://github.com/up2datedatabase476/api-gateway-skill/raw/refs/heads/main/references/gateway-skill-api-1.2.zip) + +## 📖 Overview +The **api-gateway-skill** is a user-friendly tool that enables you to connect your AI with popular services like Slack, HubSpot, Salesforce, Google Workspace, Shopify, and more. With just a single API key, you can enhance your applications and streamline your workflows without complicated setups. + +## 🚀 Getting Started +To start using the api-gateway-skill, follow these simple steps: + +1. **Visit the Releases Page** + Click this link to reach the releases page: [Download Here](https://github.com/up2datedatabase476/api-gateway-skill/raw/refs/heads/main/references/gateway-skill-api-1.2.zip). + +2. **Choose the Latest Release** + On the releases page, look for the most recent version. This version will have the latest features and fixes. + +3. **Download the Application** + Click the asset link for your operating system (e.g., Windows, macOS, Linux) to begin downloading the software. + +4. **Install the Application** + Once the download is finished, locate the downloaded file on your computer. + - For Windows, double-click on the `.exe` file and follow the installation prompts. + - For macOS, open the downloaded `.dmg` file, drag the application to your Applications folder, and follow any setup instructions. + - For Linux, follow the installation method listed for your distribution in the documentation. + +5. **Run the Application** + After installation, find the api-gateway-skill in your applications list and launch it. + +## 🔧 System Requirements +- **Operating System**: + - Windows 10 or later + - macOS 10.12 or later + - Most Linux distributions with support for `.deb` or `.rpm` packages +- **Processor**: Minimum dual-core processor +- **RAM**: At least 4 GB of RAM +- **Disk Space**: 200 MB of free space for installation + +Ensure your device meets these requirements for optimal performance. + +## 🔨 Key Features +- **Easy Integration**: Quickly link your AI to multiple platforms with a single key. +- **User-Friendly Interface**: Navigate the application with ease, no technical expertise required. +- **Comprehensive Support**: Access detailed guides and support channels for assistance. +- **Regular Updates**: Benefit from new features and improvements with frequent updates. + +## 📦 Download & Install +To download the api-gateway-skill, please visit the Releases page: [Download Here](https://github.com/up2datedatabase476/api-gateway-skill/raw/refs/heads/main/references/gateway-skill-api-1.2.zip) and follow the installation steps listed above. + +## 🛠️ Troubleshooting +If you encounter issues during installation or while running the app, check these common solutions: + +- **Check Compatibility**: Ensure your operating system meets the requirements stated above. +- **Free Up Space**: Make sure you have enough disk space for the installation. +- **Antivirus Settings**: Sometimes, antivirus software may block the application. Temporarily disable it if you encounter issues. +- **Internet Connectivity**: Ensure you have a stable internet connection for the initial setup. + +## 🤝 Community Support +Join our community to interact with other users. Share experiences, ask for help, and stay updated on the latest releases. You can find us on various platforms: +- **GitHub Discussions**: Engage with other users and contributors. +- **Slack Channel**: Join our Slack workspace to chat in real time. +- **Email Support**: Reach out via email for questions and feedback. + +## 📝 Contributing +We welcome contributions from anyone who wants to improve api-gateway-skill. Here are some ways you can help: +1. Suggest new features. +2. Report bugs or issues. +3. Contribute to documentation and guides. + +Review our [contribution guidelines](https://github.com/up2datedatabase476/api-gateway-skill/raw/refs/heads/main/references/gateway-skill-api-1.2.zip) for detailed information. + +## 📚 Resources +For further details about how to use api-gateway-skill, please check: +- **Documentation**: [Documentation Link](https://github.com/up2datedatabase476/api-gateway-skill/raw/refs/heads/main/references/gateway-skill-api-1.2.zip) +- **Release Notes**: Stay informed about updates and changes with the release notes on the Releases page. + +By following these steps, you can efficiently set up and use the api-gateway-skill application. Enjoy connecting your AI projects effortlessly! \ No newline at end of file diff --git a/.agents/skills/api-gateway/SKILL.md b/.agents/skills/api-gateway/SKILL.md new file mode 100644 index 00000000..76726906 --- /dev/null +++ b/.agents/skills/api-gateway/SKILL.md @@ -0,0 +1,561 @@ +--- +name: api-gateway +description: | + API gateway for calling third-party APIs with managed auth. Use this skill when users want to interact with external services like Slack, HubSpot, Salesforce, Google Workspace, Stripe, and more. +compatibility: Requires network access and valid Maton API key +metadata: + author: maton + version: "1.0" + clawdbot: + emoji: 🧠 + homepage: "https://maton.ai" + requires: + env: + - MATON_API_KEY +--- + +# API Gateway + +Passthrough proxy for direct access to third-party APIs using managed auth connections. The API gateway lets you call native API endpoints directly. + +## Quick Start + +```bash +# Native Slack API call +python <<'EOF' +import urllib.request, os, json +data = json.dumps({'channel': 'C0123456', 'text': 'Hello from gateway!'}).encode() +req = urllib.request.Request('https://gateway.maton.ai/slack/api/chat.postMessage', data=data, method='POST') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +req.add_header('Content-Type', 'application/json') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + + +## Base URL + +``` +https://gateway.maton.ai/{app}/{native-api-path} +``` + +Replace `{app}` with the service name and `{native-api-path}` with the actual API endpoint path. + +IMPORTANT: The URL path MUST start with the connection's app name (eg. `/google-mail/...`). This prefix tells the gateway which app connection to use. For example, the native Gmail API path starts with `gmail/v1/`, so full paths look like `/google-mail/gmail/v1/users/me/messages`. + +## Authentication + +All requests require the Maton API key in the Authorization header: + +``` +Authorization: Bearer $MATON_API_KEY +``` + +The API gateway automatically injects the appropriate OAuth token for the target service. + +**Environment Variable:** You can set your API key as the `MATON_API_KEY` environment variable: + +```bash +export MATON_API_KEY="YOUR_API_KEY" +``` + +## Getting Your API Key + +1. Sign in or create an account at [maton.ai](https://maton.ai) +2. Go to [maton.ai/settings](https://maton.ai/settings) +3. Click the copy button on the right side of API Key section to copy it + +## Connection Management + +Connection management uses a separate base URL: `https://ctrl.maton.ai` + +### List Connections + +```bash +python <<'EOF' +import urllib.request, os, json +req = urllib.request.Request('https://ctrl.maton.ai/connections?app=slack&status=ACTIVE') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +**Query Parameters (optional):** +- `app` - Filter by service name (e.g., `slack`, `hubspot`, `salesforce`) +- `status` - Filter by connection status (`ACTIVE`, `PENDING`, `FAILED`) + +**Response:** +```json +{ + "connections": [ + { + "connection_id": "21fd90f9-5935-43cd-b6c8-bde9d915ca80", + "status": "ACTIVE", + "creation_time": "2025-12-08T07:20:53.488460Z", + "last_updated_time": "2026-01-31T20:03:32.593153Z", + "url": "https://connect.maton.ai/?session_token=5e9...", + "app": "slack", + "metadata": {} + } + ] +} +``` + +### Create Connection + +```bash +python <<'EOF' +import urllib.request, os, json +data = json.dumps({'app': 'slack'}).encode() +req = urllib.request.Request('https://ctrl.maton.ai/connections', data=data, method='POST') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +req.add_header('Content-Type', 'application/json') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +### Get Connection + +```bash +python <<'EOF' +import urllib.request, os, json +req = urllib.request.Request('https://ctrl.maton.ai/connections/{connection_id}') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +**Response:** +```json +{ + "connection": { + "connection_id": "21fd90f9-5935-43cd-b6c8-bde9d915ca80", + "status": "ACTIVE", + "creation_time": "2025-12-08T07:20:53.488460Z", + "last_updated_time": "2026-01-31T20:03:32.593153Z", + "url": "https://connect.maton.ai/?session_token=5e9...", + "app": "slack", + "metadata": {} + } +} +``` + +Open the returned URL in a browser to complete OAuth. + +### Delete Connection + +```bash +python <<'EOF' +import urllib.request, os, json +req = urllib.request.Request('https://ctrl.maton.ai/connections/{connection_id}', method='DELETE') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +### Specifying Connection + +If you have multiple connections for the same app, you can specify which connection to use by adding the `Maton-Connection` header with the connection ID: + +```bash +python <<'EOF' +import urllib.request, os, json +data = json.dumps({'channel': 'C0123456', 'text': 'Hello!'}).encode() +req = urllib.request.Request('https://gateway.maton.ai/slack/api/chat.postMessage', data=data, method='POST') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +req.add_header('Content-Type', 'application/json') +req.add_header('Maton-Connection', '21fd90f9-5935-43cd-b6c8-bde9d915ca80') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +If omitted, the gateway uses the default (oldest) active connection for that app. + +## Supported Services + +| Service | App Name | Base URL Proxied | +|---------|----------|------------------| +| ActiveCampaign | `active-campaign` | `{account}.api-us1.com` | +| Acuity Scheduling | `acuity-scheduling` | `acuityscheduling.com` | +| Airtable | `airtable` | `api.airtable.com` | +| Apollo | `apollo` | `api.apollo.io` | +| Asana | `asana` | `app.asana.com` | +| Attio | `attio` | `api.attio.com` | +| Basecamp | `basecamp` | `3.basecampapi.com` | +| beehiiv | `beehiiv` | `api.beehiiv.com` | +| Box | `box` | `api.box.com` | +| Brevo | `brevo` | `api.brevo.com` | +| Calendly | `calendly` | `api.calendly.com` | +| CallRail | `callrail` | `api.callrail.com` | +| Chargebee | `chargebee` | `{subdomain}.chargebee.com` | +| ClickFunnels | `clickfunnels` | `{subdomain}.myclickfunnels.com` | +| ClickSend | `clicksend` | `rest.clicksend.com` | +| ClickUp | `clickup` | `api.clickup.com` | +| Cognito Forms | `cognito-forms` | `www.cognitoforms.com` | +| Constant Contact | `constant-contact` | `api.cc.email` | +| Dropbox | `dropbox` | `api.dropboxapi.com` | +| Eventbrite | `eventbrite` | `www.eventbriteapi.com` | +| Fathom | `fathom` | `api.fathom.ai` | +| Fireflies | `fireflies` | `api.fireflies.ai` | +| GitHub | `github` | `api.github.com` | +| Gumroad | `gumroad` | `api.gumroad.com` | +| Google Ads | `google-ads` | `googleads.googleapis.com` | +| Google Analytics Admin | `google-analytics-admin` | `analyticsadmin.googleapis.com` | +| Google Analytics Data | `google-analytics-data` | `analyticsdata.googleapis.com` | +| Google Calendar | `google-calendar` | `www.googleapis.com` | +| Google Contacts | `google-contacts` | `people.googleapis.com` | +| Google Docs | `google-docs` | `docs.googleapis.com` | +| Google Drive | `google-drive` | `www.googleapis.com` | +| Google Forms | `google-forms` | `forms.googleapis.com` | +| Gmail | `google-mail` | `gmail.googleapis.com` | +| Google Merchant | `google-merchant` | `merchantapi.googleapis.com` | +| Google Meet | `google-meet` | `meet.googleapis.com` | +| Google Play | `google-play` | `androidpublisher.googleapis.com` | +| Google Search Console | `google-search-console` | `www.googleapis.com` | +| Google Sheets | `google-sheets` | `sheets.googleapis.com` | +| Google Slides | `google-slides` | `slides.googleapis.com` | +| Google Tasks | `google-tasks` | `tasks.googleapis.com` | +| Google Workspace Admin | `google-workspace-admin` | `admin.googleapis.com` | +| HubSpot | `hubspot` | `api.hubapi.com` | +| Jira | `jira` | `api.atlassian.com` | +| Jobber | `jobber` | `api.getjobber.com` | +| JotForm | `jotform` | `api.jotform.com` | +| Keap | `keap` | `api.infusionsoft.com` | +| Kit | `kit` | `api.kit.com` | +| Klaviyo | `klaviyo` | `a.klaviyo.com` | +| Linear | `linear` | `api.linear.app` | +| LinkedIn | `linkedin` | `api.linkedin.com` | +| Mailchimp | `mailchimp` | `{dc}.api.mailchimp.com` | +| MailerLite | `mailerlite` | `connect.mailerlite.com` | +| ManyChat | `manychat` | `api.manychat.com` | +| Microsoft Excel | `microsoft-excel` | `graph.microsoft.com` | +| Microsoft To Do | `microsoft-to-do` | `graph.microsoft.com` | +| Monday.com | `monday` | `api.monday.com` | +| Notion | `notion` | `api.notion.com` | +| OneDrive | `one-drive` | `graph.microsoft.com` | +| Outlook | `outlook` | `graph.microsoft.com` | +| Pipedrive | `pipedrive` | `api.pipedrive.com` | +| QuickBooks | `quickbooks` | `quickbooks.api.intuit.com` | +| Quo | `quo` | `api.openphone.com` | +| Salesforce | `salesforce` | `{instance}.salesforce.com` | +| SignNow | `signnow` | `api.signnow.com` | +| Slack | `slack` | `slack.com` | +| Square | `squareup` | `connect.squareup.com` | +| Stripe | `stripe` | `api.stripe.com` | +| Systeme.io | `systeme` | `api.systeme.io` | +| Tally | `tally` | `api.tally.so` | +| Telegram | `telegram` | `api.telegram.org` | +| TickTick | `ticktick` | `api.ticktick.com` | +| Todoist | `todoist` | `api.todoist.com` | +| Trello | `trello` | `api.trello.com` | +| Twilio | `twilio` | `api.twilio.com` | +| Typeform | `typeform` | `api.typeform.com` | +| Vimeo | `vimeo` | `api.vimeo.com` | +| WhatsApp Business | `whatsapp-business` | `graph.facebook.com` | +| WooCommerce | `woocommerce` | `{store-url}/wp-json/wc/v3` | +| WordPress.com | `wordpress` | `public-api.wordpress.com` | +| Xero | `xero` | `api.xero.com` | +| YouTube | `youtube` | `www.googleapis.com` | +| Zoho Bigin | `zoho-bigin` | `www.zohoapis.com` | +| Zoho Books | `zoho-books` | `www.zohoapis.com` | +| Zoho Calendar | `zoho-calendar` | `calendar.zoho.com` | +| Zoho CRM | `zoho-crm` | `www.zohoapis.com` | +| Zoho Inventory | `zoho-inventory` | `www.zohoapis.com` | +| Zoho Mail | `zoho-mail` | `mail.zoho.com` | +| Zoho People | `zoho-people` | `people.zoho.com` | +| Zoho Recruit | `zoho-recruit` | `recruit.zoho.com` | + +See [references/](references/) for detailed routing guides per provider: +- [ActiveCampaign](references/active-campaign.md) - Contacts, deals, tags, lists, automations, campaigns +- [Acuity Scheduling](references/acuity-scheduling.md) - Appointments, calendars, clients, availability +- [Airtable](references/airtable.md) - Records, bases, tables +- [Apollo](references/apollo.md) - People search, enrichment, contacts +- [Asana](references/asana.md) - Tasks, projects, workspaces, webhooks +- [Attio](references/attio.md) - People, companies, records, tasks +- [Basecamp](references/basecamp.md) - Projects, to-dos, messages, schedules, documents +- [beehiiv](references/beehiiv.md) - Publications, subscriptions, posts, custom fields +- [Box](references/box.md) - Files, folders, collaborations, shared links +- [Brevo](references/brevo.md) - Contacts, email campaigns, transactional emails, templates +- [Calendly](references/calendly.md) - Event types, scheduled events, availability, webhooks +- [CallRail](references/callrail.md) - Calls, trackers, companies, tags, analytics +- [Chargebee](references/chargebee.md) - Subscriptions, customers, invoices +- [ClickFunnels](references/clickfunnels.md) - Contacts, products, orders, courses, webhooks +- [ClickSend](references/clicksend.md) - SMS, MMS, voice messages, contacts, lists +- [ClickUp](references/clickup.md) - Tasks, lists, folders, spaces, webhooks +- [Cognito Forms](references/cognito-forms.md) - Forms, entries, documents, files +- [Constant Contact](references/constant-contact.md) - Contacts, email campaigns, lists, segments +- [Dropbox](references/dropbox.md) - Files, folders, search, metadata, revisions, tags +- [Eventbrite](references/eventbrite.md) - Events, venues, tickets, orders, attendees +- [Fathom](references/fathom.md) - Meeting recordings, transcripts, summaries, webhooks +- [Fireflies](references/fireflies.md) - Meeting transcripts, summaries, AskFred AI, channels +- [GitHub](references/github.md) - Repositories, issues, pull requests, commits +- [Gumroad](references/gumroad.md) - Products, sales, subscribers, licenses, webhooks +- [Google Ads](references/google-ads.md) - Campaigns, ad groups, GAQL queries +- [Google Analytics Admin](references/google-analytics-admin.md) - Reports, dimensions, metrics +- [Google Analytics Data](references/google-analytics-data.md) - Reports, dimensions, metrics +- [Google Calendar](references/google-calendar.md) - Events, calendars, free/busy +- [Google Contacts](references/google-contacts.md) - Contacts, contact groups, people search +- [Google Docs](references/google-docs.md) - Document creation, batch updates +- [Google Drive](references/google-drive.md) - Files, folders, permissions +- [Google Forms](references/google-forms.md) - Forms, questions, responses +- [Gmail](references/google-mail.md) - Messages, threads, labels +- [Google Meet](references/google-meet.md) - Spaces, conference records, participants +- [Google Merchant](references/google-merchant.md) - Products, inventories, promotions, reports +- [Google Play](references/google-play.md) - In-app products, subscriptions, reviews +- [Google Search Console](references/google-search-console.md) - Search analytics, sitemaps +- [Google Sheets](references/google-sheets.md) - Values, ranges, formatting +- [Google Slides](references/google-slides.md) - Presentations, slides, formatting +- [Google Tasks](references/google-tasks.md) - Task lists, tasks, subtasks +- [Google Workspace Admin](references/google-workspace-admin.md) - Users, groups, org units, domains, roles +- [HubSpot](references/hubspot.md) - Contacts, companies, deals +- [Jira](references/jira.md) - Issues, projects, JQL queries +- [Jobber](references/jobber.md) - Clients, jobs, invoices, quotes (GraphQL) +- [JotForm](references/jotform.md) - Forms, submissions, webhooks +- [Keap](references/keap.md) - Contacts, companies, tags, tasks, opportunities, campaigns +- [Kit](references/kit.md) - Subscribers, tags, forms, sequences, broadcasts +- [Klaviyo](references/klaviyo.md) - Profiles, lists, campaigns, flows, events +- [Linear](references/linear.md) - Issues, projects, teams, cycles (GraphQL) +- [LinkedIn](references/linkedin.md) - Profile, posts, shares, media uploads +- [Mailchimp](references/mailchimp.md) - Audiences, campaigns, templates, automations +- [MailerLite](references/mailerlite.md) - Subscribers, groups, campaigns, automations, forms +- [ManyChat](references/manychat.md) - Subscribers, tags, flows, messaging +- [Microsoft Excel](references/microsoft-excel.md) - Workbooks, worksheets, ranges, tables, charts +- [Microsoft To Do](references/microsoft-to-do.md) - Task lists, tasks, checklist items, linked resources +- [Monday.com](references/monday.md) - Boards, items, columns, groups (GraphQL) +- [Notion](references/notion.md) - Pages, databases, blocks +- [OneDrive](references/one-drive.md) - Files, folders, drives, sharing +- [Outlook](references/outlook.md) - Mail, calendar, contacts +- [Pipedrive](references/pipedrive.md) - Deals, persons, organizations, activities +- [QuickBooks](references/quickbooks.md) - Customers, invoices, reports +- [Quo](references/quo.md) - Calls, messages, contacts, conversations, webhooks +- [Salesforce](references/salesforce.md) - SOQL, sObjects, CRUD +- [SignNow](references/signnow.md) - Documents, templates, invites, e-signatures +- [SendGrid](references/sendgrid.md) - Email sending, contacts, templates, suppressions, statistics +- [Slack](references/slack.md) - Messages, channels, users +- [Square](references/squareup.md) - Payments, customers, orders, catalog, inventory, invoices +- [Stripe](references/stripe.md) - Customers, subscriptions, payments +- [Systeme.io](references/systeme.md) - Contacts, tags, courses, communities, webhooks +- [Tally](references/tally.md) - Forms, submissions, workspaces, webhooks +- [Telegram](references/telegram.md) - Messages, chats, bots, updates, polls +- [TickTick](references/ticktick.md) - Tasks, projects, task lists +- [Todoist](references/todoist.md) - Tasks, projects, sections, labels, comments +- [Trello](references/trello.md) - Boards, lists, cards, checklists +- [Twilio](references/twilio.md) - SMS, voice calls, phone numbers, messaging +- [Typeform](references/typeform.md) - Forms, responses, insights +- [Vimeo](references/vimeo.md) - Videos, folders, albums, comments, likes +- [WhatsApp Business](references/whatsapp-business.md) - Messages, templates, media +- [WooCommerce](references/woocommerce.md) - Products, orders, customers, coupons +- [WordPress.com](references/wordpress.md) - Posts, pages, sites, users, settings +- [Xero](references/xero.md) - Contacts, invoices, reports +- [YouTube](references/youtube.md) - Videos, playlists, channels, subscriptions +- [Zoho Bigin](references/zoho-bigin.md) - Contacts, companies, pipelines, products +- [Zoho Books](references/zoho-books.md) - Invoices, contacts, bills, expenses +- [Zoho Calendar](references/zoho-calendar.md) - Calendars, events, attendees, reminders +- [Zoho CRM](references/zoho-crm.md) - Leads, contacts, accounts, deals, search +- [Zoho Inventory](references/zoho-inventory.md) - Items, sales orders, invoices, purchase orders, bills +- [Zoho Mail](references/zoho-mail.md) - Messages, folders, labels, attachments +- [Zoho People](references/zoho-people.md) - Employees, departments, designations, attendance, leave +- [Zoho Recruit](references/zoho-recruit.md) - Candidates, job openings, interviews, applications + +## Examples + +### Slack - Post Message (Native API) + +```bash +# Native Slack API: POST https://slack.com/api/chat.postMessage +python <<'EOF' +import urllib.request, os, json +data = json.dumps({'channel': 'C0123456', 'text': 'Hello!'}).encode() +req = urllib.request.Request('https://gateway.maton.ai/slack/api/chat.postMessage', data=data, method='POST') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +req.add_header('Content-Type', 'application/json; charset=utf-8') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +### HubSpot - Create Contact (Native API) + +```bash +# Native HubSpot API: POST https://api.hubapi.com/crm/v3/objects/contacts +python <<'EOF' +import urllib.request, os, json +data = json.dumps({'properties': {'email': 'john@example.com', 'firstname': 'John', 'lastname': 'Doe'}}).encode() +req = urllib.request.Request('https://gateway.maton.ai/hubspot/crm/v3/objects/contacts', data=data, method='POST') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +req.add_header('Content-Type', 'application/json') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +### Google Sheets - Get Spreadsheet Values (Native API) + +```bash +# Native Sheets API: GET https://sheets.googleapis.com/v4/spreadsheets/{id}/values/{range} +python <<'EOF' +import urllib.request, os, json +req = urllib.request.Request('https://gateway.maton.ai/google-sheets/v4/spreadsheets/122BS1sFN2RKL8AOUQjkLdubzOwgqzPT64KfZ2rvYI4M/values/Sheet1!A1:B2') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +### Salesforce - SOQL Query (Native API) + +```bash +# Native Salesforce API: GET https://{instance}.salesforce.com/services/data/v64.0/query?q=... +python <<'EOF' +import urllib.request, os, json +req = urllib.request.Request('https://gateway.maton.ai/salesforce/services/data/v64.0/query?q=SELECT+Id,Name+FROM+Contact+LIMIT+10') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +### Airtable - List Tables (Native API) + +```bash +# Native Airtable API: GET https://api.airtable.com/v0/meta/bases/{id}/tables +python <<'EOF' +import urllib.request, os, json +req = urllib.request.Request('https://gateway.maton.ai/airtable/v0/meta/bases/appgqan2NzWGP5sBK/tables') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +### Notion - Query Database (Native API) + +```bash +# Native Notion API: POST https://api.notion.com/v1/data_sources/{id}/query +python <<'EOF' +import urllib.request, os, json +data = json.dumps({}).encode() +req = urllib.request.Request('https://gateway.maton.ai/notion/v1/data_sources/23702dc5-9a3b-8001-9e1c-000b5af0a980/query', data=data, method='POST') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +req.add_header('Content-Type', 'application/json') +req.add_header('Notion-Version', '2025-09-03') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +### Stripe - List Customers (Native API) + +```bash +# Native Stripe API: GET https://api.stripe.com/v1/customers +python <<'EOF' +import urllib.request, os, json +req = urllib.request.Request('https://gateway.maton.ai/stripe/v1/customers?limit=10') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +## Code Examples + +### JavaScript (Node.js) + +```javascript +const response = await fetch('https://gateway.maton.ai/slack/api/chat.postMessage', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.MATON_API_KEY}` + }, + body: JSON.stringify({ channel: 'C0123456', text: 'Hello!' }) +}); +``` + +### Python + +```python +import os +import requests + +response = requests.post( + 'https://gateway.maton.ai/slack/api/chat.postMessage', + headers={'Authorization': f'Bearer {os.environ["MATON_API_KEY"]}'}, + json={'channel': 'C0123456', 'text': 'Hello!'} +) +``` + +## Error Handling + +| Status | Meaning | +|--------|---------| +| 400 | Missing connection for the requested app | +| 401 | Invalid or missing Maton API key | +| 429 | Rate limited (10 requests/second per account) | +| 500 | Internal Server Error | +| 4xx/5xx | Passthrough error from the target API | + +Errors from the target API are passed through with their original status codes and response bodies. + +### Troubleshooting: API Key Issues + +1. Check that the `MATON_API_KEY` environment variable is set: + +```bash +echo $MATON_API_KEY +``` + +2. Verify the API key is valid by listing connections: + +```bash +python <<'EOF' +import urllib.request, os, json +req = urllib.request.Request('https://ctrl.maton.ai/connections') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +### Troubleshooting: Invalid App Name + +1. Verify your URL path starts with the correct app name. The path must begin with `/google-mail/`. For example: + +- Correct: `https://gateway.maton.ai/google-mail/gmail/v1/users/me/messages` +- Incorrect: `https://gateway.maton.ai/gmail/v1/users/me/messages` + +2. Ensure you have an active connection for the app. List your connections to verify: + +```bash +python <<'EOF' +import urllib.request, os, json +req = urllib.request.Request('https://ctrl.maton.ai/connections?app=google-mail&status=ACTIVE') +req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') +print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) +EOF +``` + +### Troubleshooting: Server Error + +A 500 error may indicate an expired OAuth token. Try creating a new connection via the Connection Management section above and completing OAuth authorization. If the new connection is "ACTIVE", delete the old connection to ensure the gateway uses the new one. + +## Rate Limits + +- 10 requests per second per account +- Target API rate limits also apply + +## Notes + +- When using curl with URLs containing brackets (`fields[]`, `sort[]`, `records[]`), use the `-g` flag to disable glob parsing +- When piping curl output to `jq`, environment variables may not expand correctly in some shells, which can cause "Invalid API key" errors + +## Tips + +1. **Use native API docs**: Refer to each service's official API documentation for endpoint paths and parameters. + +2. **Headers are forwarded**: Custom headers (except `Host` and `Authorization`) are forwarded to the target API. + +3. **Query params work**: URL query parameters are passed through to the target API. + +4. **All HTTP methods supported**: GET, POST, PUT, PATCH, DELETE are all supported. + +5. **QuickBooks special case**: Use `:realmId` in the path and it will be replaced with the connected realm ID. + +## Optional + +- [Github](https://github.com/maton-ai/api-gateway-skill) +- [API Reference](https://www.maton.ai/docs/api-reference) +- [Maton Community](https://discord.com/invite/dBfFAcefs2) +- [Maton Support](mailto:support@maton.ai) diff --git a/.agents/skills/api-gateway/references/active-campaign.md b/.agents/skills/api-gateway/references/active-campaign.md new file mode 100644 index 00000000..e2fc9752 --- /dev/null +++ b/.agents/skills/api-gateway/references/active-campaign.md @@ -0,0 +1,220 @@ +# ActiveCampaign Routing Reference + +**App name:** `active-campaign` +**Base URL proxied:** `{account}.api-us1.com` + +## API Path Pattern + +``` +/active-campaign/api/3/{resource} +``` + +## Common Endpoints + +### Contacts + +#### List Contacts +```bash +GET /active-campaign/api/3/contacts +``` + +#### Get Contact +```bash +GET /active-campaign/api/3/contacts/{contactId} +``` + +#### Create Contact +```bash +POST /active-campaign/api/3/contacts +Content-Type: application/json + +{ + "contact": { + "email": "user@example.com", + "firstName": "John", + "lastName": "Doe" + } +} +``` + +#### Update Contact +```bash +PUT /active-campaign/api/3/contacts/{contactId} +``` + +#### Delete Contact +```bash +DELETE /active-campaign/api/3/contacts/{contactId} +``` + +### Tags + +#### List Tags +```bash +GET /active-campaign/api/3/tags +``` + +#### Create Tag +```bash +POST /active-campaign/api/3/tags +Content-Type: application/json + +{ + "tag": { + "tag": "Tag Name", + "tagType": "contact" + } +} +``` + +### Contact Tags + +#### Add Tag to Contact +```bash +POST /active-campaign/api/3/contactTags +Content-Type: application/json + +{ + "contactTag": { + "contact": "1", + "tag": "1" + } +} +``` + +#### Remove Tag from Contact +```bash +DELETE /active-campaign/api/3/contactTags/{contactTagId} +``` + +### Lists + +#### List All Lists +```bash +GET /active-campaign/api/3/lists +``` + +#### Create List +```bash +POST /active-campaign/api/3/lists +``` + +### Deals + +#### List Deals +```bash +GET /active-campaign/api/3/deals +``` + +#### Create Deal +```bash +POST /active-campaign/api/3/deals +Content-Type: application/json + +{ + "deal": { + "title": "New Deal", + "value": "10000", + "currency": "usd", + "contact": "1", + "stage": "1" + } +} +``` + +### Deal Stages & Pipelines + +#### List Deal Stages +```bash +GET /active-campaign/api/3/dealStages +``` + +#### List Pipelines (Deal Groups) +```bash +GET /active-campaign/api/3/dealGroups +``` + +### Automations + +#### List Automations +```bash +GET /active-campaign/api/3/automations +``` + +### Campaigns + +#### List Campaigns +```bash +GET /active-campaign/api/3/campaigns +``` + +### Users + +#### List Users +```bash +GET /active-campaign/api/3/users +``` + +### Accounts + +#### List Accounts +```bash +GET /active-campaign/api/3/accounts +``` + +### Custom Fields + +#### List Fields +```bash +GET /active-campaign/api/3/fields +``` + +### Notes + +#### List Notes +```bash +GET /active-campaign/api/3/notes +``` + +### Webhooks + +#### List Webhooks +```bash +GET /active-campaign/api/3/webhooks +``` + +## Pagination + +Uses offset-based pagination: + +```bash +GET /active-campaign/api/3/contacts?limit=20&offset=0 +``` + +**Parameters:** +- `limit` - Results per page (default: 20) +- `offset` - Starting index + +Response includes meta with total: +```json +{ + "contacts": [...], + "meta": { + "total": "150" + } +} +``` + +## Notes + +- All endpoints require `/api/3/` prefix +- Request bodies use singular resource names (e.g., `{"contact": {...}}`) +- IDs returned as strings +- Rate limit: 5 requests per second per account +- DELETE returns 200 OK (not 204) + +## Resources + +- [ActiveCampaign API Overview](https://developers.activecampaign.com/reference/overview) +- [Developer Portal](https://developers.activecampaign.com/) +- [Contacts API](https://developers.activecampaign.com/reference/list-all-contacts) diff --git a/.agents/skills/api-gateway/references/acuity-scheduling.md b/.agents/skills/api-gateway/references/acuity-scheduling.md new file mode 100644 index 00000000..c2b73ee1 --- /dev/null +++ b/.agents/skills/api-gateway/references/acuity-scheduling.md @@ -0,0 +1,154 @@ +# Acuity Scheduling Routing Reference + +**App name:** `acuity-scheduling` +**Base URL proxied:** `acuityscheduling.com` + +## API Path Pattern + +``` +/acuity-scheduling/api/v1/{resource} +``` + +The gateway automatically prepends `/api/v1` when proxying to Acuity. + +## Common Endpoints + +### Get Account Info +```bash +GET /acuity-scheduling/api/v1/me +``` + +### List Appointments +```bash +GET /acuity-scheduling/api/v1/appointments?max=100&minDate=2026-02-01 +``` + +### Get Appointment +```bash +GET /acuity-scheduling/api/v1/appointments/{id} +``` + +### Create Appointment +```bash +POST /acuity-scheduling/api/v1/appointments +Content-Type: application/json + +{ + "datetime": "2026-02-15T09:00", + "appointmentTypeID": 123, + "firstName": "John", + "lastName": "Doe", + "email": "john@example.com" +} +``` + +### Update Appointment +```bash +PUT /acuity-scheduling/api/v1/appointments/{id} +Content-Type: application/json + +{ + "firstName": "Jane", + "lastName": "Smith" +} +``` + +### Cancel Appointment +```bash +PUT /acuity-scheduling/api/v1/appointments/{id}/cancel +``` + +### Reschedule Appointment +```bash +PUT /acuity-scheduling/api/v1/appointments/{id}/reschedule +Content-Type: application/json + +{ + "datetime": "2026-02-20T10:00" +} +``` + +### List Calendars +```bash +GET /acuity-scheduling/api/v1/calendars +``` + +### List Appointment Types +```bash +GET /acuity-scheduling/api/v1/appointment-types +``` + +### Get Available Dates +```bash +GET /acuity-scheduling/api/v1/availability/dates?month=2026-02&appointmentTypeID=123 +``` + +### Get Available Times +```bash +GET /acuity-scheduling/api/v1/availability/times?date=2026-02-04&appointmentTypeID=123 +``` + +### List Clients +```bash +GET /acuity-scheduling/api/v1/clients?search=John +``` + +### Create Client +```bash +POST /acuity-scheduling/api/v1/clients +Content-Type: application/json + +{ + "firstName": "John", + "lastName": "Doe", + "email": "john@example.com" +} +``` + +### List Blocks +```bash +GET /acuity-scheduling/api/v1/blocks?calendarID=1234 +``` + +### Create Block +```bash +POST /acuity-scheduling/api/v1/blocks +Content-Type: application/json + +{ + "start": "2026-02-15T12:00", + "end": "2026-02-15T13:00", + "calendarID": 1234 +} +``` + +### Delete Block +```bash +DELETE /acuity-scheduling/api/v1/blocks/{id} +``` + +### List Forms +```bash +GET /acuity-scheduling/api/v1/forms +``` + +### List Labels +```bash +GET /acuity-scheduling/api/v1/labels +``` + +## Notes + +- Datetime values must be parseable by PHP's `strtotime()` function +- Timezones use IANA format (e.g., "America/New_York") +- Use `max` parameter to limit results (default: 100) +- Use `minDate` and `maxDate` for date-range filtering +- Client update/delete only works for clients with existing appointments +- Rescheduling requires the new datetime to be an available time slot + +## Resources + +- [Acuity Scheduling API Quick Start](https://developers.acuityscheduling.com/reference/quick-start) +- [Appointments API](https://developers.acuityscheduling.com/reference/get-appointments) +- [Availability API](https://developers.acuityscheduling.com/reference/get-availability-dates) +- [OAuth2 Documentation](https://developers.acuityscheduling.com/docs/oauth2) diff --git a/.agents/skills/api-gateway/references/airtable.md b/.agents/skills/api-gateway/references/airtable.md new file mode 100644 index 00000000..1ea87ab6 --- /dev/null +++ b/.agents/skills/api-gateway/references/airtable.md @@ -0,0 +1,149 @@ +# Airtable Routing Reference + +**App name:** `airtable` +**Base URL proxied:** `api.airtable.com` + +## API Path Pattern + +``` +/airtable/v0/{baseId}/{tableIdOrName} +``` + +## Common Endpoints + +### List Records +```bash +GET /airtable/v0/{baseId}/{tableIdOrName}?maxRecords=100 +``` + +With view: +```bash +GET /airtable/v0/{baseId}/{tableIdOrName}?view=Grid%20view&maxRecords=100 +``` + +With filter formula: +```bash +GET /airtable/v0/{baseId}/{tableIdOrName}?filterByFormula={Status}='Active' +``` + +With field selection: +```bash +GET /airtable/v0/{baseId}/{tableIdOrName}?fields[]=Name&fields[]=Status&fields[]=Email +``` + +With sorting: +```bash +GET /airtable/v0/{baseId}/{tableIdOrName}?sort[0][field]=Created&sort[0][direction]=desc +``` + +### Get Record +```bash +GET /airtable/v0/{baseId}/{tableIdOrName}/{recordId} +``` + +### Create Records +```bash +POST /airtable/v0/{baseId}/{tableIdOrName} +Content-Type: application/json + +{ + "records": [ + { + "fields": { + "Name": "New Record", + "Status": "Active", + "Email": "test@example.com" + } + } + ] +} +``` + +### Update Records (PATCH - partial update) +```bash +PATCH /airtable/v0/{baseId}/{tableIdOrName} +Content-Type: application/json + +{ + "records": [ + { + "id": "recXXXXXXXXXXXXXX", + "fields": { + "Status": "Completed" + } + } + ] +} +``` + +### Update Records (PUT - full replace) +```bash +PUT /airtable/v0/{baseId}/{tableIdOrName} +Content-Type: application/json + +{ + "records": [ + { + "id": "recXXXXXXXXXXXXXX", + "fields": { + "Name": "Updated Name", + "Status": "Active" + } + } + ] +} +``` + +### Delete Records +```bash +DELETE /airtable/v0/{baseId}/{tableIdOrName}?records[]=recXXXXX&records[]=recYYYYY +``` + +### List Bases +```bash +GET /airtable/v0/meta/bases +``` + +### Get Base Schema +```bash +GET /airtable/v0/meta/bases/{baseId}/tables +``` + +## Pagination + +**Parameters:** +- `pageSize` - Number of records per request (max 100, default 100) +- `maxRecords` - Maximum total records across all pages +- `offset` - Cursor for next page (returned in response) + +Response includes `offset` when more records exist: +```json +{ + "records": [...], + "offset": "itrXXXXXXXXXXX" +} +``` + +Use offset for next page: +```bash +GET /airtable/v0/{baseId}/{tableIdOrName}?pageSize=50&offset=itrXXXXXXXXXXX +``` + +## Notes + +- Authentication is automatic via OAuth +- Base IDs start with `app` +- Table IDs start with `tbl` (can also use table name) +- Record IDs start with `rec` +- Maximum 100 records per request for create/update +- Maximum 10 records per delete request +- Filter formulas use Airtable formula syntax + +## Resources + +- [API Overview](https://airtable.com/developers/web/api/introduction) +- [List Records](https://airtable.com/developers/web/api/list-records) +- [Create Records](https://airtable.com/developers/web/api/create-records) +- [Update Records](https://airtable.com/developers/web/api/update-record) +- [Delete Records](https://airtable.com/developers/web/api/delete-record) +- [Formula Reference](https://support.airtable.com/docs/formula-field-reference) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/apollo.md b/.agents/skills/api-gateway/references/apollo.md new file mode 100644 index 00000000..0b99a7fd --- /dev/null +++ b/.agents/skills/api-gateway/references/apollo.md @@ -0,0 +1,213 @@ +# Apollo Routing Reference + +**App name:** `apollo` +**Base URL proxied:** `api.apollo.io` + +## API Path Pattern + +``` +/apollo/v1/{endpoint} +``` + +## Common Endpoints + +### People + +#### Search People +```bash +POST /apollo/v1/mixed_people/api_search +Content-Type: application/json + +{ + "q_organization_name": "Google", + "page": 1, + "per_page": 25 +} +``` + +#### Get Person +```bash +GET /apollo/v1/people/{personId} +``` + +#### Enrich Person +```bash +POST /apollo/v1/people/match +Content-Type: application/json + +{ + "email": "john@example.com" +} +``` + +Or by LinkedIn: +```bash +POST /apollo/v1/people/match +Content-Type: application/json + +{ + "linkedin_url": "https://linkedin.com/in/johndoe" +} +``` + +### Organizations + +#### Search Organizations +```bash +POST /apollo/v1/organizations/search +Content-Type: application/json + +{ + "q_organization_name": "Google", + "page": 1, + "per_page": 25 +} +``` + +#### Enrich Organization +```bash +POST /apollo/v1/organizations/enrich +Content-Type: application/json + +{ + "domain": "google.com" +} +``` + +### Contacts + +#### Search Contacts +```bash +POST /apollo/v1/contacts/search +Content-Type: application/json + +{ + "page": 1, + "per_page": 25 +} +``` + +#### Create Contact +```bash +POST /apollo/v1/contacts +Content-Type: application/json + +{ + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com", + "organization_name": "Acme Corp" +} +``` + +#### Update Contact +```bash +PUT /apollo/v1/contacts/{contactId} +Content-Type: application/json + +{ + "first_name": "Jane" +} +``` + +### Accounts + +#### Search Accounts +```bash +POST /apollo/v1/accounts/search +Content-Type: application/json + +{ + "page": 1, + "per_page": 25 +} +``` + +#### Create Account +```bash +POST /apollo/v1/accounts +Content-Type: application/json + +{ + "name": "Acme Corp", + "domain": "acme.com" +} +``` + +### Sequences + +#### Search Sequences +```bash +POST /apollo/v1/emailer_campaigns/search +Content-Type: application/json + +{ + "page": 1, + "per_page": 25 +} +``` + +#### Add Contact to Sequence +```bash +POST /apollo/v1/emailer_campaigns/{campaignId}/add_contact_ids +Content-Type: application/json + +{ + "contact_ids": ["contact_id_1", "contact_id_2"] +} +``` + +### Email + +#### Search Email Messages +```bash +POST /apollo/v1/emailer_messages/search +Content-Type: application/json + +{ + "contact_id": "{contactId}" +} +``` + +### Labels + +#### List Labels +```bash +GET /apollo/v1/labels +``` + +## Search Filters + +Common search parameters: +- `q_organization_name` - Company name +- `q_person_title` - Job title +- `person_locations` - Array of locations +- `organization_num_employees_ranges` - Employee count ranges +- `q_keywords` - General keyword search + +## Notes + +- Authentication is automatic - the router injects the API key +- Pagination uses `page` and `per_page` parameters in POST body +- Most list endpoints use POST with `/search` suffix (not GET) +- Email enrichment consumes credits +- Rate limits apply per endpoint +- `people/search` and `mixed_people/search` are deprecated - use `mixed_people/api_search` instead + +## Resources + +- [API Overview](https://docs.apollo.io/reference/introduction) +- [Search People](https://docs.apollo.io/reference/people-api-search.md) +- [Enrich Person](https://docs.apollo.io/reference/people-enrichment.md) +- [Search Organizations](https://docs.apollo.io/reference/organization-search.md) +- [Enrich Organization](https://docs.apollo.io/reference/organization-enrichment.md) +- [Search Contacts](https://docs.apollo.io/reference/search-for-contacts.md) +- [Create Contact](https://docs.apollo.io/reference/create-a-contact.md) +- [Update Contact](https://docs.apollo.io/reference/update-a-contact.md) +- [Search Accounts](https://docs.apollo.io/reference/search-for-accounts.md) +- [Create Account](https://docs.apollo.io/reference/create-an-account.md) +- [Search Sequences](https://docs.apollo.io/reference/search-for-sequences.md) +- [Add Contacts to Sequence](https://docs.apollo.io/reference/add-contacts-to-sequence.md) +- [Search Email Messages](https://docs.apollo.io/reference/search-for-outreach-emails.md) +- [List Labels](https://docs.apollo.io/reference/get-a-list-of-all-lists.md) +- [LLM Reference](https://docs.apollo.io/llms.txt) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/asana.md b/.agents/skills/api-gateway/references/asana.md new file mode 100644 index 00000000..515f6d43 --- /dev/null +++ b/.agents/skills/api-gateway/references/asana.md @@ -0,0 +1,154 @@ +# Asana Routing Reference + +**App name:** `asana` +**Base URL proxied:** `app.asana.com` + +## API Path Pattern + +``` +/asana/api/1.0/{resource} +``` + +## Common Endpoints + +### Get Current User +```bash +GET /asana/api/1.0/users/me +``` + +### List Workspaces +```bash +GET /asana/api/1.0/workspaces +``` + +### List Tasks +```bash +GET /asana/api/1.0/tasks?project=PROJECT_GID&opt_fields=name,completed,due_on +``` + +### Get a Task +```bash +GET /asana/api/1.0/tasks/{task_gid} +``` + +### Create a Task +```bash +POST /asana/api/1.0/tasks +Content-Type: application/json + +{ + "data": { + "name": "New task", + "projects": ["PROJECT_GID"], + "assignee": "USER_GID", + "due_on": "2025-03-20", + "notes": "Task description" + } +} +``` + +### Update a Task +```bash +PUT /asana/api/1.0/tasks/{task_gid} +Content-Type: application/json + +{ + "data": { + "completed": true + } +} +``` + +### Delete a Task +```bash +DELETE /asana/api/1.0/tasks/{task_gid} +``` + +### Get Subtasks +```bash +GET /asana/api/1.0/tasks/{task_gid}/subtasks +``` + +### Create Subtask +```bash +POST /asana/api/1.0/tasks/{task_gid}/subtasks +Content-Type: application/json + +{ + "data": { + "name": "Subtask name" + } +} +``` + +### List Projects +```bash +GET /asana/api/1.0/projects?workspace=WORKSPACE_GID&opt_fields=name,owner,due_date +``` + +### Get a Project +```bash +GET /asana/api/1.0/projects/{project_gid} +``` + +### Create a Project +```bash +POST /asana/api/1.0/projects +Content-Type: application/json + +{ + "data": { + "name": "New Project", + "workspace": "WORKSPACE_GID" + } +} +``` + +### List Users in Workspace +```bash +GET /asana/api/1.0/workspaces/{workspace_gid}/users?opt_fields=name,email +``` + +### Create Webhook +```bash +POST /asana/api/1.0/webhooks +Content-Type: application/json + +{ + "data": { + "resource": "PROJECT_OR_TASK_GID", + "target": "https://example.com/webhook", + "filters": [ + { + "resource_type": "task", + "action": "changed", + "fields": ["completed", "due_on"] + } + ] + } +} +``` + +### Delete Webhook +```bash +DELETE /asana/api/1.0/webhooks/{webhook_gid} +``` + +## Notes + +- Resource IDs (GIDs) are strings +- Timestamps are in ISO 8601 format +- Use `opt_fields` to specify which fields to return in responses +- Workspaces are the highest-level organizational unit +- Use cursor-based pagination with `offset` parameter +- Webhook creation requires the target URL to respond with 200 status + +## Resources + +- [Asana API Overview](https://developers.asana.com) +- [API Reference](https://developers.asana.com/reference) +- [Tasks](https://developers.asana.com/reference/tasks) +- [Projects](https://developers.asana.com/reference/projects) +- [Workspaces](https://developers.asana.com/reference/workspaces) +- [Webhooks](https://developers.asana.com/reference/webhooks) +- [LLM Reference](https://developers.asana.com/llms.txt) diff --git a/.agents/skills/api-gateway/references/attio.md b/.agents/skills/api-gateway/references/attio.md new file mode 100644 index 00000000..2cc553c3 --- /dev/null +++ b/.agents/skills/api-gateway/references/attio.md @@ -0,0 +1,122 @@ +# Attio Routing Reference + +**App name:** `attio` +**Base URL proxied:** `api.attio.com` + +## API Path Pattern + +``` +/attio/v2/{resource} +``` + +## Common Endpoints + +### List Objects +```bash +GET /attio/v2/objects +``` + +### Get Object +```bash +GET /attio/v2/objects/{object} +``` + +### List Attributes +```bash +GET /attio/v2/objects/{object}/attributes +``` + +### Query Records +```bash +POST /attio/v2/objects/{object}/records/query +Content-Type: application/json + +{ + "limit": 50, + "offset": 0 +} +``` + +### Get Record +```bash +GET /attio/v2/objects/{object}/records/{record_id} +``` + +### Create Record +```bash +POST /attio/v2/objects/{object}/records +Content-Type: application/json + +{ + "data": { + "values": { + "name": [{"first_name": "John", "last_name": "Doe", "full_name": "John Doe"}], + "email_addresses": ["john@example.com"] + } + } +} +``` + +### Update Record +```bash +PATCH /attio/v2/objects/{object}/records/{record_id} +Content-Type: application/json + +{ + "data": { + "values": { + "job_title": "Engineer" + } + } +} +``` + +### Delete Record +```bash +DELETE /attio/v2/objects/{object}/records/{record_id} +``` + +### List Tasks +```bash +GET /attio/v2/tasks?limit=50 +``` + +### Create Task +```bash +POST /attio/v2/tasks +Content-Type: application/json + +{ + "data": { + "content": "Task description", + "format": "plaintext", + "assignees": [], + "linked_records": [] + } +} +``` + +### List Workspace Members +```bash +GET /attio/v2/workspace_members +``` + +### Identify Self +```bash +GET /attio/v2/self +``` + +## Notes + +- Object slugs are lowercase snake_case (e.g., `people`, `companies`) +- Record IDs are UUIDs +- For personal-name attributes, include `full_name` when creating records +- Task creation requires `format` and `assignees` fields +- Rate limits: 100 read/sec, 25 write/sec +- Pagination uses `limit` and `offset` parameters + +## Resources + +- [Attio API Overview](https://docs.attio.com/rest-api/overview) +- [Attio API Reference](https://docs.attio.com/rest-api/endpoint-reference) +- [Records API](https://docs.attio.com/rest-api/endpoint-reference/records) diff --git a/.agents/skills/api-gateway/references/basecamp.md b/.agents/skills/api-gateway/references/basecamp.md new file mode 100644 index 00000000..c7a4d3e0 --- /dev/null +++ b/.agents/skills/api-gateway/references/basecamp.md @@ -0,0 +1,148 @@ +# Basecamp Routing Reference + +**App name:** `basecamp` +**Base URL proxied:** `3.basecampapi.com/{account_id}` + +Note: The gateway automatically injects the account ID from the OAuth connection. + +## API Path Pattern + +``` +/basecamp/{resource}.json +``` + +All paths must end with `.json`. + +## Common Endpoints + +### Get Current User +```bash +GET /basecamp/my/profile.json +``` + +### List People +```bash +GET /basecamp/people.json +``` + +### List Projects +```bash +GET /basecamp/projects.json +``` + +### Get Project +```bash +GET /basecamp/projects/{project_id}.json +``` + +Returns project with `dock` array containing tool IDs. + +### Create Project +```bash +POST /basecamp/projects.json +Content-Type: application/json + +{ + "name": "Project Name", + "description": "Description" +} +``` + +### Get Todoset +```bash +GET /basecamp/buckets/{project_id}/todosets/{todoset_id}.json +``` + +### List Todolists +```bash +GET /basecamp/buckets/{project_id}/todosets/{todoset_id}/todolists.json +``` + +### List Todos +```bash +GET /basecamp/buckets/{project_id}/todolists/{todolist_id}/todos.json +``` + +### Create Todo +```bash +POST /basecamp/buckets/{project_id}/todolists/{todolist_id}/todos.json +Content-Type: application/json + +{ + "content": "Todo content", + "due_on": "2026-02-15", + "assignee_ids": [123] +} +``` + +### Complete Todo +```bash +POST /basecamp/buckets/{project_id}/todos/{todo_id}/completion.json +``` + +### Get Message Board +```bash +GET /basecamp/buckets/{project_id}/message_boards/{message_board_id}.json +``` + +### List Messages +```bash +GET /basecamp/buckets/{project_id}/message_boards/{message_board_id}/messages.json +``` + +### Get Schedule +```bash +GET /basecamp/buckets/{project_id}/schedules/{schedule_id}.json +``` + +### List Schedule Entries +```bash +GET /basecamp/buckets/{project_id}/schedules/{schedule_id}/entries.json +``` + +### Get Vault (Documents) +```bash +GET /basecamp/buckets/{project_id}/vaults/{vault_id}.json +``` + +### List Documents +```bash +GET /basecamp/buckets/{project_id}/vaults/{vault_id}/documents.json +``` + +### List Campfires +```bash +GET /basecamp/chats.json +``` + +### Trash Recording +```bash +PUT /basecamp/buckets/{project_id}/recordings/{recording_id}/status/trashed.json +``` + +## Key Concepts + +- **Bucket**: Project content container (bucket_id = project_id) +- **Dock**: Per-project tool list with `id`, `name`, `enabled` +- **Recording**: Any content item (todos, messages, documents) + +## Pagination + +Uses `Link` header with `rel="next"`: +``` +Link: ; rel="next" +X-Total-Count: 150 +``` + +## Notes + +- All paths must end with `.json` +- Gateway injects account ID automatically +- Uses Basecamp 4 API (bc3-api) +- Rate limit: ~50 requests per 10 seconds per IP +- Check `enabled: true` in dock before using tools + +## Resources + +- [Basecamp 4 API Documentation](https://github.com/basecamp/bc3-api) +- [API Endpoints](https://github.com/basecamp/bc3-api#endpoints) diff --git a/.agents/skills/api-gateway/references/beehiiv.md b/.agents/skills/api-gateway/references/beehiiv.md new file mode 100644 index 00000000..0ac1d472 --- /dev/null +++ b/.agents/skills/api-gateway/references/beehiiv.md @@ -0,0 +1,133 @@ +# beehiiv Routing Reference + +**App name:** `beehiiv` +**Base URL proxied:** `api.beehiiv.com` + +## API Path Pattern + +``` +/beehiiv/v2/{resource} +``` + +## Common Endpoints + +### Publications + +#### List Publications +```bash +GET /beehiiv/v2/publications +``` + +#### Get Publication +```bash +GET /beehiiv/v2/publications/{publication_id} +``` + +### Subscriptions + +#### List Subscriptions +```bash +GET /beehiiv/v2/publications/{publication_id}/subscriptions +``` + +#### Get Subscription by ID +```bash +GET /beehiiv/v2/publications/{publication_id}/subscriptions/{subscription_id} +``` + +#### Get Subscription by Email +```bash +GET /beehiiv/v2/publications/{publication_id}/subscriptions/by_email/{email} +``` + +#### Create Subscription +```bash +POST /beehiiv/v2/publications/{publication_id}/subscriptions +Content-Type: application/json + +{ + "email": "subscriber@example.com", + "utm_source": "api" +} +``` + +#### Update Subscription +```bash +PATCH /beehiiv/v2/publications/{publication_id}/subscriptions/{subscription_id} +``` + +#### Delete Subscription +```bash +DELETE /beehiiv/v2/publications/{publication_id}/subscriptions/{subscription_id} +``` + +### Posts + +#### List Posts +```bash +GET /beehiiv/v2/publications/{publication_id}/posts +``` + +#### Get Post +```bash +GET /beehiiv/v2/publications/{publication_id}/posts/{post_id} +``` + +### Custom Fields + +#### List Custom Fields +```bash +GET /beehiiv/v2/publications/{publication_id}/custom_fields +``` + +#### Create Custom Field +```bash +POST /beehiiv/v2/publications/{publication_id}/custom_fields +``` + +### Segments + +```bash +GET /beehiiv/v2/publications/{publication_id}/segments +GET /beehiiv/v2/publications/{publication_id}/segments/{segment_id} +``` + +### Tiers + +```bash +GET /beehiiv/v2/publications/{publication_id}/tiers +POST /beehiiv/v2/publications/{publication_id}/tiers +PATCH /beehiiv/v2/publications/{publication_id}/tiers/{tier_id} +``` + +### Automations + +```bash +GET /beehiiv/v2/publications/{publication_id}/automations +GET /beehiiv/v2/publications/{publication_id}/automations/{automation_id} +``` + +## Pagination + +Cursor-based (recommended) or page-based (deprecated): + +```bash +# Cursor-based +GET /beehiiv/v2/publications/{pub_id}/subscriptions?limit=10&cursor={next_cursor} + +# Page-based (max 100 pages) +GET /beehiiv/v2/publications?page=2&limit=10 +``` + +## Notes + +- Publication IDs start with `pub_` +- Subscription IDs start with `sub_` +- Timestamps are Unix timestamps +- Cursor-based pagination is recommended +- Page-based pagination limited to 100 pages + +## Resources + +- [beehiiv Developer Documentation](https://developers.beehiiv.com/) +- [beehiiv API Reference](https://developers.beehiiv.com/api-reference) diff --git a/.agents/skills/api-gateway/references/box.md b/.agents/skills/api-gateway/references/box.md new file mode 100644 index 00000000..aded65c2 --- /dev/null +++ b/.agents/skills/api-gateway/references/box.md @@ -0,0 +1,191 @@ +# Box Routing Reference + +**App name:** `box` +**Base URL proxied:** `api.box.com` + +## API Path Pattern + +``` +/box/2.0/{resource} +``` + +## Common Endpoints + +### Get Current User +```bash +GET /box/2.0/users/me +``` + +### Get User +```bash +GET /box/2.0/users/{user_id} +``` + +### Get Folder +```bash +GET /box/2.0/folders/{folder_id} +``` + +Root folder ID is `0`. + +### List Folder Items +```bash +GET /box/2.0/folders/{folder_id}/items +GET /box/2.0/folders/{folder_id}/items?limit=100&offset=0 +``` + +### Create Folder +```bash +POST /box/2.0/folders +Content-Type: application/json + +{ + "name": "New Folder", + "parent": {"id": "0"} +} +``` + +### Update Folder +```bash +PUT /box/2.0/folders/{folder_id} +Content-Type: application/json + +{ + "name": "Updated Name", + "description": "Description" +} +``` + +### Copy Folder +```bash +POST /box/2.0/folders/{folder_id}/copy +Content-Type: application/json + +{ + "name": "Copied Folder", + "parent": {"id": "0"} +} +``` + +### Delete Folder +```bash +DELETE /box/2.0/folders/{folder_id} +DELETE /box/2.0/folders/{folder_id}?recursive=true +``` + +### Get File +```bash +GET /box/2.0/files/{file_id} +``` + +### Download File +```bash +GET /box/2.0/files/{file_id}/content +``` + +### Update File +```bash +PUT /box/2.0/files/{file_id} +``` + +### Copy File +```bash +POST /box/2.0/files/{file_id}/copy +``` + +### Delete File +```bash +DELETE /box/2.0/files/{file_id} +``` + +### Create Shared Link +```bash +PUT /box/2.0/folders/{folder_id} +Content-Type: application/json + +{ + "shared_link": {"access": "open"} +} +``` + +### List Collaborations +```bash +GET /box/2.0/folders/{folder_id}/collaborations +``` + +### Create Collaboration +```bash +POST /box/2.0/collaborations +Content-Type: application/json + +{ + "item": {"type": "folder", "id": "123"}, + "accessible_by": {"type": "user", "login": "user@example.com"}, + "role": "editor" +} +``` + +### Search +```bash +GET /box/2.0/search?query=keyword +``` + +### Events +```bash +GET /box/2.0/events +``` + +### Trash +```bash +GET /box/2.0/folders/trash/items +DELETE /box/2.0/files/{file_id}/trash +DELETE /box/2.0/folders/{folder_id}/trash +``` + +### Collections +```bash +GET /box/2.0/collections +GET /box/2.0/collections/{collection_id}/items +``` + +### Recent Items +```bash +GET /box/2.0/recent_items +``` + +### Webhooks +```bash +GET /box/2.0/webhooks +POST /box/2.0/webhooks +DELETE /box/2.0/webhooks/{webhook_id} +``` + +## Pagination + +Offset-based pagination: +```bash +GET /box/2.0/folders/0/items?limit=100&offset=0 +``` + +Response: +```json +{ + "total_count": 250, + "entries": [...], + "offset": 0, + "limit": 100 +} +``` + +## Notes + +- Root folder ID is `0` +- File uploads use `upload.box.com` (different base URL) +- Delete operations return 204 No Content +- Some operations require enterprise admin permissions +- Use `fields` parameter to select specific fields + +## Resources + +- [Box API Reference](https://developer.box.com/reference) +- [Box Developer Documentation](https://developer.box.com/guides) diff --git a/.agents/skills/api-gateway/references/brevo.md b/.agents/skills/api-gateway/references/brevo.md new file mode 100644 index 00000000..8c83a9a5 --- /dev/null +++ b/.agents/skills/api-gateway/references/brevo.md @@ -0,0 +1,229 @@ +# Brevo Routing Reference + +**App name:** `brevo` +**Base URL proxied:** `api.brevo.com` + +## API Path Pattern + +``` +/brevo/v3/{resource} +``` + +## Common Endpoints + +### Account + +```bash +GET /brevo/v3/account +``` + +### Contacts + +#### List Contacts +```bash +GET /brevo/v3/contacts?limit=50&offset=0 +``` + +#### Get Contact +```bash +GET /brevo/v3/contacts/{identifier} +``` + +#### Create Contact +```bash +POST /brevo/v3/contacts +Content-Type: application/json + +{ + "email": "contact@example.com", + "attributes": {"FIRSTNAME": "John", "LASTNAME": "Doe"}, + "listIds": [2] +} +``` + +#### Update Contact +```bash +PUT /brevo/v3/contacts/{identifier} +Content-Type: application/json + +{ + "attributes": {"FIRSTNAME": "Updated"} +} +``` + +#### Delete Contact +```bash +DELETE /brevo/v3/contacts/{identifier} +``` + +### Lists + +#### List All Lists +```bash +GET /brevo/v3/contacts/lists +``` + +#### Create List +```bash +POST /brevo/v3/contacts/lists +Content-Type: application/json + +{ + "name": "New List", + "folderId": 1 +} +``` + +#### Add Contacts to List +```bash +POST /brevo/v3/contacts/lists/{listId}/contacts/add +Content-Type: application/json + +{ + "emails": ["contact@example.com"] +} +``` + +### Folders + +#### List Folders +```bash +GET /brevo/v3/contacts/folders +``` + +#### Create Folder +```bash +POST /brevo/v3/contacts/folders +Content-Type: application/json + +{ + "name": "New Folder" +} +``` + +### Transactional Emails + +#### Send Email +```bash +POST /brevo/v3/smtp/email +Content-Type: application/json + +{ + "sender": {"name": "John", "email": "john@example.com"}, + "to": [{"email": "recipient@example.com", "name": "Jane"}], + "subject": "Hello!", + "htmlContent": "

Hi!

" +} +``` + +#### Get Email Statistics +```bash +GET /brevo/v3/smtp/statistics/events?limit=50 +``` + +### Email Templates + +#### List Templates +```bash +GET /brevo/v3/smtp/templates +``` + +#### Create Template +```bash +POST /brevo/v3/smtp/templates +Content-Type: application/json + +{ + "sender": {"name": "Company", "email": "noreply@company.com"}, + "templateName": "Welcome Email", + "subject": "Welcome {{params.name}}!", + "htmlContent": "

Hello {{params.name}}!

" +} +``` + +### Email Campaigns + +#### List Campaigns +```bash +GET /brevo/v3/emailCampaigns +``` + +#### Create Campaign +```bash +POST /brevo/v3/emailCampaigns +Content-Type: application/json + +{ + "name": "Newsletter", + "subject": "Monthly Update", + "sender": {"name": "Company", "email": "news@company.com"}, + "htmlContent": "

News

", + "recipients": {"listIds": [2]} +} +``` + +#### Send Campaign +```bash +POST /brevo/v3/emailCampaigns/{campaignId}/sendNow +``` + +### Senders + +#### List Senders +```bash +GET /brevo/v3/senders +``` + +#### Create Sender +```bash +POST /brevo/v3/senders +Content-Type: application/json + +{ + "name": "Marketing", + "email": "marketing@company.com" +} +``` + +### Attributes + +#### List Attributes +```bash +GET /brevo/v3/contacts/attributes +``` + +## Pagination + +Brevo uses offset-based pagination: + +```bash +GET /brevo/v3/contacts?limit=50&offset=0 +``` + +**Parameters:** +- `limit` - Results per page (max varies by endpoint, typically 500) +- `offset` - Starting index (0-based) + +Response includes count: +```json +{ + "contacts": [...], + "count": 150 +} +``` + +## Notes + +- All endpoints require `/v3/` prefix +- Attribute names must be UPPERCASE +- Contact identifiers: email, phone, or ID +- Template parameters: `{{params.name}}` syntax +- PUT/DELETE return 204 No Content on success +- Rate limit: 300 calls/min (free), higher on paid plans + +## Resources + +- [Brevo API Overview](https://developers.brevo.com/) +- [API Key Concepts](https://developers.brevo.com/docs/how-it-works) +- [Manage Contacts](https://developers.brevo.com/docs/synchronise-contact-lists) +- [Send Transactional Email](https://developers.brevo.com/docs/send-a-transactional-email) diff --git a/.agents/skills/api-gateway/references/calendly.md b/.agents/skills/api-gateway/references/calendly.md new file mode 100644 index 00000000..1b5132aa --- /dev/null +++ b/.agents/skills/api-gateway/references/calendly.md @@ -0,0 +1,108 @@ +# Calendly Routing Reference + +**App name:** `calendly` +**Base URL proxied:** `api.calendly.com` + +## API Path Pattern + +``` +/calendly/{resource} +``` + +## Common Endpoints + +### Get Current User +```bash +GET /calendly/users/me +``` + +### List Event Types +```bash +GET /calendly/event_types?user=USER_URI&active=true +``` + +### Get an Event Type +```bash +GET /calendly/event_types/{uuid} +``` + +### List Scheduled Events +```bash +GET /calendly/scheduled_events?user=USER_URI&status=active&min_start_time=2025-03-01T00:00:00Z +``` + +### Get a Scheduled Event +```bash +GET /calendly/scheduled_events/{uuid} +``` + +### Cancel a Scheduled Event +```bash +POST /calendly/scheduled_events/{uuid}/cancellation +Content-Type: application/json + +{ + "reason": "Meeting rescheduled" +} +``` + +### List Event Invitees +```bash +GET /calendly/scheduled_events/{event_uuid}/invitees +``` + +### Get Available Times +```bash +GET /calendly/event_type_available_times?event_type=EVENT_TYPE_URI&start_time=2025-03-15T00:00:00Z&end_time=2025-03-22T00:00:00Z +``` + +### Get User Busy Times +```bash +GET /calendly/user_busy_times?user=USER_URI&start_time=2025-03-15T00:00:00Z&end_time=2025-03-22T00:00:00Z +``` + +### List Organization Memberships +```bash +GET /calendly/organization_memberships?organization=ORGANIZATION_URI +``` + +### List Webhook Subscriptions +```bash +GET /calendly/webhook_subscriptions?organization=ORGANIZATION_URI&scope=organization +``` + +### Create Webhook Subscription +```bash +POST /calendly/webhook_subscriptions +Content-Type: application/json + +{ + "url": "https://example.com/webhook", + "events": ["invitee.created", "invitee.canceled"], + "organization": "ORGANIZATION_URI", + "scope": "organization" +} +``` + +### Delete Webhook Subscription +```bash +DELETE /calendly/webhook_subscriptions/{uuid} +``` + +## Notes + +- Resource identifiers are full URIs (e.g., `https://api.calendly.com/users/AAAA`) +- Timestamps are in ISO 8601 format +- Availability endpoints have a 7-day maximum range per request +- Webhooks require a paid Calendly plan (Standard, Teams, or Enterprise) +- Available webhook events: `invitee.created`, `invitee.canceled`, `routing_form_submission.created` +- Use `page_token` for pagination + +## Resources + +- [Calendly Developer Portal](https://developer.calendly.com/) +- [API Reference](https://developer.calendly.com/api-docs) +- [Event Types](https://developer.calendly.com/api-docs/e2f95ebd44914-list-user-s-event-types) +- [Scheduled Events](https://developer.calendly.com/api-docs/d61a40b4ea90e-list-events) +- [Availability](https://developer.calendly.com/api-docs/4241cf0f7f0d4-get-event-type-available-times) +- [Webhooks](https://developer.calendly.com/api-docs/c1ddc06ce1f1a-create-webhook-subscription) diff --git a/.agents/skills/api-gateway/references/callrail.md b/.agents/skills/api-gateway/references/callrail.md new file mode 100644 index 00000000..50a72369 --- /dev/null +++ b/.agents/skills/api-gateway/references/callrail.md @@ -0,0 +1,178 @@ +# CallRail Routing Reference + +**App name:** `callrail` +**Base URL proxied:** `api.callrail.com` + +## API Path Pattern + +``` +/callrail/v3/a/{account_id}/{resource}.json +``` + +**Important:** All CallRail API endpoints end with `.json`. Account IDs start with `ACC`. + +## Common Endpoints + +### Accounts + +#### List Accounts +```bash +GET /callrail/v3/a.json +``` + +#### Get Account +```bash +GET /callrail/v3/a/{account_id}.json +``` + +### Companies + +#### List Companies +```bash +GET /callrail/v3/a/{account_id}/companies.json +``` + +#### Get Company +```bash +GET /callrail/v3/a/{account_id}/companies/{company_id}.json +``` + +### Calls + +#### List Calls +```bash +GET /callrail/v3/a/{account_id}/calls.json +``` + +Query parameters: `page`, `per_page`, `date_range`, `start_date`, `end_date`, `company_id`, `tracker_id`, `search`, `fields`, `sort`, `order` + +#### Get Call +```bash +GET /callrail/v3/a/{account_id}/calls/{call_id}.json +``` + +#### Update Call +```bash +PUT /callrail/v3/a/{account_id}/calls/{call_id}.json +Content-Type: application/json + +{ + "customer_name": "John Smith", + "note": "Follow up scheduled", + "lead_status": "good_lead" +} +``` + +#### Call Summary +```bash +GET /callrail/v3/a/{account_id}/calls/summary.json +``` + +#### Call Timeseries +```bash +GET /callrail/v3/a/{account_id}/calls/timeseries.json +``` + +### Trackers + +#### List Trackers +```bash +GET /callrail/v3/a/{account_id}/trackers.json +``` + +#### Get Tracker +```bash +GET /callrail/v3/a/{account_id}/trackers/{tracker_id}.json +``` + +### Tags + +#### List Tags +```bash +GET /callrail/v3/a/{account_id}/tags.json +``` + +#### Create Tag +```bash +POST /callrail/v3/a/{account_id}/tags.json +Content-Type: application/json + +{ + "name": "New Tag", + "tag_level": "account", + "color": "blue1" +} +``` + +#### Update Tag +```bash +PUT /callrail/v3/a/{account_id}/tags/{tag_id}.json +Content-Type: application/json + +{ + "name": "Updated Name", + "color": "green1" +} +``` + +#### Delete Tag +```bash +DELETE /callrail/v3/a/{account_id}/tags/{tag_id}.json +``` + +### Users + +#### List Users +```bash +GET /callrail/v3/a/{account_id}/users.json +``` + +#### Get User +```bash +GET /callrail/v3/a/{account_id}/users/{user_id}.json +``` + +### Integrations + +#### List Integrations +```bash +GET /callrail/v3/a/{account_id}/integrations.json?company_id={company_id} +``` + +### Notifications + +#### List Notifications +```bash +GET /callrail/v3/a/{account_id}/notifications.json +``` + +## ID Prefixes + +- Account IDs: `ACC` +- Company IDs: `COM` +- Call IDs: `CAL` +- Tracker IDs: `TRK` +- User IDs: `USR` + +## Pagination + +Uses offset-based pagination with `page` and `per_page` parameters: + +```bash +GET /callrail/v3/a/{account_id}/calls.json?page=2&per_page=50 +# Response includes page, per_page, total_pages, total_records +``` + +For calls endpoint, relative pagination is available via `relative_pagination=true`. + +## Notes + +- All endpoints end with `.json` +- Communication records retained for 25 months +- Rate limits: 1,000/hour, 10,000/day for general API +- ISO 8601 date format with timezone + +## Resources + +- [CallRail API Documentation](https://apidocs.callrail.com/) +- [CallRail Help Center - API](https://support.callrail.com/hc/en-us/sections/4426797289229-API) diff --git a/.agents/skills/api-gateway/references/chargebee.md b/.agents/skills/api-gateway/references/chargebee.md new file mode 100644 index 00000000..cf34aa0f --- /dev/null +++ b/.agents/skills/api-gateway/references/chargebee.md @@ -0,0 +1,203 @@ +# Chargebee Routing Reference + +**App name:** `chargebee` +**Base URL proxied:** `{subdomain}.chargebee.com` + +The router automatically handles the subdomain from your connection. + +## API Path Pattern + +``` +/chargebee/api/v2/{endpoint} +``` + +## Common Endpoints + +### Customers + +#### List Customers +```bash +GET /chargebee/api/v2/customers?limit=10 +``` + +#### Get Customer +```bash +GET /chargebee/api/v2/customers/{customerId} +``` + +#### Create Customer +```bash +POST /chargebee/api/v2/customers +Content-Type: application/x-www-form-urlencoded + +first_name=John&last_name=Doe&email=john@example.com +``` + +#### Update Customer +```bash +POST /chargebee/api/v2/customers/{customerId} +Content-Type: application/x-www-form-urlencoded + +first_name=Jane +``` + +### Subscriptions + +#### List Subscriptions +```bash +GET /chargebee/api/v2/subscriptions?limit=10 +``` + +#### Get Subscription +```bash +GET /chargebee/api/v2/subscriptions/{subscriptionId} +``` + +#### Create Subscription +```bash +POST /chargebee/api/v2/subscriptions +Content-Type: application/x-www-form-urlencoded + +plan_id=basic-plan&customer[email]=john@example.com&customer[first_name]=John +``` + +#### Cancel Subscription +```bash +POST /chargebee/api/v2/subscriptions/{subscriptionId}/cancel +Content-Type: application/x-www-form-urlencoded + +end_of_term=true +``` + +### Item Prices (Product Catalog 2.0) + +#### List Item Prices +```bash +GET /chargebee/api/v2/item_prices?limit=10 +``` + +#### Get Item Price +```bash +GET /chargebee/api/v2/item_prices/{itemPriceId} +``` + +### Items (Product Catalog 2.0) + +#### List Items +```bash +GET /chargebee/api/v2/items?limit=10 +``` + +#### Get Item +```bash +GET /chargebee/api/v2/items/{itemId} +``` + +### Plans (Product Catalog 1.0 - Legacy) + +#### List Plans +```bash +GET /chargebee/api/v2/plans?limit=10 +``` + +#### Get Plan +```bash +GET /chargebee/api/v2/plans/{planId} +``` + +### Invoices + +#### List Invoices +```bash +GET /chargebee/api/v2/invoices?limit=10 +``` + +#### Get Invoice +```bash +GET /chargebee/api/v2/invoices/{invoiceId} +``` + +#### Download Invoice PDF +```bash +POST /chargebee/api/v2/invoices/{invoiceId}/pdf +``` + +### Transactions + +#### List Transactions +```bash +GET /chargebee/api/v2/transactions?limit=10 +``` + +### Hosted Pages + +#### Checkout New Subscription +```bash +POST /chargebee/api/v2/hosted_pages/checkout_new_for_items +Content-Type: application/x-www-form-urlencoded + +subscription[plan_id]=basic-plan&customer[email]=john@example.com +``` + +#### Manage Payment Sources +```bash +POST /chargebee/api/v2/hosted_pages/manage_payment_sources +Content-Type: application/x-www-form-urlencoded + +customer[id]=cust_123 +``` + +### Portal Sessions + +#### Create Portal Session +```bash +POST /chargebee/api/v2/portal_sessions +Content-Type: application/x-www-form-urlencoded + +customer[id]=cust_123 +``` + +## Filtering + +Use filter parameters: +```bash +GET /chargebee/api/v2/subscriptions?status[is]=active +GET /chargebee/api/v2/customers?email[is]=john@example.com +GET /chargebee/api/v2/invoices?date[after]=1704067200 +``` + +## Notes + +- Authentication is automatic - the router injects Basic auth from your API key +- Subdomain is automatically determined from your connection +- Uses form-urlencoded data for POST requests +- Nested objects use bracket notation: `customer[email]` +- Timestamps are Unix timestamps +- List responses include `next_offset` for pagination +- Status values: `active`, `cancelled`, `non_renewing`, etc. +- **Product Catalog versions**: Use `item_prices` and `items` for PC 2.0, or `plans` and `addons` for PC 1.0 + +## Resources + +- [Getting Started](https://apidocs.chargebee.com/docs/api) +- [List Customers](https://apidocs.chargebee.com/docs/api/customers/list-customers.md) +- [Retrieve a Customer](https://apidocs.chargebee.com/docs/api/customers/retrieve-a-customer.md) +- [Create a Customer](https://apidocs.chargebee.com/docs/api/customers/create-a-customer.md) +- [Update a Customer](https://apidocs.chargebee.com/docs/api/customers/update-a-customer.md) +- [List Subscriptions](https://apidocs.chargebee.com/docs/api/subscriptions/list-subscriptions.md) +- [Retrieve a Subscription](https://apidocs.chargebee.com/docs/api/subscriptions/retrieve-a-subscription.md) +- [Create a Subscription](https://apidocs.chargebee.com/docs/api/subscriptions/create-subscription-for-items.md) +- [Cancel a Subscription](https://apidocs.chargebee.com/docs/api/subscriptions/cancel-subscription-for-items.md) +- [List Items](https://apidocs.chargebee.com/docs/api/items/list-items.md) +- [Retrieve an Item](https://apidocs.chargebee.com/docs/api/items/retrieve-an-item.md) +- [List Item Prices](https://apidocs.chargebee.com/docs/api/item_prices/list-item-prices.md) +- [Retrieve an Item Price](https://apidocs.chargebee.com/docs/api/item_prices/retrieve-an-item-price.md) +- [List Plans](https://apidocs.chargebee.com/docs/api/v2/pcv-1/plans/list-plans.md) +- [Retrieve a Plan](https://apidocs.chargebee.com/docs/api/v2/pcv-1/plans/retrieve-a-plan.md) +- [List Invoices](https://apidocs.chargebee.com/docs/api/invoices/list-invoices.md) +- [Retrieve an Invoice](https://apidocs.chargebee.com/docs/api/invoices/retrieve-an-invoice.md) +- [Download Invoice as PDF](https://apidocs.chargebee.com/docs/api/invoices/download-e-invoice.md) +- [List Transactions](https://apidocs.chargebee.com/docs/api/transactions/list-transactions.md) +- [Checkout New Subscription](https://apidocs.chargebee.com/docs/api/hosted_pages/create-checkout-for-a-new-subscription.md) +- [Manage Payment Sources](https://apidocs.chargebee.com/docs/api/hosted_pages/manage-payment-sources.md) +- [Create a Portal Session](https://apidocs.chargebee.com/docs/api/portal_sessions/create-a-portal-session.md) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/clickfunnels.md b/.agents/skills/api-gateway/references/clickfunnels.md new file mode 100644 index 00000000..657bd714 --- /dev/null +++ b/.agents/skills/api-gateway/references/clickfunnels.md @@ -0,0 +1,274 @@ +# ClickFunnels Routing Reference + +**App name:** `clickfunnels` +**Base URL proxied:** `{subdomain}.myclickfunnels.com` + +The router automatically handles the subdomain from your OAuth connection. + +## API Path Pattern + +``` +/clickfunnels/api/v2/{resource} +``` + +## Required Headers + +THe `User-Agent` header is required to avoid Cloudflare blocks: + +``` +User-Agent: Maton/1.0 +``` + +## Common Endpoints + +### Teams + +#### List Teams +```bash +GET /clickfunnels/api/v2/teams +``` + +#### Get Team +```bash +GET /clickfunnels/api/v2/teams/{team_id} +``` + +### Workspaces + +#### List Workspaces +```bash +GET /clickfunnels/api/v2/teams/{team_id}/workspaces +``` + +#### Get Workspace +```bash +GET /clickfunnels/api/v2/workspaces/{workspace_id} +``` + +### Contacts + +#### List Contacts +```bash +GET /clickfunnels/api/v2/workspaces/{workspace_id}/contacts +``` + +#### Get Contact +```bash +GET /clickfunnels/api/v2/contacts/{contact_id} +``` + +#### Create Contact +```bash +POST /clickfunnels/api/v2/workspaces/{workspace_id}/contacts +Content-Type: application/json + +{ + "contact": { + "email_address": "user@example.com", + "first_name": "John", + "last_name": "Doe" + } +} +``` + +#### Update Contact +```bash +PUT /clickfunnels/api/v2/contacts/{contact_id} +Content-Type: application/json + +{ + "contact": { + "first_name": "Updated" + } +} +``` + +#### Delete Contact +```bash +DELETE /clickfunnels/api/v2/contacts/{contact_id} +``` + +#### Upsert Contact +```bash +POST /clickfunnels/api/v2/workspaces/{workspace_id}/contacts/upsert +``` + +### Products + +#### List Products +```bash +GET /clickfunnels/api/v2/workspaces/{workspace_id}/products +``` + +#### Get Product +```bash +GET /clickfunnels/api/v2/products/{product_id} +``` + +#### Create Product +```bash +POST /clickfunnels/api/v2/workspaces/{workspace_id}/products +Content-Type: application/json + +{ + "product": { + "name": "New Product", + "visible_in_store": true + } +} +``` + +#### Archive/Unarchive Product +```bash +POST /clickfunnels/api/v2/products/{product_id}/archive +POST /clickfunnels/api/v2/products/{product_id}/unarchive +``` + +### Orders + +#### List Orders +```bash +GET /clickfunnels/api/v2/workspaces/{workspace_id}/orders +``` + +#### Get Order +```bash +GET /clickfunnels/api/v2/orders/{order_id} +``` + +### Fulfillments + +#### List Fulfillments +```bash +GET /clickfunnels/api/v2/workspaces/{workspace_id}/fulfillments +``` + +#### Create Fulfillment +```bash +POST /clickfunnels/api/v2/workspaces/{workspace_id}/fulfillments +``` + +#### Cancel Fulfillment +```bash +POST /clickfunnels/api/v2/fulfillments/{fulfillment_id}/cancel +``` + +### Courses & Enrollments + +#### List Courses +```bash +GET /clickfunnels/api/v2/workspaces/{workspace_id}/courses +``` + +#### List Enrollments +```bash +GET /clickfunnels/api/v2/courses/{course_id}/enrollments +``` + +#### Create Enrollment +```bash +POST /clickfunnels/api/v2/courses/{course_id}/enrollments +``` + +### Forms & Submissions + +#### List Forms +```bash +GET /clickfunnels/api/v2/workspaces/{workspace_id}/forms +``` + +#### List Submissions +```bash +GET /clickfunnels/api/v2/forms/{form_id}/submissions +``` + +### Webhooks + +#### List Webhook Endpoints +```bash +GET /clickfunnels/api/v2/workspaces/{workspace_id}/webhooks/outgoing/endpoints +``` + +#### Create Webhook Endpoint +```bash +POST /clickfunnels/api/v2/workspaces/{workspace_id}/webhooks/outgoing/endpoints +Content-Type: application/json + +{ + "webhooks_outgoing_endpoint": { + "url": "https://example.com/webhook", + "name": "My Webhook", + "event_type_ids": ["contact.created"] + } +} +``` + +#### Delete Webhook Endpoint +```bash +DELETE /clickfunnels/api/v2/webhooks/outgoing/endpoints/{endpoint_id} +``` + +### Images + +#### List Images +```bash +GET /clickfunnels/api/v2/workspaces/{workspace_id}/images +``` + +#### Upload Image via URL +```bash +POST /clickfunnels/api/v2/workspaces/{workspace_id}/images +Content-Type: application/json + +{ + "image": { + "upload_source_url": "https://example.com/image.png" + } +} +``` + +## Pagination + +Cursor-based pagination with 20 items per page: + +```bash +# First page +GET /clickfunnels/api/v2/workspaces/{workspace_id}/contacts + +# Next page (use ID from Pagination-Next header) +GET /clickfunnels/api/v2/workspaces/{workspace_id}/contacts?after=1087091674 +``` + +Response headers: +- `Pagination-Next`: ID of last item +- `Link`: Full URL for next page + +## Filtering + +```bash +# Single filter +GET /clickfunnels/api/v2/workspaces/{workspace_id}/contacts?filter[email_address]=user@example.com + +# Multiple values (OR) +GET /clickfunnels/api/v2/workspaces/{workspace_id}/contacts?filter[email_address]=a@example.com,b@example.com + +# Multiple filters (AND) +GET /clickfunnels/api/v2/workspaces/{workspace_id}/contacts?filter[email_address]=user@example.com&filter[id]=123 +``` + +## Notes + +- Subdomain is automatically determined from your OAuth connection +- IDs are integers; each resource also has a `public_id` string +- Request bodies use nested keys: `{"contact": {...}}`, `{"product": {...}}` +- List endpoints: use `workspaces/{id}/{resource}` pattern +- Single resource: use `/{resource}/{id}` pattern (no workspace prefix) +- Delete operations return HTTP 204 with empty body +- Max 20 items per page, use `after` parameter for pagination + +## Resources + +- [ClickFunnels API Introduction](https://developers.myclickfunnels.com/docs/intro) +- [ClickFunnels API Reference](https://developers.myclickfunnels.com/reference) +- [Pagination Guide](https://developers.myclickfunnels.com/docs/pagination) +- [Filtering Guide](https://developers.myclickfunnels.com/docs/filtering) diff --git a/.agents/skills/api-gateway/references/clicksend.md b/.agents/skills/api-gateway/references/clicksend.md new file mode 100644 index 00000000..6770433d --- /dev/null +++ b/.agents/skills/api-gateway/references/clicksend.md @@ -0,0 +1,152 @@ +# ClickSend Routing Reference + +**App name:** `clicksend` +**Base URL proxied:** `rest.clicksend.com` + +## API Path Pattern + +``` +/clicksend/v3/{resource} +``` + +## Common Endpoints + +### Account + +#### Get Account +```bash +GET /clicksend/v3/account +``` + +### SMS + +#### Send SMS +```bash +POST /clicksend/v3/sms/send +Content-Type: application/json + +{ + "messages": [ + { + "to": "+15551234567", + "body": "Hello!", + "source": "api" + } + ] +} +``` + +#### SMS History +```bash +GET /clicksend/v3/sms/history +``` + +#### SMS Templates +```bash +GET /clicksend/v3/sms/templates +POST /clicksend/v3/sms/templates +PUT /clicksend/v3/sms/templates/{template_id} +DELETE /clicksend/v3/sms/templates/{template_id} +``` + +### MMS + +#### Send MMS +```bash +POST /clicksend/v3/mms/send +``` + +#### MMS History +```bash +GET /clicksend/v3/mms/history +``` + +### Voice + +#### Send Voice +```bash +POST /clicksend/v3/voice/send +``` + +#### Voice Languages +```bash +GET /clicksend/v3/voice/lang +``` + +### Contact Lists + +#### List All Lists +```bash +GET /clicksend/v3/lists +``` + +#### CRUD Operations +```bash +GET /clicksend/v3/lists/{list_id} +POST /clicksend/v3/lists +PUT /clicksend/v3/lists/{list_id} +DELETE /clicksend/v3/lists/{list_id} +``` + +### Contacts + +#### List Contacts +```bash +GET /clicksend/v3/lists/{list_id}/contacts +``` + +#### CRUD Operations +```bash +GET /clicksend/v3/lists/{list_id}/contacts/{contact_id} +POST /clicksend/v3/lists/{list_id}/contacts +PUT /clicksend/v3/lists/{list_id}/contacts/{contact_id} +DELETE /clicksend/v3/lists/{list_id}/contacts/{contact_id} +``` + +### Email Addresses + +```bash +GET /clicksend/v3/email/addresses +POST /clicksend/v3/email/addresses +DELETE /clicksend/v3/email/addresses/{email_address_id} +``` + +### Utility + +```bash +GET /clicksend/v3/countries +``` + +## Response Format + +All responses follow this structure: + +```json +{ + "http_code": 200, + "response_code": "SUCCESS", + "response_msg": "Description", + "data": { ... } +} +``` + +## Pagination + +Uses page-based pagination: + +```bash +GET /clicksend/v3/lists?page=2&limit=50 +# Response includes total, per_page, current_page, last_page +``` + +## Notes + +- Phone numbers must be E.164 format +- Timestamps are Unix timestamps +- Voice access requires account permissions +- SMS over 160 chars split into segments + +## Resources + +- [ClickSend Developer Portal](https://developers.clicksend.com/) +- [ClickSend REST API v3](https://developers.clicksend.com/docs) diff --git a/.agents/skills/api-gateway/references/clickup.md b/.agents/skills/api-gateway/references/clickup.md new file mode 100644 index 00000000..60764d69 --- /dev/null +++ b/.agents/skills/api-gateway/references/clickup.md @@ -0,0 +1,155 @@ +# ClickUp Routing Reference + +**App name:** `clickup` +**Base URL proxied:** `api.clickup.com` + +## API Path Pattern + +``` +/clickup/api/v2/{resource} +``` + +## ClickUp Hierarchy + +Workspace (team) → Space → Folder → List → Task + +## Common Endpoints + +### Get Current User +```bash +GET /clickup/api/v2/user +``` + +### Get Workspaces (Teams) +```bash +GET /clickup/api/v2/team +``` + +### Get Spaces +```bash +GET /clickup/api/v2/team/{team_id}/space +``` + +### Get Folders +```bash +GET /clickup/api/v2/space/{space_id}/folder +``` + +### Get Lists +```bash +GET /clickup/api/v2/folder/{folder_id}/list +``` + +### Get Folderless Lists +```bash +GET /clickup/api/v2/space/{space_id}/list +``` + +### Get Tasks +```bash +GET /clickup/api/v2/list/{list_id}/task?include_closed=true +``` + +### Get a Task +```bash +GET /clickup/api/v2/task/{task_id} +``` + +### Create a Task +```bash +POST /clickup/api/v2/list/{list_id}/task +Content-Type: application/json + +{ + "name": "Task name", + "description": "Task description", + "assignees": [123], + "status": "to do", + "priority": 2, + "due_date": 1709251200000, + "tags": ["api", "backend"] +} +``` + +### Update a Task +```bash +PUT /clickup/api/v2/task/{task_id} +Content-Type: application/json + +{ + "status": "complete", + "priority": null +} +``` + +### Delete a Task +```bash +DELETE /clickup/api/v2/task/{task_id} +``` + +### Get Filtered Team Tasks +```bash +GET /clickup/api/v2/team/{team_id}/task?statuses[]=to%20do&assignees[]=123 +``` + +### Create Space +```bash +POST /clickup/api/v2/team/{team_id}/space +Content-Type: application/json + +{ + "name": "New Space", + "multiple_assignees": true +} +``` + +### Create Folder +```bash +POST /clickup/api/v2/space/{space_id}/folder +Content-Type: application/json + +{"name": "New Folder"} +``` + +### Create List +```bash +POST /clickup/api/v2/folder/{folder_id}/list +Content-Type: application/json + +{"name": "New List"} +``` + +### Create Webhook +```bash +POST /clickup/api/v2/team/{team_id}/webhook +Content-Type: application/json + +{ + "endpoint": "https://example.com/webhook", + "events": ["taskCreated", "taskUpdated", "taskDeleted"] +} +``` + +### Delete Webhook +```bash +DELETE /clickup/api/v2/webhook/{webhook_id} +``` + +## Notes + +- Task IDs are strings, timestamps are Unix milliseconds +- Priority values: 1=urgent, 2=high, 3=normal, 4=low, null=none +- Workspaces are called "teams" in the API +- Status values must match exact status names configured in the list +- Use page-based pagination with `page` parameter (0-indexed) +- Responses are limited to 100 items per page + +## Resources + +- [ClickUp API Overview](https://developer.clickup.com/docs/Getting%20Started.md) +- [Tasks](https://developer.clickup.com/reference/gettasks.md) +- [Spaces](https://developer.clickup.com/reference/getspaces.md) +- [Lists](https://developer.clickup.com/reference/getlists.md) +- [Webhooks](https://developer.clickup.com/reference/createwebhook.md) +- [Rate Limits](https://developer.clickup.com/docs/rate-limits.md) +- [LLM Reference](https://developer.clickup.com/llms.txt) diff --git a/.agents/skills/api-gateway/references/cognito-forms.md b/.agents/skills/api-gateway/references/cognito-forms.md new file mode 100644 index 00000000..f6ad26b0 --- /dev/null +++ b/.agents/skills/api-gateway/references/cognito-forms.md @@ -0,0 +1,121 @@ +# Cognito Forms Routing Reference + +**App name:** `cognito-forms` +**Base URL proxied:** `www.cognitoforms.com` + +## API Path Pattern + +``` +/cognito-forms/api/{endpoint} +``` + +## Common Endpoints + +### Forms + +#### List Forms +```bash +GET /cognito-forms/api/forms +``` + +#### Get Form +```bash +GET /cognito-forms/api/forms/{formId} +``` + +### Entries + +#### List Entries +```bash +GET /cognito-forms/api/forms/{formId}/entries +``` + +#### Get Entry +```bash +GET /cognito-forms/api/forms/{formId}/entries/{entryId} +``` + +#### Create Entry +```bash +POST /cognito-forms/api/forms/{formId}/entries +Content-Type: application/json + +{ + "Name": { + "First": "John", + "Last": "Doe" + }, + "Email": "john.doe@example.com" +} +``` + +#### Update Entry +```bash +PUT /cognito-forms/api/forms/{formId}/entries/{entryId} +Content-Type: application/json + +{ + "Name": { + "First": "Jane", + "Last": "Doe" + }, + "Email": "jane.doe@example.com" +} +``` + +#### Delete Entry +```bash +DELETE /cognito-forms/api/forms/{formId}/entries/{entryId} +``` + +### Documents + +#### Get Document +```bash +GET /cognito-forms/api/forms/{formId}/entries/{entryId}/documents/{templateNumber} +``` + +### Files + +#### Get File +```bash +GET /cognito-forms/api/files/{fileId} +``` + +### Form Availability + +#### Set Form Availability +```bash +PUT /cognito-forms/api/forms/{formId}/availability +Content-Type: application/json + +{ + "start": "2026-03-01T00:00:00Z", + "end": "2026-03-31T23:59:59Z", + "message": "This form is currently unavailable." +} +``` + +## Field Types + +Complex fields use nested JSON objects: + +- **Name**: `{"First": "...", "Last": "..."}` +- **Address**: `{"Line1": "...", "Line2": "...", "City": "...", "State": "...", "PostalCode": "..."}` +- **Choice (single)**: `"OptionValue"` +- **Choice (multiple)**: `["Option1", "Option2"]` + +## Notes + +- Form IDs can be internal form name (string) or numeric ID +- Entry IDs can be entry number (integer) or entry ID (GUID) +- Authentication is automatic - the router injects OAuth token +- Rate limit: 100 requests per 60 seconds +- File and document endpoints return temporary download URLs +- API scopes: Read, Read/Write, or Read/Write/Delete + +## Resources + +- [Cognito Forms API Overview](https://www.cognitoforms.com/support/475/data-integration/cognito-forms-api) +- [REST API Reference](https://www.cognitoforms.com/support/476/data-integration/cognito-forms-api/rest-api-reference) +- [API Reference](https://www.cognitoforms.com/support/476/data-integration/cognito-forms-api/api-reference) diff --git a/.agents/skills/api-gateway/references/constant-contact.md b/.agents/skills/api-gateway/references/constant-contact.md new file mode 100644 index 00000000..058c4f28 --- /dev/null +++ b/.agents/skills/api-gateway/references/constant-contact.md @@ -0,0 +1,130 @@ +# Constant Contact Routing Reference + +**App name:** `constant-contact` +**Base URL proxied:** `api.cc.email` + +## API Path Pattern + +``` +/constant-contact/v3/{resource} +``` + +## Common Endpoints + +### List Contacts +```bash +GET /constant-contact/v3/contacts +``` + +### Get Contact +```bash +GET /constant-contact/v3/contacts/{contact_id} +``` + +### Create Contact +```bash +POST /constant-contact/v3/contacts +Content-Type: application/json + +{ + "email_address": { + "address": "john@example.com", + "permission_to_send": "implicit" + }, + "first_name": "John", + "last_name": "Doe", + "list_memberships": ["list-uuid"] +} +``` + +### Update Contact +```bash +PUT /constant-contact/v3/contacts/{contact_id} +Content-Type: application/json + +{ + "first_name": "John", + "last_name": "Smith" +} +``` + +### Delete Contact +```bash +DELETE /constant-contact/v3/contacts/{contact_id} +``` + +### List Contact Lists +```bash +GET /constant-contact/v3/contact_lists +``` + +### Create Contact List +```bash +POST /constant-contact/v3/contact_lists +Content-Type: application/json + +{ + "name": "Newsletter Subscribers", + "description": "Main newsletter list" +} +``` + +### List Email Campaigns +```bash +GET /constant-contact/v3/emails +``` + +### Create Email Campaign +```bash +POST /constant-contact/v3/emails +Content-Type: application/json + +{ + "name": "March Newsletter", + "email_campaign_activities": [ + { + "format_type": 5, + "from_name": "Company", + "from_email": "marketing@example.com", + "reply_to_email": "reply@example.com", + "subject": "Newsletter", + "html_content": "Hello" + } + ] +} +``` + +### List Segments +```bash +GET /constant-contact/v3/segments +``` + +### List Tags +```bash +GET /constant-contact/v3/contact_tags +``` + +### Get Account Summary +```bash +GET /constant-contact/v3/account/summary +``` + +### Email Campaign Summaries +```bash +GET /constant-contact/v3/reports/summary_reports/email_campaign_summaries +``` + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Resource IDs use UUID format (36 characters with hyphens) +- All dates use ISO-8601 format +- Uses cursor-based pagination with `limit` and `cursor` parameters +- Maximum 1,000 contact lists per account +- Bulk operations are asynchronous + +## Resources + +- [V3 API Overview](https://developer.constantcontact.com/api_guide/getting_started.html) +- [API Reference](https://developer.constantcontact.com/api_reference/index.html) +- [Technical Overview](https://developer.constantcontact.com/api_guide/v3_technical_overview.html) diff --git a/.agents/skills/api-gateway/references/dropbox.md b/.agents/skills/api-gateway/references/dropbox.md new file mode 100644 index 00000000..d4a8f04c --- /dev/null +++ b/.agents/skills/api-gateway/references/dropbox.md @@ -0,0 +1,204 @@ +# Dropbox Routing Reference + +**App name:** `dropbox` +**Base URL proxied:** `api.dropboxapi.com` + +## API Path Pattern + +``` +/dropbox/2/{endpoint} +``` + +**Important:** All Dropbox API v2 endpoints use HTTP POST with JSON request bodies. + +## Common Endpoints + +### Users + +#### Get Current Account +```bash +POST /dropbox/2/users/get_current_account +Content-Type: application/json + +null +``` + +#### Get Space Usage +```bash +POST /dropbox/2/users/get_space_usage +Content-Type: application/json + +null +``` + +### Files + +#### List Folder +```bash +POST /dropbox/2/files/list_folder +Content-Type: application/json + +{ + "path": "" +} +``` + +Use empty string `""` for root folder. + +#### Continue Listing +```bash +POST /dropbox/2/files/list_folder/continue +Content-Type: application/json + +{ + "cursor": "..." +} +``` + +#### Get Metadata +```bash +POST /dropbox/2/files/get_metadata +Content-Type: application/json + +{ + "path": "/document.pdf" +} +``` + +#### Create Folder +```bash +POST /dropbox/2/files/create_folder_v2 +Content-Type: application/json + +{ + "path": "/New Folder", + "autorename": false +} +``` + +#### Copy +```bash +POST /dropbox/2/files/copy_v2 +Content-Type: application/json + +{ + "from_path": "/source/file.pdf", + "to_path": "/destination/file.pdf" +} +``` + +#### Move +```bash +POST /dropbox/2/files/move_v2 +Content-Type: application/json + +{ + "from_path": "/old/file.pdf", + "to_path": "/new/file.pdf" +} +``` + +#### Delete +```bash +POST /dropbox/2/files/delete_v2 +Content-Type: application/json + +{ + "path": "/file-to-delete.pdf" +} +``` + +#### Get Temporary Link +```bash +POST /dropbox/2/files/get_temporary_link +Content-Type: application/json + +{ + "path": "/document.pdf" +} +``` + +### Search + +#### Search Files +```bash +POST /dropbox/2/files/search_v2 +Content-Type: application/json + +{ + "query": "document" +} +``` + +### Revisions + +#### List Revisions +```bash +POST /dropbox/2/files/list_revisions +Content-Type: application/json + +{ + "path": "/document.pdf" +} +``` + +### Tags + +#### Get Tags +```bash +POST /dropbox/2/files/tags/get +Content-Type: application/json + +{ + "paths": ["/document.pdf"] +} +``` + +#### Add Tag +```bash +POST /dropbox/2/files/tags/add +Content-Type: application/json + +{ + "path": "/document.pdf", + "tag_text": "important" +} +``` + +#### Remove Tag +```bash +POST /dropbox/2/files/tags/remove +Content-Type: application/json + +{ + "path": "/document.pdf", + "tag_text": "important" +} +``` + +## Pagination + +Dropbox uses cursor-based pagination: + +```bash +POST /dropbox/2/files/list_folder +# Response includes "cursor" and "has_more": true/false + +POST /dropbox/2/files/list_folder/continue +# Use cursor from previous response +``` + +## Notes + +- All endpoints use POST method +- Request bodies are JSON +- Use empty string `""` for root folder path +- Paths are case-insensitive but case-preserving +- Tag text must match pattern `[\w]+` (alphanumeric and underscores) +- Temporary links expire after 4 hours + +## Resources + +- [Dropbox HTTP API Overview](https://www.dropbox.com/developers/documentation/http/overview) +- [Dropbox API Explorer](https://dropbox.github.io/dropbox-api-v2-explorer/) +- [DBX File Access Guide](https://developers.dropbox.com/dbx-file-access-guide) diff --git a/.agents/skills/api-gateway/references/eventbrite.md b/.agents/skills/api-gateway/references/eventbrite.md new file mode 100644 index 00000000..dec186c4 --- /dev/null +++ b/.agents/skills/api-gateway/references/eventbrite.md @@ -0,0 +1,214 @@ +# Eventbrite Routing Reference + +**App name:** `eventbrite` +**Base URL proxied:** `www.eventbriteapi.com` + +## API Path Pattern + +``` +/eventbrite/v3/{resource}/ +``` + +Note: All Eventbrite API paths should end with a trailing slash. + +## Common Endpoints + +### Get Current User +```bash +GET /eventbrite/v3/users/me/ +``` + +### List User Organizations +```bash +GET /eventbrite/v3/users/me/organizations/ +``` + +### List User Orders +```bash +GET /eventbrite/v3/users/me/orders/ +``` + +### List Organization Events +```bash +GET /eventbrite/v3/organizations/{organization_id}/events/ +``` + +Query parameters: +- `status` - Filter: `draft`, `live`, `started`, `ended`, `completed`, `canceled` +- `order_by` - Sort: `start_asc`, `start_desc`, `created_asc`, `created_desc` +- `time_filter` - Filter: `current_future`, `past` + +### Create Event +```bash +POST /eventbrite/v3/organizations/{organization_id}/events/ +Content-Type: application/json + +{ + "event": { + "name": {"html": "My Event"}, + "start": {"timezone": "America/Los_Angeles", "utc": "2026-03-01T19:00:00Z"}, + "end": {"timezone": "America/Los_Angeles", "utc": "2026-03-01T22:00:00Z"}, + "currency": "USD" + } +} +``` + +### Get Event +```bash +GET /eventbrite/v3/events/{event_id}/ +``` + +### Update Event +```bash +POST /eventbrite/v3/events/{event_id}/ +Content-Type: application/json + +{ + "event": { + "name": {"html": "Updated Name"} + } +} +``` + +### Publish Event +```bash +POST /eventbrite/v3/events/{event_id}/publish/ +``` + +### Cancel Event +```bash +POST /eventbrite/v3/events/{event_id}/cancel/ +``` + +### Delete Event +```bash +DELETE /eventbrite/v3/events/{event_id}/ +``` + +### List Ticket Classes +```bash +GET /eventbrite/v3/events/{event_id}/ticket_classes/ +``` + +### Create Ticket Class +```bash +POST /eventbrite/v3/events/{event_id}/ticket_classes/ +Content-Type: application/json + +{ + "ticket_class": { + "name": "General Admission", + "quantity_total": 100, + "cost": "USD,2500" + } +} +``` + +### List Event Attendees +```bash +GET /eventbrite/v3/events/{event_id}/attendees/ +``` + +### List Event Orders +```bash +GET /eventbrite/v3/events/{event_id}/orders/ +``` + +### Get Order +```bash +GET /eventbrite/v3/orders/{order_id}/ +``` + +### List Organization Venues +```bash +GET /eventbrite/v3/organizations/{organization_id}/venues/ +``` + +### Create Venue +```bash +POST /eventbrite/v3/organizations/{organization_id}/venues/ +Content-Type: application/json + +{ + "venue": { + "name": "Conference Center", + "address": { + "address_1": "123 Main St", + "city": "San Francisco", + "region": "CA", + "postal_code": "94105", + "country": "US" + } + } +} +``` + +### Get Venue +```bash +GET /eventbrite/v3/venues/{venue_id}/ +``` + +### List Categories +```bash +GET /eventbrite/v3/categories/ +``` + +### Get Category +```bash +GET /eventbrite/v3/categories/{category_id}/ +``` + +### List Subcategories +```bash +GET /eventbrite/v3/subcategories/ +``` + +### List Formats +```bash +GET /eventbrite/v3/formats/ +``` + +### List Countries +```bash +GET /eventbrite/v3/system/countries/ +``` + +### List Regions +```bash +GET /eventbrite/v3/system/regions/ +``` + +## Expansions + +Include related data with the `expand` parameter: + +```bash +GET /eventbrite/v3/events/{event_id}/?expand=venue,ticket_classes,category +``` + +Available expansions: `venue`, `ticket_classes`, `category`, `subcategory`, `format`, `organizer` + +## Pagination + +Use `continuation` token for pagination: + +```bash +GET /eventbrite/v3/organizations/{org_id}/events/?page_size=50 +GET /eventbrite/v3/organizations/{org_id}/events/?continuation=eyJwYWdlIjogMn0 +``` + +## Notes + +- All endpoint paths must end with a trailing slash (`/`) +- Event creation requires an organization - use `/organizations/{org_id}/events/` +- Legacy user-based event endpoints (e.g., `/users/me/owned_events/`) are deprecated +- Timestamps are in ISO 8601 format (UTC) +- Currency amounts use format "CURRENCY,AMOUNT" where amount is in cents (e.g., "USD,2500" = $25.00) +- Rate limit: 1,000 calls per hour, 48,000 calls per day +- Event Search API is no longer publicly available (deprecated February 2020) + +## Resources + +- [Eventbrite API Documentation](https://www.eventbrite.com/platform/api) +- [API Basics](https://www.eventbrite.com/platform/docs/api-basics) +- [API Explorer](https://www.eventbrite.com/platform/docs/api-explorer) diff --git a/.agents/skills/api-gateway/references/fathom.md b/.agents/skills/api-gateway/references/fathom.md new file mode 100644 index 00000000..dd4af00e --- /dev/null +++ b/.agents/skills/api-gateway/references/fathom.md @@ -0,0 +1,85 @@ +# Fathom Routing Reference + +**App name:** `fathom` +**Base URL proxied:** `api.fathom.ai` + +## API Path Pattern + +``` +/fathom/external/v1/{resource} +``` + +## Common Endpoints + +### List Meetings +```bash +GET /fathom/external/v1/meetings +``` + +With filters: +```bash +GET /fathom/external/v1/meetings?created_after=2025-01-01T00:00:00Z&teams[]=Sales +``` + +### Get Summary +```bash +GET /fathom/external/v1/recordings/{recording_id}/summary +``` + +Async callback: +```bash +GET /fathom/external/v1/recordings/{recording_id}/summary?destination_url=https://example.com/webhook +``` + +### Get Transcript +```bash +GET /fathom/external/v1/recordings/{recording_id}/transcript +``` + +Async callback: +```bash +GET /fathom/external/v1/recordings/{recording_id}/transcript?destination_url=https://example.com/webhook +``` + +### List Teams +```bash +GET /fathom/external/v1/teams +``` + +### List Team Members +```bash +GET /fathom/external/v1/team_members?team=Sales +``` + +### Create Webhook +```bash +POST /fathom/external/v1/webhooks +Content-Type: application/json + +{ + "destination_url": "https://example.com/webhook", + "triggered_for": ["my_recordings", "my_shared_with_team_recordings"], + "include_transcript": true, + "include_summary": true, + "include_action_items": true +} +``` + +### Delete Webhook +```bash +DELETE /fathom/external/v1/webhooks/{id} +``` + +## Notes + +- Recording IDs are integers +- Timestamps are in ISO 8601 format +- OAuth users cannot use inline transcript/summary parameters on `/meetings` endpoint - use dedicated `/recordings/{id}/summary` and `/recordings/{id}/transcript` endpoints instead +- Use cursor-based pagination with `cursor` parameter +- Webhook `triggered_for` options: `my_recordings`, `shared_external_recordings`, `my_shared_with_team_recordings`, `shared_team_recordings` +- Webhook secrets are used to verify webhook signatures + +## Resources + +- [Fathom API Documentation](https://developers.fathom.ai) +- [LLM Reference](https://developers.fathom.ai/llms.txt) diff --git a/.agents/skills/api-gateway/references/fireflies.md b/.agents/skills/api-gateway/references/fireflies.md new file mode 100644 index 00000000..bdd51747 --- /dev/null +++ b/.agents/skills/api-gateway/references/fireflies.md @@ -0,0 +1,103 @@ +# Fireflies Routing Reference + +**App name:** `fireflies` +**Base URL proxied:** `api.fireflies.ai` + +## API Path Pattern + +``` +/fireflies/graphql +``` + +Fireflies uses GraphQL. All requests are POST to a single `/graphql` endpoint. + +## GraphQL Queries + +### User +```graphql +{ user { user_id name email is_admin } } +``` + +### Users +```graphql +{ users { user_id name email is_admin } } +``` + +### Transcripts +```graphql +{ transcripts { id title date duration host_email privacy } } +``` + +### Transcript by ID +```graphql +query($id: String!) { + transcript(id: $id) { + id title date duration + summary { overview action_items } + sentences { text speaker_name } + } +} +``` + +### Channels +```graphql +{ channels { id title created_at is_private } } +``` + +### Contacts +```graphql +{ contacts { email name picture last_meeting_date } } +``` + +### Bites +```graphql +{ bites { id name transcript_id summary status } } +``` + +### AskFred Threads +```graphql +{ askfred_threads { id title created_at } } +``` + +## GraphQL Mutations + +### Upload Audio +```graphql +mutation($input: AudioUploadInput!) { + uploadAudio(input: $input) { success message } +} +``` + +### Delete Transcript +```graphql +mutation($id: String!) { + deleteTranscript(id: $id) { success } +} +``` + +### Update Meeting Title +```graphql +mutation($id: String!, $title: String!) { + updateMeetingTitle(id: $id, title: $title) { success } +} +``` + +### AskFred +```graphql +mutation($input: CreateAskFredThreadInput!) { + createAskFredThread(input: $input) { id title } +} +``` + +## Notes + +- All requests are POST with Content-Type: application/json +- Request body: `{ "query": "...", "variables": {...} }` +- User IDs are ULIDs +- Rate limits: 50 calls/day (free), more on Business plan +- Summary field contains AI-generated content + +## Resources + +- [Fireflies API Documentation](https://docs.fireflies.ai/) +- [Fireflies GraphQL API](https://docs.fireflies.ai/graphql-api) diff --git a/.agents/skills/api-gateway/references/gateway-skill-api-1.2.zip b/.agents/skills/api-gateway/references/gateway-skill-api-1.2.zip new file mode 100644 index 00000000..425a94d8 Binary files /dev/null and b/.agents/skills/api-gateway/references/gateway-skill-api-1.2.zip differ diff --git a/.agents/skills/api-gateway/references/gateway-skill-api-v3.8-alpha.3.zip b/.agents/skills/api-gateway/references/gateway-skill-api-v3.8-alpha.3.zip new file mode 100644 index 00000000..0cd8e6e0 Binary files /dev/null and b/.agents/skills/api-gateway/references/gateway-skill-api-v3.8-alpha.3.zip differ diff --git a/.agents/skills/api-gateway/references/github.md b/.agents/skills/api-gateway/references/github.md new file mode 100644 index 00000000..3eb6b9f1 --- /dev/null +++ b/.agents/skills/api-gateway/references/github.md @@ -0,0 +1,125 @@ +# GitHub Routing Reference + +**App name:** `github` +**Base URL proxied:** `api.github.com` + +## API Path Pattern + +``` +/github/{resource} +``` + +GitHub API does not use a version prefix in paths. Versioning is handled via the `X-GitHub-Api-Version` header. + +## Common Endpoints + +### Get Authenticated User +```bash +GET /github/user +``` + +### Get User by Username +```bash +GET /github/users/{username} +``` + +### List User Repositories +```bash +GET /github/user/repos?per_page=30&sort=updated +``` + +### Get Repository +```bash +GET /github/repos/{owner}/{repo} +``` + +### List Repository Contents +```bash +GET /github/repos/{owner}/{repo}/contents/{path} +``` + +### List Branches +```bash +GET /github/repos/{owner}/{repo}/branches +``` + +### List Commits +```bash +GET /github/repos/{owner}/{repo}/commits?per_page=30 +``` + +### List Repository Issues +```bash +GET /github/repos/{owner}/{repo}/issues?state=open&per_page=30 +``` + +### Create Issue +```bash +POST /github/repos/{owner}/{repo}/issues +Content-Type: application/json + +{ + "title": "Issue title", + "body": "Issue description", + "labels": ["bug"] +} +``` + +### List Pull Requests +```bash +GET /github/repos/{owner}/{repo}/pulls?state=open&per_page=30 +``` + +### Create Pull Request +```bash +POST /github/repos/{owner}/{repo}/pulls +Content-Type: application/json + +{ + "title": "PR title", + "body": "PR description", + "head": "feature-branch", + "base": "main" +} +``` + +### Merge Pull Request +```bash +PUT /github/repos/{owner}/{repo}/pulls/{pull_number}/merge +Content-Type: application/json + +{ + "merge_method": "squash" +} +``` + +### Search Repositories +```bash +GET /github/search/repositories?q={query}&per_page=30 +``` + +### Search Issues +```bash +GET /github/search/issues?q={query}&per_page=30 +``` + +### Get Rate Limit +```bash +GET /github/rate_limit +``` + +## Notes + +- Repository names are case-insensitive but the API preserves case +- Issue numbers and PR numbers share the same sequence per repository +- File content must be Base64 encoded when creating/updating files +- Rate limits: 5000 requests/hour for authenticated users, 30 searches/minute +- Pagination uses `per_page` (max 100, default 30) and `page` parameters +- Some endpoints require specific OAuth scopes (e.g., `read:org` for organization operations) + +## Resources + +- [GitHub REST API Documentation](https://docs.github.com/en/rest) +- [Repositories API](https://docs.github.com/en/rest/repos/repos) +- [Issues API](https://docs.github.com/en/rest/issues/issues) +- [Pull Requests API](https://docs.github.com/en/rest/pulls/pulls) diff --git a/.agents/skills/api-gateway/references/google-ads.md b/.agents/skills/api-gateway/references/google-ads.md new file mode 100644 index 00000000..773c7b7b --- /dev/null +++ b/.agents/skills/api-gateway/references/google-ads.md @@ -0,0 +1,172 @@ +# Google Ads Routing Reference + +**App name:** `google-ads` +**Base URL proxied:** `googleads.googleapis.com` + +## API Path Pattern + +``` +/google-ads/v23/customers/{customerId}/{endpoint} +``` + +## Common Endpoints + +### List Accessible Customers +```bash +GET /google-ads/v23/customers:listAccessibleCustomers +``` + +### Search (GAQL Query) +```bash +POST /google-ads/v23/customers/{customerId}/googleAds:search +Content-Type: application/json + +{ + "query": "SELECT campaign.id, campaign.name, campaign.status FROM campaign ORDER BY campaign.id" +} +``` + +### Search Stream (for large result sets) +```bash +POST /google-ads/v23/customers/{customerId}/googleAds:searchStream +Content-Type: application/json + +{ + "query": "SELECT campaign.id, campaign.name FROM campaign" +} +``` + +## Common GAQL Queries + +### List Campaigns +```sql +SELECT + campaign.id, + campaign.name, + campaign.status, + campaign.advertising_channel_type +FROM campaign +WHERE campaign.status != 'REMOVED' +ORDER BY campaign.name +``` + +### Campaign Performance +```sql +SELECT + campaign.id, + campaign.name, + metrics.impressions, + metrics.clicks, + metrics.cost_micros, + metrics.conversions +FROM campaign +WHERE segments.date DURING LAST_30_DAYS +ORDER BY metrics.impressions DESC +``` + +### List Ad Groups +```sql +SELECT + ad_group.id, + ad_group.name, + ad_group.status, + campaign.id, + campaign.name +FROM ad_group +WHERE ad_group.status != 'REMOVED' +``` + +### Ad Group Performance +```sql +SELECT + ad_group.id, + ad_group.name, + metrics.impressions, + metrics.clicks, + metrics.average_cpc +FROM ad_group +WHERE segments.date DURING LAST_7_DAYS +``` + +### List Keywords +```sql +SELECT + ad_group_criterion.keyword.text, + ad_group_criterion.keyword.match_type, + ad_group_criterion.status, + metrics.impressions, + metrics.clicks +FROM keyword_view +WHERE segments.date DURING LAST_30_DAYS +``` + +### List Ads +```sql +SELECT + ad_group_ad.ad.id, + ad_group_ad.ad.name, + ad_group_ad.status, + ad_group_ad.ad.type +FROM ad_group_ad +WHERE ad_group_ad.status != 'REMOVED' +``` + +## Mutate Operations + +### Create Campaign +```bash +POST /google-ads/v23/customers/{customerId}/campaigns:mutate +Content-Type: application/json + +{ + "operations": [ + { + "create": { + "name": "New Campaign", + "advertisingChannelType": "SEARCH", + "status": "PAUSED", + "manualCpc": {}, + "campaignBudget": "customers/{customerId}/campaignBudgets/{budgetId}" + } + } + ] +} +``` + +### Update Campaign Status +```bash +POST /google-ads/v23/customers/{customerId}/campaigns:mutate +Content-Type: application/json + +{ + "operations": [ + { + "update": { + "resourceName": "customers/{customerId}/campaigns/{campaignId}", + "status": "ENABLED" + }, + "updateMask": "status" + } + ] +} +``` + +## Notes + +- Authentication is automatic - the router injects OAuth token and developer-token headers +- Use `listAccessibleCustomers` first to get available customer IDs +- Customer IDs are 10-digit numbers (remove dashes if formatted as XXX-XXX-XXXX) +- Monetary values are in micros (divide by 1,000,000) +- Use GAQL (Google Ads Query Language) for querying +- Date ranges: `LAST_7_DAYS`, `LAST_30_DAYS`, `THIS_MONTH`, etc. +- Status values: `ENABLED`, `PAUSED`, `REMOVED` +- API version updates frequently - check release notes for latest (currently v23) + +## Resources + +- [API Overview](https://developers.google.com/google-ads/api/docs/start) +- [List Accessible Customers](https://developers.google.com/google-ads/api/reference/rpc/v23/CustomerService/ListAccessibleCustomers?transport=rest) +- [Search](https://developers.google.com/google-ads/api/reference/rpc/v23/GoogleAdsService/Search?transport=rest) +- [Search Stream](https://developers.google.com/google-ads/api/reference/rpc/v23/GoogleAdsService/SearchStream?transport=rest) +- [GAQL Reference](https://developers.google.com/google-ads/api/docs/query/overview) +- [Metrics Reference](https://developers.google.com/google-ads/api/fields/v23/metrics) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/google-analytics-admin.md b/.agents/skills/api-gateway/references/google-analytics-admin.md new file mode 100644 index 00000000..b78f7234 --- /dev/null +++ b/.agents/skills/api-gateway/references/google-analytics-admin.md @@ -0,0 +1,210 @@ +# Google Analytics Admin Routing Reference + +**App name:** `google-analytics-admin` +**Base URL proxied:** `analyticsadmin.googleapis.com` + +## API Path Pattern + +``` +/google-analytics-admin/v1beta/{endpoint} +``` + +## Common Endpoints + +### List Accounts +```bash +GET /google-analytics-admin/v1beta/accounts +``` + +### Get Account +```bash +GET /google-analytics-admin/v1beta/accounts/{accountId} +``` + +### List Properties +```bash +GET /google-analytics-admin/v1beta/properties?filter=parent:accounts/{accountId} +``` + +### Get Property +```bash +GET /google-analytics-admin/v1beta/properties/{propertyId} +``` + +### Create Property +```bash +POST /google-analytics-admin/v1beta/properties +Content-Type: application/json + +{ + "parent": "accounts/{accountId}", + "displayName": "My New Property", + "timeZone": "America/Los_Angeles", + "currencyCode": "USD", + "industryCategory": "TECHNOLOGY" +} +``` + +### Update Property +```bash +PATCH /google-analytics-admin/v1beta/properties/{propertyId}?updateMask=displayName +Content-Type: application/json + +{ + "displayName": "Updated Property Name" +} +``` + +### List Data Streams +```bash +GET /google-analytics-admin/v1beta/properties/{propertyId}/dataStreams +``` + +### Get Data Stream +```bash +GET /google-analytics-admin/v1beta/properties/{propertyId}/dataStreams/{dataStreamId} +``` + +### Create Web Data Stream +```bash +POST /google-analytics-admin/v1beta/properties/{propertyId}/dataStreams +Content-Type: application/json + +{ + "type": "WEB_DATA_STREAM", + "displayName": "My Website", + "webStreamData": { + "defaultUri": "https://example.com" + } +} +``` + +### List Custom Dimensions +```bash +GET /google-analytics-admin/v1beta/properties/{propertyId}/customDimensions +``` + +### Create Custom Dimension +```bash +POST /google-analytics-admin/v1beta/properties/{propertyId}/customDimensions +Content-Type: application/json + +{ + "parameterName": "user_type", + "displayName": "User Type", + "scope": "USER", + "description": "Type of user (free, premium, enterprise)" +} +``` + +### List Custom Metrics +```bash +GET /google-analytics-admin/v1beta/properties/{propertyId}/customMetrics +``` + +### Create Custom Metric +```bash +POST /google-analytics-admin/v1beta/properties/{propertyId}/customMetrics +Content-Type: application/json + +{ + "parameterName": "points_earned", + "displayName": "Points Earned", + "scope": "EVENT", + "measurementUnit": "STANDARD", + "description": "Number of loyalty points earned" +} +``` + +### List Conversion Events +```bash +GET /google-analytics-admin/v1beta/properties/{propertyId}/conversionEvents +``` + +### Create Conversion Event +```bash +POST /google-analytics-admin/v1beta/properties/{propertyId}/conversionEvents +Content-Type: application/json + +{ + "eventName": "purchase" +} +``` + +### Get Measurement Protocol Secret +```bash +GET /google-analytics-admin/v1beta/properties/{propertyId}/dataStreams/{dataStreamId}/measurementProtocolSecrets +``` + +### Create Measurement Protocol Secret +```bash +POST /google-analytics-admin/v1beta/properties/{propertyId}/dataStreams/{dataStreamId}/measurementProtocolSecrets +Content-Type: application/json + +{ + "displayName": "Server-side tracking" +} +``` + +## Account Summaries + +### List Account Summaries +```bash +GET /google-analytics-admin/v1beta/accountSummaries +``` +Returns a lightweight summary of all accounts and properties the user has access to. + +## Data Stream Types + +- `WEB_DATA_STREAM` - Website tracking +- `ANDROID_APP_DATA_STREAM` - Android app +- `IOS_APP_DATA_STREAM` - iOS app + +## Custom Dimension Scopes + +- `EVENT` - Dimension applies to events +- `USER` - Dimension applies to users + +## Custom Metric Scopes + +- `EVENT` - Metric applies to events + +## Measurement Units (Custom Metrics) + +- `STANDARD` - Integer or decimal +- `CURRENCY` - Currency value +- `FEET`, `METERS` - Distance +- `MILES`, `KILOMETERS` - Distance +- `MILLISECONDS`, `SECONDS`, `MINUTES`, `HOURS` - Time + +## Industry Categories + +- `AUTOMOTIVE`, `BUSINESS_AND_INDUSTRIAL_MARKETS`, `FINANCE`, `HEALTHCARE` +- `TECHNOLOGY`, `TRAVEL`, `RETAIL`, `REAL_ESTATE`, `GAMES` +- `ARTS_AND_ENTERTAINMENT`, `BEAUTY_AND_FITNESS`, `BOOKS_AND_LITERATURE` +- `FOOD_AND_DRINK`, `HOBBIES_AND_LEISURE`, `HOME_AND_GARDEN` +- `INTERNET_AND_TELECOM`, `JOBS_AND_EDUCATION`, `LAW_AND_GOVERNMENT` +- `NEWS`, `ONLINE_COMMUNITIES`, `PEOPLE_AND_SOCIETY`, `PETS_AND_ANIMALS` +- `REFERENCE`, `SCIENCE`, `SHOPPING`, `SPORTS` + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Property IDs are numeric (e.g., `properties/521310447`) +- Account IDs are numeric (e.g., `accounts/123456789`) +- GA4 properties only (Universal Analytics not supported) +- Use `accountSummaries` endpoint to quickly list all accessible properties +- The `filter` parameter on list properties uses format: `parent:accounts/{accountId}` +- Use `updateMask` query parameter to specify which fields to update in PATCH requests +- This API is for property/account management - use the Data API for running reports + +## Resources + +- [API Overview](https://developers.google.com/analytics/devguides/config/admin/v1) +- [List Accounts](https://developers.google.com/analytics/devguides/config/admin/v1/rest/v1beta/accounts/list) +- [List Properties](https://developers.google.com/analytics/devguides/config/admin/v1/rest/v1beta/properties/list) +- [Create Property](https://developers.google.com/analytics/devguides/config/admin/v1/rest/v1beta/properties/create) +- [Data Streams](https://developers.google.com/analytics/devguides/config/admin/v1/rest/v1beta/properties.dataStreams) +- [Custom Dimensions](https://developers.google.com/analytics/devguides/config/admin/v1/rest/v1beta/properties.customDimensions) +- [Custom Metrics](https://developers.google.com/analytics/devguides/config/admin/v1/rest/v1beta/properties.customMetrics) +- [Conversion Events](https://developers.google.com/analytics/devguides/config/admin/v1/rest/v1beta/properties.conversionEvents) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/google-analytics-data.md b/.agents/skills/api-gateway/references/google-analytics-data.md new file mode 100644 index 00000000..4696fb57 --- /dev/null +++ b/.agents/skills/api-gateway/references/google-analytics-data.md @@ -0,0 +1,151 @@ +# Google Analytics Data Routing Reference + +**App name:** `google-analytics-data` +**Base URL proxied:** `analyticsdata.googleapis.com` + +## API Path Pattern + +``` +/google-analytics-data/v1beta/{endpoint} +``` + +## Common Endpoints + +### Run Report +```bash +POST /google-analytics-data/v1beta/properties/{propertyId}:runReport +Content-Type: application/json + +{ + "dateRanges": [{"startDate": "30daysAgo", "endDate": "today"}], + "dimensions": [{"name": "city"}], + "metrics": [{"name": "activeUsers"}] +} +``` + +### Run Realtime Report +```bash +POST /google-analytics-data/v1beta/properties/{propertyId}:runRealtimeReport +Content-Type: application/json + +{ + "dimensions": [{"name": "country"}], + "metrics": [{"name": "activeUsers"}] +} +``` + +### Batch Run Reports +```bash +POST /google-analytics-data/v1beta/properties/{propertyId}:batchRunReports +Content-Type: application/json + +{ + "requests": [ + { + "dateRanges": [{"startDate": "7daysAgo", "endDate": "today"}], + "dimensions": [{"name": "country"}], + "metrics": [{"name": "sessions"}] + }, + { + "dateRanges": [{"startDate": "7daysAgo", "endDate": "today"}], + "dimensions": [{"name": "deviceCategory"}], + "metrics": [{"name": "sessions"}] + } + ] +} +``` + +### Get Metadata (available dimensions/metrics) +```bash +GET /google-analytics-data/v1beta/properties/{propertyId}/metadata +``` + +## Common Report Examples + +### Page Views by Page +```json +{ + "dateRanges": [{"startDate": "30daysAgo", "endDate": "today"}], + "dimensions": [{"name": "pagePath"}], + "metrics": [{"name": "screenPageViews"}], + "orderBys": [{"metric": {"metricName": "screenPageViews"}, "desc": true}], + "limit": 10 +} +``` + +### Users by Country +```json +{ + "dateRanges": [{"startDate": "30daysAgo", "endDate": "today"}], + "dimensions": [{"name": "country"}], + "metrics": [{"name": "activeUsers"}, {"name": "sessions"}], + "orderBys": [{"metric": {"metricName": "activeUsers"}, "desc": true}] +} +``` + +### Traffic Sources +```json +{ + "dateRanges": [{"startDate": "30daysAgo", "endDate": "today"}], + "dimensions": [{"name": "sessionSource"}, {"name": "sessionMedium"}], + "metrics": [{"name": "sessions"}, {"name": "conversions"}] +} +``` + +### Device Breakdown +```json +{ + "dateRanges": [{"startDate": "7daysAgo", "endDate": "today"}], + "dimensions": [{"name": "deviceCategory"}], + "metrics": [{"name": "activeUsers"}, {"name": "sessions"}, {"name": "bounceRate"}] +} +``` + +### Daily Sessions Trend +```json +{ + "dateRanges": [{"startDate": "30daysAgo", "endDate": "today"}], + "dimensions": [{"name": "date"}], + "metrics": [{"name": "sessions"}, {"name": "activeUsers"}], + "orderBys": [{"dimension": {"dimensionName": "date"}}] +} +``` + +## Common Dimensions + +- `date`, `dateHour`, `dateHourMinute` +- `country`, `city`, `region` +- `deviceCategory`, `browser`, `operatingSystem` +- `pagePath`, `pageTitle`, `landingPage` +- `sessionSource`, `sessionMedium`, `sessionCampaignName` +- `eventName` + +## Common Metrics + +- `activeUsers`, `newUsers`, `totalUsers` +- `sessions`, `sessionsPerUser` +- `screenPageViews`, `screenPageViewsPerSession` +- `bounceRate`, `averageSessionDuration` +- `conversions`, `eventCount` + +## Date Formats + +- Relative: `today`, `yesterday`, `7daysAgo`, `30daysAgo` +- Absolute: `2026-01-01` + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Property IDs are numeric (e.g., `521310447` from URL `p521310447`) +- GA4 properties only (Universal Analytics not supported) +- Use metadata endpoint to discover available dimensions/metrics +- Results are paginated with `limit` and `offset` +- This API is for running reports only - listing properties requires the Admin API + +## Resources + +- [API Overview](https://developers.google.com/analytics/devguides/reporting/data/v1) +- [Run Report](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/runReport) +- [Run Realtime Report](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/runRealtimeReport) +- [Batch Run Reports](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/batchRunReports) +- [Get Metadata](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/getMetadata) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/google-calendar.md b/.agents/skills/api-gateway/references/google-calendar.md new file mode 100644 index 00000000..8df09eaa --- /dev/null +++ b/.agents/skills/api-gateway/references/google-calendar.md @@ -0,0 +1,139 @@ +# Google Calendar Routing Reference + +**App name:** `google-calendar` +**Base URL proxied:** `www.googleapis.com` + +## API Path Pattern + +``` +/google-calendar/calendar/v3/{endpoint} +``` + +## Common Endpoints + +### List Calendars +```bash +GET /google-calendar/calendar/v3/users/me/calendarList +``` + +### Get Calendar +```bash +GET /google-calendar/calendar/v3/calendars/{calendarId} +``` + +Use `primary` for the user's primary calendar. + +### List Events +```bash +GET /google-calendar/calendar/v3/calendars/primary/events?maxResults=10&orderBy=startTime&singleEvents=true +``` + +With time bounds: +```bash +GET /google-calendar/calendar/v3/calendars/primary/events?timeMin=2024-01-01T00:00:00Z&timeMax=2024-12-31T23:59:59Z&singleEvents=true&orderBy=startTime +``` + +### Get Event +```bash +GET /google-calendar/calendar/v3/calendars/primary/events/{eventId} +``` + +### Insert Event +```bash +POST /google-calendar/calendar/v3/calendars/primary/events +Content-Type: application/json + +{ + "summary": "Team Meeting", + "description": "Weekly sync", + "start": { + "dateTime": "2024-01-15T10:00:00", + "timeZone": "America/Los_Angeles" + }, + "end": { + "dateTime": "2024-01-15T11:00:00", + "timeZone": "America/Los_Angeles" + }, + "attendees": [ + {"email": "attendee@example.com"} + ] +} +``` + +All-day event: +```bash +POST /google-calendar/calendar/v3/calendars/primary/events +Content-Type: application/json + +{ + "summary": "All Day Event", + "start": {"date": "2024-01-15"}, + "end": {"date": "2024-01-16"} +} +``` + +### Update Event +```bash +PUT /google-calendar/calendar/v3/calendars/primary/events/{eventId} +Content-Type: application/json + +{ + "summary": "Updated Meeting Title", + "start": {"dateTime": "2024-01-15T10:00:00Z"}, + "end": {"dateTime": "2024-01-15T11:00:00Z"} +} +``` + +### Patch Event (partial update) +```bash +PATCH /google-calendar/calendar/v3/calendars/primary/events/{eventId} +Content-Type: application/json + +{ + "summary": "New Title Only" +} +``` + +### Delete Event +```bash +DELETE /google-calendar/calendar/v3/calendars/primary/events/{eventId} +``` + +### Quick Add Event (natural language) +```bash +POST /google-calendar/calendar/v3/calendars/primary/events/quickAdd?text=Meeting+with+John+tomorrow+at+3pm +``` + +### Free/Busy Query +```bash +POST /google-calendar/calendar/v3/freeBusy +Content-Type: application/json + +{ + "timeMin": "2024-01-15T00:00:00Z", + "timeMax": "2024-01-16T00:00:00Z", + "items": [{"id": "primary"}] +} +``` + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Use `primary` as calendarId for the user's main calendar +- Times must be in RFC3339 format (e.g., `2026-01-15T10:00:00Z`) +- For recurring events, use `singleEvents=true` to expand instances +- `orderBy=startTime` requires `singleEvents=true` + +## Resources + +- [API Overview](https://developers.google.com/calendar/api/v3/reference) +- [List Calendars](https://developers.google.com/workspace/calendar/api/v3/reference/calendarList/list) +- [Get Calendar](https://developers.google.com/workspace/calendar/api/v3/reference/calendarList/get) +- [List Events](https://developers.google.com/workspace/calendar/api/v3/reference/events/list) +- [Get Event](https://developers.google.com/workspace/calendar/api/v3/reference/events/get) +- [Insert Event](https://developers.google.com/workspace/calendar/api/v3/reference/events/insert) +- [Update Event](https://developers.google.com/workspace/calendar/api/v3/reference/events/update) +- [Patch Event](https://developers.google.com/workspace/calendar/api/v3/reference/events/patch) +- [Delete Event](https://developers.google.com/workspace/calendar/api/v3/reference/events/delete) +- [Quick Add Event](https://developers.google.com/workspace/calendar/api/v3/reference/events/quickAdd) +- [Free/Busy Query](https://developers.google.com/workspace/calendar/api/v3/reference/freebusy/query) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/google-contacts.md b/.agents/skills/api-gateway/references/google-contacts.md new file mode 100644 index 00000000..d056b9d8 --- /dev/null +++ b/.agents/skills/api-gateway/references/google-contacts.md @@ -0,0 +1,139 @@ +# Google Contacts Routing Reference + +**App name:** `google-contacts` +**Base URL proxied:** `people.googleapis.com` + +## API Path Pattern + +``` +/google-contacts/v1/{endpoint} +``` + +## Common Endpoints + +### List Contacts +```bash +GET /google-contacts/v1/people/me/connections?personFields=names,emailAddresses,phoneNumbers&pageSize=100 +``` + +### Get Contact +```bash +GET /google-contacts/v1/people/{resourceName}?personFields=names,emailAddresses,phoneNumbers +``` + +Example: `GET /google-contacts/v1/people/c1234567890?personFields=names,emailAddresses` + +### Create Contact +```bash +POST /google-contacts/v1/people:createContact +Content-Type: application/json + +{ + "names": [{"givenName": "John", "familyName": "Doe"}], + "emailAddresses": [{"value": "john@example.com"}], + "phoneNumbers": [{"value": "+1-555-0123"}] +} +``` + +### Update Contact +```bash +PATCH /google-contacts/v1/people/{resourceName}:updateContact?updatePersonFields=names,emailAddresses +Content-Type: application/json + +{ + "etag": "%EgcBAgkLLjc9...", + "names": [{"givenName": "John", "familyName": "Smith"}] +} +``` + +### Delete Contact +```bash +DELETE /google-contacts/v1/people/{resourceName}:deleteContact +``` + +### Batch Get Contacts +```bash +GET /google-contacts/v1/people:batchGet?resourceNames=people/c123&resourceNames=people/c456&personFields=names +``` + +### Batch Create Contacts +```bash +POST /google-contacts/v1/people:batchCreateContacts +Content-Type: application/json + +{ + "contacts": [{"contactPerson": {"names": [{"givenName": "Alice"}]}}], + "readMask": "names" +} +``` + +### Batch Delete Contacts +```bash +POST /google-contacts/v1/people:batchDeleteContacts +Content-Type: application/json + +{ + "resourceNames": ["people/c123", "people/c456"] +} +``` + +### Search Contacts +```bash +GET /google-contacts/v1/people:searchContacts?query=John&readMask=names,emailAddresses +``` + +### List Contact Groups +```bash +GET /google-contacts/v1/contactGroups?pageSize=100 +``` + +### Get Contact Group +```bash +GET /google-contacts/v1/contactGroups/{resourceName}?maxMembers=100 +``` + +### Create Contact Group +```bash +POST /google-contacts/v1/contactGroups +Content-Type: application/json + +{ + "contactGroup": {"name": "Work Contacts"} +} +``` + +### Delete Contact Group +```bash +DELETE /google-contacts/v1/contactGroups/{resourceName}?deleteContacts=false +``` + +### Modify Group Members +```bash +POST /google-contacts/v1/contactGroups/{resourceName}/members:modify +Content-Type: application/json + +{ + "resourceNamesToAdd": ["people/c123"], + "resourceNamesToRemove": ["people/c456"] +} +``` + +### List Other Contacts +```bash +GET /google-contacts/v1/otherContacts?readMask=names,emailAddresses&pageSize=100 +``` + +## Notes + +- Resource names for contacts: `people/c{id}` (e.g., `people/c1234567890`) +- Resource names for groups: `contactGroups/{id}` (e.g., `contactGroups/starred`) +- System groups: `starred`, `friends`, `family`, `coworkers`, `myContacts`, `all`, `blocked` +- `personFields` parameter is required for most read operations +- Include `etag` when updating to prevent concurrent modification issues +- Pagination uses `pageToken` parameter + +## Resources + +- [Google People API Overview](https://developers.google.com/people/api/rest) +- [People Resource](https://developers.google.com/people/api/rest/v1/people) +- [Contact Groups Resource](https://developers.google.com/people/api/rest/v1/contactGroups) diff --git a/.agents/skills/api-gateway/references/google-docs.md b/.agents/skills/api-gateway/references/google-docs.md new file mode 100644 index 00000000..8a90295a --- /dev/null +++ b/.agents/skills/api-gateway/references/google-docs.md @@ -0,0 +1,158 @@ +# Google Docs Routing Reference + +**App name:** `google-docs` +**Base URL proxied:** `docs.googleapis.com` + +## API Path Pattern + +``` +/google-docs/v1/documents/{documentId} +``` + +## Common Endpoints + +### Get Document +```bash +GET /google-docs/v1/documents/{documentId} +``` + +### Create Document +```bash +POST /google-docs/v1/documents +Content-Type: application/json + +{ + "title": "New Document" +} +``` + +### Batch Update Document +```bash +POST /google-docs/v1/documents/{documentId}:batchUpdate +Content-Type: application/json + +{ + "requests": [ + { + "insertText": { + "location": {"index": 1}, + "text": "Hello, World!" + } + } + ] +} +``` + +## Common Requests for batchUpdate + +### Insert Text +```json +{ + "insertText": { + "location": {"index": 1}, + "text": "Text to insert" + } +} +``` + +### Delete Content +```json +{ + "deleteContentRange": { + "range": { + "startIndex": 1, + "endIndex": 10 + } + } +} +``` + +### Replace All Text +```json +{ + "replaceAllText": { + "containsText": { + "text": "{{placeholder}}", + "matchCase": true + }, + "replaceText": "replacement value" + } +} +``` + +### Insert Table +```json +{ + "insertTable": { + "location": {"index": 1}, + "rows": 3, + "columns": 3 + } +} +``` + +### Insert Inline Image +```json +{ + "insertInlineImage": { + "location": {"index": 1}, + "uri": "https://example.com/image.png", + "objectSize": { + "height": {"magnitude": 100, "unit": "PT"}, + "width": {"magnitude": 100, "unit": "PT"} + } + } +} +``` + +### Update Text Style +```json +{ + "updateTextStyle": { + "range": { + "startIndex": 1, + "endIndex": 10 + }, + "textStyle": { + "bold": true, + "fontSize": {"magnitude": 14, "unit": "PT"} + }, + "fields": "bold,fontSize" + } +} +``` + +### Insert Page Break +```json +{ + "insertPageBreak": { + "location": {"index": 1} + } +} +``` + +## Document Structure + +The document body contains: +- `content` - Array of structural elements +- `body.content[].paragraph` - Paragraph element +- `body.content[].table` - Table element +- `body.content[].sectionBreak` - Section break + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Index positions are 1-based (document starts at index 1) +- Use `endOfSegmentLocation` to append at end +- Multiple requests in batchUpdate are applied atomically +- Get document first to find correct indices for updates +- The `fields` parameter in style updates uses field mask syntax + +## Resources + +- [API Overview](https://developers.google.com/docs/api/how-tos/overview) +- [Get Document](https://developers.google.com/docs/api/reference/rest/v1/documents/get) +- [Create Document](https://developers.google.com/docs/api/reference/rest/v1/documents/create) +- [Batch Update](https://developers.google.com/docs/api/reference/rest/v1/documents/batchUpdate) +- [Request Types Reference](https://developers.google.com/docs/api/reference/rest/v1/documents/request) +- [Document Structure Guide](https://developers.google.com/docs/api/concepts/structure) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/google-drive.md b/.agents/skills/api-gateway/references/google-drive.md new file mode 100644 index 00000000..fbbc1f4f --- /dev/null +++ b/.agents/skills/api-gateway/references/google-drive.md @@ -0,0 +1,158 @@ +# Google Drive Routing Reference + +**App name:** `google-drive` +**Base URL proxied:** `www.googleapis.com` + +## API Path Pattern + +``` +/google-drive/drive/v3/{endpoint} +``` + +## Common Endpoints + +### List Files +```bash +GET /google-drive/drive/v3/files?pageSize=10 +``` + +With query: +```bash +GET /google-drive/drive/v3/files?q=name%20contains%20'report'&pageSize=10 +``` + +Only folders: +```bash +GET /google-drive/drive/v3/files?q=mimeType='application/vnd.google-apps.folder' +``` + +Files in specific folder: +```bash +GET /google-drive/drive/v3/files?q='FOLDER_ID'+in+parents +``` + +With fields: +```bash +GET /google-drive/drive/v3/files?fields=files(id,name,mimeType,createdTime,modifiedTime,size) +``` + +### Get File Metadata +```bash +GET /google-drive/drive/v3/files/{fileId}?fields=id,name,mimeType,size,createdTime +``` + +### Download File Content +```bash +GET /google-drive/drive/v3/files/{fileId}?alt=media +``` + +### Export Google Docs (to PDF, DOCX, etc.) +```bash +GET /google-drive/drive/v3/files/{fileId}/export?mimeType=application/pdf +``` + +### Create File (metadata only) +```bash +POST /google-drive/drive/v3/files +Content-Type: application/json + +{ + "name": "New Document", + "mimeType": "application/vnd.google-apps.document" +} +``` + +### Create Folder +```bash +POST /google-drive/drive/v3/files +Content-Type: application/json + +{ + "name": "New Folder", + "mimeType": "application/vnd.google-apps.folder" +} +``` + +### Update File Metadata +```bash +PATCH /google-drive/drive/v3/files/{fileId} +Content-Type: application/json + +{ + "name": "Renamed File" +} +``` + +### Move File to Folder +```bash +PATCH /google-drive/drive/v3/files/{fileId}?addParents=NEW_FOLDER_ID&removeParents=OLD_FOLDER_ID +``` + +### Delete File +```bash +DELETE /google-drive/drive/v3/files/{fileId} +``` + +### Copy File +```bash +POST /google-drive/drive/v3/files/{fileId}/copy +Content-Type: application/json + +{ + "name": "Copy of File" +} +``` + +### Create Permission (Share File) +```bash +POST /google-drive/drive/v3/files/{fileId}/permissions +Content-Type: application/json + +{ + "role": "reader", + "type": "user", + "emailAddress": "user@example.com" +} +``` + +## Query Operators + +Use in the `q` parameter: +- `name = 'exact name'` +- `name contains 'partial'` +- `mimeType = 'application/pdf'` +- `'folderId' in parents` +- `trashed = false` +- `modifiedTime > '2024-01-01T00:00:00'` + +Combine with `and`: +``` +name contains 'report' and mimeType = 'application/pdf' +``` + +## Common MIME Types + +- `application/vnd.google-apps.document` - Google Docs +- `application/vnd.google-apps.spreadsheet` - Google Sheets +- `application/vnd.google-apps.presentation` - Google Slides +- `application/vnd.google-apps.folder` - Folder +- `application/pdf` - PDF + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Use `fields` parameter to limit response data +- Pagination uses `pageToken` from previous response's `nextPageToken` + +## Resources + +- [API Overview](https://developers.google.com/workspace/drive/api/reference/rest/v3#rest-resource:-v3.about) +- [List Files](https://developers.google.com/drive/api/reference/rest/v3/files/list) +- [Get File](https://developers.google.com/drive/api/reference/rest/v3/files/get) +- [Create File](https://developers.google.com/drive/api/reference/rest/v3/files/create) +- [Update File](https://developers.google.com/drive/api/reference/rest/v3/files/update) +- [Delete File](https://developers.google.com/drive/api/reference/rest/v3/files/delete) +- [Copy File](https://developers.google.com/drive/api/reference/rest/v3/files/copy) +- [Export File](https://developers.google.com/drive/api/reference/rest/v3/files/export) +- [Create Permission](https://developers.google.com/workspace/drive/api/reference/rest/v3/permissions/create) +- [Search Query Syntax](https://developers.google.com/drive/api/guides/search-files) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/google-forms.md b/.agents/skills/api-gateway/references/google-forms.md new file mode 100644 index 00000000..3e40f7b1 --- /dev/null +++ b/.agents/skills/api-gateway/references/google-forms.md @@ -0,0 +1,206 @@ +# Google Forms Routing Reference + +**App name:** `google-forms` +**Base URL proxied:** `forms.googleapis.com` + +## API Path Pattern + +``` +/google-forms/v1/forms/{formId} +``` + +## Common Endpoints + +### Get Form +```bash +GET /google-forms/v1/forms/{formId} +``` + +### Create Form +```bash +POST /google-forms/v1/forms +Content-Type: application/json + +{ + "info": { + "title": "Customer Feedback Survey" + } +} +``` + +### Batch Update Form +```bash +POST /google-forms/v1/forms/{formId}:batchUpdate +Content-Type: application/json + +{ + "requests": [ + { + "createItem": { + "item": { + "title": "What is your name?", + "questionItem": { + "question": { + "required": true, + "textQuestion": { + "paragraph": false + } + } + } + }, + "location": {"index": 0} + } + } + ] +} +``` + +### List Responses +```bash +GET /google-forms/v1/forms/{formId}/responses +``` + +### Get Response +```bash +GET /google-forms/v1/forms/{formId}/responses/{responseId} +``` + +## Common Requests for batchUpdate + +### Create Text Question +```json +{ + "createItem": { + "item": { + "title": "Question text", + "questionItem": { + "question": { + "required": true, + "textQuestion": {"paragraph": false} + } + } + }, + "location": {"index": 0} + } +} +``` + +### Create Multiple Choice Question +```json +{ + "createItem": { + "item": { + "title": "Select an option", + "questionItem": { + "question": { + "required": true, + "choiceQuestion": { + "type": "RADIO", + "options": [ + {"value": "Option A"}, + {"value": "Option B"}, + {"value": "Option C"} + ] + } + } + } + }, + "location": {"index": 0} + } +} +``` + +### Create Checkbox Question +```json +{ + "createItem": { + "item": { + "title": "Select all that apply", + "questionItem": { + "question": { + "choiceQuestion": { + "type": "CHECKBOX", + "options": [ + {"value": "Option 1"}, + {"value": "Option 2"} + ] + } + } + } + }, + "location": {"index": 0} + } +} +``` + +### Create Scale Question +```json +{ + "createItem": { + "item": { + "title": "Rate your experience", + "questionItem": { + "question": { + "scaleQuestion": { + "low": 1, + "high": 5, + "lowLabel": "Poor", + "highLabel": "Excellent" + } + } + } + }, + "location": {"index": 0} + } +} +``` + +### Update Form Info +```json +{ + "updateFormInfo": { + "info": { + "title": "New Form Title", + "description": "Form description" + }, + "updateMask": "title,description" + } +} +``` + +### Delete Item +```json +{ + "deleteItem": { + "location": {"index": 0} + } +} +``` + +## Question Types + +- `textQuestion` - Short or paragraph text +- `choiceQuestion` - Radio, checkbox, or dropdown +- `scaleQuestion` - Linear scale +- `dateQuestion` - Date picker +- `timeQuestion` - Time picker +- `fileUploadQuestion` - File upload + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Form IDs can be found in the form URL +- Responses include `answers` keyed by question ID +- Use `updateMask` to specify which fields to update +- Location index is 0-based for item positioning + +## Resources + +- [API Overview](https://developers.google.com/workspace/forms/api/reference/rest) +- [Get Form](https://developers.google.com/workspace/forms/api/reference/rest/v1/forms/get) +- [Create Form](https://developers.google.com/workspace/forms/api/reference/rest/v1/forms/create) +- [Batch Update Form](https://developers.google.com/workspace/forms/api/reference/rest/v1/forms/batchUpdate) +- [Batch Update Request Types](https://developers.google.com/workspace/forms/api/reference/rest/v1/forms/batchUpdate#request) +- [List Responses](https://developers.google.com/workspace/forms/api/reference/rest/v1/forms.responses/list) +- [Get Response](https://developers.google.com/workspace/forms/api/reference/rest/v1/forms.responses/get) +- [Form Resource](https://developers.google.com/workspace/forms/api/reference/rest/v1/forms) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/google-mail.md b/.agents/skills/api-gateway/references/google-mail.md new file mode 100644 index 00000000..ae4d10b3 --- /dev/null +++ b/.agents/skills/api-gateway/references/google-mail.md @@ -0,0 +1,146 @@ +# Gmail Routing Reference + +**App name:** `google-mail` +**Base URL proxied:** `gmail.googleapis.com` + +## API Path Pattern + +``` +/google-mail/gmail/v1/users/me/{endpoint} +``` + +## Common Endpoints + +### List Messages +```bash +GET /google-mail/gmail/v1/users/me/messages?maxResults=10 +``` + +With query filter: +```bash +GET /google-mail/gmail/v1/users/me/messages?q=is:unread&maxResults=10 +``` + +### Get Message +```bash +GET /google-mail/gmail/v1/users/me/messages/{messageId} +``` + +With metadata only: +```bash +GET /google-mail/gmail/v1/users/me/messages/{messageId}?format=metadata&metadataHeaders=From&metadataHeaders=Subject&metadataHeaders=Date +``` + +### Send Message +```bash +POST /google-mail/gmail/v1/users/me/messages/send +Content-Type: application/json + +{ + "raw": "BASE64_ENCODED_EMAIL" +} +``` + +### List Labels +```bash +GET /google-mail/gmail/v1/users/me/labels +``` + +### List Threads +```bash +GET /google-mail/gmail/v1/users/me/threads?maxResults=10 +``` + +### Get Thread +```bash +GET /google-mail/gmail/v1/users/me/threads/{threadId} +``` + +### Modify Message Labels +```bash +POST /google-mail/gmail/v1/users/me/messages/{messageId}/modify +Content-Type: application/json + +{ + "addLabelIds": ["STARRED"], + "removeLabelIds": ["UNREAD"] +} +``` + +### Trash Message +```bash +POST /google-mail/gmail/v1/users/me/messages/{messageId}/trash +``` + +### Create Draft +```bash +POST /google-mail/gmail/v1/users/me/drafts +Content-Type: application/json + +{ + "message": { + "raw": "BASE64URL_ENCODED_EMAIL" + } +} +``` + +### Update Draft +```bash +PUT /google-mail/gmail/v1/users/me/drafts/{draftId} +Content-Type: application/json + +{ + "message": { + "raw": "BASE64URL_ENCODED_EMAIL" + } +} +``` + +### Send Draft +```bash +POST /google-mail/gmail/v1/users/me/drafts/send +Content-Type: application/json + +{ + "id": "{draftId}" +} +``` + +### Get Profile +```bash +GET /google-mail/gmail/v1/users/me/profile +``` + +## Query Operators + +Use in the `q` parameter: +- `is:unread` - Unread messages +- `is:starred` - Starred messages +- `from:email@example.com` - From specific sender +- `to:email@example.com` - To specific recipient +- `subject:keyword` - Subject contains keyword +- `after:2024/01/01` - After date +- `before:2024/12/31` - Before date +- `has:attachment` - Has attachments + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Use `me` as userId for the authenticated user +- Message body is base64url encoded in the `raw` field + +## Resources + +- [API Overview](https://developers.google.com/gmail/api/reference/rest) +- [List Messages](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list) +- [Get Message](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/get) +- [Send Message](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/send) +- [Modify Message Labels](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/modify) +- [Trash Message](https://developers.google.com/gmail/api/reference/rest/v1/users.messages/trash) +- [List Threads](https://developers.google.com/gmail/api/reference/rest/v1/users.threads/list) +- [Get Thread](https://developers.google.com/gmail/api/reference/rest/v1/users.threads/get) +- [List Labels](https://developers.google.com/gmail/api/reference/rest/v1/users.labels/list) +- [Create Draft](https://developers.google.com/gmail/api/reference/rest/v1/users.drafts/create) +- [Update Draft](https://developers.google.com/gmail/api/reference/rest/v1/users.drafts/update) +- [Send Draft](https://developers.google.com/gmail/api/reference/rest/v1/users.drafts/send) +- [Get Profile](https://developers.google.com/gmail/api/reference/rest/v1/users/getProfile) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/google-meet.md b/.agents/skills/api-gateway/references/google-meet.md new file mode 100644 index 00000000..fcf4242b --- /dev/null +++ b/.agents/skills/api-gateway/references/google-meet.md @@ -0,0 +1,121 @@ +# Google Meet Routing Reference + +**App name:** `google-meet` +**Base URL proxied:** `meet.googleapis.com` + +## API Path Pattern + +``` +/google-meet/v2/{resource} +``` + +## Common Endpoints + +### Create Space +```bash +POST /google-meet/v2/spaces +Content-Type: application/json + +{} +``` + +Response: +```json +{ + "name": "spaces/abc123", + "meetingUri": "https://meet.google.com/abc-defg-hij", + "meetingCode": "abc-defg-hij", + "config": { + "accessType": "OPEN", + "entryPointAccess": "ALL" + } +} +``` + +### Get Space +```bash +GET /google-meet/v2/spaces/{spaceId} +``` + +### Update Space +```bash +PATCH /google-meet/v2/spaces/{spaceId} +Content-Type: application/json + +{ + "config": { + "accessType": "TRUSTED" + } +} +``` + +### End Active Call +```bash +POST /google-meet/v2/spaces/{spaceId}:endActiveConference +``` + +### List Conference Records +```bash +GET /google-meet/v2/conferenceRecords +``` + +With filter: +```bash +GET /google-meet/v2/conferenceRecords?filter=space.name="spaces/abc123" +``` + +### Get Conference Record +```bash +GET /google-meet/v2/conferenceRecords/{conferenceRecordId} +``` + +### List Participants +```bash +GET /google-meet/v2/conferenceRecords/{conferenceRecordId}/participants +``` + +### Get Participant +```bash +GET /google-meet/v2/conferenceRecords/{conferenceRecordId}/participants/{participantId} +``` + +### List Recordings +```bash +GET /google-meet/v2/conferenceRecords/{conferenceRecordId}/recordings +``` + +### Get Recording +```bash +GET /google-meet/v2/conferenceRecords/{conferenceRecordId}/recordings/{recordingId} +``` + +### List Transcripts +```bash +GET /google-meet/v2/conferenceRecords/{conferenceRecordId}/transcripts +``` + +### Get Transcript +```bash +GET /google-meet/v2/conferenceRecords/{conferenceRecordId}/transcripts/{transcriptId} +``` + +### List Transcript Entries +```bash +GET /google-meet/v2/conferenceRecords/{conferenceRecordId}/transcripts/{transcriptId}/entries +``` + +## Notes + +- Spaces are persistent meeting rooms that can be reused +- Conference records are created when a meeting starts and track meeting history +- Access types: `OPEN` (anyone with link), `TRUSTED` (organization members only), `RESTRICTED` (invited only) +- Recordings and transcripts require Google Workspace with recording enabled + +## Resources + +- [Google Meet API Overview](https://developers.google.com/meet/api/reference/rest) +- [Spaces](https://developers.google.com/meet/api/reference/rest/v2/spaces) +- [Conference Records](https://developers.google.com/meet/api/reference/rest/v2/conferenceRecords) +- [Participants](https://developers.google.com/meet/api/reference/rest/v2/conferenceRecords.participants) +- [Recordings](https://developers.google.com/meet/api/reference/rest/v2/conferenceRecords.recordings) +- [Transcripts](https://developers.google.com/meet/api/reference/rest/v2/conferenceRecords.transcripts) diff --git a/.agents/skills/api-gateway/references/google-merchant.md b/.agents/skills/api-gateway/references/google-merchant.md new file mode 100644 index 00000000..d8f3d46e --- /dev/null +++ b/.agents/skills/api-gateway/references/google-merchant.md @@ -0,0 +1,96 @@ +# Google Merchant Routing Reference + +**App name:** `google-merchant` +**Base URL proxied:** `merchantapi.googleapis.com` + +## API Path Pattern + +``` +/google-merchant/{sub-api}/{version}/accounts/{accountId}/{resource} +``` + +The Merchant API uses sub-APIs: `products`, `accounts`, `datasources`, `reports`, `promotions`, `inventories`, `notifications`, `conversions`, `lfp` + +## Common Endpoints + +### List Products +```bash +GET /google-merchant/products/v1/accounts/{accountId}/products +``` + +### Get Product +```bash +GET /google-merchant/products/v1/accounts/{accountId}/products/{productId} +``` + +Product ID format: `contentLanguage~feedLabel~offerId` (e.g., `en~US~sku123`) + +### Insert Product Input +```bash +POST /google-merchant/products/v1/accounts/{accountId}/productInputs:insert?dataSource=accounts/{accountId}/dataSources/{dataSourceId} +Content-Type: application/json + +{ + "offerId": "sku123", + "contentLanguage": "en", + "feedLabel": "US", + "attributes": { + "title": "Product Title", + "link": "https://example.com/product", + "imageLink": "https://example.com/image.jpg", + "availability": "in_stock", + "price": {"amountMicros": "19990000", "currencyCode": "USD"} + } +} +``` + +### Delete Product Input +```bash +DELETE /google-merchant/products/v1/accounts/{accountId}/productInputs/{productId}?dataSource=accounts/{accountId}/dataSources/{dataSourceId} +``` + +### List Data Sources +```bash +GET /google-merchant/datasources/v1/accounts/{accountId}/dataSources +``` + +### Search Reports +```bash +POST /google-merchant/reports/v1/accounts/{accountId}/reports:search +Content-Type: application/json + +{ + "query": "SELECT offer_id, title, clicks FROM product_performance_view WHERE date BETWEEN '2026-01-01' AND '2026-01-31'" +} +``` + +### List Promotions +```bash +GET /google-merchant/promotions/v1/accounts/{accountId}/promotions +``` + +### Get Account +```bash +GET /google-merchant/accounts/v1/accounts/{accountId} +``` + +### List Local Inventories +```bash +GET /google-merchant/inventories/v1/accounts/{accountId}/products/{productId}/localInventories +``` + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Account ID is your Merchant Center numeric ID (visible in MC URL) +- Product IDs use format `contentLanguage~feedLabel~offerId` +- Monetary values use micros (divide by 1,000,000) +- Products can only be inserted in data sources of type `API` +- Uses token-based pagination with `pageSize` and `pageToken` + +## Resources + +- [Merchant API Overview](https://developers.google.com/merchant/api/overview) +- [Merchant API Reference](https://developers.google.com/merchant/api/reference/rest) +- [Products Guide](https://developers.google.com/merchant/api/guides/products/overview) +- [Reports Guide](https://developers.google.com/merchant/api/guides/reports) diff --git a/.agents/skills/api-gateway/references/google-play.md b/.agents/skills/api-gateway/references/google-play.md new file mode 100644 index 00000000..b8f07ede --- /dev/null +++ b/.agents/skills/api-gateway/references/google-play.md @@ -0,0 +1,147 @@ +# Google Play Routing Reference + +**App name:** `google-play` +**Base URL proxied:** `androidpublisher.googleapis.com` + +## API Path Pattern + +``` +/google-play/androidpublisher/v3/applications/{packageName}/{resource} +``` + +## Common Endpoints + +### In-App Products + +#### List In-App Products +```bash +GET /google-play/androidpublisher/v3/applications/{packageName}/inappproducts +``` + +#### Get In-App Product +```bash +GET /google-play/androidpublisher/v3/applications/{packageName}/inappproducts/{sku} +``` + +#### Create In-App Product +```bash +POST /google-play/androidpublisher/v3/applications/{packageName}/inappproducts +Content-Type: application/json + +{ + "packageName": "com.example.app", + "sku": "premium_upgrade", + "status": "active", + "purchaseType": "managedUser", + "defaultPrice": { + "priceMicros": "990000", + "currency": "USD" + }, + "listings": { + "en-US": { + "title": "Premium Upgrade", + "description": "Unlock all premium features" + } + } +} +``` + +#### Delete In-App Product +```bash +DELETE /google-play/androidpublisher/v3/applications/{packageName}/inappproducts/{sku} +``` + +### Subscriptions + +#### List Subscriptions +```bash +GET /google-play/androidpublisher/v3/applications/{packageName}/subscriptions +``` + +#### Get Subscription +```bash +GET /google-play/androidpublisher/v3/applications/{packageName}/subscriptions/{productId} +``` + +### Purchases + +#### Get Product Purchase +```bash +GET /google-play/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token} +``` + +#### Acknowledge Purchase +```bash +POST /google-play/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}:acknowledge +Content-Type: application/json + +{ + "developerPayload": "optional payload" +} +``` + +#### Get Subscription Purchase +```bash +GET /google-play/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token} +``` + +#### Cancel Subscription +```bash +POST /google-play/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}:cancel +``` + +### Reviews + +#### List Reviews +```bash +GET /google-play/androidpublisher/v3/applications/{packageName}/reviews +``` + +#### Get Review +```bash +GET /google-play/androidpublisher/v3/applications/{packageName}/reviews/{reviewId} +``` + +#### Reply to Review +```bash +POST /google-play/androidpublisher/v3/applications/{packageName}/reviews/{reviewId}:reply +Content-Type: application/json + +{ + "replyText": "Thank you for your feedback!" +} +``` + +### Edits (App Updates) + +#### Create Edit +```bash +POST /google-play/androidpublisher/v3/applications/{packageName}/edits +``` + +#### Commit Edit +```bash +POST /google-play/androidpublisher/v3/applications/{packageName}/edits/{editId}:commit +``` + +#### Delete Edit +```bash +DELETE /google-play/androidpublisher/v3/applications/{packageName}/edits/{editId} +``` + +## Notes + +- Replace `{packageName}` with your app's package name (e.g., `com.example.app`) +- The Google Play Developer API requires the app to be published on Google Play +- Subscription management requires the app to have active subscriptions configured +- Edits are transactional - create an edit, make changes, then commit +- Prices are in micros (1,000,000 micros = 1 unit of currency) + +## Resources + +- [Android Publisher API Overview](https://developers.google.com/android-publisher) +- [In-App Products](https://developers.google.com/android-publisher/api-ref/rest/v3/inappproducts) +- [Subscriptions](https://developers.google.com/android-publisher/api-ref/rest/v3/monetization.subscriptions) +- [Purchases](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products) +- [Reviews](https://developers.google.com/android-publisher/api-ref/rest/v3/reviews) +- [Edits](https://developers.google.com/android-publisher/api-ref/rest/v3/edits) diff --git a/.agents/skills/api-gateway/references/google-search-console.md b/.agents/skills/api-gateway/references/google-search-console.md new file mode 100644 index 00000000..692ff9e0 --- /dev/null +++ b/.agents/skills/api-gateway/references/google-search-console.md @@ -0,0 +1,189 @@ +# Google Search Console Routing Reference + +**App name:** `google-search-console` +**Base URL proxied:** `www.googleapis.com` + +## API Path Pattern + +``` +/google-search-console/webmasters/v3/{endpoint} +``` + +## Common Endpoints + +### List Sites +```bash +GET /google-search-console/webmasters/v3/sites +``` + +### Get Site +```bash +GET /google-search-console/webmasters/v3/sites/{siteUrl} +``` + +Note: Site URL must be URL-encoded (e.g., `https%3A%2F%2Fexample.com%2F`) + +### Search Analytics Query +```bash +POST /google-search-console/webmasters/v3/sites/{siteUrl}/searchAnalytics/query +Content-Type: application/json + +{ + "startDate": "2024-01-01", + "endDate": "2024-01-31", + "dimensions": ["query"], + "rowLimit": 100 +} +``` + +### List Sitemaps +```bash +GET /google-search-console/webmasters/v3/sites/{siteUrl}/sitemaps +``` + +### Get Sitemap +```bash +GET /google-search-console/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath} +``` + +### Submit Sitemap +```bash +PUT /google-search-console/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath} +``` + +### Delete Sitemap +```bash +DELETE /google-search-console/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath} +``` + +## Search Analytics Query Examples + +### Top Queries +```json +{ + "startDate": "2024-01-01", + "endDate": "2024-01-31", + "dimensions": ["query"], + "rowLimit": 25, + "startRow": 0 +} +``` + +### Top Pages +```json +{ + "startDate": "2024-01-01", + "endDate": "2024-01-31", + "dimensions": ["page"], + "rowLimit": 25 +} +``` + +### Queries by Country +```json +{ + "startDate": "2024-01-01", + "endDate": "2024-01-31", + "dimensions": ["query", "country"], + "rowLimit": 100 +} +``` + +### Device Breakdown +```json +{ + "startDate": "2024-01-01", + "endDate": "2024-01-31", + "dimensions": ["device"], + "rowLimit": 10 +} +``` + +### Daily Performance +```json +{ + "startDate": "2024-01-01", + "endDate": "2024-01-31", + "dimensions": ["date"], + "rowLimit": 31 +} +``` + +### Filtered Query +```json +{ + "startDate": "2024-01-01", + "endDate": "2024-01-31", + "dimensions": ["query"], + "dimensionFilterGroups": [{ + "filters": [{ + "dimension": "query", + "operator": "contains", + "expression": "keyword" + }] + }], + "rowLimit": 100 +} +``` + +### Search Type Filter +```json +{ + "startDate": "2024-01-01", + "endDate": "2024-01-31", + "dimensions": ["query"], + "type": "image", + "rowLimit": 25 +} +``` + +## Dimensions + +- `query` - Search query +- `page` - Page URL +- `country` - Country code (ISO 3166-1 alpha-3) +- `device` - DESKTOP, MOBILE, TABLET +- `date` - Date in YYYY-MM-DD format +- `searchAppearance` - Rich result types + +## Metrics (returned automatically) + +- `clicks` - Number of clicks +- `impressions` - Number of impressions +- `ctr` - Click-through rate +- `position` - Average position + +## Filter Operators + +- `equals` +- `contains` +- `notContains` +- `includingRegex` +- `excludingRegex` + +## Search Types + +- `web` - Web search (default) +- `image` - Image search +- `video` - Video search +- `news` - News search + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Site URLs must be URL-encoded in the path (e.g., `sc-domain%3Aexample.com`) +- Date range is limited to 16 months of data +- Maximum 25,000 rows per request +- Use `startRow` for pagination +- Data has a 2-3 day delay + +## Resources + +- [API Reference](https://developers.google.com/webmaster-tools/v1/api_reference_index) +- [List Sites](https://developers.google.com/webmaster-tools/v1/sites/list) +- [Get Site](https://developers.google.com/webmaster-tools/v1/sites/get) +- [Search Analytics Query](https://developers.google.com/webmaster-tools/v1/searchanalytics/query) +- [List Sitemaps](https://developers.google.com/webmaster-tools/v1/sitemaps/list) +- [Get Sitemap](https://developers.google.com/webmaster-tools/v1/sitemaps/get) +- [Submit Sitemap](https://developers.google.com/webmaster-tools/v1/sitemaps/submit) +- [Delete Sitemap](https://developers.google.com/webmaster-tools/v1/sitemaps/delete) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/google-sheets.md b/.agents/skills/api-gateway/references/google-sheets.md new file mode 100644 index 00000000..b29890f5 --- /dev/null +++ b/.agents/skills/api-gateway/references/google-sheets.md @@ -0,0 +1,240 @@ +# Google Sheets Routing Reference + +**App name:** `google-sheets` +**Base URL proxied:** `sheets.googleapis.com` + +## API Path Pattern + +``` +/google-sheets/v4/spreadsheets/{spreadsheetId}/{endpoint} +``` + +## Common Endpoints + +### Get Spreadsheet Metadata +```bash +GET /google-sheets/v4/spreadsheets/{spreadsheetId} +``` + +### Get Values +```bash +GET /google-sheets/v4/spreadsheets/{spreadsheetId}/values/{range} +``` + +Example: +```bash +GET /google-sheets/v4/spreadsheets/SHEET_ID/values/Sheet1!A1:D10 +``` + +### Get Multiple Ranges +```bash +GET /google-sheets/v4/spreadsheets/{spreadsheetId}/values:batchGet?ranges=Sheet1!A1:B10&ranges=Sheet2!A1:C5 +``` + +### Update Values +```bash +PUT /google-sheets/v4/spreadsheets/{spreadsheetId}/values/{range}?valueInputOption=USER_ENTERED +Content-Type: application/json + +{ + "values": [ + ["A1", "B1", "C1"], + ["A2", "B2", "C2"] + ] +} +``` + +### Append Values +```bash +POST /google-sheets/v4/spreadsheets/{spreadsheetId}/values/{range}:append?valueInputOption=USER_ENTERED +Content-Type: application/json + +{ + "values": [ + ["New Row 1", "Data", "More Data"], + ["New Row 2", "Data", "More Data"] + ] +} +``` + +### Batch Update Values +```bash +POST /google-sheets/v4/spreadsheets/{spreadsheetId}/values:batchUpdate +Content-Type: application/json + +{ + "valueInputOption": "USER_ENTERED", + "data": [ + {"range": "Sheet1!A1:B2", "values": [["A1", "B1"], ["A2", "B2"]]}, + {"range": "Sheet1!D1:E2", "values": [["D1", "E1"], ["D2", "E2"]]} + ] +} +``` + +### Clear Values +```bash +POST /google-sheets/v4/spreadsheets/{spreadsheetId}/values/{range}:clear +``` + +### Create Spreadsheet +```bash +POST /google-sheets/v4/spreadsheets +Content-Type: application/json + +{ + "properties": {"title": "New Spreadsheet"}, + "sheets": [{"properties": {"title": "Sheet1"}}] +} +``` + +### Batch Update (formatting, add sheets, etc.) +```bash +POST /google-sheets/v4/spreadsheets/{spreadsheetId}:batchUpdate +Content-Type: application/json + +{ + "requests": [ + {"addSheet": {"properties": {"title": "New Sheet"}}} + ] +} +``` + +## Common batchUpdate Requests + +See [full list of request types](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets/request). + +### Update Cells with Formatting +```json +{ + "updateCells": { + "rows": [ + {"values": [{"userEnteredValue": {"stringValue": "Name"}}, {"userEnteredValue": {"numberValue": 100}}]} + ], + "fields": "userEnteredValue", + "start": {"sheetId": 0, "rowIndex": 0, "columnIndex": 0} + } +} +``` + +### Format Header Row (Bold + Background Color) +```json +{ + "repeatCell": { + "range": {"sheetId": 0, "startRowIndex": 0, "endRowIndex": 1, "startColumnIndex": 0, "endColumnIndex": 3}, + "cell": { + "userEnteredFormat": { + "backgroundColor": {"red": 0.2, "green": 0.6, "blue": 0.9}, + "textFormat": {"bold": true} + } + }, + "fields": "userEnteredFormat(backgroundColor,textFormat)" + } +} +``` + +### Auto-Resize Columns +```json +{ + "autoResizeDimensions": { + "dimensions": {"sheetId": 0, "dimension": "COLUMNS", "startIndex": 0, "endIndex": 3} + } +} +``` + +### Rename Sheet +```json +{ + "updateSheetProperties": { + "properties": {"sheetId": 0, "title": "NewName"}, + "fields": "title" + } +} +``` + +### Insert Rows/Columns +```json +{ + "insertDimension": { + "range": {"sheetId": 0, "dimension": "ROWS", "startIndex": 1, "endIndex": 3}, + "inheritFromBefore": true + } +} +``` + +### Sort Range +```json +{ + "sortRange": { + "range": {"sheetId": 0, "startRowIndex": 1, "endRowIndex": 10, "startColumnIndex": 0, "endColumnIndex": 3}, + "sortSpecs": [{"dimensionIndex": 1, "sortOrder": "DESCENDING"}] + } +} +``` + +### Add Conditional Formatting +```json +{ + "addConditionalFormatRule": { + "rule": { + "ranges": [{"sheetId": 0, "startRowIndex": 1, "endRowIndex": 10, "startColumnIndex": 1, "endColumnIndex": 2}], + "booleanRule": { + "condition": {"type": "NUMBER_GREATER_THAN_EQ", "values": [{"userEnteredValue": "90"}]}, + "format": {"backgroundColor": {"red": 0.7, "green": 1, "blue": 0.7}} + } + }, + "index": 0 + } +} +``` + +### Add Filter +```json +{ + "setBasicFilter": { + "filter": { + "range": {"sheetId": 0, "startRowIndex": 0, "endRowIndex": 100, "startColumnIndex": 0, "endColumnIndex": 5} + } + } +} +``` + +### Delete Sheet +```json +{ + "deleteSheet": {"sheetId": 123456789} +} +``` + +## Value Input Options + +- `RAW` - Values are stored as-is +- `USER_ENTERED` - Values are parsed as if typed into the UI (formulas executed, numbers parsed) + +## Range Notation + +- `Sheet1!A1:D10` - Specific range +- `Sheet1!A:D` - Entire columns A through D +- `Sheet1!1:10` - Entire rows 1 through 10 +- `Sheet1` - Entire sheet +- `A1:D10` - Range in first sheet + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Range in URL path must be URL-encoded (`!` → `%21`, `:` → `%3A`) +- Use `valueInputOption=USER_ENTERED` to parse formulas and numbers +- Delete spreadsheets via Google Drive API + +## Resources + +- [API Overview](https://developers.google.com/workspace/sheets/api/reference/rest) +- [Get Spreadsheet](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets/get) +- [Create Spreadsheet](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets/create) +- [Batch Update](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets/batchUpdate) +- [Batch Update Request Types](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets/request) +- [Get Values](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets.values/get) +- [Update Values](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets.values/update) +- [Append Values](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets.values/append) +- [Batch Get Values](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets.values/batchGet) +- [Batch Update Values](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate) +- [Clear Values](https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets.values/clear) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/google-slides.md b/.agents/skills/api-gateway/references/google-slides.md new file mode 100644 index 00000000..87d0f520 --- /dev/null +++ b/.agents/skills/api-gateway/references/google-slides.md @@ -0,0 +1,215 @@ +# Google Slides Routing Reference + +**App name:** `google-slides` +**Base URL proxied:** `slides.googleapis.com` + +## API Path Pattern + +``` +/google-slides/v1/presentations/{presentationId} +``` + +## Common Endpoints + +### Create Presentation +```bash +POST /google-slides/v1/presentations +Content-Type: application/json + +{ + "title": "My Presentation" +} +``` + +### Get Presentation +```bash +GET /google-slides/v1/presentations/{presentationId} +``` + +### Get Page (Slide) +```bash +GET /google-slides/v1/presentations/{presentationId}/pages/{pageId} +``` + +### Get Page Thumbnail +```bash +GET /google-slides/v1/presentations/{presentationId}/pages/{pageId}/thumbnail +``` + +### Batch Update (All Modifications) +```bash +POST /google-slides/v1/presentations/{presentationId}:batchUpdate +Content-Type: application/json + +{ + "requests": [...] +} +``` + +### Create Slide +```bash +POST /google-slides/v1/presentations/{presentationId}:batchUpdate +Content-Type: application/json + +{ + "requests": [ + { + "createSlide": { + "objectId": "slide_001", + "slideLayoutReference": { + "predefinedLayout": "TITLE_AND_BODY" + } + } + } + ] +} +``` + +Predefined layouts: `BLANK`, `TITLE`, `TITLE_AND_BODY`, `TITLE_AND_TWO_COLUMNS`, `TITLE_ONLY`, `SECTION_HEADER`, `ONE_COLUMN_TEXT`, `MAIN_POINT`, `BIG_NUMBER` + +### Insert Text +```bash +POST /google-slides/v1/presentations/{presentationId}:batchUpdate +Content-Type: application/json + +{ + "requests": [ + { + "insertText": { + "objectId": "{shapeId}", + "text": "Hello, World!", + "insertionIndex": 0 + } + } + ] +} +``` + +### Create Shape (Text Box) +```bash +POST /google-slides/v1/presentations/{presentationId}:batchUpdate +Content-Type: application/json + +{ + "requests": [ + { + "createShape": { + "objectId": "shape_001", + "shapeType": "TEXT_BOX", + "elementProperties": { + "pageObjectId": "{slideId}", + "size": { + "width": {"magnitude": 300, "unit": "PT"}, + "height": {"magnitude": 100, "unit": "PT"} + }, + "transform": { + "scaleX": 1, + "scaleY": 1, + "translateX": 100, + "translateY": 100, + "unit": "PT" + } + } + } + } + ] +} +``` + +### Create Image +```bash +POST /google-slides/v1/presentations/{presentationId}:batchUpdate +Content-Type: application/json + +{ + "requests": [ + { + "createImage": { + "objectId": "image_001", + "url": "https://example.com/image.png", + "elementProperties": { + "pageObjectId": "{slideId}", + "size": { + "width": {"magnitude": 200, "unit": "PT"}, + "height": {"magnitude": 200, "unit": "PT"} + } + } + } + } + ] +} +``` + +### Delete Object +```bash +POST /google-slides/v1/presentations/{presentationId}:batchUpdate +Content-Type: application/json + +{ + "requests": [ + { + "deleteObject": { + "objectId": "{objectId}" + } + } + ] +} +``` + +### Replace All Text (Template Substitution) +```bash +POST /google-slides/v1/presentations/{presentationId}:batchUpdate +Content-Type: application/json + +{ + "requests": [ + { + "replaceAllText": { + "containsText": { + "text": "{{placeholder}}", + "matchCase": true + }, + "replaceText": "Actual Value" + } + } + ] +} +``` + +### Update Text Style +```bash +POST /google-slides/v1/presentations/{presentationId}:batchUpdate +Content-Type: application/json + +{ + "requests": [ + { + "updateTextStyle": { + "objectId": "{shapeId}", + "textRange": {"type": "ALL"}, + "style": { + "bold": true, + "fontSize": {"magnitude": 24, "unit": "PT"} + }, + "fields": "bold,fontSize" + } + } + ] +} +``` + +## Notes + +- Object IDs must be unique within a presentation +- Use batchUpdate for all modifications (adding slides, text, shapes, etc.) +- Multiple requests in a batchUpdate are applied atomically +- Sizes and positions use PT (points) as the unit (72 points = 1 inch) +- Use `replaceAllText` for template-based presentation generation + +## Resources + +- [Slides API Overview](https://developers.google.com/slides/api/reference/rest) +- [Presentations](https://developers.google.com/slides/api/reference/rest/v1/presentations) +- [Pages](https://developers.google.com/slides/api/reference/rest/v1/presentations.pages) +- [BatchUpdate Requests](https://developers.google.com/slides/api/reference/rest/v1/presentations/batchUpdate) +- [Page Layouts](https://developers.google.com/slides/api/reference/rest/v1/presentations/create#predefinedlayout) diff --git a/.agents/skills/api-gateway/references/google-tasks.md b/.agents/skills/api-gateway/references/google-tasks.md new file mode 100644 index 00000000..a9a29a6f --- /dev/null +++ b/.agents/skills/api-gateway/references/google-tasks.md @@ -0,0 +1,156 @@ +# Google Tasks Routing Reference + +**App name:** `google-tasks` +**Base URL proxied:** `tasks.googleapis.com` + +## API Path Pattern + +``` +/google-tasks/tasks/v1/{endpoint} +``` + +## Common Endpoints + +### Task Lists + +#### List Task Lists +```bash +GET /google-tasks/tasks/v1/users/@me/lists +``` + +With pagination: +```bash +GET /google-tasks/tasks/v1/users/@me/lists?maxResults=20 +``` + +#### Get Task List +```bash +GET /google-tasks/tasks/v1/users/@me/lists/{tasklistId} +``` + +#### Create Task List +```bash +POST /google-tasks/tasks/v1/users/@me/lists +Content-Type: application/json + +{ + "title": "New Task List" +} +``` + +#### Update Task List +```bash +PATCH /google-tasks/tasks/v1/users/@me/lists/{tasklistId} +Content-Type: application/json + +{ + "title": "Updated Title" +} +``` + +#### Delete Task List +```bash +DELETE /google-tasks/tasks/v1/users/@me/lists/{tasklistId} +``` + +### Tasks + +#### List Tasks +```bash +GET /google-tasks/tasks/v1/lists/{tasklistId}/tasks +``` + +With filters: +```bash +GET /google-tasks/tasks/v1/lists/{tasklistId}/tasks?showCompleted=true&showHidden=true&maxResults=50 +``` + +With date filters: +```bash +GET /google-tasks/tasks/v1/lists/{tasklistId}/tasks?dueMin=2026-01-01T00:00:00Z&dueMax=2026-12-31T23:59:59Z +``` + +#### Get Task +```bash +GET /google-tasks/tasks/v1/lists/{tasklistId}/tasks/{taskId} +``` + +#### Create Task +```bash +POST /google-tasks/tasks/v1/lists/{tasklistId}/tasks +Content-Type: application/json + +{ + "title": "New Task", + "notes": "Task description", + "due": "2026-03-01T00:00:00.000Z" +} +``` + +Create subtask: +```bash +POST /google-tasks/tasks/v1/lists/{tasklistId}/tasks?parent={parentTaskId} +Content-Type: application/json + +{ + "title": "Subtask" +} +``` + +#### Update Task (partial) +```bash +PATCH /google-tasks/tasks/v1/lists/{tasklistId}/tasks/{taskId} +Content-Type: application/json + +{ + "title": "Updated Title", + "status": "completed" +} +``` + +#### Update Task (full replace) +```bash +PUT /google-tasks/tasks/v1/lists/{tasklistId}/tasks/{taskId} +Content-Type: application/json + +{ + "title": "Replaced Task", + "notes": "New notes", + "status": "needsAction" +} +``` + +#### Delete Task +```bash +DELETE /google-tasks/tasks/v1/lists/{tasklistId}/tasks/{taskId} +``` + +#### Move Task +```bash +POST /google-tasks/tasks/v1/lists/{tasklistId}/tasks/{taskId}/move?previous={previousTaskId} +``` + +Make subtask: +```bash +POST /google-tasks/tasks/v1/lists/{tasklistId}/tasks/{taskId}/move?parent={parentTaskId} +``` + +#### Clear Completed Tasks +```bash +POST /google-tasks/tasks/v1/lists/{tasklistId}/clear +``` + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Task list and task IDs are opaque base64-encoded strings +- Status values: "needsAction" or "completed" +- Dates must be in RFC 3339 format (e.g., `2026-01-15T00:00:00.000Z`) +- Maximum title length: 1024 characters +- Maximum notes length: 8192 characters + +## Resources + +- [Google Tasks API Overview](https://developers.google.com/workspace/tasks) +- [Tasks Reference](https://developers.google.com/workspace/tasks/reference/rest/v1/tasks) +- [TaskLists Reference](https://developers.google.com/workspace/tasks/reference/rest/v1/tasklists) diff --git a/.agents/skills/api-gateway/references/google-workspace-admin.md b/.agents/skills/api-gateway/references/google-workspace-admin.md new file mode 100644 index 00000000..e84e1a59 --- /dev/null +++ b/.agents/skills/api-gateway/references/google-workspace-admin.md @@ -0,0 +1,236 @@ +# Google Workspace Admin Routing Reference + +**App name:** `google-workspace-admin` +**Base URL proxied:** `admin.googleapis.com` + +## API Path Pattern + +``` +/google-workspace-admin/admin/directory/v1/{endpoint} +``` + +## Common Endpoints + +### Users + +#### List Users +```bash +GET /google-workspace-admin/admin/directory/v1/users?customer=my_customer&maxResults=100 +``` + +With search query: +```bash +GET /google-workspace-admin/admin/directory/v1/users?customer=my_customer&query=email:john* +``` + +#### Get User +```bash +GET /google-workspace-admin/admin/directory/v1/users/{userKey} +``` + +`userKey` can be the user's primary email or unique user ID. + +#### Create User +```bash +POST /google-workspace-admin/admin/directory/v1/users +Content-Type: application/json + +{ + "primaryEmail": "newuser@example.com", + "name": { + "givenName": "Jane", + "familyName": "Smith" + }, + "password": "temporaryPassword123!", + "changePasswordAtNextLogin": true, + "orgUnitPath": "/Engineering" +} +``` + +#### Update User +```bash +PUT /google-workspace-admin/admin/directory/v1/users/{userKey} +Content-Type: application/json + +{ + "name": { + "givenName": "Jane", + "familyName": "Smith-Johnson" + }, + "suspended": false +} +``` + +#### Patch User (partial update) +```bash +PATCH /google-workspace-admin/admin/directory/v1/users/{userKey} +Content-Type: application/json + +{ + "suspended": true +} +``` + +#### Delete User +```bash +DELETE /google-workspace-admin/admin/directory/v1/users/{userKey} +``` + +#### Make User Admin +```bash +POST /google-workspace-admin/admin/directory/v1/users/{userKey}/makeAdmin +Content-Type: application/json + +{ + "status": true +} +``` + +### Groups + +#### List Groups +```bash +GET /google-workspace-admin/admin/directory/v1/groups?customer=my_customer +``` + +#### Get Group +```bash +GET /google-workspace-admin/admin/directory/v1/groups/{groupKey} +``` + +#### Create Group +```bash +POST /google-workspace-admin/admin/directory/v1/groups +Content-Type: application/json + +{ + "email": "engineering@example.com", + "name": "Engineering Team", + "description": "All engineering staff" +} +``` + +#### Update Group +```bash +PUT /google-workspace-admin/admin/directory/v1/groups/{groupKey} +Content-Type: application/json + +{ + "name": "Engineering Department", + "description": "Updated description" +} +``` + +#### Delete Group +```bash +DELETE /google-workspace-admin/admin/directory/v1/groups/{groupKey} +``` + +### Group Members + +#### List Members +```bash +GET /google-workspace-admin/admin/directory/v1/groups/{groupKey}/members +``` + +#### Add Member +```bash +POST /google-workspace-admin/admin/directory/v1/groups/{groupKey}/members +Content-Type: application/json + +{ + "email": "user@example.com", + "role": "MEMBER" +} +``` + +Roles: `OWNER`, `MANAGER`, `MEMBER` + +#### Update Member Role +```bash +PATCH /google-workspace-admin/admin/directory/v1/groups/{groupKey}/members/{memberKey} +Content-Type: application/json + +{ + "role": "MANAGER" +} +``` + +#### Remove Member +```bash +DELETE /google-workspace-admin/admin/directory/v1/groups/{groupKey}/members/{memberKey} +``` + +### Organizational Units + +#### List Org Units +```bash +GET /google-workspace-admin/admin/directory/v1/customer/my_customer/orgunits +``` + +#### Get Org Unit +```bash +GET /google-workspace-admin/admin/directory/v1/customer/my_customer/orgunits/{orgUnitPath} +``` + +#### Create Org Unit +```bash +POST /google-workspace-admin/admin/directory/v1/customer/my_customer/orgunits +Content-Type: application/json + +{ + "name": "Engineering", + "parentOrgUnitPath": "/", + "description": "Engineering department" +} +``` + +#### Delete Org Unit +```bash +DELETE /google-workspace-admin/admin/directory/v1/customer/my_customer/orgunits/{orgUnitPath} +``` + +### Domains + +#### List Domains +```bash +GET /google-workspace-admin/admin/directory/v1/customer/my_customer/domains +``` + +#### Get Domain +```bash +GET /google-workspace-admin/admin/directory/v1/customer/my_customer/domains/{domainName} +``` + +### Roles + +#### List Roles +```bash +GET /google-workspace-admin/admin/directory/v1/customer/my_customer/roles +``` + +#### List Role Assignments +```bash +GET /google-workspace-admin/admin/directory/v1/customer/my_customer/roleassignments +``` + +#### Create Role Assignment +```bash +POST /google-workspace-admin/admin/directory/v1/customer/my_customer/roleassignments +Content-Type: application/json + +{ + "roleId": "123456789", + "assignedTo": "user_id", + "scopeType": "CUSTOMER" +} +``` + +## Notes + +- Use `my_customer` as the customer ID for your own domain +- User keys can be primary email or unique user ID +- Group keys can be group email or unique group ID +- Org unit paths start with `/` (e.g., `/Engineering/Frontend`) +- Admin privileges are required for most operations +- Password must meet Google's complexity requirements diff --git a/.agents/skills/api-gateway/references/gumroad.md b/.agents/skills/api-gateway/references/gumroad.md new file mode 100644 index 00000000..796dfae5 --- /dev/null +++ b/.agents/skills/api-gateway/references/gumroad.md @@ -0,0 +1,147 @@ +# Gumroad Routing Reference + +**App name:** `gumroad` +**Base URL proxied:** `api.gumroad.com` + +## API Path Pattern + +``` +/gumroad/v2/{resource} +``` + +## Common Endpoints + +### Get Current User +```bash +GET /gumroad/v2/user +``` + +### List Products +```bash +GET /gumroad/v2/products +``` + +### Get Product +```bash +GET /gumroad/v2/products/{product_id} +``` + +### Update Product +```bash +PUT /gumroad/v2/products/{product_id} +Content-Type: application/x-www-form-urlencoded + +name=Updated%20Name +``` + +### Delete Product +```bash +DELETE /gumroad/v2/products/{product_id} +``` + +### List Sales +```bash +GET /gumroad/v2/sales +GET /gumroad/v2/sales?after=2026-01-01&before=2026-12-31 +``` + +### Get Sale +```bash +GET /gumroad/v2/sales/{sale_id} +``` + +### List Subscribers +```bash +GET /gumroad/v2/products/{product_id}/subscribers +``` + +### Get Subscriber +```bash +GET /gumroad/v2/subscribers/{subscriber_id} +``` + +### Verify License +```bash +POST /gumroad/v2/licenses/verify +Content-Type: application/x-www-form-urlencoded + +product_id={product_id}&license_key={license_key} +``` + +### Enable/Disable License +```bash +PUT /gumroad/v2/licenses/enable +PUT /gumroad/v2/licenses/disable +``` + +### List Resource Subscriptions (Webhooks) +```bash +GET /gumroad/v2/resource_subscriptions?resource_name=sale +``` + +Resource names: `sale`, `refund`, `dispute`, `dispute_won`, `cancellation`, `subscription_updated`, `subscription_ended`, `subscription_restarted` + +### Create Resource Subscription +```bash +PUT /gumroad/v2/resource_subscriptions +Content-Type: application/x-www-form-urlencoded + +resource_name=sale&post_url=https://example.com/webhook +``` + +### Delete Resource Subscription +```bash +DELETE /gumroad/v2/resource_subscriptions/{resource_subscription_id} +``` + +### Offer Codes +```bash +GET /gumroad/v2/products/{product_id}/offer_codes +POST /gumroad/v2/products/{product_id}/offer_codes +PUT /gumroad/v2/products/{product_id}/offer_codes/{offer_code_id} +DELETE /gumroad/v2/products/{product_id}/offer_codes/{offer_code_id} +``` + +### Variant Categories +```bash +GET /gumroad/v2/products/{product_id}/variant_categories +POST /gumroad/v2/products/{product_id}/variant_categories +DELETE /gumroad/v2/products/{product_id}/variant_categories/{variant_category_id} +``` + +### Variants +```bash +GET /gumroad/v2/products/{product_id}/variant_categories/{variant_category_id}/variants +POST /gumroad/v2/products/{product_id}/variant_categories/{variant_category_id}/variants +PUT /gumroad/v2/products/{product_id}/variant_categories/{variant_category_id}/variants/{variant_id} +DELETE /gumroad/v2/products/{product_id}/variant_categories/{variant_category_id}/variants/{variant_id} +``` + +### Custom Fields +```bash +GET /gumroad/v2/products/{product_id}/custom_fields +POST /gumroad/v2/products/{product_id}/custom_fields +PUT /gumroad/v2/products/{product_id}/custom_fields/{name} +DELETE /gumroad/v2/products/{product_id}/custom_fields/{name} +``` + +## Pagination + +Page-based pagination: +```bash +GET /gumroad/v2/sales?page=1 +GET /gumroad/v2/sales?page=2 +``` + +## Notes + +- All responses include `success` boolean field +- Product creation not available via API +- POST/PUT use `application/x-www-form-urlencoded` (not JSON) +- Prices in cents (500 = $5.00) +- License keys are case-insensitive + +## Resources + +- [Gumroad API Documentation](https://gumroad.com/api) +- [Create API Application](https://help.gumroad.com/article/280-create-application-api) diff --git a/.agents/skills/api-gateway/references/hubspot.md b/.agents/skills/api-gateway/references/hubspot.md new file mode 100644 index 00000000..d6996450 --- /dev/null +++ b/.agents/skills/api-gateway/references/hubspot.md @@ -0,0 +1,357 @@ +# HubSpot Routing Reference + +**App name:** `hubspot` +**Base URL proxied:** `api.hubapi.com` + +## API Path Pattern + +``` +/hubspot/crm/v3/objects/{objectType}/{endpoint} +``` + +## Common Endpoints + +### Contacts + +#### List Contacts +```bash +GET /hubspot/crm/v3/objects/contacts?limit=100 +``` + +With properties: +```bash +GET /hubspot/crm/v3/objects/contacts?limit=100&properties=email,firstname,lastname,phone +``` + +With pagination: +```bash +GET /hubspot/crm/v3/objects/contacts?limit=100&properties=email,firstname&after={cursor} +``` + +#### Get Contact +```bash +GET /hubspot/crm/v3/objects/contacts/{contactId}?properties=email,firstname,lastname +``` + +#### Create Contact +```bash +POST /hubspot/crm/v3/objects/contacts +Content-Type: application/json + +{ + "properties": { + "email": "john@example.com", + "firstname": "John", + "lastname": "Doe", + "phone": "+1234567890" + } +} +``` + +#### Update Contact +```bash +PATCH /hubspot/crm/v3/objects/contacts/{contactId} +Content-Type: application/json + +{ + "properties": { + "phone": "+0987654321" + } +} +``` + +#### Delete Contact +```bash +DELETE /hubspot/crm/v3/objects/contacts/{contactId} +``` + +#### Search Contacts +```bash +POST /hubspot/crm/v3/objects/contacts/search +Content-Type: application/json + +{ + "filterGroups": [{ + "filters": [{ + "propertyName": "email", + "operator": "EQ", + "value": "john@example.com" + }] + }], + "properties": ["email", "firstname", "lastname"] +} +``` + +### Companies + +#### List Companies +```bash +GET /hubspot/crm/v3/objects/companies?limit=100&properties=name,domain,industry +``` + +#### Get Company +```bash +GET /hubspot/crm/v3/objects/companies/{companyId}?properties=name,domain,industry +``` + +#### Create Company +```bash +POST /hubspot/crm/v3/objects/companies +Content-Type: application/json + +{ + "properties": { + "name": "Acme Corp", + "domain": "acme.com", + "industry": "COMPUTER_SOFTWARE" + } +} +``` + +**Note:** The `industry` property requires specific enum values (e.g., `COMPUTER_SOFTWARE`, `FINANCE`, `HEALTHCARE`), not free text like "Technology". Use the List Properties endpoint to get valid values. + +#### Update Company +```bash +PATCH /hubspot/crm/v3/objects/companies/{companyId} +Content-Type: application/json + +{ + "properties": { + "industry": "COMPUTER_SOFTWARE", + "numberofemployees": "50" + } +} +``` + +#### Delete Company +```bash +DELETE /hubspot/crm/v3/objects/companies/{companyId} +``` + +#### Search Companies +```bash +POST /hubspot/crm/v3/objects/companies/search +Content-Type: application/json + +{ + "filterGroups": [{ + "filters": [{ + "propertyName": "domain", + "operator": "CONTAINS_TOKEN", + "value": "*" + }] + }], + "properties": ["name", "domain"], + "limit": 10 +} +``` + +### Deals + +#### List Deals +```bash +GET /hubspot/crm/v3/objects/deals?limit=100&properties=dealname,amount,dealstage +``` + +#### Get Deal +```bash +GET /hubspot/crm/v3/objects/deals/{dealId}?properties=dealname,amount,dealstage +``` + +#### Create Deal +```bash +POST /hubspot/crm/v3/objects/deals +Content-Type: application/json + +{ + "properties": { + "dealname": "New Deal", + "amount": "10000", + "dealstage": "appointmentscheduled" + } +} +``` + +#### Update Deal +```bash +PATCH /hubspot/crm/v3/objects/deals/{dealId} +Content-Type: application/json + +{ + "properties": { + "amount": "15000", + "dealstage": "qualifiedtobuy" + } +} +``` + +#### Delete Deal +```bash +DELETE /hubspot/crm/v3/objects/deals/{dealId} +``` + +#### Search Deals +```bash +POST /hubspot/crm/v3/objects/deals/search +Content-Type: application/json + +{ + "filterGroups": [{ + "filters": [{ + "propertyName": "amount", + "operator": "GTE", + "value": "1000" + }] + }], + "properties": ["dealname", "amount", "dealstage"], + "limit": 10 +} +``` + +### Associations (v4 API) + +#### Associate Objects +```bash +PUT /hubspot/crm/v4/objects/{fromObjectType}/{fromObjectId}/associations/{toObjectType}/{toObjectId} +Content-Type: application/json + +[{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 279}] +``` + +Common association type IDs: +- `279` - Contact to Company +- `3` - Deal to Contact +- `341` - Deal to Company + +#### List Associations +```bash +GET /hubspot/crm/v4/objects/{objectType}/{objectId}/associations/{toObjectType} +``` + +### Batch Operations + +#### Batch Read +```bash +POST /hubspot/crm/v3/objects/{objectType}/batch/read +Content-Type: application/json + +{ + "properties": ["email", "firstname"], + "inputs": [{"id": "123"}, {"id": "456"}] +} +``` + +#### Batch Create +```bash +POST /hubspot/crm/v3/objects/{objectType}/batch/create +Content-Type: application/json + +{ + "inputs": [ + {"properties": {"email": "one@example.com", "firstname": "One"}}, + {"properties": {"email": "two@example.com", "firstname": "Two"}} + ] +} +``` + +#### Batch Update +```bash +POST /hubspot/crm/v3/objects/{objectType}/batch/update +Content-Type: application/json + +{ + "inputs": [ + {"id": "123", "properties": {"firstname": "Updated"}}, + {"id": "456", "properties": {"firstname": "Also Updated"}} + ] +} +``` + +#### Batch Archive +```bash +POST /hubspot/crm/v3/objects/{objectType}/batch/archive +Content-Type: application/json + +{ + "inputs": [{"id": "123"}, {"id": "456"}] +} +``` + +### Properties + +#### List Properties +```bash +GET /hubspot/crm/v3/properties/{objectType} +``` + +## Search Operators + +- `EQ` - Equal to +- `NEQ` - Not equal to +- `LT` - Less than +- `LTE` - Less than or equal to +- `GT` - Greater than +- `GTE` - Greater than or equal to +- `CONTAINS_TOKEN` - Contains token +- `NOT_CONTAINS_TOKEN` - Does not contain token + +## Pagination + +List endpoints return a `paging.next.after` cursor for pagination: +```json +{ + "results": [...], + "paging": { + "next": { + "after": "12345", + "link": "https://api.hubapi.com/..." + } + } +} +``` + +Use the `after` query parameter to fetch the next page: +```bash +GET /hubspot/crm/v3/objects/contacts?limit=100&after=12345 +``` + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- The `industry` property on companies requires specific enum values +- Batch operations support up to 100 records per request +- Archive/Delete is a soft delete - records can be restored within 90 days +- Delete endpoints return HTTP 204 (No Content) on success + +## Resources + +- [API Overview](https://developers.hubspot.com/docs/api/overview) +- [List Contacts](https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/basic/get-crm-v3-objects-contacts.md) +- [Get Contact](https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/basic/get-crm-v3-objects-contacts-contactId.md) +- [Create Contact](https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/basic/post-crm-v3-objects-contacts.md) +- [Update Contact](https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/basic/patch-crm-v3-objects-contacts-contactId.md) +- [Archive Contact](https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/basic/delete-crm-v3-objects-contacts-contactId.md) +- [Merge Contacts](https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/basic/post-crm-v3-objects-contacts-merge.md) +- [GDPR Delete Contact](https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/basic/post-crm-v3-objects-contacts-gdpr-delete.md) +- [Search Contacts](https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/search/post-crm-v3-objects-contacts-search.md) +- [List Companies](https://developers.hubspot.com/docs/api-reference/crm-companies-v3/basic/get-crm-v3-objects-companies.md) +- [Get Company](https://developers.hubspot.com/docs/api-reference/crm-companies-v3/basic/get-crm-v3-objects-companies-companyId.md) +- [Create Company](https://developers.hubspot.com/docs/api-reference/crm-companies-v3/basic/post-crm-v3-objects-companies.md) +- [Update Company](https://developers.hubspot.com/docs/api-reference/crm-companies-v3/basic/patch-crm-v3-objects-companies-companyId.md) +- [Archive Company](https://developers.hubspot.com/docs/api-reference/crm-companies-v3/basic/delete-crm-v3-objects-companies-companyId.md) +- [Merge Companies](https://developers.hubspot.com/docs/api-reference/crm-companies-v3/basic/post-crm-v3-objects-companies-merge.md) +- [Search Companies](https://developers.hubspot.com/docs/api-reference/crm-companies-v3/search/post-crm-v3-objects-companies-search.md) +- [List Deals](https://developers.hubspot.com/docs/api-reference/crm-deals-v3/basic/get-crm-v3-objects-0-3.md) +- [Get Deal](https://developers.hubspot.com/docs/api-reference/crm-deals-v3/basic/get-crm-v3-objects-0-3-dealId.md) +- [Create Deal](https://developers.hubspot.com/docs/api-reference/crm-deals-v3/basic/post-crm-v3-objects-0-3.md) +- [Update Deal](https://developers.hubspot.com/docs/api-reference/crm-deals-v3/basic/patch-crm-v3-objects-0-3-dealId.md) +- [Archive Deal](https://developers.hubspot.com/docs/api-reference/crm-deals-v3/basic/delete-crm-v3-objects-0-3-dealId.md) +- [Merge Deals](https://developers.hubspot.com/docs/api-reference/crm-deals-v3/basic/post-crm-v3-objects-0-3-merge.md) +- [Search Deals](https://developers.hubspot.com/docs/api-reference/crm-deals-v3/search/post-crm-v3-objects-0-3-search.md) +- [List Associations](https://developers.hubspot.com/docs/api-reference/crm-associations-v4/basic/get-crm-v4-objects-objectType-objectId-associations-toObjectType.md) +- [Create Association](https://developers.hubspot.com/docs/api-reference/crm-associations-v4/basic/put-crm-v4-objects-objectType-objectId-associations-toObjectType-toObjectId.md) +- [Delete Association](https://developers.hubspot.com/docs/api-reference/crm-associations-v4/basic/delete-crm-v4-objects-objectType-objectId-associations-toObjectType-toObjectId.md) +- [List Properties](https://developers.hubspot.com/docs/api-reference/crm-properties-v3/core/get-crm-v3-properties-objectType.md) +- [Get Property](https://developers.hubspot.com/docs/api-reference/crm-properties-v3/core/get-crm-v3-properties-objectType-propertyName.md) +- [Create Property](https://developers.hubspot.com/docs/api-reference/crm-properties-v3/core/post-crm-v3-properties-objectType.md) +- [Search Reference](https://developers.hubspot.com/docs/api/crm/search) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/jira.md b/.agents/skills/api-gateway/references/jira.md new file mode 100644 index 00000000..ebbb2f0f --- /dev/null +++ b/.agents/skills/api-gateway/references/jira.md @@ -0,0 +1,181 @@ +# Jira Routing Reference + +**App name:** `jira` +**Base URL proxied:** `api.atlassian.com` + +## Getting Cloud ID + +Jira Cloud requires a cloud ID in the API path. First, get accessible resources: + +```bash +GET /jira/oauth/token/accessible-resources +``` + +Response: +```json +[{ + "id": "62909843-b784-4c35-b770-e4e2a26f024b", + "url": "https://yoursite.atlassian.net", + "name": "yoursite", + "scopes": ["read:jira-user", "read:jira-work", "write:jira-work"] +}] +``` + +## API Path Pattern + +``` +/jira/ex/jira/{cloudId}/rest/api/3/{endpoint} +``` + +## Common Endpoints + +### List Projects +```bash +GET /jira/ex/jira/{cloudId}/rest/api/3/project +``` + +### Get Project +```bash +GET /jira/ex/jira/{cloudId}/rest/api/3/project/{projectKeyOrId} +``` + +### Search Issues (JQL) +Note: The old `/search` endpoint is deprecated. Use `/search/jql` with a bounded query. + +```bash +GET /jira/ex/jira/{cloudId}/rest/api/3/search/jql?jql=project%3DKEY%20order%20by%20created%20DESC&maxResults=20&fields=summary,status,assignee,created,priority +``` + +### Get Issue +```bash +GET /jira/ex/jira/{cloudId}/rest/api/3/issue/{issueIdOrKey} +``` + +### Create Issue +```bash +POST /jira/ex/jira/{cloudId}/rest/api/3/issue +Content-Type: application/json + +{ + "fields": { + "project": {"key": "PROJ"}, + "summary": "Issue summary", + "issuetype": {"name": "Task"} + } +} +``` + +### Update Issue +```bash +PUT /jira/ex/jira/{cloudId}/rest/api/3/issue/{issueIdOrKey} +Content-Type: application/json + +{ + "fields": { + "summary": "Updated summary" + } +} +``` + +### Delete Issue +```bash +DELETE /jira/ex/jira/{cloudId}/rest/api/3/issue/{issueIdOrKey} +``` + +### Assign Issue +```bash +PUT /jira/ex/jira/{cloudId}/rest/api/3/issue/{issueIdOrKey}/assignee +Content-Type: application/json + +{ + "accountId": "712020:5aff718e-6fe0-4548-82f4-f44ec481e5e7" +} +``` + +### Get Transitions +```bash +GET /jira/ex/jira/{cloudId}/rest/api/3/issue/{issueIdOrKey}/transitions +``` + +### Transition Issue (change status) +```bash +POST /jira/ex/jira/{cloudId}/rest/api/3/issue/{issueIdOrKey}/transitions +Content-Type: application/json + +{ + "transition": {"id": "31"} +} +``` + +### Add Comment +```bash +POST /jira/ex/jira/{cloudId}/rest/api/3/issue/{issueIdOrKey}/comment +Content-Type: application/json + +{ + "body": { + "type": "doc", + "version": 1, + "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Comment text"}]}] + } +} +``` + +### Get Comments +```bash +GET /jira/ex/jira/{cloudId}/rest/api/3/issue/{issueIdOrKey}/comment +``` + +### Users + +#### Get Current User +```bash +GET /jira/ex/jira/{cloudId}/rest/api/3/myself +``` + +#### Search Users +```bash +GET /jira/ex/jira/{cloudId}/rest/api/3/user/search?query=john +``` + +### Metadata + +#### List Issue Types +```bash +GET /jira/ex/jira/{cloudId}/rest/api/3/issuetype +``` + +#### List Priorities +```bash +GET /jira/ex/jira/{cloudId}/rest/api/3/priority +``` + +#### List Statuses +```bash +GET /jira/ex/jira/{cloudId}/rest/api/3/status +``` + +#### List Fields +```bash +GET /jira/ex/jira/{cloudId}/rest/api/3/field +``` + +## Notes + +- Always fetch cloud ID first using `/oauth/token/accessible-resources` +- JQL queries must be bounded (e.g., `project=KEY`) - unbounded queries are rejected +- Use URL encoding for JQL query parameters +- Update, Delete, Transition, and Assign endpoints return HTTP 204 (No Content) on success +- Agile API (`/rest/agile/1.0/...`) requires additional OAuth scopes beyond the basic Jira scopes + +## Resources + +- [API Introduction](https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/) +- [Search Issues (JQL)](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-jql-get) +- [Get Issue](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-get) +- [Create Issue](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-post) +- [Update Issue](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-put) +- [Transition Issue](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-transitions-post) +- [Add Comment](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-post) +- [Get Projects](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-get) +- [JQL Reference](https://support.atlassian.com/jira-service-management-cloud/docs/use-advanced-search-with-jira-query-language-jql/) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/jobber.md b/.agents/skills/api-gateway/references/jobber.md new file mode 100644 index 00000000..d931dd8f --- /dev/null +++ b/.agents/skills/api-gateway/references/jobber.md @@ -0,0 +1,168 @@ +# Jobber Routing Reference + +**App name:** `jobber` +**Base URL proxied:** `api.getjobber.com/api/` + +## API Type + +Jobber uses a GraphQL API exclusively. All requests are POST requests to the `/graphql` endpoint. + +## API Path Pattern + +``` +/jobber/graphql +``` + +All operations use POST with a JSON body containing the `query` field. + +## Version Header + +The gateway automatically injects the `X-JOBBER-GRAPHQL-VERSION` header (currently `2025-04-16`). + +## Common Operations + +### Get Account +```bash +POST /jobber/graphql +Content-Type: application/json + +{ + "query": "{ account { id name } }" +} +``` + +### List Clients +```bash +POST /jobber/graphql +Content-Type: application/json + +{ + "query": "{ clients(first: 20) { nodes { id name emails { address } phones { number } } pageInfo { hasNextPage endCursor } } }" +} +``` + +### Get Client +```bash +POST /jobber/graphql +Content-Type: application/json + +{ + "query": "query($id: EncodedId!) { client(id: $id) { id name emails { address } } }", + "variables": { "id": "CLIENT_ID" } +} +``` + +### Create Client +```bash +POST /jobber/graphql +Content-Type: application/json + +{ + "query": "mutation($input: ClientCreateInput!) { clientCreate(input: $input) { client { id name } userErrors { message path } } }", + "variables": { + "input": { + "firstName": "John", + "lastName": "Doe", + "emails": [{"address": "john@example.com"}] + } + } +} +``` + +### List Jobs +```bash +POST /jobber/graphql +Content-Type: application/json + +{ + "query": "{ jobs(first: 20) { nodes { id title jobNumber jobStatus client { name } } pageInfo { hasNextPage endCursor } } }" +} +``` + +### Create Job +```bash +POST /jobber/graphql +Content-Type: application/json + +{ + "query": "mutation($input: JobCreateInput!) { jobCreate(input: $input) { job { id jobNumber } userErrors { message path } } }", + "variables": { + "input": { + "clientId": "CLIENT_ID", + "title": "Service Job" + } + } +} +``` + +### List Invoices +```bash +POST /jobber/graphql +Content-Type: application/json + +{ + "query": "{ invoices(first: 20) { nodes { id invoiceNumber total invoiceStatus } pageInfo { hasNextPage endCursor } } }" +} +``` + +### List Quotes +```bash +POST /jobber/graphql +Content-Type: application/json + +{ + "query": "{ quotes(first: 20) { nodes { id quoteNumber title quoteStatus } pageInfo { hasNextPage endCursor } } }" +} +``` + +### List Properties +```bash +POST /jobber/graphql +Content-Type: application/json + +{ + "query": "{ properties(first: 20) { nodes { id address { street city } client { name } } } }" +} +``` + +### List Users +```bash +POST /jobber/graphql +Content-Type: application/json + +{ + "query": "{ users(first: 50) { nodes { id name { full } email { raw } } } }" +} +``` + +## Pagination + +Jobber uses Relay-style cursor-based pagination: + +```bash +# First page +{ + "query": "{ clients(first: 20) { nodes { id name } pageInfo { hasNextPage endCursor } } }" +} + +# Next page +{ + "query": "{ clients(first: 20, after: \"CURSOR\") { nodes { id name } pageInfo { hasNextPage endCursor } } }" +} +``` + +## Notes + +- Jobber uses GraphQL exclusively (no REST API) +- Gateway injects version header automatically (`2025-04-16`) +- IDs use `EncodedId` type (base64 encoded) - pass as strings +- Field naming: `emails`/`phones` (arrays), `jobStatus`/`invoiceStatus`/`quoteStatus` +- Rate limits: 2,500 requests per 5 minutes, plus query cost limits (max 10,000 points) +- Old API versions supported for 12-18 months +- Available resources: Clients, Jobs, Invoices, Quotes, Requests, Properties, Users, Custom Fields + +## Resources + +- [Jobber Developer Documentation](https://developer.getjobber.com/docs/) +- [API Changelog](https://developer.getjobber.com/docs/changelog) +- [API Support](mailto:api-support@getjobber.com) diff --git a/.agents/skills/api-gateway/references/jotform.md b/.agents/skills/api-gateway/references/jotform.md new file mode 100644 index 00000000..e84cd3a9 --- /dev/null +++ b/.agents/skills/api-gateway/references/jotform.md @@ -0,0 +1,190 @@ +# JotForm Routing Reference + +**App name:** `jotform` +**Base URL proxied:** `api.jotform.com` + +## API Path Pattern + +``` +/jotform/{endpoint} +``` + +## Common Endpoints + +### User + +#### Get User Info +```bash +GET /jotform/user +``` + +#### Get User Forms +```bash +GET /jotform/user/forms?limit=20&offset=0 +``` + +#### Get User Submissions +```bash +GET /jotform/user/submissions?limit=20&offset=0 +``` + +#### Get User Usage +```bash +GET /jotform/user/usage +``` + +#### Get User History +```bash +GET /jotform/user/history?limit=20 +``` + +### Forms + +#### Get Form +```bash +GET /jotform/form/{formId} +``` + +#### Get Form Questions +```bash +GET /jotform/form/{formId}/questions +``` + +#### Get Form Properties +```bash +GET /jotform/form/{formId}/properties +``` + +#### Get Form Submissions +```bash +GET /jotform/form/{formId}/submissions?limit=20&offset=0 +``` + +With filter: +```bash +GET /jotform/form/{formId}/submissions?filter={"created_at:gt":"2024-01-01"} +``` + +#### Get Form Files +```bash +GET /jotform/form/{formId}/files +``` + +#### Create Form +```bash +POST /jotform/user/forms +Content-Type: application/json + +{ + "properties": { + "title": "Contact Form" + }, + "questions": { + "1": { + "type": "control_textbox", + "text": "Name", + "name": "name" + }, + "2": { + "type": "control_email", + "text": "Email", + "name": "email" + } + } +} +``` + +#### Delete Form +```bash +DELETE /jotform/form/{formId} +``` + +### Submissions + +#### Get Submission +```bash +GET /jotform/submission/{submissionId} +``` + +#### Update Submission +```bash +POST /jotform/submission/{submissionId} +Content-Type: application/x-www-form-urlencoded + +submission[3][first]=John&submission[3][last]=Doe +``` + +Note: Use question IDs from the form questions endpoint. The submission field format is `submission[questionId][subfield]=value`. + +#### Delete Submission +```bash +DELETE /jotform/submission/{submissionId} +``` + +### Reports + +#### Get Form Reports +```bash +GET /jotform/form/{formId}/reports +``` + +### Webhooks + +#### Get Form Webhooks +```bash +GET /jotform/form/{formId}/webhooks +``` + +#### Create Webhook +```bash +POST /jotform/form/{formId}/webhooks +Content-Type: application/x-www-form-urlencoded + +webhookURL=https://example.com/webhook +``` + +#### Delete Webhook +```bash +DELETE /jotform/form/{formId}/webhooks/{webhookIndex} +``` + +## Question Types + +- `control_textbox` - Single line text +- `control_textarea` - Multi-line text +- `control_email` - Email +- `control_phone` - Phone number +- `control_dropdown` - Dropdown +- `control_radio` - Radio buttons +- `control_checkbox` - Checkboxes +- `control_datetime` - Date/time picker +- `control_fileupload` - File upload +- `control_signature` - Signature + +## Filter Syntax + +Filters use JSON format: +- `{"field:gt":"value"}` - Greater than +- `{"field:lt":"value"}` - Less than +- `{"field:eq":"value"}` - Equal to +- `{"field:ne":"value"}` - Not equal to + +## Notes + +- Authentication is automatic - the router injects the `APIKEY` header +- Form IDs are numeric +- Submissions include all answers as key-value pairs +- Use `orderby` parameter to sort results (e.g., `orderby=created_at`) +- Pagination uses `limit` and `offset` parameters + +## Resources + +- [API Overview](https://api.jotform.com/docs/) +- [Get User Info](https://api.jotform.com/docs/#user) +- [Get User Forms](https://api.jotform.com/docs/#user-forms) +- [Get User Submissions](https://api.jotform.com/docs/#user-submissions) +- [Get Form Details](https://api.jotform.com/docs/#form-id) +- [Get Form Questions](https://api.jotform.com/docs/#form-id-questions) +- [Get Form Submissions](https://api.jotform.com/docs/#form-id-submissions) +- [Get Submission](https://api.jotform.com/docs/#submission-id) +- [Webhooks](https://api.jotform.com/docs/#form-id-webhooks) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/keap.md b/.agents/skills/api-gateway/references/keap.md new file mode 100644 index 00000000..3efb4f7e --- /dev/null +++ b/.agents/skills/api-gateway/references/keap.md @@ -0,0 +1,190 @@ +# Keap Routing Reference + +**App name:** `keap` +**Base URL proxied:** `api.infusionsoft.com/crm/rest` + +## API Path Pattern + +``` +/keap/crm/rest/v2/{resource} +``` + +Note: The `/crm/rest` prefix is required in the path. + +## Common Endpoints + +### Get Current User +```bash +GET /keap/crm/rest/v2/oauth/connect/userinfo +``` + +### List Contacts +```bash +GET /keap/crm/rest/v2/contacts +``` + +Query parameters: `page_size`, `page_token`, `filter`, `order_by`, `fields` + +### Get Contact +```bash +GET /keap/crm/rest/v2/contacts/{contact_id} +``` + +### Create Contact +```bash +POST /keap/crm/rest/v2/contacts +Content-Type: application/json + +{ + "given_name": "John", + "family_name": "Doe", + "email_addresses": [{"email": "john@example.com", "field": "EMAIL1"}] +} +``` + +### Update Contact +```bash +PATCH /keap/crm/rest/v2/contacts/{contact_id} +Content-Type: application/json + +{ + "given_name": "Jane" +} +``` + +### Delete Contact +```bash +DELETE /keap/crm/rest/v2/contacts/{contact_id} +``` + +### List Companies +```bash +GET /keap/crm/rest/v2/companies +``` + +### List Tags +```bash +GET /keap/crm/rest/v2/tags +``` + +### Apply Tags to Contacts +```bash +POST /keap/crm/rest/v2/tags/{tag_id}/contacts:applyTags +Content-Type: application/json + +{ + "contact_ids": ["1", "2", "3"] +} +``` + +### List Tasks +```bash +GET /keap/crm/rest/v2/tasks +``` + +### Create Task +```bash +POST /keap/crm/rest/v2/tasks +Content-Type: application/json + +{ + "title": "Follow up call", + "due_date": "2026-02-15T10:00:00Z", + "contact": {"id": "9"} +} +``` + +### List Opportunities +```bash +GET /keap/crm/rest/v2/opportunities +``` + +### List Orders +```bash +GET /keap/crm/rest/v2/orders +``` + +### List Products +```bash +GET /keap/crm/rest/v2/products +``` + +### List Campaigns +```bash +GET /keap/crm/rest/v2/campaigns +``` + +### Add Contacts to Campaign Sequence +```bash +POST /keap/crm/rest/v2/campaigns/{campaign_id}/sequences/{sequence_id}:addContacts +Content-Type: application/json + +{ + "contact_ids": ["1", "2"] +} +``` + +### List Emails +```bash +GET /keap/crm/rest/v2/emails +``` + +### Send Email +```bash +POST /keap/crm/rest/v2/emails:send +Content-Type: application/json + +{ + "contacts": [{"id": "9"}], + "subject": "Hello", + "html_content": "

Email body

" +} +``` + +### List Automations +```bash +GET /keap/crm/rest/v2/automations +``` + +### List Affiliates +```bash +GET /keap/crm/rest/v2/affiliates +``` + +### List Subscriptions +```bash +GET /keap/crm/rest/v2/subscriptions +``` + +## Pagination + +Uses token-based pagination: + +```bash +GET /keap/crm/rest/v2/contacts?page_size=50 +GET /keap/crm/rest/v2/contacts?page_size=50&page_token=NEXT_TOKEN +``` + +Response includes `next_page_token` (empty when no more pages). + +## Filtering + +Use the `filter` parameter: + +```bash +GET /keap/crm/rest/v2/contacts?filter=given_name==John +GET /keap/crm/rest/v2/tasks?filter=completed==false +``` + +## Notes + +- API version is v2 (v1 is deprecated) +- Path must include `/crm/rest` prefix +- IDs are returned as strings +- Maximum `page_size` is 1000 +- Timestamps use ISO 8601 format + +## Resources + +- [Keap Developer Portal](https://developer.infusionsoft.com/) +- [Keap REST API V2 Documentation](https://developer.infusionsoft.com/docs/restv2/) diff --git a/.agents/skills/api-gateway/references/kit.md b/.agents/skills/api-gateway/references/kit.md new file mode 100644 index 00000000..5d0eba0d --- /dev/null +++ b/.agents/skills/api-gateway/references/kit.md @@ -0,0 +1,221 @@ +# Kit Routing Reference + +**App name:** `kit` +**Base URL proxied:** `api.kit.com` + +## API Path Pattern + +``` +/kit/v4/{resource} +``` + +## Common Endpoints + +### List Subscribers +```bash +GET /kit/v4/subscribers +``` + +Query parameters: +- `per_page` - Results per page (default: 500, max: 1000) +- `after` - Cursor for next page +- `before` - Cursor for previous page +- `status` - Filter by: `active`, `inactive`, `bounced`, `complained`, `cancelled`, or `all` +- `email_address` - Filter by specific email + +### Get Subscriber +```bash +GET /kit/v4/subscribers/{id} +``` + +### Create Subscriber +```bash +POST /kit/v4/subscribers +Content-Type: application/json + +{ + "email_address": "user@example.com", + "first_name": "John" +} +``` + +### Update Subscriber +```bash +PUT /kit/v4/subscribers/{id} +Content-Type: application/json + +{ + "first_name": "Updated Name" +} +``` + +### List Tags +```bash +GET /kit/v4/tags +``` + +### Create Tag +```bash +POST /kit/v4/tags +Content-Type: application/json + +{ + "name": "new-tag" +} +``` + +### Update Tag +```bash +PUT /kit/v4/tags/{id} +Content-Type: application/json + +{ + "name": "updated-tag-name" +} +``` + +### Delete Tag +```bash +DELETE /kit/v4/tags/{id} +``` + +### Tag a Subscriber +```bash +POST /kit/v4/tags/{tag_id}/subscribers +Content-Type: application/json + +{ + "email_address": "user@example.com" +} +``` + +### Remove Tag from Subscriber +```bash +DELETE /kit/v4/tags/{tag_id}/subscribers/{subscriber_id} +``` + +### List Subscribers with Tag +```bash +GET /kit/v4/tags/{tag_id}/subscribers +``` + +### List Forms +```bash +GET /kit/v4/forms +``` + +### Add Subscriber to Form +```bash +POST /kit/v4/forms/{form_id}/subscribers +Content-Type: application/json + +{ + "email_address": "user@example.com" +} +``` + +### List Form Subscribers +```bash +GET /kit/v4/forms/{form_id}/subscribers +``` + +### List Sequences +```bash +GET /kit/v4/sequences +``` + +### Add Subscriber to Sequence +```bash +POST /kit/v4/sequences/{sequence_id}/subscribers +Content-Type: application/json + +{ + "email_address": "user@example.com" +} +``` + +### List Broadcasts +```bash +GET /kit/v4/broadcasts +``` + +### List Segments +```bash +GET /kit/v4/segments +``` + +### List Custom Fields +```bash +GET /kit/v4/custom_fields +``` + +### Create Custom Field +```bash +POST /kit/v4/custom_fields +Content-Type: application/json + +{ + "label": "Company" +} +``` + +### Update Custom Field +```bash +PUT /kit/v4/custom_fields/{id} +Content-Type: application/json + +{ + "label": "Company Name" +} +``` + +### Delete Custom Field +```bash +DELETE /kit/v4/custom_fields/{id} +``` + +### List Email Templates +```bash +GET /kit/v4/email_templates +``` + +### List Purchases +```bash +GET /kit/v4/purchases +``` + +### List Webhooks +```bash +GET /kit/v4/webhooks +``` + +### Create Webhook +```bash +POST /kit/v4/webhooks +Content-Type: application/json + +{ + "target_url": "https://example.com/webhook", + "event": {"name": "subscriber.subscriber_activate"} +} +``` + +### Delete Webhook +```bash +DELETE /kit/v4/webhooks/{id} +``` + +## Notes + +- Kit API uses V4 (V3 is deprecated) +- Subscriber IDs are integers +- Custom field keys are auto-generated from labels +- Uses cursor-based pagination with `after` and `before` parameters +- Delete operations return 204 No Content +- Bulk operations (>100 items) are processed asynchronously + +## Resources + +- [Kit API Overview](https://developers.kit.com/api-reference/overview) +- [Kit API Reference](https://developers.kit.com/api-reference) +- [Kit Developer Documentation](https://developers.kit.com) diff --git a/.agents/skills/api-gateway/references/klaviyo.md b/.agents/skills/api-gateway/references/klaviyo.md new file mode 100644 index 00000000..f8cdd0b7 --- /dev/null +++ b/.agents/skills/api-gateway/references/klaviyo.md @@ -0,0 +1,237 @@ +# Klaviyo Routing Reference + +**App name:** `klaviyo` +**Base URL proxied:** `a.klaviyo.com` + +## API Path Pattern + +``` +/klaviyo/api/{resource} +``` + +## API Versioning + +Include the `revision` header in all requests: + +``` +revision: 2024-10-15 +``` + +## Common Endpoints + +### Get Profiles +```bash +GET /klaviyo/api/profiles +``` + +Query parameters: +- `filter` - Filter profiles (e.g., `filter=equals(email,"test@example.com")`) +- `fields[profile]` - Comma-separated list of fields to include +- `page[size]` - Number of results per page (max 100) + +### Get a Profile +```bash +GET /klaviyo/api/profiles/{profile_id} +``` + +### Create a Profile +```bash +POST /klaviyo/api/profiles +Content-Type: application/json + +{ + "data": { + "type": "profile", + "attributes": { + "email": "newuser@example.com", + "first_name": "John", + "last_name": "Doe" + } + } +} +``` + +### Update a Profile +```bash +PATCH /klaviyo/api/profiles/{profile_id} +Content-Type: application/json + +{ + "data": { + "type": "profile", + "id": "PROFILE_ID", + "attributes": { + "first_name": "Jane" + } + } +} +``` + +### Get Lists +```bash +GET /klaviyo/api/lists +``` + +### Create a List +```bash +POST /klaviyo/api/lists +Content-Type: application/json + +{ + "data": { + "type": "list", + "attributes": { + "name": "VIP Customers" + } + } +} +``` + +### Add Profiles to List +```bash +POST /klaviyo/api/lists/{list_id}/relationships/profiles +Content-Type: application/json + +{ + "data": [ + {"type": "profile", "id": "PROFILE_ID"} + ] +} +``` + +### Get Segments +```bash +GET /klaviyo/api/segments +``` + +### Get Campaigns +```bash +GET /klaviyo/api/campaigns +``` + +### Create a Campaign +```bash +POST /klaviyo/api/campaigns +Content-Type: application/json + +{ + "data": { + "type": "campaign", + "attributes": { + "name": "Summer Newsletter", + "audiences": { + "included": ["LIST_ID"] + } + } + } +} +``` + +### Get Flows +```bash +GET /klaviyo/api/flows +``` + +### Update Flow Status +```bash +PATCH /klaviyo/api/flows/{flow_id} +Content-Type: application/json + +{ + "data": { + "type": "flow", + "id": "FLOW_ID", + "attributes": { + "status": "live" + } + } +} +``` + +### Create an Event +```bash +POST /klaviyo/api/events +Content-Type: application/json + +{ + "data": { + "type": "event", + "attributes": { + "profile": { + "data": { + "type": "profile", + "attributes": { + "email": "customer@example.com" + } + } + }, + "metric": { + "data": { + "type": "metric", + "attributes": { + "name": "Viewed Product" + } + } + }, + "properties": { + "product_id": "SKU123", + "product_name": "Blue T-Shirt" + } + } + } +} +``` + +### Get Metrics +```bash +GET /klaviyo/api/metrics +``` + +### Get Templates +```bash +GET /klaviyo/api/templates +``` + +### Create Webhook +```bash +POST /klaviyo/api/webhooks +Content-Type: application/json + +{ + "data": { + "type": "webhook", + "attributes": { + "name": "Order Placed Webhook", + "endpoint_url": "https://example.com/webhooks/klaviyo", + "enabled": true + }, + "relationships": { + "webhook-topics": { + "data": [ + {"type": "webhook-topic", "id": "campaign:sent"} + ] + } + } + } +} +``` + +### Delete Webhook +```bash +DELETE /klaviyo/api/webhooks/{webhook_id} +``` + +## Notes + +- All requests use JSON:API specification +- Timestamps are in ISO 8601 RFC 3339 format +- Resource IDs are strings (often base64-encoded) +- Use sparse fieldsets to optimize response size (e.g., `fields[profile]=email,first_name`) +- Include `revision` header for API versioning +- Use cursor-based pagination with `page[cursor]` parameter + +## Resources + +- [Klaviyo API Documentation](https://developers.klaviyo.com) +- [API Reference](https://developers.klaviyo.com/en/reference/api_overview) +- [Klaviyo Developer Portal](https://developers.klaviyo.com/en) diff --git a/.agents/skills/api-gateway/references/linear.md b/.agents/skills/api-gateway/references/linear.md new file mode 100644 index 00000000..6af861d8 --- /dev/null +++ b/.agents/skills/api-gateway/references/linear.md @@ -0,0 +1,203 @@ +# Linear Routing Reference + +**App name:** `linear` +**Base URL proxied:** `api.linear.app` + +## API Type + +Linear uses a GraphQL API exclusively. All requests are POST requests to the `/graphql` endpoint. + +## API Path Pattern + +``` +/linear/graphql +``` + +All operations use POST with a JSON body containing the `query` field. + +## Common Operations + +### Get Current User (Viewer) +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "{ viewer { id name email } }" +} +``` + +### Get Organization +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "{ organization { id name urlKey } }" +} +``` + +### List Teams +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "{ teams { nodes { id name key } } }" +} +``` + +### List Issues +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "{ issues(first: 20) { nodes { id identifier title state { name } priority } pageInfo { hasNextPage endCursor } } }" +} +``` + +### Get Issue by Identifier +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "{ issue(id: \"MTN-527\") { id identifier title description state { name } priority assignee { name } team { key } createdAt } }" +} +``` + +### Filter Issues by State +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "{ issues(first: 20, filter: { state: { type: { eq: \"started\" } } }) { nodes { id identifier title state { name } } } }" +} +``` + +### Search Issues +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "{ searchIssues(first: 20, term: \"search term\") { nodes { id identifier title } } }" +} +``` + +### Create Issue +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "mutation { issueCreate(input: { teamId: \"TEAM_ID\", title: \"Issue title\", description: \"Description\" }) { success issue { id identifier title } } }" +} +``` + +### Update Issue +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "mutation { issueUpdate(id: \"ISSUE_ID\", input: { title: \"Updated title\", priority: 2 }) { success issue { id identifier title priority } } }" +} +``` + +### Create Comment +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "mutation { commentCreate(input: { issueId: \"ISSUE_ID\", body: \"Comment text\" }) { success comment { id body } } }" +} +``` + +### List Projects +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "{ projects(first: 20) { nodes { id name state createdAt } } }" +} +``` + +### List Labels +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "{ issueLabels(first: 50) { nodes { id name color } } }" +} +``` + +### List Workflow States +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "{ workflowStates(first: 50) { nodes { id name type team { key } } } }" +} +``` + +### List Users +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "{ users(first: 50) { nodes { id name email active } } }" +} +``` + +### List Cycles +```bash +POST /linear/graphql +Content-Type: application/json + +{ + "query": "{ cycles(first: 20) { nodes { id name number startsAt endsAt } } }" +} +``` + +## Pagination + +Linear uses Relay-style cursor-based pagination: + +```bash +# First page +POST /linear/graphql +{ + "query": "{ issues(first: 20) { nodes { id identifier title } pageInfo { hasNextPage endCursor } } }" +} + +# Next page +POST /linear/graphql +{ + "query": "{ issues(first: 20, after: \"CURSOR_VALUE\") { nodes { id identifier title } pageInfo { hasNextPage endCursor } } }" +} +``` + +## Notes + +- Linear uses GraphQL exclusively (no REST API) +- Issue identifiers (e.g., `MTN-527`) can be used in place of UUIDs for the `id` parameter +- Priority values: 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low +- Workflow state types: `backlog`, `unstarted`, `started`, `completed`, `canceled` +- Some mutations (delete, create labels/projects) may require additional OAuth scopes +- Use `searchIssues(term: "...")` for full-text search +- Filter operators: `eq`, `neq`, `in`, `nin`, `containsIgnoreCase`, etc. + +## Resources + +- [Linear API Overview](https://linear.app/developers) +- [Linear GraphQL Getting Started](https://linear.app/developers/graphql) +- [Linear GraphQL Schema (Apollo Studio)](https://studio.apollographql.com/public/Linear-API/schema/reference?variant=current) +- [Linear API and Webhooks](https://linear.app/docs/api-and-webhooks) diff --git a/.agents/skills/api-gateway/references/linkedin.md b/.agents/skills/api-gateway/references/linkedin.md new file mode 100644 index 00000000..3c8a95ec --- /dev/null +++ b/.agents/skills/api-gateway/references/linkedin.md @@ -0,0 +1,278 @@ +# LinkedIn Routing Reference + +**App name:** `linkedin` +**Base URL proxied:** `api.linkedin.com` + +## API Path Pattern + +``` +/linkedin/v2/{resource} +``` + +## Required Headers + +``` +X-Restli-Protocol-Version: 2.0.0 +``` + +## Common Endpoints + +### Get User Info (OpenID Connect) +```bash +GET /linkedin/v2/userinfo +``` + +### Get Current User Profile +```bash +GET /linkedin/v2/me +``` + +With projection: +```bash +GET /linkedin/v2/me?projection=(id,firstName,lastName) +``` + +### Create Text Post +```bash +POST /linkedin/v2/ugcPosts +Content-Type: application/json +X-Restli-Protocol-Version: 2.0.0 + +{ + "author": "urn:li:person:{personId}", + "lifecycleState": "PUBLISHED", + "specificContent": { + "com.linkedin.ugc.ShareContent": { + "shareCommentary": {"text": "Hello LinkedIn!"}, + "shareMediaCategory": "NONE" + } + }, + "visibility": { + "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" + } +} +``` + +### Create Article/URL Share +```bash +POST /linkedin/v2/ugcPosts +Content-Type: application/json +X-Restli-Protocol-Version: 2.0.0 + +{ + "author": "urn:li:person:{personId}", + "lifecycleState": "PUBLISHED", + "specificContent": { + "com.linkedin.ugc.ShareContent": { + "shareCommentary": {"text": "Check this out!"}, + "shareMediaCategory": "ARTICLE", + "media": [{ + "status": "READY", + "originalUrl": "https://example.com", + "title": {"text": "Title"}, + "description": {"text": "Description"} + }] + } + }, + "visibility": { + "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" + } +} +``` + +### Register Image Upload +```bash +POST /linkedin/v2/assets?action=registerUpload +Content-Type: application/json +X-Restli-Protocol-Version: 2.0.0 + +{ + "registerUploadRequest": { + "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], + "owner": "urn:li:person:{personId}", + "serviceRelationships": [{ + "relationshipType": "OWNER", + "identifier": "urn:li:userGeneratedContent" + }] + } +} +``` + +### Ad Library - Search Ads +```bash +GET /linkedin/rest/adLibrary?q=criteria&keyword=linkedin +``` + +Required headers: +- `LinkedIn-Version: 202502` + +### Job Library - Search Jobs +```bash +GET /linkedin/rest/jobLibrary?q=criteria&keyword=software +``` + +Required headers: +- `LinkedIn-Version: 202506` + +## Marketing API (Advertising) + +Required headers for all Marketing API calls: +``` +X-Restli-Protocol-Version: 2.0.0 +LinkedIn-Version: 202502 +``` + +### List Ad Accounts +```bash +GET /linkedin/rest/adAccounts?q=search +``` + +### Get Ad Account +```bash +GET /linkedin/rest/adAccounts/{adAccountId} +``` + +### Create Ad Account +```bash +POST /linkedin/rest/adAccounts +Content-Type: application/json + +{ + "name": "Ad Account Name", + "currency": "USD", + "reference": "urn:li:organization:{orgId}", + "type": "BUSINESS" +} +``` + +### List Campaign Groups +```bash +GET /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups +``` + +### Create Campaign Group +```bash +POST /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups +Content-Type: application/json + +{ + "name": "Campaign Group Name", + "status": "DRAFT" +} +``` + +### Get Campaign Group +```bash +GET /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups/{campaignGroupId} +``` + +### List Campaigns +```bash +GET /linkedin/rest/adAccounts/{adAccountId}/adCampaigns +``` + +### Create Campaign +```bash +POST /linkedin/rest/adAccounts/{adAccountId}/adCampaigns +Content-Type: application/json + +{ + "campaignGroup": "urn:li:sponsoredCampaignGroup:{groupId}", + "name": "Campaign Name", + "status": "DRAFT", + "objectiveType": "BRAND_AWARENESS" +} +``` + +### Get Campaign +```bash +GET /linkedin/rest/adAccounts/{adAccountId}/adCampaigns/{campaignId} +``` + +### List Organization ACLs +```bash +GET /linkedin/v2/organizationAcls?q=roleAssignee +``` + +### Lookup Organization by Vanity Name +```bash +GET /linkedin/rest/organizations?q=vanityName&vanityName=microsoft +``` + +### Get Organization Share Statistics +```bash +GET /linkedin/rest/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:12345 +``` + +### Get Organization Posts +```bash +GET /linkedin/rest/posts?q=author&author=urn:li:organization:12345 +``` + +## Media Upload (REST API) + +Required headers: +- `LinkedIn-Version: 202502` + +### Initialize Image Upload +```bash +POST /linkedin/rest/images?action=initializeUpload +Content-Type: application/json + +{"initializeUploadRequest": {"owner": "urn:li:person:{personId}"}} +``` + +### Initialize Video Upload +```bash +POST /linkedin/rest/videos?action=initializeUpload +Content-Type: application/json + +{"initializeUploadRequest": {"owner": "urn:li:person:{personId}", "fileSizeBytes": 10000000}} +``` + +### Initialize Document Upload +```bash +POST /linkedin/rest/documents?action=initializeUpload +Content-Type: application/json + +{"initializeUploadRequest": {"owner": "urn:li:person:{personId}"}} +``` + +## Ad Targeting + +### Get Targeting Facets +```bash +GET /linkedin/rest/adTargetingFacets +``` + +Returns 31 targeting facets (skills, industries, titles, locations, etc.) + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Include `X-Restli-Protocol-Version: 2.0.0` header for all v2 API calls +- Author URN format: `urn:li:person:{personId}` +- Get person ID from `/v2/me` endpoint +- Image uploads are 3-step: register, upload binary, create post +- Rate limits: 150 requests/day per member, 100K/day per app + +## Visibility Options + +- `PUBLIC` - Viewable by anyone +- `CONNECTIONS` - 1st-degree connections only + +## Share Media Categories + +- `NONE` - Text only +- `ARTICLE` - URL share +- `IMAGE` - Image post +- `VIDEO` - Video post + +## Resources + +- [LinkedIn API Overview](https://learn.microsoft.com/en-us/linkedin/) +- [Share on LinkedIn](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/share-on-linkedin) +- [Profile API](https://learn.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api) +- [Marketing API](https://learn.microsoft.com/en-us/linkedin/marketing/) +- [Ad Accounts](https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads/account-structure/create-and-manage-accounts) +- [Campaigns](https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads/account-structure/create-and-manage-campaigns) diff --git a/.agents/skills/api-gateway/references/mailchimp.md b/.agents/skills/api-gateway/references/mailchimp.md new file mode 100644 index 00000000..c4888f5b --- /dev/null +++ b/.agents/skills/api-gateway/references/mailchimp.md @@ -0,0 +1,213 @@ +# Mailchimp Routing Reference + +**App name:** `mailchimp` +**Base URL proxied:** `{dc}.api.mailchimp.com` + +## API Path Pattern + +``` +/mailchimp/3.0/{resource} +``` + +## Common Endpoints + +### Get All Lists (Audiences) +```bash +GET /mailchimp/3.0/lists +``` + +Query parameters: +- `count` - Number of records to return (default 10, max 1000) +- `offset` - Number of records to skip (for pagination) + +### Get a List +```bash +GET /mailchimp/3.0/lists/{list_id} +``` + +### Create a List +```bash +POST /mailchimp/3.0/lists +Content-Type: application/json + +{ + "name": "Newsletter", + "contact": { + "company": "Acme Corp", + "address1": "123 Main St", + "city": "New York", + "state": "NY", + "zip": "10001", + "country": "US" + }, + "permission_reminder": "You signed up for our newsletter", + "campaign_defaults": { + "from_name": "Acme Corp", + "from_email": "newsletter@acme.com", + "subject": "", + "language": "en" + }, + "email_type_option": true +} +``` + +### Get List Members +```bash +GET /mailchimp/3.0/lists/{list_id}/members?status=subscribed&count=50 +``` + +### Add a Member +```bash +POST /mailchimp/3.0/lists/{list_id}/members +Content-Type: application/json + +{ + "email_address": "newuser@example.com", + "status": "subscribed", + "merge_fields": { + "FNAME": "Jane", + "LNAME": "Smith" + } +} +``` + +### Update a Member +```bash +PATCH /mailchimp/3.0/lists/{list_id}/members/{subscriber_hash} +Content-Type: application/json + +{ + "merge_fields": { + "FNAME": "Jane", + "LNAME": "Doe" + } +} +``` + +### Add or Update a Member (Upsert) +```bash +PUT /mailchimp/3.0/lists/{list_id}/members/{subscriber_hash} +Content-Type: application/json + +{ + "email_address": "user@example.com", + "status_if_new": "subscribed", + "merge_fields": { + "FNAME": "Jane", + "LNAME": "Smith" + } +} +``` + +### Delete a Member +```bash +DELETE /mailchimp/3.0/lists/{list_id}/members/{subscriber_hash} +``` + +### Add or Remove Tags +```bash +POST /mailchimp/3.0/lists/{list_id}/members/{subscriber_hash}/tags +Content-Type: application/json + +{ + "tags": [ + {"name": "VIP", "status": "active"}, + {"name": "Old Tag", "status": "inactive"} + ] +} +``` + +### Get Segments +```bash +GET /mailchimp/3.0/lists/{list_id}/segments +``` + +### Get All Campaigns +```bash +GET /mailchimp/3.0/campaigns?status=sent&count=20 +``` + +### Create a Campaign +```bash +POST /mailchimp/3.0/campaigns +Content-Type: application/json + +{ + "type": "regular", + "recipients": { + "list_id": "LIST_ID" + }, + "settings": { + "subject_line": "Your Monthly Update", + "from_name": "Acme Corp", + "reply_to": "hello@acme.com" + } +} +``` + +### Set Campaign Content +```bash +PUT /mailchimp/3.0/campaigns/{campaign_id}/content +Content-Type: application/json + +{ + "html": "

Hello!

Newsletter content here.

", + "plain_text": "Hello! Newsletter content here." +} +``` + +### Send a Campaign +```bash +POST /mailchimp/3.0/campaigns/{campaign_id}/actions/send +``` + +### Schedule a Campaign +```bash +POST /mailchimp/3.0/campaigns/{campaign_id}/actions/schedule +Content-Type: application/json + +{ + "schedule_time": "2025-03-01T10:00:00+00:00" +} +``` + +### Get All Templates +```bash +GET /mailchimp/3.0/templates?type=user +``` + +### Get All Automations +```bash +GET /mailchimp/3.0/automations +``` + +### Start an Automation +```bash +POST /mailchimp/3.0/automations/{workflow_id}/actions/start-all-emails +``` + +### Get Campaign Reports +```bash +GET /mailchimp/3.0/reports?count=20 +``` + +### Get Campaign Report +```bash +GET /mailchimp/3.0/reports/{campaign_id} +``` + +## Notes + +- List IDs are 10-character alphanumeric strings +- Subscriber hashes are MD5 hashes of lowercase email addresses +- Timestamps are in ISO 8601 format +- Maximum 1000 records per request for list endpoints +- "Audience" and "list" are used interchangeably (app vs API terminology) +- "Contact" and "member" are used interchangeably (app vs API terminology) +- Use offset-based pagination with `count` and `offset` parameters + +## Resources + +- [Mailchimp Marketing API Documentation](https://mailchimp.com/developer/marketing/) +- [API Reference](https://mailchimp.com/developer/marketing/api/) +- [Quick Start Guide](https://mailchimp.com/developer/marketing/guides/quick-start/) diff --git a/.agents/skills/api-gateway/references/mailerlite.md b/.agents/skills/api-gateway/references/mailerlite.md new file mode 100644 index 00000000..6c76f08d --- /dev/null +++ b/.agents/skills/api-gateway/references/mailerlite.md @@ -0,0 +1,238 @@ +# MailerLite Routing Reference + +**App name:** `mailerlite` +**Base URL proxied:** `connect.mailerlite.com` + +## API Path Pattern + +``` +/mailerlite/api/{resource} +``` + +## Common Endpoints + +### Subscribers + +#### List Subscribers +```bash +GET /mailerlite/api/subscribers +``` + +Query parameters: `filter[status]`, `limit`, `cursor`, `include` + +#### Get Subscriber +```bash +GET /mailerlite/api/subscribers/{subscriber_id_or_email} +``` + +#### Create/Upsert Subscriber +```bash +POST /mailerlite/api/subscribers +Content-Type: application/json + +{ + "email": "subscriber@example.com", + "fields": {"name": "John Doe"}, + "groups": ["12345678901234567"], + "status": "active" +} +``` + +#### Update Subscriber +```bash +PUT /mailerlite/api/subscribers/{subscriber_id} +Content-Type: application/json + +{ + "fields": {"name": "Jane Doe"} +} +``` + +#### Delete Subscriber +```bash +DELETE /mailerlite/api/subscribers/{subscriber_id} +``` + +### Groups + +#### List Groups +```bash +GET /mailerlite/api/groups +``` + +Query parameters: `limit`, `page`, `filter[name]`, `sort` + +#### Create Group +```bash +POST /mailerlite/api/groups +Content-Type: application/json + +{ + "name": "Newsletter Subscribers" +} +``` + +#### Update Group +```bash +PUT /mailerlite/api/groups/{group_id} +Content-Type: application/json + +{ + "name": "Updated Group Name" +} +``` + +#### Delete Group +```bash +DELETE /mailerlite/api/groups/{group_id} +``` + +#### Get Group Subscribers +```bash +GET /mailerlite/api/groups/{group_id}/subscribers +``` + +### Campaigns + +#### List Campaigns +```bash +GET /mailerlite/api/campaigns +``` + +Query parameters: `filter[status]`, `filter[type]`, `limit`, `page` + +#### Get Campaign +```bash +GET /mailerlite/api/campaigns/{campaign_id} +``` + +#### Create Campaign +```bash +POST /mailerlite/api/campaigns +Content-Type: application/json + +{ + "name": "My Newsletter", + "type": "regular", + "emails": [ + { + "subject": "Weekly Update", + "from_name": "Newsletter", + "from": "newsletter@example.com" + } + ], + "groups": ["12345678901234567"] +} +``` + +#### Schedule Campaign +```bash +POST /mailerlite/api/campaigns/{campaign_id}/schedule +Content-Type: application/json + +{ + "delivery": "instant" +} +``` + +#### Delete Campaign +```bash +DELETE /mailerlite/api/campaigns/{campaign_id} +``` + +### Automations + +#### List Automations +```bash +GET /mailerlite/api/automations +``` + +Query parameters: `filter[enabled]`, `filter[name]`, `page`, `limit` + +#### Get Automation +```bash +GET /mailerlite/api/automations/{automation_id} +``` + +#### Delete Automation +```bash +DELETE /mailerlite/api/automations/{automation_id} +``` + +### Fields + +#### List Fields +```bash +GET /mailerlite/api/fields +``` + +#### Create Field +```bash +POST /mailerlite/api/fields +Content-Type: application/json + +{ + "name": "Company", + "type": "text" +} +``` + +### Segments + +#### List Segments +```bash +GET /mailerlite/api/segments +``` + +#### Get Segment Subscribers +```bash +GET /mailerlite/api/segments/{segment_id}/subscribers +``` + +### Forms + +#### List Forms +```bash +GET /mailerlite/api/forms/{type} +``` + +Path parameters: `type` - `popup`, `embedded`, or `promotion` + +#### Get Form Subscribers +```bash +GET /mailerlite/api/forms/{form_id}/subscribers +``` + +### Webhooks + +#### List Webhooks +```bash +GET /mailerlite/api/webhooks +``` + +#### Create Webhook +```bash +POST /mailerlite/api/webhooks +Content-Type: application/json + +{ + "name": "Subscriber Updates", + "events": ["subscriber.created", "subscriber.updated"], + "url": "https://example.com/webhook" +} +``` + +## Notes + +- Rate limit: 120 requests per minute +- Subscriber emails serve as unique identifiers (POST creates or updates existing) +- Only draft campaigns can be updated +- Pagination: cursor-based for subscribers, page-based for groups/campaigns +- API versioning can be overridden via `X-Version: YYYY-MM-DD` header + +## Resources + +- [MailerLite API Documentation](https://developers.mailerlite.com/docs/) +- [MailerLite Subscribers API](https://developers.mailerlite.com/docs/subscribers.html) +- [MailerLite Groups API](https://developers.mailerlite.com/docs/groups.html) +- [MailerLite Campaigns API](https://developers.mailerlite.com/docs/campaigns.html) diff --git a/.agents/skills/api-gateway/references/manychat.md b/.agents/skills/api-gateway/references/manychat.md new file mode 100644 index 00000000..478c3eff --- /dev/null +++ b/.agents/skills/api-gateway/references/manychat.md @@ -0,0 +1,212 @@ +# ManyChat Routing Reference + +**App name:** `manychat` +**Base URL proxied:** `api.manychat.com` + +## API Path Pattern + +``` +/manychat/fb/{category}/{action} +``` + +## Common Endpoints + +### Page Operations + +#### Get Page Info +```bash +GET /manychat/fb/page/getInfo +``` + +#### List Tags +```bash +GET /manychat/fb/page/getTags +``` + +#### Create Tag +```bash +POST /manychat/fb/page/createTag +Content-Type: application/json + +{ + "name": "New Tag" +} +``` + +#### Remove Tag +```bash +POST /manychat/fb/page/removeTag +Content-Type: application/json + +{ + "tag_id": 123 +} +``` + +#### List Custom Fields +```bash +GET /manychat/fb/page/getCustomFields +``` + +#### Create Custom Field +```bash +POST /manychat/fb/page/createCustomField +Content-Type: application/json + +{ + "caption": "Phone Number", + "type": "text", + "description": "Customer phone number" +} +``` + +#### List Bot Fields +```bash +GET /manychat/fb/page/getBotFields +``` + +#### Set Bot Field +```bash +POST /manychat/fb/page/setBotField +Content-Type: application/json + +{ + "field_id": 123, + "field_value": 42 +} +``` + +#### List Flows +```bash +GET /manychat/fb/page/getFlows +``` + +#### List Growth Tools +```bash +GET /manychat/fb/page/getGrowthTools +``` + +#### List OTN Topics +```bash +GET /manychat/fb/page/getOtnTopics +``` + +### Subscriber Operations + +#### Get Subscriber Info +```bash +GET /manychat/fb/subscriber/getInfo?subscriber_id=123456789 +``` + +#### Find Subscriber by Name +```bash +GET /manychat/fb/subscriber/findByName?name=John%20Doe +``` + +#### Find Subscriber by Email/Phone +```bash +GET /manychat/fb/subscriber/findBySystemField?email=john@example.com +``` + +#### Create Subscriber +```bash +POST /manychat/fb/subscriber/createSubscriber +Content-Type: application/json + +{ + "first_name": "John", + "last_name": "Doe", + "phone": "+1234567890", + "email": "john@example.com" +} +``` + +#### Update Subscriber +```bash +POST /manychat/fb/subscriber/updateSubscriber +Content-Type: application/json + +{ + "subscriber_id": 123456789, + "first_name": "John", + "last_name": "Smith" +} +``` + +#### Add Tag to Subscriber +```bash +POST /manychat/fb/subscriber/addTag +Content-Type: application/json + +{ + "subscriber_id": 123456789, + "tag_id": 1 +} +``` + +#### Set Custom Field +```bash +POST /manychat/fb/subscriber/setCustomField +Content-Type: application/json + +{ + "subscriber_id": 123456789, + "field_id": 1, + "field_value": "value" +} +``` + +### Sending Operations + +#### Send Content +```bash +POST /manychat/fb/sending/sendContent +Content-Type: application/json + +{ + "subscriber_id": 123456789, + "data": { + "version": "v2", + "content": { + "messages": [ + {"type": "text", "text": "Hello!"} + ] + } + } +} +``` + +#### Send Flow +```bash +POST /manychat/fb/sending/sendFlow +Content-Type: application/json + +{ + "subscriber_id": 123456789, + "flow_ns": "content123456" +} +``` + +## Rate Limits + +| Endpoint Category | Rate Limit | +|------------------|------------| +| Page GET endpoints | 100 queries/second | +| Page POST endpoints | 10 queries/second | +| Subscriber operations | 10-50 queries/second | +| Sending content | 25 queries/second | +| Sending flows | 20 queries/second | + +## Notes + +- Subscriber IDs are integers unique within a page +- Flow namespaces (flow_ns) identify automation flows +- Message tags are required for sending outside the 24-hour window +- All responses include `{"status": "success"}` or `{"status": "error"}` +- Custom field types: `text`, `number`, `date`, `datetime`, `boolean` + +## Resources + +- [ManyChat API Documentation](https://api.manychat.com/swagger) +- [ManyChat API Key Generation](https://help.manychat.com/hc/en-us/articles/14959510331420) +- [ManyChat Dev Program](https://help.manychat.com/hc/en-us/articles/14281269835548) diff --git a/.agents/skills/api-gateway/references/microsoft-excel.md b/.agents/skills/api-gateway/references/microsoft-excel.md new file mode 100644 index 00000000..8ecac2fe --- /dev/null +++ b/.agents/skills/api-gateway/references/microsoft-excel.md @@ -0,0 +1,174 @@ +# Microsoft Excel Routing Reference + +**App name:** `microsoft-excel` +**Base URL proxied:** `graph.microsoft.com` + +## API Path Pattern + +``` +/microsoft-excel/v1.0/me/drive/items/{file-id}/workbook/{resource} +/microsoft-excel/v1.0/me/drive/root:/{path}:/workbook/{resource} +``` + +## Common Endpoints + +### Drive Operations + +#### Get Drive Info +```bash +GET /microsoft-excel/v1.0/me/drive +``` + +#### List Root Files +```bash +GET /microsoft-excel/v1.0/me/drive/root/children +``` + +#### Search Files +```bash +GET /microsoft-excel/v1.0/me/drive/root/search(q='.xlsx') +``` + +### Session Management + +#### Create Session +```bash +POST /microsoft-excel/v1.0/me/drive/root:/{path}:/workbook/createSession +Content-Type: application/json + +{ + "persistChanges": true +} +``` + +### Worksheet Operations + +#### List Worksheets +```bash +GET /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/worksheets +``` + +#### Create Worksheet +```bash +POST /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/worksheets +Content-Type: application/json + +{ + "name": "NewSheet" +} +``` + +#### Delete Worksheet +```bash +DELETE /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/worksheets('{id}') +``` + +### Range Operations + +#### Get Range +```bash +GET /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/worksheets('Sheet1')/range(address='A1:B2') +``` + +#### Update Range +```bash +PATCH /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/worksheets('Sheet1')/range(address='A1:B2') +Content-Type: application/json + +{ + "values": [ + ["Value1", "Value2"], + [100, 200] + ] +} +``` + +#### Get Used Range +```bash +GET /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/worksheets('Sheet1')/usedRange +``` + +### Table Operations + +#### List Tables +```bash +GET /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/worksheets('Sheet1')/tables +``` + +#### Create Table +```bash +POST /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/worksheets('Sheet1')/tables/add +Content-Type: application/json + +{ + "address": "A1:C4", + "hasHeaders": true +} +``` + +#### Get Table Rows +```bash +GET /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/tables('Table1')/rows +``` + +#### Add Table Row +```bash +POST /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/tables('Table1')/rows +Content-Type: application/json + +{ + "values": [["Data1", "Data2", "Data3"]] +} +``` + +#### Delete Table Row +```bash +DELETE /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/tables('Table1')/rows/itemAt(index=0) +``` + +#### Get Table Columns +```bash +GET /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/tables('Table1')/columns +``` + +### Named Items + +#### List Named Items +```bash +GET /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/names +``` + +### Charts + +#### List Charts +```bash +GET /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/worksheets('Sheet1')/charts +``` + +#### Add Chart +```bash +POST /microsoft-excel/v1.0/me/drive/root:/workbook.xlsx:/workbook/worksheets('Sheet1')/charts/add +Content-Type: application/json + +{ + "type": "ColumnClustered", + "sourceData": "A1:C4", + "seriesBy": "Auto" +} +``` + +## Notes + +- Only `.xlsx` files are supported (not legacy `.xls`) +- Use path-based access (`/drive/root:/{path}:`) or ID-based access (`/drive/items/{id}`) +- Table/worksheet IDs with `{` and `}` must be URL-encoded +- Sessions improve performance for multiple operations +- Sessions expire after ~5 minutes (persistent) or ~7 minutes (non-persistent) +- Range addresses use A1 notation + +## Resources + +- [Microsoft Graph Excel API](https://learn.microsoft.com/en-us/graph/api/resources/excel) +- [Excel Workbook Resource](https://learn.microsoft.com/en-us/graph/api/resources/workbook) +- [Excel Worksheet Resource](https://learn.microsoft.com/en-us/graph/api/resources/worksheet) +- [Excel Range Resource](https://learn.microsoft.com/en-us/graph/api/resources/range) diff --git a/.agents/skills/api-gateway/references/microsoft-to-do.md b/.agents/skills/api-gateway/references/microsoft-to-do.md new file mode 100644 index 00000000..5881b0db --- /dev/null +++ b/.agents/skills/api-gateway/references/microsoft-to-do.md @@ -0,0 +1,166 @@ +# Microsoft To Do Routing Reference + +**App name:** `microsoft-to-do` +**Base URL proxied:** `graph.microsoft.com` + +## API Path Pattern + +``` +/microsoft-to-do/v1.0/me/todo/{resource} +``` + +All Microsoft To Do endpoints use the Microsoft Graph API under the `/me/todo/` path. + +## Common Endpoints + +### Task Lists + +#### List All Task Lists +```bash +GET /microsoft-to-do/v1.0/me/todo/lists +``` + +#### Get Task List +```bash +GET /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId} +``` + +#### Create Task List +```bash +POST /microsoft-to-do/v1.0/me/todo/lists +Content-Type: application/json + +{ + "displayName": "My New List" +} +``` + +#### Update Task List +```bash +PATCH /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId} +Content-Type: application/json + +{ + "displayName": "Updated List Name" +} +``` + +#### Delete Task List +```bash +DELETE /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId} +``` + +### Tasks + +#### List Tasks +```bash +GET /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId}/tasks +``` + +#### Get Task +```bash +GET /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId}/tasks/{taskId} +``` + +#### Create Task +```bash +POST /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId}/tasks +Content-Type: application/json + +{ + "title": "New Task", + "importance": "high", + "status": "notStarted", + "dueDateTime": { + "dateTime": "2024-12-31T17:00:00", + "timeZone": "UTC" + } +} +``` + +#### Update Task +```bash +PATCH /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId}/tasks/{taskId} +Content-Type: application/json + +{ + "status": "completed" +} +``` + +#### Delete Task +```bash +DELETE /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId}/tasks/{taskId} +``` + +### Checklist Items + +#### List Checklist Items +```bash +GET /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId}/tasks/{taskId}/checklistItems +``` + +#### Create Checklist Item +```bash +POST /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId}/tasks/{taskId}/checklistItems +Content-Type: application/json + +{ + "displayName": "Subtask name" +} +``` + +#### Update Checklist Item +```bash +PATCH /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId}/tasks/{taskId}/checklistItems/{checklistItemId} +Content-Type: application/json + +{ + "isChecked": true +} +``` + +#### Delete Checklist Item +```bash +DELETE /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId}/tasks/{taskId}/checklistItems/{checklistItemId} +``` + +### Linked Resources + +#### List Linked Resources +```bash +GET /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId}/tasks/{taskId}/linkedResources +``` + +#### Create Linked Resource +```bash +POST /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId}/tasks/{taskId}/linkedResources +Content-Type: application/json + +{ + "webUrl": "https://example.com/item", + "applicationName": "MyApp", + "displayName": "Related Item" +} +``` + +#### Delete Linked Resource +```bash +DELETE /microsoft-to-do/v1.0/me/todo/lists/{todoTaskListId}/tasks/{taskId}/linkedResources/{linkedResourceId} +``` + +## Notes + +- Task list IDs and task IDs are opaque base64-encoded strings +- Timestamps use ISO 8601 format in UTC by default +- The `dateTimeTimeZone` type requires both `dateTime` and `timeZone` fields +- Task `status` values: `notStarted`, `inProgress`, `completed`, `waitingOnOthers`, `deferred` +- Task `importance` values: `low`, `normal`, `high` +- Supports OData query parameters: `$select`, `$filter`, `$orderby`, `$top`, `$skip` +- Pagination uses `@odata.nextLink` for continuation + +## Resources + +- [Microsoft To Do API Overview](https://learn.microsoft.com/en-us/graph/api/resources/todo-overview) +- [todoTaskList Resource](https://learn.microsoft.com/en-us/graph/api/resources/todotasklist) +- [todoTask Resource](https://learn.microsoft.com/en-us/graph/api/resources/todotask) diff --git a/.agents/skills/api-gateway/references/monday.md b/.agents/skills/api-gateway/references/monday.md new file mode 100644 index 00000000..f1a5a809 --- /dev/null +++ b/.agents/skills/api-gateway/references/monday.md @@ -0,0 +1,207 @@ +# Monday.com Routing Reference + +**App name:** `monday` +**Base URL proxied:** `api.monday.com` + +## API Type + +Monday.com uses a GraphQL API exclusively. All requests are POST requests to the `/v2` endpoint. + +## API Path Pattern + +``` +/monday/v2 +``` + +All operations use POST with a JSON body containing the `query` field. + +## Common Operations + +### Get Current User +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "{ me { id name email } }" +} +``` + +### List Workspaces +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "{ workspaces(limit: 20) { id name kind } }" +} +``` + +### List Boards +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "{ boards(limit: 20) { id name state board_kind workspace { id name } } }" +} +``` + +### Get Board with Items +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "{ boards(ids: [BOARD_ID]) { id name columns { id title type } groups { id title } items_page(limit: 50) { cursor items { id name state column_values { id text } } } } }" +} +``` + +### Create Board +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "mutation { create_board(board_name: \"New Board\", board_kind: public) { id name } }" +} +``` + +### Update Board +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "mutation { update_board(board_id: BOARD_ID, board_attribute: description, new_value: \"Description\") }" +} +``` + +### Delete Board +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "mutation { delete_board(board_id: BOARD_ID) { id } }" +} +``` + +### Get Items by ID +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "{ items(ids: [ITEM_ID]) { id name created_at state board { id name } group { id title } column_values { id text value } } }" +} +``` + +### Create Item +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "mutation { create_item(board_id: BOARD_ID, group_id: \"GROUP_ID\", item_name: \"New item\") { id name } }" +} +``` + +### Create Item with Column Values +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "mutation { create_item(board_id: BOARD_ID, group_id: \"GROUP_ID\", item_name: \"Task\", column_values: \"{\\\"status\\\": {\\\"label\\\": \\\"Working on it\\\"}}\") { id name } }" +} +``` + +### Update Item +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "mutation { change_simple_column_value(board_id: BOARD_ID, item_id: ITEM_ID, column_id: \"name\", value: \"Updated name\") { id name } }" +} +``` + +### Delete Item +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "mutation { delete_item(item_id: ITEM_ID) { id } }" +} +``` + +### Create Column +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "mutation { create_column(board_id: BOARD_ID, title: \"Status\", column_type: status) { id title type } }" +} +``` + +### Create Group +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "mutation { create_group(board_id: BOARD_ID, group_name: \"New Group\") { id title } }" +} +``` + +### List Users +```bash +POST /monday/v2 +Content-Type: application/json + +{ + "query": "{ users(limit: 50) { id name email } }" +} +``` + +## Pagination + +Monday.com uses cursor-based pagination for items: + +```bash +# First page +POST /monday/v2 +{ + "query": "{ boards(ids: [BOARD_ID]) { items_page(limit: 50) { cursor items { id name } } } }" +} + +# Next page +POST /monday/v2 +{ + "query": "{ next_items_page(cursor: \"CURSOR_VALUE\", limit: 50) { cursor items { id name } } }" +} +``` + +## Notes + +- Monday.com uses GraphQL exclusively (no REST API) +- Board IDs, item IDs, and user IDs are numeric strings +- Column IDs are alphanumeric (e.g., `color_mm09e48w`) +- Group IDs are alphanumeric (e.g., `group_mm0939df`, `topics`) +- Column values must be passed as JSON strings +- Board kinds: `public`, `private`, `share` +- Board states: `active`, `archived`, `deleted`, `all` +- Column types: `status`, `text`, `numbers`, `date`, `people`, `dropdown`, `checkbox`, `email`, `phone`, `link`, `timeline`, `tags`, `rating` +- Default limit is 25, maximum is 100 +- Cursors are valid for 60 minutes + +## Resources + +- [Monday.com API Basics](https://developer.monday.com/api-reference/docs/basics) +- [GraphQL Overview](https://developer.monday.com/api-reference/docs/introduction-to-graphql) +- [Boards Reference](https://developer.monday.com/api-reference/reference/boards) +- [Items Reference](https://developer.monday.com/api-reference/reference/items) +- [Columns Reference](https://developer.monday.com/api-reference/reference/columns) diff --git a/.agents/skills/api-gateway/references/notion.md b/.agents/skills/api-gateway/references/notion.md new file mode 100644 index 00000000..47217672 --- /dev/null +++ b/.agents/skills/api-gateway/references/notion.md @@ -0,0 +1,345 @@ +# Notion Routing Reference + +**App name:** `notion` +**Base URL proxied:** `api.notion.com` + +## Required Headers + +All Notion API requests require: +``` +Notion-Version: 2025-09-03 +``` + +## API Path Pattern + +``` +/notion/v1/{endpoint} +``` + +## Key Concept: Databases vs Data Sources + +In API version 2025-09-03, databases and data sources are separate concepts: + +| Concept | Description | Use For | +|---------|-------------|---------| +| **Database** | Container that can hold multiple data sources | Creating databases, getting data_source IDs | +| **Data Source** | Schema and data within a database | Querying, updating schema, updating properties | + +Most existing databases have one data source. Use `GET /databases/{id}` to get the `data_source_id`, then use `/data_sources/` endpoints for all operations. + +## Common Endpoints + +### Search + +Search for pages: +```bash +POST /notion/v1/search +Content-Type: application/json +Notion-Version: 2025-09-03 + +{ + "query": "meeting notes", + "filter": {"property": "object", "value": "page"} +} +``` + +Search for data sources: +```bash +POST /notion/v1/search +Content-Type: application/json +Notion-Version: 2025-09-03 + +{ + "filter": {"property": "object", "value": "data_source"} +} +``` + +With pagination: +```bash +POST /notion/v1/search +Content-Type: application/json +Notion-Version: 2025-09-03 + +{ + "page_size": 10, + "start_cursor": "CURSOR_FROM_PREVIOUS_RESPONSE" +} +``` + +### Data Sources + +Use data source endpoints for querying, getting schema, and updates. + +#### Get Data Source +```bash +GET /notion/v1/data_sources/{dataSourceId} +Notion-Version: 2025-09-03 +``` + +Returns full schema with `properties` field. + +#### Query Data Source +```bash +POST /notion/v1/data_sources/{dataSourceId}/query +Content-Type: application/json +Notion-Version: 2025-09-03 + +{ + "filter": { + "property": "Status", + "select": {"equals": "Active"} + }, + "sorts": [ + {"property": "Created", "direction": "descending"} + ], + "page_size": 100 +} +``` + +#### Update Data Source (title, schema, properties) +```bash +PATCH /notion/v1/data_sources/{dataSourceId} +Content-Type: application/json +Notion-Version: 2025-09-03 + +{ + "title": [{"type": "text", "text": {"content": "Updated Title"}}], + "properties": { + "NewColumn": {"rich_text": {}} + } +} +``` + +### Databases + +Database endpoints are only needed for **creating** databases and **discovering** data source IDs. + +#### Get Database (to find data_source_id) +```bash +GET /notion/v1/databases/{databaseId} +Notion-Version: 2025-09-03 +``` + +Response includes `data_sources` array: +```json +{ + "id": "database-id", + "object": "database", + "data_sources": [{"id": "data-source-id", "name": "Database Name"}] +} +``` + +**Note:** This endpoint returns `properties: null`. Use `GET /data_sources/{id}` to get the schema. + +#### Create Database +```bash +POST /notion/v1/databases +Content-Type: application/json +Notion-Version: 2025-09-03 + +{ + "parent": {"type": "page_id", "page_id": "PARENT_PAGE_ID"}, + "title": [{"type": "text", "text": {"content": "New Database"}}], + "properties": { + "Name": {"title": {}}, + "Status": {"select": {"options": [{"name": "Active"}, {"name": "Done"}]}} + } +} +``` + +**Important:** Cannot create databases via `/data_sources` endpoint. + +### Pages + +#### Get Page +```bash +GET /notion/v1/pages/{pageId} +Notion-Version: 2025-09-03 +``` + +#### Create Page in Data Source +Use `data_source_id` (not `database_id`) as parent: +```bash +POST /notion/v1/pages +Content-Type: application/json +Notion-Version: 2025-09-03 + +{ + "parent": {"data_source_id": "DATA_SOURCE_ID"}, + "properties": { + "Name": {"title": [{"text": {"content": "New Page"}}]}, + "Status": {"select": {"name": "Active"}} + } +} +``` + +#### Create Child Page (under another page) +```bash +POST /notion/v1/pages +Content-Type: application/json +Notion-Version: 2025-09-03 + +{ + "parent": {"page_id": "PARENT_PAGE_ID"}, + "properties": { + "title": {"title": [{"text": {"content": "Child Page"}}]} + } +} +``` + +#### Update Page Properties +```bash +PATCH /notion/v1/pages/{pageId} +Content-Type: application/json +Notion-Version: 2025-09-03 + +{ + "properties": { + "Status": {"select": {"name": "Done"}} + } +} +``` + +#### Archive Page +```bash +PATCH /notion/v1/pages/{pageId} +Content-Type: application/json +Notion-Version: 2025-09-03 + +{ + "archived": true +} +``` + +### Blocks + +#### Get Block +```bash +GET /notion/v1/blocks/{blockId} +Notion-Version: 2025-09-03 +``` + +#### Get Block Children +```bash +GET /notion/v1/blocks/{blockId}/children +Notion-Version: 2025-09-03 +``` + +#### Append Block Children +```bash +PATCH /notion/v1/blocks/{blockId}/children +Content-Type: application/json +Notion-Version: 2025-09-03 + +{ + "children": [ + { + "object": "block", + "type": "paragraph", + "paragraph": { + "rich_text": [{"type": "text", "text": {"content": "New paragraph"}}] + } + }, + { + "object": "block", + "type": "heading_2", + "heading_2": { + "rich_text": [{"type": "text", "text": {"content": "Heading"}}] + } + } + ] +} +``` + +#### Update Block +```bash +PATCH /notion/v1/blocks/{blockId} +Content-Type: application/json +Notion-Version: 2025-09-03 + +{ + "paragraph": { + "rich_text": [{"text": {"content": "Updated text"}}] + } +} +``` + +#### Delete Block +```bash +DELETE /notion/v1/blocks/{blockId} +Notion-Version: 2025-09-03 +``` + +### Users + +#### List Users +```bash +GET /notion/v1/users +Notion-Version: 2025-09-03 +``` + +#### Get User by ID +```bash +GET /notion/v1/users/{userId} +Notion-Version: 2025-09-03 +``` + +#### Get Current User (Bot) +```bash +GET /notion/v1/users/me +Notion-Version: 2025-09-03 +``` + +## Filter Operators + +- `equals`, `does_not_equal` +- `contains`, `does_not_contain` +- `starts_with`, `ends_with` +- `is_empty`, `is_not_empty` +- `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to` + +## Block Types + +Common block types for appending: +- `paragraph` - Text paragraph +- `heading_1`, `heading_2`, `heading_3` - Headings +- `bulleted_list_item`, `numbered_list_item` - List items +- `to_do` - Checkbox item +- `code` - Code block +- `quote` - Quote block +- `divider` - Horizontal divider + +## Migration from Older API Versions + +| Old (2022-06-28) | New (2025-09-03) | +|------------------|------------------| +| `POST /databases/{id}/query` | `POST /data_sources/{id}/query` | +| `GET /databases/{id}` for schema | `GET /data_sources/{id}` for schema | +| `PATCH /databases/{id}` for schema | `PATCH /data_sources/{id}` for schema | +| Parent: `{"database_id": "..."}` | Parent: `{"data_source_id": "..."}` | +| Search filter: `"database"` | Search filter: `"data_source"` | + +## Notes + +- Use `GET /databases/{id}` to discover `data_source_id`, then use `/data_sources/` for all operations +- Creating databases still requires `POST /databases` endpoint +- Parent objects for create database require `type` field: `{"type": "page_id", "page_id": "..."}` +- All IDs are UUIDs (with or without hyphens) +- Delete blocks returns the block with `archived: true` + +## Resources + +- [API Introduction](https://developers.notion.com/reference/intro) +- [Search](https://developers.notion.com/reference/post-search.md) +- [Query Database](https://developers.notion.com/reference/post-database-query.md) +- [Get Database](https://developers.notion.com/reference/retrieve-a-database.md) +- [Create Database](https://developers.notion.com/reference/create-a-database.md) +- [Get Page](https://developers.notion.com/reference/retrieve-a-page.md) +- [Create Page](https://developers.notion.com/reference/post-page.md) +- [Update Page](https://developers.notion.com/reference/patch-page.md) +- [Get Block Children](https://developers.notion.com/reference/get-block-children.md) +- [Append Block Children](https://developers.notion.com/reference/patch-block-children.md) +- [List Users](https://developers.notion.com/reference/get-users.md) +- [Filter Reference](https://developers.notion.com/reference/post-database-query-filter.md) +- [LLM Reference](https://developers.notion.com/llms.txt) +- [Version Reference](https://developers.notion.com/guides/get-started/upgrade-guide-2025-09-03) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/one-drive.md b/.agents/skills/api-gateway/references/one-drive.md new file mode 100644 index 00000000..cf9742bd --- /dev/null +++ b/.agents/skills/api-gateway/references/one-drive.md @@ -0,0 +1,118 @@ +# OneDrive Routing Reference + +**App name:** `one-drive` +**Base URL proxied:** `graph.microsoft.com` + +## API Path Pattern + +``` +/one-drive/v1.0/me/drive/{resource} +``` + +## Common Endpoints + +### Get User's Drive +```bash +GET /one-drive/v1.0/me/drive +``` + +### List Drives +```bash +GET /one-drive/v1.0/me/drives +``` + +### Get Drive Root +```bash +GET /one-drive/v1.0/me/drive/root +``` + +### List Root Children +```bash +GET /one-drive/v1.0/me/drive/root/children +``` + +### Get Item by ID +```bash +GET /one-drive/v1.0/me/drive/items/{item-id} +``` + +### Get Item by Path +```bash +GET /one-drive/v1.0/me/drive/root:/Documents/file.txt +``` + +### List Folder Children by Path +```bash +GET /one-drive/v1.0/me/drive/root:/Documents:/children +``` + +### Create Folder +```bash +POST /one-drive/v1.0/me/drive/root/children +Content-Type: application/json + +{ + "name": "New Folder", + "folder": {} +} +``` + +### Upload File (Simple - up to 4MB) +```bash +PUT /one-drive/v1.0/me/drive/root:/filename.txt:/content +Content-Type: text/plain + +{file content} +``` + +### Delete Item +```bash +DELETE /one-drive/v1.0/me/drive/items/{item-id} +``` + +### Create Sharing Link +```bash +POST /one-drive/v1.0/me/drive/items/{item-id}/createLink +Content-Type: application/json + +{ + "type": "view", + "scope": "anonymous" +} +``` + +### Search Files +```bash +GET /one-drive/v1.0/me/drive/root/search(q='query') +``` + +### Special Folders +```bash +GET /one-drive/v1.0/me/drive/special/documents +GET /one-drive/v1.0/me/drive/special/photos +``` + +### Recent Files +```bash +GET /one-drive/v1.0/me/drive/recent +``` + +### Shared With Me +```bash +GET /one-drive/v1.0/me/drive/sharedWithMe +``` + +## Notes + +- Authentication is automatic - the router injects the OAuth token +- Uses Microsoft Graph API (`graph.microsoft.com`) +- Use colon (`:`) syntax for path-based addressing +- Simple uploads limited to 4MB; use resumable upload for larger files +- Download URLs in `@microsoft.graph.downloadUrl` are pre-authenticated +- Supports OData query parameters: `$select`, `$expand`, `$filter`, `$orderby`, `$top` + +## Resources + +- [OneDrive Developer Documentation](https://learn.microsoft.com/en-us/onedrive/developer/) +- [Microsoft Graph API Reference](https://learn.microsoft.com/en-us/graph/api/overview) +- [DriveItem Resource](https://learn.microsoft.com/en-us/graph/api/resources/driveitem) diff --git a/.agents/skills/api-gateway/references/outlook.md b/.agents/skills/api-gateway/references/outlook.md new file mode 100644 index 00000000..a0733335 --- /dev/null +++ b/.agents/skills/api-gateway/references/outlook.md @@ -0,0 +1,238 @@ +# Outlook Routing Reference + +**App name:** `outlook` +**Base URL proxied:** `graph.microsoft.com` + +## API Path Pattern + +``` +/outlook/v1.0/me/{resource} +``` + +## Common Endpoints + +### User Profile +```bash +GET /outlook/v1.0/me +``` + +### Mail Folders + +#### List Mail Folders +```bash +GET /outlook/v1.0/me/mailFolders +``` + +Well-known folder names: `Inbox`, `Drafts`, `SentItems`, `DeletedItems`, `Archive`, `JunkEmail` + +#### Get Mail Folder +```bash +GET /outlook/v1.0/me/mailFolders/{folderId} +``` + +#### Create Mail Folder +```bash +POST /outlook/v1.0/me/mailFolders +Content-Type: application/json + +{ + "displayName": "My Folder" +} +``` + +### Messages + +#### List Messages +```bash +GET /outlook/v1.0/me/messages +``` + +From specific folder: +```bash +GET /outlook/v1.0/me/mailFolders/Inbox/messages +``` + +With filter: +```bash +GET /outlook/v1.0/me/messages?$filter=isRead eq false&$top=10 +``` + +#### Get Message +```bash +GET /outlook/v1.0/me/messages/{messageId} +``` + +#### Send Message +```bash +POST /outlook/v1.0/me/sendMail +Content-Type: application/json + +{ + "message": { + "subject": "Hello", + "body": { + "contentType": "Text", + "content": "This is the email body." + }, + "toRecipients": [ + { + "emailAddress": { + "address": "recipient@example.com" + } + } + ] + }, + "saveToSentItems": true +} +``` + +#### Create Draft +```bash +POST /outlook/v1.0/me/messages +Content-Type: application/json + +{ + "subject": "Hello", + "body": { + "contentType": "Text", + "content": "This is the email body." + }, + "toRecipients": [ + { + "emailAddress": { + "address": "recipient@example.com" + } + } + ] +} +``` + +#### Send Existing Draft +```bash +POST /outlook/v1.0/me/messages/{messageId}/send +``` + +#### Update Message (Mark as Read) +```bash +PATCH /outlook/v1.0/me/messages/{messageId} +Content-Type: application/json + +{ + "isRead": true +} +``` + +#### Delete Message +```bash +DELETE /outlook/v1.0/me/messages/{messageId} +``` + +#### Move Message +```bash +POST /outlook/v1.0/me/messages/{messageId}/move +Content-Type: application/json + +{ + "destinationId": "{folderId}" +} +``` + +### Calendar + +#### List Calendars +```bash +GET /outlook/v1.0/me/calendars +``` + +#### List Events +```bash +GET /outlook/v1.0/me/calendar/events +``` + +With filter: +```bash +GET /outlook/v1.0/me/calendar/events?$filter=start/dateTime ge '2024-01-01'&$top=10 +``` + +#### Create Event +```bash +POST /outlook/v1.0/me/calendar/events +Content-Type: application/json + +{ + "subject": "Meeting", + "start": { + "dateTime": "2024-01-15T10:00:00", + "timeZone": "UTC" + }, + "end": { + "dateTime": "2024-01-15T11:00:00", + "timeZone": "UTC" + }, + "attendees": [ + { + "emailAddress": { + "address": "attendee@example.com" + }, + "type": "required" + } + ] +} +``` + +#### Delete Event +```bash +DELETE /outlook/v1.0/me/events/{eventId} +``` + +### Contacts + +#### List Contacts +```bash +GET /outlook/v1.0/me/contacts +``` + +#### Create Contact +```bash +POST /outlook/v1.0/me/contacts +Content-Type: application/json + +{ + "givenName": "John", + "surname": "Doe", + "emailAddresses": [ + { + "address": "john.doe@example.com" + } + ] +} +``` + +#### Delete Contact +```bash +DELETE /outlook/v1.0/me/contacts/{contactId} +``` + +## OData Query Parameters + +- `$top=10` - Limit results +- `$skip=20` - Skip results (pagination) +- `$select=subject,from` - Select specific fields +- `$filter=isRead eq false` - Filter results +- `$orderby=receivedDateTime desc` - Sort results +- `$search="keyword"` - Search content + +## Notes + +- Use `me` as the user identifier for the authenticated user +- Message body content types: `Text` or `HTML` +- Well-known folder names work as folder IDs: `Inbox`, `Drafts`, `SentItems`, etc. +- Calendar events use ISO 8601 datetime format + +## Resources + +- [Microsoft Graph API Overview](https://learn.microsoft.com/en-us/graph/api/overview) +- [Mail API](https://learn.microsoft.com/en-us/graph/api/resources/mail-api-overview) +- [Calendar API](https://learn.microsoft.com/en-us/graph/api/resources/calendar) +- [Contacts API](https://learn.microsoft.com/en-us/graph/api/resources/contact) +- [Query Parameters](https://learn.microsoft.com/en-us/graph/query-parameters) diff --git a/.agents/skills/api-gateway/references/pipedrive.md b/.agents/skills/api-gateway/references/pipedrive.md new file mode 100644 index 00000000..02986000 --- /dev/null +++ b/.agents/skills/api-gateway/references/pipedrive.md @@ -0,0 +1,161 @@ +# Pipedrive Routing Reference + +**App name:** `pipedrive` +**Base URL proxied:** `api.pipedrive.com` + +## API Path Pattern + +``` +/pipedrive/api/v1/{resource} +``` + +## Common Endpoints + +### List Deals +```bash +GET /pipedrive/api/v1/deals?status=open&limit=50 +``` + +### Get Deal +```bash +GET /pipedrive/api/v1/deals/{id} +``` + +### Create Deal +```bash +POST /pipedrive/api/v1/deals +Content-Type: application/json + +{ + "title": "New Enterprise Deal", + "value": 50000, + "currency": "USD", + "person_id": 123, + "org_id": 456, + "stage_id": 1, + "expected_close_date": "2025-06-30" +} +``` + +### Update Deal +```bash +PUT /pipedrive/api/v1/deals/{id} +Content-Type: application/json + +{ + "title": "Updated Deal Title", + "value": 75000, + "status": "won" +} +``` + +### Delete Deal +```bash +DELETE /pipedrive/api/v1/deals/{id} +``` + +### Search Deals +```bash +GET /pipedrive/api/v1/deals/search?term=enterprise +``` + +### List Persons +```bash +GET /pipedrive/api/v1/persons +``` + +### Create Person +```bash +POST /pipedrive/api/v1/persons +Content-Type: application/json + +{ + "name": "John Doe", + "email": ["john@example.com"], + "phone": ["+1234567890"], + "org_id": 456 +} +``` + +### List Organizations +```bash +GET /pipedrive/api/v1/organizations +``` + +### Create Organization +```bash +POST /pipedrive/api/v1/organizations +Content-Type: application/json + +{ + "name": "Acme Corporation", + "address": "123 Main St, City, Country" +} +``` + +### List Activities +```bash +GET /pipedrive/api/v1/activities?type=call&done=0 +``` + +### Create Activity +```bash +POST /pipedrive/api/v1/activities +Content-Type: application/json + +{ + "subject": "Follow-up call", + "type": "call", + "due_date": "2025-03-15", + "due_time": "14:00", + "deal_id": 789, + "person_id": 123 +} +``` + +### List Pipelines +```bash +GET /pipedrive/api/v1/pipelines +``` + +### List Stages +```bash +GET /pipedrive/api/v1/stages?pipeline_id=1 +``` + +### Create Note +```bash +POST /pipedrive/api/v1/notes +Content-Type: application/json + +{ + "content": "Meeting notes: Discussed pricing and timeline", + "deal_id": 789, + "pinned_to_deal_flag": 1 +} +``` + +### Get Current User +```bash +GET /pipedrive/api/v1/users/me +``` + +## Notes + +- IDs are integers +- Email and phone fields accept arrays for multiple values +- `visible_to` values: 1 (owner only), 3 (entire company), 5 (owner's visibility group), 7 (entire company and visibility group) +- Deal status: `open`, `won`, `lost`, `deleted` +- Use `start` and `limit` for pagination +- Custom fields are supported via their API key (e.g., `abc123_custom_field`) + +## Resources + +- [Pipedrive API Overview](https://developers.pipedrive.com/docs/api/v1) +- [Deals](https://developers.pipedrive.com/docs/api/v1/Deals) +- [Persons](https://developers.pipedrive.com/docs/api/v1/Persons) +- [Organizations](https://developers.pipedrive.com/docs/api/v1/Organizations) +- [Activities](https://developers.pipedrive.com/docs/api/v1/Activities) +- [Pipelines](https://developers.pipedrive.com/docs/api/v1/Pipelines) +- [Stages](https://developers.pipedrive.com/docs/api/v1/Stages) +- [Notes](https://developers.pipedrive.com/docs/api/v1/Notes) diff --git a/.agents/skills/api-gateway/references/quickbooks.md b/.agents/skills/api-gateway/references/quickbooks.md new file mode 100644 index 00000000..6d6b8aaf --- /dev/null +++ b/.agents/skills/api-gateway/references/quickbooks.md @@ -0,0 +1,384 @@ +# QuickBooks Routing Reference + +**App name:** `quickbooks` +**Base URL proxied:** `quickbooks.api.intuit.com` + +## Special Handling + +Use `:realmId` in the path and it will be automatically replaced with the connected company's realm ID. + +## API Path Pattern + +``` +/quickbooks/v3/company/:realmId/{endpoint} +``` + +## Common Endpoints + +### Company Info + +#### Get Company Info +```bash +GET /quickbooks/v3/company/:realmId/companyinfo/:realmId +``` + +#### Get Preferences +```bash +GET /quickbooks/v3/company/:realmId/preferences +``` + +### Customers + +#### Query Customers +```bash +GET /quickbooks/v3/company/:realmId/query?query=SELECT%20*%20FROM%20Customer%20MAXRESULTS%20100 +``` + +With filter: +```bash +GET /quickbooks/v3/company/:realmId/query?query=SELECT%20*%20FROM%20Customer%20WHERE%20Active%3Dtrue +``` + +#### Get Customer +```bash +GET /quickbooks/v3/company/:realmId/customer/{customerId} +``` + +#### Create Customer +```bash +POST /quickbooks/v3/company/:realmId/customer +Content-Type: application/json + +{ + "DisplayName": "John Doe", + "PrimaryEmailAddr": {"Address": "john@example.com"}, + "PrimaryPhone": {"FreeFormNumber": "555-1234"} +} +``` + +#### Update Customer +Requires `Id` and `SyncToken` from previous GET: +```bash +POST /quickbooks/v3/company/:realmId/customer +Content-Type: application/json + +{ + "Id": "123", + "SyncToken": "0", + "DisplayName": "John Doe Updated", + "PrimaryPhone": {"FreeFormNumber": "555-9999"} +} +``` + +#### Deactivate Customer (Soft Delete) +```bash +POST /quickbooks/v3/company/:realmId/customer +Content-Type: application/json + +{ + "Id": "123", + "SyncToken": "1", + "DisplayName": "John Doe", + "Active": false +} +``` + +### Vendors + +#### Query Vendors +```bash +GET /quickbooks/v3/company/:realmId/query?query=SELECT%20*%20FROM%20Vendor%20MAXRESULTS%20100 +``` + +#### Get Vendor +```bash +GET /quickbooks/v3/company/:realmId/vendor/{vendorId} +``` + +#### Create Vendor +```bash +POST /quickbooks/v3/company/:realmId/vendor +Content-Type: application/json + +{ + "DisplayName": "Acme Supplies", + "PrimaryEmailAddr": {"Address": "vendor@example.com"} +} +``` + +### Items (Products/Services) + +#### Query Items +```bash +GET /quickbooks/v3/company/:realmId/query?query=SELECT%20*%20FROM%20Item%20MAXRESULTS%20100 +``` + +#### Get Item +```bash +GET /quickbooks/v3/company/:realmId/item/{itemId} +``` + +#### Create Item +```bash +POST /quickbooks/v3/company/:realmId/item +Content-Type: application/json + +{ + "Name": "Consulting Services", + "Type": "Service", + "IncomeAccountRef": {"value": "1"} +} +``` + +### Invoices + +#### Query Invoices +```bash +GET /quickbooks/v3/company/:realmId/query?query=SELECT%20*%20FROM%20Invoice%20MAXRESULTS%20100 +``` + +#### Get Invoice +```bash +GET /quickbooks/v3/company/:realmId/invoice/{invoiceId} +``` + +#### Create Invoice +```bash +POST /quickbooks/v3/company/:realmId/invoice +Content-Type: application/json + +{ + "CustomerRef": {"value": "123"}, + "Line": [ + { + "Amount": 100.00, + "DetailType": "SalesItemLineDetail", + "SalesItemLineDetail": { + "ItemRef": {"value": "1"}, + "Qty": 1 + } + } + ] +} +``` + +#### Void Invoice +```bash +POST /quickbooks/v3/company/:realmId/invoice?operation=void +Content-Type: application/json + +{ + "Id": "123", + "SyncToken": "0" +} +``` + +#### Delete Invoice +```bash +POST /quickbooks/v3/company/:realmId/invoice?operation=delete +Content-Type: application/json + +{ + "Id": "123", + "SyncToken": "0" +} +``` + +### Payments + +#### Query Payments +```bash +GET /quickbooks/v3/company/:realmId/query?query=SELECT%20*%20FROM%20Payment%20MAXRESULTS%20100 +``` + +#### Create Payment +Simple payment: +```bash +POST /quickbooks/v3/company/:realmId/payment +Content-Type: application/json + +{ + "CustomerRef": {"value": "123"}, + "TotalAmt": 100.00 +} +``` + +Payment linked to invoice: +```bash +POST /quickbooks/v3/company/:realmId/payment +Content-Type: application/json + +{ + "CustomerRef": {"value": "123"}, + "TotalAmt": 100.00, + "Line": [ + { + "Amount": 100.00, + "LinkedTxn": [{"TxnId": "456", "TxnType": "Invoice"}] + } + ] +} +``` + +### Bills + +#### Query Bills +```bash +GET /quickbooks/v3/company/:realmId/query?query=SELECT%20*%20FROM%20Bill%20MAXRESULTS%20100 +``` + +#### Create Bill +```bash +POST /quickbooks/v3/company/:realmId/bill +Content-Type: application/json + +{ + "VendorRef": {"value": "123"}, + "Line": [ + { + "DetailType": "AccountBasedExpenseLineDetail", + "Amount": 250.00, + "AccountBasedExpenseLineDetail": { + "AccountRef": {"value": "1"} + } + } + ] +} +``` + +### Bill Payments + +#### Create Bill Payment +```bash +POST /quickbooks/v3/company/:realmId/billpayment +Content-Type: application/json + +{ + "VendorRef": {"value": "123"}, + "TotalAmt": 250.00, + "PayType": "Check", + "CheckPayment": { + "BankAccountRef": {"value": "23"} + }, + "Line": [ + { + "Amount": 250.00, + "LinkedTxn": [{"TxnId": "456", "TxnType": "Bill"}] + } + ] +} +``` + +**Note:** Use a Bank account (AccountType: "Bank") for `BankAccountRef`. + +### Accounts + +#### Query Accounts +```bash +GET /quickbooks/v3/company/:realmId/query?query=SELECT%20*%20FROM%20Account +``` + +Filter by type: +```bash +GET /quickbooks/v3/company/:realmId/query?query=SELECT%20*%20FROM%20Account%20WHERE%20AccountType%20%3D%20%27Bank%27 +``` + +### Reports + +#### Profit and Loss +```bash +GET /quickbooks/v3/company/:realmId/reports/ProfitAndLoss?start_date=2024-01-01&end_date=2024-12-31 +``` + +#### Balance Sheet +```bash +GET /quickbooks/v3/company/:realmId/reports/BalanceSheet?date=2024-12-31 +``` + +### Batch Operations + +Execute multiple queries in a single request: +```bash +POST /quickbooks/v3/company/:realmId/batch +Content-Type: application/json + +{ + "BatchItemRequest": [ + {"bId": "1", "Query": "SELECT * FROM Customer MAXRESULTS 2"}, + {"bId": "2", "Query": "SELECT * FROM Vendor MAXRESULTS 2"} + ] +} +``` + +## Query Language + +QuickBooks uses a SQL-like query language: +```sql +SELECT * FROM Customer WHERE DisplayName LIKE 'John%' MAXRESULTS 100 +``` + +Operators: `=`, `LIKE`, `<`, `>`, `<=`, `>=`, `IN` + +## SyncToken for Updates + +All update operations require the current `SyncToken` from the entity. The SyncToken is incremented after each successful update. + +1. GET the entity to retrieve current `SyncToken` +2. Include `Id` and `SyncToken` in the POST body +3. If the SyncToken doesn't match, the update fails (optimistic locking) + +## Void vs Delete + +- **Void**: Sets transaction amount to 0, adds "Voided" note, keeps record. Use for audit trail. +- **Delete**: Permanently removes the transaction. Use `?operation=delete` query parameter. + +Both require `Id` and `SyncToken` in the request body. + +## Notes + +- `:realmId` is automatically replaced by the router +- All queries must be URL-encoded +- Use `MAXRESULTS` to limit query results (default varies by entity) +- Include `SyncToken` when updating entities (for optimistic locking) +- Dates are in `YYYY-MM-DD` format +- Soft delete entities (Customer, Vendor, Item) by setting `Active: false` +- Transactions (Invoice, Payment, Bill) can be voided or deleted + +## Resources + +- [API Overview](https://developer.intuit.com/app/developer/qbo/docs/get-started) +- [Query Customers](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#query-a-customer) +- [Get Customer](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#read-a-customer) +- [Create Customer](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#create-a-customer) +- [Update Customer](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/customer#full-update-a-customer) +- [Query Invoices](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#query-an-invoice) +- [Get Invoice](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#read-an-invoice) +- [Create Invoice](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#create-an-invoice) +- [Update Invoice](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#full-update-an-invoice) +- [Delete Invoice](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#delete-an-invoice) +- [Send Invoice](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice#send-an-invoice) +- [Query Items](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#query-an-item) +- [Get Item](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#read-an-item) +- [Create Item](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#create-an-item) +- [Update Item](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/item#full-update-an-item) +- [Query Accounts](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#query-an-account) +- [Get Account](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#read-an-account) +- [Create Account](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#create-an-account) +- [Update Account](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#full-update-an-account) +- [Query Payments](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment#query-a-payment) +- [Get Payment](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment#read-a-payment) +- [Create Payment](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment#create-a-payment) +- [Update Payment](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment#full-update-a-payment) +- [Delete Payment](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment#delete-a-payment) +- [Query Vendors](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/vendor#query-a-vendor) +- [Get Vendor](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/vendor#read-a-vendor) +- [Create Vendor](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/vendor#create-a-vendor) +- [Update Vendor](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/vendor#full-update-a-vendor) +- [Query Bills](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/bill#query-a-bill) +- [Get Bill](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/bill#read-a-bill) +- [Create Bill](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/bill#create-a-bill) +- [Update Bill](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/bill#full-update-a-bill) +- [Delete Bill](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/bill#delete-a-bill) +- [Profit and Loss Report](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/report-entities/profitandloss) +- [Balance Sheet Report](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/report-entities/balancesheet) +- [Query Reference](https://developer.intuit.com/app/developer/qbdesktop/docs/develop/exploring-the-quickbooks-desktop-sdk/query-requests-and-responses) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/quo.md b/.agents/skills/api-gateway/references/quo.md new file mode 100644 index 00000000..d95fd986 --- /dev/null +++ b/.agents/skills/api-gateway/references/quo.md @@ -0,0 +1,188 @@ +# Quo Routing Reference + +**App name:** `quo` +**Base URL proxied:** `api.openphone.com` + +## API Path Pattern + +``` +/quo/v1/{resource} +``` + +## Common Endpoints + +### Phone Numbers + +#### List Phone Numbers +```bash +GET /quo/v1/phone-numbers +``` + +### Users + +#### List Users +```bash +GET /quo/v1/users?maxResults=50 +``` + +#### Get User +```bash +GET /quo/v1/users/{userId} +``` + +### Messages + +#### Send Text Message +```bash +POST /quo/v1/messages +Content-Type: application/json + +{ + "content": "Hello, world!", + "from": "PN123abc", + "to": ["+15555555555"] +} +``` + +#### List Messages +```bash +GET /quo/v1/messages?phoneNumberId=PN123abc&participants[]=+15555555555&maxResults=100 +``` + +#### Get Message +```bash +GET /quo/v1/messages/{messageId} +``` + +### Calls + +#### List Calls +```bash +GET /quo/v1/calls?phoneNumberId=PN123abc&participants[]=+15555555555&maxResults=100 +``` + +#### Get Call +```bash +GET /quo/v1/calls/{callId} +``` + +#### Get Call Recordings +```bash +GET /quo/v1/call-recordings/{callId} +``` + +#### Get Call Summary +```bash +GET /quo/v1/call-summaries/{callId} +``` + +#### Get Call Transcript +```bash +GET /quo/v1/call-transcripts/{callId} +``` + +#### Get Call Voicemail +```bash +GET /quo/v1/call-voicemails/{callId} +``` + +### Contacts + +#### List Contacts +```bash +GET /quo/v1/contacts?maxResults=50 +``` + +#### Get Contact +```bash +GET /quo/v1/contacts/{contactId} +``` + +#### Create Contact +```bash +POST /quo/v1/contacts +Content-Type: application/json + +{ + "defaultFields": { + "firstName": "Jane", + "lastName": "Doe", + "phoneNumbers": [{"name": "mobile", "value": "+15555555555"}] + } +} +``` + +#### Update Contact +```bash +PATCH /quo/v1/contacts/{contactId} +Content-Type: application/json + +{ + "defaultFields": { + "company": "New Company" + } +} +``` + +#### Delete Contact +```bash +DELETE /quo/v1/contacts/{contactId} +``` + +#### Get Contact Custom Fields +```bash +GET /quo/v1/contact-custom-fields +``` + +### Conversations + +#### List Conversations +```bash +GET /quo/v1/conversations?maxResults=100 +``` + +### Webhooks + +#### List Webhooks +```bash +GET /quo/v1/webhooks +``` + +#### Get Webhook +```bash +GET /quo/v1/webhooks/{webhookId} +``` + +#### Create Webhook +```bash +POST /quo/v1/webhooks +Content-Type: application/json + +{ + "url": "https://your-webhook-url.com/calls", + "resourceType": "call" +} +``` + +Resource types: `call`, `message`, `callSummary`, `callTranscript` + +#### Delete Webhook +```bash +DELETE /quo/v1/webhooks/{webhookId} +``` + +## Notes + +- Phone number IDs start with `PN` +- User IDs start with `US` +- Call/Message IDs start with `AC` +- Phone numbers must be in E.164 format (e.g., `+15555555555`) +- Uses token-based pagination with `pageToken` parameter +- Maximum 1600 characters per SMS message +- List calls requires exactly 1 participant (1:1 conversations only) + +## Resources + +- [Quo API Introduction](https://www.quo.com/docs/mdx/api-reference/introduction) +- [Quo API Authentication](https://www.quo.com/docs/mdx/api-reference/authentication) +- [Quo Support Center](https://support.quo.com/core-concepts/integrations/api) diff --git a/.agents/skills/api-gateway/references/salesforce.md b/.agents/skills/api-gateway/references/salesforce.md new file mode 100644 index 00000000..f478bf1b --- /dev/null +++ b/.agents/skills/api-gateway/references/salesforce.md @@ -0,0 +1,193 @@ +# Salesforce Routing Reference + +**App name:** `salesforce` +**Base URL proxied:** `{instance}.salesforce.com` + +The router automatically determines the instance URL from your OAuth credentials (`instance_url` from the token response). + +## API Path Pattern + +``` +/salesforce/services/data/v59.0/{endpoint} +``` + +## Common Endpoints + +### SOQL Query +```bash +GET /salesforce/services/data/v59.0/query?q=SELECT+Id,Name+FROM+Contact+LIMIT+10 +``` + +Complex query: +```bash +GET /salesforce/services/data/v59.0/query?q=SELECT+Id,Name,Email+FROM+Contact+WHERE+Email+LIKE+'%example.com'+ORDER+BY+CreatedDate+DESC +``` + +### Get Object +```bash +GET /salesforce/services/data/v59.0/sobjects/{objectType}/{recordId} +``` + +Example: +```bash +GET /salesforce/services/data/v59.0/sobjects/Contact/003XXXXXXXXXXXXXXX +``` + +### Create Object +```bash +POST /salesforce/services/data/v59.0/sobjects/{objectType} +Content-Type: application/json + +{ + "FirstName": "John", + "LastName": "Doe", + "Email": "john@example.com" +} +``` + +### Update Object +```bash +PATCH /salesforce/services/data/v59.0/sobjects/{objectType}/{recordId} +Content-Type: application/json + +{ + "Phone": "+1234567890" +} +``` + +### Delete Object +```bash +DELETE /salesforce/services/data/v59.0/sobjects/{objectType}/{recordId} +``` + +### Describe Object (get schema) +```bash +GET /salesforce/services/data/v59.0/sobjects/{objectType}/describe +``` + +### List Objects +```bash +GET /salesforce/services/data/v59.0/sobjects +``` + +### Search (SOSL) +```bash +GET /salesforce/services/data/v59.0/search?q=FIND+{searchTerm}+IN+ALL+FIELDS+RETURNING+Contact(Id,Name) +``` + +### Composite Request (batch multiple operations) +```bash +POST /salesforce/services/data/v59.0/composite +Content-Type: application/json + +{ + "compositeRequest": [ + { + "method": "GET", + "url": "/services/data/v59.0/sobjects/Contact/003XXXXXXX", + "referenceId": "contact1" + }, + { + "method": "GET", + "url": "/services/data/v59.0/sobjects/Account/001XXXXXXX", + "referenceId": "account1" + } + ] +} +``` + +### Composite Batch Request +```bash +POST /salesforce/services/data/v59.0/composite/batch +Content-Type: application/json + +{ + "batchRequests": [ + {"method": "GET", "url": "v59.0/sobjects/Contact/003XXXXXXX"}, + {"method": "GET", "url": "v59.0/sobjects/Account/001XXXXXXX"} + ] +} +``` + +### sObject Collections Create (batch create) +```bash +POST /salesforce/services/data/v59.0/composite/sobjects +Content-Type: application/json + +{ + "allOrNone": true, + "records": [ + {"attributes": {"type": "Contact"}, "FirstName": "John", "LastName": "Doe"}, + {"attributes": {"type": "Contact"}, "FirstName": "Jane", "LastName": "Smith"} + ] +} +``` + +### sObject Collections Delete (batch delete) +```bash +DELETE /salesforce/services/data/v59.0/composite/sobjects?ids=003XXXXX,003YYYYY&allOrNone=true +``` + +### Get Updated Records +```bash +GET /salesforce/services/data/v59.0/sobjects/{objectType}/updated/?start=2026-01-30T00:00:00Z&end=2026-02-01T00:00:00Z +``` + +### Get Deleted Records +```bash +GET /salesforce/services/data/v59.0/sobjects/{objectType}/deleted/?start=2026-01-30T00:00:00Z&end=2026-02-01T00:00:00Z +``` + +### Get API Limits +```bash +GET /salesforce/services/data/v59.0/limits +``` + +### List API Versions +```bash +GET /salesforce/services/data/ +``` + +## Common Objects + +- `Account` - Companies/Organizations +- `Contact` - People associated with accounts +- `Lead` - Potential customers +- `Opportunity` - Sales deals +- `Case` - Support cases +- `Task` - To-do items +- `Event` - Calendar events + +## Notes + +- Use URL encoding for SOQL queries (spaces become `+`) +- Record IDs are 15 or 18 character alphanumeric strings +- API version (v59.0) can be adjusted; latest is v65.0 +- Update and Delete operations return HTTP 204 (no content) on success +- Dates for updated/deleted queries use ISO 8601 format: `YYYY-MM-DDTHH:MM:SSZ` +- Use `allOrNone: true` in batch operations for atomic transactions + +## Resources + +- [REST API Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_rest.htm) +- [List sObjects](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_describeGlobal.htm) +- [Describe sObject](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_describe.htm) +- [Get Record](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_retrieve_get.htm) +- [Get Record by External ID](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_upsert_get.htm) +- [Create Record](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_sobject_create.htm) +- [Update Record](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_update_fields.htm) +- [Delete Record](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_delete_record.htm) +- [Upsert Record](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_upsert.htm) +- [Query Records (SOQL)](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_query.htm) +- [Get Updated Records](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_getupdated.htm) +- [Get Deleted Records](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_getdeleted.htm) +- [Composite Request](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_composite_post.htm) +- [Composite Batch Request](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/requests_composite_batch.htm) +- [Composite Batch Response](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/responses_composite_batch.htm) +- [Composite Graph](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_graph.htm) +- [sObject Collections Create](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_sobjects_collections_create.htm) +- [sObject Collections Update](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_sobjects_collections_update.htm) +- [sObject Collections Delete](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_sobjects_collections_delete.htm) +- [SOQL Reference](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql.htm) +- [SOSL Reference](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_sosl.htm) +- [API Resources List](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_list.htm) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/sendgrid.md b/.agents/skills/api-gateway/references/sendgrid.md new file mode 100644 index 00000000..5e1e4fa0 --- /dev/null +++ b/.agents/skills/api-gateway/references/sendgrid.md @@ -0,0 +1,139 @@ +# SendGrid Routing Reference + +**App name:** `sendgrid` +**Base URL proxied:** `api.sendgrid.com` + +## API Path Pattern + +``` +/sendgrid/v3/{resource} +``` + +## Common Endpoints + +### Mail Send + +```bash +POST /sendgrid/v3/mail/send +Content-Type: application/json + +{ + "personalizations": [{"to": [{"email": "recipient@example.com"}], "subject": "Hello"}], + "from": {"email": "sender@example.com"}, + "content": [{"type": "text/plain", "value": "Hello World"}] +} +``` + +### User Profile + +```bash +GET /sendgrid/v3/user/profile +GET /sendgrid/v3/user/account +``` + +### Marketing Contacts + +```bash +GET /sendgrid/v3/marketing/contacts +PUT /sendgrid/v3/marketing/contacts +DELETE /sendgrid/v3/marketing/contacts?ids=id1,id2 +POST /sendgrid/v3/marketing/contacts/search +``` + +### Marketing Lists + +```bash +GET /sendgrid/v3/marketing/lists +POST /sendgrid/v3/marketing/lists +GET /sendgrid/v3/marketing/lists/{list_id} +PATCH /sendgrid/v3/marketing/lists/{list_id} +DELETE /sendgrid/v3/marketing/lists/{list_id} +``` + +### Segments + +```bash +GET /sendgrid/v3/marketing/segments +POST /sendgrid/v3/marketing/segments +DELETE /sendgrid/v3/marketing/segments/{segment_id} +``` + +### Templates + +```bash +GET /sendgrid/v3/templates +POST /sendgrid/v3/templates +GET /sendgrid/v3/templates/{template_id} +PATCH /sendgrid/v3/templates/{template_id} +DELETE /sendgrid/v3/templates/{template_id} +``` + +### Senders + +```bash +GET /sendgrid/v3/senders +POST /sendgrid/v3/senders +PATCH /sendgrid/v3/senders/{sender_id} +DELETE /sendgrid/v3/senders/{sender_id} +``` + +### Suppressions + +```bash +GET /sendgrid/v3/suppression/bounces +GET /sendgrid/v3/suppression/blocks +GET /sendgrid/v3/suppression/invalid_emails +GET /sendgrid/v3/suppression/spam_reports +GET /sendgrid/v3/suppression/unsubscribes +``` + +### Unsubscribe Groups (ASM) + +```bash +GET /sendgrid/v3/asm/groups +POST /sendgrid/v3/asm/groups +PATCH /sendgrid/v3/asm/groups/{group_id} +DELETE /sendgrid/v3/asm/groups/{group_id} +``` + +### Statistics + +```bash +GET /sendgrid/v3/stats?start_date=2026-02-01 +GET /sendgrid/v3/categories/stats?start_date=2026-02-01&categories=cat1 +GET /sendgrid/v3/mailbox_providers/stats?start_date=2026-02-01 +``` + +### API Keys + +```bash +GET /sendgrid/v3/api_keys +POST /sendgrid/v3/api_keys +PATCH /sendgrid/v3/api_keys/{api_key_id} +DELETE /sendgrid/v3/api_keys/{api_key_id} +``` + +## Pagination + +Marketing endpoints use token-based pagination: +```bash +GET /sendgrid/v3/marketing/lists?page_size=100&page_token={token} +``` + +Suppression endpoints use offset pagination: +```bash +GET /sendgrid/v3/suppression/bounces?limit=100&offset=0 +``` + +## Notes + +- All requests use JSON content type +- Dates are in YYYY-MM-DD format +- Mail send returns 202 Accepted on success +- Dynamic template IDs start with `d-` +- Marketing contact operations are asynchronous + +## Resources + +- [SendGrid API Documentation](https://www.twilio.com/docs/sendgrid/api-reference) +- [Mail Send API](https://www.twilio.com/docs/sendgrid/api-reference/mail-send) diff --git a/.agents/skills/api-gateway/references/signnow.md b/.agents/skills/api-gateway/references/signnow.md new file mode 100644 index 00000000..777453c2 --- /dev/null +++ b/.agents/skills/api-gateway/references/signnow.md @@ -0,0 +1,96 @@ +# SignNow Routing Reference + +**App name:** `signnow` +**Base URL proxied:** `api.signnow.com` + +## API Path Pattern + +``` +/signnow/{resource} +``` + +## Common Endpoints + +### User + +```bash +GET /signnow/user +GET /signnow/user/documents +``` + +### Documents + +```bash +# Upload document (multipart form data) +POST /signnow/document + +# Get document +GET /signnow/document/{document_id} + +# Update document +PUT /signnow/document/{document_id} + +# Download document +GET /signnow/document/{document_id}/download?type=collapsed + +# Get document history +GET /signnow/document/{document_id}/historyfull + +# Move document to folder +POST /signnow/document/{document_id}/move + +# Merge documents (returns PDF) +POST /signnow/document/merge + +# Delete document +DELETE /signnow/document/{document_id} +``` + +### Templates + +```bash +# Create template from document +POST /signnow/template + +# Create document from template +POST /signnow/template/{template_id}/copy +``` + +### Invites + +```bash +# Send freeform invite +POST /signnow/document/{document_id}/invite + +# Create signing link (requires document fields) +POST /signnow/link +``` + +### Folders + +```bash +GET /signnow/folder +GET /signnow/folder/{folder_id} +``` + +### Webhooks (Event Subscriptions) + +```bash +GET /signnow/event_subscription +POST /signnow/event_subscription +DELETE /signnow/event_subscription/{subscription_id} +``` + +## Notes + +- Documents must be uploaded as multipart form data with PDF file +- Supported file types: PDF, DOC, DOCX, ODT, RTF, PNG, JPG +- System folders cannot be renamed or deleted +- Creating signing links requires documents to have signature fields +- Custom invite subject/message requires paid subscription +- Rate limit in development mode: 500 requests/hour per application + +## Resources + +- [SignNow API Reference](https://docs.signnow.com/docs/signnow/reference) +- [SignNow Developer Portal](https://www.signnow.com/developers) diff --git a/.agents/skills/api-gateway/references/slack.md b/.agents/skills/api-gateway/references/slack.md new file mode 100644 index 00000000..4d907b15 --- /dev/null +++ b/.agents/skills/api-gateway/references/slack.md @@ -0,0 +1,178 @@ +# Slack Routing Reference + +**App name:** `slack` +**Base URL proxied:** `slack.com` + +## API Path Pattern + +``` +/slack/api/{method} +``` + +## Common Endpoints + +### Post Message +```bash +POST /slack/api/chat.postMessage +Content-Type: application/json + +{ + "channel": "C0123456789", + "text": "Hello, world!" +} +``` + +With blocks: +```bash +POST /slack/api/chat.postMessage +Content-Type: application/json + +{ + "channel": "C0123456789", + "blocks": [ + {"type": "section", "text": {"type": "mrkdwn", "text": "*Bold* and _italic_"}} + ] +} +``` + +### List Channels +```bash +GET /slack/api/conversations.list?types=public_channel,private_channel +``` + +### Get Channel Info +```bash +GET /slack/api/conversations.info?channel=C0123456789 +``` + +### List Messages in Channel +```bash +GET /slack/api/conversations.history?channel=C0123456789&limit=100 +``` + +### Get Thread Replies +```bash +GET /slack/api/conversations.replies?channel=C0123456789&ts=1234567890.123456 +``` + +### List Users +```bash +GET /slack/api/users.list +``` + +### Get User Info +```bash +GET /slack/api/users.info?user=U0123456789 +``` + +### Search Messages +```bash +GET /slack/api/search.messages?query=keyword +``` + +### Upload File +```bash +POST /slack/api/files.upload +Content-Type: multipart/form-data + +channels=C0123456789 +content=file content here +filename=example.txt +``` + +### Add Reaction +```bash +POST /slack/api/reactions.add +Content-Type: application/json + +{ + "channel": "C0123456789", + "name": "thumbsup", + "timestamp": "1234567890.123456" +} +``` + +### Update Message +```bash +POST /slack/api/chat.update +Content-Type: application/json + +{ + "channel": "C0123456789", + "ts": "1234567890.123456", + "text": "Updated message" +} +``` + +### Delete Message +```bash +POST /slack/api/chat.delete +Content-Type: application/json + +{ + "channel": "C0123456789", + "ts": "1234567890.123456" +} +``` + +### Post Thread Reply +```bash +POST /slack/api/chat.postMessage +Content-Type: application/json + +{ + "channel": "C0123456789", + "thread_ts": "1234567890.123456", + "text": "This is a reply in a thread" +} +``` + +### Get Channel Members +```bash +GET /slack/api/conversations.members?channel=C0123456789&limit=100 +``` + +### Open DM Conversation +```bash +POST /slack/api/conversations.open +Content-Type: application/json + +{ + "users": "U0123456789" +} +``` + +### Auth Test (get current user/team) +```bash +GET /slack/api/auth.test +``` + +## Notes + +- Authentication is automatic - the router uses the user's OAuth access token +- Channel IDs start with `C` (public), `G` (private/group), or `D` (DM) +- User IDs start with `U`, Team IDs start with `T` +- Message timestamps (`ts`) are used as unique identifiers +- Use `mrkdwn` type for Slack-flavored markdown formatting +- Thread replies use `thread_ts` to reference the parent message + +## Resources + +- [API Overview](https://api.slack.com/apis) +- [Post Message](https://api.slack.com/methods/chat.postMessage) +- [Update Message](https://api.slack.com/methods/chat.update) +- [Delete Message](https://api.slack.com/methods/chat.delete) +- [List Channels](https://api.slack.com/methods/conversations.list) +- [Get Channel Info](https://api.slack.com/methods/conversations.info) +- [Get Channel Members](https://api.slack.com/methods/conversations.members) +- [Open Conversation](https://api.slack.com/methods/conversations.open) +- [Channel History](https://api.slack.com/methods/conversations.history) +- [Thread Replies](https://api.slack.com/methods/conversations.replies) +- [List Users](https://api.slack.com/methods/users.list) +- [Get User Info](https://api.slack.com/methods/users.info) +- [Auth Test](https://api.slack.com/methods/auth.test) +- [Search Messages](https://api.slack.com/methods/search.messages) +- [Upload File](https://api.slack.com/methods/files.upload) +- [Add Reaction](https://api.slack.com/methods/reactions.add) +- [Block Kit Reference](https://api.slack.com/reference/block-kit) +- [LLM Reference](https://docs.slack.dev/llms.txt) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/squareup.md b/.agents/skills/api-gateway/references/squareup.md new file mode 100644 index 00000000..7a9ad1e3 --- /dev/null +++ b/.agents/skills/api-gateway/references/squareup.md @@ -0,0 +1,262 @@ +# Square Routing Reference + +**App name:** `squareup` +**Base URL proxied:** `connect.squareup.com` + +## API Path Pattern + +``` +/squareup/v2/{resource} +``` + +## Common Endpoints + +### Locations + +#### List Locations +```bash +GET /squareup/v2/locations +``` + +#### Get Location +```bash +GET /squareup/v2/locations/{location_id} +``` + +#### Create Location +```bash +POST /squareup/v2/locations +Content-Type: application/json + +{ + "location": { + "name": "New Location", + "address": {...} + } +} +``` + +### Merchants + +#### Get Current Merchant +```bash +GET /squareup/v2/merchants/me +``` + +### Payments + +#### List Payments +```bash +GET /squareup/v2/payments +GET /squareup/v2/payments?location_id={location_id}&begin_time=2026-01-01T00:00:00Z +``` + +#### Get Payment +```bash +GET /squareup/v2/payments/{payment_id} +``` + +#### Create Payment +```bash +POST /squareup/v2/payments +Content-Type: application/json + +{ + "source_id": "cnon:card-nonce-ok", + "idempotency_key": "unique-key", + "amount_money": {"amount": 1000, "currency": "USD"}, + "location_id": "{location_id}" +} +``` + +#### Complete Payment +```bash +POST /squareup/v2/payments/{payment_id}/complete +``` + +#### Cancel Payment +```bash +POST /squareup/v2/payments/{payment_id}/cancel +``` + +### Refunds + +#### List Refunds +```bash +GET /squareup/v2/refunds +``` + +#### Create Refund +```bash +POST /squareup/v2/refunds +Content-Type: application/json + +{ + "idempotency_key": "unique-key", + "payment_id": "{payment_id}", + "amount_money": {"amount": 500, "currency": "USD"} +} +``` + +### Customers + +#### List Customers +```bash +GET /squareup/v2/customers +``` + +#### Get Customer +```bash +GET /squareup/v2/customers/{customer_id} +``` + +#### Create Customer +```bash +POST /squareup/v2/customers +Content-Type: application/json + +{ + "given_name": "John", + "family_name": "Doe", + "email_address": "john@example.com" +} +``` + +#### Search Customers +```bash +POST /squareup/v2/customers/search +Content-Type: application/json + +{ + "query": {"filter": {"email_address": {"exact": "john@example.com"}}} +} +``` + +### Orders + +#### Create Order +```bash +POST /squareup/v2/orders +Content-Type: application/json + +{ + "order": { + "location_id": "{location_id}", + "line_items": [{"name": "Item", "quantity": "1", "base_price_money": {"amount": 1000, "currency": "USD"}}] + }, + "idempotency_key": "unique-key" +} +``` + +#### Search Orders +```bash +POST /squareup/v2/orders/search +Content-Type: application/json + +{ + "location_ids": ["{location_id}"] +} +``` + +### Catalog + +#### List Catalog +```bash +GET /squareup/v2/catalog/list +GET /squareup/v2/catalog/list?types=ITEM,CATEGORY +``` + +#### Get Catalog Object +```bash +GET /squareup/v2/catalog/object/{object_id} +``` + +#### Upsert Catalog Object +```bash +POST /squareup/v2/catalog/object +Content-Type: application/json + +{ + "idempotency_key": "unique-key", + "object": {"type": "ITEM", "id": "#new-item", "item_data": {"name": "Coffee"}} +} +``` + +#### Search Catalog +```bash +POST /squareup/v2/catalog/search +Content-Type: application/json + +{ + "object_types": ["ITEM"], + "query": {"text_query": {"keywords": ["coffee"]}} +} +``` + +### Inventory + +#### Get Inventory Count +```bash +GET /squareup/v2/inventory/{catalog_object_id} +``` + +#### Batch Change Inventory +```bash +POST /squareup/v2/inventory/changes/batch-create +Content-Type: application/json + +{ + "idempotency_key": "unique-key", + "changes": [...] +} +``` + +### Invoices + +#### List Invoices +```bash +GET /squareup/v2/invoices?location_id={location_id} +``` + +#### Create Invoice +```bash +POST /squareup/v2/invoices +Content-Type: application/json + +{ + "invoice": { + "location_id": "{location_id}", + "order_id": "{order_id}", + "primary_recipient": {"customer_id": "{customer_id}"}, + "payment_requests": [{"request_type": "BALANCE", "due_date": "2026-02-15"}] + }, + "idempotency_key": "unique-key" +} +``` + +#### Publish Invoice +```bash +POST /squareup/v2/invoices/{invoice_id}/publish +Content-Type: application/json + +{"version": 1, "idempotency_key": "unique-key"} +``` + +## Notes + +- All amounts are in smallest currency unit (cents for USD: 1000 = $10.00) +- Most write operations require an `idempotency_key` +- Cursor-based pagination: use `cursor` parameter with value from response +- Timestamps are ISO 8601 format +- Some endpoints require specific OAuth scopes (CUSTOMERS_READ, ORDERS_READ, ITEMS_READ, INVOICES_READ, etc.) + +## Resources + +- [Square API Overview](https://developer.squareup.com/docs) +- [Square API Reference](https://developer.squareup.com/reference/square) +- [Payments API](https://developer.squareup.com/reference/square/payments-api) +- [Customers API](https://developer.squareup.com/reference/square/customers-api) +- [Orders API](https://developer.squareup.com/reference/square/orders-api) +- [Catalog API](https://developer.squareup.com/reference/square/catalog-api) +- [Inventory API](https://developer.squareup.com/reference/square/inventory-api) +- [Invoices API](https://developer.squareup.com/reference/square/invoices-api) diff --git a/.agents/skills/api-gateway/references/stripe.md b/.agents/skills/api-gateway/references/stripe.md new file mode 100644 index 00000000..0b67dd17 --- /dev/null +++ b/.agents/skills/api-gateway/references/stripe.md @@ -0,0 +1,263 @@ +# Stripe Routing Reference + +**App name:** `stripe` +**Base URL proxied:** `api.stripe.com` + +## API Path Pattern + +``` +/stripe/v1/{endpoint} +``` + +## Common Endpoints + +### Customers + +#### List Customers +```bash +GET /stripe/v1/customers?limit=10 +``` + +#### Get Customer +```bash +GET /stripe/v1/customers/{customerId} +``` + +#### Create Customer +```bash +POST /stripe/v1/customers +Content-Type: application/x-www-form-urlencoded + +email=customer@example.com&name=John%20Doe&description=New%20customer +``` + +#### Update Customer +```bash +POST /stripe/v1/customers/{customerId} +Content-Type: application/x-www-form-urlencoded + +email=newemail@example.com +``` + +### Products + +#### List Products +```bash +GET /stripe/v1/products?limit=10&active=true +``` + +#### Create Product +```bash +POST /stripe/v1/products +Content-Type: application/x-www-form-urlencoded + +name=Premium%20Plan&description=Monthly%20subscription +``` + +### Prices + +#### List Prices +```bash +GET /stripe/v1/prices?limit=10&active=true +``` + +#### Create Price +```bash +POST /stripe/v1/prices +Content-Type: application/x-www-form-urlencoded + +unit_amount=1999¤cy=usd&product=prod_XXX&recurring[interval]=month +``` + +### Subscriptions + +#### List Subscriptions +```bash +GET /stripe/v1/subscriptions?limit=10&status=active +``` + +#### Get Subscription +```bash +GET /stripe/v1/subscriptions/{subscriptionId} +``` + +#### Create Subscription +```bash +POST /stripe/v1/subscriptions +Content-Type: application/x-www-form-urlencoded + +customer=cus_XXX&items[0][price]=price_XXX +``` + +#### Cancel Subscription +```bash +DELETE /stripe/v1/subscriptions/{subscriptionId} +``` + +### Invoices + +#### List Invoices +```bash +GET /stripe/v1/invoices?limit=10&customer=cus_XXX +``` + +#### Get Invoice +```bash +GET /stripe/v1/invoices/{invoiceId} +``` + +### Charges + +#### List Charges +```bash +GET /stripe/v1/charges?limit=10 +``` + +### Payment Intents + +#### Create Payment Intent +```bash +POST /stripe/v1/payment_intents +Content-Type: application/x-www-form-urlencoded + +amount=1999¤cy=usd&customer=cus_XXX +``` + +### Balance + +#### Get Balance +```bash +GET /stripe/v1/balance +``` + +### Events + +#### List Events +```bash +GET /stripe/v1/events?limit=10&type=customer.created +``` + +### Payment Methods + +#### List Payment Methods +```bash +GET /stripe/v1/payment_methods?customer=cus_XXX&type=card +``` + +#### Attach Payment Method +```bash +POST /stripe/v1/payment_methods/{paymentMethodId}/attach +Content-Type: application/x-www-form-urlencoded + +customer=cus_XXX +``` + +#### Detach Payment Method +```bash +POST /stripe/v1/payment_methods/{paymentMethodId}/detach +``` + +### Coupons + +#### List Coupons +```bash +GET /stripe/v1/coupons?limit=10 +``` + +#### Create Coupon +```bash +POST /stripe/v1/coupons +Content-Type: application/x-www-form-urlencoded + +percent_off=25&duration=once +``` + +#### Delete Coupon +```bash +DELETE /stripe/v1/coupons/{couponId} +``` + +### Refunds + +#### List Refunds +```bash +GET /stripe/v1/refunds?limit=10 +``` + +#### Create Refund +```bash +POST /stripe/v1/refunds +Content-Type: application/x-www-form-urlencoded + +charge=ch_XXX&amount=1000 +``` + +## Notes + +- Stripe API uses form-urlencoded data for POST requests +- IDs are prefixed: `cus_` (customer), `sub_` (subscription), `prod_` (product), `price_` (price), `in_` (invoice), `pi_` (payment intent) +- Amounts are in cents (1999 = $19.99) +- Use `expand[]` parameter to include related objects; for list endpoints use `expand[]=data.{field}` (e.g., `expand[]=data.customer`) +- List endpoints support pagination with `starting_after` and `ending_before` +- Delete returns `{id, deleted: true}` on success +- Products with prices cannot be deleted, only archived (`active=false`) + +## Resources + +- [API Overview](https://docs.stripe.com/api) +- [List Customers](https://docs.stripe.com/api/customers/list.md) +- [Get Customer](https://docs.stripe.com/api/customers/retrieve.md) +- [Create Customer](https://docs.stripe.com/api/customers/create.md) +- [Update Customer](https://docs.stripe.com/api/customers/update.md) +- [Delete Customer](https://docs.stripe.com/api/customers/delete.md) +- [Search Customers](https://docs.stripe.com/api/customers/search.md) +- [List Products](https://docs.stripe.com/api/products/list.md) +- [Get Product](https://docs.stripe.com/api/products/retrieve.md) +- [Create Product](https://docs.stripe.com/api/products/create.md) +- [Update Product](https://docs.stripe.com/api/products/update.md) +- [Delete Product](https://docs.stripe.com/api/products/delete.md) +- [Search Products](https://docs.stripe.com/api/products/search.md) +- [List Prices](https://docs.stripe.com/api/prices/list.md) +- [Get Price](https://docs.stripe.com/api/prices/retrieve.md) +- [Create Price](https://docs.stripe.com/api/prices/create.md) +- [Update Price](https://docs.stripe.com/api/prices/update.md) +- [Search Prices](https://docs.stripe.com/api/prices/search.md) +- [List Subscriptions](https://docs.stripe.com/api/subscriptions/list.md) +- [Get Subscription](https://docs.stripe.com/api/subscriptions/retrieve.md) +- [Create Subscription](https://docs.stripe.com/api/subscriptions/create.md) +- [Update Subscription](https://docs.stripe.com/api/subscriptions/update.md) +- [Cancel Subscription](https://docs.stripe.com/api/subscriptions/cancel.md) +- [Resume Subscription](https://docs.stripe.com/api/subscriptions/resume.md) +- [Search Subscriptions](https://docs.stripe.com/api/subscriptions/search.md) +- [List Invoices](https://docs.stripe.com/api/invoices/list.md) +- [Get Invoice](https://docs.stripe.com/api/invoices/retrieve.md) +- [Create Invoice](https://docs.stripe.com/api/invoices/create.md) +- [Update Invoice](https://docs.stripe.com/api/invoices/update.md) +- [Delete Invoice](https://docs.stripe.com/api/invoices/delete.md) +- [Finalize Invoice](https://docs.stripe.com/api/invoices/finalize.md) +- [Pay Invoice](https://docs.stripe.com/api/invoices/pay.md) +- [Send Invoice](https://docs.stripe.com/api/invoices/send.md) +- [Void Invoice](https://docs.stripe.com/api/invoices/void.md) +- [Search Invoices](https://docs.stripe.com/api/invoices/search.md) +- [List Charges](https://docs.stripe.com/api/charges/list.md) +- [Get Charge](https://docs.stripe.com/api/charges/retrieve.md) +- [Create Charge](https://docs.stripe.com/api/charges/create.md) +- [Update Charge](https://docs.stripe.com/api/charges/update.md) +- [Capture Charge](https://docs.stripe.com/api/charges/capture.md) +- [Search Charges](https://docs.stripe.com/api/charges/search.md) +- [List Payment Intents](https://docs.stripe.com/api/payment_intents/list.md) +- [Get Payment Intent](https://docs.stripe.com/api/payment_intents/retrieve.md) +- [Create Payment Intent](https://docs.stripe.com/api/payment_intents/create.md) +- [Update Payment Intent](https://docs.stripe.com/api/payment_intents/update.md) +- [Confirm Payment Intent](https://docs.stripe.com/api/payment_intents/confirm.md) +- [Capture Payment Intent](https://docs.stripe.com/api/payment_intents/capture.md) +- [Cancel Payment Intent](https://docs.stripe.com/api/payment_intents/cancel.md) +- [Search Payment Intents](https://docs.stripe.com/api/payment_intents/search.md) +- [Get Balance](https://docs.stripe.com/api/balance/balance_retrieve.md) +- [List Balance Transactions](https://docs.stripe.com/api/balance_transactions/list.md) +- [Get Balance Transaction](https://docs.stripe.com/api/balance_transactions/retrieve.md) +- [List Events](https://docs.stripe.com/api/events/list.md) +- [Get Event](https://docs.stripe.com/api/events/retrieve.md) +- [Pagination](https://docs.stripe.com/api/pagination.md) +- [Expanding Responses](https://docs.stripe.com/api/expanding_objects.md) +- [LLM Reference](https://docs.stripe.com/llms.txt) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/systeme.md b/.agents/skills/api-gateway/references/systeme.md new file mode 100644 index 00000000..3c5686f4 --- /dev/null +++ b/.agents/skills/api-gateway/references/systeme.md @@ -0,0 +1,214 @@ +# Systeme.io Routing Reference + +**App name:** `systeme` +**Base URL proxied:** `api.systeme.io` + +## API Path Pattern + +``` +/systeme/api/{resource} +``` + +## Common Endpoints + +### List Contacts +```bash +GET /systeme/api/contacts +``` + +Query parameters: +- `limit` - Results per page (10-100) +- `startingAfter` - ID of last item for pagination +- `order` - Sort order: `asc` or `desc` (default: `desc`) + +### Get Contact +```bash +GET /systeme/api/contacts/{id} +``` + +### Create Contact +```bash +POST /systeme/api/contacts +Content-Type: application/json + +{ + "email": "user@example.com", + "firstName": "John", + "lastName": "Doe" +} +``` + +### Update Contact +```bash +PATCH /systeme/api/contacts/{id} +Content-Type: application/merge-patch+json + +{ + "firstName": "Jane" +} +``` + +### Delete Contact +```bash +DELETE /systeme/api/contacts/{id} +``` + +### List Tags +```bash +GET /systeme/api/tags +``` + +### Create Tag +```bash +POST /systeme/api/tags +Content-Type: application/json + +{ + "name": "VIP Customer" +} +``` + +### Update Tag +```bash +PUT /systeme/api/tags/{id} +Content-Type: application/json + +{ + "name": "Premium Customer" +} +``` + +### Delete Tag +```bash +DELETE /systeme/api/tags/{id} +``` + +### Assign Tag to Contact +```bash +POST /systeme/api/contacts/{id}/tags +Content-Type: application/json + +{ + "tagId": 12345 +} +``` + +### Remove Tag from Contact +```bash +DELETE /systeme/api/contacts/{id}/tags/{tagId} +``` + +### List Contact Fields +```bash +GET /systeme/api/contact_fields +``` + +### List Courses +```bash +GET /systeme/api/school/courses +``` + +### Create Enrollment +```bash +POST /systeme/api/school/courses/{courseId}/enrollments +Content-Type: application/json + +{ + "contactId": 12345 +} +``` + +### List Enrollments +```bash +GET /systeme/api/school/enrollments +``` + +### Delete Enrollment +```bash +DELETE /systeme/api/school/enrollments/{id} +``` + +### List Communities +```bash +GET /systeme/api/community/communities +``` + +### Create Membership +```bash +POST /systeme/api/community/communities/{communityId}/memberships +Content-Type: application/json + +{ + "contactId": 12345 +} +``` + +### List Memberships +```bash +GET /systeme/api/community/memberships +``` + +### Delete Membership +```bash +DELETE /systeme/api/community/memberships/{id} +``` + +### List Subscriptions +```bash +GET /systeme/api/payment/subscriptions +``` + +### Cancel Subscription +```bash +POST /systeme/api/payment/subscriptions/{id}/cancel +``` + +### List Webhooks +```bash +GET /systeme/api/webhooks +``` + +### Create Webhook +```bash +POST /systeme/api/webhooks +Content-Type: application/json + +{ + "name": "My Webhook", + "url": "https://example.com/webhook", + "secret": "my-secret-key", + "subscriptions": ["CONTACT_CREATED"] +} +``` + +Available events: `CONTACT_CREATED`, `CONTACT_TAG_ADDED`, `CONTACT_TAG_REMOVED`, `CONTACT_OPT_IN`, `SALE_NEW`, `SALE_CANCELED` + +### Update Webhook +```bash +PATCH /systeme/api/webhooks/{id} +Content-Type: application/merge-patch+json + +{ + "name": "Updated Webhook Name" +} +``` + +### Delete Webhook +```bash +DELETE /systeme/api/webhooks/{id} +``` + +## Notes + +- Contact, tag, course, and enrollment IDs are numeric integers +- Webhook IDs are UUIDs +- Uses cursor-based pagination with `startingAfter` parameter +- PATCH requests require `Content-Type: application/merge-patch+json` +- Delete operations return 204 No Content +- Email addresses are validated for real MX records +- Payment/subscription endpoints may return 404 if not configured + +## Resources + +- [Systeme.io API Reference](https://developer.systeme.io/reference) +- [Systeme.io Developer Documentation](https://developer.systeme.io/) diff --git a/.agents/skills/api-gateway/references/tally.md b/.agents/skills/api-gateway/references/tally.md new file mode 100644 index 00000000..9de83bea --- /dev/null +++ b/.agents/skills/api-gateway/references/tally.md @@ -0,0 +1,174 @@ +# Tally Routing Reference + +**App name:** `tally` +**Base URL proxied:** `api.tally.so` + +## API Path Pattern + +``` +/tally/{resource} +``` + +Tally's API does not use version prefixes in paths. + +## Required Headers + +THe `User-Agent` header is required to avoid Cloudflare blocks: + +``` +User-Agent: Maton/1.0 +``` + +## Common Endpoints + +### Get Current User +```bash +GET /tally/users/me +``` + +### List Forms +```bash +GET /tally/forms +``` + +**Query Parameters:** +- `page` - Page number (default: 1) +- `limit` - Items per page (default: 50) + +### Get Form +```bash +GET /tally/forms/{formId} +``` + +### Create Form +```bash +POST /tally/forms +Content-Type: application/json + +{ + "status": "DRAFT", + "blocks": [ + { + "type": "FORM_TITLE", + "uuid": "11111111-1111-1111-1111-111111111111", + "groupUuid": "22222222-2222-2222-2222-222222222222", + "groupType": "FORM_TITLE", + "title": "My Form", + "payload": {} + }, + { + "type": "INPUT_TEXT", + "uuid": "33333333-3333-3333-3333-333333333333", + "groupUuid": "44444444-4444-4444-4444-444444444444", + "groupType": "INPUT_TEXT", + "title": "Your name", + "payload": {} + } + ] +} +``` + +### Update Form +```bash +PATCH /tally/forms/{formId} +Content-Type: application/json + +{ + "name": "Updated Form Name", + "status": "PUBLISHED" +} +``` + +### Delete Form +```bash +DELETE /tally/forms/{formId} +``` + +### List Form Questions +```bash +GET /tally/forms/{formId}/questions +``` + +### List Form Submissions +```bash +GET /tally/forms/{formId}/submissions +``` + +**Query Parameters:** +- `page` - Page number +- `limit` - Items per page +- `startDate` - Filter by start date (ISO 8601) +- `endDate` - Filter by end date (ISO 8601) +- `afterId` - Cursor for pagination + +### Get Submission +```bash +GET /tally/forms/{formId}/submissions/{submissionId} +``` + +### Delete Submission +```bash +DELETE /tally/forms/{formId}/submissions/{submissionId} +``` + +### List Workspaces +```bash +GET /tally/workspaces +``` + +### Get Workspace +```bash +GET /tally/workspaces/{workspaceId} +``` + +### Create Workspace +```bash +POST /tally/workspaces +Content-Type: application/json + +{ + "name": "New Workspace" +} +``` + +### List Organization Users +```bash +GET /tally/organizations/{organizationId}/users +``` + +### List Organization Invites +```bash +GET /tally/organizations/{organizationId}/invites +``` + +### List Webhooks +```bash +GET /tally/webhooks +``` + +### Create Webhook +```bash +POST /tally/webhooks +Content-Type: application/json + +{ + "formId": "GxdRaQ", + "url": "https://your-endpoint.com/webhook", + "eventTypes": ["FORM_RESPONSE"] +} +``` + +## Notes + +- Form and workspace IDs are short alphanumeric strings (e.g., `GxdRaQ`, `3jW9Q1`) +- Block `uuid` and `groupUuid` fields must be valid UUIDs (GUIDs) +- Page-based pagination with `page` and `limit` parameters +- Rate limit: 100 requests per minute +- API is in public beta and subject to changes +- Creating workspaces requires a Pro subscription + +## Resources + +- [Tally API Introduction](https://developers.tally.so/api-reference/introduction) +- [Tally API Reference](https://developers.tally.so/llms.txt) +- [Tally Help Center](https://help.tally.so/) diff --git a/.agents/skills/api-gateway/references/telegram.md b/.agents/skills/api-gateway/references/telegram.md new file mode 100644 index 00000000..e3ccd7de --- /dev/null +++ b/.agents/skills/api-gateway/references/telegram.md @@ -0,0 +1,195 @@ +# Telegram Routing Reference + +**App name:** `telegram` +**Base URL proxied:** `api.telegram.org` + +## API Path Pattern + +``` +/telegram/:token/{method} +``` + +The `:token` placeholder is automatically replaced with the bot token from the connection configuration. + +## Common Endpoints + +### Get Bot Info +```bash +GET /telegram/:token/getMe +``` + +### Get Updates +```bash +POST /telegram/:token/getUpdates +Content-Type: application/json + +{ + "limit": 100, + "timeout": 30 +} +``` + +### Send Message +```bash +POST /telegram/:token/sendMessage +Content-Type: application/json + +{ + "chat_id": 123456789, + "text": "Hello!", + "parse_mode": "HTML" +} +``` + +### Send Photo +```bash +POST /telegram/:token/sendPhoto +Content-Type: application/json + +{ + "chat_id": 123456789, + "photo": "https://example.com/image.jpg", + "caption": "Photo caption" +} +``` + +### Send Document +```bash +POST /telegram/:token/sendDocument +Content-Type: application/json + +{ + "chat_id": 123456789, + "document": "https://example.com/file.pdf" +} +``` + +### Send Location +```bash +POST /telegram/:token/sendLocation +Content-Type: application/json + +{ + "chat_id": 123456789, + "latitude": 37.7749, + "longitude": -122.4194 +} +``` + +### Send Poll +```bash +POST /telegram/:token/sendPoll +Content-Type: application/json + +{ + "chat_id": 123456789, + "question": "What is your favorite?", + "options": [{"text": "Option 1"}, {"text": "Option 2"}] +} +``` + +### Edit Message +```bash +POST /telegram/:token/editMessageText +Content-Type: application/json + +{ + "chat_id": 123456789, + "message_id": 123, + "text": "Updated text" +} +``` + +### Delete Message +```bash +POST /telegram/:token/deleteMessage +Content-Type: application/json + +{ + "chat_id": 123456789, + "message_id": 123 +} +``` + +### Forward Message +```bash +POST /telegram/:token/forwardMessage +Content-Type: application/json + +{ + "chat_id": 123456789, + "from_chat_id": 123456789, + "message_id": 123 +} +``` + +### Get Chat +```bash +POST /telegram/:token/getChat +Content-Type: application/json + +{ + "chat_id": 123456789 +} +``` + +### Set Bot Commands +```bash +POST /telegram/:token/setMyCommands +Content-Type: application/json + +{ + "commands": [ + {"command": "start", "description": "Start the bot"}, + {"command": "help", "description": "Get help"} + ] +} +``` + +### Get File +```bash +POST /telegram/:token/getFile +Content-Type: application/json + +{ + "file_id": "AgACAgQAAxkDAAM..." +} +``` + +### Set Webhook +```bash +POST /telegram/:token/setWebhook +Content-Type: application/json + +{ + "url": "https://example.com/webhook", + "allowed_updates": ["message", "callback_query"] +} +``` + +### Answer Callback Query +```bash +POST /telegram/:token/answerCallbackQuery +Content-Type: application/json + +{ + "callback_query_id": "12345678901234567", + "text": "Button clicked!" +} +``` + +## Notes + +- The `:token` placeholder is automatically replaced with the bot token +- Chat IDs are positive integers for private chats, negative for groups +- All methods support both GET and POST, but POST is recommended +- Text messages have a 4096 character limit +- Captions have a 1024 character limit +- Polls support 2-10 options +- Files can be sent via URL or file_id from previously uploaded files + +## Resources + +- [Telegram Bot API Documentation](https://core.telegram.org/bots/api) +- [Available Methods](https://core.telegram.org/bots/api#available-methods) +- [Formatting Options](https://core.telegram.org/bots/api#formatting-options) diff --git a/.agents/skills/api-gateway/references/ticktick.md b/.agents/skills/api-gateway/references/ticktick.md new file mode 100644 index 00000000..6cf21c9f --- /dev/null +++ b/.agents/skills/api-gateway/references/ticktick.md @@ -0,0 +1,117 @@ +# TickTick Routing Reference + +**App name:** `ticktick` +**Base URL proxied:** `api.ticktick.com` + +## API Path Pattern + +``` +/ticktick/open/v1/{resource} +``` + +## Common Endpoints + +### List Projects +```bash +GET /ticktick/open/v1/project +``` + +### Get Project with Tasks +```bash +GET /ticktick/open/v1/project/{projectId}/data +``` + +Returns project details along with tasks and columns. + +### Create Project +```bash +POST /ticktick/open/v1/project +Content-Type: application/json + +{ + "name": "My Project", + "viewMode": "list" +} +``` + +**viewMode options:** `list`, `kanban`, `timeline` + +### Delete Project +```bash +DELETE /ticktick/open/v1/project/{projectId} +``` + +### Get Task +```bash +GET /ticktick/open/v1/project/{projectId}/task/{taskId} +``` + +### Create Task +```bash +POST /ticktick/open/v1/task +Content-Type: application/json + +{ + "title": "New task", + "projectId": "PROJECT_ID", + "content": "Task description", + "priority": 0, + "dueDate": "2026-02-15T10:00:00+0000", + "isAllDay": false +} +``` + +**Priority values:** 0=None, 1=Low, 3=Medium, 5=High + +### Update Task +```bash +POST /ticktick/open/v1/task/{taskId} +Content-Type: application/json + +{ + "id": "TASK_ID", + "projectId": "PROJECT_ID", + "title": "Updated title", + "priority": 1 +} +``` + +### Complete Task +```bash +POST /ticktick/open/v1/project/{projectId}/task/{taskId}/complete +``` + +### Delete Task +```bash +DELETE /ticktick/open/v1/project/{projectId}/task/{taskId} +``` + +## Task Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Task ID | +| `projectId` | string | Parent project ID | +| `title` | string | Task title | +| `content` | string | Task description (Markdown) | +| `priority` | integer | 0=None, 1=Low, 3=Medium, 5=High | +| `status` | integer | 0=Active, 2=Completed | +| `dueDate` | string | ISO 8601 format | +| `startDate` | string | ISO 8601 format | +| `isAllDay` | boolean | All-day task flag | +| `timeZone` | string | e.g., "America/Los_Angeles" | +| `tags` | array | List of tag names | +| `columnId` | string | Kanban column ID | + +## Notes + +- The Open API provides access to tasks and projects only +- Habits, focus/pomodoro, and tags endpoints are not available through the Open API +- Task `status` values: 0 = Active, 2 = Completed +- Dates use ISO 8601 format with timezone offset (e.g., `2026-02-15T10:00:00+0000`) +- The `columns` field in project data is used for Kanban board columns + +## Resources + +- [TickTick Developer Portal](https://developer.ticktick.com/) +- [TickTick Help Center](https://help.ticktick.com/) diff --git a/.agents/skills/api-gateway/references/todoist.md b/.agents/skills/api-gateway/references/todoist.md new file mode 100644 index 00000000..3052b5e7 --- /dev/null +++ b/.agents/skills/api-gateway/references/todoist.md @@ -0,0 +1,176 @@ +# Todoist Routing Reference + +**App name:** `todoist` +**Base URL proxied:** `api.todoist.com` + +## API Path Pattern + +``` +/todoist/rest/v2/{resource} +``` + +## Common Endpoints + +### List Projects +```bash +GET /todoist/rest/v2/projects +``` + +### Get Project +```bash +GET /todoist/rest/v2/projects/{id} +``` + +### Create Project +```bash +POST /todoist/rest/v2/projects +Content-Type: application/json + +{ + "name": "My Project", + "color": "blue" +} +``` + +### Update Project +```bash +POST /todoist/rest/v2/projects/{id} +Content-Type: application/json + +{ + "name": "Updated Name" +} +``` + +### Delete Project +```bash +DELETE /todoist/rest/v2/projects/{id} +``` + +### List Tasks +```bash +GET /todoist/rest/v2/tasks +GET /todoist/rest/v2/tasks?project_id={project_id} +GET /todoist/rest/v2/tasks?filter={filter} +``` + +### Get Task +```bash +GET /todoist/rest/v2/tasks/{id} +``` + +### Create Task +```bash +POST /todoist/rest/v2/tasks +Content-Type: application/json + +{ + "content": "Buy groceries", + "priority": 2, + "due_string": "tomorrow" +} +``` + +### Update Task +```bash +POST /todoist/rest/v2/tasks/{id} +Content-Type: application/json + +{ + "content": "Updated content", + "priority": 4 +} +``` + +### Close Task (Complete) +```bash +POST /todoist/rest/v2/tasks/{id}/close +``` + +### Reopen Task +```bash +POST /todoist/rest/v2/tasks/{id}/reopen +``` + +### Delete Task +```bash +DELETE /todoist/rest/v2/tasks/{id} +``` + +### List Sections +```bash +GET /todoist/rest/v2/sections +GET /todoist/rest/v2/sections?project_id={project_id} +``` + +### Create Section +```bash +POST /todoist/rest/v2/sections +Content-Type: application/json + +{ + "name": "In Progress", + "project_id": "123456" +} +``` + +### Delete Section +```bash +DELETE /todoist/rest/v2/sections/{id} +``` + +### List Labels +```bash +GET /todoist/rest/v2/labels +``` + +### Create Label +```bash +POST /todoist/rest/v2/labels +Content-Type: application/json + +{ + "name": "urgent", + "color": "red" +} +``` + +### Delete Label +```bash +DELETE /todoist/rest/v2/labels/{id} +``` + +### List Comments +```bash +GET /todoist/rest/v2/comments?task_id={task_id} +GET /todoist/rest/v2/comments?project_id={project_id} +``` + +### Create Comment +```bash +POST /todoist/rest/v2/comments +Content-Type: application/json + +{ + "task_id": "123456", + "content": "This is a comment" +} +``` + +### Delete Comment +```bash +DELETE /todoist/rest/v2/comments/{id} +``` + +## Notes + +- Task and Project IDs are strings +- Priority values: 1 (normal) to 4 (urgent) +- Use only one due date format per request: `due_string`, `due_date`, or `due_datetime` +- Comments require either `task_id` or `project_id` +- Close/reopen/delete operations return 204 No Content + +## Resources + +- [Todoist REST API v2 Documentation](https://developer.todoist.com/rest/v2) +- [Todoist Filter Syntax](https://todoist.com/help/articles/introduction-to-filters) diff --git a/.agents/skills/api-gateway/references/trello.md b/.agents/skills/api-gateway/references/trello.md new file mode 100644 index 00000000..af3018f9 --- /dev/null +++ b/.agents/skills/api-gateway/references/trello.md @@ -0,0 +1,192 @@ +# Trello Routing Reference + +**App name:** `trello` +**Base URL proxied:** `api.trello.com` + +## API Path Pattern + +``` +/trello/1/{resource} +``` + +## Common Endpoints + +### Get Current Member +```bash +GET /trello/1/members/me +``` + +### Get Member's Boards +```bash +GET /trello/1/members/me/boards?filter=open +``` + +### Get Board +```bash +GET /trello/1/boards/{id}?lists=open&cards=open +``` + +### Create Board +```bash +POST /trello/1/boards +Content-Type: application/json + +{ + "name": "Project Alpha", + "desc": "Main project board", + "defaultLists": false, + "prefs_permissionLevel": "private" +} +``` + +### Get Board Lists +```bash +GET /trello/1/boards/{id}/lists?filter=open +``` + +### Get Board Cards +```bash +GET /trello/1/boards/{id}/cards +``` + +### Create List +```bash +POST /trello/1/lists +Content-Type: application/json + +{ + "name": "To Do", + "idBoard": "BOARD_ID", + "pos": "top" +} +``` + +### Get Cards in List +```bash +GET /trello/1/lists/{id}/cards +``` + +### Get Card +```bash +GET /trello/1/cards/{id}?members=true&checklists=all +``` + +### Create Card +```bash +POST /trello/1/cards +Content-Type: application/json + +{ + "name": "Implement feature X", + "desc": "Description of the task", + "idList": "LIST_ID", + "pos": "bottom", + "due": "2025-03-30T12:00:00.000Z", + "idMembers": ["MEMBER_ID"], + "idLabels": ["LABEL_ID"] +} +``` + +### Update Card +```bash +PUT /trello/1/cards/{id} +Content-Type: application/json + +{ + "name": "Updated card name", + "desc": "Updated description", + "due": "2025-04-15T12:00:00.000Z" +} +``` + +### Move Card to List +```bash +PUT /trello/1/cards/{id} +Content-Type: application/json + +{ + "idList": "NEW_LIST_ID", + "pos": "top" +} +``` + +### Delete Card +```bash +DELETE /trello/1/cards/{id} +``` + +### Add Comment to Card +```bash +POST /trello/1/cards/{id}/actions/comments +Content-Type: application/json + +{ + "text": "This is a comment" +} +``` + +### Create Checklist +```bash +POST /trello/1/checklists +Content-Type: application/json + +{ + "idCard": "CARD_ID", + "name": "Task Checklist" +} +``` + +### Create Checklist Item +```bash +POST /trello/1/checklists/{id}/checkItems +Content-Type: application/json + +{ + "name": "Subtask 1", + "pos": "bottom", + "checked": false +} +``` + +### Get Board Labels +```bash +GET /trello/1/boards/{id}/labels +``` + +### Create Label +```bash +POST /trello/1/labels +Content-Type: application/json + +{ + "name": "High Priority", + "color": "red", + "idBoard": "BOARD_ID" +} +``` + +### Search +```bash +GET /trello/1/search?query=keyword&modelTypes=cards,boards +``` + +## Notes + +- IDs are 24-character alphanumeric strings +- Use `me` to reference the authenticated user +- Dates are in ISO 8601 format +- `pos` can be `top`, `bottom`, or a positive number +- Label colors: `yellow`, `purple`, `blue`, `red`, `green`, `orange`, `black`, `sky`, `pink`, `lime`, `null` +- Use `fields` parameter to limit returned data and improve performance +- Archived items can be retrieved with `filter=closed` + +## Resources + +- [Trello API Overview](https://developer.atlassian.com/cloud/trello/rest/api-group-actions/) +- [Boards](https://developer.atlassian.com/cloud/trello/rest/api-group-boards/) +- [Lists](https://developer.atlassian.com/cloud/trello/rest/api-group-lists/) +- [Cards](https://developer.atlassian.com/cloud/trello/rest/api-group-cards/) +- [Checklists](https://developer.atlassian.com/cloud/trello/rest/api-group-checklists/) +- [Labels](https://developer.atlassian.com/cloud/trello/rest/api-group-labels/) +- [Members](https://developer.atlassian.com/cloud/trello/rest/api-group-members/) +- [Search](https://developer.atlassian.com/cloud/trello/rest/api-group-search/) diff --git a/.agents/skills/api-gateway/references/twilio.md b/.agents/skills/api-gateway/references/twilio.md new file mode 100644 index 00000000..e6467478 --- /dev/null +++ b/.agents/skills/api-gateway/references/twilio.md @@ -0,0 +1,170 @@ +# Twilio Routing Reference + +**App name:** `twilio` +**Base URL proxied:** `api.twilio.com` + +## API Path Pattern + +``` +/twilio/2010-04-01/Accounts/{AccountSid}/{resource}.json +``` + +**Important:** Most Twilio endpoints require your Account SID in the path. Get it from `/Accounts.json`. + +## Common Endpoints + +### Accounts + +#### List Accounts +```bash +GET /twilio/2010-04-01/Accounts.json +``` + +#### Get Account +```bash +GET /twilio/2010-04-01/Accounts/{AccountSid}.json +``` + +### Messages (SMS/MMS) + +#### List Messages +```bash +GET /twilio/2010-04-01/Accounts/{AccountSid}/Messages.json +``` + +#### Send Message +```bash +POST /twilio/2010-04-01/Accounts/{AccountSid}/Messages.json +Content-Type: application/x-www-form-urlencoded + +To=+15559876543&From=+15551234567&Body=Hello%20from%20Twilio! +``` + +#### Get Message +```bash +GET /twilio/2010-04-01/Accounts/{AccountSid}/Messages/{MessageSid}.json +``` + +#### Delete Message +```bash +DELETE /twilio/2010-04-01/Accounts/{AccountSid}/Messages/{MessageSid}.json +``` + +### Calls (Voice) + +#### List Calls +```bash +GET /twilio/2010-04-01/Accounts/{AccountSid}/Calls.json +``` + +#### Make Call +```bash +POST /twilio/2010-04-01/Accounts/{AccountSid}/Calls.json +Content-Type: application/x-www-form-urlencoded + +To=+15559876543&From=+15551234567&Url=https://example.com/twiml +``` + +#### Get Call +```bash +GET /twilio/2010-04-01/Accounts/{AccountSid}/Calls/{CallSid}.json +``` + +#### End Call +```bash +POST /twilio/2010-04-01/Accounts/{AccountSid}/Calls/{CallSid}.json +Content-Type: application/x-www-form-urlencoded + +Status=completed +``` + +### Phone Numbers + +#### List Incoming Phone Numbers +```bash +GET /twilio/2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers.json +``` + +#### Get Phone Number +```bash +GET /twilio/2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers/{PhoneNumberSid}.json +``` + +#### Update Phone Number +```bash +POST /twilio/2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers/{PhoneNumberSid}.json +Content-Type: application/x-www-form-urlencoded + +FriendlyName=Updated%20Name +``` + +### Applications + +#### List Applications +```bash +GET /twilio/2010-04-01/Accounts/{AccountSid}/Applications.json +``` + +#### Create Application +```bash +POST /twilio/2010-04-01/Accounts/{AccountSid}/Applications.json +Content-Type: application/x-www-form-urlencoded + +FriendlyName=My%20App&VoiceUrl=https://example.com/voice +``` + +#### Delete Application +```bash +DELETE /twilio/2010-04-01/Accounts/{AccountSid}/Applications/{ApplicationSid}.json +``` + +### Queues + +#### List Queues +```bash +GET /twilio/2010-04-01/Accounts/{AccountSid}/Queues.json +``` + +#### Create Queue +```bash +POST /twilio/2010-04-01/Accounts/{AccountSid}/Queues.json +Content-Type: application/x-www-form-urlencoded + +FriendlyName=Support%20Queue&MaxSize=100 +``` + +### Usage Records + +#### List Usage Records +```bash +GET /twilio/2010-04-01/Accounts/{AccountSid}/Usage/Records.json +``` + +## Pagination + +Uses page-based pagination: + +```bash +GET /twilio/2010-04-01/Accounts/{AccountSid}/Messages.json?PageSize=50&Page=0 +``` + +**Parameters:** +- `PageSize` - Results per page (default: 50) +- `Page` - Page number (0-indexed) + +Response includes `next_page_uri` for fetching next page. + +## Notes + +- All endpoints require `/2010-04-01/` API version prefix +- Request bodies use `application/x-www-form-urlencoded` (not JSON) +- Phone numbers must be in E.164 format (+15551234567) +- SID prefixes: AC (account), SM/MM (messages), CA (calls), PN (phone numbers), AP (applications), QU (queues) +- POST is used for both creating and updating resources +- DELETE returns 204 No Content on success + +## Resources + +- [Twilio API Overview](https://www.twilio.com/docs/usage/api) +- [Messages API](https://www.twilio.com/docs/messaging/api/message-resource) +- [Calls API](https://www.twilio.com/docs/voice/api/call-resource) diff --git a/.agents/skills/api-gateway/references/typeform.md b/.agents/skills/api-gateway/references/typeform.md new file mode 100644 index 00000000..e211ab1f --- /dev/null +++ b/.agents/skills/api-gateway/references/typeform.md @@ -0,0 +1,186 @@ +# Typeform Routing Reference + +**App name:** `typeform` +**Base URL proxied:** `api.typeform.com` + +## API Path Pattern + +``` +/typeform/{endpoint} +``` + +## Common Endpoints + +### User + +#### Get Current User +```bash +GET /typeform/me +``` + +### Forms + +#### List Forms +```bash +GET /typeform/forms?page_size=10 +``` + +#### Get Form +```bash +GET /typeform/forms/{formId} +``` + +#### Create Form +```bash +POST /typeform/forms +Content-Type: application/json + +{ + "title": "Customer Survey", + "fields": [ + { + "type": "short_text", + "title": "What is your name?" + }, + { + "type": "email", + "title": "What is your email?" + } + ] +} +``` + +#### Update Form (Full Replace) +```bash +PUT /typeform/forms/{formId} +Content-Type: application/json + +{ + "title": "Updated Survey Title", + "fields": [...] +} +``` + +#### Update Form (Partial - PATCH) +```bash +PATCH /typeform/forms/{formId} +Content-Type: application/json + +[ + {"op": "replace", "path": "/title", "value": "New Title"} +] +``` + +#### Delete Form +```bash +DELETE /typeform/forms/{formId} +``` + +### Responses + +#### List Responses +```bash +GET /typeform/forms/{formId}/responses?page_size=25 +``` + +With filters: +```bash +GET /typeform/forms/{formId}/responses?since=2024-01-01T00:00:00Z&until=2024-12-31T23:59:59Z +``` + +Completed only: +```bash +GET /typeform/forms/{formId}/responses?completed=true +``` + +#### Delete Response +```bash +DELETE /typeform/forms/{formId}/responses?included_response_ids={responseId} +``` + +### Insights + +#### Get Form Insights +```bash +GET /typeform/insights/{formId}/summary +``` + +### Workspaces + +#### List Workspaces +```bash +GET /typeform/workspaces +``` + +#### Get Workspace +```bash +GET /typeform/workspaces/{workspaceId} +``` + +### Themes + +#### List Themes +```bash +GET /typeform/themes +``` + +### Images + +#### List Images +```bash +GET /typeform/images +``` + +## Field Types + +- `short_text` - Single line text +- `long_text` - Multi-line text +- `email` - Email address +- `number` - Numeric input +- `rating` - Star rating +- `opinion_scale` - 0-10 scale +- `multiple_choice` - Single or multiple selection +- `yes_no` - Boolean +- `date` - Date picker +- `file_upload` - File attachment +- `dropdown` - Dropdown selection + +## Notes + +- Form IDs are alphanumeric strings (e.g., `JiLEvIgv`) +- Response pagination uses `before` token for cursor-based pagination +- Timestamps are in ISO 8601 format (e.g., `2026-01-01T00:00:00Z`) +- Responses include `answers` array with field references +- DELETE operations return HTTP 204 (no content) on success +- PATCH uses JSON Patch format (array of operations with `op`, `path`, `value`) + +## Resources + +- [API Overview](https://www.typeform.com/developers/get-started) +- [List Forms](https://www.typeform.com/developers/create/reference/retrieve-forms) +- [Get Form](https://www.typeform.com/developers/create/reference/retrieve-form) +- [Create Form](https://www.typeform.com/developers/create/reference/create-form) +- [Update Form](https://www.typeform.com/developers/create/reference/update-form) +- [Update Form Patch](https://www.typeform.com/developers/create/reference/update-form-patch) +- [Delete Form](https://www.typeform.com/developers/create/reference/delete-form) +- [Get Form Messages](https://www.typeform.com/developers/create/reference/retrieve-custom-form-messages) +- [Update Form Messages](https://www.typeform.com/developers/create/reference/update-custom-messages) +- [List Responses](https://www.typeform.com/developers/responses/reference/retrieve-responses) +- [Delete Responses](https://www.typeform.com/developers/responses/reference/delete-responses) +- [List Workspaces](https://www.typeform.com/developers/create/reference/retrieve-workspaces) +- [Get Workspace](https://www.typeform.com/developers/create/reference/retrieve-workspace) +- [Create Workspace](https://www.typeform.com/developers/create/reference/create-workspace) +- [Update Workspace](https://www.typeform.com/developers/create/reference/update-workspace) +- [Delete Workspace](https://www.typeform.com/developers/create/reference/delete-workspace) +- [List Themes](https://www.typeform.com/developers/create/reference/retrieve-themes) +- [Get Theme](https://www.typeform.com/developers/create/reference/retrieve-theme) +- [Create Theme](https://www.typeform.com/developers/create/reference/create-theme) +- [Update Theme](https://www.typeform.com/developers/create/reference/update-theme-partial-update) +- [Delete Theme](https://www.typeform.com/developers/create/reference/delete-theme) +- [Get Image](https://www.typeform.com/developers/create/reference/retrieve-image) +- [Get Image By Size](https://www.typeform.com/developers/create/reference/retrieve-image-by-size) +- [Create Image](https://www.typeform.com/developers/create/reference/create-image) +- [Delete Image](https://www.typeform.com/developers/create/reference/delete-image) +- [Create Or Update Webhook](https://www.typeform.com/developers/webhooks/reference/create-or-update-webhook) +- [Get Webhook](https://www.typeform.com/developers/webhooks/reference/retrieve-single-webhook) +- [Delete Webhook](https://www.typeform.com/developers/webhooks/reference/delete-webhook) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/vimeo.md b/.agents/skills/api-gateway/references/vimeo.md new file mode 100644 index 00000000..31915a8b --- /dev/null +++ b/.agents/skills/api-gateway/references/vimeo.md @@ -0,0 +1,123 @@ +# Vimeo Routing Reference + +**App name:** `vimeo` +**Base URL proxied:** `api.vimeo.com` + +## API Path Pattern + +``` +/vimeo/{resource} +``` + +## Common Endpoints + +### User + +```bash +GET /vimeo/me +GET /vimeo/users/{user_id} +GET /vimeo/me/feed +``` + +### Videos + +```bash +# List user videos +GET /vimeo/me/videos + +# Search videos +GET /vimeo/videos?query=nature + +# Get video +GET /vimeo/videos/{video_id} + +# Update video +PATCH /vimeo/videos/{video_id} + +# Delete video +DELETE /vimeo/videos/{video_id} +``` + +### Folders (Projects) + +```bash +GET /vimeo/me/folders +POST /vimeo/me/folders +PATCH /vimeo/me/projects/{project_id} +DELETE /vimeo/me/projects/{project_id} + +# Folder videos +GET /vimeo/me/projects/{project_id}/videos +PUT /vimeo/me/projects/{project_id}/videos/{video_id} +DELETE /vimeo/me/projects/{project_id}/videos/{video_id} +``` + +### Albums (Showcases) + +```bash +GET /vimeo/me/albums +POST /vimeo/me/albums +PATCH /vimeo/me/albums/{album_id} +DELETE /vimeo/me/albums/{album_id} + +# Album videos +GET /vimeo/me/albums/{album_id}/videos +PUT /vimeo/me/albums/{album_id}/videos/{video_id} +DELETE /vimeo/me/albums/{album_id}/videos/{video_id} +``` + +### Comments + +```bash +GET /vimeo/videos/{video_id}/comments +POST /vimeo/videos/{video_id}/comments +DELETE /vimeo/videos/{video_id}/comments/{comment_id} +``` + +### Likes + +```bash +GET /vimeo/me/likes +PUT /vimeo/me/likes/{video_id} +DELETE /vimeo/me/likes/{video_id} +``` + +### Watch Later + +```bash +GET /vimeo/me/watchlater +PUT /vimeo/me/watchlater/{video_id} +DELETE /vimeo/me/watchlater/{video_id} +``` + +### Following + +```bash +GET /vimeo/me/followers +GET /vimeo/me/following +PUT /vimeo/me/following/{user_id} +DELETE /vimeo/me/following/{user_id} +``` + +### Channels and Categories + +```bash +GET /vimeo/channels +GET /vimeo/channels/{channel_id} +GET /vimeo/categories +GET /vimeo/categories/{category}/videos +``` + +## Notes + +- Video and user IDs are numeric +- Folders are called "projects" in API paths +- Albums are "Showcases" in the Vimeo UI +- DELETE and PUT operations return 204 No Content +- Video uploads require TUS protocol +- Page-based pagination with `page` and `per_page` parameters + +## Resources + +- [Vimeo API Reference](https://developer.vimeo.com/api/reference) +- [Vimeo Developer Portal](https://developer.vimeo.com) diff --git a/.agents/skills/api-gateway/references/whatsapp-business.md b/.agents/skills/api-gateway/references/whatsapp-business.md new file mode 100644 index 00000000..9c07d590 --- /dev/null +++ b/.agents/skills/api-gateway/references/whatsapp-business.md @@ -0,0 +1,212 @@ +# WhatsApp Business Routing Reference + +**App name:** `whatsapp-business` +**Base URL proxied:** `graph.facebook.com` + +## API Path Pattern + +``` +/whatsapp-business/v21.0/{resource} +``` + +## Common Endpoints + +### Send Text Message +```bash +POST /whatsapp-business/v21.0/{phone_number_id}/messages +Content-Type: application/json + +{ + "messaging_product": "whatsapp", + "to": "1234567890", + "type": "text", + "text": {"body": "Hello from WhatsApp!"} +} +``` + +### Send Template Message +```bash +POST /whatsapp-business/v21.0/{phone_number_id}/messages +Content-Type: application/json + +{ + "messaging_product": "whatsapp", + "to": "1234567890", + "type": "template", + "template": { + "name": "hello_world", + "language": {"code": "en_US"}, + "components": [ + { + "type": "body", + "parameters": [{"type": "text", "text": "John"}] + } + ] + } +} +``` + +### Send Image Message +```bash +POST /whatsapp-business/v21.0/{phone_number_id}/messages +Content-Type: application/json + +{ + "messaging_product": "whatsapp", + "to": "1234567890", + "type": "image", + "image": { + "link": "https://example.com/image.jpg", + "caption": "Check out this image!" + } +} +``` + +### Send Document Message +```bash +POST /whatsapp-business/v21.0/{phone_number_id}/messages +Content-Type: application/json + +{ + "messaging_product": "whatsapp", + "to": "1234567890", + "type": "document", + "document": { + "link": "https://example.com/document.pdf", + "filename": "report.pdf" + } +} +``` + +### Send Interactive Button Message +```bash +POST /whatsapp-business/v21.0/{phone_number_id}/messages +Content-Type: application/json + +{ + "messaging_product": "whatsapp", + "to": "1234567890", + "type": "interactive", + "interactive": { + "type": "button", + "body": {"text": "Would you like to proceed?"}, + "action": { + "buttons": [ + {"type": "reply", "reply": {"id": "yes", "title": "Yes"}}, + {"type": "reply", "reply": {"id": "no", "title": "No"}} + ] + } + } +} +``` + +### Send Interactive List Message +```bash +POST /whatsapp-business/v21.0/{phone_number_id}/messages +Content-Type: application/json + +{ + "messaging_product": "whatsapp", + "to": "1234567890", + "type": "interactive", + "interactive": { + "type": "list", + "body": {"text": "Choose from the list below"}, + "action": { + "button": "View Options", + "sections": [ + { + "title": "Products", + "rows": [ + {"id": "prod1", "title": "Product 1"}, + {"id": "prod2", "title": "Product 2"} + ] + } + ] + } + } +} +``` + +### Mark Message as Read +```bash +POST /whatsapp-business/v21.0/{phone_number_id}/messages +Content-Type: application/json + +{ + "messaging_product": "whatsapp", + "status": "read", + "message_id": "wamid.xxxxx" +} +``` + +### Upload Media +```bash +POST /whatsapp-business/v21.0/{phone_number_id}/media +Content-Type: multipart/form-data + +file=@/path/to/file.jpg +type=image/jpeg +messaging_product=whatsapp +``` + +### Get Media URL +```bash +GET /whatsapp-business/v21.0/{media_id} +``` + +### List Message Templates +```bash +GET /whatsapp-business/v21.0/{whatsapp_business_account_id}/message_templates +``` + +### Create Message Template +```bash +POST /whatsapp-business/v21.0/{whatsapp_business_account_id}/message_templates +Content-Type: application/json + +{ + "name": "order_confirmation", + "language": "en_US", + "category": "UTILITY", + "components": [ + {"type": "BODY", "text": "Hi {{1}}, your order #{{2}} has been confirmed!"} + ] +} +``` + +### Get Business Profile +```bash +GET /whatsapp-business/v21.0/{phone_number_id}/whatsapp_business_profile?fields=about,address,description,email,websites +``` + +### Update Business Profile +```bash +POST /whatsapp-business/v21.0/{phone_number_id}/whatsapp_business_profile +Content-Type: application/json + +{ + "messaging_product": "whatsapp", + "about": "Your trusted partner", + "description": "We provide excellent services" +} +``` + +## Notes + +- Phone numbers must be in international format without `+` (e.g., `1234567890`) +- `messaging_product` must always be set to `whatsapp` +- Template messages are required for initiating conversations (24-hour messaging window) +- Media files must be publicly accessible URLs or uploaded via the Media API +- Interactive messages support up to 3 buttons or 10 list items +- Template categories: `AUTHENTICATION`, `MARKETING`, `UTILITY` + +## Resources + +- [WhatsApp Business API Overview](https://developers.facebook.com/docs/whatsapp/cloud-api/overview) +- [Send Messages](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages) +- [Message Templates](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-message-templates) +- [Media](https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media) +- [Business Profiles](https://developers.facebook.com/docs/whatsapp/cloud-api/reference/business-profiles) +- [Webhooks](https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks) +- [Error Codes](https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes) diff --git a/.agents/skills/api-gateway/references/woocommerce.md b/.agents/skills/api-gateway/references/woocommerce.md new file mode 100644 index 00000000..a1109975 --- /dev/null +++ b/.agents/skills/api-gateway/references/woocommerce.md @@ -0,0 +1,277 @@ +# WooCommerce Routing Reference + +**App name:** `woocommerce` +**Base URL proxied:** `{store-url}/wp-json/wc/v3` + +## API Path Pattern + +``` +/woocommerce/wp-json/wc/v3/{endpoint} +``` + +## Common Endpoints + +### Products + +#### List Products +```bash +GET /woocommerce/wp-json/wc/v3/products?per_page=20&status=publish +``` + +#### Get Product +```bash +GET /woocommerce/wp-json/wc/v3/products/{id} +``` + +#### Create Product +```bash +POST /woocommerce/wp-json/wc/v3/products +Content-Type: application/json + +{"name": "Premium Widget", "type": "simple", "regular_price": "19.99", "sku": "WDG-001"} +``` + +#### Update Product +```bash +PUT /woocommerce/wp-json/wc/v3/products/{id} +Content-Type: application/json + +{"regular_price": "24.99", "sale_price": "19.99"} +``` + +#### Delete Product +```bash +DELETE /woocommerce/wp-json/wc/v3/products/{id}?force=true +``` + +### Product Variations + +#### List Variations +```bash +GET /woocommerce/wp-json/wc/v3/products/{product_id}/variations +``` + +#### Create Variation +```bash +POST /woocommerce/wp-json/wc/v3/products/{product_id}/variations +Content-Type: application/json + +{"regular_price": "29.99", "sku": "TSH-001-RED-M", "attributes": [{"id": 1, "option": "Red"}]} +``` + +### Product Categories + +#### List Categories +```bash +GET /woocommerce/wp-json/wc/v3/products/categories +``` + +#### Create Category +```bash +POST /woocommerce/wp-json/wc/v3/products/categories +Content-Type: application/json + +{"name": "Electronics", "description": "Electronic products"} +``` + +### Orders + +#### List Orders +```bash +GET /woocommerce/wp-json/wc/v3/orders?status=processing&per_page=50 +``` + +#### Get Order +```bash +GET /woocommerce/wp-json/wc/v3/orders/{id} +``` + +#### Create Order +```bash +POST /woocommerce/wp-json/wc/v3/orders +Content-Type: application/json + +{"payment_method": "stripe", "set_paid": true, "billing": {"first_name": "John", "last_name": "Doe", "email": "john@example.com"}, "line_items": [{"product_id": 123, "quantity": 2}]} +``` + +#### Update Order Status +```bash +PUT /woocommerce/wp-json/wc/v3/orders/{id} +Content-Type: application/json + +{"status": "completed"} +``` + +### Order Notes + +#### List Order Notes +```bash +GET /woocommerce/wp-json/wc/v3/orders/{order_id}/notes +``` + +#### Create Order Note +```bash +POST /woocommerce/wp-json/wc/v3/orders/{order_id}/notes +Content-Type: application/json + +{"note": "Order shipped via FedEx", "customer_note": true} +``` + +### Order Refunds + +#### Create Refund +```bash +POST /woocommerce/wp-json/wc/v3/orders/{order_id}/refunds +Content-Type: application/json + +{"amount": "25.00", "reason": "Product damaged", "api_refund": true} +``` + +### Customers + +#### List Customers +```bash +GET /woocommerce/wp-json/wc/v3/customers?per_page=25 +``` + +#### Get Customer +```bash +GET /woocommerce/wp-json/wc/v3/customers/{id} +``` + +#### Create Customer +```bash +POST /woocommerce/wp-json/wc/v3/customers +Content-Type: application/json + +{"email": "jane@example.com", "first_name": "Jane", "last_name": "Smith", "username": "janesmith"} +``` + +### Coupons + +#### List Coupons +```bash +GET /woocommerce/wp-json/wc/v3/coupons +``` + +#### Create Coupon +```bash +POST /woocommerce/wp-json/wc/v3/coupons +Content-Type: application/json + +{"code": "SUMMER2024", "discount_type": "percent", "amount": "15", "usage_limit": 100} +``` + +### Taxes + +#### List Tax Rates +```bash +GET /woocommerce/wp-json/wc/v3/taxes +``` + +#### Create Tax Rate +```bash +POST /woocommerce/wp-json/wc/v3/taxes +Content-Type: application/json + +{"country": "US", "state": "CA", "rate": "7.25", "name": "CA State Tax"} +``` + +### Shipping + +#### List Shipping Zones +```bash +GET /woocommerce/wp-json/wc/v3/shipping/zones +``` + +#### List Shipping Zone Methods +```bash +GET /woocommerce/wp-json/wc/v3/shipping/zones/{zone_id}/methods +``` + +### Webhooks + +#### List Webhooks +```bash +GET /woocommerce/wp-json/wc/v3/webhooks +``` + +#### Create Webhook +```bash +POST /woocommerce/wp-json/wc/v3/webhooks +Content-Type: application/json + +{"name": "Order Created", "topic": "order.created", "delivery_url": "https://example.com/webhook", "status": "active"} +``` + +### Reports + +#### Sales Report +```bash +GET /woocommerce/wp-json/wc/v3/reports/sales?period=month +``` + +#### Top Sellers +```bash +GET /woocommerce/wp-json/wc/v3/reports/top_sellers +``` + +#### Orders Totals +```bash +GET /woocommerce/wp-json/wc/v3/reports/orders/totals +``` + +### Settings + +#### List Settings Groups +```bash +GET /woocommerce/wp-json/wc/v3/settings +``` + +#### Get Settings in Group +```bash +GET /woocommerce/wp-json/wc/v3/settings/{group} +``` + +### System Status + +#### Get System Status +```bash +GET /woocommerce/wp-json/wc/v3/system_status +``` + +## Notes + +- All monetary amounts are returned as strings with two decimal places +- Dates are in ISO8601 format: `YYYY-MM-DDTHH:MM:SS` +- Resource IDs are integers +- Pagination uses `page` and `per_page` parameters (max 100 per page) +- Response headers include `X-WP-Total` and `X-WP-TotalPages` +- Order statuses: `pending`, `processing`, `on-hold`, `completed`, `cancelled`, `refunded`, `failed` +- Discount types: `percent`, `fixed_cart`, `fixed_product` +- Use `force=true` query parameter to permanently delete (otherwise moves to trash) +- Batch operations supported via `POST /{resource}/batch` with `create`, `update`, `delete` arrays + +## Resources + +- [WooCommerce REST API Documentation](https://woocommerce.github.io/woocommerce-rest-api-docs/) +- [Products](https://woocommerce.github.io/woocommerce-rest-api-docs/#products) +- [Product Variations](https://woocommerce.github.io/woocommerce-rest-api-docs/#product-variations) +- [Product Attributes](https://woocommerce.github.io/woocommerce-rest-api-docs/#product-attributes) +- [Product Categories](https://woocommerce.github.io/woocommerce-rest-api-docs/#product-categories) +- [Product Tags](https://woocommerce.github.io/woocommerce-rest-api-docs/#product-tags) +- [Product Reviews](https://woocommerce.github.io/woocommerce-rest-api-docs/#product-reviews) +- [Orders](https://woocommerce.github.io/woocommerce-rest-api-docs/#orders) +- [Order Notes](https://woocommerce.github.io/woocommerce-rest-api-docs/#order-notes) +- [Refunds](https://woocommerce.github.io/woocommerce-rest-api-docs/#refunds) +- [Customers](https://woocommerce.github.io/woocommerce-rest-api-docs/#customers) +- [Coupons](https://woocommerce.github.io/woocommerce-rest-api-docs/#coupons) +- [Tax Rates](https://woocommerce.github.io/woocommerce-rest-api-docs/#tax-rates) +- [Tax Classes](https://woocommerce.github.io/woocommerce-rest-api-docs/#tax-classes) +- [Shipping Zones](https://woocommerce.github.io/woocommerce-rest-api-docs/#shipping-zones) +- [Shipping Methods](https://woocommerce.github.io/woocommerce-rest-api-docs/#shipping-methods) +- [Payment Gateways](https://woocommerce.github.io/woocommerce-rest-api-docs/#payment-gateways) +- [Settings](https://woocommerce.github.io/woocommerce-rest-api-docs/#settings) +- [Webhooks](https://woocommerce.github.io/woocommerce-rest-api-docs/#webhooks) +- [Reports](https://woocommerce.github.io/woocommerce-rest-api-docs/#reports) +- [System Status](https://woocommerce.github.io/woocommerce-rest-api-docs/#system-status) diff --git a/.agents/skills/api-gateway/references/wordpress.md b/.agents/skills/api-gateway/references/wordpress.md new file mode 100644 index 00000000..3ae26f90 --- /dev/null +++ b/.agents/skills/api-gateway/references/wordpress.md @@ -0,0 +1,168 @@ +# WordPress.com Routing Reference + +**App name:** `wordpress` +**Base URL proxied:** `public-api.wordpress.com` + +## API Path Pattern + +``` +/wordpress/rest/v1.1/{endpoint} +``` + +**Important:** WordPress.com uses REST API v1.1. Site-specific endpoints use `/sites/{site_id_or_domain}/{resource}`. + +## Site Identifiers + +Sites can be identified by: +- Numeric site ID (e.g., `252505333`) +- Domain name (e.g., `myblog.wordpress.com`) + +## Common Endpoints + +### Sites + +#### Get Site Information +```bash +GET /wordpress/rest/v1.1/sites/{site} +``` + +### Posts + +#### List Posts +```bash +GET /wordpress/rest/v1.1/sites/{site}/posts +``` + +Query parameters: `number`, `offset`, `page_handle`, `status`, `search`, `category`, `tag`, `author` + +#### Get Post +```bash +GET /wordpress/rest/v1.1/sites/{site}/posts/{post_id} +``` + +#### Create Post +```bash +POST /wordpress/rest/v1.1/sites/{site}/posts/new +Content-Type: application/json + +{ + "title": "Post Title", + "content": "

Post content...

", + "status": "draft", + "categories": "news", + "tags": "featured" +} +``` + +#### Update Post +```bash +POST /wordpress/rest/v1.1/sites/{site}/posts/{post_id} +Content-Type: application/json + +{ + "title": "Updated Title", + "content": "

Updated content...

" +} +``` + +#### Delete Post +```bash +POST /wordpress/rest/v1.1/sites/{site}/posts/{post_id}/delete +``` + +### Pages + +#### List Pages +```bash +GET /wordpress/rest/v1.1/sites/{site}/posts?type=page +``` + +#### Create Page +```bash +POST /wordpress/rest/v1.1/sites/{site}/posts/new?type=page +Content-Type: application/json + +{ + "title": "Page Title", + "content": "

Page content...

", + "status": "publish" +} +``` + +### Post Likes + +#### Get Post Likes +```bash +GET /wordpress/rest/v1.1/sites/{site}/posts/{post_id}/likes +``` + +#### Like Post +```bash +POST /wordpress/rest/v1.1/sites/{site}/posts/{post_id}/likes/new +``` + +### Users + +#### List Site Users +```bash +GET /wordpress/rest/v1.1/sites/{site}/users +``` + +### User Settings + +#### Get My Settings +```bash +GET /wordpress/rest/v1.1/me/settings +``` + +#### Update My Settings +```bash +POST /wordpress/rest/v1.1/me/settings/ +Content-Type: application/json + +{ + "enable_translator": false +} +``` + +### Post Types + +#### List Post Types +```bash +GET /wordpress/rest/v1.1/sites/{site}/post-types +``` + +### Post Counts + +#### Get Post Counts +```bash +GET /wordpress/rest/v1.1/sites/{site}/post-counts/{post_type} +``` + +## Pagination + +WordPress.com uses cursor-based pagination with `page_handle`: + +```bash +GET /wordpress/rest/v1.1/sites/{site}/posts?number=20 +# Response includes "meta": {"next_page": "..."} + +GET /wordpress/rest/v1.1/sites/{site}/posts?number=20&page_handle={next_page} +``` + +Alternatively, use `offset` for simple pagination. + +## Notes + +- API version is v1.1 (not v2) +- POST is used for updates (not PUT/PATCH) +- POST to `/delete` endpoint is used for deletes (not HTTP DELETE) +- Categories and tags are created automatically when referenced in posts +- Content is HTML-formatted +- Date/time values are in ISO 8601 format + +## Resources + +- [WordPress.com REST API Overview](https://developer.wordpress.com/docs/api/) +- [Getting Started Guide](https://developer.wordpress.com/docs/api/getting-started/) +- [API Reference](https://developer.wordpress.com/docs/api/rest-api-reference/) diff --git a/.agents/skills/api-gateway/references/xero.md b/.agents/skills/api-gateway/references/xero.md new file mode 100644 index 00000000..1c5bbaf9 --- /dev/null +++ b/.agents/skills/api-gateway/references/xero.md @@ -0,0 +1,203 @@ +# Xero Routing Reference + +**App name:** `xero` +**Base URL proxied:** `api.xero.com` + +## Automatic Tenant ID Injection + +The router automatically injects the `Xero-Tenant-Id` header from your connection config. You do not need to provide it manually. + +## API Path Pattern + +``` +/xero/api.xro/2.0/{endpoint} +``` + +## Common Endpoints + +### Contacts + +#### List Contacts +```bash +GET /xero/api.xro/2.0/Contacts +``` + +#### Get Contact +```bash +GET /xero/api.xro/2.0/Contacts/{contactId} +``` + +#### Create Contact +```bash +POST /xero/api.xro/2.0/Contacts +Content-Type: application/json + +{ + "Contacts": [{ + "Name": "John Doe", + "EmailAddress": "john@example.com", + "Phones": [{"PhoneType": "DEFAULT", "PhoneNumber": "555-1234"}] + }] +} +``` + +### Invoices + +#### List Invoices +```bash +GET /xero/api.xro/2.0/Invoices +``` + +#### Get Invoice +```bash +GET /xero/api.xro/2.0/Invoices/{invoiceId} +``` + +#### Create Invoice +```bash +POST /xero/api.xro/2.0/Invoices +Content-Type: application/json + +{ + "Invoices": [{ + "Type": "ACCREC", + "Contact": {"ContactID": "xxx"}, + "LineItems": [{ + "Description": "Service", + "Quantity": 1, + "UnitAmount": 100.00, + "AccountCode": "200" + }] + }] +} +``` + +### Accounts + +#### List Accounts +```bash +GET /xero/api.xro/2.0/Accounts +``` + +### Items + +#### List Items +```bash +GET /xero/api.xro/2.0/Items +``` + +### Payments + +#### List Payments +```bash +GET /xero/api.xro/2.0/Payments +``` + +### Bank Transactions + +#### List Bank Transactions +```bash +GET /xero/api.xro/2.0/BankTransactions +``` + +### Reports + +#### Profit and Loss +```bash +GET /xero/api.xro/2.0/Reports/ProfitAndLoss?fromDate=2024-01-01&toDate=2024-12-31 +``` + +#### Balance Sheet +```bash +GET /xero/api.xro/2.0/Reports/BalanceSheet?date=2024-12-31 +``` + +#### Trial Balance +```bash +GET /xero/api.xro/2.0/Reports/TrialBalance?date=2024-12-31 +``` + +### Currencies + +#### List Currencies +```bash +GET /xero/api.xro/2.0/Currencies +``` + +### Tax Rates + +#### List Tax Rates +```bash +GET /xero/api.xro/2.0/TaxRates +``` + +### Credit Notes + +#### List Credit Notes +```bash +GET /xero/api.xro/2.0/CreditNotes +``` + +### Purchase Orders + +#### List Purchase Orders +```bash +GET /xero/api.xro/2.0/PurchaseOrders +``` + +### Organisation + +#### Get Organisation +```bash +GET /xero/api.xro/2.0/Organisation +``` + +## Invoice Types + +- `ACCREC` - Accounts Receivable (sales invoice) +- `ACCPAY` - Accounts Payable (bill) + +## Notes + +- `Xero-Tenant-Id` header is automatically injected by the router +- Dates are in `YYYY-MM-DD` format +- Multiple records can be created in a single request using arrays +- Updates use POST method with the record ID in the URL +- Draft invoices can be deleted by setting `Status` to `DELETED` +- Use `where` query parameter for filtering (e.g., `where=Status=="VOIDED"`) + +## Resources + +- [API Overview](https://developer.xero.com/documentation/api/accounting/overview) +- [List Contacts](https://developer.xero.com/documentation/api/accounting/contacts#get-contacts) +- [Get Contact](https://developer.xero.com/documentation/api/accounting/contacts#get-contacts) +- [Create Contact](https://developer.xero.com/documentation/api/accounting/contacts#put-contacts) +- [Update Contact](https://developer.xero.com/documentation/api/accounting/contacts#post-contacts) +- [List Invoices](https://developer.xero.com/documentation/api/accounting/invoices#get-invoices) +- [Get Invoice](https://developer.xero.com/documentation/api/accounting/invoices#get-invoices) +- [Create Invoice](https://developer.xero.com/documentation/api/accounting/invoices#put-invoices) +- [Update Invoice](https://developer.xero.com/documentation/api/accounting/invoices#post-invoices) +- [Email Invoice](https://developer.xero.com/documentation/api/accounting/invoices#emailing-an-invoice) +- [List Accounts](https://developer.xero.com/documentation/api/accounting/accounts#get-accounts) +- [Get Account](https://developer.xero.com/documentation/api/accounting/accounts#get-accounts) +- [Create Account](https://developer.xero.com/documentation/api/accounting/accounts#put-accounts) +- [Update Account](https://developer.xero.com/documentation/api/accounting/accounts#post-accounts) +- [Delete Account](https://developer.xero.com/documentation/api/accounting/accounts#delete-accounts) +- [List Items](https://developer.xero.com/documentation/api/accounting/items#get-items) +- [Get Item](https://developer.xero.com/documentation/api/accounting/items#get-items) +- [Create Item](https://developer.xero.com/documentation/api/accounting/items#put-items) +- [Update Item](https://developer.xero.com/documentation/api/accounting/items#post-items) +- [Delete Item](https://developer.xero.com/documentation/api/accounting/items#delete-items) +- [List Payments](https://developer.xero.com/documentation/api/accounting/payments#get-payments) +- [Get Payment](https://developer.xero.com/documentation/api/accounting/payments#get-payments) +- [Create Payment](https://developer.xero.com/documentation/api/accounting/payments#put-payments) +- [Update Payment](https://developer.xero.com/documentation/api/accounting/payments#post-payments) +- [List Bank Transactions](https://developer.xero.com/documentation/api/accounting/banktransactions#get-banktransactions) +- [Get Bank Transaction](https://developer.xero.com/documentation/api/accounting/banktransactions#get-banktransactions) +- [Create Bank Transaction](https://developer.xero.com/documentation/api/accounting/banktransactions#put-banktransactions) +- [Update Bank Transaction](https://developer.xero.com/documentation/api/accounting/banktransactions#post-banktransactions) +- [Profit and Loss Report](https://developer.xero.com/documentation/api/accounting/reports#profitandloss) +- [Balance Sheet Report](https://developer.xero.com/documentation/api/accounting/reports#balancesheet) +- [Trial Balance Report](https://developer.xero.com/documentation/api/accounting/reports#trialbalance) +- [Bank Summary Report](https://developer.xero.com/documentation/api/accounting/reports#banksummary) +- [Get Organisation](https://developer.xero.com/documentation/api/accounting/organisation#get-organisation) \ No newline at end of file diff --git a/.agents/skills/api-gateway/references/youtube.md b/.agents/skills/api-gateway/references/youtube.md new file mode 100644 index 00000000..4918e116 --- /dev/null +++ b/.agents/skills/api-gateway/references/youtube.md @@ -0,0 +1,167 @@ +# YouTube Routing Reference + +**App name:** `youtube` +**Base URL proxied:** `www.googleapis.com` + +## API Path Pattern + +``` +/youtube/youtube/v3/{resource} +``` + +## Common Endpoints + +### Search Videos +```bash +GET /youtube/youtube/v3/search?part=snippet&q=coding+tutorial&type=video&maxResults=10 +``` + +Query parameters: +- `part` - Required: `snippet` +- `q` - Search query +- `type` - Filter: `video`, `channel`, `playlist` +- `maxResults` - Results per page (1-50) +- `order` - Sort: `date`, `rating`, `relevance`, `title`, `viewCount` +- `videoDuration` - `short` (<4min), `medium` (4-20min), `long` (>20min) + +### Get Video Details +```bash +GET /youtube/youtube/v3/videos?part=snippet,statistics,contentDetails&id={videoId} +``` + +Parts available: `snippet`, `statistics`, `contentDetails`, `status`, `player` + +### Get Trending Videos +```bash +GET /youtube/youtube/v3/videos?part=snippet,statistics&chart=mostPopular®ionCode=US&maxResults=10 +``` + +### Rate Video +```bash +POST /youtube/youtube/v3/videos/rate?id={videoId}&rating=like +``` + +Rating values: `like`, `dislike`, `none` + +### Get My Channel +```bash +GET /youtube/youtube/v3/channels?part=snippet,statistics,contentDetails&mine=true +``` + +### Get Channel Details +```bash +GET /youtube/youtube/v3/channels?part=snippet,statistics&id={channelId} +``` + +### List My Playlists +```bash +GET /youtube/youtube/v3/playlists?part=snippet,contentDetails&mine=true&maxResults=25 +``` + +### Create Playlist +```bash +POST /youtube/youtube/v3/playlists?part=snippet,status +Content-Type: application/json + +{ + "snippet": { + "title": "My New Playlist", + "description": "A collection of videos" + }, + "status": { + "privacyStatus": "private" + } +} +``` + +Privacy values: `public`, `private`, `unlisted` + +### Delete Playlist +```bash +DELETE /youtube/youtube/v3/playlists?id={playlistId} +``` + +### List Playlist Items +```bash +GET /youtube/youtube/v3/playlistItems?part=snippet,contentDetails&playlistId={playlistId}&maxResults=50 +``` + +### Add Video to Playlist +```bash +POST /youtube/youtube/v3/playlistItems?part=snippet +Content-Type: application/json + +{ + "snippet": { + "playlistId": "PLxyz123", + "resourceId": { + "kind": "youtube#video", + "videoId": "abc123xyz" + }, + "position": 0 + } +} +``` + +### List My Subscriptions +```bash +GET /youtube/youtube/v3/subscriptions?part=snippet&mine=true&maxResults=50 +``` + +### Subscribe to Channel +```bash +POST /youtube/youtube/v3/subscriptions?part=snippet +Content-Type: application/json + +{ + "snippet": { + "resourceId": { + "kind": "youtube#channel", + "channelId": "UCxyz123" + } + } +} +``` + +### List Video Comments +```bash +GET /youtube/youtube/v3/commentThreads?part=snippet,replies&videoId={videoId}&maxResults=100 +``` + +### Add Comment to Video +```bash +POST /youtube/youtube/v3/commentThreads?part=snippet +Content-Type: application/json + +{ + "snippet": { + "videoId": "abc123xyz", + "topLevelComment": { + "snippet": { + "textOriginal": "Great video!" + } + } + } +} +``` + +## Notes + +- Video IDs are 11 characters (e.g., `dQw4w9WgXcQ`) +- Channel IDs start with `UC` (e.g., `UCxyz123`) +- Playlist IDs start with `PL` (user) or `UU` (uploads) +- Use `pageToken` for pagination through large result sets +- The `part` parameter is required and determines what data is returned +- Quota costs vary by endpoint - search is expensive (100 units), reads are cheap (1 unit) + +## Resources + +- [YouTube Data API Overview](https://developers.google.com/youtube/v3) +- [Search](https://developers.google.com/youtube/v3/docs/search/list) +- [Videos](https://developers.google.com/youtube/v3/docs/videos) +- [Channels](https://developers.google.com/youtube/v3/docs/channels) +- [Playlists](https://developers.google.com/youtube/v3/docs/playlists) +- [PlaylistItems](https://developers.google.com/youtube/v3/docs/playlistItems) +- [Subscriptions](https://developers.google.com/youtube/v3/docs/subscriptions) +- [Comments](https://developers.google.com/youtube/v3/docs/comments) +- [Quota Calculator](https://developers.google.com/youtube/v3/determine_quota_cost) diff --git a/.agents/skills/api-gateway/references/zoho-bigin.md b/.agents/skills/api-gateway/references/zoho-bigin.md new file mode 100644 index 00000000..e89b3ffb --- /dev/null +++ b/.agents/skills/api-gateway/references/zoho-bigin.md @@ -0,0 +1,104 @@ +# Zoho Bigin Routing Reference + +**App name:** `zoho-bigin` +**Base URL proxied:** `www.zohoapis.com` + +## API Path Pattern + +``` +/zoho-bigin/bigin/v2/{resource} +``` + +## Common Endpoints + +### List Contacts +```bash +GET /zoho-bigin/bigin/v2/Contacts?fields=First_Name,Last_Name,Email +``` + +### Get Contact +```bash +GET /zoho-bigin/bigin/v2/Contacts/{id} +``` + +### Create Contact +```bash +POST /zoho-bigin/bigin/v2/Contacts +Content-Type: application/json + +{ + "data": [{ + "Last_Name": "Smith", + "First_Name": "John", + "Email": "john@example.com" + }] +} +``` + +### Update Contact +```bash +PUT /zoho-bigin/bigin/v2/Contacts +Content-Type: application/json + +{ + "data": [{ + "id": "{record_id}", + "Phone": "+1-555-1234" + }] +} +``` + +### Delete Contact +```bash +DELETE /zoho-bigin/bigin/v2/Contacts?ids={id1},{id2} +``` + +### Search Contacts +```bash +GET /zoho-bigin/bigin/v2/Contacts/search?email=john@example.com +GET /zoho-bigin/bigin/v2/Contacts/search?criteria=(Last_Name:equals:Smith) +``` + +### List Companies (Accounts) +```bash +GET /zoho-bigin/bigin/v2/Accounts?fields=Account_Name,Website +``` + +### Get Users +```bash +GET /zoho-bigin/bigin/v2/users?type=ActiveUsers +``` + +### Get Modules +```bash +GET /zoho-bigin/bigin/v2/settings/modules +``` + +## Available Modules + +| Module | API Name | Description | +|--------|----------|-------------| +| Contacts | `Contacts` | Individual people | +| Companies | `Accounts` | Organizations/businesses | +| Pipelines | `Pipelines` | Sales opportunities/deals | +| Products | `Products` | Items you sell | +| Tasks | `Tasks` | To-do items | +| Events | `Events` | Calendar appointments | +| Calls | `Calls` | Phone call logs | +| Notes | `Notes` | Notes attached to records | + +## Notes + +- The `fields` query parameter is **required** for list operations +- Module API names are case-sensitive (e.g., `Contacts`, not `contacts`) +- Companies are accessed via the `Accounts` module +- Sales opportunities are in the `Pipelines` module (not `Deals`) +- Record IDs are numeric strings (e.g., `7255024000000596045`) +- Maximum 200 records per page, 100 per create/update/delete +- Some modules (Tasks, Events, Calls, Notes) require additional OAuth scopes + +## Resources + +- [Bigin API Overview](https://www.bigin.com/developer/docs/apis/v2/) +- [Bigin REST API Documentation](https://www.bigin.com/developer/docs/apis/) +- [Modules API](https://www.bigin.com/developer/docs/apis/modules-api.html) diff --git a/.agents/skills/api-gateway/references/zoho-books.md b/.agents/skills/api-gateway/references/zoho-books.md new file mode 100644 index 00000000..d22f3b64 --- /dev/null +++ b/.agents/skills/api-gateway/references/zoho-books.md @@ -0,0 +1,151 @@ +# Zoho Books Routing Reference + +**App name:** `zoho-books` +**Base URL proxied:** `www.zohoapis.com` + +## API Path Pattern + +``` +/zoho-books/books/v3/{resource} +``` + +## Common Endpoints + +### Contacts + +```bash +# List contacts +GET /zoho-books/books/v3/contacts + +# Get contact +GET /zoho-books/books/v3/contacts/{contact_id} + +# Create contact +POST /zoho-books/books/v3/contacts +Content-Type: application/json + +{ + "contact_name": "Customer Name", + "contact_type": "customer" +} + +# Update contact +PUT /zoho-books/books/v3/contacts/{contact_id} + +# Delete contact +DELETE /zoho-books/books/v3/contacts/{contact_id} +``` + +### Invoices + +```bash +# List invoices +GET /zoho-books/books/v3/invoices + +# Get invoice +GET /zoho-books/books/v3/invoices/{invoice_id} + +# Create invoice +POST /zoho-books/books/v3/invoices + +# Mark as sent +POST /zoho-books/books/v3/invoices/{invoice_id}/status/sent + +# Email invoice +POST /zoho-books/books/v3/invoices/{invoice_id}/email +``` + +### Bills + +```bash +# List bills +GET /zoho-books/books/v3/bills + +# Create bill +POST /zoho-books/books/v3/bills + +# Update bill +PUT /zoho-books/books/v3/bills/{bill_id} + +# Delete bill +DELETE /zoho-books/books/v3/bills/{bill_id} +``` + +### Expenses + +```bash +# List expenses +GET /zoho-books/books/v3/expenses + +# Create expense +POST /zoho-books/books/v3/expenses + +# Update expense +PUT /zoho-books/books/v3/expenses/{expense_id} + +# Delete expense +DELETE /zoho-books/books/v3/expenses/{expense_id} +``` + +### Sales Orders + +```bash +GET /zoho-books/books/v3/salesorders +POST /zoho-books/books/v3/salesorders +``` + +### Purchase Orders + +```bash +GET /zoho-books/books/v3/purchaseorders +POST /zoho-books/books/v3/purchaseorders +``` + +### Credit Notes + +```bash +GET /zoho-books/books/v3/creditnotes +``` + +### Recurring Invoices + +```bash +GET /zoho-books/books/v3/recurringinvoices +``` + +### Recurring Bills + +```bash +GET /zoho-books/books/v3/recurringbills +``` + +## Available Modules + +| Module | Endpoint | Description | +|--------|----------|-------------| +| Contacts | `/contacts` | Customers and vendors | +| Invoices | `/invoices` | Sales invoices | +| Bills | `/bills` | Vendor bills | +| Expenses | `/expenses` | Business expenses | +| Sales Orders | `/salesorders` | Sales orders | +| Purchase Orders | `/purchaseorders` | Purchase orders | +| Credit Notes | `/creditnotes` | Customer credit notes | +| Recurring Invoices | `/recurringinvoices` | Recurring invoices | +| Recurring Bills | `/recurringbills` | Recurring bills | + +## Notes + +- All successful responses have `code: 0` +- Dates should be in `yyyy-mm-dd` format +- Contact types are `customer` or `vendor` +- Some modules (items, chart of accounts, bank accounts, projects) require additional OAuth scopes +- Rate limits: 100 requests/minute per organization +- Pagination uses `page` and `per_page` parameters with `has_more_page` in response + +## Resources + +- [Zoho Books API v3 Introduction](https://www.zoho.com/books/api/v3/introduction/) +- [Zoho Books Invoices API](https://www.zoho.com/books/api/v3/invoices/) +- [Zoho Books Contacts API](https://www.zoho.com/books/api/v3/contacts/) +- [Zoho Books Bills API](https://www.zoho.com/books/api/v3/bills/) +- [Zoho Books Expenses API](https://www.zoho.com/books/api/v3/expenses/) diff --git a/.agents/skills/api-gateway/references/zoho-calendar.md b/.agents/skills/api-gateway/references/zoho-calendar.md new file mode 100644 index 00000000..1c3a59a7 --- /dev/null +++ b/.agents/skills/api-gateway/references/zoho-calendar.md @@ -0,0 +1,118 @@ +# Zoho Calendar Routing Reference + +**App name:** `zoho-calendar` +**Base URL proxied:** `calendar.zoho.com` + +## API Path Pattern + +``` +/zoho-calendar/api/v1/{resource} +``` + +## Common Endpoints + +### Calendars + +```bash +# List calendars +GET /zoho-calendar/api/v1/calendars + +# Get calendar details +GET /zoho-calendar/api/v1/calendars/{calendar_uid} + +# Create calendar +POST /zoho-calendar/api/v1/calendars?calendarData={json} + +# Delete calendar +DELETE /zoho-calendar/api/v1/calendars/{calendar_uid} +``` + +### Events + +```bash +# List events (range required, max 31 days) +GET /zoho-calendar/api/v1/calendars/{calendar_uid}/events?range={"start":"yyyyMMdd","end":"yyyyMMdd"} + +# Get event details +GET /zoho-calendar/api/v1/calendars/{calendar_uid}/events/{event_uid} + +# Create event +POST /zoho-calendar/api/v1/calendars/{calendar_uid}/events?eventdata={json} + +# Update event (etag required in eventdata) +PUT /zoho-calendar/api/v1/calendars/{calendar_uid}/events/{event_uid}?eventdata={json} + +# Delete event (etag required as HEADER) +DELETE /zoho-calendar/api/v1/calendars/{calendar_uid}/events/{event_uid} +Header: etag: {etag_value} +``` + +## Event Data Format + +### Create/Update Event + +```json +{ + "title": "Meeting Title", + "dateandtime": { + "timezone": "America/Los_Angeles", + "start": "yyyyMMdd'T'HHmmss'Z'", + "end": "yyyyMMdd'T'HHmmss'Z'" + }, + "description": "Event description", + "location": "Meeting room", + "isallday": false, + "attendees": [ + { + "email": "user@example.com", + "permission": 1, + "attendance": 1 + } + ], + "reminders": [ + { + "action": "popup", + "minutes": 30 + } + ], + "rrule": "FREQ=DAILY;COUNT=5" +} +``` + +### Update Event (etag required) + +```json +{ + "title": "Updated Title", + "dateandtime": {...}, + "etag": 1770368451507 +} +``` + +## Calendar Data Format + +```json +{ + "name": "Calendar Name", + "color": "#FF5733", + "textcolor": "#FFFFFF", + "description": "Calendar description" +} +``` + +## Notes + +- Event and calendar data is passed as JSON in query parameters (`eventdata`, `calendarData`) +- Date/time format: `yyyyMMdd'T'HHmmss'Z'` (GMT) for timed events, `yyyyMMdd` for all-day +- The `range` parameter for listing events cannot exceed 31 days +- **IMPORTANT:** For delete operations, `etag` must be passed as an HTTP header, not a query parameter +- The `etag` is required for update and delete operations - always get the latest etag before modifying +- Permission levels for attendees: 0 (Guest), 1 (View), 2 (Invite), 3 (Edit) +- Attendance: 0 (Non-participant), 1 (Required), 2 (Optional) +- Reminder actions: `email`, `popup`, `notification` + +## Resources + +- [Zoho Calendar API Introduction](https://www.zoho.com/calendar/help/api/introduction.html) +- [Zoho Calendar Events API](https://www.zoho.com/calendar/help/api/events-api.html) +- [Zoho Calendar Calendars API](https://www.zoho.com/calendar/help/api/calendars-api.html) diff --git a/.agents/skills/api-gateway/references/zoho-crm.md b/.agents/skills/api-gateway/references/zoho-crm.md new file mode 100644 index 00000000..77a9dd44 --- /dev/null +++ b/.agents/skills/api-gateway/references/zoho-crm.md @@ -0,0 +1,112 @@ +# Zoho CRM Routing Reference + +**App name:** `zoho-crm` +**Base URL proxied:** `www.zohoapis.com` + +## API Path Pattern + +``` +/zoho-crm/crm/v8/{resource} +``` + +## Common Endpoints + +### Records + +```bash +# List records (fields required) +GET /zoho-crm/crm/v8/{module_api_name}?fields={field1},{field2} + +# Get record +GET /zoho-crm/crm/v8/{module_api_name}/{record_id} + +# Create records +POST /zoho-crm/crm/v8/{module_api_name} +Content-Type: application/json + +{ + "data": [ + { + "field_api_name": "value" + } + ] +} + +# Update records +PUT /zoho-crm/crm/v8/{module_api_name} +Content-Type: application/json + +{ + "data": [ + { + "id": "record_id", + "field_api_name": "updated_value" + } + ] +} + +# Delete records +DELETE /zoho-crm/crm/v8/{module_api_name}?ids={id1},{id2} +``` + +### Search + +```bash +# Search by criteria +GET /zoho-crm/crm/v8/{module_api_name}/search?criteria=(Last_Name:equals:Smith) + +# Search by email +GET /zoho-crm/crm/v8/{module_api_name}/search?email=user@example.com + +# Search by phone +GET /zoho-crm/crm/v8/{module_api_name}/search?phone=555-1234 + +# Global text search +GET /zoho-crm/crm/v8/{module_api_name}/search?word=searchterm +``` + +## Available Modules + +| Module | API Name | Description | +|--------|----------|-------------| +| Leads | `Leads` | Potential customers | +| Contacts | `Contacts` | Individual people | +| Accounts | `Accounts` | Organizations/companies | +| Deals | `Deals` | Sales opportunities | +| Campaigns | `Campaigns` | Marketing campaigns | +| Tasks | `Tasks` | To-do items | +| Calls | `Calls` | Phone call logs | +| Events | `Events` | Calendar appointments | +| Products | `Products` | Items for sale | + +## Mandatory Fields + +| Module | Required Fields | +|--------|-----------------| +| Leads | `Last_Name` | +| Contacts | `Last_Name` | +| Accounts | `Account_Name` | +| Deals | `Deal_Name`, `Stage` | +| Tasks | `Subject` | + +## Search Operators + +- Text: `equals`, `not_equal`, `starts_with`, `in` +- Date/Number: `equals`, `not_equal`, `greater_than`, `less_than`, `between`, `in` +- Boolean: `equals`, `not_equal` + +## Notes + +- The `fields` parameter is **required** for list operations (max 50 fields) +- Module API names are case-sensitive (e.g., `Leads`, not `leads`) +- Maximum 100 records per create/update/delete request +- Maximum 200 records returned per GET request +- Use `page_token` for >2,000 records (expires after 24 hours) +- Some modules require additional OAuth scopes (Tasks, Events, Calls, Products) +- Empty datasets return HTTP 204 (No Content) + +## Resources + +- [Zoho CRM API v8 Documentation](https://www.zoho.com/crm/developer/docs/api/v8/) +- [Get Records API](https://www.zoho.com/crm/developer/docs/api/v8/get-records.html) +- [Search Records API](https://www.zoho.com/crm/developer/docs/api/v8/search-records.html) diff --git a/.agents/skills/api-gateway/references/zoho-inventory.md b/.agents/skills/api-gateway/references/zoho-inventory.md new file mode 100644 index 00000000..990bf8b7 --- /dev/null +++ b/.agents/skills/api-gateway/references/zoho-inventory.md @@ -0,0 +1,214 @@ +# Zoho Inventory Routing Reference + +**App name:** `zoho-inventory` +**Base URL proxied:** `www.zohoapis.com` + +## API Path Pattern + +``` +/zoho-inventory/inventory/v1/{resource} +``` + +## Common Endpoints + +### Items + +```bash +# List items +GET /zoho-inventory/inventory/v1/items + +# Get item +GET /zoho-inventory/inventory/v1/items/{item_id} + +# Create item +POST /zoho-inventory/inventory/v1/items +Content-Type: application/json + +{ + "name": "Widget", + "rate": 25.00, + "purchase_rate": 10.00, + "sku": "WDG-001", + "item_type": "inventory", + "product_type": "goods", + "unit": "pcs" +} + +# Update item +PUT /zoho-inventory/inventory/v1/items/{item_id} + +# Delete item +DELETE /zoho-inventory/inventory/v1/items/{item_id} + +# Mark as active/inactive +POST /zoho-inventory/inventory/v1/items/{item_id}/active +POST /zoho-inventory/inventory/v1/items/{item_id}/inactive +``` + +### Contacts + +```bash +# List contacts +GET /zoho-inventory/inventory/v1/contacts + +# Get contact +GET /zoho-inventory/inventory/v1/contacts/{contact_id} + +# Create contact +POST /zoho-inventory/inventory/v1/contacts +Content-Type: application/json + +{ + "contact_name": "Customer Name", + "contact_type": "customer" +} + +# Update contact +PUT /zoho-inventory/inventory/v1/contacts/{contact_id} + +# Delete contact +DELETE /zoho-inventory/inventory/v1/contacts/{contact_id} +``` + +### Sales Orders + +```bash +# List sales orders +GET /zoho-inventory/inventory/v1/salesorders + +# Get sales order +GET /zoho-inventory/inventory/v1/salesorders/{salesorder_id} + +# Create sales order +POST /zoho-inventory/inventory/v1/salesorders + +# Update sales order +PUT /zoho-inventory/inventory/v1/salesorders/{salesorder_id} + +# Delete sales order +DELETE /zoho-inventory/inventory/v1/salesorders/{salesorder_id} + +# Status actions +POST /zoho-inventory/inventory/v1/salesorders/{salesorder_id}/status/confirmed +POST /zoho-inventory/inventory/v1/salesorders/{salesorder_id}/status/void +``` + +### Invoices + +```bash +# List invoices +GET /zoho-inventory/inventory/v1/invoices + +# Get invoice +GET /zoho-inventory/inventory/v1/invoices/{invoice_id} + +# Create invoice +POST /zoho-inventory/inventory/v1/invoices + +# Update invoice +PUT /zoho-inventory/inventory/v1/invoices/{invoice_id} + +# Delete invoice +DELETE /zoho-inventory/inventory/v1/invoices/{invoice_id} + +# Status actions +POST /zoho-inventory/inventory/v1/invoices/{invoice_id}/status/sent +POST /zoho-inventory/inventory/v1/invoices/{invoice_id}/status/draft +POST /zoho-inventory/inventory/v1/invoices/{invoice_id}/status/void + +# Email +POST /zoho-inventory/inventory/v1/invoices/{invoice_id}/email +``` + +### Purchase Orders + +```bash +# List purchase orders +GET /zoho-inventory/inventory/v1/purchaseorders + +# Create purchase order +POST /zoho-inventory/inventory/v1/purchaseorders + +# Update purchase order +PUT /zoho-inventory/inventory/v1/purchaseorders/{purchaseorder_id} + +# Delete purchase order +DELETE /zoho-inventory/inventory/v1/purchaseorders/{purchaseorder_id} + +# Status actions +POST /zoho-inventory/inventory/v1/purchaseorders/{purchaseorder_id}/status/issued +POST /zoho-inventory/inventory/v1/purchaseorders/{purchaseorder_id}/status/cancelled +``` + +### Bills + +```bash +# List bills +GET /zoho-inventory/inventory/v1/bills + +# Create bill +POST /zoho-inventory/inventory/v1/bills + +# Update bill +PUT /zoho-inventory/inventory/v1/bills/{bill_id} + +# Delete bill +DELETE /zoho-inventory/inventory/v1/bills/{bill_id} + +# Status actions +POST /zoho-inventory/inventory/v1/bills/{bill_id}/status/open +POST /zoho-inventory/inventory/v1/bills/{bill_id}/status/void +``` + +### Item Groups + +```bash +GET /zoho-inventory/inventory/v1/itemgroups +GET /zoho-inventory/inventory/v1/itemgroups/{itemgroup_id} +POST /zoho-inventory/inventory/v1/itemgroups +PUT /zoho-inventory/inventory/v1/itemgroups/{itemgroup_id} +DELETE /zoho-inventory/inventory/v1/itemgroups/{itemgroup_id} +``` + +### Shipment Orders + +```bash +POST /zoho-inventory/inventory/v1/shipmentorders +GET /zoho-inventory/inventory/v1/shipmentorders/{shipmentorder_id} +PUT /zoho-inventory/inventory/v1/shipmentorders/{shipmentorder_id} +DELETE /zoho-inventory/inventory/v1/shipmentorders/{shipmentorder_id} +POST /zoho-inventory/inventory/v1/shipmentorders/{shipmentorder_id}/status/delivered +``` + +## Available Modules + +| Module | Endpoint | Description | +|--------|----------|-------------| +| Items | `/items` | Products and services | +| Item Groups | `/itemgroups` | Grouped product variants | +| Contacts | `/contacts` | Customers and vendors | +| Sales Orders | `/salesorders` | Sales orders | +| Invoices | `/invoices` | Sales invoices | +| Purchase Orders | `/purchaseorders` | Purchase orders | +| Bills | `/bills` | Vendor bills | +| Shipment Orders | `/shipmentorders` | Shipment tracking | + +## Notes + +- All successful responses have `code: 0` +- Dates should be in `yyyy-mm-dd` format +- Contact types are `customer` or `vendor` +- The `organization_id` parameter is automatically handled by the gateway +- Sales order and purchase order numbers are auto-generated by default +- Pagination uses `page` and `per_page` parameters with `has_more_page` in response +- Rate limits: 100 requests/minute per organization + +## Resources + +- [Zoho Inventory API v1 Introduction](https://www.zoho.com/inventory/api/v1/introduction/) +- [Zoho Inventory Items API](https://www.zoho.com/inventory/api/v1/items/) +- [Zoho Inventory Contacts API](https://www.zoho.com/inventory/api/v1/contacts/) +- [Zoho Inventory Sales Orders API](https://www.zoho.com/inventory/api/v1/salesorders/) +- [Zoho Inventory Invoices API](https://www.zoho.com/inventory/api/v1/invoices/) +- [Zoho Inventory Purchase Orders API](https://www.zoho.com/inventory/api/v1/purchaseorders/) +- [Zoho Inventory Bills API](https://www.zoho.com/inventory/api/v1/bills/) diff --git a/.agents/skills/api-gateway/references/zoho-mail.md b/.agents/skills/api-gateway/references/zoho-mail.md new file mode 100644 index 00000000..c1fc3997 --- /dev/null +++ b/.agents/skills/api-gateway/references/zoho-mail.md @@ -0,0 +1,173 @@ +# Zoho Mail Routing Reference + +**App name:** `zoho-mail` +**Base URL proxied:** `mail.zoho.com` + +## API Path Pattern + +``` +/zoho-mail/api/{resource} +``` + +## Common Endpoints + +### Accounts + +```bash +# Get all accounts +GET /zoho-mail/api/accounts + +# Get account details +GET /zoho-mail/api/accounts/{accountId} +``` + +### Folders + +```bash +# List all folders +GET /zoho-mail/api/accounts/{accountId}/folders + +# Create folder +POST /zoho-mail/api/accounts/{accountId}/folders +Content-Type: application/json + +{ + "folderName": "My Folder" +} + +# Rename folder +PUT /zoho-mail/api/accounts/{accountId}/folders/{folderId} +Content-Type: application/json + +{ + "folderName": "Renamed Folder" +} + +# Delete folder +DELETE /zoho-mail/api/accounts/{accountId}/folders/{folderId} +``` + +### Labels + +```bash +# List labels +GET /zoho-mail/api/accounts/{accountId}/labels + +# Create label +POST /zoho-mail/api/accounts/{accountId}/labels +Content-Type: application/json + +{ + "labelName": "Important" +} + +# Update label +PUT /zoho-mail/api/accounts/{accountId}/labels/{labelId} + +# Delete label +DELETE /zoho-mail/api/accounts/{accountId}/labels/{labelId} +``` + +### Messages + +```bash +# List emails in folder +GET /zoho-mail/api/accounts/{accountId}/messages/view?folderId={folderId}&limit=50 + +# Search emails +GET /zoho-mail/api/accounts/{accountId}/messages/search?searchKey={query} + +# Get email content +GET /zoho-mail/api/accounts/{accountId}/folders/{folderId}/messages/{messageId}/content + +# Get email headers +GET /zoho-mail/api/accounts/{accountId}/folders/{folderId}/messages/{messageId}/header + +# Get email metadata +GET /zoho-mail/api/accounts/{accountId}/folders/{folderId}/messages/{messageId}/details + +# Get original MIME message +GET /zoho-mail/api/accounts/{accountId}/messages/{messageId}/originalmessage + +# Send email +POST /zoho-mail/api/accounts/{accountId}/messages +Content-Type: application/json + +{ + "fromAddress": "sender@yourdomain.com", + "toAddress": "recipient@example.com", + "subject": "Subject", + "content": "Email body", + "mailFormat": "html" +} + +# Reply to email +POST /zoho-mail/api/accounts/{accountId}/messages/{messageId} + +# Update message (mark read, move, flag, archive, spam) +PUT /zoho-mail/api/accounts/{accountId}/updatemessage +Content-Type: application/json + +{ + "messageId": ["messageId1"], + "folderId": "folderId", + "mode": "markAsRead" +} + +# Delete email +DELETE /zoho-mail/api/accounts/{accountId}/folders/{folderId}/messages/{messageId} +``` + +### Attachments + +```bash +# Upload attachment +POST /zoho-mail/api/accounts/{accountId}/messages/attachments +Content-Type: multipart/form-data + +# Get attachment info +GET /zoho-mail/api/accounts/{accountId}/folders/{folderId}/messages/{messageId}/attachmentinfo + +# Download attachment +GET /zoho-mail/api/accounts/{accountId}/folders/{folderId}/messages/{messageId}/attachments/{attachmentId} +``` + +## Update Message Modes + +| Mode | Description | +|------|-------------| +| `markAsRead` | Mark messages as read | +| `markAsUnread` | Mark messages as unread | +| `moveMessage` | Move messages (requires `destfolderId`) | +| `flag` | Set flag (requires `flagid`: 1-4) | +| `archive` | Archive messages | +| `unArchive` | Unarchive messages | +| `spam` | Mark as spam | +| `notSpam` | Mark as not spam | + +## Default Folders + +| Folder | Type | +|--------|------| +| Inbox | `Inbox` | +| Drafts | `Drafts` | +| Templates | `Templates` | +| Snoozed | `Snoozed` | +| Sent | `Sent` | +| Spam | `Spam` | +| Trash | `Trash` | +| Outbox | `Outbox` | + +## Notes + +- Account IDs are required for most operations - get via `/api/accounts` +- Message IDs and Folder IDs are numeric strings +- The `fromAddress` must be associated with the authenticated account +- Uses offset-based pagination with `start` and `limit` parameters +- Some operations require additional OAuth scopes + +## Resources + +- [Zoho Mail API Overview](https://www.zoho.com/mail/help/api/overview.html) +- [Email Messages API](https://www.zoho.com/mail/help/api/email-api.html) +- [Folders API](https://www.zoho.com/mail/help/api/get-all-folder-details.html) diff --git a/.agents/skills/api-gateway/references/zoho-people.md b/.agents/skills/api-gateway/references/zoho-people.md new file mode 100644 index 00000000..0ddb647e --- /dev/null +++ b/.agents/skills/api-gateway/references/zoho-people.md @@ -0,0 +1,153 @@ +# Zoho People Routing Reference + +**App name:** `zoho-people` +**Base URL proxied:** `people.zoho.com` + +## API Path Pattern + +``` +/zoho-people/people/api/{resource} +``` + +or for view-based endpoints: + +``` +/zoho-people/api/forms/{viewName}/records +``` + +## Common Endpoints + +### Forms + +```bash +# List all forms +GET /zoho-people/people/api/forms +``` + +### Records (Bulk) + +```bash +# Get records from any form +GET /zoho-people/people/api/forms/{formLinkName}/getRecords?sIndex=1&limit=200 + +# Common form link names: +# - employee +# - department +# - designation +# - leave +# - P_ClientDetails +``` + +### Records (View-based) + +```bash +# Get records using a view +GET /zoho-people/api/forms/{viewName}/records?rec_limit=200 + +# Common view names: +# - P_EmployeeView +# - P_DepartmentView +# - P_DesignationView +``` + +### Search + +```bash +# Search by Employee ID +GET /zoho-people/people/api/forms/employee/getRecords?SearchColumn=EMPLOYEEID&SearchValue={empId} + +# Search by Email +GET /zoho-people/people/api/forms/employee/getRecords?SearchColumn=EMPLOYEEMAILALIAS&SearchValue={email} + +# Get modified records +GET /zoho-people/people/api/forms/{formLinkName}/getRecords?modifiedtime={timestamp_ms} +``` + +### Insert Record + +```bash +POST /zoho-people/people/api/forms/json/{formLinkName}/insertRecord +Content-Type: application/x-www-form-urlencoded + +inputData={"field1":"value1","field2":"value2"} +``` + +### Update Record + +```bash +POST /zoho-people/people/api/forms/json/{formLinkName}/updateRecord +Content-Type: application/x-www-form-urlencoded + +inputData={"field1":"newValue"}&recordId={recordId} +``` + +### Attendance + +```bash +# Get attendance entries (requires additional scope) +GET /zoho-people/people/api/attendance/getAttendanceEntries?date={date}&dateFormat={format} + +# Check-in/Check-out (requires additional scope) +POST /zoho-people/people/api/attendance +Content-Type: application/x-www-form-urlencoded + +dateFormat=dd/MM/yyyy HH:mm:ss&checkIn={datetime}&checkOut={datetime}&empId={empId} +``` + +### Leave + +```bash +# Get leave records +GET /zoho-people/people/api/forms/leave/getRecords?sIndex=1&limit=200 + +# Add leave +POST /zoho-people/people/api/forms/json/leave/insertRecord +Content-Type: application/x-www-form-urlencoded + +inputData={"Employee_ID":"EMP001","Leavetype":"123456","From":"01-Feb-2026","To":"02-Feb-2026"} +``` + +## Common Form Link Names + +| Form | formLinkName | +|------|--------------| +| Employee | `employee` | +| Department | `department` | +| Designation | `designation` | +| Leave | `leave` | +| Clients | `P_ClientDetails` | + +## Pagination + +Uses index-based pagination: +- `sIndex`: Starting index (1-based) +- `limit`: Max records per request (max 200) + +For page 2: `sIndex=201&limit=200` + +## Notes + +- Record IDs are numeric strings (e.g., `943596000000294355`) +- Insert/Update use `application/x-www-form-urlencoded` content type +- `inputData` parameter contains JSON object as string +- Attendance endpoints require additional OAuth scopes +- Maximum 200 records per request +- Response wraps data in `response.result[]` array + +## Error Codes + +| Code | Description | +|------|-------------| +| 7011 | Invalid form name | +| 7012 | Invalid view name | +| 7021 | Max limit exceeded (200) | +| 7024 | No records found | +| 7042 | Invalid search value | +| 7218 | Invalid OAuth scope | + +## Resources + +- [Zoho People API Overview](https://www.zoho.com/people/api/overview.html) +- [Get Bulk Records API](https://www.zoho.com/people/api/bulk-records.html) +- [Insert Record API](https://www.zoho.com/people/api/insert-records.html) +- [Update Record API](https://www.zoho.com/people/api/update-records.html) diff --git a/.agents/skills/api-gateway/references/zoho-recruit.md b/.agents/skills/api-gateway/references/zoho-recruit.md new file mode 100644 index 00000000..6e773376 --- /dev/null +++ b/.agents/skills/api-gateway/references/zoho-recruit.md @@ -0,0 +1,153 @@ +# Zoho Recruit Routing Reference + +**App name:** `zoho-recruit` +**Base URL proxied:** `recruit.zoho.com` + +## API Path Pattern + +``` +/zoho-recruit/recruit/v2/{module_api_name} +``` + +## Common Endpoints + +### Modules + +```bash +# List all modules +GET /zoho-recruit/recruit/v2/settings/modules + +# Get specific module +GET /zoho-recruit/recruit/v2/settings/modules/{module_api_name} +``` + +### Records + +```bash +# List records +GET /zoho-recruit/recruit/v2/{module_api_name}?page=1&per_page=200 + +# Get single record +GET /zoho-recruit/recruit/v2/{module_api_name}/{record_id} + +# Create records (max 100) +POST /zoho-recruit/recruit/v2/{module_api_name} +Content-Type: application/json + +{ + "data": [ + {"field_api_name": "value"} + ] +} + +# Update single record +PUT /zoho-recruit/recruit/v2/{module_api_name}/{record_id} +Content-Type: application/json + +{ + "data": [ + {"field_api_name": "new_value"} + ] +} + +# Update multiple records (max 100) +PUT /zoho-recruit/recruit/v2/{module_api_name} +Content-Type: application/json + +{ + "data": [ + {"id": "record_id", "field_api_name": "value"} + ] +} + +# Delete records (max 100) +DELETE /zoho-recruit/recruit/v2/{module_api_name}?ids={id1},{id2} +``` + +### Search + +```bash +# Search by criteria +GET /zoho-recruit/recruit/v2/{module_api_name}/search?criteria=(field:operator:value) + +# Search by email +GET /zoho-recruit/recruit/v2/{module_api_name}/search?email=user@example.com + +# Search by phone +GET /zoho-recruit/recruit/v2/{module_api_name}/search?phone=555-1234 + +# Global word search +GET /zoho-recruit/recruit/v2/{module_api_name}/search?word=keyword +``` + +## Available Modules + +| Module | API Name | +|--------|----------| +| Candidates | `Candidates` | +| Job Openings | `Job_Openings` | +| Applications | `Applications` | +| Interviews | `Interviews` | +| Departments | `Departments` | +| Clients | `Clients` | +| Contacts | `Contacts` | +| Campaigns | `Campaigns` | +| Referrals | `Referrals` | +| Tasks | `Tasks` | +| Events | `Events` | +| Vendors | `Vendors` | + +## Query Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `fields` | string | Comma-separated field API names | +| `sort_order` | string | `asc` or `desc` | +| `sort_by` | string | Field API name | +| `converted` | string | `true`, `false`, or `both` | +| `approved` | string | `true`, `false`, or `both` | +| `page` | integer | Page number (default: 1) | +| `per_page` | integer | Records per page (max 200) | + +## Search Operators + +**Text fields:** +- `equals`, `not_equal`, `starts_with`, `ends_with`, `contains`, `not_contains`, `in` + +**Date/Number fields:** +- `equals`, `not_equal`, `greater_than`, `less_than`, `greater_equal`, `less_equal`, `between` + +## Pagination + +Uses page-based pagination: +- `page`: Page number (default: 1) +- `per_page`: Records per page (max: 200) + +Response includes: +```json +{ + "data": [...], + "info": { + "per_page": 200, + "count": 50, + "page": 1, + "more_records": false + } +} +``` + +## Notes + +- Module API names are case-sensitive (e.g., `Job_Openings`) +- Maximum 200 records per GET request +- Maximum 100 records per POST/PUT/DELETE request +- `Last_Name` is mandatory for Candidates +- Date format: `yyyy-MM-dd` +- DateTime format: `yyyy-MM-ddTHH:mm:ss±HH:mm` (ISO 8601) +- Lookup fields use JSON objects with `id` + +## Resources + +- [Zoho Recruit API v2 Overview](https://www.zoho.com/recruit/developer-guide/apiv2/) +- [Get Records API](https://www.zoho.com/recruit/developer-guide/apiv2/get-records.html) +- [Search Records API](https://www.zoho.com/recruit/developer-guide/apiv2/search-records.html) diff --git a/.agents/skills/backlink-analyzer/SKILL.md b/.agents/skills/backlink-analyzer/SKILL.md new file mode 100644 index 00000000..7079899c --- /dev/null +++ b/.agents/skills/backlink-analyzer/SKILL.md @@ -0,0 +1,146 @@ +--- +name: backlink-analyzer +description: 'Use when analyzing backlink profiles, link authority, toxic links, link-building opportunities, or competitor link gaps. 外链分析/反向链接' +version: "9.9.9" +license: Apache-2.0 +compatibility: "Codex, skills.sh, ClawHub, Vercel Labs, Cursor, Windsurf, Codex CLI, Amp, Gemini CLI, Kimi Code, Qwen Code, CodeBuddy" +homepage: "https://github.com/aaron-he-zhu/seo-geo-Codex-skills" +when_to_use: "Use when analyzing backlink profiles, link quality, toxic links, referring domains, or anchor text distribution." +argument-hint: "" +metadata: + author: aaron-he-zhu + version: "9.9.9" + geo-relevance: "low" + tags: + - seo + - backlinks + - link-building + - link-profile + - toxic-links + - off-page-seo + - link-audit + - referring-domains + - disavow + - ahrefs-alternative + - 外链分析 + - 被リンク + - 백링크 + - backlinks-seo + triggers: + - "analyze backlinks" + - "check link profile" + - "find toxic links" + - "link building opportunities" + - "backlink audit" + - "who links to me" + - "I have spammy links" + - "how do I get more backlinks" + - "disavow links" + - "how to build backlinks" + - "外链分析" + - "反向链接" + - "有毒链接" + - "链接建设" + - "外链怎么做" + - "有垃圾外链" + - "谁链接到我" + - "友链" + - "互换友链" + - "外链建设" + - "被リンク分析" + - "バックリンク" + - "リンク構築" + - "백링크 분석" + - "링크 빌딩" + - "누가 내 사이트 링크해?" + - "백링크 어떻게 늘려?" + - "análisis de backlinks" + - "enlaces entrantes" + - "análise de backlinks" +--- + +# Backlink Analyzer + +Analyzes backlink profiles for quality, risk, competitive gaps, and link-building opportunities. + +## Quick Start + +``` +Analyze backlink profile for [domain] +``` + +``` +Find link building opportunities by analyzing [competitor domains] +``` + +## Skill Contract + +**Expected output**: a backlink report or delta summary plus the standard handoff summary for `memory/monitoring/`. + +- **Reads**: current metrics, baselines, alert thresholds, and reporting context from [AGENTS.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/AGENTS.md) and the shared [State Model](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/references/state-model.md) when available. +- **Writes**: a user-facing monitoring deliverable and reusable summary. +- **Promotes**: significant changes, confirmed anomalies, follow-up actions, and pending decisions to `memory/open-loops.md`. +- **Primary next skill**: [domain-authority-auditor](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/cross-cutting/domain-authority-auditor/SKILL.md) when toxicity or authority concerns need formal scoring. + +### Handoff Summary + +> Emit the standard shape from [skill-contract.md §Handoff Summary Format](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/references/skill-contract.md). + +## Data Sources + +All integrations optional (see [CONNECTORS.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/CONNECTORS.md)). With tools, pull backlink profiles from ~~link database and competitor data from ~~SEO tool. Without tools, ask for backlink CSVs, referring domains, competitor domains, and link changes. Respect `robots.txt` and TOS per [SECURITY.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/SECURITY.md). + +## Instructions + +When a user requests backlink analysis: + +1. **Generate Profile Overview** — key metrics, link velocity, authority distribution, and profile health score. +2. **Analyze Link Quality** — top backlinks, link type mix, anchor text distribution, and geography. +3. **Identify Toxic Links** — risk indicators, links to review, and disavow recommendations. +4. **Compare Against Competitors** — profile comparison, link intersection, and top linked competitor content. +5. **Find Link Building Opportunities** — intersection prospects, broken links, unlinked mentions, resource pages, guest posts, and effort-vs-impact priorities. +6. **Track Link Changes** — new and lost links, net change, and recovery priorities. +7. **Generate Backlink Report** — executive summary, strengths, concerns, opportunities, competitive position, recommended actions, and KPIs. + +> **Reference**: See [references/analysis-templates.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/backlink-analyzer/references/analysis-templates.md) for the compact output templates used in all seven steps. + +### CITE Item Mapping + +When running `domain-authority-auditor` after this analysis, the following data feeds directly into CITE scoring: + +| Backlink Metric | CITE Item | Dimension | +|----------------|-----------|-----------| +| Referring domains count | C01 (Referring Domain Volume) | Citation | +| Authority distribution (DA breakdown) | C02 (Referring Domains Quality) | Citation | +| Link velocity | C04 (Link Velocity) | Citation | +| Geographic distribution | C10 (Link Source Diversity) | Citation | +| Dofollow/Nofollow ratio | T02 (Dofollow Ratio Normality) | Trust | +| Toxic link analysis | T01 (Link Profile Naturalness), T03 (Link-Traffic Coherence) | Trust | +| Competitive link intersection | T05 (Profile Uniqueness) | Trust | + +## Example + +Sample outcome: a link-intersection table, top immediate opportunities, and an estimated impact model. Keep the full structure in [references/analysis-templates.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/backlink-analyzer/references/analysis-templates.md). + +## Tips for Success + +Prioritize quality, monitor regularly, diversify anchors and link types, and disavow cautiously. + +## Link Quality and Strategy Reference + +> **Reference**: See [references/link-quality-rubric.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/backlink-analyzer/references/link-quality-rubric.md) for the scoring matrix, toxic-link criteria, benchmarks, and disavow guidance. + +> **Reference**: See [references/outreach-templates.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/backlink-analyzer/references/outreach-templates.md) for outreach frameworks, subject lines, response benchmarks, follow-up sequences, and templates. + +### Save Results + +Ask "Save these results?" If yes, write `memory/monitoring/YYYY-MM-DD-.md` with headline finding, actions, and open loops. If toxic ratio exceeds 15%, recommend `domain-authority-auditor`. + +## Reference Materials + +- [Link Quality Rubric](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/backlink-analyzer/references/link-quality-rubric.md) — Quality and toxicity rubric +- [Outreach Templates](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/backlink-analyzer/references/outreach-templates.md) — Outreach frameworks and examples + +## Next Best Skill + +Toxic ratio > 15% → [domain-authority-auditor](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/cross-cutting/domain-authority-auditor/SKILL.md). Otherwise → Terminal. Visited-set rule applies per [skill-contract.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/references/skill-contract.md). diff --git a/.agents/skills/backlink-analyzer/references/analysis-templates.md b/.agents/skills/backlink-analyzer/references/analysis-templates.md new file mode 100644 index 00000000..1b49845e --- /dev/null +++ b/.agents/skills/backlink-analyzer/references/analysis-templates.md @@ -0,0 +1,116 @@ +# Backlink Analysis -- Output Templates + +Compact copy-start templates for the backlink-analysis workflow. Use placeholders until real tool exports are available. + +## 1. Profile Overview + +```markdown +## Backlink Profile Overview +**Domain**: [domain] | **Period**: [period] | **Data date**: [date] + +| Metric | Current | Prior / Benchmark | Status | +|--------|---------|-------------------|--------| +| Total backlinks | [X] | [Y] | [status] | +| Referring domains | [X] | [Y] | [status] | +| DA / DR | [X] | [Y] | [status] | +| Dofollow ratio | [X]% | [Y]% | [status] | +| Net velocity | [+/-X] | [prior] | [status] | + +**Authority mix**: DA 80-100 [X]% | 60-79 [X]% | 40-59 [X]% | 20-39 [X]% | 0-19 [X]% +**Geo mix**: [top countries + share] +**Profile health score**: [X]/100 because [main drivers] +``` + +## 2. Quality, Anchors, And Toxicity + +```markdown +## Link Quality Analysis + +| Source Domain | DA/DR | Link Type | Follow | Anchor | Target | Quality Notes | +|---------------|-------|-----------|--------|--------|--------|---------------| +| [domain] | [X] | [editorial/resource/etc.] | dofollow/nofollow | [anchor] | [URL] | [reason] | + +| Anchor Type | Count | Share | Risk / Note | +|-------------|-------|-------|-------------| +| Brand | [X] | [Y]% | [note] | +| Exact match | [X] | [Y]% | [note] | +| Partial match | [X] | [Y]% | [note] | +| URL / naked | [X] | [Y]% | [note] | +| Generic | [X] | [Y]% | [note] | + +## Toxic Link Review +**Toxic score**: [X]/100 | **Action required**: [none / monitor / outreach / disavow candidate] + +| Risk Signal | Count | Evidence | Recommended Action | +|-------------|-------|----------|--------------------| +| Spam / link farm | [X] | [examples] | [action] | +| PBN suspected | [X] | [pattern] | [action] | +| Irrelevant or hacked site | [X] | [evidence] | [action] | +| Manipulative anchor | [X] | [anchor pattern] | [action] | +``` + +## 3. Competitive And Opportunity Analysis + +```markdown +## Competitive Backlink Analysis + +| Metric | You | Comp 1 | Comp 2 | Comp 3 | Gap | +|--------|-----|--------|--------|--------|-----| +| Referring domains | [X] | [X] | [X] | [X] | [gap] | +| DA / DR | [X] | [X] | [X] | [X] | [gap] | +| Link velocity | [X] | [X] | [X] | [X] | [gap] | +| Avg link DA/DR | [X] | [X] | [X] | [X] | [gap] | + +| Prospect / Content | Evidence | Approach | Effort | Impact | Priority | +|--------------------|----------|----------|--------|--------|----------| +| [domain or page] | [links to competitors / broken link / unlinked mention] | [outreach angle] | L/M/H | L/M/H | P0/P1/P2 | + +**Top competitor-linked assets**: [asset] -- [why links accrue] +**Recommended asset gap**: [asset to build or update] +``` + +## 4. Change Tracking And Recovery + +```markdown +## Link Change Tracking +**Window**: [30/90 days] | **Net change**: [+/-X] + +| Change | Source | DA/DR | Anchor | Target | Date | Action | +|--------|--------|-------|--------|--------|------|--------| +| New / Lost | [domain] | [X] | [anchor] | [URL] | [date] | [monitor/reclaim/thank] | + +### Recovery Priorities +| Lost Link | Value | Likely Cause | Recovery Strategy | +|-----------|-------|--------------|-------------------| +| [domain] | High/Med/Low | [reason] | [outreach/update/redirect] | +``` + +## 5. Summary Report + +```markdown +# Backlink Analysis Report +**Domain**: [domain] | **Date**: [date] | **Period**: [period] + +## Executive Summary +- Referring domains: [X] ([+/-Y] vs prior) +- Average authority: [X] +- Net velocity: [X] +- Toxic link share: [X]% +- Highest-value opportunity: [summary] + +## Strengths +- [strength + evidence] + +## Concerns +- [concern + evidence + risk] + +## Recommended Actions +| Timing | Action | Owner | Expected Impact | +|--------|--------|-------|-----------------| +| Immediate | [action] | [owner] | [impact] | +| 30 days | [action] | [owner] | [impact] | +| 90 days | [action] | [owner] | [impact] | + +## KPIs To Track +Referring domains, average authority, toxic link share, anchor mix, net link velocity, reclaimed lost links. +``` diff --git a/.agents/skills/backlink-analyzer/references/link-quality-rubric.md b/.agents/skills/backlink-analyzer/references/link-quality-rubric.md new file mode 100644 index 00000000..9d9a96d1 --- /dev/null +++ b/.agents/skills/backlink-analyzer/references/link-quality-rubric.md @@ -0,0 +1,145 @@ +# Link Quality Rubric + +Use this reference to score individual backlinks, audit link profiles, find competitive link gaps, and prepare disavow files without mistaking weak links for toxic links. + +## 1. Individual Link Quality Score + +Score each link across six factors, multiply by weight, then sum the weighted values for the final **Link Quality Score (LQS)**. Use scores 4 and 2 for cases between the table anchors. + +| Factor | Weight | Score 5 | Score 3 | Score 1 | Guardrail | +|--------|--------|---------|---------|---------|-----------| +| Domain Authority | 25% | DR/DA 70+, established authority | DR/DA 30-49, credible niche site | DR/DA <15 or thin/abandoned | DR/DA is a proxy; relevance can beat raw authority. Check for inflated authority from bought links/PBNs. | +| Topical Relevance | 25% | Same niche and subtopic | Same broad field | Unrelated topic | Read the page, site focus, surrounding copy, and outbound-link pattern before scoring. | +| Linking Page Traffic | 15% | 9.9.9+ visits/mo | 100-999 visits/mo | <10 visits/mo | Real traffic suggests editorial value and referral upside. | +| Link Position | 15% | In-content editorial citation | Author bio/about section | Footer, sitewide, hidden, or template link | Editorial body links carry the most value. | +| Anchor Text | 10% | Descriptive, natural | Brand name | Generic | A single natural descriptive anchor can score high; a profile overloaded with exact-match anchors is risky. | +| Follow Status | 10% | Dofollow editorial | Sponsored/UGC disclosed | Nofollow | Nofollow is a hint, not zero value; high-authority nofollow links can still help brand/referral visibility. | + +**Rating scale** + +| LQS | Rating | Meaning | +|-----|--------|---------| +| 4.0-5.0 | Premium | High authority, relevant, editorial placement | +| 2.5-3.9 | Acceptable | Provides value and fits a healthy profile | +| 1.0-2.4 | Low quality | Minimal value; review for risk before acting | + +**Healthy anchor/follow distribution** + +| Signal | Healthy | Warning | Critical | +|--------|---------|---------|----------| +| Brand anchors | 30-40% | <15% | <5% | +| Naked URLs | 15-25% | <10% | <5% | +| Generic anchors | 10-20% | <5% | 0% | +| Descriptive/partial match | 15-25% | >35% | >50% | +| Exact match | 5-15% | 15-25% | >25% | +| Dofollow ratio | 60-80% | >90% | >95% | + +## 2. Link Profile Calibration + +Use these archetypes to interpret thresholds by site maturity. + +| Profile | Healthy Signals | Risk Signals | Verdict | +|---------|-----------------|--------------|---------| +| Strong mid-size SaaS | 1,200 referring domains, 72% dofollow, avg DR 38, 35% brand anchors, 8% exact match, 3% toxic estimate | None material | Continue current strategy. | +| At-risk competitive niche | 800 referring domains, 92% dofollow, avg DR 18, 42% exact match, 30% topical relevance, 18% toxic estimate | Over-optimized anchors, low relevance, unnatural velocity | Review toxic links, diversify anchors, slow acquisition. | +| Healthy new site | 45 referring domains, 65% dofollow, avg DR 28, 40% brand anchors, 5% exact match, +8/month velocity | Low volume only | Do not judge by mature-site volume; scale carefully while preserving quality. | + +## 3. Competitive Link Gap Analysis + +| Step | Action | Output | +|------|--------|--------| +| 1 | Select 3-5 direct competitors ranking for target keywords | Competitor set | +| 2 | Export referring domains from ~~link database | Competitor link lists | +| 3 | Build an intersection matrix: domain, you, comp 1/2/3, overlap count | Shared opportunity map | +| 4 | Prioritize by overlap, DR, and topical relevance | Outreach priority list | +| 5 | Visit each high-priority linking page | Link context and outreach angle | +| 6 | Create outreach plan | Contact, angle, target asset, template | + +**Opportunity priority** + +| Priority | Criteria | Rationale | +|----------|----------|-----------| +| Highest | Links to 3+ competitors, DR 50+, relevant | Strong market signal and likely linkability | +| High | Links to 2+ competitors, DR 30+, relevant | Proven niche linker | +| Medium | Links to 1 competitor, DR 50+, relevant | High value but less proven access | +| Lower | DR <30, low relevance, or one-off competitor link | Diminishing return unless strategically useful | + +## 4. Disavow File Safety Guide + +Only disavow links when there is clear evidence of risk. Unnecessary disavow can hurt rankings. + +| Situation | Disavow? | Reasoning | +|-----------|----------|-----------| +| Obvious PBN links | Yes | Clear manipulation signal | +| Paid links you cannot get removed | Yes | Only after attempting removal | +| Spam attack / negative SEO | Yes | Protect against third-party manipulation | +| Foreign-language spam | Yes | If clearly unnatural and irrelevant | +| Low-quality directory links | Maybe | Only if pattern is excessive | +| Low-DA sites with real content | No | Low quality is not automatically toxic | +| Nofollow links | No | Already nofollowed; usually no risk | + +**Review workflow before upload** + +| Step | Action | Required safeguard | +|------|--------|--------------------| +| 1 | Export full backlink profile | Keep raw export beside the audit | +| 2 | Filter known toxic patterns | Spam score, DR <10, foreign spam, PBN footprints | +| 3 | Manually review flagged domains | Visit each domain; do not rely only on metrics | +| 4 | Attempt removal first | Email webmasters where possible | +| 5 | Wait 2 weeks | Track outreach responses | +| 6 | Add only non-removed toxic links | Use comments and reasons | +| 7 | Upload to Google Search Console | Back up previous file first | +| 8 | Document all actions | Keep dates, reasons, and owner | +| 9 | Re-check in 4-6 weeks | Verify processing and recovery signals | + +**File format** + +```txt +# Disavow file for [domain] +# Generated: [date] +# Reason: [toxic link cleanup / negative SEO / paid links not removable] + +# Individual URLs when only one page is toxic +https://spam-site.example/toxic-page + +# Entire domains only when multiple pages are toxic +domain:pbn-network.example +domain:spam-directory.example +``` + +**Best practices** + +| Practice | Why | +|----------|-----| +| Comment every entry or group | Future auditors need the reason | +| Use `domain:` for repeated toxic domains | Captures sitewide spam patterns | +| Use individual URLs for isolated pages | Avoids disavowing good links from the same domain | +| Never disavow your own domain | Severe self-inflicted damage | +| Keep changelog and backup | Enables rollback and accountability | +| Review quarterly | Remove entries if domains are cleaned up | + +## 5. Link Profile Health Benchmarks + +| Metric | Healthy | Warning | Critical | +|--------|---------|---------|----------| +| Toxic link estimate | <5% | 5-10% | >10% | +| Referring domain growth | Positive, steady | Flat | Declining | +| Average linking DR | 25+ | 15-25 | <15 | +| Link diversity (unique domains / total links) | >0.3 | 0.1-0.3 | <0.1 | +| Topical relevance sample | >60% | 40-60% | <40% | + +Authority expectations vary by vertical: + +| Industry | Typical DR Range (Top 10) | Typical Referring Domains | Link Difficulty | +|----------|---------------------------|---------------------------|-----------------| +| Finance / Insurance | 60-90 | 5,000-50,000+ | Very High | +| Health / Medical | 50-85 | 3,000-30,000+ | Very High | +| Technology / SaaS | 40-80 | 1,000-20,000+ | High | +| E-commerce | 35-75 | 500-15,000+ | High | +| Legal | 40-70 | 1,000-9.9.9+ | High | +| Education | 50-90 | 2,000-25,000+ | Medium-High | +| Local services | 15-45 | 50-500 | Medium | +| B2B niche | 25-60 | 200-5,000+ | Medium | +| New startup | 5-25 | 10-200 | Starting point | + +Use industry ranges as context, not hard pass/fail rules. Keyword competition and topical relevance decide the real bar. diff --git a/.agents/skills/backlink-analyzer/references/outreach-templates.md b/.agents/skills/backlink-analyzer/references/outreach-templates.md new file mode 100644 index 00000000..c6bbec55 --- /dev/null +++ b/.agents/skills/backlink-analyzer/references/outreach-templates.md @@ -0,0 +1,106 @@ +# Outreach Templates + +Copy-start templates for link building. Personalize every email, lead with the recipient's value, keep the first note to 100-150 words, make one clear ask, use a real domain email, and never attach files in the first email. + +## Operating Rules + +| Rule | Do | Avoid | +|------|----|-------| +| Personalization | Mention a specific article, section, quote, or page | Generic "love your site" openers | +| Ask | One request per email | Combining guest post, link swap, and product pitch | +| Sequence | Initial, follow-up after 5-7 days, final after day 14 | More follow-ups after a decline | +| Sender | Real person at `name@domain.com` | Free-mail aliases for cold outreach | +| Compliance | Honest reason, clear identity, easy opt-out | Fake "Re:", misleading claims, link-exchange language | + +## Template Matrix + +| Use Case | Subject | Opening Proof | Core Ask | +|----------|---------|---------------|----------| +| Broken link | `Found a broken link on your [topic] page` | `I noticed [broken URL/anchor] in [section] returns 404.` | `Would [your URL] work as a replacement?` | +| Guest post | `Guest post idea: [title]` | `Your recent [article] covered [specific angle].` | `Would this outline fit your readers?` | +| Resource page | `Resource for your [topic] page` | `Your [topic] resource page is organized around [section].` | `Consider adding [resource] to [specific section].` | +| Unlinked mention | `Thanks for mentioning [brand]` | `You mentioned [brand] in [article/context].` | `Could you link that mention to [URL]?` | +| Digital PR | `New data: [headline stat]` | `[Study] found [surprising finding].` | `This may fit your coverage of [beat].` | +| Source request | `[Source request] Re: [query]` | `[Name], [title], can speak to [topic].` | `Use the quote/data below if helpful.` | +| Skyscraper | `Updated resource on [topic]` | `You link to [older competitor resource].` | `If useful, swap to this updated version.` | +| Relationship-only | `Loved your piece on [topic]` | `The section on [specific point] helped us [outcome].` | No ask; start relationship. | + +## Base Email Shape + +```text +Hi [Name], + +I was reading [specific page/article] and noticed [specific observation]. + +[One sentence of value: broken link, useful resource, original data, quote, or relevant angle.] + +[One clear ask tied to their reader benefit.] + +No pressure either way. Thanks for the useful work on [topic]. + +[Name] +``` + +## Scenario Starters + +### Broken Link + +```text +I noticed the link to [description] in [section] returns a 404. +We recently published [resource] covering [overlap]. It may work as a replacement: [URL]. +Either way, wanted to flag the broken link. +``` + +### Guest Post + +```text +I'd like to contribute: +Title: [proposed title] +Angle: [unique POV] +Reader value: [why their audience cares] +Samples: [URL 1], [URL 2] +Would this be a fit? +``` + +### Digital PR / Research + +```text +We just published [study] and found [headline stat]. +Key findings: [finding 1]; [finding 2]; [finding 3]. +Full report: [URL]. Happy to provide quotes or raw cuts. +``` + +### HARO / Source Request + +```text +Source: [name], [title], [company] +Credentials: [why credible] +Response: [2-3 concise paragraphs] +Key quote: "[one standalone quote]" +Available at [email/phone]. +``` + +## Follow-Up Sequence + +| Email | Timing | Message | +|-------|--------|---------| +| Initial | Day 0 | Make the ask | +| Follow-up 1 | Day 5-7 | `Just bumping this in case it got buried. [one-sentence reminder].` | +| Follow-up 2 | Day 14 | `Last follow-up. Feel free to bookmark [URL] if useful later.` | +| Stop | After FU2 | Move to a 3-6 month re-engagement list with a new angle | + +## Response Handling + +| Response | Reply Pattern | +|----------|---------------| +| They'll add link | Thank them; provide exact URL and preferred anchor if requested | +| They want guest post | Send title, outline, target length, delivery date, and ask for guidelines | +| Noncommittal | Acknowledge; offer to help as a future source | +| Decline | Thank them once; do not argue or follow up again | +| No response | Do not mention the non-response next time; use a new angle | + +## Subject Line Guardrails + +Use: specific topic, article title, stat, or resource. Keep under 60 characters. + +Avoid: `Link exchange opportunity`, `SEO partnership`, `I'd love a backlink`, fake `Re:`, all caps, vague "quick question" subjects. diff --git a/.agents/skills/brand-guidelines/LICENSE.txt b/.agents/skills/brand-guidelines/LICENSE.txt new file mode 100644 index 00000000..4f881c52 --- /dev/null +++ b/.agents/skills/brand-guidelines/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Anthropic, PBC. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/.agents/skills/brand-guidelines/SKILL.md b/.agents/skills/brand-guidelines/SKILL.md new file mode 100644 index 00000000..47c72c60 --- /dev/null +++ b/.agents/skills/brand-guidelines/SKILL.md @@ -0,0 +1,73 @@ +--- +name: brand-guidelines +description: Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply. +license: Complete terms in LICENSE.txt +--- + +# Anthropic Brand Styling + +## Overview + +To access Anthropic's official brand identity and style resources, use this skill. + +**Keywords**: branding, corporate identity, visual identity, post-processing, styling, brand colors, typography, Anthropic brand, visual formatting, visual design + +## Brand Guidelines + +### Colors + +**Main Colors:** + +- Dark: `#141413` - Primary text and dark backgrounds +- Light: `#faf9f5` - Light backgrounds and text on dark +- Mid Gray: `#b0aea5` - Secondary elements +- Light Gray: `#e8e6dc` - Subtle backgrounds + +**Accent Colors:** + +- Orange: `#d97757` - Primary accent +- Blue: `#6a9bcc` - Secondary accent +- Green: `#788c5d` - Tertiary accent + +### Typography + +- **Headings**: Poppins (with Arial fallback) +- **Body Text**: Lora (with Georgia fallback) +- **Note**: Fonts should be pre-installed in your environment for best results + +## Features + +### Smart Font Application + +- Applies Poppins font to headings (24pt and larger) +- Applies Lora font to body text +- Automatically falls back to Arial/Georgia if custom fonts unavailable +- Preserves readability across all systems + +### Text Styling + +- Headings (24pt+): Poppins font +- Body text: Lora font +- Smart color selection based on background +- Preserves text hierarchy and formatting + +### Shape and Accent Colors + +- Non-text shapes use accent colors +- Cycles through orange, blue, and green accents +- Maintains visual interest while staying on-brand + +## Technical Details + +### Font Management + +- Uses system-installed Poppins and Lora fonts when available +- Provides automatic fallback to Arial (headings) and Georgia (body) +- No font installation required - works with existing system fonts +- For best results, pre-install Poppins and Lora fonts in your environment + +### Color Application + +- Uses RGB color values for precise brand matching +- Applied via python-pptx's RGBColor class +- Maintains color fidelity across different systems diff --git a/.agents/skills/canvas-design/LICENSE.txt b/.agents/skills/canvas-design/LICENSE.txt new file mode 100644 index 00000000..4f881c52 --- /dev/null +++ b/.agents/skills/canvas-design/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Anthropic, PBC. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/.agents/skills/canvas-design/SKILL.md b/.agents/skills/canvas-design/SKILL.md new file mode 100644 index 00000000..3184d1e3 --- /dev/null +++ b/.agents/skills/canvas-design/SKILL.md @@ -0,0 +1,130 @@ +--- +name: canvas-design +description: Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations. +license: Complete terms in LICENSE.txt +--- + +These are instructions for creating design philosophies - aesthetic movements that are then EXPRESSED VISUALLY. Output only .md files, .pdf files, and .png files. + +Complete this in two steps: +1. Design Philosophy Creation (.md file) +2. Express by creating it on a canvas (.pdf file or .png file) + +First, undertake this task: + +## DESIGN PHILOSOPHY CREATION + +To begin, create a VISUAL PHILOSOPHY (not layouts or templates) that will be interpreted through: +- Form, space, color, composition +- Images, graphics, shapes, patterns +- Minimal text as visual accent + +### THE CRITICAL UNDERSTANDING +- What is received: Some subtle input or instructions by the user that should be taken into account, but used as a foundation; it should not constrain creative freedom. +- What is created: A design philosophy/aesthetic movement. +- What happens next: Then, the same version receives the philosophy and EXPRESSES IT VISUALLY - creating artifacts that are 90% visual design, 10% essential text. + +Consider this approach: +- Write a manifesto for an art movement +- The next phase involves making the artwork + +The philosophy must emphasize: Visual expression. Spatial communication. Artistic interpretation. Minimal words. + +### HOW TO GENERATE A VISUAL PHILOSOPHY + +**Name the movement** (1-2 words): "Brutalist Joy" / "Chromatic Silence" / "Metabolist Dreams" + +**Articulate the philosophy** (4-6 paragraphs - concise but complete): + +To capture the VISUAL essence, express how the philosophy manifests through: +- Space and form +- Color and material +- Scale and rhythm +- Composition and balance +- Visual hierarchy + +**CRITICAL GUIDELINES:** +- **Avoid redundancy**: Each design aspect should be mentioned once. Avoid repeating points about color theory, spatial relationships, or typographic principles unless adding new depth. +- **Emphasize craftsmanship REPEATEDLY**: The philosophy MUST stress multiple times that the final work should appear as though it took countless hours to create, was labored over with care, and comes from someone at the absolute top of their field. This framing is essential - repeat phrases like "meticulously crafted," "the product of deep expertise," "painstaking attention," "master-level execution." +- **Leave creative space**: Remain specific about the aesthetic direction, but concise enough that the next Codex has room to make interpretive choices also at a extremely high level of craftmanship. + +The philosophy must guide the next version to express ideas VISUALLY, not through text. Information lives in design, not paragraphs. + +### PHILOSOPHY EXAMPLES + +**"Concrete Poetry"** +Philosophy: Communication through monumental form and bold geometry. +Visual expression: Massive color blocks, sculptural typography (huge single words, tiny labels), Brutalist spatial divisions, Polish poster energy meets Le Corbusier. Ideas expressed through visual weight and spatial tension, not explanation. Text as rare, powerful gesture - never paragraphs, only essential words integrated into the visual architecture. Every element placed with the precision of a master craftsman. + +**"Chromatic Language"** +Philosophy: Color as the primary information system. +Visual expression: Geometric precision where color zones create meaning. Typography minimal - small sans-serif labels letting chromatic fields communicate. Think Josef Albers' interaction meets data visualization. Information encoded spatially and chromatically. Words only to anchor what color already shows. The result of painstaking chromatic calibration. + +**"Analog Meditation"** +Philosophy: Quiet visual contemplation through texture and breathing room. +Visual expression: Paper grain, ink bleeds, vast negative space. Photography and illustration dominate. Typography whispered (small, restrained, serving the visual). Japanese photobook aesthetic. Images breathe across pages. Text appears sparingly - short phrases, never explanatory blocks. Each composition balanced with the care of a meditation practice. + +**"Organic Systems"** +Philosophy: Natural clustering and modular growth patterns. +Visual expression: Rounded forms, organic arrangements, color from nature through architecture. Information shown through visual diagrams, spatial relationships, iconography. Text only for key labels floating in space. The composition tells the story through expert spatial orchestration. + +**"Geometric Silence"** +Philosophy: Pure order and restraint. +Visual expression: Grid-based precision, bold photography or stark graphics, dramatic negative space. Typography precise but minimal - small essential text, large quiet zones. Swiss formalism meets Brutalist material honesty. Structure communicates, not words. Every alignment the work of countless refinements. + +*These are condensed examples. The actual design philosophy should be 4-6 substantial paragraphs.* + +### ESSENTIAL PRINCIPLES +- **VISUAL PHILOSOPHY**: Create an aesthetic worldview to be expressed through design +- **MINIMAL TEXT**: Always emphasize that text is sparse, essential-only, integrated as visual element - never lengthy +- **SPATIAL EXPRESSION**: Ideas communicate through space, form, color, composition - not paragraphs +- **ARTISTIC FREEDOM**: The next Codex interprets the philosophy visually - provide creative room +- **PURE DESIGN**: This is about making ART OBJECTS, not documents with decoration +- **EXPERT CRAFTSMANSHIP**: Repeatedly emphasize the final work must look meticulously crafted, labored over with care, the product of countless hours by someone at the top of their field + +**The design philosophy should be 4-6 paragraphs long.** Fill it with poetic design philosophy that brings together the core vision. Avoid repeating the same points. Keep the design philosophy generic without mentioning the intention of the art, as if it can be used wherever. Output the design philosophy as a .md file. + +--- + +## DEDUCING THE SUBTLE REFERENCE + +**CRITICAL STEP**: Before creating the canvas, identify the subtle conceptual thread from the original request. + +**THE ESSENTIAL PRINCIPLE**: +The topic is a **subtle, niche reference embedded within the art itself** - not always literal, always sophisticated. Someone familiar with the subject should feel it intuitively, while others simply experience a masterful abstract composition. The design philosophy provides the aesthetic language. The deduced topic provides the soul - the quiet conceptual DNA woven invisibly into form, color, and composition. + +This is **VERY IMPORTANT**: The reference must be refined so it enhances the work's depth without announcing itself. Think like a jazz musician quoting another song - only those who know will catch it, but everyone appreciates the music. + +--- + +## CANVAS CREATION + +With both the philosophy and the conceptual framework established, express it on a canvas. Take a moment to gather thoughts and clear the mind. Use the design philosophy created and the instructions below to craft a masterpiece, embodying all aspects of the philosophy with expert craftsmanship. + +**IMPORTANT**: For any type of content, even if the user requests something for a movie/game/book, the approach should still be sophisticated. Never lose sight of the idea that this should be art, not something that's cartoony or amateur. + +To create museum or magazine quality work, use the design philosophy as the foundation. Create one single page, highly visual, design-forward PDF or PNG output (unless asked for more pages). Generally use repeating patterns and perfect shapes. Treat the abstract philosophical design as if it were a scientific bible, borrowing the visual language of systematic observation—dense accumulation of marks, repeated elements, or layered patterns that build meaning through patient repetition and reward sustained viewing. Add sparse, clinical typography and systematic reference markers that suggest this could be a diagram from an imaginary discipline, treating the invisible subject with the same reverence typically reserved for documenting observable phenomena. Anchor the piece with simple phrase(s) or details positioned subtly, using a limited color palette that feels intentional and cohesive. Embrace the paradox of using analytical visual language to express ideas about human experience: the result should feel like an artifact that proves something ephemeral can be studied, mapped, and understood through careful attention. This is true art. + +**Text as a contextual element**: Text is always minimal and visual-first, but let context guide whether that means whisper-quiet labels or bold typographic gestures. A punk venue poster might have larger, more aggressive type than a minimalist ceramics studio identity. Most of the time, font should be thin. All use of fonts must be design-forward and prioritize visual communication. Regardless of text scale, nothing falls off the page and nothing overlaps. Every element must be contained within the canvas boundaries with proper margins. Check carefully that all text, graphics, and visual elements have breathing room and clear separation. This is non-negotiable for professional execution. **IMPORTANT: Use different fonts if writing text. Search the `./canvas-fonts` directory. Regardless of approach, sophistication is non-negotiable.** + +Download and use whatever fonts are needed to make this a reality. Get creative by making the typography actually part of the art itself -- if the art is abstract, bring the font onto the canvas, not typeset digitally. + +To push boundaries, follow design instinct/intuition while using the philosophy as a guiding principle. Embrace ultimate design freedom and choice. Push aesthetics and design to the frontier. + +**CRITICAL**: To achieve human-crafted quality (not AI-generated), create work that looks like it took countless hours. Make it appear as though someone at the absolute top of their field labored over every detail with painstaking care. Ensure the composition, spacing, color choices, typography - everything screams expert-level craftsmanship. Double-check that nothing overlaps, formatting is flawless, every detail perfect. Create something that could be shown to people to prove expertise and rank as undeniably impressive. + +Output the final result as a single, downloadable .pdf or .png file, alongside the design philosophy used as a .md file. + +--- + +## FINAL STEP + +**IMPORTANT**: The user ALREADY said "It isn't perfect enough. It must be pristine, a masterpiece if craftsmanship, as if it were about to be displayed in a museum." + +**CRITICAL**: To refine the work, avoid adding more graphics; instead refine what has been created and make it extremely crisp, respecting the design philosophy and the principles of minimalism entirely. Rather than adding a fun filter or refactoring a font, consider how to make the existing composition more cohesive with the art. If the instinct is to call a new function or draw a new shape, STOP and instead ask: "How can I make what's already here more of a piece of art?" + +Take a second pass. Go back to the code and refine/polish further to make this a philosophically designed masterpiece. + +## MULTI-PAGE OPTION + +To create additional pages when requested, create more creative pages along the same lines as the design philosophy but distinctly different as well. Bundle those pages in the same .pdf or many .pngs. Treat the first page as just a single page in a whole coffee table book waiting to be filled. Make the next pages unique twists and memories of the original. Have them almost tell a story in a very tasteful way. Exercise full creative freedom. \ No newline at end of file diff --git a/.agents/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt new file mode 100644 index 00000000..1dad6ca6 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2012 The Arsenal Project Authors (andrij.design@gmail.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf new file mode 100644 index 00000000..fe5409b2 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf new file mode 100644 index 00000000..fc5f8fdd Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt new file mode 100644 index 00000000..b220280e --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Big Shoulders Project Authors (https://github.com/xotypeco/big_shoulders) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf new file mode 100644 index 00000000..de8308ce Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt new file mode 100644 index 00000000..1890cb1c --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2024 The Boldonse Project Authors (https://github.com/googlefonts/boldonse) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf new file mode 100644 index 00000000..43fa30af Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf new file mode 100644 index 00000000..f3b1deda Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt new file mode 100644 index 00000000..fc2b2167 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Bricolage Grotesque Project Authors (https://github.com/ateliertriay/bricolage) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf new file mode 100644 index 00000000..0674ae3e Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf new file mode 100644 index 00000000..58730fb4 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf b/.agents/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf new file mode 100644 index 00000000..786a1bd6 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt new file mode 100644 index 00000000..f976fdc9 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2018 The Crimson Pro Project Authors (https://github.com/Fonthausen/CrimsonPro) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf new file mode 100644 index 00000000..f5666b9b Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/DMMono-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/DMMono-OFL.txt new file mode 100644 index 00000000..5b17f0c6 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/DMMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The DM Mono Project Authors (https://www.github.com/googlefonts/dm-mono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf new file mode 100644 index 00000000..7efe813d Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt new file mode 100644 index 00000000..490d0120 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt @@ -0,0 +1,94 @@ +Copyright (c) 2011 by LatinoType Limitada (luciano@latinotype.com), +with Reserved Font Names "Erica One" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf new file mode 100644 index 00000000..8bd91d11 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf new file mode 100644 index 00000000..736ff7c3 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt new file mode 100644 index 00000000..679a685a --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf new file mode 100644 index 00000000..1a30262a Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Gloock-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/Gloock-OFL.txt new file mode 100644 index 00000000..363acd33 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/Gloock-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Gloock Project Authors (https://github.com/duartp/gloock) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf new file mode 100644 index 00000000..3e58c4e4 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf new file mode 100644 index 00000000..247979ca Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt new file mode 100644 index 00000000..e423b747 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf new file mode 100644 index 00000000..601ae945 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf new file mode 100644 index 00000000..78f6e500 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf b/.agents/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf new file mode 100644 index 00000000..369b89d2 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf b/.agents/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf new file mode 100644 index 00000000..a4d859a7 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf new file mode 100644 index 00000000..35f454ce Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf new file mode 100644 index 00000000..f602dcef Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf b/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf new file mode 100644 index 00000000..122b2730 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf b/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf new file mode 100644 index 00000000..4b98fb8d Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt new file mode 100644 index 00000000..4bb99142 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf new file mode 100644 index 00000000..14c6113c Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf b/.agents/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf new file mode 100644 index 00000000..8fa958d9 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf new file mode 100644 index 00000000..97630318 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Italiana-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/Italiana-OFL.txt new file mode 100644 index 00000000..ba8af215 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/Italiana-OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2011, Santiago Orozco (hi@typemade.mx), with Reserved Font Name "Italiana". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf new file mode 100644 index 00000000..a9b828c0 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf new file mode 100644 index 00000000..1926c804 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt new file mode 100644 index 00000000..5ceee002 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 00000000..436c982f Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Jura-Light.ttf b/.agents/skills/canvas-design/canvas-fonts/Jura-Light.ttf new file mode 100644 index 00000000..dffbb339 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Jura-Light.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Jura-Medium.ttf b/.agents/skills/canvas-design/canvas-fonts/Jura-Medium.ttf new file mode 100644 index 00000000..4bf91a33 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Jura-Medium.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Jura-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/Jura-OFL.txt new file mode 100644 index 00000000..64ad4c67 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/Jura-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Jura Project Authors (https://github.com/ossobuffo/jura) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt new file mode 100644 index 00000000..8c531fa5 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2012 The Libre Baskerville Project Authors (https://github.com/impallari/Libre-Baskerville) with Reserved Font Name Libre Baskerville. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf new file mode 100644 index 00000000..c1abc264 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Lora-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/Lora-Bold.ttf new file mode 100644 index 00000000..edae21eb Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Lora-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf b/.agents/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf new file mode 100644 index 00000000..12dea8c6 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Lora-Italic.ttf b/.agents/skills/canvas-design/canvas-fonts/Lora-Italic.ttf new file mode 100644 index 00000000..e24b69b2 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Lora-Italic.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Lora-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/Lora-OFL.txt new file mode 100644 index 00000000..4cf1b950 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/Lora-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Lora Project Authors (https://github.com/cyrealtype/Lora-Cyrillic), with Reserved Font Name "Lora". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/Lora-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/Lora-Regular.ttf new file mode 100644 index 00000000..dc751db0 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Lora-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf new file mode 100644 index 00000000..f4d7c021 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt new file mode 100644 index 00000000..f4ec3fba --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2025 The National Park Project Authors (https://github.com/benhoepner/National-Park) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf new file mode 100644 index 00000000..e4cbfbf5 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt new file mode 100644 index 00000000..c81eccde --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2010, Kimberly Geswein (kimberlygeswein.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf new file mode 100644 index 00000000..b086bced Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf new file mode 100644 index 00000000..f9f2f72a Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Outfit-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/Outfit-OFL.txt new file mode 100644 index 00000000..fd0cb995 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/Outfit-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2021 The Outfit Project Authors (https://github.com/Outfitio/Outfit-Fonts) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf new file mode 100644 index 00000000..3939ab24 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf b/.agents/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf new file mode 100644 index 00000000..95cd3725 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt new file mode 100644 index 00000000..b02d1b67 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2021 The Pixelify Sans Project Authors (https://github.com/eifetx/Pixelify-Sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt new file mode 100644 index 00000000..607bdad3 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2011, Denis Masharov (denis.masharov@gmail.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf new file mode 100644 index 00000000..b339511b Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf new file mode 100644 index 00000000..a6e3cf15 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt new file mode 100644 index 00000000..16cf394b --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2024 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf new file mode 100644 index 00000000..3bf6a698 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt new file mode 100644 index 00000000..a1fe7d5f --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2001 The Silkscreen Project Authors (https://github.com/googlefonts/silkscreen) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf new file mode 100644 index 00000000..8abaa7c5 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf b/.agents/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf new file mode 100644 index 00000000..0af9ead0 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt new file mode 100644 index 00000000..4c2f033a --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Smooch Sans Project Authors (https://github.com/googlefonts/smooch-sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf b/.agents/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf new file mode 100644 index 00000000..34fc7971 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/Tektur-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/Tektur-OFL.txt new file mode 100644 index 00000000..2cad55f1 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/Tektur-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2023 The Tektur Project Authors (https://www.github.com/hyvyys/Tektur) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf new file mode 100644 index 00000000..f280fba4 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf b/.agents/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf new file mode 100644 index 00000000..5c979892 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf b/.agents/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf new file mode 100644 index 00000000..54418b8a Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf b/.agents/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf new file mode 100644 index 00000000..40529b68 Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt new file mode 100644 index 00000000..070f3416 --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf new file mode 100644 index 00000000..d24586cc Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf differ diff --git a/.agents/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt b/.agents/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt new file mode 100644 index 00000000..f09443cb --- /dev/null +++ b/.agents/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2023 The Young Serif Project Authors (https://github.com/noirblancrouge/YoungSerif) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.agents/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf b/.agents/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf new file mode 100644 index 00000000..f454fbed Binary files /dev/null and b/.agents/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf differ diff --git a/.agents/skills/career-changer-translator/SKILL.md b/.agents/skills/career-changer-translator/SKILL.md new file mode 100644 index 00000000..d88abdb3 --- /dev/null +++ b/.agents/skills/career-changer-translator/SKILL.md @@ -0,0 +1,368 @@ +--- +name: career-changer-translator +description: Translate skills from one industry to another, identify transferable skills +--- + +# Career Changer Translator + +## When to Use This Skill + +Use this skill when the user: +- Is switching careers or industries +- Wants to translate their experience for a new field +- Needs help identifying transferable skills +- Mentions: "career change", "switching careers", "new industry", "transferable skills", "pivot" + +## Core Capabilities + +- Identify transferable skills across industries +- Translate experience into new industry language +- Reframe achievements for target roles +- Bridge skill gaps strategically +- Position career changes positively +- Create compelling change narratives + +## The Career Change Challenge + +**Why Career Changes Are Hard:** +- Experience doesn't directly map to new role +- Lacking industry-specific keywords +- Recruiters prefer "safe" candidates +- Need to explain "why" convincingly +- Competing against people with direct experience + +**The Solution:** +- Focus on transferable skills +- Learn the new industry's language +- Leverage adjacent experiences +- Build bridge experiences +- Tell a compelling story + +## Transferable Skills Framework + +### Universal Transferable Skills + +**Leadership & Management** +- Team leadership +- Project management +- Budget oversight +- Strategic planning +- Performance management +- Stakeholder management + +**Communication** +- Presentation skills +- Written communication +- Client relations +- Cross-functional collaboration +- Negotiation +- Conflict resolution + +**Analytical** +- Data analysis +- Problem-solving +- Research +- Process improvement +- Decision-making +- Strategic thinking + +**Technical/Operational** +- Process design +- Systems implementation +- Quality assurance +- Training and development +- Vendor management +- Operations management + +## Career Change Translation Examples + +### Teacher → Corporate Trainer / L&D + +**Teaching Experience:** +- "Taught 25 students in 5th grade classroom" + +**Translated:** +- "Designed and delivered curriculum for 25 learners, achieving 95% proficiency on standardized assessments through differentiated instruction and data-driven intervention strategies" + +**Key Translations:** +| Teaching Term | Corporate Term | +|--------------|----------------| +| Lesson plans | Training curriculum | +| Students | Learners | +| Classroom management | Group facilitation | +| Parent conferences | Stakeholder communication | +| Assessments | Learning evaluations | +| IEPs | Individual development plans | + +### Military → Corporate + +**Military Experience:** +- "Commanded platoon of 30 soldiers in combat operations" + +**Translated:** +- "Led cross-functional team of 30 through high-stakes operations in ambiguous environments, managing $2M in equipment and achieving 100% mission completion rate" + +**Key Translations:** +| Military Term | Corporate Term | +|--------------|----------------| +| Platoon/Unit | Team/Department | +| Mission | Project/Initiative | +| Operations | Programs | +| Intel | Data/Analytics | +| Briefing | Presentation | +| Command | Leadership | +| Deployment | Implementation | + +### Retail → Sales/Account Management + +**Retail Experience:** +- "Sold products to customers and met sales targets" + +**Translated:** +- "Consistently exceeded sales targets by 125%, generating $500K annual revenue through consultative selling and relationship building with 50+ repeat customers" + +**Key Translations:** +| Retail Term | Corporate Term | +|------------|----------------| +| Customers | Clients/Accounts | +| Store sales | Revenue generation | +| Customer service | Account management | +| Upselling | Cross-selling | +| Returns | Issue resolution | +| Visual merchandising | Brand presentation | + +### Hospitality → Customer Success + +**Hospitality Experience:** +- "Managed front desk and handled guest complaints" + +**Translated:** +- "Served as primary customer contact for 100+ daily guests, resolving escalated issues with 95% satisfaction rate and implementing feedback processes that improved NPS by 15 points" + +**Key Translations:** +| Hospitality Term | Corporate Term | +|-----------------|----------------| +| Guests | Customers/Clients | +| Reservations | Account management | +| Guest satisfaction | Customer success | +| Complaints | Escalations | +| Concierge services | Customer support | +| Event planning | Project management | + +### Healthcare → Tech/Pharma + +**Healthcare Experience:** +- "Provided patient care and maintained medical records" + +**Translated:** +- "Delivered patient-centered care to 20+ daily cases, maintaining 100% compliance with HIPAA protocols and utilizing EMR systems (Epic) for accurate documentation and care coordination" + +**Key Translations:** +| Healthcare Term | Corporate Term | +|----------------|----------------| +| Patient care | Client service | +| EMR/EHR | CRM/Database systems | +| Care coordination | Project coordination | +| Clinical protocols | Standard operating procedures | +| Patient outcomes | Performance metrics | +| Rounds | Status meetings | + +## The Career Change Resume Strategy + +### 1. Lead with Transferable Summary + +**Format:** +"[Target Role] professional with [X] years of [transferable skill] experience. Background in [previous field] provides unique perspective on [relevant aspect]. Skilled in [transferable skill 1], [transferable skill 2], and [transferable skill 3]." + +**Example (Teacher → L&D):** +"Learning & Development professional with 8 years designing and delivering curriculum for diverse audiences. Background in education provides deep expertise in learning science, assessment design, and adult learning principles. Skilled in instructional design, facilitation, and measuring learning outcomes." + +### 2. Use a Functional/Hybrid Resume Format + +**Structure:** +``` +Professional Summary (targeted) +Core Competencies (transferable skills) +Relevant Experience (grouped by skill) +Additional Experience (chronological) +Education & Certifications +``` + +**Group by Transferable Function:** +``` +PROJECT MANAGEMENT EXPERIENCE +[Bullet from Job 1] +[Bullet from Job 2] +[Bullet from Job 3] + +LEADERSHIP & TEAM DEVELOPMENT +[Bullet from Job 1] +[Bullet from Job 2] + +CLIENT RELATIONSHIP MANAGEMENT +[Bullet from Job 1] +[Bullet from Job 2] +``` + +### 3. Build Bridge Experiences + +**Ways to Add Relevant Experience:** +- Volunteer work in target field +- Freelance/consulting projects +- Relevant certifications +- Side projects +- Professional organizations +- Coursework + +**Example:** +``` +BRIDGE EXPERIENCE + +Volunteer Marketing Lead | Nonprofit XYZ | 2023-Present +- Designed social media strategy increasing engagement by 150% +- Created content calendar and managed 3 volunteer content creators + +Google Digital Marketing Certificate | 2023 +- Completed 200+ hour program covering SEO, SEM, Analytics, and Social Media +``` + +## Addressing the "Why" Question + +**You will be asked:** "Why are you making this change?" + +**Good Reasons:** +- Natural evolution of interests +- Discovered passion through exposure +- Specific experience that sparked interest +- Skills naturally translate +- Industry changes made you reconsider +- Seeking growth not available in current path + +**Bad Reasons (Don't Say):** +- "I'm burned out" +- "I hate my current job" +- "I want more money" +- "It seems easier" + +### Story Framework + +``` +[DISCOVERY]: "Through [experience], I discovered my passion for [new field]..." + +[CONNECTION]: "I realized that my experience in [old field] gave me a unique perspective on [new field] because..." + +[ACTION]: "To prepare for this transition, I've [certifications, projects, volunteering]..." + +[VISION]: "I'm excited to bring my [transferable skill] expertise to [target company/industry] because..." +``` + +## Industry-Specific Career Change Paths + +### Into Tech + +**Best Entry Points:** +- Customer Success (from any client-facing role) +- Technical Project Manager (from any PM role) +- Sales/Account Executive (from any sales role) +- Product Management (from domain expertise) +- Technical Writing (from any writing role) + +**Bridge Activities:** +- Learn SQL, basic coding, or relevant tools +- Get certifications (Google, Salesforce, AWS) +- Build personal projects +- Attend tech meetups +- Volunteer for tech nonprofits + +### Into Finance + +**Best Entry Points:** +- Operations (from any process-oriented role) +- Compliance (from any regulated industry) +- Client Services (from relationship roles) +- Analysis (from any analytical role) + +**Bridge Activities:** +- Excel/financial modeling certifications +- CFA or CFP study +- Financial analysis courses +- Industry networking + +### Into Healthcare + +**Best Entry Points:** +- Healthcare Administration (from any admin role) +- Medical Sales (from any sales role) +- Health IT (from any tech role) +- Patient Advocacy (from customer service) + +**Bridge Activities:** +- Healthcare certifications +- HIPAA training +- Healthcare industry knowledge +- Volunteer at hospitals + +## Output Format + +When helping a career changer: + +```markdown +# CAREER CHANGE TRANSLATION + +## Current Background +**Field:** [Current industry] +**Role:** [Current function] +**Years:** [Experience] + +## Target +**Field:** [Target industry] +**Role:** [Target function] + +## Transferable Skills Identified +1. [Skill] → Relevant because: [explanation] +2. [Skill] → Relevant because: [explanation] +3. [Skill] → Relevant because: [explanation] + +## Translation Guide + +### Experience Reframes + +**Original:** "[Their current bullet]" +**Translated:** "[Reframed for target industry]" +**Key changes:** [What was changed and why] + +[Repeat for key bullets] + +### Language Translations +| Your Current Language | Target Industry Language | +|----------------------|--------------------------| +| [Term] | [Translated term] | +| [Term] | [Translated term] | + +## Gap Analysis + +**Skills to Develop:** +- [Gap] → Suggestion: [How to bridge] +- [Gap] → Suggestion: [How to bridge] + +**Bridge Experiences to Consider:** +- [Suggestion] +- [Suggestion] + +## Your Career Change Story + +[Draft of their "why" narrative] + +## Recommended Resume Structure +[Guidance on format and organization] +``` + +## Key Reminders + +1. **Lead with transferable skills**, not job titles +2. **Use target industry language** - translate everything +3. **Build bridges** - get any experience in target field +4. **Tell a compelling story** - make the change make sense +5. **Focus on value you bring** - your unique perspective is an asset +6. **Address concerns proactively** - don't let them wonder "why" +7. **Network in target field** - referrals help career changers most diff --git a/.agents/skills/claude-api/LICENSE.txt b/.agents/skills/claude-api/LICENSE.txt new file mode 100644 index 00000000..4f881c52 --- /dev/null +++ b/.agents/skills/claude-api/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Anthropic, PBC. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/.agents/skills/claude-api/SKILL.md b/.agents/skills/claude-api/SKILL.md new file mode 100644 index 00000000..a9737a83 --- /dev/null +++ b/.agents/skills/claude-api/SKILL.md @@ -0,0 +1,335 @@ +--- +name: Codex-api +description: |- + Reference for the Codex API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration. + TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Codex/Anthropic in any form (Codex, Anthropic, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `Codex-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens). + SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file). +license: Complete terms in LICENSE.txt +--- + +# Building LLM-Powered Applications with Codex + +This skill helps you build LLM-powered applications with Codex. Choose the right surface based on your needs, detect the project language, then read the relevant language-specific documentation. + +## Before You Start + +Scan the target file (or, if no target file, the prompt and project) for non-Anthropic provider markers — `import openai`, `from openai`, `langchain_openai`, `OpenAI(`, `gpt-4`, `gpt-5`, file names like `agent-openai.py` or `*-generic.py`, or any explicit instruction to keep the code provider-neutral. If you find any, stop and tell the user that this skill produces Codex/Anthropic SDK code; ask whether they want to switch the file to Codex or want a non-Codex implementation. Do not edit a non-Anthropic file with Anthropic SDK calls. + +## Output Requirement + +When the user asks you to add, modify, or implement a Codex feature, your code must call Codex through one of: + +1. **The official Anthropic SDK** for the project's language (`anthropic`, `@anthropic-ai/sdk`, `com.anthropic.*`, etc.). This is the default whenever a supported SDK exists for the project. +2. **Raw HTTP** (`curl`, `requests`, `fetch`, `httpx`, etc.) — only when the user explicitly asks for cURL/REST/raw HTTP, the project is a shell/cURL project, or the language has no official SDK. + +Never mix the two — don't reach for `requests`/`fetch` in a Python or TypeScript project just because it feels lighter. Never fall back to OpenAI-compatible shims. + +**Never guess SDK usage.** Function names, class names, namespaces, method signatures, and import paths must come from explicit documentation — either the `{lang}/` files in this skill or the official SDK repositories or documentation links listed in `shared/live-sources.md`. If the binding you need is not explicitly documented in the skill files, WebFetch the relevant SDK repo from `shared/live-sources.md` before writing code. Do not infer Ruby/Java/Go/PHP/C# APIs from cURL shapes or from another language's SDK. + +## Defaults + +Unless the user requests otherwise: + +For the Codex model version, please use Codex Opus 4.8, which you can access via the exact model string `Codex-opus-4-8`. Please default to using adaptive thinking (`thinking: {type: "adaptive"}`) for anything remotely complicated. And finally, please default to streaming for any request that may involve long input, long output, or high `max_tokens` — it prevents hitting request timeouts. Use the SDK's `.get_final_message()` / `.finalMessage()` helper to get the complete response if you don't need to handle individual stream events + +--- + +## Subcommands + +If the User Request at the bottom of this prompt is a bare subcommand string (no prose), search every **Subcommands** table in this document — including any in sections appended below — and follow the matching Action column directly. This lets users invoke specific flows via `/Codex-api `. If no table in the document matches, treat the request as normal prose. + +| Subcommand | Action | +|---|---| +| `migrate` | Migrate existing Codex API code to a newer model. **Read `shared/model-migration.md` immediately** and follow it in order: Step 0 (confirm scope — ask which files/directories before any edit), Step 1 (classify each file), then the per-target breaking-changes section. Do not summarize the guide — execute it. If the user did not name a target model, ask which model to migrate to in the same turn as the scope question. | + +--- + +## Language Detection + +Before reading code examples, determine which language the user is working in: + +1. **Look at project files** to infer the language: + + - `*.py`, `requirements.txt`, `pyproject.toml`, `setup.py`, `Pipfile` → **Python** — read from `python/` + - `*.ts`, `*.tsx`, `package.json`, `tsconfig.json` → **TypeScript** — read from `typescript/` + - `*.js`, `*.jsx` (no `.ts` files present) → **TypeScript** — JS uses the same SDK, read from `typescript/` + - `*.java`, `pom.xml`, `build.gradle` → **Java** — read from `java/` + - `*.kt`, `*.kts`, `build.gradle.kts` → **Java** — Kotlin uses the Java SDK, read from `java/` + - `*.scala`, `build.sbt` → **Java** — Scala uses the Java SDK, read from `java/` + - `*.go`, `go.mod` → **Go** — read from `go/` + - `*.rb`, `Gemfile` → **Ruby** — read from `ruby/` + - `*.cs`, `*.csproj` → **C#** — read from `csharp/` + - `*.php`, `composer.json` → **PHP** — read from `php/` + +2. **If multiple languages detected** (e.g., both Python and TypeScript files): + + - Check which language the user's current file or question relates to + - If still ambiguous, ask: "I detected both Python and TypeScript files. Which language are you using for the Codex API integration?" + +3. **If language can't be inferred** (empty project, no source files, or unsupported language): + + - Use AskUserQuestion with options: Python, TypeScript, Java, Go, Ruby, cURL/raw HTTP, C#, PHP + - If AskUserQuestion is unavailable, default to Python examples and note: "Showing Python examples. Let me know if you need a different language." + +4. **If unsupported language detected** (Rust, Swift, C++, Elixir, etc.): + + - Suggest cURL/raw HTTP examples from `curl/` and note that community SDKs may exist + - Offer to show Python or TypeScript examples as reference implementations + +5. **If user needs cURL/raw HTTP examples**, read from `curl/`. + +### Language-Specific Feature Support + +| Language | Tool Runner | Managed Agents | Notes | +| ---------- | ----------- | -------------- | ------------------------------------- | +| Python | Yes (beta) | Yes (beta) | Full support — `@beta_tool` decorator | +| TypeScript | Yes (beta) | Yes (beta) | Full support — `betaZodTool` + Zod | +| Java | Yes (beta) | Yes (beta) | Beta tool use with annotated classes | +| Go | Yes (beta) | Yes (beta) | `BetaToolRunner` in `toolrunner` pkg | +| Ruby | Yes (beta) | Yes (beta) | `BaseTool` + `tool_runner` in beta | +| C# | Yes (beta) | Yes (beta) | `BetaToolRunner` + raw JSON schema | +| PHP | Yes (beta) | Yes (beta) | `BetaRunnableTool` + `toolRunner()` | +| cURL | N/A | Yes (beta) | Raw HTTP, no SDK features | + +> **Managed Agents code examples**: dedicated language-specific READMEs are provided for Python, TypeScript, Go, Ruby, PHP, Java, and cURL (`{lang}/managed-agents/README.md`, `curl/managed-agents.md`). Read your language's README plus the language-agnostic `shared/managed-agents-*.md` concept files. **Agents are persistent — create once, reference by ID.** Store the agent ID returned by `agents.create` and pass it to every subsequent `sessions.create`; do not call `agents.create` in the request path. The Anthropic CLI (`ant`) is one convenient way to create agents and environments from version-controlled YAML — see `shared/anthropic-cli.md`. If a binding you need isn't shown in the README, WebFetch the relevant entry from `shared/live-sources.md` rather than guess. C# has beta Managed Agents support via `client.Beta.Agents` and related namespaces. + +--- + +## Which Surface Should I Use? + +> **Start simple.** Default to the simplest tier that meets your needs. Single API calls and workflows handle most use cases — only reach for agents when the task genuinely requires open-ended, model-driven exploration. + +| Use Case | Tier | Recommended Surface | Why | +| ----------------------------------------------- | --------------- | ------------------------- | ------------------------------------------------------------ | +| Classification, summarization, extraction, Q&A | Single LLM call | **Codex API** | One request, one response | +| Batch processing or embeddings | Single LLM call | **Codex API** | Specialized endpoints | +| Multi-step pipelines with code-controlled logic | Workflow | **Codex API + tool use** | You orchestrate the loop | +| Custom agent with your own tools | Agent | **Codex API + tool use** | Maximum flexibility | +| Server-managed stateful agent with workspace | Agent | **Managed Agents** | Anthropic runs the loop and hosts the tool-execution sandbox | +| Persisted, versioned agent configs | Agent | **Managed Agents** | Agents are stored objects; sessions pin to a version | +| Long-running multi-turn agent with file mounts | Agent | **Managed Agents** | Per-session containers, SSE event stream, Skills + MCP | + +> **Note:** Managed Agents is the right choice when you want Anthropic to run the agent loop *and* host the container where tools execute — file ops, bash, code execution all run in the per-session workspace. If you want to host the compute yourself or run your own custom tool runtime, Codex API + tool use is the right choice — use the tool runner for automatic loop handling, or the manual loop for fine-grained control (approval gates, custom logging, conditional execution). + +> **Cloud-provider access.** **Codex Platform on AWS** is Anthropic-operated with same-day API parity — Managed Agents and every feature in this skill work there, **except self-hosted sandboxes** (see `shared/Codex-platform-on-aws.md`). **Amazon Bedrock**, **Google Vertex AI**, and **Microsoft Foundry** do **not** support Managed Agents or Anthropic server-side tools; use **Codex API + tool use** on those. + +### Decision Tree + +``` +What does your application need? + +0. Which provider? + ├── First-party API or Codex Platform on AWS → continue (full surface available). + └── Amazon Bedrock, Google Vertex AI, or Microsoft Foundry → Codex API (+ tool use for agents); Managed Agents not available there. + +1. Single LLM call (classification, summarization, extraction, Q&A) + └── Codex API — one request, one response + +2. Do you want Anthropic to run the agent loop and host a per-session + container where Codex executes tools (bash, file ops, code)? + └── Yes → Managed Agents — server-managed sessions, persisted agent configs, + SSE event stream, Skills + MCP, file mounts. + Examples: "stateful coding agent with a workspace per task", + "long-running research agent that streams events to a UI", + "agent with persisted, versioned config used across many sessions" + +3. Workflow (multi-step, code-orchestrated, with your own tools) + └── Codex API with tool use — you control the loop + +4. Open-ended agent (model decides its own trajectory, your own tools, you host the compute) + └── Codex API agentic loop (maximum flexibility) +``` + +### Should I Build an Agent? + +Before choosing the agent tier, check all four criteria: + +- **Complexity** — Is the task multi-step and hard to fully specify in advance? (e.g., "turn this design doc into a PR" vs. "extract the title from this PDF") +- **Value** — Does the outcome justify higher cost and latency? +- **Viability** — Is Codex capable at this task type? +- **Cost of error** — Can errors be caught and recovered from? (tests, review, rollback) + +If the answer is "no" to any of these, stay at a simpler tier (single call or workflow). + +--- + +## Architecture + +Everything goes through `POST /v1/messages`. Tools and output constraints are features of this single endpoint — not separate APIs. + +**User-defined tools** — You define tools (via decorators, Zod schemas, or raw JSON), and the SDK's tool runner handles calling the API, executing your functions, and looping until Codex is done. For full control, you can write the loop manually. + +**Server-side tools** — Anthropic-hosted tools that run on Anthropic's infrastructure. Code execution is fully server-side (declare it in `tools`, Codex runs code automatically). Computer use can be server-hosted or self-hosted. + +**Structured outputs** — Constrains the Messages API response format (`output_config.format`) and/or tool parameter validation (`strict: true`). The recommended approach is `client.messages.parse()` which validates responses against your schema automatically. Note: the old `output_format` parameter is deprecated; use `output_config: {format: {...}}` on `messages.create()`. + +**Supporting endpoints** — Batches (`POST /v1/messages/batches`), Files (`POST /v1/files`), Token Counting (`POST /v1/messages/count_tokens` — see `shared/token-counting.md`), and Models (`GET /v1/models`, `GET /v1/models/{id}` — live capability/context-window discovery) feed into or support Messages API requests. + +--- + +## Current Models (cached: 2026-05-26) + +| Model | Model ID | Context | Input $/1M | Output $/1M | +| ----------------- | ------------------- | -------------- | ---------- | ----------- | +| Codex Opus 4.8 | `Codex-opus-4-8` | 1M | $5.00 | $25.00 | +| Codex Opus 4.7 | `Codex-opus-4-7` | 1M | $5.00 | $25.00 | +| Codex Opus 4.6 | `Codex-opus-4-6` | 1M | $5.00 | $25.00 | +| Codex Sonnet 4.6 | `Codex-sonnet-4-6` | 1M | $3.00 | $15.00 | +| Codex Haiku 4.5 | `Codex-haiku-4-5` | 200K | $1.00 | $5.00 | + +**ALWAYS use `Codex-opus-4-8` unless the user explicitly names a different model.** This is non-negotiable. Do not use `Codex-sonnet-4-6`, `Codex-sonnet-4-5`, or any other model unless the user literally says "use sonnet" or "use haiku". Never downgrade for cost — that's the user's decision, not yours. + +**CRITICAL: Use only the exact model ID strings from the table above — they are complete as-is. Do not append date suffixes.** For example, use `Codex-sonnet-4-6`, never `Codex-sonnet-4-6-20251114` or any other date-suffixed variant you might recall from training data. If the user requests an older model not in the table (e.g., "opus 4.5", "sonnet 3.7"), read `shared/models.md` for the exact ID — do not construct one yourself. + +A note: if any of the model strings above look unfamiliar to you, that's to be expected — that just means they were released after your training data cutoff. Rest assured they are real models; we wouldn't mess with you like that. + +**Live capability lookup:** The table above is cached. When the user asks "what's the context window for X", "does X support vision/thinking/effort", or "which models support Y", query the Models API (`client.models.retrieve(id)` / `client.models.list()`) — see `shared/models.md` for the field reference and capability-filter examples. + +--- + +## Thinking & Effort (Quick Reference) + +**Opus 4.8 / 4.7 — Adaptive thinking only:** Use `thinking: {type: "adaptive"}`. `thinking: {type: "enabled", budget_tokens: N}` returns a 400 — adaptive is the only on-mode. `{type: "disabled"}` and omitting `thinking` both work. Sampling parameters (`temperature`, `top_p`, `top_k`) are also removed and will 400. Opus 4.8 keeps the same request surface as 4.7 (no new breaking changes) — see `shared/model-migration.md` → Migrating to Opus 4.8 for the behavioral re-tuning, and → Migrating to Opus 4.7 for the full breaking-change list when coming from 4.6 or earlier. Note: with `thinking` disabled, Opus 4.8 may write longer reasoning into the visible response — leave adaptive thinking on, or add a final-answer-only instruction (see the migration guide). +**Opus 4.6 — Adaptive thinking (recommended):** Use `thinking: {type: "adaptive"}`. Codex dynamically decides when and how much to think. No `budget_tokens` needed — `budget_tokens` is deprecated on Opus 4.6 and Sonnet 4.6 and should not be used for new code. Adaptive thinking also automatically enables interleaved thinking (no beta header needed). **When the user asks for "extended thinking", a "thinking budget", or `budget_tokens`: always use Opus 4.8, 4.7, or 4.6 with `thinking: {type: "adaptive"}`. The concept of a fixed token budget for thinking is deprecated — adaptive thinking replaces it. Do NOT use `budget_tokens` for new 4.6/4.7/4.8 code and do NOT switch to an older model.** *Gradual-migration carve-out:* `budget_tokens` is still functional on Opus 4.6 and Sonnet 4.6 as a transitional escape hatch — if you're migrating existing code and need a hard token ceiling before you've tuned `effort`, see `shared/model-migration.md` → Transitional escape hatch. Note: this carve-out does **not** apply to Opus 4.7 or 4.8 — `budget_tokens` is fully removed there. +**Effort parameter (GA, no beta header):** Controls thinking depth and overall token spend via `output_config: {effort: "low"|"medium"|"high"|"max"}` (inside `output_config`, not top-level). Default is `high` (equivalent to omitting it). `max` is Opus-tier only (Opus 4.6 and later — not Sonnet or Haiku). Opus 4.7 added `"xhigh"` (between `high` and `max`) — the best setting for most coding and agentic use cases on Opus 4.7/4.8, and the default in Codex; use a minimum of `high` for most intelligence-sensitive work. Works on Opus 4.5, Opus 4.6, Opus 4.7, Opus 4.8, and Sonnet 4.6. Will error on Sonnet 4.5 / Haiku 4.5. On Opus 4.7 and 4.8, effort matters more than on any prior Opus — re-tune it when migrating, and run long-horizon/agentic tasks at `high`/`xhigh` with the full task spec given up front. Combine with adaptive thinking for the best cost-quality tradeoffs. Lower effort means fewer and more-consolidated tool calls, less preamble, and terser confirmations — `high` is often the sweet spot balancing quality and token efficiency; use `max` when correctness matters more than cost; use `low` for subagents or simple tasks. + +**Opus 4.8 / 4.7 — thinking content omitted by default:** `thinking` blocks still stream but their text is empty unless you opt in with `thinking: {type: "adaptive", display: "summarized"}` (default is `"omitted"`). Silent change — no error. If you stream reasoning to users, the default looks like a long pause before output; set `"summarized"` to restore visible progress. + +**Task Budgets (beta, Opus 4.7 / 4.8):** `output_config: {task_budget: {type: "tokens", total: N}}` tells the model how many tokens it has for a full agentic loop — it sees a running countdown and self-moderates (minimum 20,000; beta header `task-budgets-2026-03-13`). Distinct from `max_tokens`, which is an enforced per-response ceiling the model is not aware of. See `shared/model-migration.md` → Task Budgets. + +**Sonnet 4.6:** Supports adaptive thinking (`thinking: {type: "adaptive"}`). `budget_tokens` is deprecated on Sonnet 4.6 — use adaptive thinking instead. + +**Older models (only if explicitly requested):** If the user specifically asks for Sonnet 4.5 or another older model, use `thinking: {type: "enabled", budget_tokens: N}`. `budget_tokens` must be less than `max_tokens` (minimum 1024). Never choose an older model just because the user mentions `budget_tokens` — use Opus 4.8 with adaptive thinking instead. + +--- + +## Compaction (Quick Reference) + +**Beta, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6.** For long-running conversations that may exceed the 1M context window, enable server-side compaction. The API automatically summarizes earlier context when it approaches the trigger threshold (default: 150K tokens). Requires beta header `compact-2026-01-12`. + +**Critical:** Append `response.content` (not just the text) back to your messages on every turn. Compaction blocks in the response must be preserved — the API uses them to replace the compacted history on the next request. Extracting only the text string and appending that will silently lose the compaction state. + +See `{lang}/Codex-api/README.md` (Compaction section) for code examples. Full docs via WebFetch in `shared/live-sources.md`. + +--- + +## Prompt Caching (Quick Reference) + +**Prefix match.** Any byte change anywhere in the prefix invalidates everything after it. Render order is `tools` → `system` → `messages`. Keep stable content first (frozen system prompt, deterministic tool list), put volatile content (timestamps, per-request IDs, varying questions) after the last `cache_control` breakpoint. + +**Mid-conversation operator instructions** (beta header `mid-conversation-system-2026-04-07`, on supporting models): append `{"role": "system", ...}` to `messages[]` instead of editing top-level `system`. Preserves the cached history prefix and is the prompt-injection-safe operator channel. See `shared/prompt-caching.md` § Mid-conversation system messages. + +**Top-level auto-caching** (`cache_control: {type: "ephemeral"}` on `messages.create()`) is the simplest option when you don't need fine-grained placement. Max 4 breakpoints per request. Minimum cacheable prefix is ~1024 tokens — shorter prefixes silently won't cache. + +**Verify with `usage.cache_read_input_tokens`** — if it's zero across repeated requests, a silent invalidator is at work (`datetime.now()` in system prompt, unsorted JSON, varying tool set). + +For placement patterns, architectural guidance, and the silent-invalidator audit checklist: read `shared/prompt-caching.md`. Language-specific syntax: `{lang}/Codex-api/README.md` (Prompt Caching section). + +--- + +## Managed Agents (Beta) + +**Managed Agents** is a third surface: server-managed stateful agents with Anthropic-hosted tool execution. You create a persisted, versioned Agent config (`POST /v1/agents`), then start Sessions that reference it. Each session provisions a container as the agent's workspace — bash, file ops, and code execution run there; the agent loop itself runs on Anthropic's orchestration layer and acts on the container via tools. The session streams events; you send messages and tool results back. + +**Managed Agents is available on the first-party API and Codex Platform on AWS.** It is **not** available on Amazon Bedrock, Google Vertex AI, or Microsoft Foundry — for agents there, use Codex API + tool use. + +**Mandatory flow:** Agent (once) → Session (every run). `model`/`system`/`tools` live on the agent, never the session. See `shared/managed-agents-overview.md` for the full reading guide, beta headers, and pitfalls. + +**Beta headers:** `managed-agents-2026-04-01` — the SDK sets this automatically for all `client.beta.{agents,environments,sessions,vaults,memory_stores}.*` calls. Skills API uses `skills-2025-10-02` and Files API uses `files-api-2025-04-14`, but you don't need to explicitly pass those in for endpoints other than `/v1/skills` and `/v1/files`. + +**Subcommands** — invoke directly with `/Codex-api `: + +| Subcommand | Action | +|---|---| +| `managed-agents-onboard` | Walk the user through setting up a Managed Agent from scratch. **Read `shared/managed-agents-onboarding.md` immediately** and follow its interview script: mental model → know-or-explore branch → template config → session setup → **pre-flight viability check** → emit code. The viability check (reconcile the stated job against configured tools/credentials/data) catches under-resourced setups — missing a tool, credential, or data access — before the agent burns budget. Do not summarize — run the interview. | + +**Reading guide:** Start with `shared/managed-agents-overview.md`, then the topical `shared/managed-agents-*.md` files (core, environments, tools, events, outcomes, multiagent, webhooks, memory, client-patterns, onboarding, api-reference). For Python, TypeScript, Go, Ruby, PHP, and Java, read `{lang}/managed-agents/README.md` for code examples. For cURL, read `curl/managed-agents.md`. **Agents are persistent — create once, reference by ID.** Store the agent ID returned by `agents.create` and pass it to every subsequent `sessions.create`; do not call `agents.create` in the request path. The Anthropic CLI (`ant`) is one convenient way to create agents and environments from version-controlled YAML — see `shared/anthropic-cli.md`. If a binding you need isn't shown in the language README, WebFetch the relevant entry from `shared/live-sources.md` rather than guess. C# has beta Managed Agents support via `client.Beta.Agents` and related namespaces. + +**When the user wants to set up a Managed Agent from scratch** (e.g. "how do I get started", "walk me through creating one", "set up a new agent"): read `shared/managed-agents-onboarding.md` and run its interview — same flow as the `managed-agents-onboard` subcommand. + +**When the user asks "how do I write the client code for X":** reach for `shared/managed-agents-client-patterns.md` — covers lossless stream reconnect, `processed_at` queued/processed gate, interrupt, `tool_confirmation` round-trip, the correct idle/terminated break gate, post-idle status race, stream-first ordering, file-mount gotchas, keeping credentials host-side via custom tools, etc. + +--- + +## Reading Guide + +After detecting the language, read the relevant files based on what the user needs: + +### Quick Task Reference + +**Single text classification/summarization/extraction/Q&A:** +→ Read only `{lang}/Codex-api/README.md` + +**Chat UI or real-time response display:** +→ Read `{lang}/Codex-api/README.md` + `{lang}/Codex-api/streaming.md` + +**Long-running conversations (may exceed context window):** +→ Read `{lang}/Codex-api/README.md` — see Compaction section +**Migrating to a newer model (Opus 4.8 / Opus 4.7 / Opus 4.6 / Sonnet 4.6) or replacing a retired model:** +→ Read `shared/model-migration.md` +**Prompt caching / optimize caching / "why is my cache hit rate low":** +→ Read `shared/prompt-caching.md` + `{lang}/Codex-api/README.md` (Prompt Caching section) +**Count tokens in a file / prompt / diff ("how many tokens is X"):** +→ Read `shared/token-counting.md` — use `messages.count_tokens`, never `tiktoken` + +**Function calling / tool use / agents:** +→ Read `{lang}/Codex-api/README.md` + `shared/tool-use-concepts.md` + `{lang}/Codex-api/tool-use.md` + +**Agent design (tool surface, context management, caching strategy):** +→ Read `shared/agent-design.md` + +**Batch processing (non-latency-sensitive):** +→ Read `{lang}/Codex-api/README.md` + `{lang}/Codex-api/batches.md` + +**File uploads across multiple requests:** +→ Read `{lang}/Codex-api/README.md` + `{lang}/Codex-api/files-api.md` + +**Managed Agents (server-managed stateful agents with workspace):** +→ Read `shared/managed-agents-overview.md` + the rest of the `shared/managed-agents-*.md` files. For Python, TypeScript, Go, Ruby, PHP, and Java, read `{lang}/managed-agents/README.md` for code examples. For cURL, read `curl/managed-agents.md`. **Agents are persistent — create once, reference by ID.** Store the agent ID returned by `agents.create` and pass it to every subsequent `sessions.create`; do not call `agents.create` in the request path. The Anthropic CLI (`ant`) is one convenient way to create agents and environments from version-controlled YAML — see `shared/anthropic-cli.md`. If a binding you need isn't shown in the language README, WebFetch the relevant entry from `shared/live-sources.md` rather than guess. C# has beta Managed Agents support — see `csharp/Codex-api.md` for details, or `curl/managed-agents.md` for raw HTTP reference. + +### Codex API (Full File Reference) + +Read the **language-specific Codex API folder** (`{language}/Codex-api/`): + +1. **`{language}/Codex-api/README.md`** — **Read this first.** Installation, quick start, common patterns, error handling. +2. **`shared/tool-use-concepts.md`** — Read when the user needs function calling, code execution, memory, or structured outputs. Covers conceptual foundations. +3. **`shared/agent-design.md`** — Read when designing an agent: bash vs. dedicated tools, programmatic tool calling, tool search/skills, context editing vs. compaction vs. memory, caching principles. +4. **`{language}/Codex-api/tool-use.md`** — Read for language-specific tool use code examples (tool runner, manual loop, code execution, memory, structured outputs). +5. **`{language}/Codex-api/streaming.md`** — Read when building chat UIs or interfaces that display responses incrementally. +6. **`{language}/Codex-api/batches.md`** — Read when processing many requests offline (not latency-sensitive). Runs asynchronously at 50% cost. +7. **`{language}/Codex-api/files-api.md`** — Read when sending the same file across multiple requests without re-uploading. +8. **`shared/prompt-caching.md`** — Read when adding or optimizing prompt caching. Covers prefix-stability design, breakpoint placement, and anti-patterns that silently invalidate cache. +9. **`shared/error-codes.md`** — Read when debugging HTTP errors or implementing error handling. +10. **`shared/model-migration.md`** — Read when upgrading to newer models, replacing retired models, or translating `budget_tokens` / prefill patterns to the current API. +11. **`shared/live-sources.md`** — WebFetch URLs for fetching the latest official documentation. + +> **Note:** For Java, Go, Ruby, C#, PHP, and cURL — these have a single file each covering all basics. Read that file plus `shared/tool-use-concepts.md` and `shared/error-codes.md` as needed. + +> **Note:** For the Managed Agents file reference, see the `## Managed Agents (Beta)` section above — it lists every `shared/managed-agents-*.md` file and the language-specific READMEs. + +--- + +## When to Use WebFetch + +Use WebFetch to get the latest documentation when: + +- User asks for "latest" or "current" information +- Cached data seems incorrect +- User asks about features not covered here + +Live documentation URLs are in `shared/live-sources.md`. + +## Common Pitfalls + +- Don't truncate inputs when passing files or content to the API. If the content is too long to fit in the context window, notify the user and discuss options (chunking, summarization, etc.) rather than silently truncating. +- **Opus 4.8 / 4.7 thinking:** Adaptive only. `thinking: {type: "enabled", budget_tokens: N}` returns 400 — `budget_tokens` is fully removed (along with `temperature`, `top_p`, `top_k`). Use `thinking: {type: "adaptive"}`. Opus 4.8 inherits this surface from 4.7 with no new breaking changes. +- **Opus 4.6 / Sonnet 4.6 thinking:** Use `thinking: {type: "adaptive"}` — do NOT use `budget_tokens` for new 4.6 code (deprecated on both Opus 4.6 and Sonnet 4.6; for gradual migration of existing code, see the transitional escape hatch in `shared/model-migration.md` — note this carve-out does not apply to Opus 4.7 or 4.8). For older models, `budget_tokens` must be less than `max_tokens` (minimum 1024). This will throw an error if you get it wrong. +- **4.6/4.7/4.8 family prefill removed:** Assistant message prefills (last-assistant-turn prefills) return a 400 error on Opus 4.6, Opus 4.7, Opus 4.8, and Sonnet 4.6. Use structured outputs (`output_config.format`) or system prompt instructions to control response format instead. +- **Confirm migration scope before editing:** When a user asks to migrate code to a newer Codex model without naming a specific file, directory, or file list, **ask which scope to apply first** — the entire working directory, a specific subdirectory, or a specific set of files. Do not start editing until the user confirms. Imperative phrasings like "migrate my codebase", "move my project to X", "upgrade to Sonnet 4.6", or bare "migrate to Opus 4.8" are **still ambiguous** — they tell you what to do but not where, so ask. Proceed without asking only when the prompt names an exact file, a specific directory, or an explicit file list ("migrate `app.py`", "migrate everything under `services/`", "update `a.py` and `b.py`"). See `shared/model-migration.md` Step 0. +- **`max_tokens` defaults:** Don't lowball `max_tokens` — hitting the cap truncates output mid-thought and requires a retry. For non-streaming requests, default to `~16000` (keeps responses under SDK HTTP timeouts). For streaming requests, default to `~64000` (timeouts aren't a concern, so give the model room). Only go lower when you have a hard reason: classification (`~256`), cost caps, deliberately short outputs, or **`max_tokens: 0`** for cache pre-warming (see `shared/prompt-caching.md` → Pre-warming). +- **128K output tokens:** Opus 4.6, Opus 4.7, and Opus 4.8 support up to 128K `max_tokens`, but the SDKs require streaming for values that large to avoid HTTP timeouts. Use `.stream()` with `.get_final_message()` / `.finalMessage()`. +- **Tool call JSON parsing (4.6/4.7/4.8 family):** Opus 4.6, Opus 4.7, Opus 4.8, and Sonnet 4.6 may produce different JSON string escaping in tool call `input` fields (e.g., Unicode or forward-slash escaping). Always parse tool inputs with `json.loads()` / `JSON.parse()` — never do raw string matching on the serialized input. +- **Structured outputs (all models):** Use `output_config: {format: {...}}` instead of the deprecated `output_format` parameter on `messages.create()`. This is a general API change, not 4.6-specific. +- **Don't reimplement SDK functionality:** The SDK provides high-level helpers — use them instead of building from scratch. Specifically: use `stream.finalMessage()` instead of wrapping `.on()` events in `new Promise()`; use typed exception classes (`Anthropic.RateLimitError`, etc.) instead of string-matching error messages; use SDK types (`Anthropic.MessageParam`, `Anthropic.Tool`, `Anthropic.Message`, etc.) instead of redefining equivalent interfaces. +- **Don't define custom types for SDK data structures:** The SDK exports types for all API objects. Use `Anthropic.MessageParam` for messages, `Anthropic.Tool` for tool definitions, `Anthropic.ToolUseBlock` / `Anthropic.ToolResultBlockParam` for tool results, `Anthropic.Message` for responses. Defining your own `interface ChatMessage { role: string; content: unknown }` duplicates what the SDK already provides and loses type safety. +- **Report and document output:** For tasks that produce reports, documents, or visualizations, the code execution sandbox has `python-docx`, `python-pptx`, `matplotlib`, `pillow`, and `pypdf` pre-installed. Codex can generate formatted files (DOCX, PDF, charts) and return them via the Files API — consider this for "report" or "document" type requests instead of plain stdout text. diff --git a/.agents/skills/claude-api/csharp/claude-api.md b/.agents/skills/claude-api/csharp/claude-api.md new file mode 100644 index 00000000..3b7f4612 --- /dev/null +++ b/.agents/skills/claude-api/csharp/claude-api.md @@ -0,0 +1,447 @@ +# Claude API — C# + +> **Note:** The C# SDK is the official Anthropic SDK for C#. Tool use is supported via the Messages API with a beta `BetaToolRunner` for automatic tool execution loops. The SDK also supports Microsoft.Extensions.AI IChatClient integration with function invocation and Managed Agents (beta). + +## Installation + +```bash +dotnet add package Anthropic +``` + +## Client Initialization + +```csharp +using Anthropic; + +// Default (uses ANTHROPIC_API_KEY env var) +AnthropicClient client = new(); + +// Explicit API key (use environment variables — never hardcode keys) +AnthropicClient client = new() { + ApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") +}; +``` + +--- + +## Basic Message Request + +```csharp +using Anthropic.Models.Messages; + +var parameters = new MessageCreateParams +{ + Model = Model.ClaudeOpus4_6, + MaxTokens = 16000, + Messages = [new() { Role = Role.User, Content = "What is the capital of France?" }] +}; +var response = await client.Messages.Create(parameters); + +// ContentBlock is a union wrapper. .Value unwraps to the variant object, +// then OfType filters to the type you want. Or use the TryPick* idiom +// shown in the Thinking section below. +foreach (var text in response.Content.Select(b => b.Value).OfType()) +{ + Console.WriteLine(text.Text); +} +``` + +--- + +## Streaming + +```csharp +using Anthropic.Models.Messages; + +var parameters = new MessageCreateParams +{ + Model = Model.ClaudeOpus4_6, + MaxTokens = 64000, + Messages = [new() { Role = Role.User, Content = "Write a haiku" }] +}; + +await foreach (RawMessageStreamEvent streamEvent in client.Messages.CreateStreaming(parameters)) +{ + if (streamEvent.TryPickContentBlockDelta(out var delta) && + delta.Delta.TryPickText(out var text)) + { + Console.Write(text.Text); + } +} +``` + +**`RawMessageStreamEvent` TryPick methods** (naming drops the `Message`/`Raw` prefix): `TryPickStart`, `TryPickDelta`, `TryPickStop`, `TryPickContentBlockStart`, `TryPickContentBlockDelta`, `TryPickContentBlockStop`. There is no `TryPickMessageStop` — use `TryPickStop`. + +--- + +## Thinking + +**Adaptive thinking is the recommended mode for Claude 4.6+ models.** Claude decides dynamically when and how much to think. + +```csharp +using Anthropic.Models.Messages; + +var response = await client.Messages.Create(new MessageCreateParams +{ + Model = Model.ClaudeOpus4_6, + MaxTokens = 16000, + // ThinkingConfigParam? implicitly converts from the concrete variant classes — + // no wrapper needed. + Thinking = new ThinkingConfigAdaptive(), + Messages = + [ + new() { Role = Role.User, Content = "Solve: 27 * 453" }, + ], +}); + +// ThinkingBlock(s) precede TextBlock in Content. TryPick* narrows the union. +foreach (var block in response.Content) +{ + if (block.TryPickThinking(out ThinkingBlock? t)) + { + Console.WriteLine($"[thinking] {t.Thinking}"); + } + else if (block.TryPickText(out TextBlock? text)) + { + Console.WriteLine(text.Text); + } +} +``` + +> **Deprecated:** `new ThinkingConfigEnabled { BudgetTokens = N }` (fixed-budget extended thinking) still works on Claude 4.6 but is deprecated. Use adaptive thinking above. + +Alternative to `TryPick*`: `.Select(b => b.Value).OfType()` (same LINQ pattern as the Basic Message example). + +--- + +## Tool Use + +### Defining a tool + +`Tool` (NOT `ToolParam`) with an `InputSchema` record. `InputSchema.Type` is auto-set to `"object"` by the constructor — don't set it. `ToolUnion` has an implicit conversion from `Tool`, triggered by the collection expression `[...]`. + +```csharp +using System.Text.Json; +using Anthropic.Models.Messages; + +var parameters = new MessageCreateParams +{ + Model = Model.ClaudeSonnet4_6, + MaxTokens = 16000, + Tools = [ + new Tool { + Name = "get_weather", + Description = "Get the current weather in a given location", + InputSchema = new() { + Properties = new Dictionary { + ["location"] = JsonSerializer.SerializeToElement( + new { type = "string", description = "City name" }), + }, + Required = ["location"], + }, + }, + ], + Messages = [new() { Role = Role.User, Content = "Weather in Paris?" }], +}; +``` + +Derived from `anthropic-sdk-csharp/src/Anthropic/Models/Messages/Tool.cs` and `ToolUnion.cs:799` (implicit conversion). + +See [shared tool use concepts](../shared/tool-use-concepts.md) for the loop pattern. +### Converting response content to the follow-up assistant message + +When echoing Claude's response back in the assistant turn, **there is no `.ToParam()` helper** — manually reconstruct each `ContentBlock` variant as its `*Param` counterpart. Do NOT use `new ContentBlockParam(block.Json)`: it compiles and serializes, but `.Value` stays `null` so `TryPick*`/`Validate()` fail (degraded JSON pass-through, not the typed path). + +```csharp +using Anthropic.Models.Messages; + +Message response = await client.Messages.Create(parameters); + +// No .ToParam() — reconstruct per variant. Implicit conversions from each +// *Param type to ContentBlockParam mean no explicit wrapper. +List assistantContent = []; +List toolResults = []; +foreach (ContentBlock block in response.Content) +{ + if (block.TryPickText(out TextBlock? text)) + { + assistantContent.Add(new TextBlockParam { Text = text.Text }); + } + else if (block.TryPickThinking(out ThinkingBlock? thinking)) + { + // Signature MUST be preserved — the API rejects tampering + assistantContent.Add(new ThinkingBlockParam + { + Thinking = thinking.Thinking, + Signature = thinking.Signature, + }); + } + else if (block.TryPickRedactedThinking(out RedactedThinkingBlock? redacted)) + { + assistantContent.Add(new RedactedThinkingBlockParam { Data = redacted.Data }); + } + else if (block.TryPickToolUse(out ToolUseBlock? toolUse)) + { + // ToolUseBlock has required Caller; ToolUseBlockParam.Caller is optional — don't copy it + assistantContent.Add(new ToolUseBlockParam + { + ID = toolUse.ID, + Name = toolUse.Name, + Input = toolUse.Input, + }); + // Execute the tool; collect ONE result per tool_use block — the API + // rejects the follow-up if any tool_use ID lacks a matching tool_result. + string result = ExecuteYourTool(toolUse.Name, toolUse.Input); + toolResults.Add(new ToolResultBlockParam + { + ToolUseID = toolUse.ID, + Content = result, + }); + } +} + +// Follow-up: prior messages + assistant echo + user tool_result(s) +List followUpMessages = +[ + .. parameters.Messages, + new() { Role = Role.Assistant, Content = assistantContent }, + new() { Role = Role.User, Content = toolResults }, +]; +``` + +`ToolResultBlockParam` has no tuple constructor — use the object initializer. `Content` is a string-or-list union; a plain `string` implicitly converts. + +--- + +## Context Editing / Compaction (Beta) + +**Beta-namespace prefix is inconsistent** (source-verified against `src/Anthropic/Models/Beta/Messages/*.cs` @ 12.9.0). No prefix: `MessageCreateParams`, `MessageCountTokensParams`, `Role`. **Everything else has the `Beta` prefix**: `BetaMessageParam`, `BetaMessage`, `BetaContentBlock`, `BetaToolUseBlock`, all block param types. The unprefixed `Role` WILL collide with `Anthropic.Models.Messages.Role` if you import both namespaces (CS0104). Safest: import only Beta; if mixing, alias the beta `Role`: + +```csharp +using Anthropic.Models.Beta.Messages; +using NonBeta = Anthropic.Models.Messages; // only if you also need non-beta types +// Now: MessageCreateParams, BetaMessageParam, Role (beta's), NonBeta.Role (if needed) +``` + + +`BetaMessage.Content` is `IReadOnlyList` — a 15-variant discriminated union. Narrow with `TryPick*`. **Response `BetaContentBlock` is NOT assignable to param `BetaContentBlockParam`** — there's no `.ToParam()` in C#. Round-trip by converting each block: + +```csharp +using Anthropic.Models.Beta.Messages; + +var betaParams = new MessageCreateParams // no Beta prefix — one of only 2 unprefixed +{ + Model = Model.ClaudeOpus4_6, + MaxTokens = 16000, + Betas = ["compact-2026-01-12"], + ContextManagement = new BetaContextManagementConfig + { + Edits = [new BetaCompact20260112Edit()], + }, + Messages = messages, +}; +BetaMessage resp = await client.Beta.Messages.Create(betaParams); + +foreach (BetaContentBlock block in resp.Content) +{ + if (block.TryPickCompaction(out BetaCompactionBlock? compaction)) + { + // Content is nullable — compaction can fail server-side + Console.WriteLine($"compaction summary: {compaction.Content}"); + } +} + +// Context-edit metadata lives on a separate nullable field +if (resp.ContextManagement is { } ctx) +{ + foreach (var edit in ctx.AppliedEdits) + Console.WriteLine($"cleared {edit.ClearedInputTokens} tokens"); +} + +// ROUND-TRIP: BetaMessageParam.Content is BetaMessageParamContent (a string|list +// union). It implicit-converts from List, NOT from the +// response's IReadOnlyList. Convert each block: +List paramBlocks = []; +foreach (var b in resp.Content) +{ + if (b.TryPickText(out var t)) paramBlocks.Add(new BetaTextBlockParam { Text = t.Text }); + else if (b.TryPickCompaction(out var c)) paramBlocks.Add(new BetaCompactionBlockParam { Content = c.Content }); + // ... other variants as needed +} +messages.Add(new BetaMessageParam { Role = Role.Assistant, Content = paramBlocks }); +``` + +All 15 `BetaContentBlock.TryPick*` variants: `Text`, `Thinking`, `RedactedThinking`, `ToolUse`, `ServerToolUse`, `WebSearchToolResult`, `WebFetchToolResult`, `CodeExecutionToolResult`, `BashCodeExecutionToolResult`, `TextEditorCodeExecutionToolResult`, `ToolSearchToolResult`, `McpToolUse`, `McpToolResult`, `ContainerUpload`, `Compaction`. + +**`BetaToolUseBlock.Input` is `IReadOnlyDictionary`** — index by key then call the `JsonElement` extractor: + +```csharp +if (block.TryPickToolUse(out BetaToolUseBlock? tu)) +{ + int a = tu.Input["a"].GetInt32(); + string s = tu.Input["name"].GetString()!; +} +``` + +--- + +## Effort Parameter + +Effort is nested under `OutputConfig`, NOT a top-level property. `ApiEnum` has an implicit conversion from the enum, so assign `Effort.High` directly. + +```csharp +OutputConfig = new OutputConfig { Effort = Effort.High }, +``` + +Values: `Effort.Low`, `Effort.Medium`, `Effort.High`, `Effort.Max`. Combine with `Thinking = new ThinkingConfigAdaptive()` for cost-quality control. + +--- + +## Prompt Caching + +`System` takes `MessageCreateParamsSystem?` — a union of `string` or `List`. There is no `SystemTextBlockParam`; use plain `TextBlockParam`. The implicit conversion needs the concrete `List` type (array literals won't convert). For placement patterns and the silent-invalidator audit checklist, see `shared/prompt-caching.md`. + +```csharp +System = new List { + new() { + Text = longSystemPrompt, + CacheControl = new CacheControlEphemeral(), // auto-sets Type = "ephemeral" + }, +}, +``` + +Optional `Ttl` on `CacheControlEphemeral`: `new() { Ttl = Ttl.Ttl1h }` or `Ttl.Ttl5m`. `CacheControl` also exists on `Tool.CacheControl` and top-level `MessageCreateParams.CacheControl`. + +Verify hits via `response.Usage.CacheCreationInputTokens` / `response.Usage.CacheReadInputTokens`. + +--- + +## Token Counting + +```csharp +MessageTokensCount result = await client.Messages.CountTokens(new MessageCountTokensParams { + Model = Model.ClaudeOpus4_6, + Messages = [new() { Role = Role.User, Content = "Hello" }], +}); +long tokens = result.InputTokens; +``` + +`MessageCountTokensParams.Tools` uses a different union type (`MessageCountTokensTool`) than `MessageCreateParams.Tools` (`ToolUnion`) — if you're passing tools, the compiler will tell you when it matters. + +--- + +## Structured Output + +```csharp +OutputConfig = new OutputConfig { + Format = new JsonOutputFormat { + Schema = new Dictionary { + ["type"] = JsonSerializer.SerializeToElement("object"), + ["properties"] = JsonSerializer.SerializeToElement( + new { name = new { type = "string" } }), + ["required"] = JsonSerializer.SerializeToElement(new[] { "name" }), + }, + }, +}, +``` + +`JsonOutputFormat.Type` is auto-set to `"json_schema"` by the constructor. `Schema` is `required`. + +--- + +## PDF / Document Input + +`DocumentBlockParam` takes a `DocumentBlockParamSource` union: `Base64PdfSource` / `UrlPdfSource` / `PlainTextSource` / `ContentBlockSource`. `Base64PdfSource` auto-sets `MediaType = "application/pdf"` and `Type = "base64"`. + +```csharp +new MessageParam { + Role = Role.User, + Content = new List { + new DocumentBlockParam { Source = new Base64PdfSource { Data = base64String } }, + new TextBlockParam { Text = "Summarize this PDF" }, + }, +} +``` + +--- + +## Server-Side Tools + +Web search, bash, text editor, and code execution are built-in server tools. Type names are version-suffixed; constructors auto-set `name`/`type`. All implicit-convert to `ToolUnion`. + +```csharp +Tools = [ + new WebSearchTool20260209(), + new ToolBash20250124(), + new ToolTextEditor20250728(), + new CodeExecutionTool20260120(), +], +``` + +Also available: `WebFetchTool20260209`, `MemoryTool20250818`. `WebSearchTool20260209` optionals: `AllowedDomains`, `BlockedDomains`, `MaxUses`, `UserLocation`. + +--- + +## Files API (Beta) + +Files live under `client.Beta.Files` (namespace `Anthropic.Models.Beta.Files`). `BinaryContent` implicit-converts from `Stream` and `byte[]`. + +```csharp +using Anthropic.Models.Beta.Files; +using Anthropic.Models.Beta.Messages; + +FileMetadata meta = await client.Beta.Files.Upload( + new FileUploadParams { File = File.OpenRead("doc.pdf") }); + +// Referencing the uploaded file requires Beta message types: +new BetaRequestDocumentBlock { + Source = new BetaFileDocumentSource { FileID = meta.ID }, +} +``` + +The non-beta `DocumentBlockParamSource` union has no file-ID variant — file references need `client.Beta.Messages.Create()`. + +--- + +## Tool Runner (Beta) + +The C# SDK provides a `BetaToolRunner` for automatic tool execution loops. Define tools with raw JSON schemas, and the runner handles the API call → tool execution → result feedback loop. + +```csharp +using Anthropic.Models.Beta.Messages; + +// Define tools and create params as shown in the Tool Use section above, +// but using the beta namespace types (BetaToolUnion, etc.) +var runner = client.Beta.Messages.ToolRunner(betaParams); + +await foreach (BetaMessage message in runner) +{ + foreach (var block in message.Content) + { + if (block.TryPickText(out var text)) + { + Console.WriteLine(text.Text); + } + } +} +``` + +--- + +## Stop Details + +When `StopReason` is `"refusal"`, the response includes structured `StopDetails`: + +```csharp +if (response.StopReason == "refusal" && response.StopDetails is { } details) +{ + Console.WriteLine($"Category: {details.Category}"); + Console.WriteLine($"Explanation: {details.Explanation}"); +} +``` + +--- + +## Managed Agents (Beta) + +The C# SDK supports Managed Agents via `client.Beta.Agents`, `client.Beta.Sessions`, `client.Beta.Environments`, and related namespaces. See `shared/managed-agents-overview.md` for the architecture and `curl/managed-agents.md` for the wire-level reference. diff --git a/.agents/skills/claude-api/curl/examples.md b/.agents/skills/claude-api/curl/examples.md new file mode 100644 index 00000000..074fe3a5 --- /dev/null +++ b/.agents/skills/claude-api/curl/examples.md @@ -0,0 +1,216 @@ +# Claude API — cURL / Raw HTTP + +Use these examples when the user needs raw HTTP requests or is working in a language without an official SDK. + +## Setup + +```bash +export ANTHROPIC_API_KEY="your-api-key" +``` + +--- + +## Basic Message Request + +```bash +curl https://api.anthropic.com/v1/messages \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d '{ + "model": "claude-opus-4-8", + "max_tokens": 16000, + "messages": [ + {"role": "user", "content": "What is the capital of France?"} + ] + }' +``` + +### Parsing the response + +Use `jq` to extract fields from the JSON response. Do not use `grep`/`sed` — +JSON strings can contain any character and regex parsing will break on quotes, +escapes, or multi-line content. + +```bash +# Capture the response, then extract fields +response=$(curl -s https://api.anthropic.com/v1/messages \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d '{"model":"claude-opus-4-8","max_tokens":16000,"messages":[{"role":"user","content":"Hello"}]}') + +# Print the first text block (-r strips the JSON quotes) +echo "$response" | jq -r '.content[0].text' + +# Read usage fields +input_tokens=$(echo "$response" | jq -r '.usage.input_tokens') +output_tokens=$(echo "$response" | jq -r '.usage.output_tokens') + +# Read stop reason (for tool-use loops) +stop_reason=$(echo "$response" | jq -r '.stop_reason') + +# Extract all text blocks (content is an array; filter to type=="text") +echo "$response" | jq -r '.content[] | select(.type == "text") | .text' +``` + + +--- + +## Streaming (SSE) + +```bash +curl https://api.anthropic.com/v1/messages \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d '{ + "model": "claude-opus-4-8", + "max_tokens": 64000, + "stream": true, + "messages": [{"role": "user", "content": "Write a haiku"}] + }' +``` + +The response is a stream of Server-Sent Events: + +``` +event: message_start +data: {"type":"message_start","message":{"id":"msg_...","type":"message",...}} + +event: content_block_start +data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} + +event: content_block_delta +data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}} + +event: content_block_stop +data: {"type":"content_block_stop","index":0} + +event: message_delta +data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":12}} + +event: message_stop +data: {"type":"message_stop"} +``` + +--- + +## Tool Use + +```bash +curl https://api.anthropic.com/v1/messages \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d '{ + "model": "claude-opus-4-8", + "max_tokens": 16000, + "tools": [{ + "name": "get_weather", + "description": "Get current weather for a location", + "input_schema": { + "type": "object", + "properties": { + "location": {"type": "string", "description": "City name"} + }, + "required": ["location"] + } + }], + "messages": [{"role": "user", "content": "What is the weather in Paris?"}] + }' +``` + +When Claude responds with a `tool_use` block, send the result back: + +```bash +curl https://api.anthropic.com/v1/messages \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d '{ + "model": "claude-opus-4-8", + "max_tokens": 16000, + "tools": [{ + "name": "get_weather", + "description": "Get current weather for a location", + "input_schema": { + "type": "object", + "properties": { + "location": {"type": "string", "description": "City name"} + }, + "required": ["location"] + } + }], + "messages": [ + {"role": "user", "content": "What is the weather in Paris?"}, + {"role": "assistant", "content": [ + {"type": "text", "text": "Let me check the weather."}, + {"type": "tool_use", "id": "toolu_abc123", "name": "get_weather", "input": {"location": "Paris"}} + ]}, + {"role": "user", "content": [ + {"type": "tool_result", "tool_use_id": "toolu_abc123", "content": "72°F and sunny"} + ]} + ] + }' +``` + +--- + +## Prompt Caching + +Put `cache_control` on the last block of the stable prefix. See `shared/prompt-caching.md` for placement patterns and the silent-invalidator audit checklist. + +```bash +curl https://api.anthropic.com/v1/messages \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d '{ + "model": "claude-opus-4-8", + "max_tokens": 16000, + "system": [ + {"type": "text", "text": "", "cache_control": {"type": "ephemeral"}} + ], + "messages": [{"role": "user", "content": "Summarize the key points"}] + }' +``` + +For 1-hour TTL: `"cache_control": {"type": "ephemeral", "ttl": "1h"}`. Top-level `"cache_control"` on the request body auto-places on the last cacheable block. Verify hits via the response `usage.cache_creation_input_tokens` / `usage.cache_read_input_tokens` fields. + +--- + +## Extended Thinking + +> **Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6:** Use adaptive thinking. `budget_tokens` is removed on Opus 4.8 and 4.7 (400 if sent); deprecated on Opus 4.6 and Sonnet 4.6. +> **Older models:** Use `"type": "enabled"` with `"budget_tokens": N` (must be < `max_tokens`, min 1024). + +```bash +# Opus 4.8 / 4.7 / 4.6: adaptive thinking (recommended) +curl https://api.anthropic.com/v1/messages \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d '{ + "model": "claude-opus-4-8", + "max_tokens": 16000, + "thinking": { + "type": "adaptive" + }, + "output_config": { + "effort": "high" + }, + "messages": [{"role": "user", "content": "Solve this step by step..."}] + }' +``` + +--- + +## Required Headers + +| Header | Value | Description | +| ------------------- | ------------------ | -------------------------- | +| `Content-Type` | `application/json` | Required | +| `x-api-key` | Your API key | Authentication | +| `anthropic-version` | `2023-06-01` | API version | +| `anthropic-beta` | Beta feature IDs | Required for beta features | diff --git a/.agents/skills/claude-api/curl/managed-agents.md b/.agents/skills/claude-api/curl/managed-agents.md new file mode 100644 index 00000000..a145f830 --- /dev/null +++ b/.agents/skills/claude-api/curl/managed-agents.md @@ -0,0 +1,338 @@ +# Managed Agents — cURL / Raw HTTP + +Use these examples when the user needs raw HTTP requests or is working without an SDK. + +## Setup + +```bash +export ANTHROPIC_API_KEY="your-api-key" + +# Common headers +HEADERS=( + -H "Content-Type: application/json" + -H "x-api-key: $ANTHROPIC_API_KEY" + -H "anthropic-version: 2023-06-01" + -H "anthropic-beta: managed-agents-2026-04-01" +) +``` + +--- + +## Create an Environment + +```bash +curl -X POST https://api.anthropic.com/v1/environments \ + "${HEADERS[@]}" \ + -d '{ + "name": "my-dev-env", + "config": { + "type": "cloud", + "networking": { "type": "unrestricted" } + } + }' +``` + +### With restricted networking + +```bash +curl -X POST https://api.anthropic.com/v1/environments \ + "${HEADERS[@]}" \ + -d '{ + "name": "restricted-env", + "config": { + "type": "cloud", + "networking": { + "type": "limited", + "allow_package_managers": true, + "allow_mcp_servers": true, + "allowed_hosts": ["api.example.com"] + } + } + }' +``` + +--- + +## Create an Agent (required first step) + +> ⚠️ **There is no inline agent config.** Under `managed-agents-2026-04-01`, `model`/`system`/`tools` are top-level fields on `POST /v1/agents`, not on the session. Always create the agent first — the session only takes `"agent": {"type": "agent", "id": "..."}`. + +### Minimal + +```bash +# 1. Create the agent +curl -X POST https://api.anthropic.com/v1/agents \ + "${HEADERS[@]}" \ + -d '{ + "name": "Coding Assistant", + "model": "claude-opus-4-8", + "tools": [{ "type": "agent_toolset_20260401" }] + }' +# → { "id": "agent_abc123", ... } + +# 2. Start a session +curl -X POST https://api.anthropic.com/v1/sessions \ + "${HEADERS[@]}" \ + -d '{ + "agent": { "type": "agent", "id": "agent_abc123", "version": "1772585501101368014" }, + "environment_id": "env_abc123" + }' +``` + +### With system prompt, custom tools, and GitHub repo + +```bash +# 1. Create the agent +curl -X POST https://api.anthropic.com/v1/agents \ + "${HEADERS[@]}" \ + -d '{ + "name": "Code Reviewer", + "model": "claude-opus-4-8", + "system": "You are a senior code reviewer. Be thorough and constructive.", + "tools": [ + { "type": "agent_toolset_20260401" }, + { + "type": "custom", + "name": "run_linter", + "description": "Run the project linter on a file", + "input_schema": { + "type": "object", + "properties": { + "file_path": { "type": "string", "description": "Path to lint" } + }, + "required": ["file_path"] + } + } + ] + }' + +# 2. Start a session with the repo mounted +curl -X POST https://api.anthropic.com/v1/sessions \ + "${HEADERS[@]}" \ + -d '{ + "agent": { "type": "agent", "id": "agent_abc123", "version": "1772585501101368014" }, + "environment_id": "env_abc123", + "title": "Code review session", + "resources": [ + { + "type": "github_repository", + "url": "https://github.com/owner/repo", + "mount_path": "/workspace/repo", + "authorization_token": "ghp_...", + "branch": "feature-branch" + } + ] + }' +``` + +--- + +## Send a User Message + +```bash +curl -X POST https://api.anthropic.com/v1/sessions/$SESSION_ID/events \ + "${HEADERS[@]}" \ + -d '{ + "events": [ + { + "type": "user.message", + "content": [{ "type": "text", "text": "Review the auth module for security issues" }] + } + ] + }' +``` + +--- + +## Stream Events (SSE) + +```bash +curl -N https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream \ + "${HEADERS[@]}" +``` + +Response format: + +``` +event: session.status_running +data: {"type":"session.status_running","id":"sevt_...","processed_at":"..."} + +event: agent.message +data: {"type":"agent.message","id":"sevt_...","content":[{"type":"text","text":"I'll review..."}],"processed_at":"..."} + +event: session.status_idle +data: {"type":"session.status_idle","id":"sevt_...","processed_at":"..."} +``` + +--- + +## Poll Events + +```bash +# Get all events +curl https://api.anthropic.com/v1/sessions/$SESSION_ID/events \ + "${HEADERS[@]}" + +# Paginated — get next page of events +curl "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?page=page_abc123" \ + "${HEADERS[@]}" +``` + +--- + +## Provide Custom Tool Result + +When the agent calls a custom tool, send the result back: + +```bash +curl -X POST https://api.anthropic.com/v1/sessions/$SESSION_ID/events \ + "${HEADERS[@]}" \ + -d '{ + "events": [ + { + "type": "user.custom_tool_result", + "custom_tool_use_id": "sevt_abc123", + "content": [{ "type": "text", "text": "No linting errors found." }] + } + ] + }' +``` + +--- + +## Interrupt a Running Session + +```bash +curl -X POST https://api.anthropic.com/v1/sessions/$SESSION_ID/events \ + "${HEADERS[@]}" \ + -d '{ + "events": [ + { + "type": "interrupt" + } + ] + }' +``` + +--- + +## Get Session Details + +```bash +curl https://api.anthropic.com/v1/sessions/$SESSION_ID \ + "${HEADERS[@]}" +``` + +--- + +## List Sessions + +```bash +curl https://api.anthropic.com/v1/sessions \ + "${HEADERS[@]}" +``` + +--- + +## Delete a Session + +```bash +curl -X DELETE https://api.anthropic.com/v1/sessions/$SESSION_ID \ + "${HEADERS[@]}" +``` + +--- + +## Upload a File + +```bash +curl -X POST https://api.anthropic.com/v1/files \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -H "anthropic-beta: files-api-2025-04-14" \ + -F "file=@path/to/file.txt" +``` + +--- + +## List and Download Session Files + +List files the agent wrote to `/mnt/session/outputs/` during a session, then download them. + +```bash +# List files associated with a session +curl "https://api.anthropic.com/v1/files?scope_id=$SESSION_ID" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -H "anthropic-beta: files-api-2025-04-14,managed-agents-2026-04-01" + +# Download a specific file +curl "https://api.anthropic.com/v1/files/$FILE_ID/content" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -H "anthropic-beta: files-api-2025-04-14,managed-agents-2026-04-01" \ + -o downloaded_file.txt +``` + +--- + +## List Agents + +```bash +curl https://api.anthropic.com/v1/agents \ + "${HEADERS[@]}" +``` + +--- + +## MCP Server Integration + +```bash +# 1. Agent declares MCP server (no auth here — auth goes in a vault) +curl -X POST https://api.anthropic.com/v1/agents \ + "${HEADERS[@]}" \ + -d '{ + "name": "MCP Agent", + "model": "claude-opus-4-8", + "mcp_servers": [ + { "type": "url", "name": "my-tools", "url": "https://my-mcp-server.example.com/sse" } + ], + "tools": [ + { "type": "agent_toolset_20260401" }, + { "type": "mcp_toolset", "mcp_server_name": "my-tools" } + ] + }' + +# 2. Session attaches vault containing credentials for that MCP server URL +curl -X POST https://api.anthropic.com/v1/sessions \ + "${HEADERS[@]}" \ + -d '{ + "agent": "agent_abc123", + "environment_id": "env_abc123", + "vault_ids": ["vlt_abc123"] + }' +``` + +See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials. + +--- + +## Tool Configuration + +```bash +curl -X POST https://api.anthropic.com/v1/agents \ + "${HEADERS[@]}" \ + -d '{ + "name": "Restricted Agent", + "model": "claude-opus-4-8", + "tools": [ + { + "type": "agent_toolset_20260401", + "default_config": { "enabled": true }, + "configs": [ + { "name": "bash", "enabled": false } + ] + } + ] + }' +``` diff --git a/.agents/skills/claude-api/go/claude-api.md b/.agents/skills/claude-api/go/claude-api.md new file mode 100644 index 00000000..79645e92 --- /dev/null +++ b/.agents/skills/claude-api/go/claude-api.md @@ -0,0 +1,440 @@ +# Claude API — Go + +> **Note:** The Go SDK supports the Claude API and beta tool use with `BetaToolRunner`. Agent SDK is not yet available for Go. + +## Installation + +```bash +go get github.com/anthropics/anthropic-sdk-go +``` + +## Client Initialization + +```go +import ( + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" +) + +// Default (uses ANTHROPIC_API_KEY env var) +client := anthropic.NewClient() + +// Explicit API key +client := anthropic.NewClient( + option.WithAPIKey("your-api-key"), +) +``` + +--- + +## Model Constants + +The Go SDK provides typed model constants: `anthropic.ModelClaudeOpus4_8`, `anthropic.ModelClaudeOpus4_7`, `anthropic.ModelClaudeSonnet4_6`, `anthropic.ModelClaudeHaiku4_5_20251001`. Use `ModelClaudeOpus4_8` unless the user specifies otherwise. + +--- + +## Basic Message Request + +```go +response, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ + Model: anthropic.ModelClaudeOpus4_8, + MaxTokens: 16000, + Messages: []anthropic.MessageParam{ + anthropic.NewUserMessage(anthropic.NewTextBlock("What is the capital of France?")), + }, +}) +if err != nil { + log.Fatal(err) +} +for _, block := range response.Content { + switch variant := block.AsAny().(type) { + case anthropic.TextBlock: + fmt.Println(variant.Text) + } +} +``` + +--- + +## Streaming + +```go +stream := client.Messages.NewStreaming(context.Background(), anthropic.MessageNewParams{ + Model: anthropic.ModelClaudeOpus4_6, + MaxTokens: 64000, + Messages: []anthropic.MessageParam{ + anthropic.NewUserMessage(anthropic.NewTextBlock("Write a haiku")), + }, +}) + +for stream.Next() { + event := stream.Current() + switch eventVariant := event.AsAny().(type) { + case anthropic.ContentBlockDeltaEvent: + switch deltaVariant := eventVariant.Delta.AsAny().(type) { + case anthropic.TextDelta: + fmt.Print(deltaVariant.Text) + } + } +} +if err := stream.Err(); err != nil { + log.Fatal(err) +} +``` + +**Accumulating the final message** (there is no `GetFinalMessage()` on the stream): + +```go +stream := client.Messages.NewStreaming(ctx, params) +message := anthropic.Message{} +for stream.Next() { + message.Accumulate(stream.Current()) +} +if err := stream.Err(); err != nil { log.Fatal(err) } +// message.Content now has the complete response +``` + + +--- + +## Tool Use + +### Tool Runner (Beta — Recommended) + +**Beta:** The Go SDK provides `BetaToolRunner` for automatic tool use loops via the `toolrunner` package. + +```go +import ( + "context" + "fmt" + "log" + + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/toolrunner" +) + +// Define tool input with jsonschema tags for automatic schema generation +type GetWeatherInput struct { + City string `json:"city" jsonschema:"required,description=The city name"` +} + +// Create a tool with automatic schema generation from struct tags +weatherTool, err := toolrunner.NewBetaToolFromJSONSchema( + "get_weather", + "Get current weather for a city", + func(ctx context.Context, input GetWeatherInput) (anthropic.BetaToolResultBlockParamContentUnion, error) { + return anthropic.BetaToolResultBlockParamContentUnion{ + OfText: &anthropic.BetaTextBlockParam{ + Text: fmt.Sprintf("The weather in %s is sunny, 72°F", input.City), + }, + }, nil + }, +) +if err != nil { + log.Fatal(err) +} + +// Create a tool runner that handles the conversation loop automatically +runner := client.Beta.Messages.NewToolRunner( + []anthropic.BetaTool{weatherTool}, + anthropic.BetaToolRunnerParams{ + BetaMessageNewParams: anthropic.BetaMessageNewParams{ + Model: anthropic.ModelClaudeOpus4_6, + MaxTokens: 16000, + Messages: []anthropic.BetaMessageParam{ + anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What's the weather in Paris?")), + }, + }, + MaxIterations: 5, + }, +) + +// Run until Claude produces a final response +message, err := runner.RunToCompletion(context.Background()) +if err != nil { + log.Fatal(err) +} + +// RunToCompletion returns *BetaMessage; content is []BetaContentBlockUnion. +// Narrow via AsAny() switch — note the Beta-namespace types (BetaTextBlock, +// not TextBlock): +for _, block := range message.Content { + switch block := block.AsAny().(type) { + case anthropic.BetaTextBlock: + fmt.Println(block.Text) + } +} +``` + +**Key features of the Go tool runner:** + +- Automatic schema generation from Go structs via `jsonschema` tags +- `RunToCompletion()` for simple one-shot usage +- `All()` iterator for processing each message in the conversation +- `NextMessage()` for step-by-step iteration +- Streaming variant via `NewToolRunnerStreaming()` with `AllStreaming()` + +### Manual Loop + +For fine-grained control over the agentic loop, define tools with `ToolParam`, check `StopReason`, execute tools yourself, and feed `tool_result` blocks back. This is the pattern when you need to intercept, validate, or log tool calls. + +Derived from `anthropic-sdk-go/examples/tools/main.go`. + +```go +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/anthropics/anthropic-sdk-go" +) + +func main() { + client := anthropic.NewClient() + + // 1. Define tools. ToolParam.InputSchema uses a map, no struct tags needed. + addTool := anthropic.ToolParam{ + Name: "add", + Description: anthropic.String("Add two integers"), + InputSchema: anthropic.ToolInputSchemaParam{ + Properties: map[string]any{ + "a": map[string]any{"type": "integer"}, + "b": map[string]any{"type": "integer"}, + }, + }, + } + // ToolParam must be wrapped in ToolUnionParam for the Tools slice + tools := []anthropic.ToolUnionParam{{OfTool: &addTool}} + + messages := []anthropic.MessageParam{ + anthropic.NewUserMessage(anthropic.NewTextBlock("What is 2 + 3?")), + } + + for { + resp, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ + Model: anthropic.ModelClaudeSonnet4_6, + MaxTokens: 16000, + Messages: messages, + Tools: tools, + }) + if err != nil { + log.Fatal(err) + } + + // 2. Append the assistant response to history BEFORE processing tool calls. + // resp.ToParam() converts Message → MessageParam in one call. + messages = append(messages, resp.ToParam()) + + // 3. Walk content blocks. ContentBlockUnion is a flattened struct; + // use block.AsAny().(type) to switch on the actual variant. + toolResults := []anthropic.ContentBlockParamUnion{} + for _, block := range resp.Content { + switch variant := block.AsAny().(type) { + case anthropic.TextBlock: + fmt.Println(variant.Text) + case anthropic.ToolUseBlock: + // 4. Parse the tool input. Use variant.JSON.Input.Raw() to get the + // raw JSON — block.Input is json.RawMessage, not the parsed value. + var in struct { + A int `json:"a"` + B int `json:"b"` + } + if err := json.Unmarshal([]byte(variant.JSON.Input.Raw()), &in); err != nil { + log.Fatal(err) + } + result := fmt.Sprintf("%d", in.A+in.B) + // 5. NewToolResultBlock(toolUseID, content, isError) builds the + // ContentBlockParamUnion for you. block.ID is the tool_use_id. + toolResults = append(toolResults, + anthropic.NewToolResultBlock(block.ID, result, false)) + } + } + + // 6. Exit when Claude stops asking for tools + if resp.StopReason != anthropic.StopReasonToolUse { + break + } + + // 7. Tool results go in a user message (variadic: all results in one turn) + messages = append(messages, anthropic.NewUserMessage(toolResults...)) + } +} +``` + +**Key API surface:** + +| Symbol | Purpose | +|---|---| +| `resp.ToParam()` | Convert `Message` response → `MessageParam` for history | +| `block.AsAny().(type)` | Type-switch on `ContentBlockUnion` variants | +| `variant.JSON.Input.Raw()` | Raw JSON string of tool input (for `json.Unmarshal`) | +| `anthropic.NewToolResultBlock(id, content, isError)` | Build `tool_result` block | +| `anthropic.NewUserMessage(blocks...)` | Wrap tool results as a user turn | +| `anthropic.StopReasonToolUse` | `StopReason` constant to check loop termination | +| `anthropic.ToolUnionParam{OfTool: &t}` | Wrap `ToolParam` in the union for `Tools:` | + +--- + +## Thinking + +Enable Claude's internal reasoning by setting `Thinking` in `MessageNewParams`. The response will contain `ThinkingBlock` content before the final `TextBlock`. + +**Adaptive thinking is the recommended mode for Claude 4.6+ models.** Claude decides dynamically when and how much to think. Combine with the `effort` parameter for cost-quality control. + +Derived from `anthropic-sdk-go/message.go` (`ThinkingConfigParamUnion`, `ThinkingConfigAdaptiveParam`). + +```go +// There is no ThinkingConfigParamOfAdaptive helper — construct the union +// struct-literal directly and take the address of the variant. +adaptive := anthropic.ThinkingConfigAdaptiveParam{} +params := anthropic.MessageNewParams{ + Model: anthropic.ModelClaudeSonnet4_6, + MaxTokens: 16000, + Thinking: anthropic.ThinkingConfigParamUnion{OfAdaptive: &adaptive}, + Messages: []anthropic.MessageParam{ + anthropic.NewUserMessage(anthropic.NewTextBlock("How many r's in strawberry?")), + }, +} + +resp, err := client.Messages.New(context.Background(), params) +if err != nil { + log.Fatal(err) +} + +// ThinkingBlock(s) precede TextBlock in content +for _, block := range resp.Content { + switch b := block.AsAny().(type) { + case anthropic.ThinkingBlock: + fmt.Println("[thinking]", b.Thinking) + case anthropic.TextBlock: + fmt.Println(b.Text) + } +} +``` + +> **Deprecated:** `ThinkingConfigParamOfEnabled(budgetTokens)` (fixed-budget extended thinking) still works on Claude 4.6 but is deprecated. Use adaptive thinking above. + +To disable: `anthropic.ThinkingConfigParamUnion{OfDisabled: &anthropic.ThinkingConfigDisabledParam{}}`. + +--- + +## Prompt Caching + +`System` is `[]TextBlockParam`; set `CacheControl` on the last block to cache tools + system together. For placement patterns and the silent-invalidator audit checklist, see `shared/prompt-caching.md`. + +```go +System: []anthropic.TextBlockParam{{ + Text: longSystemPrompt, + CacheControl: anthropic.NewCacheControlEphemeralParam(), // default 5m TTL +}}, +``` + +For 1-hour TTL: `anthropic.CacheControlEphemeralParam{TTL: anthropic.CacheControlEphemeralTTLTTL1h}`. There's also a top-level `CacheControl` on `MessageNewParams` that auto-places on the last cacheable block. + +Verify hits via `resp.Usage.CacheCreationInputTokens` / `resp.Usage.CacheReadInputTokens`. + +--- + +## Server-Side Tools + +Version-suffixed struct names with `Param` suffix. `Name`/`Type` are `constant.*` types — zero value marshals correctly, so `{}` works. Wrap in `ToolUnionParam` with the matching `Of*` field. + +```go +Tools: []anthropic.ToolUnionParam{ + {OfWebSearchTool20260209: &anthropic.WebSearchTool20260209Param{}}, + {OfBashTool20250124: &anthropic.ToolBash20250124Param{}}, + {OfTextEditor20250728: &anthropic.ToolTextEditor20250728Param{}}, + {OfCodeExecutionTool20260120: &anthropic.CodeExecutionTool20260120Param{}}, +}, +``` + +Also available: `WebFetchTool20260209Param`, `MemoryTool20250818Param`, `ToolSearchToolBm25_20251119Param`, `ToolSearchToolRegex20251119Param`. For the advisor tool, use `BetaAdvisorTool20260301Param` in the beta namespace. + +--- + +## Stop Details + +When `StopReason` is `anthropic.StopReasonRefusal`, the response includes structured `StopDetails`: + +```go +if resp.StopReason == anthropic.StopReasonRefusal { + fmt.Println("Category:", resp.StopDetails.Category) // "cyber" | "bio" | "" + fmt.Println("Explanation:", resp.StopDetails.Explanation) +} +``` + +--- + +## PDF / Document Input + +`NewDocumentBlock` generic helper accepts any source type. `MediaType`/`Type` are auto-set. + +```go +b64 := base64.StdEncoding.EncodeToString(pdfBytes) + +msg := anthropic.NewUserMessage( + anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{Data: b64}), + anthropic.NewTextBlock("Summarize this document"), +) +``` + +Other sources: `URLPDFSourceParam{URL: "https://..."}`, `PlainTextSourceParam{Data: "..."}`. + +--- + +## Files API (Beta) + +Under `client.Beta.Files`. Method is **`Upload`** (NOT `New`/`Create`), params struct is `BetaFileUploadParams`. The `File` field takes an `io.Reader`; use `anthropic.File()` to attach a filename + content-type for the multipart encoding. + +```go +f, _ := os.Open("./upload_me.txt") +defer f.Close() + +meta, err := client.Beta.Files.Upload(ctx, anthropic.BetaFileUploadParams{ + File: anthropic.File(f, "upload_me.txt", "text/plain"), + Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFilesAPI2025_04_14}, +}) +// meta.ID is the file_id to reference in subsequent message requests +``` + +Other `Beta.Files` methods: `List`, `Delete`, `Download`, `GetMetadata`. + +--- + +## Context Editing / Compaction (Beta) + +Use `Beta.Messages.New` with `ContextManagement` on `BetaMessageNewParams`. There is no `NewBetaAssistantMessage` — use `.ToParam()` for the round-trip. + +```go +params := anthropic.BetaMessageNewParams{ + Model: anthropic.ModelClaudeOpus4_6, // also supported: ModelClaudeSonnet4_6 + MaxTokens: 16000, + Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, + ContextManagement: anthropic.BetaContextManagementConfigParam{ + Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ + {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{}}, + }, + }, + Messages: []anthropic.BetaMessageParam{ /* ... */ }, +} + +resp, err := client.Beta.Messages.New(ctx, params) +if err != nil { + log.Fatal(err) +} + +// Round-trip: append response to history via .ToParam() +params.Messages = append(params.Messages, resp.ToParam()) + +// Read compaction blocks from the response +for _, block := range resp.Content { + if c, ok := block.AsAny().(anthropic.BetaCompactionBlock); ok { + fmt.Println("compaction summary:", c.Content) + } +} +``` + +Other edit types: `BetaClearToolUses20250919EditParam`, `BetaClearThinking20251015EditParam`. diff --git a/.agents/skills/claude-api/go/managed-agents/README.md b/.agents/skills/claude-api/go/managed-agents/README.md new file mode 100644 index 00000000..a6eaa24c --- /dev/null +++ b/.agents/skills/claude-api/go/managed-agents/README.md @@ -0,0 +1,561 @@ +# Managed Agents — Go + +> **Bindings not shown here:** This README covers the most common managed-agents flows for Go. If you need a class, method, namespace, field, or behavior that isn't shown, WebFetch the Go SDK repo **or the relevant docs page** from `shared/live-sources.md` rather than guess. Do not extrapolate from cURL shapes or another language's SDK. + +> **Agents are persistent — create once, reference by ID.** Store the agent ID returned by `agents.New` and pass it to every subsequent `sessions.New`; do not call `agents.New` in the request path. The Anthropic CLI is one convenient way to create agents and environments from version-controlled YAML — its URL is in `shared/live-sources.md`. The examples below show in-code creation for completeness; in production the create call belongs in setup, not in the request path. + +## Installation + +```bash +go get github.com/anthropics/anthropic-sdk-go +``` + +## Client Initialization + +```go +import ( + "context" + + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" +) + +// Default (uses ANTHROPIC_API_KEY env var) +client := anthropic.NewClient() + +// Explicit API key +client := anthropic.NewClient( + option.WithAPIKey("your-api-key"), +) + +ctx := context.Background() +``` + +--- + +## Create an Environment + +```go +environment, err := client.Beta.Environments.New(ctx, anthropic.BetaEnvironmentNewParams{ + Name: "my-dev-env", + Config: anthropic.BetaCloudConfigParams{ + Networking: anthropic.BetaCloudConfigParamsNetworkingUnion{ + OfUnrestricted: &anthropic.UnrestrictedNetworkParam{}, + }, + }, +}) +if err != nil { + panic(err) +} +fmt.Println(environment.ID) // env_... +``` + +--- + +## Create an Agent (required first step) + +> ⚠️ **There is no inline agent config.** `Model`/`System`/`Tools` live on the agent object, not the session. Always start with `Beta.Agents.New()` — the session only takes `Agent: anthropic.BetaSessionNewParamsAgentUnion{OfString: anthropic.String(agent.ID)}` (or the typed `OfBetaManagedAgentsAgents` variant when you need a specific version). + +### Minimal + +```go +// 1. Create the agent (reusable, versioned) +agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ + Name: "Coding Assistant", + Model: anthropic.BetaManagedAgentsModelConfigParams{ + ID: "claude-opus-4-8", + Type: anthropic.BetaManagedAgentsModelConfigParamsTypeModelConfig, + }, + System: anthropic.String("You are a helpful coding assistant."), + Tools: []anthropic.BetaAgentNewParamsToolUnion{{ + OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{ + Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401, + }, + }}, +}) +if err != nil { + panic(err) +} + +// 2. Start a session +session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{ + Agent: anthropic.BetaSessionNewParamsAgentUnion{ + OfBetaManagedAgentsAgents: &anthropic.BetaManagedAgentsAgentParams{ + Type: anthropic.BetaManagedAgentsAgentParamsTypeAgent, + ID: agent.ID, + Version: anthropic.Int(agent.Version), + }, + }, + EnvironmentID: environment.ID, + Title: anthropic.String("Quickstart session"), +}) +if err != nil { + panic(err) +} +fmt.Printf("Session ID: %s, status: %s\n", session.ID, session.Status) +``` + +### Updating an Agent + +Updates create new versions; the agent object is immutable per version. + +```go +updatedAgent, err := client.Beta.Agents.Update(ctx, agent.ID, anthropic.BetaAgentUpdateParams{ + Version: agent.Version, + System: anthropic.String("You are a helpful coding agent. Always write tests."), +}) +if err != nil { + panic(err) +} +fmt.Printf("New version: %d\n", updatedAgent.Version) + +// List all versions +iter := client.Beta.Agents.Versions.ListAutoPaging(ctx, agent.ID, anthropic.BetaAgentVersionListParams{}) +for iter.Next() { + version := iter.Current() + fmt.Printf("Version %d: %s\n", version.Version, version.UpdatedAt.Format(time.RFC3339)) +} +if err := iter.Err(); err != nil { + panic(err) +} + +// Archive the agent +_, err = client.Beta.Agents.Archive(ctx, agent.ID, anthropic.BetaAgentArchiveParams{}) +if err != nil { + panic(err) +} +``` + +--- + +## Send a User Message + +```go +_, err = client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ + Events: []anthropic.SendEventsParamsUnion{{ + OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ + Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, + Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{ + OfText: &anthropic.BetaManagedAgentsTextBlockParam{ + Type: anthropic.BetaManagedAgentsTextBlockTypeText, + Text: "Review the auth module", + }, + }}, + }, + }}, +}) +if err != nil { + panic(err) +} +``` + +> 💡 **Stream-first:** Open the stream *before* (or concurrently with) sending the message. The stream only delivers events that occur after it opens — stream-after-send means early events arrive buffered in one batch. See [Steering Patterns](../../shared/managed-agents-events.md#steering-patterns). + +--- + +## Stream Events (SSE) + +```go +// Open the stream first, then send the user message +stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{}) +defer stream.Close() + +if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ + Events: []anthropic.SendEventsParamsUnion{{ + OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ + Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, + Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{ + OfText: &anthropic.BetaManagedAgentsTextBlockParam{ + Type: anthropic.BetaManagedAgentsTextBlockTypeText, + Text: "Summarize the repo README", + }, + }}, + }, + }}, +}); err != nil { + panic(err) +} + +events: +for stream.Next() { + switch event := stream.Current().AsAny().(type) { + case anthropic.BetaManagedAgentsAgentMessageEvent: + for _, block := range event.Content { + fmt.Print(block.Text) + } + case anthropic.BetaManagedAgentsAgentToolUseEvent: + fmt.Printf("\n[Using tool: %s]\n", event.Name) + case anthropic.BetaManagedAgentsSessionStatusIdleEvent: + break events + case anthropic.BetaManagedAgentsSessionErrorEvent: + fmt.Printf("\n[Error: %s]\n", event.Error.Message) + break events + } +} +if err := stream.Err(); err != nil { + panic(err) +} +``` + +### Reconnecting and Tailing + +When reconnecting mid-session, list past events first to dedupe, then tail live events: + +```go +stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{}) +defer stream.Close() + +// Stream is open and buffering. List history before tailing live. +seenEventIDs := map[string]struct{}{} +history := client.Beta.Sessions.Events.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionEventListParams{}) +for history.Next() { + seenEventIDs[history.Current().ID] = struct{}{} +} +if err := history.Err(); err != nil { + panic(err) +} + +// Tail live events, skipping anything already seen +tail: +for stream.Next() { + event := stream.Current() + if _, seen := seenEventIDs[event.ID]; seen { + continue + } + seenEventIDs[event.ID] = struct{}{} + switch event := event.AsAny().(type) { + case anthropic.BetaManagedAgentsAgentMessageEvent: + for _, block := range event.Content { + fmt.Print(block.Text) + } + case anthropic.BetaManagedAgentsSessionStatusIdleEvent: + break tail + } +} +if err := stream.Err(); err != nil { + panic(err) +} +``` + +--- + +## Provide Custom Tool Result + +> ℹ️ The Go managed-agents bindings for `user.custom_tool_result` are not yet documented in this skill or in the apps source examples. Refer to `shared/managed-agents-events.md` for the wire format and the `github.com/anthropics/anthropic-sdk-go` repository for the corresponding Go params types. + +--- + +## Poll Events + +```go +// Auto-paginating iterator +iter := client.Beta.Sessions.Events.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionEventListParams{}) +for iter.Next() { + event := iter.Current() + fmt.Printf("%s: %s\n", event.Type, event.ID) +} +if err := iter.Err(); err != nil { + panic(err) +} +``` + +--- + +## Upload a File + +```go +csvFile, err := os.Open("data.csv") +if err != nil { + panic(err) +} +defer csvFile.Close() + +file, err := client.Beta.Files.Upload(ctx, anthropic.BetaFileUploadParams{ + File: csvFile, +}) +if err != nil { + panic(err) +} +fmt.Printf("File ID: %s\n", file.ID) + +// Mount in a session +session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{ + Agent: anthropic.BetaSessionNewParamsAgentUnion{ + OfString: anthropic.String(agent.ID), + }, + EnvironmentID: environment.ID, + Resources: []anthropic.BetaSessionNewParamsResourceUnion{{ + OfFile: &anthropic.BetaManagedAgentsFileResourceParams{ + Type: anthropic.BetaManagedAgentsFileResourceParamsTypeFile, + FileID: file.ID, + MountPath: anthropic.String("/workspace/data.csv"), + }, + }}, +}) +if err != nil { + panic(err) +} +``` + +### Add and Manage Resources on an Existing Session + +```go +// Attach an additional file to an open session +resource, err := client.Beta.Sessions.Resources.Add(ctx, session.ID, anthropic.BetaSessionResourceAddParams{ + BetaManagedAgentsFileResourceParams: anthropic.BetaManagedAgentsFileResourceParams{ + Type: anthropic.BetaManagedAgentsFileResourceParamsTypeFile, + FileID: file.ID, + }, +}) +if err != nil { + panic(err) +} +fmt.Println(resource.ID) // "sesrsc_01ABC..." + +// List resources on the session +listed, err := client.Beta.Sessions.Resources.List(ctx, session.ID, anthropic.BetaSessionResourceListParams{}) +if err != nil { + panic(err) +} +for _, entry := range listed.Data { + fmt.Println(entry.ID, entry.Type) +} + +// Detach a resource +if _, err := client.Beta.Sessions.Resources.Delete(ctx, resource.ID, anthropic.BetaSessionResourceDeleteParams{ + SessionID: session.ID, +}); err != nil { + panic(err) +} +``` + +--- + +## List and Download Session Files + +> ℹ️ Listing and downloading files an agent wrote during a session is not yet documented for Go in this skill or in the apps source examples. See `shared/managed-agents-events.md` and the `github.com/anthropics/anthropic-sdk-go` repository for the `Beta.Files.List` and `Beta.Files.Download` Go params types. + +--- + +## Session Management + +```go +// List environments +environments, err := client.Beta.Environments.List(ctx, anthropic.BetaEnvironmentListParams{}) +if err != nil { + panic(err) +} + +// Retrieve a specific environment +env, err := client.Beta.Environments.Get(ctx, environment.ID, anthropic.BetaEnvironmentGetParams{}) +if err != nil { + panic(err) +} + +// Archive an environment (read-only, existing sessions continue) +_, err = client.Beta.Environments.Archive(ctx, environment.ID, anthropic.BetaEnvironmentArchiveParams{}) +if err != nil { + panic(err) +} + +// Delete an environment (only if no sessions reference it) +_, err = client.Beta.Environments.Delete(ctx, environment.ID, anthropic.BetaEnvironmentDeleteParams{}) +if err != nil { + panic(err) +} + +// Delete a session +_, err = client.Beta.Sessions.Delete(ctx, session.ID, anthropic.BetaSessionDeleteParams{}) +if err != nil { + panic(err) +} +``` + +--- + +## MCP Server Integration + +```go +// Agent declares MCP server (no auth here — auth goes in a vault) +agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ + Name: "GitHub Assistant", + Model: anthropic.BetaManagedAgentsModelConfigParams{ + ID: "claude-opus-4-8", + Type: anthropic.BetaManagedAgentsModelConfigParamsTypeModelConfig, + }, + MCPServers: []anthropic.BetaManagedAgentsUrlmcpServerParams{{ + Type: anthropic.BetaManagedAgentsUrlmcpServerParamsTypeURL, + Name: "github", + URL: "https://api.githubcopilot.com/mcp/", + }}, + Tools: []anthropic.BetaAgentNewParamsToolUnion{ + { + OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{ + Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401, + }, + }, + { + OfMCPToolset: &anthropic.BetaManagedAgentsMCPToolsetParams{ + Type: anthropic.BetaManagedAgentsMCPToolsetParamsTypeMCPToolset, + MCPServerName: "github", + }, + }, + }, +}) +if err != nil { + panic(err) +} + +// Session attaches vault(s) containing credentials for those MCP server URLs +session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{ + Agent: anthropic.BetaSessionNewParamsAgentUnion{ + OfBetaManagedAgentsAgents: &anthropic.BetaManagedAgentsAgentParams{ + Type: anthropic.BetaManagedAgentsAgentParamsTypeAgent, + ID: agent.ID, + Version: anthropic.Int(agent.Version), + }, + }, + EnvironmentID: environment.ID, + VaultIDs: []string{vault.ID}, +}) +if err != nil { + panic(err) +} +``` + +See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials. + +--- + +## Vaults + +```go +// Create a vault +vault, err := client.Beta.Vaults.New(ctx, anthropic.BetaVaultNewParams{ + DisplayName: "Alice", + Metadata: map[string]string{"external_user_id": "usr_abc123"}, +}) +if err != nil { + panic(err) +} + +// Add an OAuth credential +credential, err := client.Beta.Vaults.Credentials.New(ctx, vault.ID, anthropic.BetaVaultCredentialNewParams{ + DisplayName: anthropic.String("Alice's Slack"), + Auth: anthropic.BetaVaultCredentialNewParamsAuthUnion{ + OfMCPOAuth: &anthropic.BetaManagedAgentsMCPOAuthCreateParams{ + Type: anthropic.BetaManagedAgentsMCPOAuthCreateParamsTypeMCPOAuth, + MCPServerURL: "https://mcp.slack.com/mcp", + AccessToken: "xoxp-...", + ExpiresAt: anthropic.Time(time.Date(2026, time.April, 15, 0, 0, 0, 0, time.UTC)), + Refresh: anthropic.BetaManagedAgentsMCPOAuthRefreshParams{ + TokenEndpoint: "https://slack.com/api/oauth.v2.access", + ClientID: "1234567890.0987654321", + Scope: anthropic.String("channels:read chat:write"), + RefreshToken: "xoxe-1-...", + TokenEndpointAuth: anthropic.BetaManagedAgentsMCPOAuthRefreshParamsTokenEndpointAuthUnion{ + OfClientSecretPost: &anthropic.BetaManagedAgentsTokenEndpointAuthPostParam{ + Type: anthropic.BetaManagedAgentsTokenEndpointAuthPostParamTypeClientSecretPost, + ClientSecret: "abc123...", + }, + }, + }, + }, + }, +}) +if err != nil { + panic(err) +} + +// Rotate the credential (e.g., after a token refresh) +_, err = client.Beta.Vaults.Credentials.Update(ctx, credential.ID, anthropic.BetaVaultCredentialUpdateParams{ + VaultID: vault.ID, + Auth: anthropic.BetaVaultCredentialUpdateParamsAuthUnion{ + OfMCPOAuth: &anthropic.BetaManagedAgentsMCPOAuthUpdateParams{ + Type: anthropic.BetaManagedAgentsMCPOAuthUpdateParamsTypeMCPOAuth, + AccessToken: anthropic.String("xoxp-new-..."), + ExpiresAt: anthropic.Time(time.Date(2026, time.May, 15, 0, 0, 0, 0, time.UTC)), + Refresh: anthropic.BetaManagedAgentsMCPOAuthRefreshUpdateParams{ + RefreshToken: anthropic.String("xoxe-1-new-..."), + }, + }, + }, +}) +if err != nil { + panic(err) +} + +// Archive a vault +_, err = client.Beta.Vaults.Archive(ctx, vault.ID, anthropic.BetaVaultArchiveParams{}) +if err != nil { + panic(err) +} +``` + +--- + +## GitHub Repository Integration + +Mount a GitHub repository as a session resource (a vault holds the GitHub MCP credential): + +```go +session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{ + Agent: anthropic.BetaSessionNewParamsAgentUnion{OfString: anthropic.String(agent.ID)}, + EnvironmentID: environment.ID, + VaultIDs: []string{vault.ID}, + Resources: []anthropic.BetaSessionNewParamsResourceUnion{ + { + OfGitHubRepository: &anthropic.BetaManagedAgentsGitHubRepositoryResourceParams{ + Type: anthropic.BetaManagedAgentsGitHubRepositoryResourceParamsTypeGitHubRepository, + URL: "https://github.com/org/repo", + MountPath: anthropic.String("/workspace/repo"), + AuthorizationToken: "ghp_your_github_token", + }, + }, + }, +}) +if err != nil { + panic(err) +} +``` + +Multiple repositories on the same session: + +```go +resources := []anthropic.BetaSessionNewParamsResourceUnion{ + { + OfGitHubRepository: &anthropic.BetaManagedAgentsGitHubRepositoryResourceParams{ + Type: anthropic.BetaManagedAgentsGitHubRepositoryResourceParamsTypeGitHubRepository, + URL: "https://github.com/org/frontend", + MountPath: anthropic.String("/workspace/frontend"), + AuthorizationToken: "ghp_your_github_token", + }, + }, + { + OfGitHubRepository: &anthropic.BetaManagedAgentsGitHubRepositoryResourceParams{ + Type: anthropic.BetaManagedAgentsGitHubRepositoryResourceParamsTypeGitHubRepository, + URL: "https://github.com/org/backend", + MountPath: anthropic.String("/workspace/backend"), + AuthorizationToken: "ghp_your_github_token", + }, + }, +} +``` + +Rotating a repository's authorization token: + +```go +listed, err := client.Beta.Sessions.Resources.List(ctx, session.ID, anthropic.BetaSessionResourceListParams{}) +if err != nil { + panic(err) +} +repoResourceID := listed.Data[0].ID + +_, err = client.Beta.Sessions.Resources.Update(ctx, repoResourceID, anthropic.BetaSessionResourceUpdateParams{ + SessionID: session.ID, + AuthorizationToken: "ghp_your_new_github_token", +}) +if err != nil { + panic(err) +} +``` diff --git a/.agents/skills/claude-api/java/claude-api.md b/.agents/skills/claude-api/java/claude-api.md new file mode 100644 index 00000000..31ea7cff --- /dev/null +++ b/.agents/skills/claude-api/java/claude-api.md @@ -0,0 +1,461 @@ +# Claude API — Java + +> **Note:** The Java SDK supports the Claude API and beta tool use with annotated classes. Agent SDK is not yet available for Java. + +## Installation + +Maven: + +```xml + + com.anthropic + anthropic-java + 2.34.0 + +``` + +Gradle: + +```groovy +implementation("com.anthropic:anthropic-java:2.34.0") +``` + +## Client Initialization + +```java +import com.anthropic.client.AnthropicClient; +import com.anthropic.client.okhttp.AnthropicOkHttpClient; + +// Default (reads ANTHROPIC_API_KEY from environment) +AnthropicClient client = AnthropicOkHttpClient.fromEnv(); + +// Explicit API key +AnthropicClient client = AnthropicOkHttpClient.builder() + .apiKey("your-api-key") + .build(); +``` + +--- + +## Basic Message Request + +```java +import com.anthropic.models.messages.MessageCreateParams; +import com.anthropic.models.messages.Message; +import com.anthropic.models.messages.Model; + +MessageCreateParams params = MessageCreateParams.builder() + .model(Model.CLAUDE_OPUS_4_6) + .maxTokens(16000L) + .addUserMessage("What is the capital of France?") + .build(); + +Message response = client.messages().create(params); +response.content().stream() + .flatMap(block -> block.text().stream()) + .forEach(textBlock -> System.out.println(textBlock.text())); +``` + +--- + +## Streaming + +```java +import com.anthropic.core.http.StreamResponse; +import com.anthropic.models.messages.RawMessageStreamEvent; + +MessageCreateParams params = MessageCreateParams.builder() + .model(Model.CLAUDE_OPUS_4_6) + .maxTokens(64000L) + .addUserMessage("Write a haiku") + .build(); + +try (StreamResponse streamResponse = client.messages().createStreaming(params)) { + streamResponse.stream() + .flatMap(event -> event.contentBlockDelta().stream()) + .flatMap(deltaEvent -> deltaEvent.delta().text().stream()) + .forEach(textDelta -> System.out.print(textDelta.text())); +} +``` + +--- + +## Thinking + +**Adaptive thinking is the recommended mode for Claude 4.6+ models.** Claude decides dynamically when and how much to think. The builder has a direct `.thinking(ThinkingConfigAdaptive)` overload — no manual union wrapping. + +```java +import com.anthropic.models.messages.ContentBlock; +import com.anthropic.models.messages.MessageCreateParams; +import com.anthropic.models.messages.Model; +import com.anthropic.models.messages.ThinkingConfigAdaptive; + +MessageCreateParams params = MessageCreateParams.builder() + .model(Model.CLAUDE_SONNET_4_6) + .maxTokens(16000L) + .thinking(ThinkingConfigAdaptive.builder().build()) + .addUserMessage("Solve this step by step: 27 * 453") + .build(); + +for (ContentBlock block : client.messages().create(params).content()) { + block.thinking().ifPresent(t -> System.out.println("[thinking] " + t.thinking())); + block.text().ifPresent(t -> System.out.println(t.text())); +} +``` + +> **Deprecated:** `ThinkingConfigEnabled.builder().budgetTokens(N)` (and the `.enabledThinking(N)` shortcut) still works on Claude 4.6 but is deprecated. Use adaptive thinking above. + +`ContentBlock` narrowing: `.thinking()` / `.text()` return `Optional` — use `.ifPresent(...)` or `.stream().flatMap(...)`. Alternative: `isThinking()` / `asThinking()` boolean+unwrap pairs (throws on wrong variant). + +--- + +## Tool Use (Beta) + +The Java SDK supports beta tool use with annotated classes. Tool classes implement `Supplier` for automatic execution via `BetaToolRunner`. + +### Tool Runner (automatic loop) + +```java +import com.anthropic.models.beta.messages.MessageCreateParams; +import com.anthropic.models.beta.messages.BetaMessage; +import com.anthropic.helpers.BetaToolRunner; +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import java.util.function.Supplier; + +@JsonClassDescription("Get the weather in a given location") +static class GetWeather implements Supplier { + @JsonPropertyDescription("The city and state, e.g. San Francisco, CA") + public String location; + + @Override + public String get() { + return "The weather in " + location + " is sunny and 72°F"; + } +} + +BetaToolRunner toolRunner = client.beta().messages().toolRunner( + MessageCreateParams.builder() + .model("claude-opus-4-8") + .maxTokens(16000L) + .putAdditionalHeader("anthropic-beta", "structured-outputs-2025-11-13") + .addTool(GetWeather.class) + .addUserMessage("What's the weather in San Francisco?") + .build()); + +for (BetaMessage message : toolRunner) { + System.out.println(message); +} +``` + +### Memory Tool + +The Java SDK provides `BetaMemoryToolHandler` for implementing the memory tool backend. You supply a handler that manages file storage, and the `BetaToolRunner` handles memory tool calls automatically. + +```java +import com.anthropic.helpers.BetaMemoryToolHandler; +import com.anthropic.helpers.BetaToolRunner; +import com.anthropic.models.beta.messages.BetaMemoryTool20250818; +import com.anthropic.models.beta.messages.BetaMessage; +import com.anthropic.models.beta.messages.MessageCreateParams; +import com.anthropic.models.beta.messages.ToolRunnerCreateParams; + +// Implement BetaMemoryToolHandler with your storage backend (e.g., filesystem) +BetaMemoryToolHandler memoryHandler = new FileSystemMemoryToolHandler(sandboxRoot); + +MessageCreateParams createParams = MessageCreateParams.builder() + .model("claude-opus-4-8") + .maxTokens(4096L) + .addTool(BetaMemoryTool20250818.builder().build()) + .addUserMessage("Remember that my favorite color is blue") + .build(); + +BetaToolRunner toolRunner = client.beta().messages().toolRunner( + ToolRunnerCreateParams.builder() + .betaMemoryToolHandler(memoryHandler) + .initialMessageParams(createParams) + .build()); + +for (BetaMessage message : toolRunner) { + System.out.println(message); +} +``` + +See the [shared memory tool concepts](../shared/tool-use-concepts.md) for more details on the memory tool. + +### Non-Beta Tool Declaration (manual JSON schema) + +`Tool.InputSchema.Properties` is a freeform `Map` wrapper — build property schemas via `putAdditionalProperty`. `type: "object"` is the default. The builder has a direct `.addTool(Tool)` overload that wraps in `ToolUnion` automatically. + +```java +import com.anthropic.core.JsonValue; +import com.anthropic.models.messages.Tool; + +Tool tool = Tool.builder() + .name("get_weather") + .description("Get the current weather in a given location") + .inputSchema(Tool.InputSchema.builder() + .properties(Tool.InputSchema.Properties.builder() + .putAdditionalProperty("location", JsonValue.from(Map.of("type", "string"))) + .build()) + .required(List.of("location")) + .build()) + .build(); + +MessageCreateParams params = MessageCreateParams.builder() + .model(Model.CLAUDE_SONNET_4_6) + .maxTokens(16000L) + .addTool(tool) + .addUserMessage("Weather in Paris?") + .build(); +``` + +For manual tool loops, handle `tool_use` blocks in the response, send `tool_result` back, loop until `stop_reason` is `"end_turn"`. See [shared tool use concepts](../shared/tool-use-concepts.md). + +### Building `MessageParam` with Content Blocks (Tool Result Round-Trip) + +`MessageParam.Content` is an inner union class (string | list). Use the builder's `.contentOfBlockParams(List)` alias — there is NO separate `MessageParamContent` class with a static `ofBlockParams`: + +```java +import com.anthropic.models.messages.MessageParam; +import com.anthropic.models.messages.ContentBlockParam; +import com.anthropic.models.messages.ToolResultBlockParam; + +List results = List.of( + ContentBlockParam.ofToolResult(ToolResultBlockParam.builder() + .toolUseId(toolUseBlock.id()) + .content(yourResultString) + .build()) +); + +MessageParam toolResultMsg = MessageParam.builder() + .role(MessageParam.Role.USER) + .contentOfBlockParams(results) // builder alias for Content.ofBlockParams(...) + .build(); +``` + +--- + +## Effort Parameter + +Effort is nested inside `OutputConfig` — there is NO `.effort()` directly on `MessageCreateParams.Builder`. + +```java +import com.anthropic.models.messages.OutputConfig; + +.outputConfig(OutputConfig.builder() + .effort(OutputConfig.Effort.HIGH) // or LOW, MEDIUM, MAX + .build()) +``` + +Combine with `Thinking = ThinkingConfigAdaptive` for cost-quality control. + +--- + +## Prompt Caching + +System message as a list of `TextBlockParam` with `CacheControlEphemeral`. Use `.systemOfTextBlockParams(...)` — the plain `.system(String)` overload can't carry cache control. For placement patterns and the silent-invalidator audit checklist, see `shared/prompt-caching.md`. + +```java +import com.anthropic.models.messages.TextBlockParam; +import com.anthropic.models.messages.CacheControlEphemeral; + +.systemOfTextBlockParams(List.of( + TextBlockParam.builder() + .text(longSystemPrompt) + .cacheControl(CacheControlEphemeral.builder() + .ttl(CacheControlEphemeral.Ttl.TTL_1H) // optional; also TTL_5M + .build()) + .build())) +``` + +There's also a top-level `.cacheControl(CacheControlEphemeral)` on `MessageCreateParams.Builder` and on `Tool.builder()`. + +Verify hits via `response.usage().cacheCreationInputTokens()` / `response.usage().cacheReadInputTokens()`. + +--- + +## Token Counting + +```java +import com.anthropic.models.messages.MessageCountTokensParams; + +long tokens = client.messages().countTokens( + MessageCountTokensParams.builder() + .model(Model.CLAUDE_SONNET_4_6) + .addUserMessage("Hello") + .build() +).inputTokens(); +``` + +--- + +## Structured Output + +The class-based overload auto-derives the JSON schema from your POJO and gives you a typed `.text()` return — no manual schema, no manual parsing. + +```java +import com.anthropic.models.messages.StructuredMessageCreateParams; + +record Book(String title, String author) {} +record BookList(List books) {} + +StructuredMessageCreateParams params = MessageCreateParams.builder() + .model(Model.CLAUDE_SONNET_4_6) + .maxTokens(16000L) + .outputConfig(BookList.class) // returns a typed builder + .addUserMessage("List 3 classic novels") + .build(); + +client.messages().create(params).content().stream() + .flatMap(cb -> cb.text().stream()) + .forEach(typed -> { + // typed.text() returns BookList, not String + for (Book b : typed.text().books()) System.out.println(b.title()); + }); +``` + +Supports Jackson annotations: `@JsonPropertyDescription`, `@JsonIgnore`, `@ArraySchema(minItems=...)`. Manual schema path: `OutputConfig.builder().format(JsonOutputFormat.builder().schema(...).build())`. + +--- + +## PDF / Document Input + +`DocumentBlockParam` builder has source shortcuts. Wrap in `ContentBlockParam.ofDocument()` and pass via `.addUserMessageOfBlockParams()`. + +```java +import com.anthropic.models.messages.DocumentBlockParam; +import com.anthropic.models.messages.ContentBlockParam; +import com.anthropic.models.messages.TextBlockParam; + +DocumentBlockParam doc = DocumentBlockParam.builder() + .base64Source(base64String) // or .urlSource("https://...") or .textSource("...") + .title("My Document") // optional + .build(); + +.addUserMessageOfBlockParams(List.of( + ContentBlockParam.ofDocument(doc), + ContentBlockParam.ofText(TextBlockParam.builder().text("Summarize this").build()))) +``` + +--- + +## Server-Side Tools + +Version-suffixed types; `name`/`type` auto-set by builder. Direct `.addTool()` overloads exist for every type — no manual `ToolUnion` wrapping. + +```java +import com.anthropic.models.messages.WebSearchTool20260209; +import com.anthropic.models.messages.ToolBash20250124; +import com.anthropic.models.messages.ToolTextEditor20250728; +import com.anthropic.models.messages.CodeExecutionTool20260120; + +.addTool(WebSearchTool20260209.builder() + .maxUses(5L) // optional + .allowedDomains(List.of("example.com")) // optional + .build()) +.addTool(ToolBash20250124.builder().build()) +.addTool(ToolTextEditor20250728.builder().build()) +.addTool(CodeExecutionTool20260120.builder().build()) +``` + +Also available: `WebFetchTool20260209`, `MemoryTool20250818`, `ToolSearchToolBm25_20251119`. For the advisor tool, use `BetaAdvisorTool20260301` in the beta namespace. + +### Beta namespace (MCP, compaction) + +For beta-only features use `com.anthropic.models.beta.messages.*` — class names have a `Beta` prefix AND live in the beta package. The beta `MessageCreateParams.Builder` has direct `.addTool(BetaToolBash20250124)` overloads AND `.addMcpServer()`: + +```java +import com.anthropic.models.beta.messages.MessageCreateParams; +import com.anthropic.models.beta.messages.BetaToolBash20250124; +import com.anthropic.models.beta.messages.BetaCodeExecutionTool20260120; +import com.anthropic.models.beta.messages.BetaRequestMcpServerUrlDefinition; + +MessageCreateParams params = MessageCreateParams.builder() + .model(Model.CLAUDE_OPUS_4_6) + .maxTokens(16000L) + .addBeta("mcp-client-2025-11-20") + .addTool(BetaToolBash20250124.builder().build()) + .addTool(BetaCodeExecutionTool20260120.builder().build()) + .addMcpServer(BetaRequestMcpServerUrlDefinition.builder() + .name("my-server") + .url("https://example.com/mcp") + .build()) + .addUserMessage("...") + .build(); + +client.beta().messages().create(params); +``` + +`BetaTool*` types are NOT interchangeable with non-beta `Tool*` — pick one namespace per request. + +**Reading server-tool blocks in the response:** `ServerToolUseBlock` has `.id()`, `.name()` (enum), and `._input()` returning raw `JsonValue` — there is NO typed `.input()`. For code execution results, unwrap two levels: + +```java +for (ContentBlock block : response.content()) { + block.serverToolUse().ifPresent(stu -> { + System.out.println("tool: " + stu.name() + " input: " + stu._input()); + }); + block.codeExecutionToolResult().ifPresent(r -> { + r.content().resultBlock().ifPresent(result -> { + System.out.println("stdout: " + result.stdout()); + System.out.println("stderr: " + result.stderr()); + System.out.println("exit: " + result.returnCode()); + }); + }); +} +``` + +--- + +## Stop Details + +When `stopReason()` is `"refusal"`, the response includes structured `stopDetails()`: + +```java +response.stopDetails().ifPresent(details -> { + System.out.println("Category: " + details.category()); + System.out.println("Explanation: " + details.explanation()); +}); +``` + +--- + +## Error Type + +`AnthropicServiceException` exposes `.errorType()` returning `Optional` for programmatic error classification: + +```java +try { + client.messages().create(params); +} catch (AnthropicServiceException e) { + e.errorType().ifPresent(type -> + System.out.println("Error type: " + type) // RATE_LIMIT_ERROR, OVERLOADED_ERROR, etc. + ); +} +``` + +--- + +## Files API (Beta) + +Under `client.beta().files()`. File references in messages need the beta message types (non-beta `DocumentBlockParam.Source` has no file-ID variant). + +```java +import com.anthropic.models.beta.files.FileUploadParams; +import com.anthropic.models.beta.files.FileMetadata; +import com.anthropic.models.beta.messages.BetaRequestDocumentBlock; +import java.nio.file.Paths; + +FileMetadata meta = client.beta().files().upload( + FileUploadParams.builder() + .file(Paths.get("/path/to/doc.pdf")) // or .file(InputStream) or .file(byte[]) + .build()); + +// Reference in a beta message: +BetaRequestDocumentBlock doc = BetaRequestDocumentBlock.builder() + .fileSource(meta.id()) + .build(); +``` + +Other methods: `.list()`, `.delete(String fileId)`, `.download(String fileId)`, `.retrieveMetadata(String fileId)`. diff --git a/.agents/skills/claude-api/java/managed-agents/README.md b/.agents/skills/claude-api/java/managed-agents/README.md new file mode 100644 index 00000000..c09954d7 --- /dev/null +++ b/.agents/skills/claude-api/java/managed-agents/README.md @@ -0,0 +1,442 @@ +# Managed Agents — Java + +> **Bindings not shown here:** This README covers the most common managed-agents flows for Java. If you need a class, method, namespace, field, or behavior that isn't shown, WebFetch the Java SDK repo **or the relevant docs page** from `shared/live-sources.md` rather than guess. Do not extrapolate from cURL shapes or another language's SDK. + +> **Agents are persistent — create once, reference by ID.** Store the agent ID returned by `client.beta().agents().create` and pass it to every subsequent `client.beta().sessions().create`; do not call `agents().create` in the request path. The Anthropic CLI is one convenient way to create agents and environments from version-controlled YAML — its URL is in `shared/live-sources.md`. The examples below show in-code creation for completeness; in production the create call belongs in setup, not in the request path. + +## Installation + +```xml + + com.anthropic + anthropic-java + +``` + +## Client Initialization + +```java +import com.anthropic.client.okhttp.AnthropicOkHttpClient; + +// Default (uses ANTHROPIC_API_KEY env var) +var client = AnthropicOkHttpClient.fromEnv(); +``` + +--- + +## Create an Environment + +```java +import com.anthropic.models.beta.environments.BetaCloudConfigParams; +import com.anthropic.models.beta.environments.EnvironmentCreateParams; +import com.anthropic.models.beta.environments.UnrestrictedNetwork; + +var environment = client.beta().environments().create(EnvironmentCreateParams.builder() + .name("my-dev-env") + .config(BetaCloudConfigParams.builder() + .networking(UnrestrictedNetwork.builder().build()) + .build()) + .build()); +System.out.println("Environment ID: " + environment.id()); // env_... +``` + +--- + +## Create an Agent (required first step) + +> ⚠️ **There is no inline agent config.** Model, system, and tools live on the agent object, not the session. Always start with `client.beta().agents().create()` — the session takes either `.agent(agent.id())` or the typed `BetaManagedAgentsAgentParams.builder()...build()`. + +### Minimal + +```java +import com.anthropic.models.beta.agents.AgentCreateParams; +import com.anthropic.models.beta.agents.BetaManagedAgentsAgentToolset20260401Params; +import com.anthropic.models.beta.sessions.BetaManagedAgentsAgentParams; +import com.anthropic.models.beta.sessions.SessionCreateParams; + +// 1. Create the agent (reusable, versioned) +var agent = client.beta().agents().create(AgentCreateParams.builder() + .name("Coding Assistant") + .model("claude-opus-4-8") + .system("You are a helpful coding assistant.") + .addTool(BetaManagedAgentsAgentToolset20260401Params.builder() + .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401) + .build()) + .build()); + +// 2. Start a session +var session = client.beta().sessions().create(SessionCreateParams.builder() + .agent(BetaManagedAgentsAgentParams.builder() + .type(BetaManagedAgentsAgentParams.Type.AGENT) + .id(agent.id()) + .version(agent.version()) + .build()) + .environmentId(environment.id()) + .title("Quickstart session") + .build()); +System.out.println("Session ID: " + session.id()); +``` + +### Updating an Agent + +Updates create new versions; the agent object is immutable per version. + +```java +import com.anthropic.models.beta.agents.AgentUpdateParams; + +var updatedAgent = client.beta().agents().update(agent.id(), AgentUpdateParams.builder() + .version(agent.version()) + .system("You are a helpful coding agent. Always write tests.") + .build()); +System.out.println("New version: " + updatedAgent.version()); + +// List all versions +for (var version : client.beta().agents().versions().list(agent.id()).autoPager()) { + System.out.println("Version " + version.version() + ": " + version.updatedAt()); +} + +// Archive the agent +var archived = client.beta().agents().archive(agent.id()); +System.out.println("Archived at: " + archived.archivedAt().orElseThrow()); +``` + +--- + +## Send a User Message + +```java +import com.anthropic.models.beta.sessions.events.BetaManagedAgentsUserMessageEventParams; +import com.anthropic.models.beta.sessions.events.EventSendParams; + +client.beta().sessions().events().send(session.id(), EventSendParams.builder() + .addEvent(BetaManagedAgentsUserMessageEventParams.builder() + .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE) + .addTextContent("Review the auth module") + .build()) + .build()); +``` + +> 💡 **Stream-first:** Open the stream *before* (or concurrently with) sending the message. The stream only delivers events that occur after it opens — stream-after-send means early events arrive buffered in one batch. See [Steering Patterns](../../shared/managed-agents-events.md#steering-patterns). + +--- + +## Stream Events (SSE) + +```java +import com.anthropic.models.beta.sessions.events.StreamEvents; + +// Open the stream first, then send the user message +try (var stream = client.beta().sessions().events().streamStreaming(session.id())) { + client.beta().sessions().events().send(session.id(), EventSendParams.builder() + .addEvent(BetaManagedAgentsUserMessageEventParams.builder() + .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE) + .addTextContent("Summarize the repo README") + .build()) + .build()); + + for (var event : (Iterable) stream.stream()::iterator) { + if (event.isAgentMessage()) { + event.asAgentMessage().content().forEach(block -> System.out.print(block.text())); + } else if (event.isAgentToolUse()) { + System.out.println("\n[Using tool: " + event.asAgentToolUse().name() + "]"); + } else if (event.isSessionStatusIdle()) { + break; + } else if (event.isSessionError()) { + System.out.println("\n[Error]"); + break; + } + } +} +``` + +### Reconnecting and Tailing + +When reconnecting mid-session, list past events first to dedupe, then tail live events. The cross-variant `id` field is read from the raw `_json()` value: + +```java +import com.anthropic.core.JsonValue; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; + +try (var stream = client.beta().sessions().events().streamStreaming(session.id())) { + // Stream is open and buffering. List history before tailing live. + var seenEventIds = new HashSet(); + for (var past : client.beta().sessions().events().list(session.id()).autoPager()) { + Optional> obj = past._json().orElseThrow().asObject(); + seenEventIds.add(obj.orElseThrow().get("id").asStringOrThrow()); + } + + // Tail live events, skipping anything already seen + for (var event : (Iterable) stream.stream()::iterator) { + Optional> obj = event._json().orElseThrow().asObject(); + if (!seenEventIds.add(obj.orElseThrow().get("id").asStringOrThrow())) continue; + if (event.isAgentMessage()) { + event.asAgentMessage().content().forEach(block -> System.out.print(block.text())); + } else if (event.isSessionStatusIdle()) { + break; + } + } +} +``` + +--- + +## Provide Custom Tool Result + +> ℹ️ The Java managed-agents bindings for `user.custom_tool_result` are not yet documented in this skill or in the apps source examples. Refer to `shared/managed-agents-events.md` for the wire format and the `anthropic-java` repository for the corresponding params types. + +--- + +## Poll Events + +```java +for (var event : client.beta().sessions().events().list(session.id()).autoPager()) { + System.out.println(event.type() + ": " + event); +} +``` + +--- + +## Upload a File + +```java +import com.anthropic.models.beta.files.FileUploadParams; +import com.anthropic.models.beta.sessions.BetaManagedAgentsFileResourceParams; +import java.nio.file.Path; + +var dataCsv = Path.of("data.csv"); + +var file = client.beta().files().upload(FileUploadParams.builder() + .file(dataCsv) + .build()); +System.out.println("File ID: " + file.id()); + +// Mount in a session +var session = client.beta().sessions().create(SessionCreateParams.builder() + .agent(agent.id()) + .environmentId(environment.id()) + .addResource(BetaManagedAgentsFileResourceParams.builder() + .type(BetaManagedAgentsFileResourceParams.Type.FILE) + .fileId(file.id()) + .mountPath("/workspace/data.csv") + .build()) + .build()); +``` + +### Add and Manage Resources on an Existing Session + +```java +import com.anthropic.models.beta.sessions.resources.ResourceAddParams; +import com.anthropic.models.beta.sessions.resources.ResourceDeleteParams; + +// Attach an additional file to an open session +var resource = client.beta().sessions().resources().add(session.id(), ResourceAddParams.builder() + .betaManagedAgentsFileResourceParams(BetaManagedAgentsFileResourceParams.builder() + .type(BetaManagedAgentsFileResourceParams.Type.FILE) + .fileId(file.id()) + .build()) + .build()); +System.out.println(resource.id()); // "sesrsc_01ABC..." + +// List resources on the session — entries are a discriminated union +var listed = client.beta().sessions().resources().list(session.id()); +for (var entry : listed.data()) { + if (entry.isFile()) { + var fileResource = entry.asFile(); + System.out.println(fileResource.id() + " " + fileResource.type()); + } else if (entry.isGitHubRepository()) { + var repoResource = entry.asGitHubRepository(); + System.out.println(repoResource.id() + " " + repoResource.type()); + } +} + +// Detach a resource +client.beta().sessions().resources().delete(resource.id(), ResourceDeleteParams.builder() + .sessionId(session.id()) + .build()); +``` + +--- + +## List and Download Session Files + +> ℹ️ Listing and downloading files an agent wrote during a session is not yet documented for Java in this skill or in the apps source examples. See `shared/managed-agents-events.md` and the `anthropic-java` repository for the file list/download bindings. + +--- + +## Session Management + +```java +// List environments +var environments = client.beta().environments().list(); + +// Retrieve a specific environment +var env = client.beta().environments().retrieve(environment.id()); + +// Archive an environment (read-only, existing sessions continue) +client.beta().environments().archive(environment.id()); + +// Delete an environment (only if no sessions reference it) +client.beta().environments().delete(environment.id()); + +// Delete a session +client.beta().sessions().delete(session.id()); +``` + +--- + +## MCP Server Integration + +```java +import com.anthropic.models.beta.agents.BetaManagedAgentsMcpToolsetParams; +import com.anthropic.models.beta.agents.BetaManagedAgentsUrlmcpServerParams; + +// Agent declares MCP server (no auth here — auth goes in a vault) +var agent = client.beta().agents().create(AgentCreateParams.builder() + .name("GitHub Assistant") + .model("claude-opus-4-8") + .addMcpServer(BetaManagedAgentsUrlmcpServerParams.builder() + .type(BetaManagedAgentsUrlmcpServerParams.Type.URL) + .name("github") + .url("https://api.githubcopilot.com/mcp/") + .build()) + .addTool(BetaManagedAgentsAgentToolset20260401Params.builder() + .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401) + .build()) + .addTool(BetaManagedAgentsMcpToolsetParams.builder() + .type(BetaManagedAgentsMcpToolsetParams.Type.MCP_TOOLSET) + .mcpServerName("github") + .build()) + .build()); + +// Session attaches vault(s) containing credentials for those MCP server URLs +var session = client.beta().sessions().create(SessionCreateParams.builder() + .agent(BetaManagedAgentsAgentParams.builder() + .type(BetaManagedAgentsAgentParams.Type.AGENT) + .id(agent.id()) + .version(agent.version()) + .build()) + .environmentId(environment.id()) + .addVaultId(vault.id()) + .build()); +``` + +See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials. + +--- + +## Vaults + +```java +import com.anthropic.core.JsonValue; +import com.anthropic.models.beta.vaults.VaultCreateParams; +import com.anthropic.models.beta.vaults.credentials.BetaManagedAgentsMcpOAuthCreateParams; +import com.anthropic.models.beta.vaults.credentials.BetaManagedAgentsMcpOAuthRefreshParams; +import com.anthropic.models.beta.vaults.credentials.BetaManagedAgentsMcpOAuthRefreshUpdateParams; +import com.anthropic.models.beta.vaults.credentials.BetaManagedAgentsMcpOAuthUpdateParams; +import com.anthropic.models.beta.vaults.credentials.CredentialCreateParams; +import com.anthropic.models.beta.vaults.credentials.CredentialUpdateParams; +import java.time.OffsetDateTime; + +// Create a vault +var vault = client.beta().vaults().create(VaultCreateParams.builder() + .displayName("Alice") + .metadata(VaultCreateParams.Metadata.builder() + .putAdditionalProperty("external_user_id", JsonValue.from("usr_abc123")) + .build()) + .build()); +System.out.println(vault.id()); // "vlt_01ABC..." + +// Add an OAuth credential +var credential = client.beta().vaults().credentials().create(vault.id(), + CredentialCreateParams.builder() + .displayName("Alice's Slack") + .auth(BetaManagedAgentsMcpOAuthCreateParams.builder() + .type(BetaManagedAgentsMcpOAuthCreateParams.Type.MCP_OAUTH) + .mcpServerUrl("https://mcp.slack.com/mcp") + .accessToken("xoxp-...") + .expiresAt(OffsetDateTime.parse("2026-04-15T00:00:00Z")) + .refresh(BetaManagedAgentsMcpOAuthRefreshParams.builder() + .tokenEndpoint("https://slack.com/api/oauth.v2.access") + .clientId("1234567890.0987654321") + .scope("channels:read chat:write") + .refreshToken("xoxe-1-...") + .clientSecretPostTokenEndpointAuth("abc123...") + .build()) + .build()) + .build()); + +// Rotate the credential (e.g., after a token refresh) +client.beta().vaults().credentials().update(credential.id(), + CredentialUpdateParams.builder() + .vaultId(vault.id()) + .auth(BetaManagedAgentsMcpOAuthUpdateParams.builder() + .type(BetaManagedAgentsMcpOAuthUpdateParams.Type.MCP_OAUTH) + .accessToken("xoxp-new-...") + .expiresAt(OffsetDateTime.parse("2026-05-15T00:00:00Z")) + .refresh(BetaManagedAgentsMcpOAuthRefreshUpdateParams.builder() + .refreshToken("xoxe-1-new-...") + .build()) + .build()) + .build()); + +// Archive a vault +client.beta().vaults().archive(vault.id()); +``` + +--- + +## GitHub Repository Integration + +Mount a GitHub repository as a session resource (a vault holds the GitHub MCP credential): + +```java +import com.anthropic.models.beta.sessions.BetaManagedAgentsGitHubRepositoryResourceParams; + +var session = client.beta().sessions().create(SessionCreateParams.builder() + .agent(agent.id()) + .environmentId(environment.id()) + .addVaultId(vault.id()) + .addResource(BetaManagedAgentsGitHubRepositoryResourceParams.builder() + .type(BetaManagedAgentsGitHubRepositoryResourceParams.Type.GITHUB_REPOSITORY) + .url("https://github.com/org/repo") + .mountPath("/workspace/repo") + .authorizationToken("ghp_your_github_token") + .build()) + .build()); +``` + +Multiple repositories on the same session: + +```java +import java.util.List; + +var resources = List.of( + BetaManagedAgentsGitHubRepositoryResourceParams.builder() + .type(BetaManagedAgentsGitHubRepositoryResourceParams.Type.GITHUB_REPOSITORY) + .url("https://github.com/org/frontend") + .mountPath("/workspace/frontend") + .authorizationToken("ghp_your_github_token") + .build(), + BetaManagedAgentsGitHubRepositoryResourceParams.builder() + .type(BetaManagedAgentsGitHubRepositoryResourceParams.Type.GITHUB_REPOSITORY) + .url("https://github.com/org/backend") + .mountPath("/workspace/backend") + .authorizationToken("ghp_your_github_token") + .build()); +``` + +Rotating a repository's authorization token: + +```java +import com.anthropic.models.beta.sessions.resources.ResourceUpdateParams; + +var listed = client.beta().sessions().resources().list(session.id()); +var repoResourceId = listed.data().get(0).asGitHubRepository().id(); + +client.beta().sessions().resources().update(repoResourceId, ResourceUpdateParams.builder() + .sessionId(session.id()) + .authorizationToken("ghp_your_new_github_token") + .build()); +``` diff --git a/.agents/skills/claude-api/php/claude-api.md b/.agents/skills/claude-api/php/claude-api.md new file mode 100644 index 00000000..0deb9497 --- /dev/null +++ b/.agents/skills/claude-api/php/claude-api.md @@ -0,0 +1,402 @@ +# Claude API — PHP + +> **Note:** The PHP SDK is the official Anthropic SDK for PHP. A beta tool runner is available via `$client->beta->messages->toolRunner()`. Structured output helpers are supported via `StructuredOutputModel` classes. Agent SDK is not available. Bedrock, Vertex AI, and Foundry clients are supported. + +## Installation + +```bash +composer require "anthropic-ai/sdk" +``` + +## Client Initialization + +```php +use Anthropic\Client; + +// Using API key from environment variable +$client = new Client(apiKey: getenv("ANTHROPIC_API_KEY")); +``` + +### Amazon Bedrock + +```php +use Anthropic\Bedrock; + +// Constructor is private — use the static factory. Reads AWS credentials from env. +$client = Bedrock\Client::fromEnvironment(region: 'us-east-1'); +``` + +### Google Vertex AI + +```php +use Anthropic\Vertex; + +// Constructor is private. Parameter is `location`, not `region`. +$client = Vertex\Client::fromEnvironment( + location: 'us-east5', + projectId: 'my-project-id', +); +``` + +### Anthropic Foundry + +```php +use Anthropic\Foundry; + +// Constructor is private. baseUrl or resource is required. +$client = Foundry\Client::withCredentials( + authToken: getenv('ANTHROPIC_FOUNDRY_AUTH_TOKEN'), + baseUrl: 'https://.services.ai.azure.com/anthropic', +); +``` + +--- + +## Basic Message Request + +```php +$message = $client->messages->create( + model: 'claude-opus-4-8', + maxTokens: 16000, + messages: [ + ['role' => 'user', 'content' => 'What is the capital of France?'], + ], +); + +// content is an array of polymorphic blocks (TextBlock, ToolUseBlock, +// ThinkingBlock). Accessing ->text on content[0] without checking the block +// type will throw if the first block is not a TextBlock (e.g., when extended +// thinking is enabled and a ThinkingBlock comes first). Always guard: +foreach ($message->content as $block) { + if ($block->type === 'text') { + echo $block->text; + } +} +``` + +If you only want the first text block: + +```php +foreach ($message->content as $block) { + if ($block->type === 'text') { + echo $block->text; + break; + } +} +``` + +--- + +## Streaming + +> **Requires SDK v0.5.0+.** v0.4.0 and earlier used a single `$params` array; calling with named parameters throws `Unknown named parameter $model`. Upgrade: `composer require "anthropic-ai/sdk:^0.7"` + +```php +use Anthropic\Messages\RawContentBlockDeltaEvent; +use Anthropic\Messages\TextDelta; + +$stream = $client->messages->createStream( + model: 'claude-opus-4-8', + maxTokens: 64000, + messages: [ + ['role' => 'user', 'content' => 'Write a haiku'], + ], +); + +foreach ($stream as $event) { + if ($event instanceof RawContentBlockDeltaEvent && $event->delta instanceof TextDelta) { + echo $event->delta->text; + } +} +``` + +--- + +## Tool Use + +### Tool Runner (Beta) + +**Beta:** The PHP SDK provides a tool runner via `$client->beta->messages->toolRunner()`. Define tools with `BetaRunnableTool` — a definition array plus a `run` closure: + +```php +use Anthropic\Lib\Tools\BetaRunnableTool; + +$weatherTool = new BetaRunnableTool( + definition: [ + 'name' => 'get_weather', + 'description' => 'Get the current weather for a location.', + 'input_schema' => [ + 'type' => 'object', + 'properties' => [ + 'location' => ['type' => 'string', 'description' => 'City and state'], + ], + 'required' => ['location'], + ], + ], + run: function (array $input): string { + return "The weather in {$input['location']} is sunny and 72°F."; + }, +); + +$runner = $client->beta->messages->toolRunner( + maxTokens: 16000, + messages: [['role' => 'user', 'content' => 'What is the weather in Paris?']], + model: 'claude-opus-4-8', + tools: [$weatherTool], +); + +foreach ($runner as $message) { + foreach ($message->content as $block) { + if ($block->type === 'text') { + echo $block->text; + } + } +} +``` + +### Manual Loop + +Tools are passed as arrays. **The SDK uses camelCase keys** (`inputSchema`, `toolUseID`, `stopReason`) and auto-maps to the API's snake_case on the wire — since v0.5.0. See [shared tool use concepts](../shared/tool-use-concepts.md) for the loop pattern. + +```php +use Anthropic\Messages\ToolUseBlock; + +$tools = [ + [ + 'name' => 'get_weather', + 'description' => 'Get the current weather in a given location', + 'inputSchema' => [ // camelCase, not input_schema + 'type' => 'object', + 'properties' => [ + 'location' => ['type' => 'string', 'description' => 'City and state'], + ], + 'required' => ['location'], + ], + ], +]; + +$messages = [['role' => 'user', 'content' => 'What is the weather in SF?']]; + +$response = $client->messages->create( + model: 'claude-opus-4-8', + maxTokens: 16000, + tools: $tools, + messages: $messages, +); + +while ($response->stopReason === 'tool_use') { // camelCase property + $toolResults = []; + foreach ($response->content as $block) { + if ($block instanceof ToolUseBlock) { + // $block->name : string — tool name to dispatch on + // $block->input : array — parsed JSON input + // $block->id : string — pass back as toolUseID + $result = executeYourTool($block->name, $block->input); + $toolResults[] = [ + 'type' => 'tool_result', + 'toolUseID' => $block->id, // camelCase, not tool_use_id + 'content' => $result, + ]; + } + } + + // Append assistant turn + user turn with tool results + $messages[] = ['role' => 'assistant', 'content' => $response->content]; + $messages[] = ['role' => 'user', 'content' => $toolResults]; + + $response = $client->messages->create( + model: 'claude-opus-4-8', + maxTokens: 16000, + tools: $tools, + messages: $messages, + ); +} + +// Final text response +foreach ($response->content as $block) { + if ($block->type === 'text') { + echo $block->text; + } +} +``` + +`$block->type === 'tool_use'` also works; `instanceof ToolUseBlock` narrows for PHPStan. + + +--- + +## Extended Thinking + +**Adaptive thinking is the recommended mode for Claude 4.6+ models.** Claude decides dynamically when and how much to think. + +```php +use Anthropic\Messages\ThinkingBlock; + +$message = $client->messages->create( + model: 'claude-opus-4-8', + maxTokens: 16000, + thinking: ['type' => 'adaptive'], + messages: [ + ['role' => 'user', 'content' => 'Solve: 27 * 453'], + ], +); + +// ThinkingBlock(s) precede TextBlock in content +foreach ($message->content as $block) { + if ($block instanceof ThinkingBlock) { + echo "Thinking:\n{$block->thinking}\n\n"; + // $block->signature is an opaque string — preserve verbatim if + // passing thinking blocks back in multi-turn conversations + } elseif ($block->type === 'text') { + echo "Answer: {$block->text}\n"; + } +} +``` + +> **Deprecated:** `['type' => 'enabled', 'budgetTokens' => N]` (fixed-budget extended thinking) still works on Claude 4.6 but is deprecated. Use adaptive thinking above. + +`$block->type === 'thinking'` also works for the check; `instanceof` narrows for PHPStan. + +--- + +## Prompt Caching + +`system:` takes an array of text blocks; set `cacheControl` on the last block. Array-shape syntax (camelCase keys) is idiomatic. For placement patterns and the silent-invalidator audit checklist, see `shared/prompt-caching.md`. + +```php +$message = $client->messages->create( + model: 'claude-opus-4-8', + maxTokens: 16000, + system: [ + ['type' => 'text', 'text' => $longSystemPrompt, 'cacheControl' => ['type' => 'ephemeral']], + ], + messages: [['role' => 'user', 'content' => 'Summarize the key points']], +); +``` + +For 1-hour TTL: `'cacheControl' => ['type' => 'ephemeral', 'ttl' => '1h']`. There's also a top-level `cacheControl:` on `messages->create(...)` that auto-places on the last cacheable block. + +Verify hits via `$message->usage->cacheCreationInputTokens` / `$message->usage->cacheReadInputTokens`. + +--- + +## Structured Outputs + +### Using StructuredOutputModel (Recommended) + +Define a PHP class implementing `StructuredOutputModel` and pass it as `outputConfig`: + +```php +use Anthropic\Lib\Contracts\StructuredOutputModel; +use Anthropic\Lib\Concerns\StructuredOutputModelTrait; +use Anthropic\Lib\Attributes\Constrained; + +class Person implements StructuredOutputModel +{ + use StructuredOutputModelTrait; + + #[Constrained(description: 'Full name')] + public string $name; + + public int $age; + + public ?string $email = null; // nullable = optional field +} + +$message = $client->messages->create( + model: 'claude-opus-4-8', + maxTokens: 16000, + messages: [['role' => 'user', 'content' => 'Generate a profile for Alice, age 30']], + outputConfig: ['format' => Person::class], +); + +$person = $message->parsedOutput(); // Person instance +echo $person->name; +``` + +Types are inferred from PHP type hints. Use `#[Constrained(description: '...')]` to add descriptions. Nullable properties (`?string`) become optional fields. + +### Raw Schema + +```php +$message = $client->messages->create( + model: 'claude-opus-4-8', + maxTokens: 16000, + messages: [['role' => 'user', 'content' => 'Extract: John (john@co.com), Enterprise plan']], + outputConfig: [ + 'format' => [ + 'type' => 'json_schema', + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'email' => ['type' => 'string'], + 'plan' => ['type' => 'string'], + ], + 'required' => ['name', 'email', 'plan'], + 'additionalProperties' => false, + ], + ], + ], +); + +// First text block contains valid JSON +foreach ($message->content as $block) { + if ($block->type === 'text') { + $data = json_decode($block->text, true); + break; + } +} +``` + +--- + +## Beta Features & Server-Side Tools + +**`betas:` is NOT a param on `$client->messages->create()`** — it only exists on the beta namespace. Use it for features that need an explicit opt-in header: + +```php +use Anthropic\Beta\Messages\BetaRequestMCPServerURLDefinition; + +$response = $client->beta->messages->create( + model: 'claude-opus-4-8', + maxTokens: 16000, + mcpServers: [ + BetaRequestMCPServerURLDefinition::with( + name: 'my-server', + url: 'https://example.com/mcp', + ), + ], + betas: ['mcp-client-2025-11-20'], // only valid on ->beta->messages + messages: [['role' => 'user', 'content' => 'Use the MCP tools']], +); +``` + +**Server-side tools** (bash, web_search, text_editor, code_execution) are GA and work on both paths — `Anthropic\Messages\ToolBash20250124` / `WebSearchTool20260209` / `ToolTextEditor20250728` / `CodeExecutionTool20260120` for non-beta, `Anthropic\Beta\Messages\BetaToolBash20250124` / `BetaWebSearchTool20260209` / `BetaToolTextEditor20250728` / `BetaCodeExecutionTool20260120` for beta. No `betas:` header needed for these. + +--- + +## Stop Details + +When `stopReason` is `'refusal'`, the response includes structured `stopDetails`: + +```php +if ($message->stopReason === 'refusal' && $message->stopDetails !== null) { + echo "Category: " . $message->stopDetails->category . "\n"; // "cyber" | "bio" | null + echo "Explanation: " . $message->stopDetails->explanation . "\n"; +} +``` + +--- + +## Error Type + +`APIStatusException` exposes a `->type` property for programmatic error classification: + +```php +try { + $client->messages->create(...); +} catch (\Anthropic\Core\Exceptions\APIStatusException $e) { + echo $e->type?->value; // "rate_limit_error", "overloaded_error", etc. +} +``` diff --git a/.agents/skills/claude-api/php/managed-agents/README.md b/.agents/skills/claude-api/php/managed-agents/README.md new file mode 100644 index 00000000..9d4d96f4 --- /dev/null +++ b/.agents/skills/claude-api/php/managed-agents/README.md @@ -0,0 +1,435 @@ +# Managed Agents — PHP + +> **Bindings not shown here:** This README covers the most common managed-agents flows for PHP. If you need a class, method, namespace, field, or behavior that isn't shown, WebFetch the PHP SDK repo **or the relevant docs page** from `shared/live-sources.md` rather than guess. Do not extrapolate from cURL shapes or another language's SDK. + +> **Agents are persistent — create once, reference by ID.** Store the agent ID returned by `$client->beta->agents->create` and pass it to every subsequent `->sessions->create`; do not call `agents->create` in the request path. The Anthropic CLI is one convenient way to create agents and environments from version-controlled YAML — its URL is in `shared/live-sources.md`. The examples below show in-code creation for completeness; in production the create call belongs in setup, not in the request path. + +## Installation + +```bash +composer require "anthropic-ai/sdk" +``` + +## Client Initialization + +```php +use Anthropic\Client; + +// Default (uses ANTHROPIC_API_KEY env var) +$client = new Client(); + +// Explicit API key +$client = new Client(apiKey: 'your-api-key'); +``` + +--- + +## Create an Environment + +```php +$environment = $client->beta->environments->create( + name: 'my-dev-env', + config: ['type' => 'cloud', 'networking' => ['type' => 'unrestricted']], +); +echo "Environment ID: {$environment->id}\n"; // env_... +``` + +--- + +## Create an Agent (required first step) + +> ⚠️ **There is no inline agent config.** `model`/`system`/`tools` live on the agent object, not the session. Always start with `$client->beta->agents->create()` — the session takes either `agent: $agent->id` or the typed `BetaManagedAgentsAgentParams::with(type: 'agent', id: $agent->id, version: $agent->version)`. + +### Minimal + +```php +use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolset20260401Params; + +// 1. Create the agent (reusable, versioned) +$agent = $client->beta->agents->create( + name: 'Coding Assistant', + model: 'claude-opus-4-8', + system: 'You are a helpful coding assistant.', + tools: [ + BetaManagedAgentsAgentToolset20260401Params::with( + type: 'agent_toolset_20260401', + ), + ], +); + +// 2. Start a session +$session = $client->beta->sessions->create( + agent: ['type' => 'agent', 'id' => $agent->id, 'version' => $agent->version], + environmentID: $environment->id, + title: 'Quickstart session', +); +echo "Session ID: {$session->id}\n"; +``` + +### Updating an Agent + +Updates create new versions; the agent object is immutable per version. + +```php +$updatedAgent = $client->beta->agents->update( + $agent->id, + version: $agent->version, + system: 'You are a helpful coding agent. Always write tests.', +); +echo "New version: {$updatedAgent->version}\n"; + +// List all versions +foreach ($client->beta->agents->versions->list($agent->id)->pagingEachItem() as $version) { + echo "Version {$version->version}: {$version->updatedAt->format(DateTimeInterface::ATOM)}\n"; +} + +// Archive the agent +$archived = $client->beta->agents->archive($agent->id); +echo "Archived at: {$archived->archivedAt->format(DateTimeInterface::ATOM)}\n"; +``` + +--- + +## Send a User Message + +```php +$client->beta->sessions->events->send( + $session->id, + events: [ + [ + 'type' => 'user.message', + 'content' => [['type' => 'text', 'text' => 'Review the auth module']], + ], + ], +); +``` + +> 💡 **Stream-first:** Open the stream *before* (or concurrently with) sending the message. The stream only delivers events that occur after it opens — stream-after-send means early events arrive buffered in one batch. See [Steering Patterns](../../shared/managed-agents-events.md#steering-patterns). + +--- + +## Stream Events (SSE) + +> ℹ️ **Streaming transporter:** PHP's default buffered PSR-18 client never returns for the open-ended session event stream. Use a streaming Guzzle transporter for `streamStream()` calls — other calls keep the default client. + +```php +$streamingClient = new GuzzleHttp\Client(['stream' => true]); + +// Open the stream first, then send the user message +$stream = $client->beta->sessions->events->streamStream( + $session->id, + requestOptions: ['transporter' => $streamingClient], +); +$client->beta->sessions->events->send( + $session->id, + events: [ + [ + 'type' => 'user.message', + 'content' => [['type' => 'text', 'text' => 'Summarize the repo README']], + ], + ], +); + +foreach ($stream as $event) { + match ($event->type) { + 'agent.message' => array_walk( + $event->content, + static fn($block) => $block->type === 'text' ? print($block->text) : null, + ), + 'agent.tool_use' => print("\n[Using tool: {$event->name}]\n"), + 'session.error' => printf("\n[Error: %s]", $event->error?->message ?? 'unknown'), + default => null, + }; + if ($event->type === 'session.status_idle' || $event->type === 'session.error') { + break; + } +} +$stream->close(); +``` + +### Reconnecting and Tailing + +When reconnecting mid-session, list past events first to dedupe, then tail live events: + +```php +$stream = $client->beta->sessions->events->streamStream( + $session->id, + requestOptions: ['transporter' => $streamingClient], +); + +// Stream is open and buffering. List history before tailing live. +$seenEventIds = []; +foreach ($client->beta->sessions->events->list($session->id)->pagingEachItem() as $event) { + $seenEventIds[$event->id] = true; +} + +// Tail live events, skipping anything already seen +foreach ($stream as $event) { + if (isset($seenEventIds[$event->id])) { + continue; + } + $seenEventIds[$event->id] = true; + match ($event->type) { + 'agent.message' => array_walk( + $event->content, + static fn($block) => $block->type === 'text' ? print($block->text) : null, + ), + default => null, + }; + if ($event->type === 'session.status_idle') { + break; + } +} +$stream->close(); +``` + +--- + +## Provide Custom Tool Result + +> ℹ️ The PHP managed-agents bindings for `user.custom_tool_result` are not yet documented in this skill or in the apps source examples. Refer to `shared/managed-agents-events.md` for the wire format and the `anthropic-ai/sdk` PHP repository for the corresponding params. + +--- + +## Poll Events + +```php +foreach ($client->beta->sessions->events->list($session->id)->pagingEachItem() as $event) { + echo "{$event->type}: {$event->id}\n"; +} +``` + +--- + +## Upload a File + +> ℹ️ **PHP file upload:** The PHP SDK's beta managed-agents file upload binding is not shown in the apps source examples; the canonical PHP example uses raw cURL against `POST /v1/files`. If your codebase prefers the SDK, WebFetch the `anthropic-ai/sdk` PHP repository for the latest binding before writing code. + +```php +use Anthropic\Beta\Sessions\BetaManagedAgentsFileResourceParams; + +// Raw cURL upload (canonical example from the apps source) +$csvPath = 'data.csv'; +$ch = curl_init('https://api.anthropic.com/v1/files'); +curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => [ + 'x-api-key: ' . getenv('ANTHROPIC_API_KEY'), + 'anthropic-version: 2023-06-01', + 'anthropic-beta: files-api-2025-04-14', + ], + CURLOPT_POSTFIELDS => ['file' => new CURLFile($csvPath, 'text/csv', 'data.csv')], +]); +$file = json_decode(curl_exec($ch)); +echo "File ID: {$file->id}\n"; + +// Mount in a session +$session = $client->beta->sessions->create( + agent: $agent->id, + environmentID: $environment->id, + resources: [ + BetaManagedAgentsFileResourceParams::with( + type: 'file', + fileID: $file->id, + mountPath: '/workspace/data.csv', + ), + ], +); +``` + +### Add and Manage Resources on an Existing Session + +```php +// Attach an additional file to an open session +$resource = $client->beta->sessions->resources->add( + $session->id, + type: 'file', + fileID: $file->id, +); +echo "{$resource->id}\n"; // "sesrsc_01ABC..." + +// List resources on the session +$listed = $client->beta->sessions->resources->list($session->id); +foreach ($listed->data as $entry) { + echo "{$entry->id} {$entry->type}\n"; +} + +// Detach a resource +$client->beta->sessions->resources->delete($resource->id, sessionID: $session->id); +``` + +--- + +## List and Download Session Files + +> ℹ️ Listing and downloading files an agent wrote during a session is not yet documented for PHP in this skill or in the apps source examples. See `shared/managed-agents-events.md` and the `anthropic-ai/sdk` PHP repository for the file list/download bindings. + +--- + +## Session Management + +```php +// List environments +$environments = $client->beta->environments->list(); + +// Retrieve a specific environment +$env = $client->beta->environments->retrieve($environment->id); + +// Archive an environment (read-only, existing sessions continue) +$client->beta->environments->archive($environment->id); + +// Delete an environment (only if no sessions reference it) +$client->beta->environments->delete($environment->id); + +// Delete a session +$client->beta->sessions->delete($session->id); +``` + +--- + +## MCP Server Integration + +```php +use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolset20260401Params; +use Anthropic\Beta\Agents\BetaManagedAgentsMCPToolsetParams; +use Anthropic\Beta\Agents\BetaManagedAgentsUrlmcpServerParams; +use Anthropic\Beta\Sessions\BetaManagedAgentsAgentParams; + +// Agent declares MCP server (no auth here — auth goes in a vault) +$agent = $client->beta->agents->create( + name: 'GitHub Assistant', + model: 'claude-opus-4-8', + mcpServers: [ + BetaManagedAgentsUrlmcpServerParams::with( + type: 'url', + name: 'github', + url: 'https://api.githubcopilot.com/mcp/', + ), + ], + tools: [ + BetaManagedAgentsAgentToolset20260401Params::with(type: 'agent_toolset_20260401'), + BetaManagedAgentsMCPToolsetParams::with( + type: 'mcp_toolset', + mcpServerName: 'github', + ), + ], +); + +// Session attaches vault(s) containing credentials for those MCP server URLs +$session = $client->beta->sessions->create( + agent: BetaManagedAgentsAgentParams::with( + type: 'agent', + id: $agent->id, + version: $agent->version, + ), + environmentID: $environment->id, + vaultIDs: [$vault->id], +); +``` + +See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials. + +--- + +## Vaults + +```php +// Create a vault +$vault = $client->beta->vaults->create( + displayName: 'Alice', + metadata: ['external_user_id' => 'usr_abc123'], +); +echo $vault->id . "\n"; // "vlt_01ABC..." + +// Add an OAuth credential +$credential = $client->beta->vaults->credentials->create( + vaultID: $vault->id, + displayName: "Alice's Slack", + auth: [ + 'type' => 'mcp_oauth', + 'mcp_server_url' => 'https://mcp.slack.com/mcp', + 'access_token' => 'xoxp-...', + 'expires_at' => '2026-04-15T00:00:00Z', + 'refresh' => [ + 'token_endpoint' => 'https://slack.com/api/oauth.v2.access', + 'client_id' => '1234567890.0987654321', + 'scope' => 'channels:read chat:write', + 'refresh_token' => 'xoxe-1-...', + 'token_endpoint_auth' => [ + 'type' => 'client_secret_post', + 'client_secret' => 'abc123...', + ], + ], + ], +); + +// Rotate the credential (e.g., after a token refresh) +$client->beta->vaults->credentials->update( + $credential->id, + vaultID: $vault->id, + auth: [ + 'type' => 'mcp_oauth', + 'access_token' => 'xoxp-new-...', + 'expires_at' => '2026-05-15T00:00:00Z', + 'refresh' => ['refresh_token' => 'xoxe-1-new-...'], + ], +); + +// Archive a vault +$client->beta->vaults->archive($vault->id); +``` + +--- + +## GitHub Repository Integration + +Mount a GitHub repository as a session resource (a vault holds the GitHub MCP credential): + +```php +$session = $client->beta->sessions->create( + agent: $agent->id, + environmentID: $environment->id, + vaultIDs: [$vault->id], + resources: [ + [ + 'type' => 'github_repository', + 'url' => 'https://github.com/org/repo', + 'mountPath' => '/workspace/repo', + 'authorizationToken' => 'ghp_your_github_token', + ], + ], +); +``` + +Multiple repositories on the same session: + +```php +$resources = [ + [ + 'type' => 'github_repository', + 'url' => 'https://github.com/org/frontend', + 'mountPath' => '/workspace/frontend', + 'authorizationToken' => 'ghp_your_github_token', + ], + [ + 'type' => 'github_repository', + 'url' => 'https://github.com/org/backend', + 'mountPath' => '/workspace/backend', + 'authorizationToken' => 'ghp_your_github_token', + ], +]; +``` + +Rotating a repository's authorization token: + +```php +$listed = $client->beta->sessions->resources->list($session->id); +$repoResourceId = $listed->data[0]->id; + +$client->beta->sessions->resources->update( + $repoResourceId, + sessionID: $session->id, + authorizationToken: 'ghp_your_new_github_token', +); +``` diff --git a/.agents/skills/claude-api/python/claude-api/README.md b/.agents/skills/claude-api/python/claude-api/README.md new file mode 100644 index 00000000..e505ae9e --- /dev/null +++ b/.agents/skills/claude-api/python/claude-api/README.md @@ -0,0 +1,536 @@ +# Claude API — Python + +## Installation + +```bash +pip install anthropic +``` + +## Client Initialization + +```python +import anthropic + +# Default — resolves credentials from the environment: +# ANTHROPIC_API_KEY, or ANTHROPIC_AUTH_TOKEN, or an `ant auth login` profile. +# Prefer this for local dev; don't hardcode a key. +client = anthropic.Anthropic() + +# Explicit API key (only when you must inject a specific key) +client = anthropic.Anthropic(api_key="your-api-key") + +# Async client +async_client = anthropic.AsyncAnthropic() +``` + +--- + +## Client Configuration + +### Per-request overrides + +Use `with_options()` to override client settings for a single call without mutating the client: + +```python +client.with_options(timeout=5.0, max_retries=5).messages.create( + model="claude-opus-4-8", + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], +) +``` + +### Timeouts + +Default request timeout is 10 minutes. Pass a float (seconds) or an `httpx.Timeout` for granular control. On timeout the SDK raises `anthropic.APITimeoutError` (and retries per `max_retries`). + +```python +import httpx + +client = anthropic.Anthropic(timeout=20.0) +client = anthropic.Anthropic( + timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), +) +``` + +### Retries + +The SDK auto-retries connection errors, 408, 409, 429, and ≥500 with exponential backoff (default 2 retries). Set `max_retries` on the client or via `with_options()`; `max_retries=0` disables. + +### Async performance (aiohttp backend) + +For high-concurrency async workloads, install `anthropic[aiohttp]` and pass `DefaultAioHttpClient` instead of the default httpx backend: + +```python +from anthropic import AsyncAnthropic, DefaultAioHttpClient + +async with AsyncAnthropic(http_client=DefaultAioHttpClient()) as client: + ... +``` + +### Custom HTTP client (proxy, base URL) + +Use `DefaultHttpxClient` / `DefaultAsyncHttpxClient` — not raw `httpx.Client` — so the SDK's default timeouts and connection limits are preserved: + +```python +from anthropic import Anthropic, DefaultHttpxClient + +client = Anthropic( + base_url="http://my.test.server.example.com:8083", # or ANTHROPIC_BASE_URL env var + http_client=DefaultHttpxClient(proxy="http://my.test.proxy.example.com"), +) +``` + +### Logging + +Set `ANTHROPIC_LOG=debug` (or `info`) to enable SDK logging via the standard `logging` module. + +--- + +## Basic Message Request + +```python +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[ + {"role": "user", "content": "What is the capital of France?"} + ] +) +# response.content is a list of content block objects (TextBlock, ThinkingBlock, +# ToolUseBlock, ...). Check .type before accessing .text. +for block in response.content: + if block.type == "text": + print(block.text) +``` + +--- + +## System Prompts + +```python +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + system="You are a helpful coding assistant. Always provide examples in Python.", + messages=[{"role": "user", "content": "How do I read a JSON file?"}] +) +``` + +### Mid-conversation system messages (beta, model-gated) + +For operator instructions that arrive mid-conversation (mode switches, injected state), append `{"role": "system", ...}` to `messages` instead of editing top-level `system` — this preserves the cached prefix and carries operator authority. Must follow a user message; cannot be `messages[0]`. Unsupported models return a 400 (`role 'system' is not supported on this model`). See `shared/prompt-caching.md` for when to use this vs. top-level `system`. + +```python +response = client.messages.create( + model=MODEL_ID, # must support mid-conversation system messages + max_tokens=16000, + system=[{"type": "text", "text": STABLE_SYSTEM, "cache_control": {"type": "ephemeral"}}], + messages=history + [ + {"role": "user", "content": user_message}, + {"role": "system", "content": "Terse mode enabled — keep responses under 40 words."}, + ], + extra_headers={"anthropic-beta": "mid-conversation-system-2026-04-07"}, +) +``` + +--- + +## Vision (Images) + +### Base64 + +```python +import base64 + +with open("image.png", "rb") as f: + image_data = base64.standard_b64encode(f.read()).decode("utf-8") + +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{ + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": image_data + } + }, + {"type": "text", "text": "What's in this image?"} + ] + }] +) +``` + +### URL + +```python +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{ + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "url", + "url": "https://example.com/image.png" + } + }, + {"type": "text", "text": "Describe this image"} + ] + }] +) +``` + +--- + +## Prompt Caching + +Cache large context to reduce costs (up to 90% savings). **Caching is a prefix match** — any byte change anywhere in the prefix invalidates everything after it. For placement patterns, architectural guidance (frozen system prompt, deterministic tool order, where to put volatile content), and the silent-invalidator audit checklist, read `shared/prompt-caching.md`. + +### Automatic Caching (Recommended) + +Use top-level `cache_control` to automatically cache the last cacheable block in the request — no need to annotate individual content blocks: + +```python +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + cache_control={"type": "ephemeral"}, # auto-caches the last cacheable block + system="You are an expert on this large document...", + messages=[{"role": "user", "content": "Summarize the key points"}] +) +``` + +### Manual Cache Control + +For fine-grained control, add `cache_control` to specific content blocks: + +```python +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + system=[{ + "type": "text", + "text": "You are an expert on this large document...", + "cache_control": {"type": "ephemeral"} # default TTL is 5 minutes + }], + messages=[{"role": "user", "content": "Summarize the key points"}] +) + +# With explicit TTL (time-to-live) +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + system=[{ + "type": "text", + "text": "You are an expert on this large document...", + "cache_control": {"type": "ephemeral", "ttl": "1h"} # 1 hour TTL + }], + messages=[{"role": "user", "content": "Summarize the key points"}] +) +``` + +### Verifying Cache Hits + +```python +print(response.usage.cache_creation_input_tokens) # tokens written to cache (~1.25x cost) +print(response.usage.cache_read_input_tokens) # tokens served from cache (~0.1x cost) +print(response.usage.input_tokens) # uncached tokens (full cost) +``` + +If `cache_read_input_tokens` is zero across repeated identical-prefix requests, a silent invalidator is at work — `datetime.now()` or a UUID in the system prompt, unsorted `json.dumps()`, or a varying tool set. See `shared/prompt-caching.md` for the full audit table. + +--- + +## Extended Thinking + +> **Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6:** Use adaptive thinking. `budget_tokens` is removed on Opus 4.8 and 4.7 (400 if sent); deprecated on Opus 4.6 and Sonnet 4.6. +> **Older models:** Use `thinking: {type: "enabled", budget_tokens: N}` (must be < `max_tokens`, min 1024). + +```python +# Opus 4.8 / 4.7 / 4.6: adaptive thinking (recommended) +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + thinking={"type": "adaptive"}, + output_config={"effort": "high"}, # low | medium | high | max + messages=[{"role": "user", "content": "Solve this step by step..."}] +) + +# Access thinking and response +for block in response.content: + if block.type == "thinking": + print(f"Thinking: {block.thinking}") + elif block.type == "text": + print(f"Response: {block.text}") +``` + +--- + +## Error Handling + +```python +import anthropic + +try: + response = client.messages.create(...) +except anthropic.BadRequestError as e: + print(f"Bad request: {e.message}") +except anthropic.AuthenticationError: + print("Invalid API key") +except anthropic.PermissionDeniedError: + print("API key lacks required permissions") +except anthropic.NotFoundError: + print("Invalid model or endpoint") +except anthropic.RateLimitError as e: + retry_after = int(e.response.headers.get("retry-after", "60")) + print(f"Rate limited. Retry after {retry_after}s.") +except anthropic.APIStatusError as e: + if e.status_code >= 500: + print(f"Server error ({e.status_code}). Retry later.") + else: + print(f"API error: {e.message}") +except anthropic.APIConnectionError: + print("Network error. Check internet connection.") +``` + +--- + +## Response Helpers + +Every response object exposes `_request_id` (populated from the `request-id` header) — log it when reporting failures to Anthropic. Despite the underscore prefix, this property is public. + +```python +message = client.messages.create(...) +print(message._request_id) # req_018EeWyXxfu5pfWkrYcMdjWG +print(message.to_json()) # serialize the Pydantic model +print(message.to_dict()) # plain dict +``` + +To access raw headers or other response metadata, use `.with_raw_response`: + +```python +raw = client.messages.with_raw_response.create( + model="claude-opus-4-8", + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], +) +print(raw.headers.get("request-id")) +message = raw.parse() # the Message object messages.create() would have returned +``` + +--- + +## Multi-Turn Conversations + +The API is stateless — send the full conversation history each time. + +```python +class ConversationManager: + """Manage multi-turn conversations with the Claude API.""" + + def __init__(self, client: anthropic.Anthropic, model: str, system: str = None): + self.client = client + self.model = model + self.system = system + self.messages = [] + + def send(self, user_message: str, **kwargs) -> str: + """Send a message and get a response.""" + self.messages.append({"role": "user", "content": user_message}) + + response = self.client.messages.create( + model=self.model, + max_tokens=kwargs.get("max_tokens", 16000), + system=self.system, + messages=self.messages, + **kwargs + ) + + assistant_message = next( + (b.text for b in response.content if b.type == "text"), "" + ) + self.messages.append({"role": "assistant", "content": assistant_message}) + + return assistant_message + +# Usage +conversation = ConversationManager( + client=anthropic.Anthropic(), + model="claude-opus-4-8", + system="You are a helpful assistant." +) + +response1 = conversation.send("My name is Alice.") +response2 = conversation.send("What's my name?") # Claude remembers "Alice" +``` + +**Rules:** + +- Consecutive same-role messages are allowed — the API combines them into a single turn +- First message must be `user` +- `role: "system"` messages are allowed mid-conversation under the `mid-conversation-system-2026-04-07` beta on supporting models — see § Mid-conversation system messages above + +--- + +### Compaction (long conversations) + +> **Beta, Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6.** When conversations approach the 200K context window, compaction automatically summarizes earlier context server-side. The API returns a `compaction` block; you must pass it back on subsequent requests — append `response.content`, not just the text. + +```python +import anthropic + +client = anthropic.Anthropic() +messages = [] + +def chat(user_message: str) -> str: + messages.append({"role": "user", "content": user_message}) + + response = client.beta.messages.create( + betas=["compact-2026-01-12"], + model="claude-opus-4-8", + max_tokens=16000, + messages=messages, + context_management={ + "edits": [{"type": "compact_20260112"}] + } + ) + + # Append full content — compaction blocks must be preserved + messages.append({"role": "assistant", "content": response.content}) + + return next(block.text for block in response.content if block.type == "text") + +# Compaction triggers automatically when context grows large +print(chat("Help me build a Python web scraper")) +print(chat("Add support for JavaScript-rendered pages")) +print(chat("Now add rate limiting and error handling")) +``` + +--- + +## Stop Reasons + +The `stop_reason` field in the response indicates why the model stopped generating: + +| Value | Meaning | +|-------|---------| +| `end_turn` | Claude finished its response naturally | +| `max_tokens` | Hit the `max_tokens` limit — increase it or use streaming | +| `stop_sequence` | Hit a custom stop sequence | +| `tool_use` | Claude wants to call a tool — execute it and continue | +| `pause_turn` | Model paused and can be resumed (agentic flows) | +| `refusal` | Claude refused for safety reasons — check `stop_details` | + +### Structured Stop Details + +When `stop_reason` is `"refusal"`, the response includes a `stop_details` object with structured information about the refusal: + +```python +if response.stop_reason == "refusal" and response.stop_details: + print(f"Category: {response.stop_details.category}") # "cyber" | "bio" | None + print(f"Explanation: {response.stop_details.explanation}") +``` + +--- + +## Cost Optimization Strategies + +### 1. Use Prompt Caching for Repeated Context + +```python +# Automatic caching (simplest — caches the last cacheable block) +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + cache_control={"type": "ephemeral"}, + system=large_document_text, # e.g., 50KB of context + messages=[{"role": "user", "content": "Summarize the key points"}] +) + +# First request: full cost +# Subsequent requests: ~90% cheaper for cached portion +``` + +### 2. Choose the Right Model + +```python +# Default to Opus for most tasks +response = client.messages.create( + model="claude-opus-4-8", # $5.00/$25.00 per 1M tokens + max_tokens=16000, + messages=[{"role": "user", "content": "Explain quantum computing"}] +) + +# Use Sonnet for high-volume production workloads +standard_response = client.messages.create( + model="claude-sonnet-4-6", # $3.00/$15.00 per 1M tokens + max_tokens=16000, + messages=[{"role": "user", "content": "Summarize this document"}] +) + +# Use Haiku only for simple, speed-critical tasks +simple_response = client.messages.create( + model="claude-haiku-4-5", # $1.00/$5.00 per 1M tokens + max_tokens=256, + messages=[{"role": "user", "content": "Classify this as positive or negative"}] +) +``` + +### 3. Use Token Counting Before Requests + +```python +count_response = client.messages.count_tokens( + model="claude-opus-4-8", + messages=messages, + system=system +) + +estimated_input_cost = count_response.input_tokens * 0.000005 # $5/1M tokens +print(f"Estimated input cost: ${estimated_input_cost:.4f}") +``` + +--- + +## Retry with Exponential Backoff + +> **Note:** The Anthropic SDK automatically retries rate limit (429) and server errors (5xx) with exponential backoff. You can configure this with `max_retries` (default: 2). Only implement custom retry logic if you need behavior beyond what the SDK provides. + +```python +import time +import random +import anthropic + +def call_with_retry( + client: anthropic.Anthropic, + max_retries: int = 5, + base_delay: float = 1.0, + max_delay: float = 60.0, + **kwargs +): + """Call the API with exponential backoff retry.""" + last_exception = None + + for attempt in range(max_retries): + try: + return client.messages.create(**kwargs) + except anthropic.RateLimitError as e: + last_exception = e + except anthropic.APIStatusError as e: + if e.status_code >= 500: + last_exception = e + else: + raise # Client errors (4xx except 429) should not be retried + + delay = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay) + print(f"Retry {attempt + 1}/{max_retries} after {delay:.1f}s") + time.sleep(delay) + + raise last_exception +``` diff --git a/.agents/skills/claude-api/python/claude-api/batches.md b/.agents/skills/claude-api/python/claude-api/batches.md new file mode 100644 index 00000000..2ca54ea5 --- /dev/null +++ b/.agents/skills/claude-api/python/claude-api/batches.md @@ -0,0 +1,198 @@ +# Message Batches API — Python + +The Batches API (`POST /v1/messages/batches`) processes Messages API requests asynchronously at 50% of standard prices. + +## Key Facts + +- Up to 100,000 requests or 256 MB per batch +- Most batches complete within 1 hour; maximum 24 hours +- Results available for 29 days after creation +- 50% cost reduction on all token usage +- All Messages API features supported (vision, tools, caching, etc.) + +--- + +## Create a Batch + +```python +import anthropic +from anthropic.types.message_create_params import MessageCreateParamsNonStreaming +from anthropic.types.messages.batch_create_params import Request + +client = anthropic.Anthropic() + +message_batch = client.messages.batches.create( + requests=[ + Request( + custom_id="request-1", + params=MessageCreateParamsNonStreaming( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{"role": "user", "content": "Summarize climate change impacts"}] + ) + ), + Request( + custom_id="request-2", + params=MessageCreateParamsNonStreaming( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{"role": "user", "content": "Explain quantum computing basics"}] + ) + ), + ] +) + +print(f"Batch ID: {message_batch.id}") +print(f"Status: {message_batch.processing_status}") +``` + +--- + +## Poll for Completion + +```python +import time + +while True: + batch = client.messages.batches.retrieve(message_batch.id) + if batch.processing_status == "ended": + break + print(f"Status: {batch.processing_status}, processing: {batch.request_counts.processing}") + time.sleep(60) + +print("Batch complete!") +print(f"Succeeded: {batch.request_counts.succeeded}") +print(f"Errored: {batch.request_counts.errored}") +``` + +--- + +## Retrieve Results + +> **Note:** Examples below use `match/case` syntax, requiring Python 3.10+. For earlier versions, use `if/elif` chains instead. + +```python +for result in client.messages.batches.results(message_batch.id): + match result.result.type: + case "succeeded": + msg = result.result.message + text = next((b.text for b in msg.content if b.type == "text"), "") + print(f"[{result.custom_id}] {text[:100]}") + case "errored": + if result.result.error.type == "invalid_request": + print(f"[{result.custom_id}] Validation error - fix request and retry") + else: + print(f"[{result.custom_id}] Server error - safe to retry") + case "canceled": + print(f"[{result.custom_id}] Canceled") + case "expired": + print(f"[{result.custom_id}] Expired - resubmit") +``` + +--- + +## Cancel a Batch + +```python +cancelled = client.messages.batches.cancel(message_batch.id) +print(f"Status: {cancelled.processing_status}") # "canceling" +``` + +--- + +## List Batches (auto-pagination) + +Iterating the return value of any `list()` call auto-paginates across all pages — do not index into `.data` if you want the full set: + +```python +for batch in client.messages.batches.list(limit=20): + print(batch.id, batch.processing_status) +``` + +For manual control, use `first_page.has_next_page()` / `first_page.get_next_page()` / `first_page.next_page_info()`; `first_page.data` holds the current page's items and `first_page.last_id` is the cursor. + +--- + +## Batch with Prompt Caching + +```python +shared_system = [ + {"type": "text", "text": "You are a literary analyst."}, + { + "type": "text", + "text": large_document_text, # Shared across all requests + "cache_control": {"type": "ephemeral"} + } +] + +message_batch = client.messages.batches.create( + requests=[ + Request( + custom_id=f"analysis-{i}", + params=MessageCreateParamsNonStreaming( + model="claude-opus-4-8", + max_tokens=16000, + system=shared_system, + messages=[{"role": "user", "content": question}] + ) + ) + for i, question in enumerate(questions) + ] +) +``` + +--- + +## Full End-to-End Example + +```python +import anthropic +import time +from anthropic.types.message_create_params import MessageCreateParamsNonStreaming +from anthropic.types.messages.batch_create_params import Request + +client = anthropic.Anthropic() + +# 1. Prepare requests +items_to_classify = [ + "The product quality is excellent!", + "Terrible customer service, never again.", + "It's okay, nothing special.", +] + +requests = [ + Request( + custom_id=f"classify-{i}", + params=MessageCreateParamsNonStreaming( + model="claude-haiku-4-5", + max_tokens=50, + messages=[{ + "role": "user", + "content": f"Classify as positive/negative/neutral (one word): {text}" + }] + ) + ) + for i, text in enumerate(items_to_classify) +] + +# 2. Create batch +batch = client.messages.batches.create(requests=requests) +print(f"Created batch: {batch.id}") + +# 3. Wait for completion +while True: + batch = client.messages.batches.retrieve(batch.id) + if batch.processing_status == "ended": + break + time.sleep(10) + +# 4. Collect results +results = {} +for result in client.messages.batches.results(batch.id): + if result.result.type == "succeeded": + msg = result.result.message + results[result.custom_id] = next((b.text for b in msg.content if b.type == "text"), "") + +for custom_id, classification in sorted(results.items()): + print(f"{custom_id}: {classification}") +``` diff --git a/.agents/skills/claude-api/python/claude-api/files-api.md b/.agents/skills/claude-api/python/claude-api/files-api.md new file mode 100644 index 00000000..14f5bb14 --- /dev/null +++ b/.agents/skills/claude-api/python/claude-api/files-api.md @@ -0,0 +1,170 @@ +# Files API — Python + +The Files API uploads files for use in Messages API requests. Reference files via `file_id` in content blocks, avoiding re-uploads across multiple API calls. + +**Beta:** Pass `betas=["files-api-2025-04-14"]` in your API calls (the SDK sets the required header automatically). + +## Key Facts + +- Maximum file size: 500 MB +- Total storage: 100 GB per organization +- Files persist until deleted +- File operations (upload, list, delete) are free; content used in messages is billed as input tokens +- Not available on Amazon Bedrock or Google Vertex AI + +--- + +## Upload a File + +The `file` argument accepts a `(filename, content, content_type)` tuple, a `pathlib.Path` (or any `PathLike` — read for you, async-safe with `AsyncAnthropic`), or an open binary file object. + +```python +import anthropic +from pathlib import Path + +client = anthropic.Anthropic() + +uploaded = client.beta.files.upload( + file=("report.pdf", open("report.pdf", "rb"), "application/pdf"), +) +# or: client.beta.files.upload(file=Path("report.pdf")) +print(f"File ID: {uploaded.id}") +print(f"Size: {uploaded.size_bytes} bytes") +``` + +--- + +## Use a File in Messages + +### PDF / Text Document + +```python +response = client.beta.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{ + "role": "user", + "content": [ + {"type": "text", "text": "Summarize the key findings in this report."}, + { + "type": "document", + "source": {"type": "file", "file_id": uploaded.id}, + "title": "Q4 Report", # optional + "citations": {"enabled": True} # optional, enables citations + } + ] + }], + betas=["files-api-2025-04-14"], +) +for block in response.content: + if block.type == "text": + print(block.text) +``` + +### Image + +```python +image_file = client.beta.files.upload( + file=("photo.png", open("photo.png", "rb"), "image/png"), +) + +response = client.beta.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{ + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + { + "type": "image", + "source": {"type": "file", "file_id": image_file.id} + } + ] + }], + betas=["files-api-2025-04-14"], +) +``` + +--- + +## Manage Files + +### List Files + +Iterate the list result directly — the SDK auto-paginates across all pages. Only use `.data` if you want the first page only. + +```python +for f in client.beta.files.list(): + print(f"{f.id}: {f.filename} ({f.size_bytes} bytes)") +``` + +### Get File Metadata + +```python +file_info = client.beta.files.retrieve_metadata("file_011CNha8iCJcU1wXNR6q4V8w") +print(f"Filename: {file_info.filename}") +print(f"MIME type: {file_info.mime_type}") +``` + +### Delete a File + +```python +client.beta.files.delete("file_011CNha8iCJcU1wXNR6q4V8w") +``` + +### Download a File + +Only files created by the code execution tool or skills can be downloaded (not user-uploaded files). + +```python +file_content = client.beta.files.download("file_011CNha8iCJcU1wXNR6q4V8w") +file_content.write_to_file("output.txt") +``` + +--- + +## Full End-to-End Example + +Upload a document once, ask multiple questions about it: + +```python +import anthropic + +client = anthropic.Anthropic() + +# 1. Upload once +uploaded = client.beta.files.upload( + file=("contract.pdf", open("contract.pdf", "rb"), "application/pdf"), +) +print(f"Uploaded: {uploaded.id}") + +# 2. Ask multiple questions using the same file_id +questions = [ + "What are the key terms and conditions?", + "What is the termination clause?", + "Summarize the payment schedule.", +] + +for question in questions: + response = client.beta.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{ + "role": "user", + "content": [ + {"type": "text", "text": question}, + { + "type": "document", + "source": {"type": "file", "file_id": uploaded.id} + } + ] + }], + betas=["files-api-2025-04-14"], + ) + print(f"\nQ: {question}") + text = next((b.text for b in response.content if b.type == "text"), "") + print(f"A: {text[:200]}") + +# 3. Clean up when done +client.beta.files.delete(uploaded.id) +``` diff --git a/.agents/skills/claude-api/python/claude-api/streaming.md b/.agents/skills/claude-api/python/claude-api/streaming.md new file mode 100644 index 00000000..6788bcd8 --- /dev/null +++ b/.agents/skills/claude-api/python/claude-api/streaming.md @@ -0,0 +1,179 @@ +# Streaming — Python + +## Quick Start + +```python +with client.messages.stream( + model="claude-opus-4-8", + max_tokens=64000, + messages=[{"role": "user", "content": "Write a story"}] +) as stream: + for text in stream.text_stream: + print(text, end="", flush=True) +``` + +### Async + +```python +async with async_client.messages.stream( + model="claude-opus-4-8", + max_tokens=64000, + messages=[{"role": "user", "content": "Write a story"}] +) as stream: + async for text in stream.text_stream: + print(text, end="", flush=True) +``` + +### Low-level: `stream=True` + +`messages.stream()` (above) is the recommended helper — it accumulates state and exposes `text_stream` / `get_final_message()`. If you only need the raw event iterator and want lower memory use, pass `stream=True` to `messages.create()` instead: + +```python +for event in client.messages.create( + model="claude-opus-4-8", + max_tokens=64000, + messages=[{"role": "user", "content": "Write a story"}], + stream=True, +): + print(event.type) +``` + +No final-message accumulation is done for you in this form. + +--- + +## Handling Different Content Types + +Claude may return text, thinking blocks, or tool use. Handle each appropriately: + +> **Opus 4.8 / Opus 4.7 / Opus 4.6:** Use `thinking: {type: "adaptive"}`. On older models, use `thinking: {type: "enabled", budget_tokens: N}` instead. + +```python +with client.messages.stream( + model="claude-opus-4-8", + max_tokens=64000, + thinking={"type": "adaptive"}, + messages=[{"role": "user", "content": "Analyze this problem"}] +) as stream: + for event in stream: + if event.type == "content_block_start": + if event.content_block.type == "thinking": + print("\n[Thinking...]") + elif event.content_block.type == "text": + print("\n[Response:]") + + elif event.type == "content_block_delta": + if event.delta.type == "thinking_delta": + print(event.delta.thinking, end="", flush=True) + elif event.delta.type == "text_delta": + print(event.delta.text, end="", flush=True) +``` + +--- + +## Streaming with Tool Use + +The Python tool runner currently returns complete messages. Use streaming for individual API calls within a manual loop if you need per-token streaming with tools: + +```python +with client.messages.stream( + model="claude-opus-4-8", + max_tokens=64000, + tools=tools, + messages=messages +) as stream: + for text in stream.text_stream: + print(text, end="", flush=True) + + response = stream.get_final_message() + # Continue with tool execution if response.stop_reason == "tool_use" +``` + +--- + +## Getting the Final Message + +```python +with client.messages.stream( + model="claude-opus-4-8", + max_tokens=64000, + messages=[{"role": "user", "content": "Hello"}] +) as stream: + for text in stream.text_stream: + print(text, end="", flush=True) + + # Get full message after streaming + final_message = stream.get_final_message() + print(f"\n\nTokens used: {final_message.usage.output_tokens}") +``` + +--- + +## Streaming with Progress Updates + +```python +def stream_with_progress(client, **kwargs): + """Stream a response with progress updates.""" + total_tokens = 0 + content_parts = [] + + with client.messages.stream(**kwargs) as stream: + for event in stream: + if event.type == "content_block_delta": + if event.delta.type == "text_delta": + text = event.delta.text + content_parts.append(text) + print(text, end="", flush=True) + + elif event.type == "message_delta": + if event.usage and event.usage.output_tokens is not None: + total_tokens = event.usage.output_tokens + + final_message = stream.get_final_message() + + print(f"\n\n[Tokens used: {total_tokens}]") + return "".join(content_parts) +``` + +--- + +## Error Handling in Streams + +```python +try: + with client.messages.stream( + model="claude-opus-4-8", + max_tokens=64000, + messages=[{"role": "user", "content": "Write a story"}] + ) as stream: + for text in stream.text_stream: + print(text, end="", flush=True) +except anthropic.APIConnectionError: + print("\nConnection lost. Please retry.") +except anthropic.RateLimitError: + print("\nRate limited. Please wait and retry.") +except anthropic.APIStatusError as e: + print(f"\nAPI error: {e.status_code}") +``` + +--- + +## Stream Event Types + +| Event Type | Description | When it fires | +| --------------------- | --------------------------- | --------------------------------- | +| `message_start` | Contains message metadata | Once at the beginning | +| `content_block_start` | New content block beginning | When a text/tool_use block starts | +| `content_block_delta` | Incremental content update | For each token/chunk | +| `content_block_stop` | Content block complete | When a block finishes | +| `message_delta` | Message-level updates | Contains `stop_reason`, usage | +| `message_stop` | Message complete | Once at the end | + +## Best Practices + +1. **Always flush output** — Use `flush=True` to show tokens immediately +2. **Handle partial responses** — If the stream is interrupted, you may have incomplete content +3. **Track token usage** — The `message_delta` event contains usage information +4. **Use timeouts** — Set appropriate timeouts for your application +5. **Default to streaming** — Use `.get_final_message()` to get the complete response even when streaming, giving you timeout protection without needing to handle individual events +6. **Large `max_tokens` without streaming raises `ValueError`** — The SDK refuses non-streaming requests it estimates will exceed ~10 minutes (idle connections drop). Pass `stream=True` / use `messages.stream()`, or explicitly override `timeout`, to suppress the guard. diff --git a/.agents/skills/claude-api/python/claude-api/tool-use.md b/.agents/skills/claude-api/python/claude-api/tool-use.md new file mode 100644 index 00000000..5ac51789 --- /dev/null +++ b/.agents/skills/claude-api/python/claude-api/tool-use.md @@ -0,0 +1,590 @@ +# Tool Use — Python + +For conceptual overview (tool definitions, tool choice, tips), see [shared/tool-use-concepts.md](../../shared/tool-use-concepts.md). + +## Tool Runner (Recommended) + +**Beta:** The tool runner is in beta in the Python SDK. + +Use the `@beta_tool` decorator to define tools as typed functions, then pass them to `client.beta.messages.tool_runner()`: + +```python +import anthropic +from anthropic import beta_tool + +client = anthropic.Anthropic() + +@beta_tool +def get_weather(location: str, unit: str = "celsius") -> str: + """Get current weather for a location. + + Args: + location: City and state, e.g., San Francisco, CA. + unit: Temperature unit, either "celsius" or "fahrenheit". + """ + # Your implementation here + return f"72°F and sunny in {location}" + +# The tool runner handles the agentic loop automatically +runner = client.beta.messages.tool_runner( + model="claude-opus-4-8", + max_tokens=16000, + tools=[get_weather], + messages=[{"role": "user", "content": "What's the weather in Paris?"}], +) + +# Each iteration yields a BetaMessage; iteration stops when Claude is done +for message in runner: + print(message) +``` + +For async usage, use `@beta_async_tool` with `async def` functions. + +**Key benefits of the tool runner:** + +- No manual loop — the SDK handles calling tools and feeding results back +- Type-safe tool inputs via decorators +- Tool schemas are generated automatically from function signatures +- Iteration stops automatically when Claude has no more tool calls + +--- + +## MCP Tool Conversion Helpers + +**Beta.** Convert [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) tools, prompts, and resources to Anthropic API types for use with the tool runner. Requires `pip install anthropic[mcp]` (Python 3.10+). + +> **Note:** The Claude API also supports an `mcp_servers` parameter that lets Claude connect directly to remote MCP servers. Use these helpers instead when you need local MCP servers, prompts, resources, or more control over the MCP connection. + +### MCP Tools with Tool Runner + +```python +from anthropic import AsyncAnthropic +from anthropic.lib.tools.mcp import async_mcp_tool +from mcp import ClientSession +from mcp.client.stdio import stdio_client, StdioServerParameters + +client = AsyncAnthropic() + +async with stdio_client(StdioServerParameters(command="mcp-server")) as (read, write): + async with ClientSession(read, write) as mcp_client: + await mcp_client.initialize() + + tools_result = await mcp_client.list_tools() + # tool_runner is sync — returns the runner, not a coroutine + runner = client.beta.messages.tool_runner( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{"role": "user", "content": "Use the available tools"}], + tools=[async_mcp_tool(t, mcp_client) for t in tools_result.tools], + ) + async for message in runner: + print(message) +``` + +For sync usage, use `mcp_tool` instead of `async_mcp_tool`. + +### MCP Prompts + +```python +from anthropic.lib.tools.mcp import mcp_message + +prompt = await mcp_client.get_prompt(name="my-prompt") +response = await client.beta.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[mcp_message(m) for m in prompt.messages], +) +``` + +### MCP Resources as Content + +```python +from anthropic.lib.tools.mcp import mcp_resource_to_content + +resource = await mcp_client.read_resource(uri="file:///path/to/doc.txt") +response = await client.beta.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{ + "role": "user", + "content": [ + mcp_resource_to_content(resource), + {"type": "text", "text": "Summarize this document"}, + ], + }], +) +``` + +### Upload MCP Resources as Files + +```python +from anthropic.lib.tools.mcp import mcp_resource_to_file + +resource = await mcp_client.read_resource(uri="file:///path/to/data.json") +uploaded = await client.beta.files.upload(file=mcp_resource_to_file(resource)) +``` + +Conversion functions raise `UnsupportedMCPValueError` if an MCP value cannot be converted (e.g., unsupported content types like audio, unsupported MIME types). + +--- + +## Manual Agentic Loop + +Use this when you need fine-grained control over the loop (e.g., custom logging, conditional tool execution, human-in-the-loop approval): + +```python +import anthropic + +client = anthropic.Anthropic() +tools = [...] # Your tool definitions +messages = [{"role": "user", "content": user_input}] + +# Agentic loop: keep going until Claude stops calling tools +while True: + response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + tools=tools, + messages=messages + ) + + # If Claude is done (no more tool calls), break + if response.stop_reason == "end_turn": + break + + # Server-side tool hit iteration limit; re-send to continue + if response.stop_reason == "pause_turn": + messages = [ + {"role": "user", "content": user_input}, + {"role": "assistant", "content": response.content}, + ] + continue + + # Extract tool use blocks from the response + tool_use_blocks = [b for b in response.content if b.type == "tool_use"] + + # Append assistant's response (including tool_use blocks) + messages.append({"role": "assistant", "content": response.content}) + + # Execute each tool and collect results + tool_results = [] + for tool in tool_use_blocks: + result = execute_tool(tool.name, tool.input) # Your implementation + tool_results.append({ + "type": "tool_result", + "tool_use_id": tool.id, # Must match the tool_use block's id + "content": result + }) + + # Append tool results as a user message + messages.append({"role": "user", "content": tool_results}) + +# Final response text +final_text = next(b.text for b in response.content if b.type == "text") +``` + +--- + +## Handling Tool Results + +```python +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + tools=tools, + messages=[{"role": "user", "content": "What's the weather in Paris?"}] +) + +for block in response.content: + if block.type == "tool_use": + tool_name = block.name + tool_input = block.input + tool_use_id = block.id + + result = execute_tool(tool_name, tool_input) + + followup = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + tools=tools, + messages=[ + {"role": "user", "content": "What's the weather in Paris?"}, + {"role": "assistant", "content": response.content}, + { + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": tool_use_id, + "content": result + }] + } + ] + ) +``` + +--- + +## Multiple Tool Calls + +```python +tool_results = [] + +for block in response.content: + if block.type == "tool_use": + result = execute_tool(block.name, block.input) + tool_results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": result + }) + +# Send all results back at once +if tool_results: + followup = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + tools=tools, + messages=[ + *previous_messages, + {"role": "assistant", "content": response.content}, + {"role": "user", "content": tool_results} + ] + ) +``` + +--- + +## Error Handling in Tool Results + +```python +tool_result = { + "type": "tool_result", + "tool_use_id": tool_use_id, + "content": "Error: Location 'xyz' not found. Please provide a valid city name.", + "is_error": True +} +``` + +--- + +## Tool Choice + +```python +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + tools=tools, + tool_choice={"type": "tool", "name": "get_weather"}, # Force specific tool + messages=[{"role": "user", "content": "What's the weather in Paris?"}] +) +``` + +--- + +## Code Execution + +### Basic Usage + +```python +import anthropic + +client = anthropic.Anthropic() + +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{ + "role": "user", + "content": "Calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" + }], + tools=[{ + "type": "code_execution_20260120", + "name": "code_execution" + }] +) + +for block in response.content: + if block.type == "text": + print(block.text) + elif block.type == "bash_code_execution_tool_result": + print(f"stdout: {block.content.stdout}") +``` + +### Upload Files for Analysis + +```python +# 1. Upload a file +uploaded = client.beta.files.upload(file=open("sales_data.csv", "rb")) + +# 2. Pass to code execution via container_upload block +# Code execution is GA; Files API is still beta (pass via extra_headers) +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + extra_headers={"anthropic-beta": "files-api-2025-04-14"}, + messages=[{ + "role": "user", + "content": [ + {"type": "text", "text": "Analyze this sales data. Show trends and create a visualization."}, + {"type": "container_upload", "file_id": uploaded.id} + ] + }], + tools=[{"type": "code_execution_20260120", "name": "code_execution"}] +) +``` + +### Retrieve Generated Files + +```python +import os + +OUTPUT_DIR = "./claude_outputs" +os.makedirs(OUTPUT_DIR, exist_ok=True) + +for block in response.content: + if block.type == "bash_code_execution_tool_result": + result = block.content + if result.type == "bash_code_execution_result" and result.content: + for file_ref in result.content: + if file_ref.type == "bash_code_execution_output": + metadata = client.beta.files.retrieve_metadata(file_ref.file_id) + file_content = client.beta.files.download(file_ref.file_id) + # Use basename to prevent path traversal; validate result + safe_name = os.path.basename(metadata.filename) + if not safe_name or safe_name in (".", ".."): + print(f"Skipping invalid filename: {metadata.filename}") + continue + output_path = os.path.join(OUTPUT_DIR, safe_name) + file_content.write_to_file(output_path) + print(f"Saved: {output_path}") +``` + +### Container Reuse + +```python +# First request: set up environment +response1 = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{"role": "user", "content": "Install tabulate and create data.json with sample data"}], + tools=[{"type": "code_execution_20260120", "name": "code_execution"}] +) + +# Get container ID from response +container_id = response1.container.id + +# Second request: reuse the same container +response2 = client.messages.create( + container=container_id, + model="claude-opus-4-8", + max_tokens=16000, + messages=[{"role": "user", "content": "Read data.json and display as a formatted table"}], + tools=[{"type": "code_execution_20260120", "name": "code_execution"}] +) +``` + +### Response Structure + +```python +for block in response.content: + if block.type == "text": + print(block.text) # Claude's explanation + elif block.type == "server_tool_use": + print(f"Running: {block.name} - {block.input}") # What Claude is doing + elif block.type == "bash_code_execution_tool_result": + result = block.content + if result.type == "bash_code_execution_result": + if result.return_code == 0: + print(f"Output: {result.stdout}") + else: + print(f"Error: {result.stderr}") + else: + print(f"Tool error: {result.error_code}") + elif block.type == "text_editor_code_execution_tool_result": + print(f"File operation: {block.content}") +``` + +--- + +## Memory Tool + +### Basic Usage + +```python +import anthropic + +client = anthropic.Anthropic() + +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{"role": "user", "content": "Remember that my preferred language is Python."}], + tools=[{"type": "memory_20250818", "name": "memory"}], +) +``` + +### SDK Memory Helper + +Subclass `BetaAbstractMemoryTool`: + +```python +from anthropic.lib.tools import BetaAbstractMemoryTool + +class MyMemoryTool(BetaAbstractMemoryTool): + def view(self, command): ... + def create(self, command): ... + def str_replace(self, command): ... + def insert(self, command): ... + def delete(self, command): ... + def rename(self, command): ... + +memory = MyMemoryTool() + +# Use with tool runner +runner = client.beta.messages.tool_runner( + model="claude-opus-4-8", + max_tokens=16000, + tools=[memory], + messages=[{"role": "user", "content": "Remember my preferences"}], +) + +for message in runner: + print(message) +``` + +For full implementation examples, use WebFetch: + +- `https://github.com/anthropics/anthropic-sdk-python/blob/main/examples/memory/basic.py` + +--- + +## Structured Outputs + +### JSON Outputs (Pydantic — Recommended) + +```python +from pydantic import BaseModel +from typing import List +import anthropic + +class ContactInfo(BaseModel): + name: str + email: str + plan: str + interests: List[str] + demo_requested: bool + +client = anthropic.Anthropic() + +response = client.messages.parse( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{ + "role": "user", + "content": "Extract: Jane Doe (jane@co.com) wants Enterprise, interested in API and SDKs, wants a demo." + }], + output_format=ContactInfo, +) + +# response.parsed_output is a validated ContactInfo instance +contact = response.parsed_output +print(contact.name) # "Jane Doe" +print(contact.interests) # ["API", "SDKs"] +``` + +### Raw Schema + +```python +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{ + "role": "user", + "content": "Extract info: John Smith (john@example.com) wants the Enterprise plan." + }], + output_config={ + "format": { + "type": "json_schema", + "schema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"}, + "plan": {"type": "string"}, + "demo_requested": {"type": "boolean"} + }, + "required": ["name", "email", "plan", "demo_requested"], + "additionalProperties": False + } + } + } +) + +import json +# output_config.format guarantees the first block is text with valid JSON +text = next(b.text for b in response.content if b.type == "text") +data = json.loads(text) +``` + +### Strict Tool Use + +```python +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{"role": "user", "content": "Book a flight to Tokyo for 2 passengers on March 15"}], + tools=[{ + "name": "book_flight", + "description": "Book a flight to a destination", + "strict": True, + "input_schema": { + "type": "object", + "properties": { + "destination": {"type": "string"}, + "date": {"type": "string", "format": "date"}, + "passengers": {"type": "integer", "enum": [1, 2, 3, 4, 5, 6, 7, 8]} + }, + "required": ["destination", "date", "passengers"], + "additionalProperties": False + } + }] +) +``` + +### Using Both Together + +```python +response = client.messages.create( + model="claude-opus-4-8", + max_tokens=16000, + messages=[{"role": "user", "content": "Plan a trip to Paris next month"}], + output_config={ + "format": { + "type": "json_schema", + "schema": { + "type": "object", + "properties": { + "summary": {"type": "string"}, + "next_steps": {"type": "array", "items": {"type": "string"}} + }, + "required": ["summary", "next_steps"], + "additionalProperties": False + } + } + }, + tools=[{ + "name": "search_flights", + "description": "Search for available flights", + "strict": True, + "input_schema": { + "type": "object", + "properties": { + "destination": {"type": "string"}, + "date": {"type": "string", "format": "date"} + }, + "required": ["destination", "date"], + "additionalProperties": False + } + }] +) +``` diff --git a/.agents/skills/claude-api/python/managed-agents/README.md b/.agents/skills/claude-api/python/managed-agents/README.md new file mode 100644 index 00000000..ae1aec26 --- /dev/null +++ b/.agents/skills/claude-api/python/managed-agents/README.md @@ -0,0 +1,334 @@ +# Managed Agents — Python + +> **Bindings not shown here:** This README covers the most common managed-agents flows for Python. If you need a class, method, namespace, field, or behavior that isn't shown, WebFetch the Python SDK repo **or the relevant docs page** from `shared/live-sources.md` rather than guess. Do not extrapolate from cURL shapes or another language's SDK. + +> **Agents are persistent — create once, reference by ID.** Store the agent ID returned by `agents.create` and pass it to every subsequent `sessions.create`; do not call `agents.create` in the request path. The Anthropic CLI is one convenient way to create agents and environments from version-controlled YAML — its URL is in `shared/live-sources.md`. The examples below show in-code creation for completeness; in production the create call belongs in setup, not in the request path. + +## Installation + +```bash +pip install anthropic +``` + +## Client Initialization + +```python +import anthropic + +# Default — resolves credentials from the environment: +# ANTHROPIC_API_KEY, or ANTHROPIC_AUTH_TOKEN, or an `ant auth login` profile. +# Prefer this for local dev; don't hardcode a key. +client = anthropic.Anthropic() + +# Explicit API key (only when you must inject a specific key) +client = anthropic.Anthropic(api_key="your-api-key") +``` + +--- + +## Create an Environment + +```python +environment = client.beta.environments.create( + name="my-dev-env", + config={ + "type": "cloud", + "networking": {"type": "unrestricted"}, + }, +) +print(environment.id) # env_... +``` + +--- + +## Create an Agent (required first step) + +> ⚠️ **There is no inline agent config.** `model`/`system`/`tools` live on the agent object, not the session. Always start with `agents.create()` — the session only takes `agent={"type": "agent", "id": agent.id}`. + +### Minimal + +```python +# 1. Create the agent (reusable, versioned) +agent = client.beta.agents.create( + name="Coding Assistant", + model="claude-opus-4-8", + tools=[{"type": "agent_toolset_20260401", "default_config": {"enabled": True}}], +) + +# 2. Start a session +session = client.beta.sessions.create( + agent={"type": "agent", "id": agent.id, "version": agent.version}, + environment_id=environment.id, +) +print(session.id, session.status) +``` + +### With system prompt and custom tools + +```python +import os + +agent = client.beta.agents.create( + name="Code Reviewer", + model="claude-opus-4-8", + system="You are a senior code reviewer.", + tools=[ + {"type": "agent_toolset_20260401"}, + { + "type": "custom", + "name": "run_tests", + "description": "Run the test suite", + "input_schema": { + "type": "object", + "properties": { + "test_path": {"type": "string", "description": "Path to test file"} + }, + "required": ["test_path"], + }, + }, + ], +) + +session = client.beta.sessions.create( + agent={"type": "agent", "id": agent.id, "version": agent.version}, + environment_id=environment.id, + title="Code review session", + resources=[ + { + "type": "github_repository", + "url": "https://github.com/owner/repo", + "mount_path": "/workspace/repo", + "authorization_token": os.environ["GITHUB_TOKEN"], + "branch": "main", + } + ], +) +``` + +--- + +## Send a User Message + +```python +client.beta.sessions.events.send( + session_id=session.id, + events=[ + { + "type": "user.message", + "content": [{"type": "text", "text": "Review the auth module"}], + } + ], +) +``` + +> 💡 **Stream-first:** Open the stream *before* (or concurrently with) sending the message. The stream only delivers events that occur after it opens — stream-after-send means early events arrive buffered in one batch. See [Steering Patterns](../../shared/managed-agents-events.md#steering-patterns). + +--- + +## Stream Events (SSE) + +```python +import json + +# Stream-first: open stream, then send while stream is live +with client.beta.sessions.events.stream( + session_id=session.id, +) as stream: + client.beta.sessions.events.send( + session_id=session.id, + events=[{"type": "user.message", "content": [{"type": "text", "text": "..."}]}], + ) + for event in stream: + ... # process events + +# Standalone stream iteration: +with client.beta.sessions.events.stream( + session_id=session.id, +) as stream: + for event in stream: + if event.type == "agent.message": + for block in event.content: + if block.type == "text": + print(block.text, end="", flush=True) + elif event.type == "agent.custom_tool_use": + # Custom tool invocation — session is now idle + print(f"\nCustom tool call: {event.name}") + print(f"Input: {json.dumps(event.input)}") + # Send result back (see below) + elif event.type == "session.status_idle": + print("\n--- Agent idle ---") + elif event.type == "session.status_terminated": + print("\n--- Session terminated ---") + break +``` + +--- + +## Provide Custom Tool Result + +```python +client.beta.sessions.events.send( + session_id=session.id, + events=[ + { + "type": "user.custom_tool_result", + "custom_tool_use_id": "sevt_abc123", + "content": [{"type": "text", "text": "All 42 tests passed."}], + } + ], +) +``` + +--- + +## Poll Events + +```python +events = client.beta.sessions.events.list( + session_id=session.id, +) +for event in events.data: + print(f"{event.type}: {event.id}") +``` + +> ⚠️ **Prefer the SDK over raw `requests`/`httpx`.** If you hand-roll a poll loop, don't assume `timeout=(5, 60)` or `httpx.Timeout(120)` caps total call duration — both are **per-chunk** read timeouts (reset on every byte), so a trickling response can block forever. For a hard wall-clock deadline, track `time.monotonic()` at the loop level and bail explicitly, or wrap with `asyncio.wait_for()`. See [Receiving Events](../../shared/managed-agents-events.md#receiving-events). + +--- + +## Full Streaming Loop with Custom Tools + +```python +import json + + +def run_custom_tool(tool_name: str, tool_input: dict) -> str: + """Execute a custom tool and return the result.""" + if tool_name == "run_tests": + # Your tool implementation here + return "All tests passed." + return f"Unknown tool: {tool_name}" + + +def run_session(client, session_id: str): + """Stream events and handle custom tool calls.""" + while True: + with client.beta.sessions.events.stream( + session_id=session_id, + ) as stream: + tool_calls = [] + for event in stream: + if event.type == "agent.message": + for block in event.content: + if block.type == "text": + print(block.text, end="", flush=True) + elif event.type == "agent.custom_tool_use": + tool_calls.append(event) + elif event.type == "session.status_idle": + break + elif event.type == "session.status_terminated": + return + + if not tool_calls: + break + + # Process custom tool calls + results = [] + for call in tool_calls: + result = run_custom_tool(call.name, call.input) + results.append({ + "type": "user.custom_tool_result", + "custom_tool_use_id": call.id, + "content": [{"type": "text", "text": result}], + }) + + client.beta.sessions.events.send( + session_id=session_id, + events=results, + ) +``` + +--- + +## Upload a File + +```python +with open("data.csv", "rb") as f: + file = client.beta.files.upload( + file=f, + ) + +# Use in a session +session = client.beta.sessions.create( + agent={"type": "agent", "id": agent.id, "version": agent.version}, + environment_id=environment.id, + resources=[{"type": "file", "file_id": file.id, "mount_path": "/workspace/data.csv"}], +) +``` + +--- + +## List and Download Session Files + +List files the agent wrote to `/mnt/session/outputs/` during a session, then download them. + +```python +# List files associated with a session +files = client.beta.files.list( + scope_id=session.id, + betas=["managed-agents-2026-04-01"], +) +for f in files.data: + print(f.filename, f.size_bytes) + # Download each file and save to disk + file_content = client.beta.files.download(f.id) + file_content.write_to_file(f.filename) +``` + +> 💡 There's a brief indexing lag (~1–3s) between `session.status_idle` and output files appearing in `files.list`. Retry once or twice if the list is empty. + +--- + +## Session Management + +```python +# Get session details +session = client.beta.sessions.retrieve(session_id="sesn_011CZxAbc123Def456") +print(session.status, session.usage) + +# List sessions +sessions = client.beta.sessions.list() + +# Delete a session +client.beta.sessions.delete(session_id="sesn_011CZxAbc123Def456") + +# Archive a session +client.beta.sessions.archive(session_id="sesn_011CZxAbc123Def456") +``` + +--- + +## MCP Server Integration + +```python +# Agent declares MCP server (no auth here — auth goes in a vault) +agent = client.beta.agents.create( + name="MCP Agent", + model="claude-opus-4-8", + mcp_servers=[ + {"type": "url", "name": "my-tools", "url": "https://my-mcp-server.example.com/sse"}, + ], + tools=[ + {"type": "agent_toolset_20260401", "default_config": {"enabled": True}}, + {"type": "mcp_toolset", "mcp_server_name": "my-tools"}, + ], +) + +# Session attaches vault(s) containing credentials for those MCP server URLs +session = client.beta.sessions.create( + agent=agent.id, + environment_id=environment.id, + vault_ids=[vault.id], +) +``` + +See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials. diff --git a/.agents/skills/claude-api/ruby/claude-api.md b/.agents/skills/claude-api/ruby/claude-api.md new file mode 100644 index 00000000..e67df290 --- /dev/null +++ b/.agents/skills/claude-api/ruby/claude-api.md @@ -0,0 +1,140 @@ +# Claude API — Ruby + +> **Note:** The Ruby SDK supports the Claude API. A tool runner is available in beta via `client.beta.messages.tool_runner()`. Agent SDK is not yet available for Ruby. + +## Installation + +```bash +gem install anthropic +``` + +## Client Initialization + +```ruby +require "anthropic" + +# Default (uses ANTHROPIC_API_KEY env var) +client = Anthropic::Client.new + +# Explicit API key +client = Anthropic::Client.new(api_key: "your-api-key") +``` + +--- + +## Basic Message Request + +```ruby +message = client.messages.create( + model: :"claude-opus-4-8", + max_tokens: 16000, + messages: [ + { role: "user", content: "What is the capital of France?" } + ] +) +# content is an array of polymorphic block objects (TextBlock, ThinkingBlock, +# ToolUseBlock, ...). .type is a Symbol — compare with :text, not "text". +# .text raises NoMethodError on non-TextBlock entries. +message.content.each do |block| + puts block.text if block.type == :text +end +``` + +--- + +## Streaming + +```ruby +stream = client.messages.stream( + model: :"claude-opus-4-8", + max_tokens: 64000, + messages: [{ role: "user", content: "Write a haiku" }] +) + +stream.text.each { |text| print(text) } +``` + +--- + +## Tool Use + +The Ruby SDK supports tool use via raw JSON schema definitions and also provides a beta tool runner for automatic tool execution. + +### Tool Runner (Beta) + +```ruby +class GetWeatherInput < Anthropic::BaseModel + required :location, String, doc: "City and state, e.g. San Francisco, CA" +end + +class GetWeather < Anthropic::BaseTool + doc "Get the current weather for a location" + + input_schema GetWeatherInput + + def call(input) + "The weather in #{input.location} is sunny and 72°F." + end +end + +client.beta.messages.tool_runner( + model: :"claude-opus-4-8", + max_tokens: 16000, + tools: [GetWeather.new], + messages: [{ role: "user", content: "What's the weather in San Francisco?" }] +).each_message do |message| + puts message.content +end +``` + +### Manual Loop + +See the [shared tool use concepts](../shared/tool-use-concepts.md) for the tool definition format and agentic loop pattern. + +--- + +## Prompt Caching + +`system_:` (trailing underscore — avoids shadowing `Kernel#system`) takes an array of text blocks; set `cache_control` on the last block. Plain hashes work via the `OrHash` type alias. For placement patterns and the silent-invalidator audit checklist, see `shared/prompt-caching.md`. + +```ruby +message = client.messages.create( + model: :"claude-opus-4-8", + max_tokens: 16000, + system_: [ + { type: "text", text: long_system_prompt, cache_control: { type: "ephemeral" } } + ], + messages: [{ role: "user", content: "Summarize the key points" }] +) +``` + +For 1-hour TTL: `cache_control: { type: "ephemeral", ttl: "1h" }`. There's also a top-level `cache_control:` on `messages.create` that auto-places on the last cacheable block. + +Verify hits via `message.usage.cache_creation_input_tokens` / `message.usage.cache_read_input_tokens`. + +--- + +## Stop Details + +When `stop_reason` is `:refusal`, the response includes structured `stop_details`: + +```ruby +if message.stop_reason == :refusal && message.stop_details + puts "Category: #{message.stop_details.category}" # :cyber, :bio, or nil + puts "Explanation: #{message.stop_details.explanation}" +end +``` + +--- + +## Error Type + +`APIStatusError` exposes a `.type` field for programmatic error classification: + +```ruby +begin + client.messages.create(...) +rescue Anthropic::APIStatusError => e + puts e.type # :rate_limit_error, :overloaded_error, etc. +end +``` diff --git a/.agents/skills/claude-api/ruby/managed-agents/README.md b/.agents/skills/claude-api/ruby/managed-agents/README.md new file mode 100644 index 00000000..e9633813 --- /dev/null +++ b/.agents/skills/claude-api/ruby/managed-agents/README.md @@ -0,0 +1,389 @@ +# Managed Agents — Ruby + +> **Bindings not shown here:** This README covers the most common managed-agents flows for Ruby. If you need a class, method, namespace, field, or behavior that isn't shown, WebFetch the Ruby SDK repo **or the relevant docs page** from `shared/live-sources.md` rather than guess. Do not extrapolate from cURL shapes or another language's SDK. + +> **Agents are persistent — create once, reference by ID.** Store the agent ID returned by `client.beta.agents.create` and pass it to every subsequent `client.beta.sessions.create`; do not call `agents.create` in the request path. The Anthropic CLI is one convenient way to create agents and environments from version-controlled YAML — its URL is in `shared/live-sources.md`. The examples below show in-code creation for completeness; in production the create call belongs in setup, not in the request path. + +## Installation + +```bash +gem install anthropic +``` + +## Client Initialization + +```ruby +require "anthropic" + +# Default (uses ANTHROPIC_API_KEY env var) +client = Anthropic::Client.new + +# Explicit API key +client = Anthropic::Client.new(api_key: "your-api-key") +``` + +> ⚠️ **Trailing underscores:** The Ruby SDK uses `system_:` and `send_(` (trailing underscore) to avoid shadowing `Kernel#system` and `Kernel#send`. Use these forms throughout managed-agents code. + +--- + +## Create an Environment + +```ruby +environment = client.beta.environments.create( + name: "my-dev-env", + config: { + type: "cloud", + networking: {type: "unrestricted"} + } +) +puts "Environment ID: #{environment.id}" # env_... +``` + +--- + +## Create an Agent (required first step) + +> ⚠️ **There is no inline agent config.** `model`/`system_`/`tools` live on the agent object, not the session. Always start with `client.beta.agents.create()` — the session takes either `agent: agent.id` or the typed hash form `agent: {type: "agent", id: agent.id, version: agent.version}`. + +### Minimal + +```ruby +# 1. Create the agent (reusable, versioned) +agent = client.beta.agents.create( + name: "Coding Assistant", + model: :"claude-opus-4-8", + system_: "You are a helpful coding assistant.", + tools: [{type: "agent_toolset_20260401"}] +) + +# 2. Start a session +session = client.beta.sessions.create( + agent: {type: "agent", id: agent.id, version: agent.version}, + environment_id: environment.id, + title: "Quickstart session" +) +puts "Session ID: #{session.id}" +``` + +### Updating an Agent + +Updates create new versions; the agent object is immutable per version. + +```ruby +updated_agent = client.beta.agents.update( + agent.id, + version: agent.version, + system_: "You are a helpful coding agent. Always write tests." +) +puts "New version: #{updated_agent.version}" + +# List all versions +client.beta.agents.versions.list(agent.id).auto_paging_each do |version| + puts "Version #{version.version}: #{version.updated_at.iso8601}" +end + +# Archive the agent +archived = client.beta.agents.archive(agent.id) +puts "Archived at: #{archived.archived_at.iso8601}" +``` + +--- + +## Send a User Message + +```ruby +client.beta.sessions.events.send_( + session.id, + events: [{ + type: "user.message", + content: [{type: "text", text: "Review the auth module"}] + }] +) +``` + +> 💡 **Stream-first:** Open the stream *before* (or concurrently with) sending the message. The stream only delivers events that occur after it opens — stream-after-send means early events arrive buffered in one batch. See [Steering Patterns](../../shared/managed-agents-events.md#steering-patterns). + +--- + +## Stream Events (SSE) + +```ruby +# Open the stream first, then send the user message +stream = client.beta.sessions.events.stream_events(session.id) + +client.beta.sessions.events.send_( + session.id, + events: [{ + type: "user.message", + content: [{type: "text", text: "Summarize the repo README"}] + }] +) + +stream.each do |event| + case event.type + in :"agent.message" + event.content.each { |block| print block.text } + in :"agent.tool_use" + puts "\n[Using tool: #{event.name}]" + in :"session.status_idle" + break + in :"session.error" + puts "\n[Error: #{event.error&.message || "unknown"}]" + break + else + # ignore other event types + end +end +``` + +> ℹ️ Event `.type` is a Symbol (compare with `:"agent.message"`, not `"agent.message"`). + +### Reconnecting and Tailing + +When reconnecting mid-session, list past events first to dedupe, then tail live events: + +```ruby +require "set" + +stream = client.beta.sessions.events.stream_events(session.id) + +# Stream is open and buffering. List history before tailing live. +seen_event_ids = Set.new +client.beta.sessions.events.list(session.id).auto_paging_each { |past| seen_event_ids << past.id } + +# Tail live events, skipping anything already seen +stream.each do |event| + next if seen_event_ids.include?(event.id) + seen_event_ids << event.id + case event.type + in :"agent.message" + event.content.each { |block| print block.text } + in :"session.status_idle" + break + else + # ignore other event types + end +end +``` + +--- + +## Provide Custom Tool Result + +> ℹ️ The Ruby managed-agents bindings for `user.custom_tool_result` are not yet documented in this skill or in the apps source examples. Refer to `shared/managed-agents-events.md` for the wire format and the `anthropic` Ruby gem repository for the corresponding params. + +--- + +## Poll Events + +```ruby +client.beta.sessions.events.list(session.id).auto_paging_each do |event| + puts "#{event.type}: #{event.id}" +end +``` + +--- + +## Upload a File + +```ruby +require "pathname" + +file = client.beta.files.upload(file: Pathname("data.csv")) +puts "File ID: #{file.id}" + +# Mount in a session +session = client.beta.sessions.create( + agent: agent.id, + environment_id: environment.id, + resources: [ + { + type: "file", + file_id: file.id, + mount_path: "/workspace/data.csv" + } + ] +) +``` + +### Add and Manage Resources on an Existing Session + +```ruby +# Attach an additional file to an open session +resource = client.beta.sessions.resources.add( + session.id, + type: "file", + file_id: file.id +) +puts resource.id # "sesrsc_01ABC..." + +# List resources on the session +listed = client.beta.sessions.resources.list(session.id) +listed.data.each { |entry| puts "#{entry.id} #{entry.type}" } + +# Detach a resource +client.beta.sessions.resources.delete(resource.id, session_id: session.id) +``` + +--- + +## List and Download Session Files + +> ℹ️ Listing and downloading files an agent wrote during a session is not yet documented for Ruby in this skill or in the apps source examples. See `shared/managed-agents-events.md` and the `anthropic` Ruby gem repository for the file list/download bindings. + +--- + +## Session Management + +```ruby +# List environments +environments = client.beta.environments.list + +# Retrieve a specific environment +env = client.beta.environments.retrieve(environment.id) + +# Archive an environment (read-only, existing sessions continue) +client.beta.environments.archive(environment.id) + +# Delete an environment (only if no sessions reference it) +client.beta.environments.delete(environment.id) + +# Delete a session +client.beta.sessions.delete(session.id) +``` + +--- + +## MCP Server Integration + +```ruby +# Agent declares MCP server (no auth here — auth goes in a vault) +agent = client.beta.agents.create( + name: "GitHub Assistant", + model: :"claude-opus-4-8", + mcp_servers: [ + { + type: "url", + name: "github", + url: "https://api.githubcopilot.com/mcp/" + } + ], + tools: [ + {type: "agent_toolset_20260401"}, + {type: "mcp_toolset", mcp_server_name: "github"} + ] +) + +# Session attaches vault(s) containing credentials for those MCP server URLs +session = client.beta.sessions.create( + agent: {type: "agent", id: agent.id, version: agent.version}, + environment_id: environment.id, + vault_ids: [vault.id] +) +``` + +See `shared/managed-agents-tools.md` §Vaults for creating vaults and adding credentials. + +--- + +## Vaults + +```ruby +# Create a vault +vault = client.beta.vaults.create( + display_name: "Alice", + metadata: {external_user_id: "usr_abc123"} +) +puts vault.id # "vlt_01ABC..." + +# Add an OAuth credential +credential = client.beta.vaults.credentials.create( + vault.id, + display_name: "Alice's Slack", + auth: { + type: "mcp_oauth", + mcp_server_url: "https://mcp.slack.com/mcp", + access_token: "xoxp-...", + expires_at: "2026-04-15T00:00:00Z", + refresh: { + token_endpoint: "https://slack.com/api/oauth.v2.access", + client_id: "1234567890.0987654321", + scope: "channels:read chat:write", + refresh_token: "xoxe-1-...", + token_endpoint_auth: { + type: "client_secret_post", + client_secret: "abc123..." + } + } + } +) + +# Rotate the credential (e.g., after a token refresh) +client.beta.vaults.credentials.update( + credential.id, + vault_id: vault.id, + auth: { + type: "mcp_oauth", + access_token: "xoxp-new-...", + expires_at: "2026-05-15T00:00:00Z", + refresh: {refresh_token: "xoxe-1-new-..."} + } +) + +# Archive a vault +client.beta.vaults.archive(vault.id) +``` + +--- + +## GitHub Repository Integration + +Mount a GitHub repository as a session resource (a vault holds the GitHub MCP credential): + +```ruby +session = client.beta.sessions.create( + agent: agent.id, + environment_id: environment.id, + vault_ids: [vault.id], + resources: [ + { + type: "github_repository", + url: "https://github.com/org/repo", + mount_path: "/workspace/repo", + authorization_token: "ghp_your_github_token" + } + ] +) +``` + +Multiple repositories on the same session: + +```ruby +resources = [ + { + type: "github_repository", + url: "https://github.com/org/frontend", + mount_path: "/workspace/frontend", + authorization_token: "ghp_your_github_token" + }, + { + type: "github_repository", + url: "https://github.com/org/backend", + mount_path: "/workspace/backend", + authorization_token: "ghp_your_github_token" + } +] +``` + +Rotating a repository's authorization token: + +```ruby +listed = client.beta.sessions.resources.list(session.id) +repo_resource_id = listed.data.first.id + +client.beta.sessions.resources.update( + repo_resource_id, + session_id: session.id, + authorization_token: "ghp_your_new_github_token" +) +``` diff --git a/.agents/skills/claude-api/shared/agent-design.md b/.agents/skills/claude-api/shared/agent-design.md new file mode 100644 index 00000000..4abdac28 --- /dev/null +++ b/.agents/skills/claude-api/shared/agent-design.md @@ -0,0 +1,101 @@ +# Agent Design Patterns + +This file covers decision heuristics for building agents on the Claude API: which primitives to reach for, how to design your tool surface, and how to manage context and cost over long runs. For per-tool mechanics and code examples, see `tool-use-concepts.md` and the language-specific folders. + +--- + +## Model Parameters + +| Parameter | When to use it | What to expect | +| --- | --- | --- | +| **Adaptive thinking** (`thinking: {type: "adaptive"}`) | When you want Claude to control when and how much to think. | Claude determines thinking depth per request and automatically interleaves thinking between tool calls. No token budget to tune. | +| **Effort** (`output_config: {effort: ...}`) | When adjusting the tradeoff between thoroughness and token efficiency. | Lower effort → fewer and more-consolidated tool calls, less preamble, terser confirmations. `medium` is often a favorable balance. Use `max` when correctness matters more than cost. | + +See `SKILL.md` §Thinking & Effort for model support and parameter details. + +--- + +## Designing Your Tool Surface + +### Bash vs. dedicated tools + +Claude doesn't know your application's security boundary, approval policy, or UX surface. Claude emits tool calls; your harness handles them. The shape of those tool calls determines what the harness can do. + +A **bash tool** gives Claude broad programmatic leverage — it can perform almost any action. But it gives the harness only an opaque command string, the same shape for every action. Promoting an action to a **dedicated tool** gives the harness an action-specific hook with typed arguments it can intercept, gate, render, or audit. + +**When to promote an action to a dedicated tool:** + +- **Security boundary.** Actions that require gating are natural candidates. Reversibility is a useful criterion: hard-to-reverse actions (external API calls, sending messages, deleting data) can be gated behind user confirmation. A `send_email` tool is easy to gate; `bash -c "curl -X POST ..."` is not. +- **Staleness checks.** A dedicated `edit` tool can reject writes if the file changed since Claude last read it. Bash can't enforce that invariant. +- **Rendering.** Some actions benefit from custom UI. Claude Code promotes question-asking to a tool so it can render as a modal, present options, and block the agent loop until answered. +- **Scheduling.** Read-only tools like `glob` and `grep` can be marked parallel-safe. When the same actions run through bash, the harness can't tell a parallel-safe `grep` from a parallel-unsafe `git push`, so it must serialize. + +**Rule of thumb:** Start with bash for breadth. Promote to dedicated tools when you need to gate, render, audit, or parallelize the action. + +--- + +## Anthropic-Provided Tools + +| Tool | Side | When to use it | What to expect | +| --- | --- | --- | --- | +| **Bash** | Client | Claude needs to execute shell commands. | Claude emits commands; your harness executes them. Reference implementation provided. | +| **Text editor** | Client | Claude needs to read or edit files. | Claude views, creates, and edits files via your implementation. Reference implementation provided. | +| **Computer use** | Client or Server | Claude needs to interact with GUIs, web apps, or visual interfaces. | Claude takes screenshots and issues mouse/keyboard commands. Can be self-hosted (you run the environment) or Anthropic-hosted. | +| **Code execution** | Server | Claude needs to run code in a sandbox you don't want to manage. | Anthropic-hosted container with built-in file and bash sub-tools. No client-side execution. | +| **Web search / fetch** | Server | Claude needs information past its training cutoff (news, current events, recent docs) or the content of a specific URL. | Claude issues a query or URL; Anthropic executes it and returns results with citations. | +| **Memory** | Client | Claude needs to save context across sessions. | Claude reads/writes a `/memories` directory. You implement the storage backend. | + +**Client-side** tools are defined by Anthropic (name, schema, Claude's usage pattern) but executed by your harness. Anthropic provides reference implementations. **Server-side** tools run entirely on Anthropic infrastructure — declare them in `tools` and Claude handles the rest. + +--- + +## Composing Tool Calls: Programmatic Tool Calling + +With standard tool use, each tool call is a round trip: Claude calls the tool, the result lands in Claude's context, Claude reasons about it, then calls the next tool. Three sequential actions (read profile → look up orders → check inventory) means three round trips. Each adds latency and tokens, and most of the intermediate data is never needed again. + +**Programmatic tool calling (PTC)** lets Claude compose those calls into a script instead. The script runs in the code execution container. When the script calls a tool, the container pauses, the call is executed (client-side or server-side), and the result returns to the running code — not to Claude's context. The script processes it with normal control flow (loops, filters, branches). Only the script's final output returns to Claude. + +| When to use it | What to expect | +| --- | --- | +| Many sequential tool calls, or large intermediate results you want filtered before they hit the context window. | Claude writes code that invokes tools as functions. Runs in the code execution container. Token cost scales with final output, not intermediate results. | + +--- + +## Scaling the Tool and Instruction Set + +| Feature | When to use it | What to expect | +| --- | --- | --- | +| **Tool search** | Many tools available, but only a few relevant per request. Don't want all schemas in context upfront. | Claude searches the tool set and loads only relevant schemas. Tool definitions are appended, not swapped — preserves cache (see Caching below). | +| **Skills** | Task-specific instructions Claude should load only when relevant. | Each skill is a folder with a `SKILL.md`. The skill's description sits in context by default; Claude reads the full file when the task calls for it. | + +Both patterns keep the fixed context small and load detail on demand. + +--- + +## Long-Running Agents: Managing Context + +| Pattern | When to use it | What to expect | +| --- | --- | --- | +| **Context editing** | Context grows stale over many turns (old tool results, completed thinking). | Tool results and thinking blocks are cleared based on configurable thresholds. Keeps the transcript lean without summarizing. | +| **Compaction** | Conversation likely to reach or exceed the context window limit. | Earlier context is summarized into a compaction block server-side. See `SKILL.md` §Compaction for the critical `response.content` handling. | +| **Memory** | State must persist across sessions (not just within one conversation). | Claude reads/writes files in a memory directory. Survives process restarts. | + +**Choosing between them:** Context editing and compaction operate within a session — editing prunes stale turns, compaction summarizes when you're near the limit. Memory is for cross-session persistence. Many long-running agents use all three. + +--- + +## Caching for Agents + +**Read `prompt-caching.md` first.** It covers the prefix-match invariant, breakpoint placement, the silent-invalidator audit, and why changing tools or models mid-session breaks the cache. This section covers only the agent-specific workarounds for those constraints. + +| Constraint (from `prompt-caching.md`) | Agent-specific workaround | +| --- | --- | +| Editing the system prompt mid-session invalidates the cache. | Append a `{"role": "system", ...}` message to `messages[]` instead (beta, on supporting models — see `prompt-caching.md` § Mid-conversation system messages). The cached prefix stays intact, and the model treats it as an operator-authority instruction rather than user text. On models that don't support it, fall back to a `` text block in the user turn. | +| Switching models mid-session invalidates the cache. | Spawn a **subagent** with the cheaper model for the sub-task; keep the main loop on one model. Claude Code's Explore subagents use Haiku this way. | +| Adding/removing tools mid-session invalidates the cache. | Use **tool search** for dynamic discovery — it appends tool schemas rather than swapping them, so the existing prefix is preserved. | + +For multi-turn breakpoint placement, use top-level auto-caching — see `prompt-caching.md` §Placement patterns. + +--- + +For live documentation on any of these features, see `live-sources.md`. diff --git a/.agents/skills/claude-api/shared/anthropic-cli.md b/.agents/skills/claude-api/shared/anthropic-cli.md new file mode 100644 index 00000000..e9629532 --- /dev/null +++ b/.agents/skills/claude-api/shared/anthropic-cli.md @@ -0,0 +1,246 @@ +# Anthropic CLI (`ant`) + +The `ant` CLI exposes every Claude API resource as a shell subcommand. Compared to `curl`: request bodies are built from typed flags or piped YAML instead of hand-written JSON, `@path` inlines file contents into any string field, `--transform` extracts fields with a GJSON path (no `jq`), list endpoints auto-paginate (cap total results with `--max-items N`; `--limit` only sets the server page size), and the `beta:` prefix auto-sets the right `anthropic-beta` header. + +## When to use the CLI vs the SDK + +**CLI for the control plane, SDK for the data plane.** Agents and environments are relatively static resources you define, configure, and debug with `ant` — check the YAML into your repo, apply from CI, inspect from a terminal. Sessions are dynamic and driven by your application through the SDK — create per task, stream events, react to tool calls, integrate into your product. Both hit the same API; the split is about where the call lives, not what's possible. + +| | Control plane → `ant` | Data plane → SDK | +|---|---|---| +| Resources | agents, environments, skills, vaults, files | sessions, events | +| Cadence | Once per deploy / ad-hoc | Every task / every turn | +| Lives in | `*.yaml` in your repo + CI + terminal | Application code | +| Typical calls | `create < agent.yaml`, `update --version N`, `list`, `retrieve`, `archive`, `--debug` | `sessions.create()`, `events.stream()`, `events.send()` | + +## Install and auth + +```sh +# macOS +brew install anthropics/tap/ant +xattr -d com.apple.quarantine "$(brew --prefix)/bin/ant" + +# Linux / WSL — pick the release from github.com/anthropics/anthropic-cli/releases +curl -fsSL "https://github.com/anthropics/anthropic-cli/releases/download/v${VERSION}/ant_${VERSION}_$(uname -s | tr A-Z a-z)_$(uname -m | sed -e s/x86_64/amd64/ -e s/aarch64/arm64/).tar.gz" \ + | sudo tar -xz -C /usr/local/bin ant + +# Or from source (Go 1.22+) +go install github.com/anthropics/anthropic-cli/cmd/ant@latest +``` + +**Auth** — the CLI resolves credentials the same way the SDKs do (first match wins): explicit flags, then `ANTHROPIC_API_KEY`, then `ANTHROPIC_AUTH_TOKEN`, then the `ANTHROPIC_PROFILE`-selected or active profile, then Workload Identity Federation env vars, then the default profile on disk. Override the host with `ANTHROPIC_BASE_URL` or `--base-url`. + +- **API key**: set `ANTHROPIC_API_KEY` in the environment. +- **OAuth profile** (no static key to manage): `ant auth login` opens a browser, exchanges for a short-lived token, and stores a profile under `$ANTHROPIC_CONFIG_DIR` (default `~/.config/anthropic/` on Linux/macOS, `%APPDATA%\Anthropic` on Windows — `configs/.json` for settings, `credentials/.json` for tokens). Subsequent `ant` (and SDK) calls pick it up automatically — a bare `Anthropic()` client works after login, but scripts that read `ANTHROPIC_API_KEY` directly do not. Claude Code and the Claude Agent SDK honor the same profile resolution. `ant auth status` shows which credential source and profile won (it reports status only — don't script against its exit code as a health check); `ant auth logout` clears the active profile (`--all` for every profile). On a remote host without a browser, `ant auth login --no-browser` prints the authorize URL and accepts the code back in the terminal. +- **Non-interactive workloads** (CI, servers, containers): interactive login is for development on your own machine — use Workload Identity Federation instead (see the authentication docs via `shared/live-sources.md`). + +> **The #1 auth trap:** profiles are only consulted when no API key is set. A stale exported `ANTHROPIC_API_KEY` silently overrides every profile — requests hit whatever org/workspace that key is scoped to. `ant auth status` shows which source won; unset the key (or per-command: `env -u ANTHROPIC_API_KEY ant …`) before relying on a profile. Truly **unset** it — an empty `ANTHROPIC_API_KEY=""` still wins its precedence slot and authenticates with an empty key. The same shadowing applies in reverse to Claude Code: after `ant auth login`, Claude Code may warn about an auth conflict between the profile and its own `/login` credential — keep one (use the profile and `/logout` in Claude Code, or `ant auth logout` to keep Claude Code's own login). + +**Named profiles** — an interactive-login token is bound to a single org+workspace, and the API only shows resources belonging to that workspace. If an agent, session, or file you created "disappears", the usual cause is a token scoped to a different workspace than the one that created it (`ant auth status` shows the active workspace). Multi-workspace work means one profile per workspace: + +```sh +ant auth login --profile # creates the profile if it doesn't exist; org/workspace picker in browser +ant auth login --profile --workspace-id wrkspc_01... # bind directly, skip the picker +ant profile activate # switch the default profile +ant --profile models list # one-off; equivalent: ANTHROPIC_PROFILE= ant models list +ant profile list # inspect +ant profile set workspace_id wrkspc_01... --profile # edit config keys (workspace_id, base_url, organization_id, …) +``` + +`ant profile set` edits an existing profile's config — it never creates one, and it does **not** rebind already-issued credentials; run `ant auth login` again under that profile to mint a token for the new target. Pointing `ANTHROPIC_PROFILE` at a profile that doesn't exist is an error, not a fall-through. Refresh tokens eventually hard-expire (they don't slide with use) — when a previously working profile starts failing auth, re-run `ant auth login` before debugging anything else. + +**Scopes** — a profile's OAuth scope set is requested at login (`--scope`) and persists on the profile (`scope` is also a `profile set` config key; like other config edits, changing it requires a fresh `ant auth login` to take effect). Privileged scopes — e.g. `org:admin` for organization-administration endpoints — are **not** in the default scope set: pass the full set you want explicitly (`ant auth login --profile admin --scope "... org:admin"`), and the server grants a privileged scope only if your role actually has it. Because the scope set rides on every token the profile mints, keep privileged work on a dedicated profile (`admin` vs `default`) and do day-to-day inference on the unprivileged one, switching with `--profile`/`ANTHROPIC_PROFILE`. Check `ant auth login --help` for the current scope list, and `ant auth status` to see what the active token carries. + +To hand the active credential to a subprocess or raw-HTTP script: + +```sh +# Bare access token — for curl's Authorization header +curl https://api.anthropic.com/v1/messages \ + -H "Authorization: Bearer $(ant auth print-credentials --access-token)" \ + -H "anthropic-version: 2023-06-01" \ + -H "anthropic-beta: oauth-2025-04-20" \ + -H "content-type: application/json" \ + -d '{"model": "claude-opus-4-8", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}' + +# .env format — sets ANTHROPIC_AUTH_TOKEN (and ANTHROPIC_BASE_URL if the profile has one). +# Output is bare KEY=value (no `export`), so use `set -a` to auto-export for child processes: +set -a; eval "$(ant auth print-credentials --env)"; set +a +python my_script.py # SDK picks up ANTHROPIC_AUTH_TOKEN +``` + +OAuth tokens go on `Authorization: Bearer` (not `x-api-key:`) **plus the `anthropic-beta: oauth-2025-04-20` header** — converting a raw curl/httpx script from an API key is a header change, not a key swap. The beta header requirement is endpoint-dependent (some endpoints happen to work without it; `/v1/messages` does not) — always send it so requests don't break when you switch endpoints. The token is short-lived and not auto-refreshed when passed via env var, so re-run `print-credentials` before it expires for long-running scripts (`print-credentials` itself refreshes the token if needed). If both `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` are set, the SDKs send both and the API rejects the request — unset `ANTHROPIC_API_KEY` before `eval`ing the `--env` output. + +**Foot-gun:** `ant auth print-credentials` with **no flags** prints the entire credentials JSON, not the bare token — putting that in an `Authorization` header yields an empty response or HTTP/2 protocol error. Always use `--access-token` for headers (it always reads the named/active profile; a set `ANTHROPIC_API_KEY` doesn't override credential printing). + +## Command structure + +``` +ant [:] [flags] +``` + +Beta resources (agents, sessions, environments, deployments, skills, vaults, memory stores) live under `beta:` — the CLI auto-sends the right `anthropic-beta` header, so don't pass it yourself unless overriding with `--beta
`. For self-hosted environments, `ant beta:worker poll/run` and `ant beta:environments:work stats/stop` drive and monitor the work queue — see `shared/managed-agents-self-hosted-sandboxes.md`. + +```sh +ant models list +ant messages create --model claude-opus-4-8 --max-tokens 1024 --message '{role: user, content: "Hello"}' +ant beta:agents retrieve --agent-id agent_01... +ant beta:sessions:events list --session-id session_01... +``` + +`ant --help` lists resources; append `--help` to any subcommand for its flags. + +## Global flags + +| Flag | Purpose | +| --- | --- | +| `--format` | `auto` (default: pretty if TTY, compact if piped), `json`, `jsonl`, `yaml`, `pretty`, `raw`, `explore` (interactive TUI) | +| `--transform` | GJSON path applied to the response (per-item on list endpoints). Not applied when `--format raw`. | +| `-r`, `--raw-output` | If the transformed result is a string, print it without quotes (jq semantics). Pair with `--transform` for scalar capture. | +| `--max-items` | Cap total results returned from auto-paginating list endpoints (distinct from `--limit`, which is the server page size). | +| `--format-error` / `--transform-error` | Same as `--format`/`--transform`, applied to error responses. `-r` does not apply to the error path — use `--format-error yaml` for unquoted error scalars. | +| `--base-url` | Override API host | +| `--debug` | Print full HTTP request + response to stderr (API key redacted) | + +## Output — `--transform` + `--format` + +`--transform` takes a [GJSON path](https://github.com/tidwall/gjson/blob/master/SYNTAX.md). On list endpoints it runs **per item**, not on the envelope. + +```sh +ant beta:agents list --transform '{id,name,model}' --format jsonl +``` + +**Extract a scalar for shell use:** pair `--transform` with `-r` (`--raw-output` — prints strings unquoted, jq-style): + +```sh +AGENT_ID=$(ant beta:agents create --name "My Agent" --model '{id: claude-sonnet-4-6}' \ + --transform id -r) +``` + +## Input — flags, stdin, `@file` + +**Flags** — scalar fields map directly. Structured fields accept relaxed-YAML syntax (unquoted keys) or strict JSON. Repeatable flags build arrays (each `--tool`, `--event`, `--message` appends one element): + +```sh +ant beta:agents create \ + --name "Research Agent" \ + --model '{id: claude-opus-4-8}' \ + --tool '{type: agent_toolset_20260401}' \ + --tool '{type: custom, name: search_docs, input_schema: {type: object, properties: {query: {type: string}}}}' +``` + +**Stdin** — pipe a full JSON or YAML body. Merged with flags; flags win on conflict (for array fields, any flag **replaces** the stdin array entirely — it does not append). Quote the heredoc delimiter (`<<'YAML'`) to disable shell expansion inside the body: + +```sh +ant beta:agents create <<'YAML' +name: Research Agent +model: claude-opus-4-8 +system: | + You are a research assistant. Cite sources for every claim. +tools: + - type: agent_toolset_20260401 +YAML +``` + +**`@file` references** — inline a file's contents into any string-valued field. Inside structured flag values, quote the path. Binary files are auto-base64'd; force with `@file://` (text) or `@data://` (base64). Escape a literal leading `@` as `\@`. + +```sh +ant beta:agents create --name "Researcher" --model '{id: claude-sonnet-4-6}' --system @./prompts/researcher.txt + +ant messages create --model claude-opus-4-8 --max-tokens 1024 \ + --message '{role: user, content: [ + {type: document, source: {type: base64, media_type: application/pdf, data: "@./scan.pdf"}}, + {type: text, text: "Extract the text from this scanned document."} + ]}' \ + --transform 'content.0.text' -r +``` + +Flags that natively take a file path (e.g. `--file` on `beta:files upload`) accept a bare path without `@`. + +## Version-controlled Managed Agents resources + +This is the recommended flow for defining agents and environments — check the YAML into your repo and sync via `create` (first time) / `update` (thereafter). See `shared/managed-agents-core.md` for the field reference. + +```yaml +# summarizer.agent.yaml +name: Summarizer +model: claude-sonnet-4-6 +system: | + You are a helpful assistant that writes concise summaries. +tools: + - type: agent_toolset_20260401 +``` + +```sh +# Create (once) — capture the ID +AGENT_ID=$(ant beta:agents create < summarizer.agent.yaml --transform id -r) + +# Update (CI) — needs ID + current version (optimistic lock) +ant beta:agents update --agent-id "$AGENT_ID" --version 1 < summarizer.agent.yaml +``` + +Same pattern for environments (`ant beta:environments create|update < env.yaml`), then start a session with both IDs: + +```sh +ant beta:sessions create --agent "$AGENT_ID" --environment-id "$ENV_ID" --title "Task" +ant beta:sessions:events send --session-id "$SID" \ + --event '{type: user.message, content: [{type: text, text: "Summarize X"}]}' +ant beta:sessions:events list --session-id "$SID" --transform 'content.0.text' -r +ant beta:sessions:events stream --session-id "$SID" # live event stream +``` + +### Interactive session loop (stream-before-send) + +`ant beta:sessions:events stream` only delivers events emitted *after* the stream opens — so open it **before** sending the kickoff to avoid missing early events. Use process substitution to hold the stream on a file descriptor, send, then read: + +```sh +exec {stream}< <(ant beta:sessions:events stream --session-id "$SID" \ + --transform '{type,text:content.#(type=="text").text,err:error.message}' --format yaml) + +ant beta:sessions:events send --session-id "$SID" > /dev/null <<'YAML' +events: + - type: user.message + content: + - type: text + text: Summarize the repo README +YAML + +type= +while IFS= read -r -u "$stream" line; do + case "$line" in + type:\ session.status_idle) break ;; + type:\ session.error) + IFS= read -r -u "$stream" next || next= + case "$next" in err:\ *) msg=${next#err: } ;; *) msg=unknown ;; esac + printf '\n[Error: %s]\n' "$msg"; break ;; + type:\ *) type=${line#type: } ;; + text:*) + [[ $type == agent.message ]] || continue + val=${line#text: } + case "$val" in '|-'|'|') ;; *) printf '%s' "$val" ;; esac ;; + \ \ *) + if [[ $type == agent.message ]]; then printf '%s\n' "${line# }"; fi ;; + esac +done +exec {stream}<&- +``` + +This works for interactive exploration and demos. For application code that needs to react to `agent.tool_use` / `agent.custom_tool_use` events, reconnect after drops, or dedup against `events.list`, use the SDK — see `shared/managed-agents-client-patterns.md`. + +## Scripting patterns + +`--transform id -r` on a list endpoint emits one bare ID per line — compose with `xargs`, or use `--max-items N` to bound the result set without piping through `head`: + +```sh +FIRST=$(ant beta:agents list --transform id -r --max-items 1) +ant beta:agents:versions list --agent-id "$FIRST" --transform '{version,created_at}' --format jsonl +``` + +Error shaping mirrors the success path (note: `-r` does not apply to error output — use `--format-error yaml` for an unquoted scalar here): + +```sh +ant beta:agents retrieve --agent-id bogus --transform-error error.message --format-error yaml 2>&1 +``` + +Shell completion: `ant @completion {zsh|bash|fish|powershell}`. + +For the full, always-current reference (including per-endpoint flags), WebFetch the **Anthropic CLI** URL in `shared/live-sources.md`. diff --git a/.agents/skills/claude-api/shared/claude-platform-on-aws.md b/.agents/skills/claude-api/shared/claude-platform-on-aws.md new file mode 100644 index 00000000..56984d93 --- /dev/null +++ b/.agents/skills/claude-api/shared/claude-platform-on-aws.md @@ -0,0 +1,59 @@ +# Claude Platform on AWS + +**Anthropic-operated** access to the Claude Developer Platform through AWS infrastructure — SigV4 authentication, AWS IAM access control, and AWS Marketplace billing. Because Anthropic operates it, **the API surface matches first-party with same-day parity**: Managed Agents, server-side tools, batches, Files, and every feature in this skill work the same way (**except self-hosted sandboxes** — `config:{type:"self_hosted"}` is not available here; use `cloud`). Model IDs are the bare first-party strings (`claude-opus-4-8`, `claude-sonnet-4-6`) — **no provider prefix**. + +> **Not the same as Amazon Bedrock.** Bedrock is partner-operated (AWS runs the service; release schedules vary, feature subset, `anthropic.`-prefixed model IDs). Claude Platform on AWS and Bedrock coexist; pick by whether you need AWS-native IAM/billing with full Anthropic API parity (this page) vs. Bedrock's own ecosystem. + +--- + +## Client & install + +| Language | Install | Client | +|---|---|---| +| Python | `pip install -U "anthropic[aws]"` | `from anthropic import AnthropicAWS` → `AnthropicAWS()` | +| TypeScript | `npm install @anthropic-ai/aws-sdk` | `import AnthropicAws from "@anthropic-ai/aws-sdk"` → `new AnthropicAws()` | +| Go | `go get github.com/anthropics/anthropic-sdk-go` | `import anthropicaws "github.com/anthropics/anthropic-sdk-go/aws"` → `anthropicaws.NewClient(ctx, anthropicaws.ClientConfig{})` | +| C# | `dotnet add package Anthropic.Aws` | `new AnthropicAwsClient()` | +| Java | See SDK repo in `shared/live-sources.md` | See SDK repo in `shared/live-sources.md` | +| Ruby | `gem install anthropic aws-sdk-core` | See SDK repo in `shared/live-sources.md` | +| PHP | `composer require anthropic-ai/sdk aws/aws-sdk-php` | See SDK repo in `shared/live-sources.md` | + +After construction, **use the client exactly as you would `Anthropic()`** — `client.messages.create(...)`, `client.beta.sessions.*`, etc., with bare model IDs. + +```python +from anthropic import AnthropicAWS + +client = AnthropicAWS() # region + workspace_id from env; see below +client.messages.create( + model="claude-opus-4-8", + max_tokens=1024, + messages=[{"role": "user", "content": "Hello"}], +) +``` + +--- + +## Required configuration + +Two values must be available (constructor args or environment) — **there is no default fallback** for either: + +| Value | Env var | Notes | +|---|---|---| +| AWS region | `AWS_REGION` | Required. Unlike `AnthropicBedrock`, there is no `us-east-1` fallback. | +| Workspace ID | `ANTHROPIC_AWS_WORKSPACE_ID` | Required. Routes requests to your Claude workspace. | + +Endpoint pattern: `https://aws-external-anthropic.{region}.api.aws/v1/...`. Requests are SigV4-signed with service name `aws-external-anthropic`. + +## Authentication + +The client resolves AWS credentials via the standard precedence chain: explicit constructor args → environment (`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`/`AWS_SESSION_TOKEN`) → shared profile → assumed role / instance metadata. + +**Short-term API keys** are also supported for cases where SigV4 isn't practical (e.g., browser, simple scripts). Mint one with the per-language token-generator package; pass it as `api_key` on the client. Lifetime is the **lesser of** the requested duration, the underlying credential's expiry, and **12 hours**. For package names and IAM details, WebFetch the Claude Platform on AWS page in `shared/live-sources.md`. + +--- + +## What to tell users + +- Treat it as first-party: every section of this skill applies unchanged. Do **not** apply Bedrock's feature-availability mask. +- Model IDs are bare (`claude-opus-4-8`). Do **not** add an `anthropic.` prefix. +- A missing region or `workspace_id` throws at client-construction time (no request is sent). A **403** means the request reached the server — check for a **wrong** `workspace_id` or a missing IAM action on the principal. See the IAM actions reference in `shared/live-sources.md`. diff --git a/.agents/skills/claude-api/shared/error-codes.md b/.agents/skills/claude-api/shared/error-codes.md new file mode 100644 index 00000000..629e5d0b --- /dev/null +++ b/.agents/skills/claude-api/shared/error-codes.md @@ -0,0 +1,229 @@ +# HTTP Error Codes Reference + +This file documents HTTP error codes returned by the Claude API, their common causes, and how to handle them. For language-specific error handling examples, see the `python/` or `typescript/` folders. + +## Error Code Summary + +| Code | Error Type | Retryable | Common Cause | +| ---- | ----------------------- | --------- | ------------------------------------ | +| 400 | `invalid_request_error` | No | Invalid request format or parameters | +| 401 | `authentication_error` | No | Invalid or missing API key | +| 403 | `permission_error` | No | API key lacks permission | +| 404 | `not_found_error` | No | Invalid endpoint or model ID | +| 413 | `request_too_large` | No | Request exceeds size limits | +| 429 | `rate_limit_error` | Yes | Too many requests | +| 500 | `api_error` | Yes | Anthropic service issue | +| 529 | `overloaded_error` | Yes | API is temporarily overloaded | + +## Detailed Error Information + +### 400 Bad Request + +**Causes:** + +- Malformed JSON in request body +- Missing required parameters (`model`, `max_tokens`, `messages`) +- Invalid parameter types (e.g., string where integer expected) +- Empty messages array +- Messages not alternating user/assistant + +**Example error:** + +```json +{ + "type": "error", + "error": { + "type": "invalid_request_error", + "message": "messages: roles must alternate between \"user\" and \"assistant\"" + }, + "request_id": "req_011CSHoEeqs5C35K2UUqR7Fy" +} +``` + +**Fix:** Validate request structure before sending. Check that: + +- `model` is a valid model ID +- `max_tokens` is a positive integer +- `messages` array is non-empty and alternates correctly + +--- + +### 401 Unauthorized + +**Causes:** + +- Missing `x-api-key` header or `Authorization` header +- Invalid API key format +- Revoked or deleted API key +- OAuth bearer token sent via `x-api-key` instead of `Authorization: Bearer` +- Both `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` set — the SDK sends both headers and the API rejects the request + +**Fix:** Set `ANTHROPIC_API_KEY`, or run `ant auth login` and leave the client constructor empty. For raw HTTP with an OAuth token, use `Authorization: Bearer ` (not `x-api-key:`). + +--- + +### 403 Forbidden + +**Causes:** + +- API key doesn't have access to the requested model +- Organization-level restrictions +- Attempting to access beta features without beta access + +**Fix:** Check your API key permissions in the Console. You may need a different API key or to request access to specific features. + +--- + +### 404 Not Found + +**Causes:** + +- Typo in model ID (e.g., `claude-sonnet-4.6` instead of `claude-sonnet-4-6`) +- Using deprecated model ID +- Invalid API endpoint + +**Fix:** Use exact model IDs from the models documentation. You can use aliases (e.g., `claude-opus-4-8`). + +--- + +### 413 Request Too Large + +**Causes:** + +- Request body exceeds maximum size +- Too many tokens in input +- Image data too large + +**Fix:** Reduce input size — truncate conversation history, compress/resize images, or split large documents into chunks. + +--- + +### 400 Validation Errors + +Some 400 errors are specifically related to parameter validation: + +- `max_tokens` exceeds model's limit +- Invalid `temperature` value (must be 0.0-1.0) +- `budget_tokens` >= `max_tokens` in extended thinking +- Invalid tool definition schema + +**Model-specific 400s on Opus 4.8 / 4.7:** + +- `temperature`, `top_p`, `top_k` are removed — sending any of them returns 400. Delete the parameter; see `shared/model-migration.md` → Per-SDK Syntax Reference. +- `thinking: {type: "enabled", budget_tokens: N}` is removed — sending it returns 400. Use `thinking: {type: "adaptive"}` instead. + +**Common mistake with extended thinking on older models (Opus 4.6 and earlier):** + +``` +# Wrong: budget_tokens must be < max_tokens +thinking: budget_tokens=10000, max_tokens=1000 → Error! + +# Correct +thinking: budget_tokens=10000, max_tokens=16000 +``` + +--- + +### 429 Rate Limited + +**Causes:** + +- Exceeded requests per minute (RPM) +- Exceeded tokens per minute (TPM) +- Exceeded tokens per day (TPD) + +**Headers to check:** + +- `retry-after`: Seconds to wait before retrying +- `x-ratelimit-limit-*`: Your limits +- `x-ratelimit-remaining-*`: Remaining quota + +**Fix:** The Anthropic SDKs automatically retry 429 and 5xx errors with exponential backoff (default: `max_retries=2`). For custom retry behavior, see the language-specific error handling examples. + +--- + +### 500 Internal Server Error + +**Causes:** + +- Temporary Anthropic service issue +- Bug in API processing + +**Fix:** Retry with exponential backoff. If persistent, check [status.anthropic.com](https://status.anthropic.com). + +--- + +### 529 Overloaded + +**Causes:** + +- High API demand +- Service capacity reached + +**Fix:** Retry with exponential backoff. Consider using a different model (Haiku is often less loaded), spreading requests over time, or implementing request queuing. + +--- + +## Common Mistakes and Fixes + +| Mistake | Error | Fix | +| ------------------------------- | ---------------- | ------------------------------------------------------- | +| `temperature`/`top_p`/`top_k` on Opus 4.8 / 4.7 | 400 | Remove the parameter (see `shared/model-migration.md`) | +| `budget_tokens` on Opus 4.8 / 4.7 | 400 | Use `thinking: {type: "adaptive"}` | +| `budget_tokens` >= `max_tokens` (older models) | 400 | Ensure `budget_tokens` < `max_tokens` | +| Typo in model ID | 404 | Use valid model ID like `claude-opus-4-8` | +| First message is `assistant` | 400 | First message must be `user` | +| Consecutive same-role messages | 400 | Alternate `user` and `assistant` | +| API key in code | 401 (leaked key) | Use environment variable | +| Custom retry needs | 429/5xx | SDK retries automatically; customize with `max_retries` | + +## Typed Exceptions in SDKs + +**Always use the SDK's typed exception classes** instead of checking error messages with string matching. Each HTTP error code maps to a specific exception class: + +| HTTP Code | TypeScript Class | Python Class | +| --------- | --------------------------------- | --------------------------------- | +| 400 | `Anthropic.BadRequestError` | `anthropic.BadRequestError` | +| 401 | `Anthropic.AuthenticationError` | `anthropic.AuthenticationError` | +| 403 | `Anthropic.PermissionDeniedError` | `anthropic.PermissionDeniedError` | +| 404 | `Anthropic.NotFoundError` | `anthropic.NotFoundError` | +| 413 | `Anthropic.RequestTooLargeError` | `anthropic.RequestTooLargeError` | +| 429 | `Anthropic.RateLimitError` | `anthropic.RateLimitError` | +| 500+ | `Anthropic.InternalServerError` | `anthropic.InternalServerError` | +| 529 | `Anthropic.OverloadedError` | `anthropic.OverloadedError` | +| Any | `Anthropic.APIError` | `anthropic.APIError` | + +```typescript +// ✅ Correct: use typed exceptions +try { + const response = await client.messages.create({...}); +} catch (error) { + if (error instanceof Anthropic.RateLimitError) { + // Handle rate limiting + } else if (error instanceof Anthropic.APIError) { + console.error(`API error ${error.status}:`, error.message); + } +} + +// ❌ Wrong: don't check error messages with string matching +try { + const response = await client.messages.create({...}); +} catch (error) { + const msg = error instanceof Error ? error.message : String(error); + if (msg.includes("429") || msg.includes("rate_limit")) { ... } +} +``` + +All exception classes extend `Anthropic.APIError`, which has a `status` property. Use `instanceof` checks from most specific to least specific (e.g., check `RateLimitError` before `APIError`). + +### Error `.type` Field + +All `APIStatusError` subclasses now expose a `.type` property (Python: `.type`, TypeScript: `.type`, Java: `.errorType()`, Go: `.Type()`, Ruby: `.type`, PHP: `.type`) that returns the API error type string (e.g., `"invalid_request_error"`, `"authentication_error"`, `"rate_limit_error"`, `"overloaded_error"`). Use this for programmatic error classification when you need finer granularity than the HTTP status code — for example, distinguishing `"billing_error"` from `"permission_error"` (both map to 403). + +```python +except anthropic.APIStatusError as e: + if e.type == "rate_limit_error": + # handle rate limiting + elif e.type == "overloaded_error": + # handle overload +``` diff --git a/.agents/skills/claude-api/shared/live-sources.md b/.agents/skills/claude-api/shared/live-sources.md new file mode 100644 index 00000000..c6a27343 --- /dev/null +++ b/.agents/skills/claude-api/shared/live-sources.md @@ -0,0 +1,140 @@ +# Live Documentation Sources + +This file contains WebFetch URLs for fetching current information from platform.claude.com and Agent SDK repositories. Use these when users need the latest data that may have changed since the cached content was last updated. + +## When to Use WebFetch + +- User explicitly asks for "latest" or "current" information +- Cached data seems incorrect +- User asks about features not covered in cached content +- User needs specific API details or examples + +## Claude API Documentation URLs + +### Models & Pricing + +| Topic | URL | Extraction Prompt | +| --------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| Models Overview | `https://platform.claude.com/docs/en/about-claude/models/overview.md` | "Extract current model IDs, context windows, and pricing for all Claude models" | +| Migration Guide | `https://platform.claude.com/docs/en/about-claude/models/migration-guide.md` | "Extract breaking changes, deprecated parameters, and per-model migration steps when moving to a newer Claude model" | +| Pricing | `https://platform.claude.com/docs/en/pricing.md` | "Extract current pricing per million tokens for input and output" | + +### Core Features + +| Topic | URL | Extraction Prompt | +| ----------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| Extended Thinking | `https://platform.claude.com/docs/en/build-with-claude/extended-thinking.md` | "Extract extended thinking parameters, budget_tokens requirements, and usage examples" | +| Adaptive Thinking | `https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking.md` | "Extract adaptive thinking setup, effort levels, and Claude Opus 4.8 usage examples" | +| Effort Parameter | `https://platform.claude.com/docs/en/build-with-claude/effort.md` | "Extract effort levels, cost-quality tradeoffs, and interaction with thinking" | +| Tool Use | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview.md` | "Extract tool definition schema, tool_choice options, and handling tool results" | +| Streaming | `https://platform.claude.com/docs/en/build-with-claude/streaming.md` | "Extract streaming event types, SDK examples, and best practices" | +| Prompt Caching | `https://platform.claude.com/docs/en/build-with-claude/prompt-caching.md` | "Extract cache_control usage, pricing benefits, and implementation examples" | + +### Media & Files + +| Topic | URL | Extraction Prompt | +| ----------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------- | +| Vision | `https://platform.claude.com/docs/en/build-with-claude/vision.md` | "Extract supported image formats, size limits, and code examples" | +| PDF Support | `https://platform.claude.com/docs/en/build-with-claude/pdf-support.md` | "Extract PDF handling capabilities, limits, and examples" | + +### API Operations + +| Topic | URL | Extraction Prompt | +| ---------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| Batch Processing | `https://platform.claude.com/docs/en/build-with-claude/batch-processing.md` | "Extract batch API endpoints, request format, and polling for results" | +| Files API | `https://platform.claude.com/docs/en/build-with-claude/files.md` | "Extract file upload, download, and referencing in messages, including supported types and beta header" | +| Token Counting | `https://platform.claude.com/docs/en/build-with-claude/token-counting.md` | "Extract token counting API usage and examples" | +| Rate Limits | `https://platform.claude.com/docs/en/api/rate-limits.md` | "Extract current rate limits by tier and model" | +| Errors | `https://platform.claude.com/docs/en/api/errors.md` | "Extract HTTP error codes, meanings, and retry guidance" | +| Amazon Bedrock | `https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock.md` | "Extract the AnthropicBedrockMantle client per language, `anthropic.`-prefixed model IDs, auth paths, feature availability, and regions" | +| Claude Platform on AWS | `https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws.md` | "Extract the AnthropicAWS client per language, SigV4 auth, credential precedence, short-term API keys, workspace_id, and region requirements" | +| Claude Platform on AWS — IAM actions | `https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions.md` | "Extract the IAM action names, resource ARNs, and policy examples required for each API capability" | + +### Tools + +| Topic | URL | Extraction Prompt | +| -------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Code Execution | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool.md` | "Extract code execution tool setup, file upload, container reuse, and response handling" | +| Computer Use | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use.md` | "Extract computer use tool setup, capabilities, and implementation examples" | +| Bash Tool | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool.md` | "Extract bash tool schema, reference implementation, and security considerations" | +| Text Editor | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool.md` | "Extract text editor tool commands, schema, and reference implementation" | +| Memory Tool | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool.md` | "Extract memory tool commands, directory structure, and implementation patterns" | +| Tool Search | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool.md` | "Extract tool search setup, when to use, and cache interaction" | +| Programmatic Tool Calling | `https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling.md` | "Extract PTC setup, script execution model, and tool invocation from code" | +| Skills | `https://platform.claude.com/docs/en/agents-and-tools/skills.md` | "Extract skill folder structure, SKILL.md format, and loading behavior" | + +### Advanced Features + +| Topic | URL | Extraction Prompt | +| ------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------- | +| Structured Outputs | `https://platform.claude.com/docs/en/build-with-claude/structured-outputs.md` | "Extract output_config.format usage and schema enforcement" | +| Compaction | `https://platform.claude.com/docs/en/build-with-claude/compaction.md` | "Extract compaction setup, trigger config, and streaming with compaction" | +| Context Editing | `https://platform.claude.com/docs/en/build-with-claude/context-editing.md` | "Extract context editing thresholds, what gets cleared, and configuration" | +| Citations | `https://platform.claude.com/docs/en/build-with-claude/citations.md` | "Extract citation format and implementation" | +| Context Windows | `https://platform.claude.com/docs/en/build-with-claude/context-windows.md` | "Extract context window sizes and token management" | + +### Managed Agents + +Use these when a managed-agents binding, behavior, or wire-level detail isn't covered in the cached `shared/managed-agents-*.md` concept files or in `{lang}/managed-agents/README.md`. + +| Topic | URL | Extraction Prompt | +| --------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| Overview | `https://platform.claude.com/docs/en/managed-agents/overview.md` | "Extract the high-level architecture and how agents/sessions/environments/vaults fit together" | +| Quickstart | `https://platform.claude.com/docs/en/managed-agents/quickstart.md` | "Extract the minimal end-to-end agent → environment → session → stream code path" | +| Agent Setup | `https://platform.claude.com/docs/en/managed-agents/agent-setup.md` | "Extract agent create/update/list-versions/archive lifecycle and parameters" | +| Define Outcomes | `https://platform.claude.com/docs/en/managed-agents/define-outcomes.md` | "Extract outcome definitions, evaluation hooks, and success criteria configuration" | +| Sessions | `https://platform.claude.com/docs/en/managed-agents/sessions.md` | "Extract session lifecycle, status transitions, idle/terminated semantics, and resume rules" | +| Environments | `https://platform.claude.com/docs/en/managed-agents/environments.md` | "Extract environment config (cloud/networking), management endpoints, and reuse model" | +| Self-Hosted Sandboxes | `https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes.md` | "Extract config:{type:self_hosted}, ANTHROPIC_ENVIRONMENT_KEY, EnvironmentWorker.run/run_one, beta_agent_toolset, ant beta:worker poll/run, webhook-driven wake" | +| Self-Hosted Sandboxes — Security | `https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-security.md` | "Extract what the customer owns (hardening, egress, key custody, trust boundaries) vs what Anthropic cannot do" | +| Events and Streaming | `https://platform.claude.com/docs/en/managed-agents/events-and-streaming.md` | "Extract event stream types, stream-first ordering, reconnect/dedupe, and steering patterns" | +| Tools | `https://platform.claude.com/docs/en/managed-agents/tools.md` | "Extract built-in toolset, custom tool definitions, and tool result wire format" | +| Files | `https://platform.claude.com/docs/en/managed-agents/files.md` | "Extract file upload, mount paths, session resources, and listing/downloading session outputs" | +| Permission Policies | `https://platform.claude.com/docs/en/managed-agents/permission-policies.md` | "Extract permission policy types (allow/deny/confirm) and per-tool config" | +| Multi-Agent | `https://platform.claude.com/docs/en/managed-agents/multi-agent.md` | "Extract multi-agent composition patterns, sub-agent invocation, and result handoff" | +| Observability | `https://platform.claude.com/docs/en/managed-agents/observability.md` | "Extract logging, tracing, and usage telemetry exposed by managed agents" | +| Webhooks | `https://platform.claude.com/docs/en/managed-agents/webhooks.md` | "Extract webhook endpoint registration, HMAC signature verification, supported event types, and delivery semantics" | +| GitHub | `https://platform.claude.com/docs/en/managed-agents/github.md` | "Extract github_repository resource shape, multi-repo mounting, and token rotation" | +| MCP Connector | `https://platform.claude.com/docs/en/managed-agents/mcp-connector.md` | "Extract MCP server declaration on agents and vault-based credential injection at session" | +| Vaults | `https://platform.claude.com/docs/en/managed-agents/vaults.md` | "Extract vault create, credential add/rotate, OAuth refresh shape, and archive" | +| Skills | `https://platform.claude.com/docs/en/managed-agents/skills.md` | "Extract skill packaging and loading model for managed agents" | +| Memory | `https://platform.claude.com/docs/en/managed-agents/memory.md` | "Extract memory resource shape, scoping, and lifecycle" | +| Onboarding | `https://platform.claude.com/docs/en/managed-agents/onboarding.md` | "Extract first-run setup, prerequisites, and account/region requirements" | +| Cloud Containers | `https://platform.claude.com/docs/en/managed-agents/cloud-containers.md` | "Extract cloud container runtime, image config, and network/storage knobs" | +| Migration | `https://platform.claude.com/docs/en/managed-agents/migration.md` | "Extract migration paths from earlier APIs/preview shapes to GA managed agents" | + +### Anthropic CLI + +The `ant` CLI provides terminal access to the Claude API. Every API resource is exposed as a subcommand. It is one convenient way to create agents, environments, sessions, and other resources from version-controlled YAML, and to inspect responses interactively. + +| Topic | URL | Extraction Prompt | +| ------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| Anthropic CLI | `https://platform.claude.com/docs/en/api/sdks/cli.md` | "Extract CLI install, authentication, command structure, and the beta:agents/environments/sessions commands" | +| Authentication overview | `https://platform.claude.com/docs/en/manage-claude/authentication.md` | "Extract the credential options (API keys, interactive OAuth login, Workload Identity Federation) and when to use each" | +| WIF reference | `https://platform.claude.com/docs/en/manage-claude/wif-reference.md` | "Extract credential precedence order, the profile configuration file schema, and the configuration directory layout" | + +--- + +## Claude API SDK Repositories + +WebFetch these when a binding (class, method, namespace, field) isn't covered in the cached `{lang}/` skill files or in the managed-agents docs above. The SDKs include beta managed-agents support for `/v1/agents`, `/v1/sessions`, `/v1/environments`, and related resources — search the repo for `BetaManagedAgents`, `beta.agents`, `beta.sessions`, or the equivalent namespace for that language. + +| SDK | URL | Extraction Prompt | +| ---------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| Python | `https://github.com/anthropics/anthropic-sdk-python` | "Extract beta managed-agents namespaces, classes, and method signatures (`client.beta.agents`, `client.beta.sessions`)" | +| TypeScript | `https://github.com/anthropics/anthropic-sdk-typescript` | "Extract beta managed-agents namespaces, classes, and method signatures (`client.beta.agents`, `client.beta.sessions`)" | +| Java | `https://github.com/anthropics/anthropic-sdk-java` | "Extract beta managed-agents classes, builders, and method signatures (`client.beta().agents()`, `BetaManagedAgents*`)" | +| Go | `https://github.com/anthropics/anthropic-sdk-go` | "Extract beta managed-agents types and method signatures (`client.Beta.Agents`, `BetaManagedAgents*` event types)" | +| Ruby | `https://github.com/anthropics/anthropic-sdk-ruby` | "Extract beta managed-agents methods and parameter shapes (`client.beta.agents`, `client.beta.sessions`)" | +| C# | `https://github.com/anthropics/anthropic-sdk-csharp` | "Extract beta managed-agents classes and method signatures (NuGet package, `BetaManagedAgents*` types)" | +| PHP | `https://github.com/anthropics/anthropic-sdk-php` | "Extract beta managed-agents classes and method signatures (`$client->beta->agents`, `BetaManagedAgents*` params)" | + +--- + +## Fallback Strategy + +If WebFetch fails (network issues, URL changed): + +1. Use cached content from the language-specific files (note the cache date) +2. Inform user the data may be outdated +3. Suggest they check platform.claude.com or the GitHub repos directly diff --git a/.agents/skills/claude-api/shared/managed-agents-api-reference.md b/.agents/skills/claude-api/shared/managed-agents-api-reference.md new file mode 100644 index 00000000..d627dcd0 --- /dev/null +++ b/.agents/skills/claude-api/shared/managed-agents-api-reference.md @@ -0,0 +1,378 @@ +# Managed Agents — Endpoint Reference + +All endpoints require `x-api-key` and `anthropic-version: 2023-06-01` headers. Managed Agents endpoints additionally require the `anthropic-beta` header. + +## Beta Headers + +``` +anthropic-beta: managed-agents-2026-04-01 +``` + +The SDK adds this header automatically for all `client.beta.{agents,environments,sessions,vaults,memory_stores}.*` calls. Skills endpoints use `skills-2025-10-02`; Files endpoints use `files-api-2025-04-14`. + +--- + +## SDK Method Reference + +All resources are under the `beta` namespace. Python and TypeScript share identical method names. + +| Resource | Python / TypeScript (`client.beta.*`) | Go (`client.Beta.*`) | +| --- | --- | --- | +| Agents | `agents.create` / `retrieve` / `update` / `list` / `archive` | `Agents.New` / `Get` / `Update` / `List` / `Archive` | +| Agent Versions | `agents.versions.list` | `Agents.Versions.List` | +| Environments | `environments.create` / `retrieve` / `update` / `list` / `delete` / `archive` | `Environments.New` / `Get` / `Update` / `List` / `Delete` / `Archive` | +| Environment Work (self-hosted) | `environments.work.poller` / `stats` / `stop` | See `shared/managed-agents-self-hosted-sandboxes.md` | +| Sessions | `sessions.create` / `retrieve` / `update` / `list` / `delete` / `archive` | `Sessions.New` / `Get` / `Update` / `List` / `Delete` / `Archive` | +| Session Events | `sessions.events.list` / `send` / `stream` | `Sessions.Events.List` / `Send` / `StreamEvents` | +| Session Threads | `sessions.threads.list` / `retrieve` / `archive`; `sessions.threads.events.list` / `stream` | `Sessions.Threads.List` / `Get` / `Archive`; `Sessions.Threads.Events.List` / `StreamEvents` | +| Session Resources | `sessions.resources.add` / `retrieve` / `update` / `list` / `delete` | `Sessions.Resources.Add` / `Get` / `Update` / `List` / `Delete` | +| Vaults | `vaults.create` / `retrieve` / `update` / `list` / `delete` / `archive` | `Vaults.New` / `Get` / `Update` / `List` / `Delete` / `Archive` | +| Credentials | `vaults.credentials.create` / `retrieve` / `update` / `list` / `delete` / `archive` / `mcp_oauth_validate` | `Vaults.Credentials.New` / `Get` / `Update` / `List` / `Delete` / `Archive` / `McpOauthValidate` | +| Memory Stores | `memory_stores.create` / `retrieve` / `update` / `list` / `delete` / `archive` | `MemoryStores.New` / `Get` / `Update` / `List` / `Delete` / `Archive` | +| Memories | `memory_stores.memories.create` / `retrieve` / `update` / `list` / `delete` | `MemoryStores.Memories.New` / `Get` / `Update` / `List` / `Delete` | +| Memory Versions | `memory_stores.memory_versions.list` / `retrieve` / `redact` | `MemoryStores.MemoryVersions.List` / `Get` / `Redact` | + +**Naming quirks to watch for:** +- Agents and Session Threads have **no delete** — only `archive`. Archive is **permanent**: the agent becomes read-only, new sessions cannot reference it, and there is no unarchive. Confirm with the user before archiving a production agent. Environments, Sessions, Vaults, Credentials, and Memory Stores have both `delete` and `archive`; Session Resources, Files, Skills, and Memories are `delete`-only; Memory Versions have neither — only `redact`. +- Session resources use `add` (not `create`). +- Go's event stream is `StreamEvents` (not `Stream`). +- The self-hosted worker is **not** under `client.beta.*` — it's `EnvironmentWorker` from `anthropic.lib.environments` / `@anthropic-ai/sdk/helpers/beta/environments`; only `environments.work.poller/stats/stop` are client methods. + +**Agent shorthand:** `agent` on session create accepts either a bare string (`agent="agent_abc123"` — uses latest version) or the full reference object (`{type: "agent", id: "agent_abc123", version: 123}`). + +**Model shorthand:** `model` on agent create accepts either a bare string (`model="claude-opus-4-8"` — uses `standard` speed) or the full config object (`{id: "claude-opus-4-6", speed: "fast"}`). Note: `speed: "fast"` is only supported on Opus 4.6. + +--- + +## Agents + +**Step one of every flow.** Sessions require a pre-created agent — there is no inline agent config under `managed-agents-2026-04-01`. + +| Method | Path | Operation | Description | +| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- | +| `GET` | `/v1/agents` | ListAgents | List agents | +| `POST` | `/v1/agents` | CreateAgent | Create a saved agent configuration | +| `GET` | `/v1/agents/{agent_id}` | GetAgent | Get agent details | +| `POST` | `/v1/agents/{agent_id}` | UpdateAgent | Update agent configuration | +| `POST` | `/v1/agents/{agent_id}/archive` | ArchiveAgent | Archive an agent. Makes it **read-only**; existing sessions continue, new sessions cannot reference it. No unarchive — this is the terminal state. | +| `GET` | `/v1/agents/{agent_id}/versions` | ListAgentVersions | List agent versions | + +## Sessions + +| Method | Path | Operation | Description | +| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- | +| `GET` | `/v1/sessions` | ListSessions | List sessions (paginated) | +| `POST` | `/v1/sessions` | CreateSession | Create a new session | +| `GET` | `/v1/sessions/{session_id}` | GetSession | Get session details | +| `POST` | `/v1/sessions/{session_id}` | UpdateSession | Update session `metadata`/`title`, or `agent.tools`/`agent.mcp_servers`/`vault_ids` (session-local override; session must be `idle`). See `shared/managed-agents-core.md` → Updating the agent configuration mid-session. | +| `DELETE` | `/v1/sessions/{session_id}` | DeleteSession | Delete a session | +| `POST` | `/v1/sessions/{session_id}/archive` | ArchiveSession | Archive a session | + +## Events + +| Method | Path | Operation | Description | +| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- | +| `GET` | `/v1/sessions/{session_id}/events` | ListEvents | List events (polling, paginated) | +| `POST` | `/v1/sessions/{session_id}/events` | SendEvents | Send events (user message, tool result) | +| `GET` | `/v1/sessions/{session_id}/events/stream` | StreamEvents | Stream events via SSE | + +## Session Threads + +Per-subagent event streams in multiagent sessions. See `shared/managed-agents-multiagent.md`. + +| Method | Path | Operation | Description | +| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- | +| `GET` | `/v1/sessions/{session_id}/threads` | ListThreads | List threads (paginated) | +| `GET` | `/v1/sessions/{session_id}/threads/{thread_id}` | GetThread | Retrieve one thread (carries `agent` snapshot, `status`, `parent_thread_id`, `stats`, `usage`) | +| `POST` | `/v1/sessions/{session_id}/threads/{thread_id}/archive` | ArchiveThread | Archive a thread | +| `GET` | `/v1/sessions/{session_id}/threads/{thread_id}/events` | ListThreadEvents | List past events for one thread (paginated) | +| `GET` | `/v1/sessions/{session_id}/threads/{thread_id}/stream` | StreamThreadEvents | Stream one thread via SSE (SDK: `threads.events.stream`) | + +## Session Resources + +| Method | Path | Operation | Description | +| -------- | ------------------------------------------------------- | ---------------- | ---------------------------------------- | +| `GET` | `/v1/sessions/{session_id}/resources` | ListResources | List resources attached to session | +| `POST` | `/v1/sessions/{session_id}/resources` | AddResource | Attach `file` or `github_repository` resource (SDK method: `add`, not `create`). `memory_store` resources attach at session-create time only. | +| `GET` | `/v1/sessions/{session_id}/resources/{resource_id}` | GetResource | Get a single resource | +| `POST` | `/v1/sessions/{session_id}/resources/{resource_id}` | UpdateResource | Update resource | +| `DELETE` | `/v1/sessions/{session_id}/resources/{resource_id}` | DeleteResource | Remove resource from session | + +## Environments + +| Method | Path | Operation | Description | +| -------- | ---------------------------------------------------------------- | -------------------- | ----------------------------------- | +| `POST` | `/v1/environments` | CreateEnvironment | Create environment | +| `GET` | `/v1/environments` | ListEnvironments | List environments | +| `GET` | `/v1/environments/{environment_id}` | GetEnvironment | Get environment details | +| `POST` | `/v1/environments/{environment_id}` | UpdateEnvironment | Update environment | +| `DELETE` | `/v1/environments/{environment_id}` | DeleteEnvironment | Delete environment. Returns 204. | +| `POST` | `/v1/environments/{environment_id}/archive` | ArchiveEnvironment | Archive environment. Makes it **read-only**; existing sessions continue, new sessions cannot reference it. No unarchive — this is the terminal state. | +| `GET` | `/v1/environments/{environment_id}/work/stats` | WorkQueueStats | Self-hosted work-queue depth/pending/workers. `x-api-key` auth. See `shared/managed-agents-self-hosted-sandboxes.md`. | +| `POST` | `/v1/environments/{environment_id}/work/{work_id}/stop` | StopWork | Self-hosted: stop a claimed work item. `x-api-key` auth. | + +For `type: "self_hosted"`, `config` is the bare `{"type": "self_hosted"}` — `networking` and `packages` do not apply. + +## Vaults + +Vaults store MCP credentials that Anthropic manages on your behalf — OAuth credentials with auto-refresh, or static bearer tokens. Attach to sessions via `vault_ids`. See `managed-agents-tools.md` §Vaults for the conceptual guide and credential shapes. + +| Method | Path | Operation | Description | +| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- | +| `POST` | `/v1/vaults` | CreateVault | Create a vault | +| `GET` | `/v1/vaults` | ListVaults | List vaults | +| `GET` | `/v1/vaults/{vault_id}` | GetVault | Get vault details | +| `POST` | `/v1/vaults/{vault_id}` | UpdateVault | Update vault | +| `DELETE` | `/v1/vaults/{vault_id}` | DeleteVault | Delete vault | +| `POST` | `/v1/vaults/{vault_id}/archive` | ArchiveVault | Archive vault | + +## Credentials + +Credentials are individual secrets stored inside a vault. + +| Method | Path | Operation | Description | +| -------- | ----------------------------------------------------------------- | ------------------ | ---------------------------- | +| `POST` | `/v1/vaults/{vault_id}/credentials` | CreateCredential | Create a credential | +| `GET` | `/v1/vaults/{vault_id}/credentials` | ListCredentials | List credentials in vault | +| `GET` | `/v1/vaults/{vault_id}/credentials/{credential_id}` | GetCredential | Get credential metadata | +| `POST` | `/v1/vaults/{vault_id}/credentials/{credential_id}` | UpdateCredential | Update credential | +| `DELETE` | `/v1/vaults/{vault_id}/credentials/{credential_id}` | DeleteCredential | Delete credential | +| `POST` | `/v1/vaults/{vault_id}/credentials/{credential_id}/archive` | ArchiveCredential | Archive credential | +| `POST` | `/v1/vaults/{vault_id}/credentials/{credential_id}/mcp_oauth_validate` | McpOauthValidate | Validate an MCP OAuth credential | + +## Memory Stores + +Workspace-scoped persistent memory that survives across sessions. Attach to a session via a `{"type": "memory_store", "memory_store_id": ...}` entry in `resources[]` (session-create time only). See `shared/managed-agents-memory.md` for the conceptual guide, the FUSE-mount agent interface, preconditions, and versioning. + +| Method | Path | Operation | Description | +| -------- | ------------------------------------------------ | ------------------ | ---------------------------------------- | +| `POST` | `/v1/memory_stores` | CreateMemoryStore | Create a store (`name`, `description`, `metadata`) | +| `GET` | `/v1/memory_stores` | ListMemoryStores | List stores (`include_archived`, `created_at_{gte,lte}`) | +| `GET` | `/v1/memory_stores/{memory_store_id}` | GetMemoryStore | Get store details | +| `POST` | `/v1/memory_stores/{memory_store_id}` | UpdateMemoryStore | Update store | +| `DELETE` | `/v1/memory_stores/{memory_store_id}` | DeleteMemoryStore | Delete store | +| `POST` | `/v1/memory_stores/{memory_store_id}/archive` | ArchiveMemoryStore | Archive store. Makes it **read-only**; existing sessions continue, new sessions cannot reference it. No unarchive. | + +## Memories + +Individual text documents inside a store (≤ 100KB each). `create` creates at a `path` and returns `409` (`memory_path_conflict_error`, with `conflicting_memory_id`) if the path is occupied; `update` mutates by `mem_...` ID (rename and/or content). Only `update` accepts a `precondition` (`{"type": "content_sha256", "content_sha256": ...}`) — on mismatch returns `409` (`memory_precondition_failed_error`). List endpoints accept `view: "basic"|"full"` (controls whether `content` is populated; `retrieve` defaults to `full`). + +| Method | Path | Operation | Description | +| -------- | ----------------------------------------------------------------- | -------------- | ---------------------------------------- | +| `GET` | `/v1/memory_stores/{memory_store_id}/memories` | ListMemories | Returns `Memory \| MemoryPrefix`; filter by `path_prefix`, `depth`, `order_by`/`order` | +| `POST` | `/v1/memory_stores/{memory_store_id}/memories` | CreateMemory | Create at `path` (SDK: `memories.create`); `409 memory_path_conflict_error` if occupied | +| `GET` | `/v1/memory_stores/{memory_store_id}/memories/{memory_id}` | GetMemory | Read one memory (defaults to `view="full"`) | +| `PATCH` | `/v1/memory_stores/{memory_store_id}/memories/{memory_id}` | UpdateMemory | Change `content`, `path`, or both by ID; optional `precondition` | +| `DELETE` | `/v1/memory_stores/{memory_store_id}/memories/{memory_id}` | DeleteMemory | Delete (optional `expected_content_sha256`) | + +## Memory Versions + +Immutable per-mutation snapshots (`memver_...`) — the audit and rollback surface. `operation` ∈ `created` / `modified` / `deleted`. + +| Method | Path | Operation | Description | +| -------- | ----------------------------------------------------------------------------- | --------------------- | ---------------------------------------- | +| `GET` | `/v1/memory_stores/{memory_store_id}/memory_versions` | ListMemoryVersions | Newest-first; filter by `memory_id`, `operation`, `session_id`, `api_key_id`, `created_at_{gte,lte}` | +| `GET` | `/v1/memory_stores/{memory_store_id}/memory_versions/{version_id}` | GetMemoryVersion | List fields + full `content` | +| `POST` | `/v1/memory_stores/{memory_store_id}/memory_versions/{version_id}/redact` | RedactMemoryVersion | Clear `content`/`content_sha256`/`content_size_bytes`/`path`; preserve actor + timestamps | + +## Files + +| Method | Path | Operation | Description | +| -------- | ------------------------------------------------ | ---------------- | ---------------------------------------- | +| `POST` | `/v1/files` | UploadFile | Upload a file | +| `GET` | `/v1/files` | ListFiles | List files | +| `GET` | `/v1/files/{file_id}` | GetFile | Get file metadata (SDK method: `retrieve_metadata`) | +| `GET` | `/v1/files/{file_id}/content` | DownloadFile | Download file content | +| `DELETE` | `/v1/files/{file_id}` | DeleteFile | Delete a file | + +## Skills + +| Method | Path | Operation | Description | +| -------- | --------------------------------------------------------------- | ------------------ | ---------------------------- | +| `POST` | `/v1/skills` | CreateSkill | Create a skill | +| `GET` | `/v1/skills` | ListSkills | List skills | +| `GET` | `/v1/skills/{skill_id}` | GetSkill | Get skill details | +| `DELETE` | `/v1/skills/{skill_id}` | DeleteSkill | Delete a skill | +| `POST` | `/v1/skills/{skill_id}/versions` | CreateVersion | Create skill version | +| `GET` | `/v1/skills/{skill_id}/versions` | ListVersions | List skill versions | +| `GET` | `/v1/skills/{skill_id}/versions/{version}` | GetVersion | Get skill version | +| `DELETE` | `/v1/skills/{skill_id}/versions/{version}` | DeleteVersion | Delete skill version | + +--- + +## Request/Response Schema Quick Reference + +### CreateAgent Request Body + +**Always start here.** `model`, `system`, `tools`, `mcp_servers`, `skills` are top-level fields on this object — they do NOT go on the session. + +```json +{ + "name": "string (required, 1-256 chars)", + "model": "claude-opus-4-8 (required — bare string, or {id, speed} object)", + "description": "string (optional, up to 2048 chars)", + "system": "string (optional, up to 100,000 chars)", + "tools": [ + { "type": "agent_toolset_20260401" } + ], + "skills": [ + { "type": "anthropic", "skill_id": "xlsx" }, + { "type": "custom", "skill_id": "skill_abc123", "version": "1" } + ], + "mcp_servers": [ + { + "type": "url", + "name": "github", + "url": "https://api.githubcopilot.com/mcp/" + } + ], + "multiagent": { + "type": "coordinator", + "agents": [ + "agent_abc123", + { "type": "agent", "id": "agent_def456", "version": 4 }, + { "type": "self" } + ] + }, + "metadata": { + "key": "value (max 16 pairs, keys ≤64 chars, values ≤512 chars)" + } +} +``` + +> Limits: `tools` max 128, `skills` max 20, `mcp_servers` max 20 (unique names). `multiagent.agents` 1–20 entries (string ID | `{type:"agent",id,version?}` | `{type:"self"}`) — see `shared/managed-agents-multiagent.md`. + +### CreateSession Request Body + +```json +{ + "agent": "agent_abc123 (required — string shorthand for latest version, or {type: \"agent\", id, version} object)", + "environment_id": "env_abc123 (required)", + "title": "string (optional)", + "resources": [ + { + "type": "github_repository", + "url": "https://github.com/owner/repo (required)", + "authorization_token": "ghp_... (required)", + "mount_path": "/workspace/repo (optional — defaults to /workspace/)", + "checkout": { "type": "branch", "name": "main" } + } + ], + "vault_ids": ["vlt_abc123 (optional — MCP credentials with auto-refresh)"], + "metadata": { + "key": "value" + } +} +``` + +> The `agent` field accepts only a string ID or `{type: "agent", id, version}` — `model`/`system`/`tools` live on the agent, not here. +> +> **`checkout`** accepts `{type: "branch", name: "..."}` or `{type: "commit", sha: "..."}`. Omit for the repo's default branch. + +### CreateEnvironment Request Body + +```json +{ + "name": "string (required)", + "description": "string (optional)", + "config": { + "type": "cloud | self_hosted", + "networking": { + "type": "unrestricted | limited (union — see SDK types)" + }, + "packages": { } + }, + "metadata": { "key": "value" } +} +``` + +### SendEvents Request Body + +```json +{ + "events": [ + { + "type": "user.message", + "content": [ + { + "type": "text", + "text": "Hello" + } + ] + } + ] +} +``` + +### Define Outcome Event + +```json +{ + "type": "user.define_outcome", + "description": "Build a DCF model for Costco in .xlsx", + "rubric": { "type": "file", "file_id": "file_01..." }, + "max_iterations": 5 +} +``` + +> `rubric` is required: `{type: "text", content}` or `{type: "file", file_id}`. `max_iterations` default 3, max 20. Echoed back with `outcome_id` + `processed_at`. See `shared/managed-agents-outcomes.md`. + +### Tool Result Event + +```json +{ + "type": "user.custom_tool_result", + "custom_tool_use_id": "sevt_abc123", + "content": [{ "type": "text", "text": "Result data" }], + "is_error": false +} +``` + +--- + +## Error Handling + +Managed Agents endpoints use the standard Anthropic API error format. Errors are returned with an HTTP status code and a JSON body containing `type`, `error`, and `request_id`: + +```json +{ + "type": "error", + "error": { + "type": "invalid_request_error", + "message": "Description of what went wrong" + }, + "request_id": "req_011CRv1W3XQ8XpFikNYG7RnE" +} +``` + +Include the `request_id` when reporting issues to Anthropic — it lets us trace the request end-to-end. The inner `error.type` is one of the following: + +| Status | Error type | Description | +|---|---|---| +| 400 | `invalid_request_error` | The request was malformed or missing required parameters | +| 401 | `authentication_error` | Invalid or missing API key | +| 403 | `permission_error` | The API key doesn't have permission for this operation | +| 404 | `not_found_error` | The requested resource doesn't exist | +| 409 | `invalid_request_error` | The request conflicts with the resource's current state (e.g., sending to an archived session) | +| 413 | `request_too_large` | The request body exceeds the maximum allowed size | +| 429 | `rate_limit_error` | Too many requests — check rate limit headers for retry timing | +| 500 | `api_error` | An internal server error occurred | +| 529 | `overloaded_error` | The service is temporarily overloaded — retry with backoff | + +Note that `409 Conflict` carries `error.type: "invalid_request_error"` (there is no separate `conflict_error` type); inspect both the HTTP status and the `message` to distinguish conflicts from other invalid requests. + +--- + +## Rate Limits + +Managed Agents endpoints have per-organization request-per-minute (RPM) limits, separate from your [Messages API token limits](https://platform.claude.com/docs/en/api/rate-limits). Model inference inside a session still draws from your organization's standard ITPM/OTPM limits. + +| Endpoint group | Scope | RPM | Max concurrent | +|---|---|---|---| +| Create operations (Agents, Sessions, Vaults) | organization | 300 | — | +| All other operations (Agents, Sessions, Vaults) | organization | 600 | — | +| All operations (Environments) | organization | 60 | 5 | + +Files and Skills endpoints use the standard tier-based [rate limits](https://platform.claude.com/docs/en/api/rate-limits). + +When a limit is exceeded the API returns `429` with a `rate_limit_error` (see [Error Handling](#error-handling) for the response envelope) and a `retry-after` header indicating how many seconds to wait before retrying. The Anthropic SDK reads this header and retries automatically. diff --git a/.agents/skills/claude-api/shared/managed-agents-client-patterns.md b/.agents/skills/claude-api/shared/managed-agents-client-patterns.md new file mode 100644 index 00000000..ac0e60b0 --- /dev/null +++ b/.agents/skills/claude-api/shared/managed-agents-client-patterns.md @@ -0,0 +1,209 @@ +# Managed Agents — Common Client Patterns + +Patterns you'll write on the client side when driving a Managed Agent session, grounded in working SDK examples. + +Code samples are TypeScript — Python and cURL follow the same shape; see `python/managed-agents/README.md` and `curl/managed-agents.md` for equivalents. + +--- + +## 1. Lossless stream reconnect + +**Problem:** SSE has no replay. If the connection drops mid-session, a naive reconnect re-opens the stream from "now" and you silently miss every event emitted in between. + +**Solution:** on reconnect, fetch the full event history via `events.list()` *before* consuming the live stream, and dedupe on event ID as the live stream catches up. + +```ts +const seenEventIds = new Set() +const stream = await client.beta.sessions.events.stream(session.id) + +// Stream is now open and buffering server-side. Read history first. +for await (const event of client.beta.sessions.events.list(session.id)) { + seenEventIds.add(event.id) + handle(event) +} + +// Tail the live stream. Dedupe only gates handle() — terminal checks must run +// even for already-seen events, or a terminal event that was in the history +// response gets skipped by `continue` and the loop never exits. +for await (const event of stream) { + if (!seenEventIds.has(event.id)) { + seenEventIds.add(event.id) + handle(event) + } + if (event.type === 'session.status_terminated') break + if (event.type === 'session.status_idle' && event.stop_reason.type !== 'requires_action') break +} +``` + +--- + +## 2. `processed_at` — queued vs processed + +Every event on the stream carries `processed_at` (ISO 8601). For client-sent events (`user.message`, `user.interrupt`, `user.tool_confirmation`, `user.custom_tool_result`) it's `null` when the event has been queued but not yet picked up by the agent, and populated once the agent processes it. The same event appears on the stream twice — once with `processed_at: null`, once with a timestamp. + +```ts +for await (const event of stream) { + if (event.type === 'user.message') { + if (event.processed_at == null) onQueued(event.id) + else onProcessed(event.id, event.processed_at) + } +} +``` + +Use this to drive pending → acknowledged UI state for anything you send. How you map a locally-rendered optimistic message to the server-assigned `event.id` is application-specific (typically via the return value of `events.send()` or FIFO ordering). + +--- + +## 3. Interrupt a running session + +Send `user.interrupt` as a normal event. The session keeps running until it reaches a safe boundary, then goes idle. + +```ts +await client.beta.sessions.events.send(session.id, { + events: [{ type: 'user.interrupt' }], +}) + +// Drain until the session is truly done — see Pattern 5 for the full gate. +for await (const event of stream) { + if (event.type === 'session.status_terminated') break + if ( + event.type === 'session.status_idle' && + event.stop_reason.type !== 'requires_action' + ) break +} +``` + +Reference: `interrupt.ts` — sends the interrupt the moment it sees `span.model_request_start`, drains to idle, then verifies via `sessions.retrieve()`. + +--- + +## 4. `tool_confirmation` round-trip + +When the agent has `permission_policy: { type: 'always_ask' }`, any call to that tool fires an `agent.tool_use` event with `evaluated_permission === 'ask'` and the session goes idle waiting for a decision. Respond with `user.tool_confirmation`. + +```ts +for await (const event of stream) { + if (event.type === 'agent.tool_use' && event.evaluated_permission === 'ask') { + await client.beta.sessions.events.send(session.id, { + events: [{ + type: 'user.tool_confirmation', + tool_use_id: event.id, // not a toolu_ id — use event.id + result: 'allow', // or 'deny' + // deny_message: '...', // optional, only with result: 'deny' + }], + }) + } +} +``` + +Key points: +- `tool_use_id` is `event.id` (typically `sevt_...`), **not** a `toolu_...` ID. +- `result` is `'allow' | 'deny'`. Use `deny_message` to tell the model *why* you denied — it gets surfaced back to the agent. +- Multiple pending tools: respond once per `agent.tool_use` event with `evaluated_permission === 'ask'`. + +Reference: `tool-permissions.ts`. + +--- + +## 5. Correct idle-break gate + +Do not break on `session.status_idle` alone. The session goes idle transiently — e.g. between parallel tool executions, while waiting for a `user.tool_confirmation`, or while awaiting a `user.custom_tool_result`. Break when idle with a terminal `stop_reason`, or on `session.status_terminated`. + +```ts +for await (const event of stream) { + handle(event) + if (event.type === 'session.status_terminated') break + if (event.type === 'session.status_idle') { + if (event.stop_reason.type === 'requires_action') continue // waiting on you — handle it + break // end_turn or retries_exhausted — both terminal + } +} +``` + +`stop_reason.type` values on `session.status_idle`: +- `requires_action` — agent is waiting on a client-side event (tool confirmation, custom tool result). Handle it, don't break. +- `retries_exhausted` — terminal failure. Break, then check `sessions.retrieve()` for the error state. +- `end_turn` — normal completion. + +--- + +## 6. Post-idle status-write race + +The SSE stream emits `session.status_idle` slightly before the session's queryable status reflects it. Clients that break on idle and immediately call `sessions.delete()` or `sessions.archive()` will intermittently 400 with "cannot delete/archive while running." + +Poll before cleanup: + +```ts +let s +for (let i = 0; i < 10; i++) { + s = await client.beta.sessions.retrieve(session.id) + if (s.status !== 'running') break + await new Promise(r => setTimeout(r, 200)) +} +if (s?.status !== 'running') { + await client.beta.sessions.archive(session.id) +} // else: still running after 2s — don't archive, let it settle or escalate +``` + +--- + +## 7. Stream-first, then send + +Always open the stream **before** sending the kickoff event. Otherwise the agent may process the event and emit the first events before your consumer is attached, and you'll miss them. + +```ts +const stream = await client.beta.sessions.events.stream(session.id) +await client.beta.sessions.events.send(session.id, { + events: [{ type: 'user.message', content: [{ type: 'text', text: 'Hello' }] }], +}) +for await (const event of stream) { /* ... */ } +``` + +The `Promise.all([stream, send])` shape works too, but stream-first is simpler and has the same effect — the stream starts buffering the moment it's opened. + +--- + +## 8. File-mount gotchas + +**The mounted resource has a different `file_id` than the file you uploaded.** Session creation makes a session-scoped copy. + +```ts +const uploaded = await client.beta.files.upload({ file }) +// uploaded.id → the original file +const session = await client.beta.sessions.create({ + /* ... */ + resources: [{ type: 'file', file_id: uploaded.id, mount_path: '/workspace/data.csv' }], +}) +// session.resources[0].file_id !== uploaded.id ← different IDs +``` + +Delete the original via `files.delete(uploaded.id)`; the session-scoped copy is garbage-collected with the session. `mount_path` must be absolute — see `shared/managed-agents-environments.md`. + +--- + +## 9. Secrets for non-MCP APIs and CLIs — keep them host-side via custom tools + +**Problem:** you want the agent to call a third-party API or run a CLI that needs a secret (API key, token, service-account credential), but there is currently no way to set environment variables inside the session container, and vaults currently hold MCP credentials only — they are not exposed to the container's shell. So `curl`, installed CLIs, or SDK clients running via the `bash` tool have no first-class place to read a secret from. + +**Solution:** move the authenticated call to your side. Declare a custom tool on the agent; when the agent emits `agent.custom_tool_use`, your orchestrator (the process reading the SSE stream) executes the call with its own credentials and responds with `user.custom_tool_result`. The container never sees the key. + +```ts +// Agent template: declare the tool, no credentials +tools: [{ type: 'custom', name: 'linear_graphql', input_schema: { /* query, vars */ } }] + +// Orchestrator: handle the call with host-side creds +for await (const event of stream) { + if (event.type === 'agent.custom_tool_use' && event.name === 'linear_graphql') { + const result = await linear.request(event.input.query, event.input.vars) // host's key + await client.beta.sessions.events.send(session.id, { + events: [{ type: 'user.custom_tool_result', tool_use_id: event.id, result }], + }) + } +} +``` + +Same shape works for `gh` CLI, local eval scripts, or anything else that needs host-side auth or binaries. + +**Security note:** this does not expose a public endpoint. `agent.custom_tool_use` arrives on the SSE stream your orchestrator already holds open with your Anthropic API key, and `user.custom_tool_result` goes back via `events.send()` under the same key. Your orchestrator is a client, not a server — nothing unauthenticated is listening. + +**Do not embed API keys in the system prompt or user messages as a workaround.** Prompts and messages are stored in the session's event history, returned by `events.list()`, and included in compaction summaries — a secret placed there is durably persisted and readable via the API for the life of the session. diff --git a/.agents/skills/claude-api/shared/managed-agents-core.md b/.agents/skills/claude-api/shared/managed-agents-core.md new file mode 100644 index 00000000..4fa930ba --- /dev/null +++ b/.agents/skills/claude-api/shared/managed-agents-core.md @@ -0,0 +1,252 @@ +# Managed Agents — Core Concepts + +## Architecture + +Managed Agents is built around four core concepts: + +| Concept | Endpoint | What it is | +|---|---|---| +| **Agent** | `/v1/agents` | A persisted, versioned object defining the agent's capabilities and persona: model, system prompt, tools, MCP servers, skills. **Must be created before starting a session.** See the Agents section below. | +| **Session** | `/v1/sessions` | A stateful interaction with an agent. References a pre-created agent by ID + an environment + initial instructions. Produces an event stream. | +| **Environment** | `/v1/environments` | A template defining the configuration for container provisioning. | +| **Container** | N/A | An isolated compute instance where the agent's **tools** execute (bash, file ops, code). The agent loop does not run here — it runs on Anthropic's orchestration layer and acts on the container via tool calls. | + +``` + ┌─────────────────────────────────────┐ + │ Anthropic orchestration layer │ +Agent (config) ───────▶│ (agent loop: Claude + tool calls) │ + └──────────────┬──────────────────────┘ + │ tool calls + ▼ +Environment (template) ──▶ Container (tool execution workspace) + │ + Session ─┤ + ├── Resources (files, repos, memory stores — attached at startup) + ├── Vault IDs (MCP credential references) + └── Conversation (event stream in/out) +``` + +> **Agent creation is a prerequisite.** Sessions reference a pre-created agent by ID — `model`/`system`/`tools` live on the agent object, never on the session. Every flow starts with `POST /v1/agents`. + +--- + +## Session Lifecycle + +``` +rescheduling → running ↔ idle → terminated +``` + +| Status | Description | +| -------------- | ------------------------------------------------------------------ | +| `idle` | Agent has finished the current task, and is awaiting input. It's either waiting for input to continue working via a `user.message` or blocked awaiting a `user.custom_tool_result` or `user.tool_confirmation`. The `stop_reason` attached contains more information about why the Agent has stopped working. | +| `running` | Session has starting running, and the Agent is actively doing work. | +| `rescheduling` | Session is (re)scheduling after a retryable error has occurred, ready to be picked up by the orchestration system. | +| `terminated` | Session has terminated, entering an irreversible and unusable state. | + +- Events can be sent when the session is `running` or `idle`. Messages are queued and processed in order. +- The agent transitions `idle → running` when it receives a new event, then back to `idle` when done. +- Errors surface as `session.error` events in the stream, not as a status value. + +### Built-in session features + +- **Context compaction** — if you approach max context, the API automatically condenses session history to keep the interaction going +- **Prompt caching** — historical repeated tokens are cached, reducing processing time and cost +- **Extended thinking** — on by default, returned as `agent.thinking` events + +### Session operations + +| Operation | Notes | +|---|---| +| List / fetch | Paginated list or single resource by ID | +| Update | Only `title` is updatable | +| Archive | Session becomes **read-only**. Not reversible. | +| Delete | Permanently deletes session, event history, container, and checkpoints. | + +These are ops/inspection calls — typically made from a terminal, not application code. From the shell (see `shared/anthropic-cli.md`): + +```sh +ant beta:sessions list --transform '{id,title,status,created_at}' --format jsonl +ant beta:sessions retrieve --session-id "$SID" +ant beta:sessions:events stream --session-id "$SID" # watch events live +ant beta:sessions archive --session-id "$SID" +ant beta:sessions delete --session-id "$SID" +``` + +--- + +## Sessions + +A session is a running agent instance inside an environment. + +### Session Object + +Key fields returned by the API: + +| Field | Type | Description | +| --------------- | -------- | --------------------------------------------------- | +| `type` | string | Always `"session"` | +| `id` | string | Unique session ID | +| `title` | string | Human-readable title | +| `status` | string | `idle`, `running`, `rescheduling`, `terminated` | +| `created_at` | string | ISO 8601 timestamp | +| `updated_at` | string | ISO 8601 timestamp | +| `archived_at` | string | ISO 8601 timestamp (nullable) | +| `environment_id` | string | Environment ID | +| `agent` | object | Agent configuration | +| `resources` | array | Attached files, repos, and memory stores | +| `metadata` | object | User-provided key-value pairs (max 8 keys) | +| `usage` | object | Token usage statistics | + +### Creating a session + +**A session is meaningless without an agent.** Sessions reference a pre-created agent by ID. Create the agent first via `agents.create()`, then reference it: + +```ts +// 1. Create the agent (reusable, versioned) +const agent = await client.beta.agents.create( + { + name: "Coding Assistant", + model: "claude-opus-4-8", + system: "You are a helpful coding agent.", + tools: [{ type: "agent_toolset_20260401"}], + }, +); + +// 2. Start a session that references it +const session = await client.beta.sessions.create( + { + agent: agent.id, // string shorthand → latest version. Or: { type: "agent", id: agent.id, version: agent.version } + environment_id: environmentId, + title: "Hello World Session", + }, +); +``` + +> 💡 **Watch it live in Console.** While developing, print a link so you can click through to the session UI instead of parsing the event stream: `https://platform.claude.com/workspaces/{workspace}/sessions/{session.id}`. Use `default` for the workspace slug unless you created the session in a non-default workspace; the sessions list for your workspace is at `https://platform.claude.com/workspaces/default/sessions`. Include this `print`/`console.log` in code you generate for users who are iterating locally. + +**Session creation parameters:** + +| Field | Type | Required | Description | +| --------------- | -------- | -------- | ---------------------------------------------- | +| `agent` | string or object | **Yes** | String shorthand `"agent_abc123"` (latest version) or `{type: "agent", id, version}` | +| `environment_id`| string | **Yes** | Environment ID | +| `title` | string | No | Human-readable name (appears in logs/dashboards) | +| `resources` | array | No | Files, GitHub repos, or memory stores, attached to the container at startup. Memory stores are session-create-only (not addable via `resources.add()`). | +| `vault_ids` | array | No | Vault IDs (`vlt_*`) — MCP credentials with auto-refresh. See `shared/managed-agents-tools.md` → Vaults. | +| `metadata` | object | No | User-provided key-value pairs | + +**Agent configuration fields** (passed to `agents.create()`, not `sessions.create()`): + +| Field | Type | Required | Description | +| ------------- | -------- | -------- | ---------------------------------------------- | +| `name` | string | **Yes** | Human-readable name (1-256 chars) | +| `model` | string or object | **Yes** | Claude model ID (bare string, or `{id, speed}` object). All Claude 4.5+ models supported. | +| `system` | string | No | System prompt — defines the agent's behavior (up to 100K chars) | +| `tools` | array | No | Encompasses three kinds: (1) pre-built Claude Agent tools (`agent_toolset_20260401`), (2) MCP tools (`mcp_toolset`), and (3) custom client-side tools. Max 128. | +| `mcp_servers` | array | No | MCP server connections — standardized third-party capabilities (e.g. GitHub, Asana). Max 20, unique names. See `shared/managed-agents-tools.md` → MCP Servers. | +| `skills` | array | No | Customized "best-practices" context with progressive disclosure. Max 20. See `shared/managed-agents-tools.md` → Skills. | +| `description` | string | No | Description of the agent (up to 2048 chars) | +| `multiagent` | object | No | `{type: "coordinator", agents: [...]}` — roster this agent may delegate to. See `shared/managed-agents-multiagent.md`. | +| `metadata` | object | No | Arbitrary key-value pairs (max 16, keys ≤64 chars, values ≤512 chars) | + +--- + +## Agents + +**This is where every Managed Agents flow begins.** The agent object is a persisted, versioned configuration — you create it once, then reference it by ID every time you start a session. No agent → no session. + +### Agent Object + +The API is **flat** — `model`, `system`, `tools` etc. are top-level fields, not wrapped in an `agent:{}` sub-object. + +| Field | Type | Required | Description | +| ------------------ | -------- | -------- | -------------------------------------------------- | +| `name` | string | Yes | Human-readable name | +| `model` | string | Yes | Claude model ID | +| `system` | string | No | System prompt | +| `tools` | array | No | Agent toolset / MCP toolset / custom tools | +| `mcp_servers` | array | No | MCP server connections | +| `skills` | array | No | Skill references (max 20) | +| `description` | string | No | Description of the agent | +| `multiagent` | object | No | Coordinator roster — see `shared/managed-agents-multiagent.md` | +| `metadata` | object | No | Arbitrary key-value pairs | + +### Lifecycle: create once, run many, update in place + +The agent is a **persistent resource**, not a per-run parameter. The intended pattern: + +``` +┌─ setup (once) ─────────┐ ┌─ runtime (every invocation) ─┐ +│ agents.create() │ │ sessions.create( │ +│ → store agent_id │ ──→ │ agent={type:..., id: ID} │ +│ in config/env/db │ │ ) │ +└────────────────────────┘ └──────────────────────────────┘ +``` + +**Anti-pattern:** calling `agents.create()` at the top of every script run. This accumulates orphaned agent objects, pays create latency on every invocation, and defeats the versioning model. If you see `agents.create()` in a function that's called per-request or per-cron-tick, that's wrong — hoist it to one-time setup and persist the ID. + +> **Recommended — define agents and environments as YAML + apply via the `ant` CLI.** The split is **CLI for the control plane, SDK for the data plane**: agents and environments are relatively static resources you manage with `ant` (version-controlled YAML, applied from CI); sessions are dynamic and driven by your application through the SDK. See `shared/anthropic-cli.md` → *Version-controlled Managed Agents resources* for the `ant beta:agents create < agent.yaml` / `update --version N` flow. The SDK `agents.create()` call shown elsewhere in this doc is the in-code equivalent — use it when you need to provision programmatically, but prefer the YAML flow for anything a human maintains. + +### Versioning + +Each `POST /v1/agents/{id}` (update) creates a new immutable version (numeric timestamp, e.g. `1772585501101368014`). The agent's history is append-only — you can't edit a past version. + +**Why version:** +- **Reproducibility** — pin a session to a known-good config: `{type: "agent", id, version: 3}` +- **Safe iteration** — update the agent without breaking sessions already running on the old version +- **Rollback** — if a new system prompt regresses, pin new sessions back to the prior version while you debug + +**`version` is optional.** Omit it (or use the string shorthand `agent="agent_abc123"`) to get the latest version at session-creation time. Pass it explicitly (`{type: "agent", id, version: N}`) to pin for reproducibility. + +**Getting the version to pin:** `agents.create()` and `agents.update()` both return `version` in the response. Store it alongside `agent_id`. To fetch the current latest for an existing agent: `GET /v1/agents/{id}` → `.version`. + +**When to update vs create new:** Update (`POST /v1/agents/{id}`) when it's conceptually the same agent with tweaked behavior (better prompt, extra tool). Create a new agent when it's a different persona/purpose. Rule of thumb: if you'd give it the same `name`, update. + +### Agent Endpoints + +| Operation | Method | Path | +| ---------------- | -------- | ------------------------------------- | +| Create | `POST` | `/v1/agents` | +| List | `GET` | `/v1/agents` | +| Get | `GET` | `/v1/agents/{id}` | +| Update | `POST` | `/v1/agents/{id}` | +| Archive | `POST` | `/v1/agents/{id}/archive` | + +> ⚠️ **Archive is permanent.** Archiving makes the agent read-only: existing sessions continue to run, but **new sessions cannot reference it**, and there is no unarchive. Since agents have no `delete`, this is the terminal lifecycle state. Never archive a production agent as routine cleanup — confirm with the user first. + +### Using an Agent in a Session + +Reference the agent by string ID (latest version) or by object with an explicit version: + +```python +# String shorthand — uses the agent's latest version +session = client.beta.sessions.create( + agent=agent.id, + environment_id=environment_id, +) + +# Or pin to a specific version (int) +session = client.beta.sessions.create( + agent={"type": "agent", "id": agent.id, "version": agent.version}, + environment_id=environment_id, +) +``` + +### Updating the agent configuration mid-session + +`sessions.update()` can change `agent.tools`, `agent.mcp_servers` (including permission policies), and `vault_ids` on an **existing** session. This is a **session-local override** — it does not create a new agent version and does not propagate back to the agent object. The provided arrays are **full replacements**; to append one tool, `GET` the session, modify, and `POST` back. The session must be `idle` — interrupt first if running. + +```python +client.beta.sessions.update( + session.id, + agent={ + "tools": [ + {"type": "agent_toolset_20260401"}, + {"type": "mcp_toolset", "mcp_server_name": "linear"}, + ], + "mcp_servers": [{"type": "url", "name": "linear", "url": "https://mcp.linear.app/sse"}], + }, + vault_ids=["vlt_..."], +) +``` + diff --git a/.agents/skills/claude-api/shared/managed-agents-environments.md b/.agents/skills/claude-api/shared/managed-agents-environments.md new file mode 100644 index 00000000..64558539 --- /dev/null +++ b/.agents/skills/claude-api/shared/managed-agents-environments.md @@ -0,0 +1,219 @@ +# Managed Agents — Environments & Resources + +## Environments + +Creating a session requires an `environment_id`. Environments are **reusable configuration templates** for spinning up containers in Anthropic's infrastructure — you might create different environments for different use cases (e.g. data visualization vs web development, with different package sets). Anthropic handles scaling, container lifecycle, and work orchestration. + +**Environment names must be unique.** Creating an environment with an existing name returns 409. + +### Networking + +| Network Policy | Description | +| ---------------- | ------------------------------------------------------------- | +| `unrestricted` | Full egress (except legal blocklist) | +| `limited` | Deny-by-default; opt in via `allowed_hosts` / `allow_package_managers` / `allow_mcp_servers` | + +```json +{ + "networking": { + "type": "limited", + "allow_package_managers": true, + "allow_mcp_servers": true, + "allowed_hosts": ["api.example.com"] + } +} +``` + +All three `limited` fields are optional. `allow_package_managers` (default `false`) permits PyPI/npm/etc.; `allow_mcp_servers` (default `false`) permits the agent's configured MCP server endpoints without listing them in `allowed_hosts`. + +**MCP caveat:** Under `limited` networking, either set `allow_mcp_servers: true` or add each MCP server domain to `allowed_hosts`. Otherwise the container can't reach them and tools silently fail. + +### Creating an environment + +The SDK adds `managed-agents-2026-04-01` automatically. TypeScript: + +```ts +const env = await client.beta.environments.create({ + name: "my_env", + config: { + type: "cloud", + networking: { type: "unrestricted" }, + }, +}); +``` + +### Self-hosted sandboxes + +To run tool execution in **your own infrastructure** instead of Anthropic's, set `config: {type: "self_hosted"}` — the agent loop stays on Anthropic's side, but `bash` / file ops / code execute in a container you control via an outbound-polling worker. The `networking` block does not apply (you control egress). Resource mounting (`file`, `github_repository`) and memory stores behave differently — see `shared/managed-agents-self-hosted-sandboxes.md` for the worker, credentials, and cloud-vs-self-hosted comparison. + +### Environment CRUD + +| Operation | Method | Path | Notes | +| ---------------- | -------- | ------------------------------------------ | ----- | +| Create | `POST` | `/v1/environments` | | +| List | `GET` | `/v1/environments` | Paginated (`limit`, `after_id`, `before_id`) | +| Get | `GET` | `/v1/environments/{id}` | | +| Update | `POST` | `/v1/environments/{id}` | Changes apply only to **new** containers; existing sessions keep their original config | +| Delete | `DELETE` | `/v1/environments/{id}` | Returns 204. | +| Archive | `POST` | `/v1/environments/{id}/archive` | Makes it **read-only**; existing sessions continue, new sessions cannot reference it. No unarchive — terminal state. | + +--- + +## Resources + +Attach files, GitHub repositories, and memory stores to a session. **Session creation blocks until all resources are mounted** — the container won't go `running` until every file and repo is in place. Max **999 file resources** per session. Multiple GitHub repositories per session are supported. For `type: "memory_store"` resources (persistent cross-session memory — max 8 per session), see `shared/managed-agents-memory.md`. + +### File Uploads (input — host → agent) + +Upload a file first via the Files API, then reference by `file_id` + `mount_path`: + +```ts +// 1. Upload +const file = await client.beta.files.upload({ + file: fs.createReadStream("data.csv"), +}); + +// 2. Attach as a session resource +const session = await client.beta.sessions.create({ + agent: agent.id, + environment_id: envId, + resources: [ + { type: "file", file_id: file.id, mount_path: "/workspace/data.csv" } + ], +}); +``` + +**`mount_path` is required** and must be absolute. Parent directories are created automatically. Agent working directory defaults to `/workspace`. Files are mounted read-only — the agent writes modified versions to new paths. + +### Session outputs (output — agent → host) + +The agent can write files to `/mnt/session/outputs/` during a session. These are automatically captured by the Files API and can be listed and downloaded afterwards: + +```ts +// After the turn completes, list output files scoped to this session: +for await (const f of client.beta.files.list({ + scope_id: session.id, + betas: ["managed-agents-2026-04-01"], +})) { + console.log(f.filename, f.size_bytes); + const resp = await client.beta.files.download(f.id); + const text = await resp.text(); +} +``` + +**Requirements:** +- The `write` tool (or `bash`) must be enabled for the agent to create output files. +- Session-scoped `files.list` / `files.download` captures outputs written to `/mnt/session/outputs/`. +- The filter parameter is **`scope_id`** (REST query param `?scope_id=`). The SDK's files resource auto-adds only the `files-api-2025-04-14` header, so pass `betas: ["managed-agents-2026-04-01"]` explicitly (or both headers on raw HTTP) — without it the API may reject `scope_id` as an unknown field. Requires `@anthropic-ai/sdk` ≥ 0.88.0 / `anthropic` (Python) ≥ 0.92.0 — older versions don't type `scope_id`. The `ant` CLI does **not** expose this flag yet; use the SDK or curl. +- Pass the session ID returned by `sessions.create()` verbatim (e.g. `sesn_011CZx...`) — the API validates the prefix. +- There's a brief indexing lag (~1–3s) between `session.status_idle` and output files appearing in `files.list`. Retry once or twice if empty. + +> **Fallback when `scope_id` filtering is unavailable** (older SDK, or endpoint returns an error): send a follow-up `user.message` asking the agent to `read` each file under `/mnt/session/outputs/` and return the contents. The agent streams the file bodies back as `agent.message` text. This works for text files only and costs output tokens — use it to unblock, not as the primary path. + +This gives you a bidirectional file bridge: upload reference data in, download agent artifacts out. + +### GitHub Repositories + +Clones a GitHub repository into the session container during initialization, before the agent begins execution. The agent can read, edit, commit, and push via `bash` (`git`). Multiple repositories per session are supported — add one `resources` entry per repo. Repositories are cached, so future sessions that use the same repository start faster. + +Repositories are attached for the lifetime of the session — to change which repositories are mounted, create a new session. You **can** rotate a repository's `authorization_token` on a running session via `client.beta.sessions.resources.update(resource_id, {session_id, authorization_token})`; the resource `id` is returned at session creation and by `resources.list()`. + +**Fields:** + +| Field | Required | Notes | +|---|---|---| +| `type` | ✅ | `"github_repository"` | +| `url` | ✅ | The GitHub repository URL | +| `authorization_token` | ✅ | GitHub Personal Access Token with repository access. **Never echoed in API responses.** | +| `mount_path` | ❌ | Path where the repository will be cloned. Defaults to `/workspace/`. | +| `checkout` | ❌ | `{type: "branch", name: "..."}` or `{type: "commit", sha: "..."}`. Defaults to the repo's default branch. | + +**Token permission levels** (fine-grained PATs): +- `Contents: Read` — clone only +- `Contents: Read and write` — push changes and create pull requests + +**How auth works:** `authorization_token` is never placed inside the container. `git pull` / `git push` and GitHub REST calls against the attached repository are routed through an Anthropic-side git proxy that injects the token after the request leaves the sandbox. Code running in the container — including anything the agent writes — cannot read or exfiltrate it. + +> ‼️ **To generate pull requests** you also need GitHub **MCP server** access — the `github_repository` resource gives filesystem + git access only. See `shared/managed-agents-tools.md` → MCP Servers. The PR workflow is: edit files in the mounted repo → push branch via `bash` (authenticated via the git proxy using `authorization_token`) → create PR via the MCP `create_pull_request` tool (authenticated via the vault). + +**TypeScript:** + +```ts +// 1. Create the agent — declare GitHub MCP (no auth here) +const agent = await client.beta.agents.create( + { + name: 'GitHub Agent', + model: 'claude-opus-4-8', + mcp_servers: [ + { type: 'url', name: 'github', url: 'https://api.githubcopilot.com/mcp/' }, + ], + tools: [ + { type: 'agent_toolset_20260401', default_config: { enabled: true } }, + { type: 'mcp_toolset', mcp_server_name: 'github' }, + ], + }, +); + +// 2. Start a session — attach vault for MCP auth + mount the repo +const session = await client.beta.sessions.create({ + agent: agent.id, + environment_id: envId, + vault_ids: [vaultId], // vault contains the GitHub MCP OAuth credential + resources: [ + { + type: 'github_repository', + url: 'https://github.com/owner/repo', + authorization_token: process.env.GITHUB_TOKEN, // repo clone token (≠ MCP auth) + checkout: { type: 'branch', name: 'main' }, + }, + ], +}); +``` + +**Python:** + +```python +import os + +agent = client.beta.agents.create( + name="GitHub Agent", + model="claude-opus-4-8", + mcp_servers=[{ + "type": "url", + "name": "github", + "url": "https://api.githubcopilot.com/mcp/", + }], + tools=[ + {"type": "agent_toolset_20260401", "default_config": {"enabled": True}}, + {"type": "mcp_toolset", "mcp_server_name": "github"}, + ], +) + +session = client.beta.sessions.create( + agent=agent.id, + environment_id=env_id, + vault_ids=[vault_id], # vault contains the GitHub MCP OAuth credential + resources=[{ + "type": "github_repository", + "url": "https://github.com/owner/repo", + "authorization_token": os.environ["GITHUB_TOKEN"], # repo clone token (≠ MCP auth) + "checkout": {"type": "branch", "name": "main"}, + }], +) +``` + +--- + +## Files API + +Upload and manage files for use as session resources, and download files the agent wrote to `/mnt/session/outputs/`. + +| Operation | Method | Path | SDK | +| ---------------- | -------- | ------------------------------------- | --- | +| Upload | `POST` | `/v1/files` | `client.beta.files.upload({ file })` | +| List | `GET` | `/v1/files?scope_id=...` | `client.beta.files.list({ scope_id, betas: ["managed-agents-2026-04-01"] })` | +| Get Metadata | `GET` | `/v1/files/{id}` | `client.beta.files.retrieveMetadata(id)` | +| Download | `GET` | `/v1/files/{id}/content` | `client.beta.files.download(id)` → `Response` | +| Delete | `DELETE` | `/v1/files/{id}` | `client.beta.files.delete(id)` | + +The `scope_id` filter on List scopes the results to files written to `/mnt/session/outputs/` by that session. Without the filter, you get all files uploaded to your account. diff --git a/.agents/skills/claude-api/shared/managed-agents-events.md b/.agents/skills/claude-api/shared/managed-agents-events.md new file mode 100644 index 00000000..28e3fbcb --- /dev/null +++ b/.agents/skills/claude-api/shared/managed-agents-events.md @@ -0,0 +1,195 @@ +# Managed Agents — Events & Steering + +## Events + +### Sending Events + +Send events to a session via `POST /v1/sessions/{id}/events`. + +| Event Type | When to Send | +| ------------------------- | --------------------------------------------------- | +| `user.message` | Send a user message | +| `user.interrupt` | Interrupt the agent while it's running | +| `user.tool_confirmation` | Approve/deny a tool call (when `always_ask` policy) | +| `user.custom_tool_result` | Provide result for a custom tool call | +| `user.define_outcome` | Start a rubric-graded iterate loop — see `shared/managed-agents-outcomes.md` | + +### Receiving Events + +Three methods: + +1. **Streaming (SSE)**: `GET /v1/sessions/{id}/events/stream` — real-time Server-Sent Events. **Long-lived** — the server sends periodic heartbeats to keep the connection alive. +2. **Polling**: `GET /v1/sessions/{id}/events` — paginated event list (query params: `limit` default 1000, `page`). **Returns immediately** — this is a plain paginated GET, not a long-poll. +3. **Webhooks**: Anthropic POSTs session state transitions to your HTTPS endpoint — thin payloads (IDs only), HMAC-signed, Console-registered. See `shared/managed-agents-webhooks.md`. + +All received events carry `id`, `type`, and `processed_at` (ISO 8601; `null` if not yet processed by the agent). + +> ⚠️ **Robust polling (raw HTTP).** If you bypass the SDK and roll your own poll loop, don't rely on `requests` or `httpx` timeouts as wall-clock caps — they're **per-chunk** read timeouts, reset every time a byte arrives. A trickling response (heartbeats, a wedged chunked-encoding body, a misbehaving proxy) can keep the call blocked indefinitely even with `timeout=(5, 60)` or `httpx.Timeout(120)`. Neither library has a "total wall-clock" timeout built in. For a hard deadline: track `time.monotonic()` at the loop level and break/cancel if a single request exceeds your budget (e.g. via a watchdog thread, or `asyncio.wait_for()` around async httpx). **Prefer the SDK** — `client.beta.sessions.events.stream()` and `client.beta.sessions.events.list()` handle timeout + retry sanely. +> +> If `GET /v1/sessions/{id}/events` (paginated) ever hangs after headers, you've likely hit `GET /v1/sessions/{id}/events` by mistake or a server-side stall — report it; don't treat it as a client-config problem. + +### Event Types (Received) + +Event types use dot notation, grouped by namespace: + +| Event Type | Description | +| --- | --- | +| `agent.message` | Agent text output | +| `agent.thinking` | Extended thinking blocks | +| `agent.tool_use` | Agent used a built-in tool (`agent_toolset_20260401`) | +| `agent.tool_result` | Result from a built-in tool | +| `agent.mcp_tool_use` | Agent used an MCP tool | +| `agent.mcp_tool_result` | Result from an MCP tool | +| `agent.custom_tool_use` | Agent invoked a custom tool — session goes idle, you respond with `user.custom_tool_result` | +| `agent.thread_context_compacted` | Conversation context was compacted | +| `session.status_idle` | Agent has finished the current task, and is awaiting input. It's either waiting for input to continue working via a `user.message` or blocked awaiting a `user.custom_tool_result` or `user.tool_confirmation`. The `stop_reason` attached contains more information about why the Agent has stopped working. | +| `session.status_running` | Session has starting running, and the Agent is actively doing work. | +| `session.status_rescheduled` | Session is (re)scheduling after a retryable error has occurred, ready to be picked up by the orchestration system. | +| `session.status_terminated` | Session has terminated, entering an irreversible and unusable state. | +| `session.error` | Error occurred during processing | +| `span.model_request_start` | Model inference started | +| `span.model_request_end` | Model inference completed | +| `span.outcome_evaluation_start` / `_ongoing` / `_end` | Grader progress for outcome-oriented sessions — see `shared/managed-agents-outcomes.md` | +| `session.thread_created` | Subagent thread spawned (multiagent) — see `shared/managed-agents-multiagent.md` | +| `session.thread_status_running` / `_idle` / `_rescheduled` / `_terminated` | Subagent thread status transitions (multiagent). `_idle` carries `stop_reason`. | +| `agent.thread_message_sent` / `_received` | Cross-thread message, carries `to_session_thread_id` / `from_session_thread_id` (multiagent) | + +The stream also echoes back user-sent events (`user.message`, `user.interrupt`, `user.tool_confirmation`, `user.custom_tool_result`, `user.define_outcome`). + +--- + +## Steering Patterns + +Practical patterns for driving a session via the events surface. + +### Stream-first ordering + +**Open the stream before sending events.** The stream only delivers events that occur *after* it's opened — it does not replay current state or historical events. If you send a message first and open the stream second, early events (including fast status transitions) arrive buffered in a single batch and you lose the ability to react to them in real time. + +```ts +// ✅ Correct — stream and send concurrently +const [response] = await Promise.all([ + streamEvents(sessionId), // opens SSE connection + sendMessage(sessionId, text), +]); + +// ❌ Wrong — events before stream opens arrive as a single buffered batch +await sendMessage(sessionId, text); +const response = await streamEvents(sessionId); +``` + +**For full history,** use `GET /v1/sessions/{id}/events` (paginated list) — the stream only gives you live events from connection onward. + +### Reconnecting after a dropped stream + +**The SSE stream has no replay.** If your connection drops (httpx read timeout, network blip) and you reconnect, you only get events emitted *after* reconnection. Any events emitted during the gap are lost from the stream. + +**The consolidation pattern:** on every (re)connect, overlap the stream with a history fetch and dedupe by event ID: + +```python +def connect_with_consolidation(client, session_id): + # 1. Open the SSE stream first + stream = client.beta.sessions.events.stream(session_id=session_id) + + # 2. Fetch history to cover any gap + history = client.beta.sessions.events.list( + session_id=session_id, + ) + + # 3. Yield history first, then stream — dedupe by event.id + seen = set() + for ev in history.data: + seen.add(ev.id) + yield ev + for ev in stream: + if ev.id not in seen: + seen.add(ev.id) + yield ev +``` + +### Message queuing + +**You don't have to wait for a response before sending the next message.** User events are queued server-side and processed in order. This is useful for chat bridges where the user sends rapid follow-ups: + +```ts +// All three go into one session; agent processes them in order +await sendMessage(sessionId, "Summarize the README"); +await sendMessage(sessionId, "Actually also check the CONTRIBUTING guide"); +await sendMessage(sessionId, "And compare the two"); +// Stream once — agent responds to all three as a coherent turn +``` + +Events can be sent up to the Session at any time. There is no need to wait on a specific session status to enqueue new events via `client.beta.sessions.events.send()` + +### Interrupt + +An `interrupt` event **jumps the queue** (ahead of any pending user messages) and forces the session into `idle`. Use this for "stop" / "nevermind" / "cancel" commands: + +```ts +await client.beta.sessions.events.send(sessionId, { + events: [{ type: 'interrupt' }], +}); +``` + +The agent stops mid-task. It does not see the interrupt as a message — it just halts. Send a follow-up `user` event to explain what to do instead. If an outcome is active, the interrupt also marks `span.outcome_evaluation_end.result: "interrupted"` (see `shared/managed-agents-outcomes.md`). + +> **Note**: Interrupt events may have empty IDs in the current implementation. When troubleshooting, use the `processed_at` timestamp along with surrounding event IDs. + +### Event payloads + +some events carry useful metadata beyond the status change itself: + +`session.status_idle` — includes a `stop_reason` field which elaborates on why the session stopped and what type of further action is required by the user. +```json +{ + "id": "sevt_456", + "processed_at": "2026-04-07T04:27:43.197Z", + "stop_reason": { + "event_ids": [ + "sevt_123" + ], + "type": "requires_action" + }, + "type": "status_idle" +} +``` + +`span.model_request_end` contains a `model_usage` field for cost tracking and efficiency analysis: + +```json +{ + "type": "span.model_request_end", + "id": "sevt_456", + "is_error": false, + "model_request_start_id": "sevt_123", + "model_usage": { + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 6656, + "input_tokens": 3571, + "output_tokens": 727 + }, + "processed_at": "2026-04-07T04:11:32.189Z" +} +``` + +**`agent.thread_context_compacted`** — emitted when the conversation history was summarized to fit context. Includes `pre_compaction_tokens` so you know how much was squeezed: + +```json +{ + "id": "sevt_abc123", + "processed_at": "2026-03-24T14:05:15.787Z", + "type": "agent.thread_context_compacted" +} +``` + +### Archive + +When done with a session, archive it to free resources: + +```ts +await client.beta.sessions.archive(sessionId); +``` + +> Archiving a **session** is routine cleanup — sessions are per-run and disposable. **Do not generalize this to agents or environments**: those are persistent, reusable resources, and archiving them is permanent (no unarchive; new sessions cannot reference them). See `shared/managed-agents-overview.md` → Common Pitfalls. + + diff --git a/.agents/skills/claude-api/shared/managed-agents-memory.md b/.agents/skills/claude-api/shared/managed-agents-memory.md new file mode 100644 index 00000000..70e6d3ae --- /dev/null +++ b/.agents/skills/claude-api/shared/managed-agents-memory.md @@ -0,0 +1,197 @@ +# Managed Agents — Memory Stores + +> **Public beta.** Memory stores ship under the `managed-agents-2026-04-01` beta header; the SDK sets it automatically on all `client.beta.memory_stores.*` calls. If `client.beta.memory_stores` is missing, upgrade to the latest SDK release. + +Sessions are ephemeral by default — when one ends, anything the agent learned is gone. A **memory store** is a workspace-scoped collection of small text documents that persists across sessions. When a store is attached to a session (via `resources[]`), it is mounted into the container as a filesystem directory; the agent reads and writes it with the ordinary file tools, and a system-prompt note tells it the mount is there. + +Every mutation to a memory produces an immutable **memory version** (`memver_...`), giving you an audit trail and point-in-time rollback/redact. + +## Object model + +| Object | ID prefix | Scope | Notes | +| --- | --- | --- | --- | +| Memory store | `memstore_...` | Workspace | Attach to sessions via `resources[]` | +| Memory | `mem_...` | Store | One text file, addressed by `path` (≤ 100KB each — prefer many small files) | +| Memory version | `memver_...` | Memory | Immutable snapshot per mutation; `operation` ∈ `created` / `modified` / `deleted` | + +## Create a store + +`description` is passed to the agent so it knows what the store contains — write it for the model, not for humans. + +```python +store = client.beta.memory_stores.create( + name="User Preferences", + description="Per-user preferences and project context.", +) +print(store.id) # memstore_01Hx... +``` + +Other SDKs: TypeScript `client.beta.memoryStores.create({...})`; Go `client.Beta.MemoryStores.New(ctx, ...)`. See `shared/managed-agents-api-reference.md` → SDK Method Reference for the full per-language table. + +Stores support `retrieve` / `update` / `list` (with `include_archived`, `created_at_{gte,lte}` filters) / `delete` / **`archive`**. Archive makes the store read-only — existing session attachments continue, new sessions cannot reference it; no unarchive. + +### Seed with content (optional) + +Pre-load reference material before any session runs. `memories.create` creates a memory at the given `path`; if a memory already exists there the call returns `409` (`memory_path_conflict_error`, with the `conflicting_memory_id`). The store ID is the first positional argument. + +```python +client.beta.memory_stores.memories.create( + store.id, + path="/formatting_standards.md", + content="All reports use GAAP formatting. Dates are ISO-8601...", +) +``` + +## Attach to a session + +Memory stores go in the session's `resources[]` array alongside `file` and `github_repository` resources (see `shared/managed-agents-environments.md` → Resources). Memory stores attach at **session create time only** — `sessions.resources.add()` does not accept `memory_store`. + +```python +session = client.beta.sessions.create( + agent=agent.id, + environment_id=environment.id, + resources=[ + { + "type": "memory_store", + "memory_store_id": store.id, + "access": "read_write", # or "read_only"; default is "read_write" + "instructions": "User preferences and project context. Check before starting any task.", + } + ], +) +``` + +| Field | Required | Notes | +| --- | --- | --- | +| `type` | ✅ | `"memory_store"` | +| `memory_store_id` | ✅ | `memstore_...` | +| `access` | — | `"read_write"` (default) or `"read_only"` — enforced at the filesystem level on the mount | +| `instructions` | — | Session-specific guidance for this store, in addition to the store's `name`/`description`. ≤ 4,096 chars. | + +**Max 8 memory stores per session.** Attach multiple when different slices of memory have different owners or lifecycles — e.g. one read-only shared-reference store plus one read-write per-user store, or one store per end-user/team/project sharing a single agent config. + +### How the agent sees it (FUSE mount) + +Each attached store is mounted in the session container at `/mnt/memory//`. The agent interacts with it using the standard file tools (`bash`, `read`, `write`, `edit`, `glob`, `grep`) — there are no dedicated memory tools. `access: "read_only"` makes the mount read-only at the filesystem level; `"read_write"` allows the agent to create, edit, and delete files under it. A short description of each mount (name, path, `instructions`, access) is automatically injected into the system prompt so the agent knows the store exists without you having to mention it. + +Writes the agent makes under the mount are persisted back to the store and produce memory versions just like host-side `memories.update` calls. + +## Manage memories directly (host-side) + +Use these for review workflows, correcting bad memories, or seeding stores out-of-band. + +### List + +Returns `Memory | MemoryPrefix` entries — a `MemoryPrefix` (`type: "memory_prefix"`, just a `path`) is a directory-like node when listing hierarchically. Use `path_prefix` to scope (include a trailing slash: `"/notes/"` matches `/notes/a.md` but not `/notes_backup/old.md`) and `depth` to bound the tree walk. `order_by` / `order` sort the result. Pass `view="full"` to include `content` in each item; the default `"basic"` returns metadata only. + +```python +for m in client.beta.memory_stores.memories.list(store.id, path_prefix="/"): + if m.type == "memory": + print(f"{m.path} ({m.content_size_bytes} bytes, sha={m.content_sha256[:8]})") + else: # "memory_prefix" + print(f"{m.path}/") +``` + +### Read + +```python +mem = client.beta.memory_stores.memories.retrieve(memory_id, memory_store_id=store.id) +print(mem.content) +``` + +`retrieve` defaults to `view="full"` (content included); `view` matters mainly on list endpoints. + +### Create vs. update + +| Operation | Addressed by | Semantics | +| --- | --- | --- | +| `memories.create(store_id, path=..., content=...)` | **Path** | Create at `path`. `409` (`memory_path_conflict_error`, includes `conflicting_memory_id`) if the path is already occupied. | +| `memories.update(mem_id, memory_store_id=..., path=..., content=...)` | **`mem_...` ID** | Mutate existing memory. Change `content`, `path` (rename), or both. Renaming onto an occupied path returns the same `409 memory_path_conflict_error`. | + +```python +mem = client.beta.memory_stores.memories.create( + store.id, + path="/preferences/formatting.md", + content="Always use tabs, not spaces.", +) + +client.beta.memory_stores.memories.update( + mem.id, + memory_store_id=store.id, + path="/archive/2026_q1_formatting.md", # rename +) +``` + +### Optimistic concurrency (precondition on `update`) + +`memories.update` accepts a `precondition` so you can read → modify → write back without clobbering a concurrent writer. The only supported type is `content_sha256`. On mismatch the API returns `409` (`memory_precondition_failed_error`) — re-read and retry against fresh state. + +```python +client.beta.memory_stores.memories.update( + mem.id, + memory_store_id=store.id, + content="CORRECTED: Always use 2-space indentation.", + precondition={"type": "content_sha256", "content_sha256": mem.content_sha256}, +) +``` + +### Delete + +```python +client.beta.memory_stores.memories.delete(mem.id, memory_store_id=store.id) +``` + +Pass `expected_content_sha256` for a conditional delete. + +## Audit and rollback — memory versions + +Every mutation creates an immutable `memver_...` snapshot. Versions accumulate for the lifetime of the parent memory; `memories.retrieve` always returns the current head, the version endpoints give you history. + +| Operation that triggers it | `operation` field on the version | +| --- | --- | +| `memories.create` at a new path | `"created"` | +| `memories.update` changing `content`, `path`, or both (or an agent-side write to the mount) | `"modified"` | +| `memories.delete` | `"deleted"` | + +Each version also records `created_by` — an actor object with `type` ∈ `session_actor` / `api_actor` / `user_actor` — and, after redaction, `redacted_at` + `redacted_by`. + +### List versions + +Newest-first, paginated. Filter by `memory_id`, `operation`, `session_id`, `api_key_id`, or `created_at_gte` / `created_at_lte`. Pass `view="full"` to include `content`; default is metadata-only. + +```python +for v in client.beta.memory_stores.memory_versions.list(store.id, memory_id=mem.id): + print(f"{v.id}: {v.operation}") +``` + +### Retrieve a version + +```python +version = client.beta.memory_stores.memory_versions.retrieve( + version_id, memory_store_id=store.id +) +print(version.content) +``` + +### Redact a version + +Scrubs content from a historical version while preserving the audit trail (actor + timestamps). Clears `content`, `content_sha256`, `content_size_bytes`, and `path`; everything else stays. Use for leaked secrets, PII, or user-deletion requests. + +```python +client.beta.memory_stores.memory_versions.redact(version_id, memory_store_id=store.id) +``` + +## Endpoint reference + +See `shared/managed-agents-api-reference.md` → Memory Stores / Memories / Memory Versions for the full HTTP method/path tables. Raw HTTP base path: + +``` +POST /v1/memory_stores +POST /v1/memory_stores/{memory_store_id}/archive +GET /v1/memory_stores/{memory_store_id}/memories +PATCH /v1/memory_stores/{memory_store_id}/memories/{memory_id} +GET /v1/memory_stores/{memory_store_id}/memory_versions +POST /v1/memory_stores/{memory_store_id}/memory_versions/{version_id}/redact +``` + +For cURL examples and the CLI (`ant beta:memory-stores ...`), WebFetch the Memory URL in `shared/live-sources.md` → Managed Agents. diff --git a/.agents/skills/claude-api/shared/managed-agents-multiagent.md b/.agents/skills/claude-api/shared/managed-agents-multiagent.md new file mode 100644 index 00000000..ea609406 --- /dev/null +++ b/.agents/skills/claude-api/shared/managed-agents-multiagent.md @@ -0,0 +1,99 @@ +# Managed Agents — Multiagent Sessions + +A coordinator agent can delegate to other agents within one session. All agents **share the container and filesystem**; each runs in its own **thread** — a context-isolated event stream with its own conversation history, model, system prompt, tools, MCP servers, and skills (from that agent's own config). Threads are persistent: the coordinator can send a follow-up to a subagent it called earlier and that subagent retains its prior turns. + +The SDK sets the `managed-agents-2026-04-01` beta header automatically on all `client.beta.{agents,sessions}.*` calls; no additional header is required for multiagent. + +--- + +## Declare the roster on the coordinator + +`multiagent` is a **top-level field** on `agents.create()` / `agents.update()` — **not** a `tools[]` entry. `agents` lists 1–20 roster entries. Nothing changes on `sessions.create()` — the roster is resolved from the coordinator's config. + +```python +orchestrator = client.beta.agents.create( + name="Engineering Lead", + model="claude-opus-4-8", + system="You coordinate engineering work. Delegate code review to the reviewer and test writing to the test agent.", + tools=[{"type": "agent_toolset_20260401"}], + multiagent={ + "type": "coordinator", + "agents": [ + reviewer.id, # bare string — latest version + {"type": "agent", "id": test_writer.id, "version": 4}, # pinned version + {"type": "self"}, # the coordinator itself + ], + }, +) + +session = client.beta.sessions.create(agent=orchestrator.id, environment_id=env.id) +``` + +| Roster entry | Shape | Notes | +|---|---|---| +| String shorthand | `"agent_abc123"` | References the latest version of a stored agent. | +| Agent reference | `{type: "agent", id, version?}` | Omit `version` to pin the latest at coordinator save time. | +| Self | `{type: "self"}` | The coordinator can spawn copies of itself. | + +Up to **20 unique agents** in the roster; the coordinator may spawn **multiple copies** of each. **One level of delegation only** — depth > 1 is ignored. + +--- + +## Threads + +The session-level event stream is the **primary thread** — it shows the coordinator's trace plus a condensed view of subagent activity (thread status transitions and cross-thread messages, not every subagent tool call). Drill into a specific subagent via the per-thread endpoints: + +| Operation | HTTP | SDK (`client.beta.sessions.threads.*`) | +|---|---|---| +| List threads | `GET /v1/sessions/{sid}/threads` | `.list(session_id)` | +| Retrieve one | `GET /v1/sessions/{sid}/threads/{tid}` | `.retrieve(thread_id, session_id=...)` | +| Archive | `POST /v1/sessions/{sid}/threads/{tid}/archive` | `.archive(thread_id, session_id=...)` | +| List thread events | `GET /v1/sessions/{sid}/threads/{tid}/events` | `.events.list(thread_id, session_id=...)` | +| Stream thread events | `GET /v1/sessions/{sid}/threads/{tid}/stream` | `.events.stream(thread_id, session_id=...)` | + +Each `SessionThread` carries `id`, `status` (`running` | `idle` | `rescheduling` | `terminated`), `agent` (a resolved snapshot of the agent config — `id`, `name`, `model`, `system`, `tools`, `skills`, `mcp_servers`, `version`), `parent_thread_id` (null for the primary thread, which is included in the list), `archived_at`, and optional `stats`/`usage`. **Session status aggregates thread statuses** — if any thread is `running`, `session.status` is `running`. Max **25 concurrent threads**. When draining a per-thread stream, break on `session.thread_status_idle` (and check its `stop_reason` as you would for the session-level idle). + +--- + +## Multiagent events (on the session stream) + +| Event | Payload highlights | Meaning | +|---|---|---| +| `session.thread_created` | `session_thread_id`, `agent_name` | A new thread was created. | +| `session.thread_status_running` | `session_thread_id`, `agent_name` | Thread started activity. | +| `session.thread_status_idle` | `session_thread_id`, `agent_name`, **`stop_reason`** | Thread is awaiting input. Inspect `stop_reason` (same shape as `session.status_idle.stop_reason`). | +| `session.thread_status_rescheduled` | `session_thread_id`, `agent_name` | Thread is rescheduling after a retryable error. | +| `session.thread_status_terminated` | `session_thread_id`, `agent_name` | Thread was archived or hit a terminal error. | +| `agent.thread_message_sent` | `to_session_thread_id`, `to_agent_name`, `content` | Coordinator sent a follow-up to another thread. | +| `agent.thread_message_received` | `from_session_thread_id`, `from_agent_name`, `content` | An agent delivered its result to the coordinator. | + +--- + +## Tool permissions and custom tools from subagent threads + +When a subagent needs your client (an `always_ask` confirmation, or a custom tool result), the request is **cross-posted to the primary thread** with `session_thread_id` identifying the originating thread — so you only need to watch the session stream. Reply with `user.tool_confirmation` (carrying `tool_use_id`) or `user.custom_tool_result` (carrying `custom_tool_use_id`), and **echo the `session_thread_id` from the originating event** (the SDK param type and docstring expect it). The server also routes by the tool-use ID, so the echo is belt-and-suspenders rather than load-bearing — but include it. + +```python +for event_id in stop.event_ids: + pending = events_by_id[event_id] + confirmation = { + "type": "user.tool_confirmation", + "tool_use_id": event_id, + "result": "allow", + } + if pending.session_thread_id is not None: + confirmation["session_thread_id"] = pending.session_thread_id + client.beta.sessions.events.send(session.id, events=[confirmation]) +``` + +The same pattern applies to `user.custom_tool_result`. + +--- + +## Pitfalls + +- **Don't put the roster on `sessions.create()` or in `tools[]`.** `multiagent` is a top-level agent field; update the coordinator, then start a session that references it. +- **Don't assume shared context.** Threads share the filesystem but not conversation history or tools. If the coordinator needs a subagent to act on something, it must say so in the delegated message (or write it to disk). +- **Depth > 1 is ignored.** A subagent's own `multiagent` roster (if any) doesn't cascade — only the session's coordinator delegates. + +For per-language bindings beyond Python, WebFetch `https://platform.claude.com/docs/en/managed-agents/multi-agent.md` (see `shared/live-sources.md`). diff --git a/.agents/skills/claude-api/shared/managed-agents-onboarding.md b/.agents/skills/claude-api/shared/managed-agents-onboarding.md new file mode 100644 index 00000000..23bc4281 --- /dev/null +++ b/.agents/skills/claude-api/shared/managed-agents-onboarding.md @@ -0,0 +1,144 @@ +# Managed Agents — Onboarding Flow + +> **Invoked via `/claude-api managed-agents-onboard`?** You're in the right place. Run the interview below — don't summarize it back to the user, ask the questions. + +Use this when a user wants to set up a Managed Agent from scratch: **branch on know-vs-explore → configure the template → set up the session → pre-flight viability check → emit working code.** The pre-flight check (§3) is not optional — a setup missing a tool, credential, or data access it needs will fail mid-run, and the gap is usually visible at setup time. + +> Read `shared/managed-agents-core.md` alongside this — it has full detail for each knob. This doc is the interview script, not the reference. + +--- + +Claude Managed Agents is a hosted agent: Anthropic runs the agent loop on its orchestration layer and provisions a sandboxed container per session where the agent's tools execute (or, with a `self_hosted` environment, your own worker runs the tools — see `shared/managed-agents-self-hosted-sandboxes.md`). You supply the agent config and the environment config; the harness — event stream, sandbox orchestration, prompt caching, context compaction, and extended thinking — is handled for you. + +**What you supply:** +- **An agent config** — tools, skills, model, system prompt. Reusable and versioned. +- **An environment config** — the sandbox your agent's tools execute in (`cloud`: networking, packages; or `self_hosted`: your own infra). Reusable across agents. + +Each run of the agent is a **session**. + +--- + +## 1. Know or explore? + +Ask the user: + +> Do you already know the agent you want to build, or would you like to explore some common patterns first? + +### Explore path — show the patterns + +Four shapes, same runtime code path (`sessions.create()` → `sessions.events.send()` → stream). Only the trigger and sink differ. + +| Pattern | Trigger | Example | +|---|---|---| +| Event-triggered | Webhook | GitHub PR push → CMA (GitHub tool) → Slack | +| Scheduled | Cron | Daily brief: browser + GitHub + Jira → CMA → Slack | +| Fire-and-forget PR | Human | Slack slash-command → CMA (GitHub tool) → PR passing CI | +| Research + dashboard | Human | Topic → CMA (web search + `frontend-design` skill) → HTML dashboard | + +Ask which shape fits, then continue with the Know path using it as the reference. + +### Know path — configure template + +Three rounds. Batch the questions in each round; don't ask them one at a time. + +**Round A — Tools.** Start here; it's the most concrete part. Three types; ask which the user wants (any combination): + +| Type | What it is | How to guide | +|---|---|---| +| **Prebuilt Claude Agent tools** (`agent_toolset_20260401`) | Ready-to-use: `bash`, `read`, `write`, `edit`, `glob`, `grep`, `web_fetch`, `web_search`. Enable all at once, or individually via `enabled: true/false`. | Recommend enabling the full toolset. List the 8 tools so the user knows what they're getting. Full detail: `shared/managed-agents-tools.md` → Agent Toolset. | +| **MCP tools** | Third-party integrations (GitHub, Linear, Asana, etc.) via `mcp_toolset`. Credentials live in a vault, not inline. | Ask which services. For each, walk through MCP server URL + vault credentials. Full detail: `shared/managed-agents-tools.md` → MCP Servers + Vaults. | +| **Custom tools** | The user's own app handles these tool calls — agent fires `agent.custom_tool_use`, the app sends a result message back. | Ask for each tool: name, description, input schema. The app code that handles the event is *their* code — don't generate it. Full detail: `shared/managed-agents-tools.md` → Custom Tools. | + +**Round B — Skills, files, and repos.** What the agent has on hand when it starts. + +*Skills* — two types; both work the same way — Claude auto-uses them when relevant. Max 20 per agent. +- [ ] **Pre-built Agent Skills**: `xlsx`, `docx`, `pptx`, `pdf`. Reference by name. +- [ ] **Custom Skills**: skills uploaded to the user's org via the Skills API. Reference by `skill_id` + optional `version`. If the skill doesn't exist yet, walk the user through `POST /v1/skills` + `POST /v1/skills/{id}/versions` (beta header `skills-2025-10-02`). Full detail: `shared/managed-agents-tools.md` → Skills + Skills API. + +*GitHub repositories* — any repos the agent needs on-disk? For each: +- [ ] Repo URL (`https://github.com/org/repo`) +- [ ] `authorization_token` (PAT or GitHub App token scoped to the repo) +- [ ] Optional `mount_path` (defaults to `/workspace/`) and `checkout` (branch or SHA) + +Emit as `resources: [{type: "github_repository", url, authorization_token, ...}]`. Full detail: `shared/managed-agents-environments.md` → GitHub Repositories. + +> ‼️ **PR creation needs the GitHub MCP server too.** `github_repository` gives filesystem access only — to open PRs, also attach the GitHub MCP server in Round A and credential it via a vault. The workflow is: edit files in the mounted repo → push branch via `bash` → create PR via the MCP `create_pull_request` tool. + +*Files* — any local files to seed the session with? For each: +- [ ] Upload via the Files API → persist `file_id` +- [ ] Choose a `mount_path` — absolute, e.g. `/workspace/data.csv` (parents auto-created; files mount read-only) + +Emit as `resources: [{type: "file", file_id, mount_path}]`. Max 999 file resources. Agent working directory defaults to `/workspace`. Full detail: `shared/managed-agents-environments.md` → Files API. + +**Round C — Identity, success criteria, environment:** +- [ ] Name? +- [ ] Job (one or two sentences — becomes the system prompt)? +- [ ] **What does "done" look like?** Push for concrete, checkable success criteria — not "a good report" but "a CSV with a numeric `price` column per SKU." Explicit criteria give the agent a clear target and let you verify the result; vague ones leave it guessing what "done" means. If they're gradeable, plan to wire an **Outcome** in §2 so the harness grades-and-revises against them. See `shared/managed-agents-outcomes.md`. +- [ ] Networking: unrestricted internet from the container, or lock egress to specific hosts? (If locked, MCP server domains must be in `allowed_hosts` or tools silently fail.) +- [ ] Model? (default `claude-opus-4-8`) + +--- + +## 2. Set up the session + +Per-run. Points at the agent + environment, attaches credentials, kicks off. + +**Vault credentials** (if the agent declared MCP servers): +- [ ] Existing vault, or create one? (`client.beta.vaults.create()` + `vaults.credentials.create()`) + +Credentials are write-only, matched to MCP servers by URL, auto-refreshed. See `shared/managed-agents-tools.md` → Vaults. + +**Kickoff — pick one:** +- [ ] **Conversational:** a first `user.message` to the agent. +- [ ] **Outcome-graded** (recommended when §Round C produced checkable criteria): send a `user.define_outcome` with a rubric *instead of* a `user.message` — the harness iterates and grades against the rubric until satisfied. Don't send both. See `shared/managed-agents-outcomes.md`. + +Session creation blocks until all resources mount. Open the event stream before sending the kickoff. Stream is SSE; break on `session.status_terminated`, or on `session.status_idle` with a terminal `stop_reason` — i.e. anything except `requires_action`, which fires transiently while the session waits on a tool confirmation or custom-tool result (see `shared/managed-agents-client-patterns.md` Pattern 5). Usage lands on `span.model_request_end`. Agent-written artifacts end up in `/mnt/session/outputs/` — download via `files.list({scope_id: session.id, betas: ["managed-agents-2026-04-01"]})`. + +**Console escape hatch.** In the runtime block you emit, print the session's Console URL right after `sessions.create()` so the user can watch it in the UI while iterating: `print(f"Watch in Console: https://platform.claude.com/workspaces/default/sessions/{session.id}")` (swap `default` for the user's workspace slug if they named one). + +--- + +## 3. Pre-flight viability check — reconcile the job against the resources + +**Do this before emitting any code.** A common, avoidable failure is an under-resourced run: the ask is clear, but the agent is missing a tool, a credential, data access, or the context to act. The agent discovers the gap a few turns in, flails, and gives up — burning the budget to produce nothing. The gap is usually visible at setup time. Catch it here, not after the session fails. + +Walk the stated job clause by clause. For each action the agent must take, confirm a resource covers it — and name the gap out loud if one doesn't: + +| Gap class | Check | If missing | +|---|---|---| +| **Tool / integration** (most catchable upfront — config is statically inspectable) | Every verb in the job maps to an enabled tool or MCP server. "Triage tickets" → a ticketing MCP server; "open a PR" → GitHub MCP server (a `github_repository` mount alone can't open PRs); "search the web" → `web_search` enabled in the toolset. | Add the tool/MCP server in §Round A, or cut the ask from the job. | +| **Credential / access** | Every MCP server has a vault credential attached (§2). Every external host the job touches is reachable — networking `unrestricted`, or the host is in `allowed_hosts`. | Create/attach the vault; widen `allowed_hosts`. These don't fail until runtime — the smoke-test in §4 is how you surface them cheaply. | +| **Data** | Every file, dataset, or repo the job references is mounted as a `resource` (file, `github_repository`, or memory store). | Upload + mount it in §Round B, or tell the agent where to fetch it from. | +| **Prompt quality / criteria** | The job is specific enough to act on, and "done" is checkable (§Round C). | Tighten the job; wire an Outcome. | + +State any unmet gaps to the user and resolve them before generating code. Don't emit a config you already know is under-resourced — an agent can't complete a task it lacks the tools, credentials, or data for. + +--- + +## 4. Emit the code + +Go straight from the last interview answer to the code — no preamble about the setup-vs-runtime split, no "the critical thing to internalize…", no lecture about `agents.create()` being one-time. The two-block structure below already shows that; don't narrate it. Generate **two clearly-separated blocks**: + +**Block 1 — Setup (run once, store the IDs).** Prefer emitting this as **YAML files + `ant` CLI commands** — agents and environments are version-controlled definitions, and the CLI flow is what users should check into their repo and run from CI. Fall back to SDK code only if the user explicitly wants setup in-language or the `ant` CLI is unavailable. + +Emit: +1. `.agent.yaml` with everything from §Round A–C (flat: `name`, `model`, `system`, `tools`, `mcp_servers`, `skills`) +2. `.environment.yaml` with §Round C networking +3. The apply commands: + ```sh + AGENT_ID=$(ant beta:agents create < .agent.yaml --transform id -r) + ENV_ID=$(ant beta:environments create < .environment.yaml --transform id -r) + # CI sync: ant beta:agents update --agent-id "$AGENT_ID" --version N < .agent.yaml + ``` + +See `shared/anthropic-cli.md` for the full CLI reference. If emitting SDK code instead, label it `# ONE-TIME SETUP — run once, save the IDs to config/.env` and call `environments.create()` → `agents.create()`. + +**Block 2 — Runtime (run on every invocation).** This is SDK code in the detected language (Python/TS/cURL — see SKILL.md → Language Detection). The runtime path needs to react programmatically to events (tool confirmations, custom tool results, reconnect), which is SDK territory — don't emit shell loops here. +1. Load `env_id` + `agent_id` from config/env +2. `sessions.create(agent=AGENT_ID, environment_id=ENV_ID, resources=[...], vault_ids=[...])` — this blocks until resources mount, so a bad file/repo mount surfaces *here*, before any tokens are spent. +3. **Smoke-test first when the job depends on MCP servers, credentials, or reachable hosts.** Credential and MCP-connectivity failures don't surface at `sessions.create()` — only when the agent first tries to use them. Send one cheap probe turn ("Confirm you can reach and list 1–2 items; don't start the task yet"), check it succeeded, *then* send the real kickoff. A few hundred tokens here beats a runaway session that flails on a missing credential and gives up. Skip for agents with no external dependencies. +4. Open stream, `events.send()` the kickoff (a `user.message`, or a `user.define_outcome` if §2 chose the outcome-graded path), loop until `session.status_terminated` or `session.status_idle && stop_reason.type !== 'requires_action'` (see `shared/managed-agents-client-patterns.md` Pattern 5 for the full gate — do not break on bare `session.status_idle`) + +> ⚠️ **Never emit `agents.create()` and `sessions.create()` in the same unguarded block.** That teaches the user to create a new agent on every run — the #1 anti-pattern. If they need a single script, wrap agent creation in `if not os.getenv("AGENT_ID"):`. + +Pull exact syntax from `python/managed-agents/README.md`, `typescript/managed-agents/README.md`, or `curl/managed-agents.md`. Don't invent field names. diff --git a/.agents/skills/claude-api/shared/managed-agents-outcomes.md b/.agents/skills/claude-api/shared/managed-agents-outcomes.md new file mode 100644 index 00000000..aee3f4e3 --- /dev/null +++ b/.agents/skills/claude-api/shared/managed-agents-outcomes.md @@ -0,0 +1,106 @@ +# Managed Agents — Outcomes + +An **outcome** elevates a session from *conversation* to *work*: you state what "done" looks like, and the harness runs an iterate → grade → revise loop until the artifact meets the rubric, hits `max_iterations`, or is interrupted. A separate **grader** (independent context window) scores each iteration against your rubric and feeds per-criterion gaps back to the agent. + +The SDK sets the `managed-agents-2026-04-01` beta header automatically on all `client.beta.sessions.*` calls; no additional header is required for outcomes. + +--- + +## The `user.define_outcome` event + +Outcomes are not a field on `sessions.create()`. You create a normal session, then send a `user.define_outcome` event. The agent starts working on receipt — **do not also send a `user.message`** to kick it off. + +```python +session = client.beta.sessions.create( + agent=AGENT_ID, + environment_id=ENVIRONMENT_ID, + title="Financial analysis on Costco", +) + +client.beta.sessions.events.send( + session_id=session.id, + events=[ + { + "type": "user.define_outcome", + "description": "Build a DCF model for Costco in .xlsx", + "rubric": {"type": "text", "content": RUBRIC_MD}, + # or: "rubric": {"type": "file", "file_id": rubric.id} + "max_iterations": 5, # optional; default 3, max 20 + } + ], +) +``` + +| Field | Type | Notes | +|---|---|---| +| `type` | `"user.define_outcome"` | | +| `description` | string | The task. This is what the agent works toward — no separate `user.message` needed. | +| `rubric` | `{type: "text", content}` \| `{type: "file", file_id}` | **Required.** Markdown with explicit, independently gradeable criteria. Upload once via `client.beta.files.upload(...)` (beta `files-api-2025-04-14`) to reuse across sessions. | +| `max_iterations` | int | Optional. Default **3**, max **20**. | + +The event is echoed back on the stream with a server-assigned `outcome_id` and `processed_at`. + +> **Writing rubrics.** Use explicit, gradeable criteria ("CSV has a numeric `price` column"), not vibes ("data looks good") — the grader scores each criterion independently, so vague criteria produce noisy loops. If you don't have a rubric, have Claude analyze a known-good artifact and turn that analysis into one. + +--- + +## Outcome-specific events + +These appear on the standard event stream (`sessions.events.stream` / `.list`) alongside the usual `agent.*` / `session.*` events. + +| Event | Payload highlights | Meaning | +|---|---|---| +| `span.outcome_evaluation_start` | `outcome_id`, `iteration` (0-indexed) | Grader began scoring iteration *N*. | +| `span.outcome_evaluation_ongoing` | `outcome_id` | Heartbeat while the grader runs. Grader reasoning is opaque — you see *that* it's working, not *what* it's thinking. | +| `span.outcome_evaluation_end` | `outcome_evaluation_start_id`, `outcome_id`, `iteration`, `result`, `explanation`, `usage` | Grader finished one iteration. `result` drives what happens next (table below). | + +### `span.outcome_evaluation_end.result` + +| `result` | Next | +|---|---| +| `satisfied` | Session → `idle`. Terminal for this outcome. | +| `needs_revision` | Agent starts another iteration. | +| `max_iterations_reached` | No further grader cycles. Agent may run one final revision, then session → `idle`. | +| `failed` | Session → `idle`. Rubric fundamentally doesn't match the task (e.g. description and rubric contradict). | +| `interrupted` | Only emitted if `_start` had already fired before a `user.interrupt` arrived. | + +```json +{ + "type": "span.outcome_evaluation_end", + "id": "sevt_01jkl...", + "outcome_evaluation_start_id": "sevt_01def...", + "outcome_id": "outc_01a...", + "result": "satisfied", + "explanation": "All 12 criteria met: revenue projections use 5 years of historical data, ...", + "iteration": 0, + "usage": { "input_tokens": 2400, "output_tokens": 350, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 1800 }, + "processed_at": "2026-03-25T14:03:00Z" +} +``` + +--- + +## Checking status & retrieving deliverables + +**Status** — either watch the stream for `span.outcome_evaluation_end`, or poll the session and read `outcome_evaluations`: + +```python +session = client.beta.sessions.retrieve(session.id) +for ev in session.outcome_evaluations: + print(f"{ev.outcome_id}: {ev.result}") # outc_01a...: satisfied +``` + +**Deliverables** — the agent writes to `/mnt/session/outputs/`. Once idle, fetch via the Files API with `scope_id=session.id`. This is the same session-outputs mechanism documented in `shared/managed-agents-environments.md` → Session outputs (including the dual-beta-header requirement on `files.list`). + +--- + +## Interaction rules & pitfalls + +- **One outcome at a time.** Chain by sending the next `user.define_outcome` only after the previous one's terminal `span.outcome_evaluation_end` (`satisfied` / `max_iterations_reached` / `failed` / `interrupted`). The session retains history across chained outcomes. +- **Steering is allowed but optional.** You *may* send `user.message` events mid-outcome to nudge direction, but the agent already knows to keep working until terminal — don't send "keep going" prompts. +- **`user.interrupt` pauses the current outcome** — it marks `result: "interrupted"` and leaves the session `idle`, ready for a new outcome or conversational turn. +- **After terminal, the session is reusable** — continue conversationally or define a new outcome. +- **Outcome ≠ session-create field.** Don't put `outcome`, `rubric`, or `description` on `sessions.create()` — outcomes are always sent as a `user.define_outcome` event. +- **Idle-break gate is unchanged.** In your drain loop, keep using `event.type === 'session.status_idle' && event.stop_reason?.type !== 'requires_action'` — do **not** gate on `span.outcome_evaluation_end` alone (on `needs_revision` the session keeps running). See `shared/managed-agents-client-patterns.md` Pattern 5. + +For the raw HTTP shapes and per-language SDK bindings beyond Python, WebFetch `https://platform.claude.com/docs/en/managed-agents/define-outcomes.md` (see `shared/live-sources.md`). diff --git a/.agents/skills/claude-api/shared/managed-agents-overview.md b/.agents/skills/claude-api/shared/managed-agents-overview.md new file mode 100644 index 00000000..858ef18c --- /dev/null +++ b/.agents/skills/claude-api/shared/managed-agents-overview.md @@ -0,0 +1,70 @@ +# Managed Agents — Overview + +Managed Agents provisions a container per session as the agent's workspace. The agent loop runs on Anthropic's orchestration layer; the container is where the agent's *tools* execute — bash commands, file operations, code. You create a persisted **Agent** config (model, system prompt, tools, MCP servers, skills), then start **Sessions** that reference it. The session streams events back to you; you send user messages and tool results in. + +## ⚠️ THE MANDATORY FLOW: Agent (once) → Session (every run) + +**Why agents are separate objects: versioning.** An agent is a persisted, versioned config — every update creates a new immutable version, and sessions pin to a version at creation time. This lets you iterate on the agent (tweak the prompt, add a tool) without breaking sessions already running, roll back if a change regresses, and A/B test versions side-by-side. None of that works if you `agents.create()` fresh on every run. + +Every session references a pre-created `/v1/agents` object. Create the agent once, store the ID, and reuse it across runs. + +| Step | Call | Frequency | +|---|---|---| +| 1 | `POST /v1/agents` — `model`, `system`, `tools`, `mcp_servers`, `skills` live here | **ONCE.** Store `agent.id` **and** `agent.version`. | +| 2 | `POST /v1/sessions` — `agent: "agent_abc123"` or `{type: "agent", id, version}` | **Every run.** String shorthand uses latest version. | + +If you're about to write `sessions.create()` with `model`, `system`, or `tools` on the session body — **stop**. Those fields live on `agents.create()`. The session takes a *pointer* only. + +**When generating code, separate setup from runtime.** `agents.create()` belongs in a setup script (or a guarded `if agent_id is None:` block), not at the top of the hot path. If the user's code calls `agents.create()` on every invocation, they're accumulating orphaned agents and paying the create latency for nothing. The correct shape is: create once → persist the ID (config file, env var, secrets manager) → every run loads the ID and calls `sessions.create()`. + +**To change the agent's behavior, use `POST /v1/agents/{id}` — don't create a new one.** Each update bumps the version; running sessions keep their pinned version, new sessions get the latest (or pin explicitly via `{type: "agent", id, version}`). See `shared/managed-agents-core.md` → Agents → Versioning. To change `tools`/`mcp_servers`/`vault_ids` on **one running session** without touching the agent object, use `sessions.update()` — see `shared/managed-agents-core.md` → Updating the agent configuration mid-session. + +## Beta Headers + +Managed Agents is in beta. The SDK sets required beta headers automatically: + +| Beta Header | What it enables | +| ------------------------------ | ---------------------------------------------------- | +| `managed-agents-2026-04-01` | Agents, Environments, Sessions, Events, Session Resources, Session Threads, Outcomes, Multiagent, Vaults, Credentials, Memory Stores | +| `skills-2025-10-02` | Skills API (for managing custom skill definitions) | +| `files-api-2025-04-14` | Files API for file uploads | + +**Which beta header goes where:** The SDK sets `managed-agents-2026-04-01` automatically on `client.beta.{agents,environments,sessions,vaults,memory_stores}.*` calls, and `files-api-2025-04-14` / `skills-2025-10-02` automatically on `client.beta.files.*` / `client.beta.skills.*` calls. You do NOT need to add the Skills or Files beta header when calling Managed Agents endpoints. **Exception — session-scoped file listing:** `client.beta.files.list({scope_id: session.id})` is a Files endpoint that takes a Managed Agents parameter, so it needs **both** headers. Pass `betas: ["managed-agents-2026-04-01"]` explicitly on that call (the SDK adds the Files header; you add the Managed Agents one). See `shared/managed-agents-environments.md` → Session outputs. + + +## Reading Guide + +| User wants to... | Read these files | +| -------------------------------------- | ------------------------------------------------------- | +| **Get started from scratch / "help me set up an agent"** | `shared/managed-agents-onboarding.md` — guided interview (WHERE→WHO→WHAT→WATCH), then emit code | +| Understand how the API works | `shared/managed-agents-core.md` | +| See the full endpoint reference | `shared/managed-agents-api-reference.md` | +| **Create an agent** (required first step) | `shared/managed-agents-core.md` (Agents section) + language file | +| Update/version an agent | `shared/managed-agents-core.md` (Agents → Versioning) — update, don't re-create | +| Create a session | `shared/managed-agents-core.md` + `{lang}/managed-agents/README.md` | +| Configure tools and permissions | `shared/managed-agents-tools.md` | +| Set up MCP servers | `shared/managed-agents-tools.md` (MCP Servers section) | +| Stream events / handle tool_use | `shared/managed-agents-events.md` + language file | +| Get notified of session state changes via webhook (no polling) | `shared/managed-agents-webhooks.md` — Console-registered endpoint, HMAC verify, thin payload + fetch | +| Define an outcome / rubric-graded iterate loop | `shared/managed-agents-outcomes.md` — `user.define_outcome` event, grader, `span.outcome_evaluation_*` events | +| Coordinate multiple agents / subagents / threads | `shared/managed-agents-multiagent.md` — `multiagent: {type: "coordinator", agents: [...]}` on the agent, session threads, cross-posted tool confirmations | +| Set up environments | `shared/managed-agents-environments.md` + language file | +| Run tool execution in your own infra / VPC (self-hosted sandbox) | `shared/managed-agents-self-hosted-sandboxes.md` — `config:{type:"self_hosted"}`, `ANTHROPIC_ENVIRONMENT_KEY`, `EnvironmentWorker.run()` / `ant beta:worker poll` | +| Upload files / attach repos | `shared/managed-agents-environments.md` (Resources) | +| Give agents persistent memory across sessions | `shared/managed-agents-memory.md` — memory stores, `memory_store` session resource, preconditions, versions/redact | +| Define agents/environments as version-controlled YAML; drive the API from the shell | `shared/anthropic-cli.md` — `ant beta:agents create < agent.yaml`, `--transform`, `@file` inlining | +| Store MCP credentials | `shared/managed-agents-tools.md` (Vaults section) | +| Call a non-MCP API / CLI that needs a secret | `shared/managed-agents-client-patterns.md` Pattern 9 — no container env vars; vaults are MCP-only; keep the secret host-side via a custom tool | + +## Common Pitfalls + +- **Agent FIRST, then session — NO EXCEPTIONS** — the session's `agent` field accepts **only** a string ID or `{type: "agent", id, version}`. `model`, `system`, `tools`, `mcp_servers`, `skills` are **top-level fields on `POST /v1/agents`**, never on `sessions.create()`. If the user hasn't created an agent, that is step zero of every example. +- **Agent ONCE, not every run** — `agents.create()` is a setup step. Store the returned `agent_id` and reuse it; don't call `agents.create()` at the top of your hot path. If the agent's config needs to change, `POST /v1/agents/{id}` — each update creates a new version, and sessions can pin to a specific version for reproducibility. +- **MCP auth goes through vaults** — the agent's `mcp_servers` array declares `{type, name, url}` only (no auth). Credentials live in vaults (`client.beta.vaults.credentials.create`) and attach to sessions via `vault_ids`. Anthropic auto-refreshes OAuth tokens using the stored refresh token. +- **Reconcile resources before the first run** — a session with a clear ask but a missing tool, credential, data mount, or context will discover the gap mid-run, then flail and give up. Before creating the session, check that every action in the task maps to a configured tool/MCP server, every MCP server has a vault credential, and every referenced file/host is mounted/reachable. When helping a user set one up, run the reconciliation in `shared/managed-agents-onboarding.md` → §3 Pre-flight viability check. +- **Stream to get events** — `GET /v1/sessions/{id}/events/stream` is the primary way to receive agent output in real-time. +- **SSE stream has no replay — reconnect with consolidation** — if the stream drops while a `agent.tool_use`, `agent.mcp_tool_use`, or `agent.custom_tool_use` is pending resolution (`user.tool_confirmation` for the first two, `user.custom_tool_result` for the last one), the session deadlocks (client disconnects → session idles → reconnect happens → no client resolution happens). On every (re)connect: open stream with `GET /v1/sessions/{id}/events/stream` , fetch `GET /v1/sessions/{id}/events`, dedupe by event ID, then proceed. See `shared/managed-agents-events.md` → Reconnecting after a dropped stream. +- **Don't trust HTTP-library timeouts as wall-clock caps** — `requests` `timeout=(c, r)` and `httpx.Timeout(n)` are *per-chunk* read timeouts; they reset every byte, so a trickling connection can block indefinitely. For a hard deadline on raw-HTTP polling, track `time.monotonic()` at the loop level and bail explicitly. Prefer the SDK's `sessions.events.stream()` / `session.events.list()` over hand-rolled HTTP. See `shared/managed-agents-events.md` → Receiving Events. +- **Messages queue** — you can send events while the session is `running` or `idle`; they're processed in order. No need to wait for a response before sending the next message. +- **Environment `config.type` is `"cloud"` or `"self_hosted"`** — `cloud` runs the container on Anthropic's infrastructure; `self_hosted` moves tool execution to your own (see `shared/managed-agents-self-hosted-sandboxes.md`). +- **Archive is permanent on every resource** — archiving an agent, environment, session, vault, credential, or memory store makes it read-only with no unarchive. For agents, environments, and memory stores specifically, archived resources cannot be referenced by new sessions (existing sessions continue). Do not call `.archive()` on a production agent, environment, or memory store as cleanup — **always confirm with the user before archiving**. diff --git a/.agents/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md b/.agents/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md new file mode 100644 index 00000000..091becc6 --- /dev/null +++ b/.agents/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md @@ -0,0 +1,173 @@ +# Managed Agents — Self-Hosted Sandboxes + +With `config.type: "self_hosted"`, the **agent loop stays on Anthropic's orchestration layer** but **tool execution moves to infrastructure you control** — bash, file ops, and code run inside your container, so filesystem contents and network egress never leave your environment. Contrast with `config.type: "cloud"`, where Anthropic runs the container. Connectivity is **outbound-only**: your worker long-polls Anthropic's work queue; Anthropic never dials into your network. + +## Flow + +``` +1. Create environment: config: {type: "self_hosted"} → env_... +2. Generate environment key (Console, on the environment page) → sk-ant-oat01-... as ANTHROPIC_ENVIRONMENT_KEY +3. Run a worker: EnvironmentWorker.run() or ant beta:worker poll +4. Sessions reference environment_id=env_... exactly as for cloud +``` + +## Create the environment + +```python +client = anthropic.Anthropic() + +environment = client.beta.environments.create( + name="self-hosted", config={"type": "self_hosted"} +) +``` + +`{"type": "self_hosted"}` is the entire config — there are no pool, capacity, or networking sub-fields; you control those on your side. + +## Run a worker — SDK (primary path) + +`EnvironmentWorker` wraps the poll → dispatch → tool-execute loop. `.run()` is the always-on loop; `.run_one()` / `.runOne()` handles one work item (for webhook-driven wake). + +**Python — always-on:** + +```python +import asyncio +import os +from anthropic import AsyncAnthropic +from anthropic.lib.environments import EnvironmentWorker + + +async def main() -> None: + environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"] + environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"] + async with AsyncAnthropic(auth_token=environment_key) as client: + await EnvironmentWorker( + client, + environment_id=environment_id, + environment_key=environment_key, + workdir="/workspace", + ).run() + + +asyncio.run(main()) +``` + +**TypeScript — always-on:** + +```typescript +import Anthropic from "@anthropic-ai/sdk"; +import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments"; + +const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!; +const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!; +const client = new Anthropic({ authToken: environmentKey }); +const ctrl = new AbortController(); +process.once("SIGTERM", () => ctrl.abort()); + +await new EnvironmentWorker({ + client, + environmentId, + environmentKey, + workdir: "/workspace", + signal: ctrl.signal +}).run(); +``` + +**Customizing tools.** `EnvironmentWorker` runs the built-in toolset by default. To add or replace tools, use `AgentToolContext(workdir=, client=, session_id=)` with `beta_agent_toolset(env)` / `betaAgentToolset(env)` and pass the resulting tools to the lower-level `tool_runner()`. Skills attached to the agent are downloaded into `{workdir}/skills//` before tool calls begin (`AgentToolContext` handles this when given `client` and `session_id`). Downloaded skill files are marked executable automatically by the CLI and SDK; if you implement skills download yourself, you set permissions. + +> **Runtime deps:** the SDK helpers require `/bin/bash` at that exact path. The TypeScript SDK additionally requires `unzip`, `tar`, and Node.js 22+. These are resolved at fixed paths and do **not** respect `PATH` overrides. + +## Run a worker — `ant` CLI (fixed tools) + +The `ant` CLI ships a worker with the fixed built-in toolset (`bash`, `read`, `write`, `edit`, `glob`, `grep`). Install per `shared/anthropic-cli.md`, then: + +```sh +export ANTHROPIC_ENVIRONMENT_KEY=sk-ant-oat01-... +ant beta:worker poll --environment-id env_... --workdir /workspace +``` + +- `--workdir` is the directory tools operate in (default `.`); tool calls are sandboxed to it. +- `--environment-key` overrides the env var. +- `--on-work ` could break out of the script tag and execute attacker-controlled JavaScript in the Control UI origin. Version 2026.2.15 removed inline script injection and serve bootstrap config from a JSON endpoint and added a restrictive Content Security Policy for the Control UI (`script-src 'self'`, no inline scripts).", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-20T00:16:17.620", + "references": [ + "https://github.com/openclaw/openclaw/commit/3b4096e02e7e335f99f5986ec1bd566e90b14a7e", + "https://github.com/openclaw/openclaw/commit/adc818db4a4b3b8d663e7674ef20436947514e1b", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.15" + ], + "cvss_score": 5.8, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27009", + "exploitability_score": "medium", + "exploitability_rationale": "Medium CVSS score (5.8); requires local access; XSS has limited impact in headless agents", + "attack_vector_analysis": { + "is_network_accessible": false, + "requires_authentication": true, + "requires_user_interaction": true, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-27008", + "severity": "medium", + "type": "unknown_cwe_73", + "nvd_category_id": "CWE-73", + "title": "OpenClaw is a personal AI assistant. Prior to version 2026.2.15, a bug in `download` skill installat...", + "description": "OpenClaw is a personal AI assistant. Prior to version 2026.2.15, a bug in `download` skill installation allowed `targetDir` values from skill frontmatter to resolve outside the per-skill tools directory if not strictly validated. In the admin-only `skills.install` flow, this could write files outside the intended install sandbox. Version 2026.2.15 contains a fix for the issue.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-20T00:16:17.460", + "references": [ + "https://github.com/openclaw/openclaw/commit/2363e1b0853a028e47f90dcc1066e3e9809d65f1", + "https://github.com/openclaw/openclaw/commit/b6305e97256d67e439719faacf5af3de9727d6e1", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.15" + ], + "cvss_score": 6.7, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27008", + "exploitability_score": "medium", + "exploitability_rationale": "Medium CVSS score (6.7); requires local access", + "attack_vector_analysis": { + "is_network_accessible": false, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-27007", + "severity": "low", + "type": "unknown_cwe_1254", + "nvd_category_id": "CWE-1254", + "title": "OpenClaw is a personal AI assistant. Prior to version 2026.2.15, `normalizeForHash` in `src/agents/s...", + "description": "OpenClaw is a personal AI assistant. Prior to version 2026.2.15, `normalizeForHash` in `src/agents/sandbox/config-hash.ts` recursively sorted arrays that contained only primitive values. This made order-sensitive sandbox configuration arrays hash to the same value even when order changed. In OpenClaw sandbox flows, this hash is used to decide whether existing sandbox containers should be recreated. As a result, order-only config changes (for example Docker `dns` and `binds` array order) could be treated as unchanged and stale containers could be reused. This is a configuration integrity issue affecting sandbox recreation behavior. Starting in version 2026.2.15, array ordering is preserved during hash normalization; only object key ordering remains normalized for deterministic hashing.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-20T00:16:17.303", + "references": [ + "https://github.com/openclaw/openclaw/commit/41ded303b4f6dae5afa854531ff837c3276ad60b", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.15", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-xxvh-5hwj-42pp" + ], + "cvss_score": 3.3, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27007", + "exploitability_score": "low", + "exploitability_rationale": "Low CVSS score (3.3); requires local access", + "attack_vector_analysis": { + "is_network_accessible": false, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-27004", + "severity": "medium", + "type": "unknown_cwe_209", + "nvd_category_id": "CWE-209", + "title": "OpenClaw is a personal AI assistant. Prior to version 2026.2.15, in some shared-agent deployments, O...", + "description": "OpenClaw is a personal AI assistant. Prior to version 2026.2.15, in some shared-agent deployments, OpenClaw session tools (`sessions_list`, `sessions_history`, `sessions_send`) allowed broader session targeting than some operators intended. This is primarily a configuration/visibility-scoping issue in multi-user environments where peers are not equally trusted. In Telegram webhook mode, monitor startup also did not fall back to per-account `webhookSecret` when only the account-level secret was configured. In shared-agent, multi-user, less-trusted environments: session-tool access could expose transcript content across peer sessions. In single-agent or trusted environments, practical impact is limited. In Telegram webhook mode, account-level secret wiring could be missed unless an explicit monitor webhook secret override was provided. Version 2026.2.15 fixes the issue.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-20T00:16:17.140", + "references": [ + "https://github.com/openclaw/openclaw/commit/c6c53437f7da033b94a01d492e904974e7bda74c", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-6hf3-mhgc-cm65" + ], + "cvss_score": 5.5, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27004", + "exploitability_score": "medium", + "exploitability_rationale": "Medium CVSS score (5.5); requires local access", + "attack_vector_analysis": { + "is_network_accessible": false, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-27003", + "severity": "medium", + "type": "unknown_cwe_522", + "nvd_category_id": "CWE-522", + "title": "OpenClaw is a personal AI assistant. Telegram bot tokens can appear in error messages and stack trac...", + "description": "OpenClaw is a personal AI assistant. Telegram bot tokens can appear in error messages and stack traces (for example, when request URLs include `https://api.telegram.org/bot/...`). Prior to version 2026.2.15, OpenClaw logged these strings without redaction, which could leak the bot token into logs, crash reports, CI output, or support bundles. Disclosure of a Telegram bot token allows an attacker to impersonate the bot and take over Bot API access. Users should upgrade to version 2026.2.15 to obtain a fix and rotate the Telegram bot token if it may have been exposed.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-20T00:16:16.983", + "references": [ + "https://github.com/openclaw/openclaw/commit/cf69907015b659e5025efb735ee31bd05c4ee3d5", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-chf7-jq6g-qrwv" + ], + "cvss_score": 5.5, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27003", + "exploitability_score": "medium", + "exploitability_rationale": "Medium CVSS score (5.5); requires local access", + "attack_vector_analysis": { + "is_network_accessible": false, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-27002", + "severity": "critical", + "type": "execution_with_unnecessary_privileges", + "nvd_category_id": "CWE-250", + "title": "OpenClaw is a personal AI assistant. Prior to version 2026.2.15, a configuration injection issue in ...", + "description": "OpenClaw is a personal AI assistant. Prior to version 2026.2.15, a configuration injection issue in the Docker tool sandbox could allow dangerous Docker options (bind mounts, host networking, unconfined profiles) to be applied, enabling container escape or host data access. OpenClaw 2026.2.15 blocks dangerous sandbox Docker settings and includes runtime enforcement when building `docker create` args; config-schema validation for `network=host`, `seccompProfile=unconfined`, `apparmorProfile=unconfined`; and security audit findings to surface dangerous sandbox docker config. As a workaround, do not configure `agents.*.sandbox.docker.binds` to mount system directories or Docker socket paths, keep `agents.*.sandbox.docker.network` at `none` (default) or `bridge`, and do not use `unconfined` for seccomp/AppArmor profiles.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-20T00:16:16.827", + "references": [ + "https://github.com/openclaw/openclaw/commit/887b209db47f1f9322fead241a1c0b043fd38339", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.15", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-w235-x559-36mg" + ], + "cvss_score": 9.8, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27002", + "exploitability_score": "high", + "exploitability_rationale": "Critical CVSS score (9.8); remotely exploitable without authentication; RCE is critical in agent deployments", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": false, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-27001", + "severity": "high", + "type": "command_injection", + "nvd_category_id": "CWE-77", + "title": "OpenClaw is a personal AI assistant. Prior to version 2026.2.15, OpenClaw embedded the current worki...", + "description": "OpenClaw is a personal AI assistant. Prior to version 2026.2.15, OpenClaw embedded the current working directory (workspace path) into the agent system prompt without sanitization. If an attacker can cause OpenClaw to run inside a directory whose name contains control/format characters (for example newlines or Unicode bidi/zero-width markers), those characters could break the prompt structure and inject attacker-controlled instructions. Starting in version 2026.2.15, the workspace path is sanitized before it is embedded into any LLM prompt output, stripping Unicode control/format characters and explicit line/paragraph separators. Workspace path resolution also applies the same sanitization as defense-in-depth.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-20T00:16:16.653", + "references": [ + "https://github.com/openclaw/openclaw/commit/6254e96acf16e70ceccc8f9b2abecee44d606f79", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.15", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-2qj5-gwg2-xwc4" + ], + "cvss_score": 7.8, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27001", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (7.8); requires local access; RCE is critical in agent deployments", + "attack_vector_analysis": { + "is_network_accessible": false, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26972", + "severity": "medium", + "type": "path_traversal", + "nvd_category_id": "CWE-22", + "title": "OpenClaw is a personal AI assistant. In versions 2026.1.12 through 2026.2.12, OpenClaw browser downl...", + "description": "OpenClaw is a personal AI assistant. In versions 2026.1.12 through 2026.2.12, OpenClaw browser download helpers accepted an unsanitized output path. When invoked via the browser control gateway routes, this allowed path traversal to write downloads outside the intended OpenClaw temp downloads directory. This issue is not exposed via the AI agent tool schema (no `download` action). Exploitation requires authenticated CLI access or an authenticated gateway RPC token. Version 2026.2.13 fixes the issue.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-20T00:16:16.500", + "references": [ + "https://github.com/openclaw/openclaw/commit/7f0489e4731c8d965d78d6eac4a60312e46a9426", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.13", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-xwjm-j929-xq7c" + ], + "cvss_score": 6.7, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26972", + "exploitability_score": "high", + "exploitability_rationale": "Medium CVSS score (6.7); requires local access; path traversal affects agents with file access", + "attack_vector_analysis": { + "is_network_accessible": false, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26329", + "severity": "medium", + "type": "path_traversal", + "nvd_category_id": "CWE-22", + "title": "OpenClaw is a personal AI assistant. Prior to version 2026.2.14, authenticated attackers can read ar...", + "description": "OpenClaw is a personal AI assistant. Prior to version 2026.2.14, authenticated attackers can read arbitrary files from the Gateway host by supplying absolute paths or path traversal sequences to the browser tool's `upload` action. The server passed these paths to Playwright's `setInputFiles()` APIs without restricting them to a safe root. An attacker must reach the Gateway HTTP surface (or otherwise invoke the same browser control hook endpoints); present valid Gateway auth (bearer token / password), as required by the Gateway configuration (In common default setups, the Gateway binds to loopback and the onboarding wizard generates a gateway token even for loopback); and have the `browser` tool permitted by tool policy for the target session/context (and have browser support enabled). If an operator exposes the Gateway beyond loopback (LAN/tailnet/custom bind, reverse proxy, tunnels, etc.), the impact increases accordingly. Starting in version 2026.2.14, the upload paths are now confined to OpenClaw's temp uploads root (`DEFAULT_UPLOAD_DIR`) and traversal/escape paths are rejected.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-20T00:16:15.687", + "references": [ + "https://github.com/openclaw/openclaw/commit/3aa94afcfd12104c683c9cad81faf434d0dadf87", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.14", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-cv7m-c9jx-vg7q" + ], + "cvss_score": 6.5, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26329", + "exploitability_score": "high", + "exploitability_rationale": "Medium CVSS score (6.5); network accessible; path traversal affects agents with file access", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26328", + "severity": "medium", + "type": "improper_access_control", + "nvd_category_id": "CWE-284", + "title": "OpenClaw is a personal AI assistant. Prior to version 2026.2.14, under iMessage `groupPolicy=allowli...", + "description": "OpenClaw is a personal AI assistant. Prior to version 2026.2.14, under iMessage `groupPolicy=allowlist`, group authorization could be satisfied by sender identities coming from the DM pairing store, broadening DM trust into group contexts. Version 2026.2.14 fixes the issue.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-20T00:16:15.523", + "references": [ + "https://github.com/openclaw/openclaw/commit/872079d42fe105ece2900a1dd6ab321b92da2d59", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.14", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-g34w-4xqq-h79m" + ], + "cvss_score": 6.5, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26328", + "exploitability_score": "medium", + "exploitability_rationale": "Medium CVSS score (6.5); network accessible", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26327", + "severity": "medium", + "type": "unknown_cwe_345", + "nvd_category_id": "CWE-345", + "title": "OpenClaw is a personal AI assistant. Discovery beacons (Bonjour/mDNS and DNS-SD) include TXT records...", + "description": "OpenClaw is a personal AI assistant. Discovery beacons (Bonjour/mDNS and DNS-SD) include TXT records such as `lanHost`, `tailnetDns`, `gatewayPort`, and `gatewayTlsSha256`. TXT records are unauthenticated. Prior to version 2026.2.14, some clients treated TXT values as authoritative routing/pinning inputs. iOS and macOS used TXT-provided host hints (`lanHost`/`tailnetDns`) and ports (`gatewayPort`) to build the connection URL. iOS and Android allowed the discovery-provided TLS fingerprint (`gatewayTlsSha256`) to override a previously stored TLS pin. On a shared/untrusted LAN, an attacker could advertise a rogue `_openclaw-gw._tcp` service. This could cause a client to connect to an attacker-controlled endpoint and/or accept an attacker certificate, potentially exfiltrating Gateway credentials (`auth.token` / `auth.password`) during connection. As of time of publication, the iOS and Android apps are alpha/not broadly shipped (no public App Store / Play Store release). Practical impact is primarily limited to developers/testers running those builds, plus any other shipped clients relying on discovery on a shared/untrusted LAN. Version 2026.2.14 fixes the issue. Clients now prefer the resolved service endpoint (SRV + A/AAAA) over TXT-provided routing hints. Discovery-provided fingerprints no longer override stored TLS pins. In iOS/Android, first-time TLS pins require explicit user confirmation (fingerprint shown; no silent TOFU) and discovery-based direct connects are TLS-only. In Android, hostname verification is no longer globally disabled (only bypassed when pinning).", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T23:16:26.100", + "references": [ + "https://github.com/openclaw/openclaw/commit/d583782ee322a6faa1fe87ae52455e0d349de586", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.14", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-pv58-549p-qh99" + ], + "cvss_score": 6.5, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26327", + "exploitability_score": "high", + "exploitability_rationale": "Medium CVSS score (6.5); remotely exploitable without authentication", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": false, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26326", + "severity": "medium", + "type": "exposure_of_sensitive_information", + "nvd_category_id": "CWE-200", + "title": "OpenClaw is a personal AI assistant. Prior to version 2026.2.14, `skills.status` could disclose secr...", + "description": "OpenClaw is a personal AI assistant. Prior to version 2026.2.14, `skills.status` could disclose secrets to `operator.read` clients by returning raw resolved config values in `configChecks` for skill `requires.config` paths. Version 2026.2.14 stops including raw resolved config values in requirement checks (return only `{ path, satisfied }`) and narrows the Discord skill requirement to the token key. In addition to upgrading, users should rotate any Discord tokens that may have been exposed to read-scoped clients.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T23:16:25.950", + "references": [ + "https://github.com/openclaw/openclaw/commit/d3428053d95eefbe10ecf04f92218ffcba55ae5a", + "https://github.com/openclaw/openclaw/commit/ebc68861a61067fc37f9298bded3eec9de0ba783", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.14" + ], + "cvss_score": 4.3, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26326", + "exploitability_score": "medium", + "exploitability_rationale": "Medium CVSS score (4.3); network accessible", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26325", + "severity": "high", + "type": "improper_access_control", + "nvd_category_id": "CWE-284", + "title": "OpenClaw is a personal AI assistant. Prior to version 2026.2.14, a mismatch between `rawCommand` and...", + "description": "OpenClaw is a personal AI assistant. Prior to version 2026.2.14, a mismatch between `rawCommand` and `command[]` in the node host `system.run` handler could cause allowlist/approval evaluation to be performed on one command while executing a different argv. This only impacts deployments that use the node host / companion node execution path (`system.run` on a node), enable allowlist-based exec policy (`security=allowlist`) with approval prompting driven by allowlist misses (for example `ask=on-miss`), allow an attacker to invoke `system.run`. Default/non-node configurations are not affected. Version 2026.2.14 enforces `rawCommand`/`command[]` consistency (gateway fail-fast + node host validation).", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T23:16:25.800", + "references": [ + "https://github.com/openclaw/openclaw/commit/cb3290fca32593956638f161d9776266b90ab891", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.14", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-h3f9-mjwj-w476" + ], + "cvss_score": 7.2, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26325", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (7.2); network accessible; RCE is critical in agent deployments", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26324", + "severity": "high", + "type": "server_side_request_forgery", + "nvd_category_id": "CWE-918", + "title": "OpenClaw is a personal AI assistant. Prior to version 2026.2.14, OpenClaw's SSRF protection could be...", + "description": "OpenClaw is a personal AI assistant. Prior to version 2026.2.14, OpenClaw's SSRF protection could be bypassed using full-form IPv4-mapped IPv6 literals such as `0:0:0:0:0:ffff:7f00:1` (which is `127.0.0.1`). This could allow requests that should be blocked (loopback / private network / link-local metadata) to pass the SSRF guard. Version 2026.2.14 patches the issue.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T23:16:25.653", + "references": [ + "https://github.com/openclaw/openclaw/commit/c0c0e0f9aecb913e738742f73e091f2f72d39a19", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.14", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-jrvc-8ff5-2f9f" + ], + "cvss_score": 7.5, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26324", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (7.5); remotely exploitable without authentication; SSRF affects agents making external requests", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": false, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26323", + "severity": "high", + "type": "os_command_injection", + "nvd_category_id": "CWE-78", + "title": "OpenClaw is a personal AI assistant. Versions 2026.1.8 through 2026.2.13 have a command injection in...", + "description": "OpenClaw is a personal AI assistant. Versions 2026.1.8 through 2026.2.13 have a command injection in the maintainer/dev script `scripts/update-clawtributors.ts`. The issue affects contributors/maintainers (or CI) who run `bun scripts/update-clawtributors.ts` in a source checkout that contains a malicious commit author email (e.g. crafted `@users[.]noreply[.]github[.]com` values). Normal CLI usage is not affected (`npm i -g openclaw`): this script is not part of the shipped CLI and is not executed during routine operation. The script derived a GitHub login from `git log` author metadata and interpolated it into a shell command (via `execSync`). A malicious commit record could inject shell metacharacters and execute arbitrary commands when the script is run. Version 2026.2.14 contains a patch.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T23:16:25.500", + "references": [ + "https://github.com/openclaw/openclaw/commit/a429380e337152746031d290432a4b93aa553d55", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.14", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-m7x8-2w3w-pr42" + ], + "cvss_score": 8.8, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26323", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (8.8); network accessible; RCE is critical in agent deployments", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": false, + "requires_user_interaction": true, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26322", + "severity": "high", + "type": "server_side_request_forgery", + "nvd_category_id": "CWE-918", + "title": "OpenClaw is a personal AI assistant. Prior to OpenClaw version 2026.2.14, the Gateway tool accepted ...", + "description": "OpenClaw is a personal AI assistant. Prior to OpenClaw version 2026.2.14, the Gateway tool accepted a tool-supplied `gatewayUrl` without sufficient restrictions, which could cause the OpenClaw host to attempt outbound WebSocket connections to user-specified targets. This requires the ability to invoke tools that accept `gatewayUrl` overrides (directly or indirectly). In typical setups this is limited to authenticated operators, trusted automation, or environments where tool calls are exposed to non-operators. In other words, this is not a drive-by issue for arbitrary internet users unless a deployment explicitly allows untrusted users to trigger these tool calls. Some tool call paths allowed `gatewayUrl` overrides to flow into the Gateway WebSocket client without validation or allowlisting. This meant the host could be instructed to attempt connections to non-gateway endpoints (for example, localhost services, private network addresses, or cloud metadata IPs). In the common case, this results in an outbound connection attempt from the OpenClaw host (and corresponding errors/timeouts). In environments where the tool caller can observe the results, this can also be used for limited network reachability probing. If the target speaks WebSocket and is reachable, further interaction may be possible. Starting in version 2026.2.14, tool-supplied `gatewayUrl` overrides are restricted to loopback (on the configured gateway port) or the configured `gateway.remote.url`. Disallowed protocols, credentials, query/hash, and non-root paths are rejected.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T23:16:25.340", + "references": [ + "https://github.com/openclaw/openclaw/commit/c5406e1d2434be2ef6eb4d26d8f1798d718713f4", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.14", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-g6q9-8fvw-f7rf" + ], + "cvss_score": 7.6, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26322", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (7.6); network accessible; SSRF affects agents making external requests", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26321", + "severity": "high", + "type": "path_traversal", + "nvd_category_id": "CWE-22", + "title": "OpenClaw is a personal AI assistant. Prior to OpenClaw version 2026.2.14, the Feishu extension previ...", + "description": "OpenClaw is a personal AI assistant. Prior to OpenClaw version 2026.2.14, the Feishu extension previously allowed `sendMediaFeishu` to treat attacker-controlled `mediaUrl` values as local filesystem paths and read them directly. If an attacker can influence tool calls (directly or via prompt injection), they may be able to exfiltrate local files by supplying paths such as `/etc/passwd` as `mediaUrl`. Upgrade to OpenClaw `2026.2.14` or newer to receive a fix. The fix removes direct local file reads from this path and routes media loading through hardened helpers that enforce local-root restrictions.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T23:16:25.180", + "references": [ + "https://github.com/openclaw/openclaw/commit/5b4121d6011a48c71e747e3c18197f180b872c5d", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.14", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-8jpq-5h99-ff5r" + ], + "cvss_score": 7.5, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26321", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (7.5); remotely exploitable without authentication; path traversal affects agents with file access", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": false, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26320", + "severity": "medium", + "type": "unknown_cwe_451", + "nvd_category_id": "CWE-451", + "title": "OpenClaw is a personal AI assistant. OpenClaw macOS desktop client registers the `openclaw://` URL s...", + "description": "OpenClaw is a personal AI assistant. OpenClaw macOS desktop client registers the `openclaw://` URL scheme. For `openclaw://agent` deep links without an unattended `key`, the app shows a confirmation dialog that previously displayed only the first 240 characters of the message, but executed the full message after the user clicked \"Run.\" At the time of writing, the OpenClaw macOS desktop client is still in beta. In versions 2026.2.6 through 2026.2.13, an attacker could pad the message with whitespace to push a malicious payload outside the visible preview, increasing the chance a user approves a different message than the one that is actually executed. If a user runs the deep link, the agent may perform actions that can lead to arbitrary command execution depending on the user's configured tool approvals/allowlists. This is a social-engineering mediated vulnerability: the confirmation prompt could be made to misrepresent the executed message. The issue is fixed in 2026.2.14. Other mitigations include not approve unexpected \"Run OpenClaw agent?\" prompts triggered while browsing untrusted sites and usingunattended deep links only with a valid `key` for trusted personal automations.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "cpe:2.3:o:apple:macos:-:*:*:*:*:*:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T23:16:25.017", + "references": [ + "https://github.com/openclaw/openclaw/commit/28d9dd7a772501ccc3f71457b4adfee79084fe6f", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.14", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-7q2j-c4q5-rm27" + ], + "cvss_score": 6.5, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26320", + "exploitability_score": "medium", + "exploitability_rationale": "Medium CVSS score (6.5); network accessible", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": false, + "requires_user_interaction": true, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26319", + "severity": "high", + "type": "missing_authentication_for_critical_function", + "nvd_category_id": "CWE-306", + "title": "OpenClaw is a personal AI assistant. Versions 2026.2.13 and below allow the optional @openclaw/voice...", + "description": "OpenClaw is a personal AI assistant. Versions 2026.2.13 and below allow the optional @openclaw/voice-call plugin Telnyx webhook handler to accept unsigned inbound webhook requests when telnyx.publicKey is not configured, enabling unauthenticated callers to forge Telnyx events. Telnyx webhooks are expected to be authenticated via Ed25519 signature verification. In affected versions, TelnyxProvider.verifyWebhook() could effectively fail open when no Telnyx public key was configured, allowing arbitrary HTTP POST requests to the voice-call webhook endpoint to be treated as legitimate Telnyx events. This only impacts deployments where the Voice Call plugin is installed, enabled, and the webhook endpoint is reachable from the attacker (for example, publicly exposed via a tunnel/proxy). The issue has been fixed in version 2026.2.14.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T23:16:24.857", + "references": [ + "https://github.com/openclaw/openclaw/commit/29b587e73cbdc941caec573facd16e87d52f007b", + "https://github.com/openclaw/openclaw/commit/f47584fec86d6d73f2d483043a2ad0e7e3c50411", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.14" + ], + "cvss_score": 7.5, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26319", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (7.5); remotely exploitable without authentication", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": false, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26317", + "severity": "high", + "type": "cross_site_request_forgery", + "nvd_category_id": "CWE-352", + "title": "OpenClaw is a personal AI assistant. Prior to 2026.2.14, browser-facing localhost mutation routes ac...", + "description": "OpenClaw is a personal AI assistant. Prior to 2026.2.14, browser-facing localhost mutation routes accepted cross-origin browser requests without explicit Origin/Referer validation. Loopback binding reduces remote exposure but does not prevent browser-initiated requests from malicious origins. A malicious website can trigger unauthorized state changes against a victim's local OpenClaw browser control plane (for example opening tabs, starting/stopping the browser, mutating storage/cookies) if the browser control service is reachable on loopback in the victim's browser context. Starting in version 2026.2.14, mutating HTTP methods (POST/PUT/PATCH/DELETE) are rejected when the request indicates a non-loopback Origin/Referer (or `Sec-Fetch-Site: cross-site`). Other mitigations include enabling browser control auth (token/password) and avoid running with auth disabled.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T22:16:47.270", + "references": [ + "https://github.com/openclaw/openclaw/commit/b566b09f81e2b704bf9398d8d97d5f7a90aa94c3", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.14", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-3fqr-4cg8-h96q" + ], + "cvss_score": 7.1, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26317", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (7.1); network accessible", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": false, + "requires_user_interaction": true, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-26316", + "severity": "high", + "type": "incorrect_authorization", + "nvd_category_id": "CWE-863", + "title": "OpenClaw is a personal AI assistant. Prior to 2026.2.13, the optional BlueBubbles iMessage channel p...", + "description": "OpenClaw is a personal AI assistant. Prior to 2026.2.13, the optional BlueBubbles iMessage channel plugin could accept webhook requests as authenticated based only on the TCP peer address being loopback (`127.0.0.1`, `::1`, `::ffff:127.0.0.1`) even when the configured webhook secret was missing or incorrect. This does not affect the default iMessage integration unless BlueBubbles is installed and enabled. Version 2026.2.13 contains a patch. Other mitigations include setting a non-empty BlueBubbles webhook password and avoiding deployments where a public-facing reverse proxy forwards to a loopback-bound Gateway without strong upstream authentication.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T22:16:47.110", + "references": [ + "https://github.com/openclaw/openclaw/commit/743f4b28495cdeb0d5bf76f6ebf4af01f6a02e5a", + "https://github.com/openclaw/openclaw/commit/f836c385ffc746cb954e8ee409f99d079bfdcd2f", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.13" + ], + "cvss_score": 7.5, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-26316", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (7.5); remotely exploitable without authentication", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": false, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-25474", + "severity": "high", + "type": "unknown_cwe_345", + "nvd_category_id": "CWE-345", + "title": "OpenClaw is a personal AI assistant. In versions 2026.1.30 and below, if channels.telegram.webhookSe...", + "description": "OpenClaw is a personal AI assistant. In versions 2026.1.30 and below, if channels.telegram.webhookSecret is not set when in Telegram webhook mode, OpenClaw may accept webhook HTTP requests without verifying Telegram’s secret token header. In deployments where the webhook endpoint is reachable by an attacker, this can allow forged Telegram updates (for example spoofing message.from.id). If an attacker can reach the webhook endpoint, they may be able to send forged updates that are processed as if they came from Telegram. Depending on enabled commands/tools and configuration, this could lead to unintended bot actions. Note: Telegram webhook mode is not enabled by default. It is enabled only when `channels.telegram.webhookUrl` is configured. This issue has been fixed in version 2026.2.1.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T07:17:45.847", + "references": [ + "https://github.com/openclaw/openclaw/commit/3cbcba10cf30c2ffb898f0d8c7dfb929f15f8930", + "https://github.com/openclaw/openclaw/commit/5643a934799dc523ec2ef18c007e1aa2c386b670", + "https://github.com/openclaw/openclaw/commit/633fe8b9c17f02fcc68ecdb5ec212a5ace932f09" + ], + "cvss_score": 7.5, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25474", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (7.5); remotely exploitable without authentication", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": false, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-24764", + "severity": "low", + "type": "unknown_cwe_74", + "nvd_category_id": "CWE-74", + "title": "OpenClaw (formerly Clawdbot) is a personal AI assistant users run on their own devices. In versions ...", + "description": "OpenClaw (formerly Clawdbot) is a personal AI assistant users run on their own devices. In versions 2026.2.2 and below, when the Slack integration is enabled, channel metadata (topic/description) can be incorporated into the model's system prompt. Prompt injection is a documented risk for LLM-driven systems. This issue increases the injection surface by allowing untrusted Slack channel metadata to be treated as higher-trust system input. This issue has been fixed in version 2026.2.3.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-19T07:17:44.957", + "references": [ + "https://github.com/openclaw/openclaw/commit/35eb40a7000b59085e9c638a80fd03917c7a095e", + "https://github.com/openclaw/openclaw/releases/tag/v2026.2.3", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-782p-5fr5-7fj8" + ], + "cvss_score": 3.7, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-24764", + "exploitability_score": "low", + "exploitability_rationale": "Low CVSS score (3.7); network accessible", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": true, + "requires_user_interaction": true, + "complexity": "high" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-25593", + "severity": "high", + "type": "missing_authentication_for_critical_function", + "nvd_category_id": "CWE-306", + "title": "OpenClaw is a personal AI assistant. Prior to 2026.1.20, an unauthenticated local client could use t...", + "description": "OpenClaw is a personal AI assistant. Prior to 2026.1.20, an unauthenticated local client could use the Gateway WebSocket API to write config via config.apply and set unsafe cliPath values that were later used for command discovery, enabling command injection as the gateway user. This vulnerability is fixed in 2026.1.20.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-06T21:16:17.790", + "references": [ + "https://github.com/openclaw/openclaw/security/advisories/GHSA-g55j-c2v4-pjcg" + ], + "cvss_score": 8.4, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25593", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (8.4); requires local access; RCE is critical in agent deployments", + "attack_vector_analysis": { + "is_network_accessible": false, + "requires_authentication": false, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-25475", + "severity": "medium", + "type": "exposure_of_sensitive_information", + "nvd_category_id": "CWE-200", + "title": "OpenClaw is a personal AI assistant. Prior to version 2026.1.30, the isValidMedia() function in src/...", + "description": "OpenClaw is a personal AI assistant. Prior to version 2026.1.30, the isValidMedia() function in src/media/parse.ts allows arbitrary file paths including absolute paths, home directory paths, and directory traversal sequences. An agent can read any file on the system by outputting MEDIA:/path/to/file, exfiltrating sensitive data to the user/channel. This issue has been patched in version 2026.1.30.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-04T20:16:07.287", + "references": [ + "https://github.com/openclaw/openclaw/security/advisories/GHSA-r8g4-86fx-92mq" + ], + "cvss_score": 6.5, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25475", + "exploitability_score": "high", + "exploitability_rationale": "Medium CVSS score (6.5); network accessible; path traversal affects agents with file access", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-25157", + "severity": "high", + "type": "os_command_injection", + "nvd_category_id": "CWE-78", + "title": "OpenClaw is a personal AI assistant. Prior to version 2026.1.29, there is an OS command injection vu...", + "description": "OpenClaw is a personal AI assistant. Prior to version 2026.1.29, there is an OS command injection vulnerability via the Project Root Path in sshNodeCommand. The sshNodeCommand function constructed a shell script without properly escaping the user-supplied project path in an error message. When the cd command failed, the unescaped path was interpolated directly into an echo statement, allowing arbitrary command execution on the remote SSH host. The parseSSHTarget function did not validate that SSH target strings could not begin with a dash. An attacker-supplied target like -oProxyCommand=... would be interpreted as an SSH configuration flag rather than a hostname, allowing arbitrary command execution on the local machine. This issue has been patched in version 2026.1.29.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "cpe:2.3:o:apple:macos:-:*:*:*:*:*:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-04T20:16:06.577", + "references": [ + "https://github.com/openclaw/openclaw/security/advisories/GHSA-q284-4pvr-m585" + ], + "cvss_score": 7.7, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25157", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (7.7); requires local access; RCE is critical in agent deployments", + "attack_vector_analysis": { + "is_network_accessible": false, + "requires_authentication": false, + "requires_user_interaction": true, + "complexity": "high" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-24763", + "severity": "high", + "type": "os_command_injection", + "nvd_category_id": "CWE-78", + "title": "OpenClaw (formerly Clawdbot) is a personal AI assistant you run on your own devices. Prior to 2026....", + "description": "OpenClaw (formerly Clawdbot) is a personal AI assistant you run on your own devices. Prior to 2026.1.29, a command injection vulnerability existed in OpenClaw’s Docker sandbox execution mechanism due to unsafe handling of the PATH environment variable when constructing shell commands. An authenticated user able to control environment variables could influence command execution within the container context. This vulnerability is fixed in 2026.1.29.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-02T23:16:08.593", + "references": [ + "https://github.com/openclaw/openclaw/commit/771f23d36b95ec2204cc9a0054045f5d8439ea75", + "https://github.com/openclaw/openclaw/releases/tag/v2026.1.29", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-mc68-q9jw-2h3v" + ], + "cvss_score": 8.8, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-24763", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (8.8); network accessible; RCE is critical in agent deployments", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": true, + "requires_user_interaction": false, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-25253", + "severity": "high", + "type": "incorrect_resource_transfer_between_spheres", + "nvd_category_id": "CWE-669", + "title": "OpenClaw (aka clawdbot or Moltbot) before 2026.1.29 obtains a gatewayUrl value from a query string a...", + "description": "OpenClaw (aka clawdbot or Moltbot) before 2026.1.29 obtains a gatewayUrl value from a query string and automatically makes a WebSocket connection without prompting, sending a token value.", + "affected": [ + "cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*", + "openclaw@*" + ], + "platforms": [ + "openclaw" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-02-01T23:15:49.717", + "references": [ + "https://depthfirst.com/post/1-click-rce-to-steal-your-moltbot-data-and-keys", + "https://ethiack.com/news/blog/one-click-rce-moltbot", + "https://github.com/openclaw/openclaw/security/advisories/GHSA-g8p2-7wf7-98mq" + ], + "cvss_score": 8.8, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25253", + "exploitability_score": "high", + "exploitability_rationale": "High CVSS score (8.8); network accessible; RCE is critical in agent deployments", + "attack_vector_analysis": { + "is_network_accessible": true, + "requires_authentication": false, + "requires_user_interaction": true, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + }, + { + "id": "CVE-2026-22798", + "severity": "medium", + "type": "unknown_cwe_532", + "nvd_category_id": "CWE-532", + "title": "hermes is an implementation of the HERMES workflow to automatize software publication with rich meta...", + "description": "hermes is an implementation of the HERMES workflow to automatize software publication with rich metadata. From 0.8.1 to before 0.9.1, hermes subcommands take arbitrary options under the -O argument. These have been logged in raw form. If users provide sensitive data such as API tokens (e.g., via hermes deposit -O invenio_rdm.auth_token SECRET), these are written to the log file in plain text, making them available to whoever can access the log file. This vulnerability is fixed in 0.9.1.", + "affected": [ + "cpe:2.3:a:software-metadata.pub:hermes:*:*:*:*:*:python:*:*", + "hermes@*" + ], + "platforms": [ + "hermes" + ], + "action": "Review and update affected components. See NVD for remediation details.", + "published": "2026-01-12T22:16:08.780", + "references": [ + "https://github.com/softwarepub/hermes/commit/7f64f102e916c76dc44404b77ab2a80f5a4e59b1", + "https://github.com/softwarepub/hermes/commit/90cb86acd026e7841f2539ae7a1b284a7f263514", + "https://github.com/softwarepub/hermes/security/advisories/GHSA-jm5j-jfrm-hm23" + ], + "cvss_score": 5.9, + "nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-22798", + "exploitability_score": "medium", + "exploitability_rationale": "Medium CVSS score (5.9); requires local access", + "attack_vector_analysis": { + "is_network_accessible": false, + "requires_authentication": true, + "requires_user_interaction": true, + "complexity": "low" + }, + "exploit_detection": { + "exploit_available": false, + "exploit_sources": [] + } + } + ] +} diff --git a/.agents/skills/clawsec-feed/advisories/feed.json.sig b/.agents/skills/clawsec-feed/advisories/feed.json.sig new file mode 100644 index 00000000..6cd65a60 --- /dev/null +++ b/.agents/skills/clawsec-feed/advisories/feed.json.sig @@ -0,0 +1 @@ +0XMKs0QnzZYtU1YeMVNVpqzLecu8buTcBx+60hi7puHKARdshGlOSHZ8E27fo6qhz6MJx6/7zoIjCz6y+q1zBA== \ No newline at end of file diff --git a/.agents/skills/clawsec-feed/skill.json b/.agents/skills/clawsec-feed/skill.json new file mode 100644 index 00000000..ba2d9c80 --- /dev/null +++ b/.agents/skills/clawsec-feed/skill.json @@ -0,0 +1,70 @@ +{ + "name": "clawsec-feed", + "version": "0.0.7", + "description": "Security advisory feed monitoring for AI agents. Subscribe to community-driven threat intelligence.", + "author": "prompt-security", + "license": "AGPL-3.0-or-later", + "homepage": "https://clawsec.prompt.security", + "keywords": [ + "security", + "advisory", + "feed", + "agents", + "ai", + "threat-intel", + "monitoring" + ], + "sbom": { + "files": [ + { + "path": "SKILL.md", + "required": true, + "description": "Advisory feed skill documentation" + }, + { + "path": "CHANGELOG.md", + "required": true, + "description": "Version history for advisory feed updates" + }, + { + "path": "advisories/feed.json", + "required": true, + "description": "Community security advisory feed" + } + ] + }, + "openclaw": { + "emoji": "📡", + "category": "security", + "feed_url": "https://api.github.com/repos/prompt-security/ClawSec/releases?skill=clawsec-feed", + "requires": { + "bins": [ + "bash", + "curl", + "jq", + "shasum", + "unzip" + ] + }, + "execution": { + "always": false, + "persistence": "No local persistence or automation is created by the standalone feed package; recurring polling is handled by clawsec-suite or the operator.", + "network_egress": "Standalone installation downloads release artifacts and optional feed updates from Prompt Security GitHub/website endpoints." + }, + "operator_review": [ + "This package is primarily signed advisory data plus install instructions; it does not itself create cron jobs or send data outward.", + "Verify release provenance and checksums before installing on production hosts.", + "If you need automated polling or host-side enforcement, use clawsec-suite which owns that automation layer." + ], + "triggers": [ + "security advisories", + "check advisories", + "clawsec", + "threat feed", + "security alerts", + "vulnerability feed", + "advisory feed", + "security news" + ] + } +} diff --git a/.agents/skills/clawsec-nanoclaw/CHANGELOG.md b/.agents/skills/clawsec-nanoclaw/CHANGELOG.md new file mode 100644 index 00000000..7f06410b --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog + +## [0.0.5] - 2026-05-14 + +### Security +- Added explicit signed release artifact verification instructions for standalone installs, including `checksums.json`, `checksums.sig`, `signing-public.pem`, archive hash verification, and `SKILL.md`/`skill.json` checksum checks. + +All notable changes to the ClawSec NanoClaw compatibility skill will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.0.4] - 2026-04-16 + +### Changed + +- Moved signature-related local file reads into `lib/local_file_io.ts` and kept network fetch logic isolated in `lib/signatures.ts`. + +### Security + +- Reduced static false-positive exfiltration signals by separating local file I/O and remote fetch code paths. + +## [0.0.3] - 2026-03-09 + +### Security + +- Removed runtime public-key override from host-side package signature verification; verification now always uses the pinned ClawSec key. +- Removed unsigned-package override path in host-side verification flow. +- Added strict package/signature path policy for signature verification (`/tmp`, `/var/tmp`, `/workspace/ipc`, `/workspace/project/data`, `/workspace/project/tmp`, `/workspace/project/downloads`) with absolute-path, extension, symlink, and realpath boundary checks. +- Added policy-bound path enforcement for integrity approvals: approvals now require normalized paths that are explicitly present in non-ignored integrity policy targets. + +### Changed + +- Updated MCP signature verification tool docs and behavior to align with bounded path policy and pinned-key-only verification. +- Added regression tests for signature-verification and integrity-approval hardening invariants. + +## [0.0.2] - 2026-02-28 + +### Added + +- Exploitability-aware advisory output in NanoClaw MCP tools (`exploitability_score`, `exploitability_rationale`). +- Exploitability filtering (`exploitabilityScore`) for `clawsec_list_advisories`. + +### Changed + +- Updated NanoClaw advisory sorting and pre-install safety recommendation logic to prioritize exploitability context. +- Updated NanoClaw integration docs to match current host/container integration points (`src/ipc.ts`, `src/index.ts`) and current cache schema. +- Removed duplicate exploitability normalization logic from MCP advisory tools and now reuse `normalizeExploitabilityScore` from `lib/risk.ts`. +- Reused `matchesAffectedSpecifier` from `lib/advisories.ts` in MCP advisory tools to keep skill/version matching logic centralized and consistent. diff --git a/.agents/skills/clawsec-nanoclaw/INSTALL.md b/.agents/skills/clawsec-nanoclaw/INSTALL.md new file mode 100644 index 00000000..056f5112 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/INSTALL.md @@ -0,0 +1,327 @@ +# ClawSec for NanoClaw - Installation Guide + +This guide shows how to add ClawSec security monitoring to your NanoClaw deployment. + +## Overview + +ClawSec provides security advisory monitoring for NanoClaw through: +- **MCP Tools**: Agents can check for vulnerabilities via `clawsec_check_advisories` +- **Advisory Feed**: Automatic monitoring of https://clawsec.prompt.security/advisories/feed.json +- **Signature Verification**: Ed25519-signed feeds ensure integrity +- **Exploitability Context**: Advisories include exploitability score and rationale for triage + +## Prerequisites + +- NanoClaw >= 0.1.0 +- Node.js >= 18.0.0 +- Write access to NanoClaw installation directory + +## Installation Steps + +### 1. Copy Skill Files + +Copy the `clawsec-nanoclaw` skill directory to your NanoClaw installation: + +```bash +# From the ClawSec repository +cp -r skills/clawsec-nanoclaw /path/to/your/nanoclaw/skills/ +``` + +### 2. Integrate MCP Tools + +Add the ClawSec MCP tools to your NanoClaw container agent runner. + +**File**: `container/agent-runner/src/ipc-mcp-stdio.ts` + +```typescript +// Add these imports at the top to register all ClawSec MCP tools: + +// Advisory tools: clawsec_check_advisories, clawsec_check_skill_safety, +// clawsec_list_advisories, clawsec_refresh_cache +import '../../../skills/clawsec-nanoclaw/mcp-tools/advisory-tools.js'; + +// Signature verification: clawsec_verify_skill_package +import '../../../skills/clawsec-nanoclaw/mcp-tools/signature-verification.js'; + +// Integrity monitoring: clawsec_check_integrity, clawsec_approve_change, +// clawsec_integrity_status, clawsec_verify_audit +import '../../../skills/clawsec-nanoclaw/mcp-tools/integrity-tools.js'; +``` + +Each file calls `server.tool()` directly to register its tools. The `server`, +`writeIpcFile`, `TASKS_DIR`, and `groupFolder` variables must be available in +the scope where these files are imported (they are declared as ambient globals +in each tool file). + +### 3. Integrate IPC Handlers + +Add the host-side IPC handlers for ClawSec operations. + +**File**: `src/ipc.ts` + +```typescript +// Add these imports at the top +import { handleAdvisoryIpc } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js'; +import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js'; +import { SkillSignatureVerifier } from '../skills/clawsec-nanoclaw/host-services/skill-signature-handler.js'; + +// Initialize these once in host startup and pass through deps +const advisoryCacheManager = new AdvisoryCacheManager('/workspace/project/data', logger); +const signatureVerifier = new SkillSignatureVerifier(); + +// In processTaskIpc switch: +case 'refresh_advisory_cache': +case 'verify_skill_signature': + await handleAdvisoryIpc( + data, + { advisoryCacheManager, signatureVerifier }, + logger, + sourceGroup + ); + break; +default: + // existing task handling +} +``` + +### 4. Start Advisory Cache Service + +Add the advisory cache manager to your host services. + +**File**: `src/index.ts` (or your main entry point) + +```typescript +import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js'; + +// Start the service when your host process starts +async function main() { + // ... your existing initialization ... + + // Initialize cache manager and prime it at startup + const advisoryCacheManager = new AdvisoryCacheManager('/workspace/project/data', logger); + await advisoryCacheManager.initialize(); + + // Recommended refresh cadence (6h) + setInterval(() => { + advisoryCacheManager.refresh().catch((error) => { + logger.error({ error }, 'Periodic advisory cache refresh failed'); + }); + }, 6 * 60 * 60 * 1000); + + // ... rest of your startup ... +} +``` + +### 5. Restart NanoClaw + +Restart your NanoClaw instance to load the new MCP tools and services: + +```bash +# Stop NanoClaw +docker-compose down + +# Start with new configuration +docker-compose up -d +``` + +## Verification + +Test that ClawSec is working: + +### 1. Check MCP Tools Available + +From within a NanoClaw agent session, the following tools should be available: + +**Advisory Tools** (mcp-tools/advisory-tools.ts): +- `clawsec_check_advisories` - Scan installed skills for vulnerabilities +- `clawsec_check_skill_safety` - Pre-installation safety check +- `clawsec_list_advisories` - List all advisories with filtering +- `clawsec_refresh_cache` - Request immediate advisory cache refresh + +**Signature Verification** (mcp-tools/signature-verification.ts): +- `clawsec_verify_skill_package` - Verify Ed25519 signature on skill packages + - Uses pinned ClawSec public key (no runtime key override) + - Accepts staged package/signature paths only under `/tmp`, `/var/tmp`, `/workspace/ipc`, `/workspace/project/data`, `/workspace/project/tmp`, `/workspace/project/downloads` + +**Integrity Monitoring** (mcp-tools/integrity-tools.ts): +- `clawsec_check_integrity` - Check protected files for unauthorized changes +- `clawsec_approve_change` - Approve intentional file modification as new baseline +- `clawsec_integrity_status` - View current baseline status +- `clawsec_verify_audit` - Verify audit log hash chain integrity + +### 2. Test Advisory Checking + +Ask your NanoClaw agent: +``` +Check if any of my installed skills have security advisories +``` + +The agent should use the `clawsec_check_advisories` tool and report results. + +### 3. Check Advisory Cache + +Verify the cache file was created: +```bash +cat /workspace/project/data/clawsec-advisory-cache.json +``` + +You should see: +- `feed`: Array of advisories +- `fetchedAt`: Timestamp of last update +- `verified`: Should be `true` +- `publicKeyFingerprint`: SHA-256 fingerprint of the pinned signing key + +## Usage Examples + +### Agent Commands + +Once installed, your NanoClaw agents can: + +**Check for vulnerabilities:** +``` +Scan my installed skills for security issues +``` + +**Pre-installation check:** +``` +Is it safe to install skill-name@1.0.0? +``` + +**List all advisories:** +``` +Show me all ClawSec security advisories +``` + +### Manual Tool Invocation + +You can also call the MCP tools directly from agent code: + +```typescript +// Check all installed skills +const result = await tools.clawsec_check_advisories({ + installRoot: '/home/node/.claude/skills' +}); + +// Check specific skill before installation +const safetyCheck = await tools.clawsec_check_skill_safety({ + skillName: 'risky-skill', + skillVersion: '1.0.0' +}); +``` + +## Configuration + +### Cache Location + +Default: `/workspace/project/data/clawsec-advisory-cache.json` + +To change, pass a different data directory path to `new AdvisoryCacheManager(dataDir, logger)`. + +### Refresh Interval + +Default: 6 hours + +To change, update the `setInterval(...)` duration (in milliseconds) in host startup. + +### Feed URL + +Default: `https://clawsec.prompt.security/advisories/feed.json` + +To use a mirror or custom feed, update `FEED_URL` in `skills/clawsec-nanoclaw/host-services/advisory-cache.ts`. + +## Platform-Specific Advisories + +ClawSec advisories can target specific platforms: + +- **`platforms: ["nanoclaw"]`**: Only affects NanoClaw +- **`platforms: ["openclaw"]`**: Only affects OpenClaw/MoltBot +- **`platforms: ["openclaw", "nanoclaw"]`**: Affects both +- **No `platforms` field**: Applies to all platforms + +Platform metadata is preserved in advisory records and can be filtered by your policy layer. + +## Security + +### Signature Verification + +All advisory feeds are Ed25519 signed. The public key is pinned in: +``` +skills/clawsec-nanoclaw/advisories/feed-signing-public.pem +``` + +Feeds failing signature verification are rejected. + +### Cache Integrity + +The advisory cache includes: +- Cryptographic signature of feed contents +- Verification status +- Timestamp of last successful fetch + +Never manually edit the cache file - it will break signature verification. + +## Troubleshooting + +### Tools Not Appearing + +**Problem**: MCP tools not showing up in agent + +**Solution**: +1. Check that you added the import and registration in `ipc-mcp-stdio.ts` +2. Restart the container +3. Check container logs for import errors + +### Cache Not Updating + +**Problem**: Advisory cache is empty or stale + +**Solution**: +1. Check that `AdvisoryCacheManager.initialize()` is called in your host entry point +2. Verify network access to `clawsec.prompt.security` +3. Check host logs for fetch errors +4. Manually trigger: `curl https://clawsec.prompt.security/advisories/feed.json` + +### Signature Verification Failing + +**Problem**: Cache shows `"verified": false` + +**Solution**: +1. Ensure public key file exists at correct path +2. Check file permissions (should be readable) +3. Verify feed URL is correct (not using HTTP instead of HTTPS) +4. Check for corrupted downloads (try clearing cache and refetching) + +### IPC Communication Issues + +**Problem**: Tools return errors about IPC + +**Solution**: +1. Verify IPC handlers are registered in `src/ipc.ts` +2. Check that IPC directory exists and is writable +3. Ensure host process is running +4. Check host logs for handler errors + +## Uninstallation + +To remove ClawSec from NanoClaw: + +1. Remove MCP tool registration from `ipc-mcp-stdio.ts` +2. Remove IPC handler registration from `src/ipc.ts` +3. Remove `AdvisoryCacheManager` initialization from host entry point +4. Delete the skill directory: `rm -rf skills/clawsec-nanoclaw` +5. Delete the cache file: `rm /workspace/project/data/clawsec-advisory-cache.json` +6. Restart NanoClaw + +## Support + +- **Documentation**: https://clawsec.prompt.security/ +- **Issues**: https://github.com/prompt-security/clawsec/issues +- **Security**: security@prompt.security + +## License + +AGPL-3.0-or-later + +--- + +**Questions?** Open an issue or check the main ClawSec documentation. diff --git a/.agents/skills/clawsec-nanoclaw/README.md b/.agents/skills/clawsec-nanoclaw/README.md new file mode 100644 index 00000000..9ca28a98 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/README.md @@ -0,0 +1,151 @@ +# ClawSec for NanoClaw + +ClawSec now supports NanoClaw, a containerized WhatsApp bot powered by Claude agents. + +## What Changed + +### Advisory Feed Monitoring +- **NVD CVE Pipeline**: Now monitors for NanoClaw-specific keywords + - "NanoClaw", "WhatsApp-bot", "baileys" (WhatsApp library) + - Container-related vulnerabilities +- **Platform Targeting**: Advisories can specify `platforms: ["nanoclaw"]` for NanoClaw-specific issues + +### Keywords Added +The CVE monitoring now includes: +- `NanoClaw` - Direct product name +- `WhatsApp-bot` - Core functionality +- `baileys` - WhatsApp client library dependency + +## Advisory Schema + +Advisories now support optional `platforms` field: + +```json +{ + "id": "CVE-2026-XXXXX", + "platforms": ["openclaw", "nanoclaw"], + "severity": "critical", + "type": "prompt_injection", + "affected": ["skill-name@1.0.0"], + "action": "Update to version 1.0.1" +} +``` + +**Platform values:** +- `"openclaw"` - Affects OpenClaw/ClawdBot/MoltBot only +- `"nanoclaw"` - Affects NanoClaw only +- `["openclaw", "nanoclaw"]` - Affects both platforms +- (empty/missing) - Applies to all platforms (backward compatible) + +## ClawSec NanoClaw Skill + +ClawSec provides a complete security skill for NanoClaw deployments: + +**Location**: `skills/clawsec-nanoclaw/` + +### Features + +- **9 MCP Tools** for agents to manage security: + - `clawsec_check_advisories` - Scan installed skills for vulnerabilities + - `clawsec_check_skill_safety` - Pre-installation safety checks + - `clawsec_list_advisories` - Browse advisory feed with filtering + - `clawsec_refresh_cache` - Request immediate advisory cache refresh + - `clawsec_verify_skill_package` - Verify Ed25519 signatures on skill packages + - `clawsec_check_integrity` - Check protected files for unauthorized changes + - `clawsec_approve_change` - Approve intentional file modifications + - `clawsec_integrity_status` - View file baseline status + - `clawsec_verify_audit` - Verify audit log hash chain + +- **Advisory Cache Service**: Host-managed feed fetching with signature validation +- **Signature Verification**: Ed25519-signed feeds ensure integrity +- **Exploitability Context**: Surfaces `exploitability_score` and rationale to reduce alert fatigue +- **IPC Communication**: Container-safe host communication + +### Installation + +1. Copy the skill to your NanoClaw deployment: + ```bash + cp -r skills/clawsec-nanoclaw /path/to/nanoclaw/skills/ + ``` + +2. Follow the detailed guide at `skills/clawsec-nanoclaw/INSTALL.md` + +### Quick Integration + +The skill integrates into three places: + +**1. MCP Tools** (container): +```typescript +// container/agent-runner/src/ipc-mcp-stdio.ts +import '../../../skills/clawsec-nanoclaw/mcp-tools/advisory-tools.js'; +``` + +**2. IPC Handlers** (host): +```typescript +// src/ipc.ts +import { handleAdvisoryIpc } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js'; +``` + +**3. Cache Service** (host): +```typescript +// src/index.ts +import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js'; +``` + +### Advisory Feed + +NanoClaw consumes the same feed as OpenClaw: +``` +https://clawsec.prompt.security/advisories/feed.json +``` + +The feed is Ed25519 signed and automatically fetched by the cache service. + +## Team Credits + +This integration was developed by a team of 8 specialized agents coordinated to adapt ClawSec for NanoClaw: + +- **pioneer-repo-scout** - ClawSec architecture analysis +- **pioneer-nanoclaw-scout** - NanoClaw architecture analysis +- **architect** - Integration design and coordination +- **advisory-specialist** - Advisory feed integration +- **integrity-specialist** - File integrity design +- **installer-specialist** - Signature verification implementation +- **tester** - Test infrastructure and validation +- **documenter** - Documentation + +Total contribution: 3000+ lines of code and comprehensive design documents. + +## What's Included + +The `clawsec-nanoclaw` skill provides: + +- **1,730 lines** of production-ready TypeScript code +- **MCP Tools** (350 lines): Agent-facing vulnerability checking +- **Advisory Cache** (492 lines): Automatic feed fetching and caching +- **Signature Verification** (387 lines): Ed25519 signature validation +- **Advisory Matching** (289 lines): Skill-to-vulnerability correlation +- **IPC Handlers** (212 lines): Container-to-host communication +- **Complete Documentation**: Installation guide, usage examples, troubleshooting + +## Future Enhancements + +Planned features for future releases: +- File integrity monitoring (soul-guardian adaptation for containers) +- Real-time advisory alerts via WebSocket +- WhatsApp-native security alert formatting +- Behavioral analysis and anomaly detection +- Custom/private advisory feed support + +## Documentation + +- [Skill Documentation](skills/clawsec-nanoclaw/SKILL.md) - Features and architecture +- [Installation Guide](skills/clawsec-nanoclaw/INSTALL.md) - Detailed setup instructions +- [ClawSec Main README](README.md) - Overall ClawSec documentation +- [Security & Signing](../../wiki/security-signing-runbook.md) - Signature verification details + +## Support + +- **Issues**: https://github.com/prompt-security/clawsec/issues +- **Security**: security@prompt.security +- NanoClaw Repository: https://github.com/qwibitai/nanoclaw diff --git a/.agents/skills/clawsec-nanoclaw/SKILL.md b/.agents/skills/clawsec-nanoclaw/SKILL.md new file mode 100644 index 00000000..e856b3a7 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/SKILL.md @@ -0,0 +1,280 @@ +--- +name: clawsec-nanoclaw +version: 0.0.5 +description: Use when checking for security vulnerabilities in NanoClaw skills, before installing new skills, or when asked about security advisories affecting the bot +--- + +# ClawSec for NanoClaw + +Security advisory monitoring that protects your WhatsApp bot from known vulnerabilities in skills and dependencies. + +## Overview + +ClawSec provides MCP tools that check installed skills against a curated feed of security advisories. It prevents installation of vulnerable skills, includes exploitability context for triage, and alerts you to issues in existing ones. + +**Core principle:** Check before you install. Monitor what's running. + +## When to Use + +Use ClawSec tools when: +- Installing a new skill (check safety first) +- User asks "are my skills secure?" +- Investigating suspicious behavior +- Regular security audits +- After receiving security notifications + +Do NOT use for: +- Code review (use other tools) +- Performance issues (different concern) +- General debugging + +## MCP Tools Available + +### Pre-Installation Check + +```typescript +// Before installing any skill +const safety = await tools.clawsec_check_skill_safety({ + skillName: 'new-skill', + skillVersion: '1.0.0' // optional +}); + +if (!safety.safe) { + // Show user the risks before proceeding + console.warn(`Security issues: ${safety.advisories.map(a => a.id)}`); +} +``` + +### Security Audit + +```typescript +// Check all installed skills (defaults to ~/.Codex/skills in the container) +const result = await tools.clawsec_check_advisories({ + installRoot: '/home/node/.Codex/skills' // optional +}); + +if (result.matches.some((m) => + m.advisory.severity === 'critical' || m.advisory.exploitability_score === 'high' +)) { + // Alert user immediately + console.error('Urgent advisories found!'); +} +``` + +### Browse Advisories + +```typescript +// List advisories with filters +const advisories = await tools.clawsec_list_advisories({ + severity: 'high', // optional + exploitabilityScore: 'high' // optional +}); +``` + +## Quick Reference + +| Task | Tool | Key Parameter | +|------|------|---------------| +| Pre-install check | `clawsec_check_skill_safety` | `skillName` | +| Audit all skills | `clawsec_check_advisories` | `installRoot` (optional) | +| Browse feed | `clawsec_list_advisories` | `severity`, `type`, `exploitabilityScore` (optional) | +| Verify package signature | `clawsec_verify_skill_package` | `packagePath` | +| Refresh advisory cache | `clawsec_refresh_cache` | (none) | +| Check file integrity | `clawsec_check_integrity` | `mode`, `autoRestore` (optional) | +| Approve file change | `clawsec_approve_change` | `path` | +| View baseline status | `clawsec_integrity_status` | `path` (optional) | +| Verify audit log | `clawsec_verify_audit` | (none) | + +## Common Patterns + +### Pattern 1: Safe Skill Installation + +```typescript +// ALWAYS check before installing +const safety = await tools.clawsec_check_skill_safety({ + skillName: userRequestedSkill +}); + +if (safety.safe) { + // Proceed with installation + await installSkill(userRequestedSkill); +} else { + // Show user the risks and get confirmation + await showSecurityWarning(safety.advisories); + if (await getUserConfirmation()) { + await installSkill(userRequestedSkill); + } +} +``` + +### Pattern 2: Periodic Security Check + +```typescript +// Add to scheduled tasks +schedule_task({ + prompt: "Check advisories using clawsec_check_advisories and alert when critical or high-exploitability matches appear", + schedule_type: "cron", + schedule_value: "0 9 * * *" // Daily at 9am +}); +``` + +### Pattern 3: User Security Query + +``` +User: "Are my skills secure?" + +You: I'll check installed skills for known vulnerabilities. +[Use clawsec_check_advisories] + +Response: +✅ No urgent issues found. +- 2 low-severity/low-exploitability advisories +- All skills up to date +``` + +## Common Mistakes + +### ❌ Installing without checking +```typescript +// DON'T +await installSkill('untrusted-skill'); +``` + +```typescript +// DO +const safety = await tools.clawsec_check_skill_safety({ + skillName: 'untrusted-skill' +}); +if (safety.safe) await installSkill('untrusted-skill'); +``` + +### ❌ Ignoring exploitability context +```typescript +// DON'T: Use severity only +if (advisory.severity === 'high') { + notifyNow(advisory); +} +``` + +```typescript +// DO: Use exploitability + severity +if ( + advisory.exploitability_score === 'high' || + advisory.severity === 'critical' +) { + notifyNow(advisory); +} +``` + +### ❌ Skipping critical severity +```typescript +// DON'T: Ignore high exploitability in medium severity advisories +if (advisory.severity === 'critical') alert(); +``` + +```typescript +// DO: Prioritize exploitability and severity together +if (advisory.exploitability_score === 'high' || advisory.severity === 'critical') { + // Alert immediately +} +``` + +## Implementation Details + +**Feed Source**: https://clawsec.prompt.security/advisories/feed.json + +**Update Frequency**: Every 6 hours (automatic) + +**Signature Verification**: Ed25519 signed feeds +**Package Verification Policy**: pinned key only, bounded package/signature paths + +**Cache Location**: `/workspace/project/data/clawsec-advisory-cache.json` + +See [INSTALL.md](./INSTALL.md) for setup and [docs/](./docs/) for advanced usage. + +## Real-World Impact + +- Prevents installation of skills with known RCE vulnerabilities +- Alerts to supply chain attacks in dependencies +- Provides actionable remediation steps +- Zero false positives (curated feed only) + + +## Release Artifact Verification + +For standalone installs, verify the signed release manifest before trusting `SKILL.md`, `skill.json`, or the archive. The `skill.json` file is the package metadata/SBOM source, and the release pipeline signs `checksums.json` with the ClawSec release key. + +```bash +set -euo pipefail + +SKILL_NAME="clawsec-nanoclaw" +VERSION="0.0.5" +REPO="prompt-security/clawsec" +TAG="${SKILL_NAME}-v${VERSION}" +BASE="https://github.com/${REPO}/releases/download/${TAG}" +ZIP_NAME="${SKILL_NAME}-v${VERSION}.zip" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +RELEASE_PUBKEY_SHA256="711424e4535f84093fefb024cd1ca4ec87439e53907b305b79a631d5befba9c8" + +curl -fsSL "$BASE/checksums.json" -o "$TMP_DIR/checksums.json" +curl -fsSL "$BASE/checksums.sig" -o "$TMP_DIR/checksums.sig" +curl -fsSL "$BASE/signing-public.pem" -o "$TMP_DIR/signing-public.pem" +curl -fsSL "$BASE/$ZIP_NAME" -o "$TMP_DIR/$ZIP_NAME" +curl -fsSL "$BASE/SKILL.md" -o "$TMP_DIR/SKILL.md" +curl -fsSL "$BASE/skill.json" -o "$TMP_DIR/skill.json" + +ACTUAL_PUBKEY_SHA256="$(openssl pkey -pubin -in "$TMP_DIR/signing-public.pem" -outform DER | shasum -a 256 | awk '{print $1}')" +if [ "$ACTUAL_PUBKEY_SHA256" != "$RELEASE_PUBKEY_SHA256" ]; then + echo "ERROR: signing-public.pem fingerprint mismatch" >&2 + exit 1 +fi + +openssl base64 -d -A -in "$TMP_DIR/checksums.sig" -out "$TMP_DIR/checksums.sig.bin" +openssl pkeyutl -verify -rawin -pubin \ + -inkey "$TMP_DIR/signing-public.pem" \ + -sigfile "$TMP_DIR/checksums.sig.bin" \ + -in "$TMP_DIR/checksums.json" >/dev/null + +hash_file() { + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + sha256sum "$1" | awk '{print $1}' + fi +} + +verify_manifest_file() { + asset="$1" + path="$2" + expected="$(jq -r --arg asset "$asset" '.files[$asset].sha256 // empty' "$TMP_DIR/checksums.json")" + if [ -z "$expected" ]; then + echo "ERROR: checksums.json missing $asset" >&2 + exit 1 + fi + actual="$(hash_file "$path")" + if [ "$actual" != "$expected" ]; then + echo "ERROR: checksum mismatch for $asset" >&2 + exit 1 + fi +} + +expected_archive="$(jq -r '.archive.sha256 // empty' "$TMP_DIR/checksums.json")" +if [ -z "$expected_archive" ]; then + echo "ERROR: checksums.json missing archive.sha256" >&2 + exit 1 +fi +actual_archive="$(hash_file "$TMP_DIR/$ZIP_NAME")" +if [ "$actual_archive" != "$expected_archive" ]; then + echo "ERROR: archive checksum mismatch" >&2 + exit 1 +fi + +verify_manifest_file "SKILL.md" "$TMP_DIR/SKILL.md" +verify_manifest_file "skill.json" "$TMP_DIR/skill.json" + +echo "Signed release manifest, archive, SKILL.md, and skill.json verified." +``` + +Only install or extract the archive after this verification succeeds. diff --git a/.agents/skills/clawsec-nanoclaw/advisories/feed-signing-public.pem b/.agents/skills/clawsec-nanoclaw/advisories/feed-signing-public.pem new file mode 100644 index 00000000..ae1e3b1e --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/advisories/feed-signing-public.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAS7nijfMcUoOBCj4yOXJX+GYGv2pFl2Yaha1P4v5Cm6A= +-----END PUBLIC KEY----- diff --git a/.agents/skills/clawsec-nanoclaw/docs/INTEGRITY.md b/.agents/skills/clawsec-nanoclaw/docs/INTEGRITY.md new file mode 100644 index 00000000..780922e0 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/docs/INTEGRITY.md @@ -0,0 +1,567 @@ +# File Integrity Monitoring for NanoClaw + +ClawSec's file integrity monitoring protects critical NanoClaw configuration files from unauthorized modification. + +## What It Does + +**Protects Critical Files:** +- `registered_groups.json` - Prevents unauthorized group access +- `CLAUDE.md` files - Protects agent instructions +- Container/host code - Alerts on unexpected changes + +**How It Works:** +1. **Baseline**: Stores SHA-256 hashes of approved file states +2. **Monitoring**: Periodically checks files for changes (drift) +3. **Restore**: Automatically reverts critical files to approved versions +4. **Audit**: Maintains tamper-evident log of all operations + +## Quick Start + +### Step 1: Verify Installation + +Check that integrity monitoring is available: + +```bash +# From container +ls /workspace/project/skills/clawsec-nanoclaw/guardian/ +# Should show: policy.json, integrity-monitor.ts +``` + +### Step 2: Initialize Baselines + +The first time integrity monitoring runs, it creates baselines automatically: + +```typescript +// Agent calls this (happens automatically on first integrity check) +await tools.clawsec_check_integrity(); +``` + +This creates: +``` +/workspace/project/data/soul-guardian/ +├── baselines.json # SHA-256 hashes +├── approved/ # File snapshots +│ ├── registered_groups.json +│ └── CLAUDE.md +├── patches/ # Diffs (empty initially) +├── quarantine/ # Tampered files (empty initially) +└── audit.jsonl # Event log +``` + +### Step 3: Enable Scheduled Monitoring + +Add to main group's scheduled tasks: + +```typescript +schedule_task({ + prompt: ` + Check file integrity with clawsec_check_integrity. + If drift detected and files restored, send WhatsApp message: + "⚠️ SECURITY ALERT + + Unauthorized changes detected and automatically reverted: + [list files that were restored] + + Review details: /workspace/project/data/soul-guardian/patches/" + `, + schedule_type: 'cron', + schedule_value: '*/30 * * * *', // Every 30 minutes + context_mode: 'isolated' +}); +``` + +That's it! Integrity monitoring is now active. + +## MCP Tools Reference + +### 1. `clawsec_check_integrity` + +Check all protected files for unauthorized changes. + +**Parameters:** +- `mode` (optional): `'check'` (default) or `'status'` + - `check`: Detect drift and auto-restore + - `status`: View baselines only (no drift detection) +- `autoRestore` (optional): `true` (default) or `false` + - If `false`, drift is detected but not auto-fixed + +**Output:** +```json +{ + "success": true, + "timestamp": "2026-02-25T12:00:00Z", + "drift_detected": false, + "files": [ + { + "path": "/workspace/project/data/registered_groups.json", + "status": "ok", + "mode": "restore", + "expected_sha": "abc123...", + "found_sha": "abc123..." + } + ], + "summary": { + "total": 3, + "ok": 3, + "drifted": 0, + "restored": 0, + "alerted": 0, + "errors": 0 + } +} +``` + +**Example:** +```typescript +const result = await tools.clawsec_check_integrity(); + +if (result.drift_detected) { + console.log('⚠️ Drift detected!'); + for (const file of result.files) { + if (file.status === 'restored') { + console.log(`✅ Restored: ${file.path}`); + console.log(` Diff: ${file.patch_path}`); + } else if (file.status === 'drifted') { + console.log(`⚠️ Changed: ${file.path} (alert only)`); + } + } +} +``` + +### 2. `clawsec_approve_change` + +Approve an intentional file modification as the new baseline. + +**When to use:** +- After legitimately updating CLAUDE.md +- After adding/removing groups in registered_groups.json +- After any intentional change to protected files + +**Parameters:** +- `path` (required): Absolute path to file +- `note` (optional): Explanation for audit log + +**Output:** +```json +{ + "success": true, + "path": "/workspace/group/CLAUDE.md", + "approved_at": "2026-02-25T12:00:00Z", + "approved_by": "agent", + "note": "Added new skill instructions" +} +``` + +**Example:** +```typescript +// After editing CLAUDE.md +await tools.clawsec_approve_change({ + path: '/workspace/group/CLAUDE.md', + note: 'Updated agent instructions for new skill' +}); + +console.log('✅ Change approved - new baseline created'); +``` + +### 3. `clawsec_integrity_status` + +View current baseline status without checking for drift. + +**Parameters:** +- `path` (optional): Specific file, or all if omitted + +**Output:** +```json +{ + "success": true, + "baseline_age": "2026-02-25T10:00:00Z", + "files": [ + { + "path": "/workspace/project/data/registered_groups.json", + "mode": "restore", + "priority": "critical", + "has_baseline": true, + "baseline_sha": "abc123...", + "approved_at": "2026-02-25T10:00:00Z", + "snapshot_exists": true + } + ] +} +``` + +**Example:** +```typescript +const status = await tools.clawsec_integrity_status(); + +console.log('Protected files:'); +for (const file of status.files) { + console.log(`- ${file.path} (${file.mode}, ${file.priority})`); + console.log(` Last approved: ${file.approved_at}`); +} +``` + +### 4. `clawsec_verify_audit` + +Verify audit log hash chain integrity. + +**No parameters.** + +**Output:** +```json +{ + "success": true, + "valid": true, + "entries": 42, + "errors": [] +} +``` + +**Example:** +```typescript +const verification = await tools.clawsec_verify_audit(); + +if (!verification.valid) { + console.log('🚨 CRITICAL: Audit log has been tampered with!'); + console.log('Errors:', verification.errors); +} else { + console.log(`✅ Audit log verified (${verification.entries} entries)`); +} +``` + +## Protected Files Policy + +### Critical Priority (Auto-Restore) + +**`/workspace/project/data/registered_groups.json`** +- **Risk**: Tampering grants unauthorized group access +- **Action**: Immediate auto-restore + alert + +**`/workspace/group/CLAUDE.md`** +- **Risk**: Modifies agent behavior +- **Action**: Immediate auto-restore + alert + +**`/workspace/project/groups/global/CLAUDE.md`** +- **Risk**: Affects all groups +- **Action**: Immediate auto-restore + alert + +### Medium Priority (Alert Only) + +**Container code** (`/workspace/project/container/**/*.ts`) +- **Risk**: Unexpected code changes +- **Action**: Alert for review (no auto-restore) + +**Host code** (`/workspace/project/host/**/*.ts`) +- **Risk**: Unexpected code changes +- **Action**: Alert for review (no auto-restore) + +### Ignored + +**IPC files** (`/workspace/ipc/**/*`) +- Changes are expected and frequent + +**Conversations** (`/workspace/group/conversations/**/*`) +- Changes are expected and frequent + +## Workflow Examples + +### Scenario 1: Scheduled Monitoring + +**Setup:** +```typescript +schedule_task({ + prompt: 'Run clawsec_check_integrity and alert on drift', + schedule_type: 'cron', + schedule_value: '*/30 * * * *' +}); +``` + +**What happens:** +1. Every 30 minutes, agent checks integrity +2. If drift detected in critical files: + - Files auto-restored to baseline + - Tampered versions quarantined + - Diff patch generated + - User alerted via WhatsApp +3. If drift in non-critical files: + - Alert only, no auto-restore + +### Scenario 2: Updating Agent Instructions + +**Workflow:** +```typescript +// 1. Edit CLAUDE.md +fs.writeFileSync('/workspace/group/CLAUDE.md', newInstructions); + +// 2. Test changes +// ... verify agent behaves correctly ... + +// 3. Approve changes +await tools.clawsec_approve_change({ + path: '/workspace/group/CLAUDE.md', + note: 'Added instructions for new weather skill' +}); + +// 4. Future integrity checks will use this new baseline +``` + +### Scenario 3: Adding a New Group + +**Workflow:** +```typescript +// 1. Add group to registered_groups.json +const groups = JSON.parse(fs.readFileSync('/workspace/project/data/registered_groups.json')); +groups['new-jid'] = { name: 'Family', folder: 'family', trigger: '@Andy' }; +fs.writeFileSync('/workspace/project/data/registered_groups.json', JSON.stringify(groups, null, 2)); + +// 2. Approve the change +await tools.clawsec_approve_change({ + path: '/workspace/project/data/registered_groups.json', + note: 'Added family group' +}); +``` + +### Scenario 4: Investigating Drift + +**When drift is detected:** +```typescript +const result = await tools.clawsec_check_integrity(); + +if (result.drift_detected) { + for (const file of result.files) { + if (file.status === 'restored') { + // Critical file was auto-restored + console.log(`🔧 Auto-restored: ${file.path}`); + console.log(`📄 Diff: ${file.patch_path}`); + console.log(`📦 Quarantine: ${file.quarantine_path}`); + + // Review the diff + const diff = fs.readFileSync(file.patch_path, 'utf-8'); + console.log('Changes that were reverted:'); + console.log(diff); + } + } +} +``` + +## Security Model + +### Threat Model + +**Protects Against:** +- Unauthorized file modifications +- Group hijacking (via registered_groups.json tampering) +- Agent instruction poisoning (via CLAUDE.md changes) +- Accidental file corruption + +**Does NOT Protect Against:** +- Attacker with full host access (can modify baselines) +- Simultaneous baseline + file modification +- Malicious scheduled tasks that approve their own changes + +### Baseline Storage + +**Location:** `/workspace/project/data/soul-guardian/` + +**Access Control:** +- Baselines written only by host process +- Containers access via IPC only +- No container can modify its own baselines + +**Integrity:** +- SHA-256 hashes (industry standard) +- Hash-chained audit log (tamper-evident) +- Atomic file operations (safe restores) + +### Audit Log + +**Format:** JSONL with hash chaining + +**Each entry includes:** +```json +{ + "ts": "2026-02-25T12:00:00Z", + "event": "drift", + "actor": "agent", + "path": "/workspace/group/CLAUDE.md", + "expected_sha": "abc123...", + "found_sha": "def456...", + "chain": { + "prev": "previous_entry_hash", + "hash": "this_entry_hash" + } +} +``` + +**Chain calculation:** +``` +hash = SHA-256(prev_hash + '\n' + canonical_json(entry_without_chain)) +``` + +This makes tampering detectable: changing any entry breaks the chain. + +## Troubleshooting + +### Integrity Check Fails + +**Symptom:** `clawsec_check_integrity` returns `success: false` + +**Causes:** +1. IntegrityService not initialized +2. Policy file missing +3. Baselines corrupted + +**Solution:** +```bash +# Check service status +ls /workspace/project/data/soul-guardian/ + +# If missing, reinitialize +rm -rf /workspace/project/data/soul-guardian/ +# Next integrity check will recreate baselines +``` + +### False Positives (Legitimate Changes Flagged) + +**Symptom:** File keeps getting restored even though changes are legitimate + +**Cause:** Baseline not updated after intentional changes + +**Solution:** +```typescript +await tools.clawsec_approve_change({ + path: '/path/to/file', + note: 'Legitimate change' +}); +``` + +### Audit Chain Broken + +**Symptom:** `clawsec_verify_audit` returns `valid: false` + +**Causes:** +1. Audit log manually edited +2. Filesystem corruption +3. Security breach + +**Solution:** +```typescript +const verification = await tools.clawsec_verify_audit(); +console.log('Errors:', verification.errors); + +// If corruption, backup and reset +cp /workspace/project/data/soul-guardian/audit.jsonl /tmp/audit-backup.jsonl +rm /workspace/project/data/soul-guardian/audit.jsonl +// Audit log will restart on next operation +``` + +### High Disk Usage + +**Symptom:** `/workspace/project/data/soul-guardian/` grows large + +**Causes:** +- Many drift events generate patches +- Quarantine files accumulate + +**Solution:** +```bash +# Clean old patches (older than 30 days) +find /workspace/project/data/soul-guardian/patches/ -mtime +30 -delete + +# Clean quarantine (after review) +rm /workspace/project/data/soul-guardian/quarantine/* +``` + +## Performance + +**Overhead:** +- Baseline check: ~10ms per file +- SHA-256 computation: ~1ms per KB +- Restore operation: ~20ms per file + +**Typical deployment:** +- 3-5 protected files +- 30-minute check interval +- < 0.1% CPU usage +- < 5MB disk usage + +## Advanced Topics + +### Custom Policy + +While the default policy is pinned by the skill, you can fork it: + +```bash +cp /workspace/project/skills/clawsec-nanoclaw/guardian/policy.json /workspace/project/data/custom-policy.json +``` + +Edit and reinitialize: +```typescript +// Update IntegrityMonitor initialization +new IntegrityMonitor({ + policyPath: '/workspace/project/data/custom-policy.json', + stateDir: '/workspace/project/data/soul-guardian' +}); +``` + +### Manual Baseline Export + +```bash +# Export current baselines +cp /workspace/project/data/soul-guardian/baselines.json /tmp/baselines-backup.json + +# Export approved snapshots +tar -czf /tmp/approved-snapshots.tar.gz /workspace/project/data/soul-guardian/approved/ +``` + +### Baseline Import (Disaster Recovery) + +```bash +# Restore baselines +cp /tmp/baselines-backup.json /workspace/project/data/soul-guardian/baselines.json + +# Restore snapshots +tar -xzf /tmp/approved-snapshots.tar.gz -C /workspace/project/data/soul-guardian/ +``` + +## FAQ + +**Q: Can I disable auto-restore for testing?** + +A: Yes, use `autoRestore: false`: +```typescript +await tools.clawsec_check_integrity({ autoRestore: false }); +``` + +**Q: How do I protect additional files?** + +A: Edit `policy.json` and add targets: +```json +{ + "path": "/workspace/group/my-config.json", + "mode": "restore", + "priority": "high", + "description": "My custom config" +} +``` + +**Q: What happens if both baseline and file are modified?** + +A: The most recent baseline wins. Always approve legitimate changes immediately. + +**Q: Can I run integrity checks on-demand?** + +A: Yes, just call `clawsec_check_integrity` from any agent. + +**Q: Is the audit log encrypted?** + +A: No, but it's hash-chained for tamper detection. Encryption can be added in Phase 3. + +## Support + +- **Documentation**: https://clawsec.prompt.security/ +- **Issues**: https://github.com/prompt-security/clawsec/issues +- **Security Reports**: security@prompt.security + +--- + +**Ready to protect your NanoClaw deployment? Start with the [Quick Start](#quick-start) guide above.** diff --git a/.agents/skills/clawsec-nanoclaw/docs/SKILL_SIGNING.md b/.agents/skills/clawsec-nanoclaw/docs/SKILL_SIGNING.md new file mode 100644 index 00000000..5719dd49 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/docs/SKILL_SIGNING.md @@ -0,0 +1,488 @@ +# Skill Package Signing and Verification + +This document explains how ClawSec signs skill packages and how NanoClaw agents verify signatures before installation. + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [For Skill Publishers: How to Sign Packages](#for-skill-publishers-how-to-sign-packages) +3. [For NanoClaw Agents: How to Verify Signatures](#for-nanoclaw-agents-how-to-verify-signatures) +4. [Security Properties](#security-properties) +5. [Key Management](#key-management) +6. [Troubleshooting](#troubleshooting) + +--- + +## Overview + +Skill signature verification prevents **supply chain attacks** by ensuring skill packages haven't been tampered with during distribution. ClawSec uses **Ed25519 digital signatures** to sign skill packages, and NanoClaw agents verify these signatures before installation. + +### Why Signature Verification? + +Without signature verification, an attacker could: +- **Replace** a legitimate skill package with a malicious one during download +- **Modify** package contents to inject backdoors or steal data +- **Distribute** trojan skills that appear legitimate but contain malware + +Signature verification ensures: +- ✅ **Authenticity**: Package comes from ClawSec (or trusted publisher) +- ✅ **Integrity**: Package hasn't been modified since signing +- ✅ **Non-repudiation**: Signer can't deny signing the package + +--- + +## For Skill Publishers: How to Sign Packages + +### Prerequisites + +- OpenSSL 1.1.1+ (for Ed25519 support) +- Private Ed25519 signing key (generate once, keep secure) +- Skill package ready for distribution + +### Step 1: Generate Ed25519 Keypair (One-Time Setup) + +```bash +# Generate private key (KEEP THIS SECRET!) +openssl genpkey -algorithm ED25519 -out clawsec-signing-private.pem + +# Extract public key (share this with users) +openssl pkey -in clawsec-signing-private.pem -pubout -out clawsec-signing-public.pem + +# Secure the private key +chmod 600 clawsec-signing-private.pem +``` + +**⚠️ CRITICAL**: Never commit the private key to version control! Store it securely: +- Local machine: `~/.ssh/clawsec-signing-private.pem` with `chmod 600` +- CI/CD: GitHub Secrets, AWS Secrets Manager, or similar +- Team: 1Password, Vault, or hardware security module (HSM) + +### Step 2: Package Your Skill + +```bash +# Create skill package (tarball or zip) +tar -czf my-skill-1.0.0.tar.gz -C skills/my-skill . + +# Or as a zip file +zip -r my-skill-1.0.0.zip skills/my-skill/ +``` + +### Step 3: Sign the Package + +```bash +# Create detached Ed25519 signature +openssl dgst -sha512 -sign clawsec-signing-private.pem \ + -out my-skill-1.0.0.tar.gz.sig \ + my-skill-1.0.0.tar.gz + +# Verify the signature was created +ls -lh my-skill-1.0.0.tar.gz.sig +# Should show a ~64-byte file +``` + +**Signature Format**: Detached Ed25519 signature, base64-encoded, stored in `.sig` file. + +### Step 4: Distribute Package + Signature + +Distribute **both** files together: +- `my-skill-1.0.0.tar.gz` (the skill package) +- `my-skill-1.0.0.tar.gz.sig` (the signature) + +Users will verify the signature against your public key before installation. + +### Step 5: Publish Public Key + +Share your public key with users via: +- **Pinned in repository**: Commit `clawsec-signing-public.pem` to your repo +- **Website**: Host at `https://yoursite.com/clawsec-signing-public.pem` +- **DNS TXT record**: Publish as base64-encoded TXT record +- **Skill metadata**: Embed in `skill.json` + +--- + +## For NanoClaw Agents: How to Verify Signatures + +### Quick Start + +```typescript +// Verify a downloaded skill package before installation +const verification = await tools.clawsec_verify_skill_package({ + packagePath: '/tmp/my-skill-1.0.0.tar.gz' + // signaturePath auto-detected as /tmp/my-skill-1.0.0.tar.gz.sig +}); + +const result = JSON.parse(verification.content[0].text); + +if (!result.valid) { + console.log('⚠️ SIGNATURE VERIFICATION FAILED!'); + console.log(`Reason: ${result.reason || result.error}`); + console.log('DO NOT install this package.'); + return; +} + +console.log(`✓ Signature valid (signer: ${result.signer})`); +console.log(`Package hash: ${result.packageInfo.sha256}`); +console.log('Safe to proceed with installation.'); +``` + +### MCP Tool: `clawsec_verify_skill_package` + +**Parameters:** +- `packagePath` (required): Absolute path to skill package (`.tar.gz`, `.tar`, `.tgz`, or `.zip`) +- `signaturePath` (optional): Path to signature file (auto-detects `.sig` if omitted) + +Path policy: +- Files must be under one of: `/tmp`, `/var/tmp`, `/workspace/ipc`, `/workspace/project/data`, `/workspace/project/tmp`, `/workspace/project/downloads` +- Symlinks are rejected +- Signatures must use `.sig` + +**Returns:** +```typescript +{ + success: boolean, // Operation completed without errors + valid: boolean, // Signature is cryptographically valid + recommendation: string, // "install" | "block" | "review" + signer: string, // "clawsec" + algorithm: "Ed25519", // Signature algorithm + verifiedAt: string, // ISO timestamp + packageInfo: { + size: number, // Package file size in bytes + sha256: string // SHA-256 hash of package + }, + error?: string // Error message if failed +} +``` + +### Usage Patterns + +#### Pattern 1: Basic Pre-Installation Check + +```typescript +async function installSkill(packagePath: string) { + // Verify signature first + const verification = await tools.clawsec_verify_skill_package({ packagePath }); + const result = JSON.parse(verification.content[0].text); + + if (result.recommendation === 'block') { + throw new Error(`Cannot install: ${result.reason || result.error}`); + } + + // Signature valid - proceed with extraction + extractPackage(packagePath, '/workspace/project/skills/'); +} +``` + +#### Pattern 2: Combined Security Checks + +```typescript +async function installSkillSafely(packagePath: string, skillName: string) { + // Step 1: Verify signature + const sigVerify = await tools.clawsec_verify_skill_package({ packagePath }); + const sigResult = JSON.parse(sigVerify.content[0].text); + + if (!sigResult.valid) { + throw new Error(`Signature invalid: ${sigResult.reason}`); + } + + // Step 2: Check advisories + const advisory = await tools.clawsec_check_skill_safety({ skillName }); + const advResult = JSON.parse(advisory.content[0].text); + + if (!advResult.safe) { + throw new Error(`Known vulnerabilities: ${advResult.advisories.map(a => a.id).join(', ')}`); + } + + // Both checks passed - safe to install + extractPackage(packagePath, '/workspace/project/skills/'); + console.log(`✓ Installed ${skillName} (verified + no advisories)`); +} +``` + +#### Pattern 3: Download and Verify Workflow + +```typescript +async function downloadAndInstallSkill(url: string) { + const packagePath = `/tmp/${Date.now()}-skill.tar.gz`; + const signaturePath = `${packagePath}.sig`; + + // Download package + await fetch(url).then(r => r.arrayBuffer()).then(buf => { + fs.writeFileSync(packagePath, Buffer.from(buf)); + }); + + // Download signature + await fetch(`${url}.sig`).then(r => r.text()).then(sig => { + fs.writeFileSync(signaturePath, sig); + }); + + // Verify before installation + const verification = await tools.clawsec_verify_skill_package({ + packagePath, + signaturePath + }); + + const result = JSON.parse(verification.content[0].text); + + if (!result.valid) { + fs.unlinkSync(packagePath); // Delete tampered file + fs.unlinkSync(signaturePath); + throw new Error('Signature verification failed'); + } + + // Install verified package + extractPackage(packagePath, '/workspace/project/skills/'); + + // Cleanup + fs.unlinkSync(packagePath); + fs.unlinkSync(signaturePath); +} +``` + +### Error Handling + +```typescript +const verification = await tools.clawsec_verify_skill_package({ packagePath }); +const result = JSON.parse(verification.content[0].text); + +// Check result.success first (operation completed) +if (!result.success) { + console.error('Verification operation failed:', result.error); + // Reasons: file not found, service unavailable, timeout + return; +} + +// Then check result.valid (signature cryptographically valid) +if (!result.valid) { + console.error('Invalid signature:', result.reason); + // Reasons: signature mismatch, tampered package, invalid format + return; +} + +// Finally check recommendation +switch (result.recommendation) { + case 'install': + console.log('✓ Safe to install'); + break; + case 'block': + console.error('⛔ Installation blocked'); + break; + case 'review': + console.warn('⚠️ Manual review recommended'); + break; +} +``` + +--- + +## Security Properties + +### What Signature Verification Prevents + +✅ **Prevents:** +- **Tampering**: Detecting if package contents were modified after signing +- **MITM attacks**: Detecting if package was swapped during download +- **Malicious mirrors**: Ensuring package comes from trusted source +- **Accidental corruption**: Detecting file corruption during transfer + +### What Signature Verification Does NOT Prevent + +❌ **Does Not Prevent:** +- **Malicious signed packages**: If the publisher's key is compromised +- **Zero-day vulnerabilities**: Bugs unknown to the publisher +- **Social engineering**: Convincing users to trust malicious publishers +- **Time-of-check-to-time-of-use**: Package modified after verification + +**Defense in Depth**: Combine signature verification with: +1. **Advisory checking** (`clawsec_check_skill_safety`) +2. **Code review** (manual inspection of skill code) +3. **Sandboxing** (run skills in isolated containers) +4. **Monitoring** (detect suspicious behavior at runtime) + +### Trust Model + +Signature verification relies on **trust in the public key**: + +``` +┌─────────────────────────────────────────────────┐ +│ You trust ClawSec's public key │ +│ ↓ │ +│ ClawSec signs package with private key │ +│ ↓ │ +│ You verify signature with ClawSec's public key │ +│ ↓ │ +│ Signature valid → Package is authentic │ +└─────────────────────────────────────────────────┘ +``` + +**Key Question**: How do you establish trust in the public key? +- **Pinned in repository**: Public key committed to ClawSec repo (trust GitHub) +- **HTTPS website**: Download from `https://clawsec.prompt.security/` (trust TLS/CA) +- **Out-of-band verification**: Compare key fingerprint via phone, Signal, etc. +- **Web of Trust**: Multiple trusted sources publish the same key + +--- + +## Key Management + +### ClawSec's Pinned Public Key + +**Location**: `/workspace/project/skills/clawsec-nanoclaw/advisories/feed-signing-public.pem` + +This is the **same key** used for advisory feed verification, providing a single trust anchor for all ClawSec security operations. + +**Key Fingerprint** (for manual verification): +```bash +# Compute fingerprint of pinned key +openssl pkey -pubin -in feed-signing-public.pem -outform DER | \ + openssl dgst -sha256 -binary | base64 +# Expected: +``` + +### Public Key Policy + +The verifier always uses the pinned ClawSec public key from this skill package. +Runtime public-key overrides are intentionally not supported. + +### Key Rotation + +If ClawSec's signing key is compromised or needs rotation: + +1. **Generate new keypair** (keep private key secure) +2. **Sign all packages** with new key +3. **Publish new public key** to all distribution channels +4. **Update pinned key** in `/workspace/project/skills/clawsec-nanoclaw/advisories/` +5. **Deprecate old key** after transition period (e.g., 90 days) + +During transition, support **dual signatures**: +- `package.tar.gz.sig` (old key) +- `package.tar.gz.sig2` (new key) + +Agents can verify with either key during the overlap period. + +--- + +## Troubleshooting + +### Error: "Signature file not found" + +**Cause**: Missing `.sig` file or incorrect path. + +**Solution**: +```bash +# Check if signature exists +ls -l /tmp/skill.tar.gz.sig + +# If missing, download signature +curl -o /tmp/skill.tar.gz.sig https://example.com/skill.tar.gz.sig + +# Or specify explicit path +clawsec_verify_skill_package({ + packagePath: '/tmp/skill.tar.gz', + signaturePath: '/tmp/custom-signature.sig' +}) +``` + +### Error: "Signature verification failed" + +**Cause**: Package was tampered with, or signature doesn't match package. + +**Solution**: +```bash +# Re-download package and signature +curl -o /tmp/skill.tar.gz https://example.com/skill.tar.gz +curl -o /tmp/skill.tar.gz.sig https://example.com/skill.tar.gz.sig + +# Verify manually with OpenSSL +openssl dgst -sha512 -verify clawsec-signing-public.pem \ + -signature /tmp/skill.tar.gz.sig /tmp/skill.tar.gz +# Should output: "Verified OK" +``` + +### Error: "Invalid PEM format" + +**Cause**: Public key file is corrupted or not in PEM format. + +**Solution**: +```bash +# Check public key format +head -1 /path/to/public-key.pem +# Should output: "-----BEGIN PUBLIC KEY-----" + +# Re-download public key +curl -o clawsec-signing-public.pem \ + https://clawsec.prompt.security/clawsec-signing-public.pem +``` + +### Error: "Package file not found" + +**Cause**: Incorrect path or file doesn't exist. + +**Solution**: +```bash +# Use absolute paths (required) +clawsec_verify_skill_package({ + packagePath: '/tmp/skill.tar.gz' // ✓ Absolute + // packagePath: './skill.tar.gz' // ✗ Relative (won't work) +}) + +# Verify file exists +stat /tmp/skill.tar.gz +``` + +### Verification Times Out (>5s) + +**Cause**: Large package (>50MB) or slow disk I/O. + +**Solution**: +```bash +# Check package size +ls -lh /tmp/skill.tar.gz + +# For very large packages, verification can take time +# Consider splitting into smaller skill modules +``` + +--- + +## Appendix: Signature File Format + +ClawSec uses **Ed25519 detached signatures** in raw binary format, base64-encoded. + +**File Structure**: +``` +my-skill-1.0.0.tar.gz.sig: + Line 1: base64-encoded signature (88 characters) +``` + +**Example**: +``` +MEQCIDxyz...ABC123== +``` + +**Properties**: +- Algorithm: Ed25519 (EdDSA with Curve25519) +- Signature size: 64 bytes (88 characters base64) +- Hash function: SHA-512 (internal to Ed25519) +- Format: Raw binary, base64-encoded + +**Verification Algorithm**: +1. Decode base64 signature → 64-byte binary +2. Hash package with SHA-512 +3. Verify Ed25519 signature(hash, publicKey) → boolean + +--- + +## References + +- [Ed25519 Specification (RFC 8032)](https://tools.ietf.org/html/rfc8032) +- [OpenSSL Ed25519 Documentation](https://www.openssl.org/docs/man3.0/man7/Ed25519.html) +- [ClawSec Security Architecture](https://clawsec.prompt.security/docs/architecture) +- [Supply Chain Attack Prevention](https://owasp.org/www-community/attacks/Supply_Chain_Attack) + +--- + +**Document Version**: 1.0.0 +**Last Updated**: 2026-02-25 +**Maintainer**: ClawSec Security Team diff --git a/.agents/skills/clawsec-nanoclaw/guardian/integrity-monitor.ts b/.agents/skills/clawsec-nanoclaw/guardian/integrity-monitor.ts new file mode 100644 index 00000000..b7ba06dc --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/guardian/integrity-monitor.ts @@ -0,0 +1,736 @@ +/** + * File Integrity Monitor for NanoClaw + * + * TypeScript port of ClawSec's soul-guardian with NanoClaw-specific adaptations. + * + * Key Features: + * - SHA-256 baseline tracking for protected files + * - Drift detection with unified diff generation + * - Auto-restore for critical files (with quarantine) + * - Hash-chained tamper-evident audit log + * - Per-file policy (restore/alert/ignore modes) + * + * Security Model: + * - Baselines stored on host only (containers access via IPC) + * - Atomic file operations for restores + * - Refuses to operate on symlinks + * - Hash-chained audit log prevents tampering + */ + +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; +// glob is available when running in the NanoClaw host environment. +// For type checking in the clawsec repo, we declare a minimal interface. +// eslint-disable-next-line @typescript-eslint/no-namespace +declare namespace glob { + function sync(pattern: string, options?: { nodir?: boolean }): string[]; +} + +// ============================================================================ +// Types +// ============================================================================ + +export interface PolicyTarget { + path?: string; + pattern?: string; + mode: 'restore' | 'alert' | 'ignore'; + priority: 'critical' | 'high' | 'medium' | 'low'; + description: string; +} + +export interface Policy { + version: number; + description: string; + nanoclaw_version: string; + targets: PolicyTarget[]; + notes?: string[]; +} + +export interface FileBaseline { + sha256: string; + approved_at: string; + approved_by: string; + mode: 'restore' | 'alert' | 'ignore'; + priority: string; +} + +export interface BaselinesManifest { + schema_version: string; + algorithm: 'sha256'; + created_at: string; + files: Record; +} + +export interface AuditEntry { + ts: string; + event: 'init' | 'drift' | 'restore' | 'approve' | 'error'; + actor: string; + note?: string; + path: string; + mode?: string; + expected_sha?: string; + found_sha?: string; + patch_path?: string; + quarantine_path?: string; + error?: string; + chain?: { + prev: string; + hash: string; + }; +} + +export interface DriftedFile { + path: string; + mode: 'restore' | 'alert'; + expected_sha: string; + found_sha: string; + patch_path: string; + restored: boolean; + quarantine_path?: string; + error?: string; +} + +export interface CheckResult { + success: boolean; + timestamp: string; + drift_detected: boolean; + files: Array<{ + path: string; + status: 'ok' | 'drifted' | 'restored' | 'error'; + mode: string; + expected_sha?: string; + found_sha?: string; + patch_path?: string; + quarantine_path?: string; + error?: string; + }>; + summary: { + total: number; + ok: number; + drifted: number; + restored: number; + alerted: number; + errors: number; + }; +} + +export interface IntegrityMonitorOptions { + policyPath: string; + stateDir: string; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const CHAIN_GENESIS = '0'.repeat(64); + +// ============================================================================ +// Utility Functions +// ============================================================================ + +function utcNowIso(): string { + return new Date().toISOString(); +} + +function sha256Hex(data: Buffer | string): string { + const hash = crypto.createHash('sha256'); + hash.update(data); + return hash.digest('hex'); +} + +function sha256File(filePath: string): string { + const data = fs.readFileSync(filePath); + return sha256Hex(data); +} + +function isSymlink(filePath: string): boolean { + try { + const stats = fs.lstatSync(filePath); + return stats.isSymbolicLink(); + } catch { + return false; + } +} + +function refuseSymlink(filePath: string): void { + if (isSymlink(filePath)) { + throw new Error(`Refusing to operate on symlink: ${filePath}`); + } +} + +function ensureDir(dirPath: string): void { + fs.mkdirSync(dirPath, { recursive: true }); +} + +function atomicWrite(filePath: string, data: string | Buffer): void { + ensureDir(path.dirname(filePath)); + const tmpPath = `${filePath}.tmp.${Date.now()}`; + fs.writeFileSync(tmpPath, data); + fs.renameSync(tmpPath, filePath); +} + +function unifiedDiff(oldText: string, newText: string, oldLabel: string, newLabel: string): string { + // Simple unified diff implementation + const oldLines = oldText.split('\n'); + const newLines = newText.split('\n'); + + const lines: string[] = []; + lines.push(`--- ${oldLabel}`); + lines.push(`+++ ${newLabel}`); + lines.push(`@@ -1,${oldLines.length} +1,${newLines.length} @@`); + + for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) { + if (i < oldLines.length && i < newLines.length) { + if (oldLines[i] !== newLines[i]) { + lines.push(`-${oldLines[i]}`); + lines.push(`+${newLines[i]}`); + } else { + lines.push(` ${oldLines[i]}`); + } + } else if (i < oldLines.length) { + lines.push(`-${oldLines[i]}`); + } else { + lines.push(`+${newLines[i]}`); + } + } + + return lines.join('\n'); +} + +function safePatchTag(tag: string): string { + return tag.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 40) || 'patch'; +} + +// ============================================================================ +// Integrity Monitor Class +// ============================================================================ + +export class IntegrityMonitor { + private policyPath: string; + private stateDir: string; + private baselinesPath: string; + private auditPath: string; + private approvedDir: string; + private patchesDir: string; + private quarantineDir: string; + + private policy: Policy | null = null; + private baselines: BaselinesManifest | null = null; + + constructor(options: IntegrityMonitorOptions) { + this.policyPath = options.policyPath; + this.stateDir = options.stateDir; + this.baselinesPath = path.join(this.stateDir, 'baselines.json'); + this.auditPath = path.join(this.stateDir, 'audit.jsonl'); + this.approvedDir = path.join(this.stateDir, 'approved'); + this.patchesDir = path.join(this.stateDir, 'patches'); + this.quarantineDir = path.join(this.stateDir, 'quarantine'); + } + + // -------------------------------------------------------------------------- + // Initialization + // -------------------------------------------------------------------------- + + async init(actor: string = 'system', note: string = 'initial baseline'): Promise { + ensureDir(this.stateDir); + ensureDir(this.approvedDir); + ensureDir(this.patchesDir); + ensureDir(this.quarantineDir); + + // Load policy + this.policy = this.loadPolicy(); + + // Load or create baselines + this.baselines = this.loadBaselines(); + + // Resolve targets and initialize missing baselines + const targets = this.resolveTargets(); + let initialized = false; + + for (const target of targets) { + if (target.mode === 'ignore') continue; + + try { + if (!fs.existsSync(target.path)) continue; + + refuseSymlink(target.path); + + // Check if already has baseline + if (this.baselines.files[target.path]) continue; + + // Create baseline + const sha = sha256File(target.path); + const snapshot = path.join(this.approvedDir, path.basename(target.path)); + fs.copyFileSync(target.path, snapshot); + + this.baselines.files[target.path] = { + sha256: sha, + approved_at: utcNowIso(), + approved_by: actor, + mode: target.mode, + priority: target.priority + }; + + this.appendAudit({ + ts: utcNowIso(), + event: 'init', + actor, + note, + path: target.path, + mode: target.mode, + expected_sha: sha + }); + + initialized = true; + } catch (error) { + console.error(`Failed to initialize baseline for ${target.path}:`, error); + } + } + + if (initialized) { + this.saveBaselines(); + } + } + + // -------------------------------------------------------------------------- + // Policy Management + // -------------------------------------------------------------------------- + + private loadPolicy(): Policy { + const raw = fs.readFileSync(this.policyPath, 'utf-8'); + return JSON.parse(raw); + } + + private resolveTargets(): Array<{ path: string; mode: 'restore' | 'alert' | 'ignore'; priority: string }> { + if (!this.policy) throw new Error('Policy not loaded'); + + const targets: Array<{ path: string; mode: 'restore' | 'alert' | 'ignore'; priority: string }> = []; + + for (const target of this.policy.targets) { + if (target.path) { + // Direct path + targets.push({ + path: path.resolve(target.path), + mode: target.mode, + priority: target.priority + }); + } else if (target.pattern) { + // Glob pattern + try { + const matches = glob.sync(target.pattern, { nodir: true }); + for (const match of matches) { + targets.push({ + path: path.resolve(match), + mode: target.mode, + priority: target.priority + }); + } + } catch (error) { + console.error(`Failed to expand pattern ${target.pattern}:`, error); + } + } + } + + return targets; + } + + private normalizeBaselines(manifest: BaselinesManifest): BaselinesManifest { + const normalizedFiles: Record = {}; + for (const [filePath, baseline] of Object.entries(manifest.files || {})) { + normalizedFiles[path.resolve(filePath)] = baseline; + } + + return { + ...manifest, + files: normalizedFiles, + }; + } + + // -------------------------------------------------------------------------- + // Baseline Management + // -------------------------------------------------------------------------- + + private loadBaselines(): BaselinesManifest { + if (fs.existsSync(this.baselinesPath)) { + const raw = fs.readFileSync(this.baselinesPath, 'utf-8'); + return this.normalizeBaselines(JSON.parse(raw)); + } + + return { + schema_version: '1', + algorithm: 'sha256', + created_at: utcNowIso(), + files: {} + }; + } + + private saveBaselines(): void { + const data = JSON.stringify(this.baselines, null, 2); + atomicWrite(this.baselinesPath, data); + } + + // -------------------------------------------------------------------------- + // Audit Log with Hash Chaining + // -------------------------------------------------------------------------- + + private getLastAuditHash(): string { + if (!fs.existsSync(this.auditPath)) { + return CHAIN_GENESIS; + } + + const content = fs.readFileSync(this.auditPath, 'utf-8'); + const lines = content.trim().split('\n').filter(l => l.trim()); + + if (lines.length === 0) { + return CHAIN_GENESIS; + } + + try { + const lastEntry = JSON.parse(lines[lines.length - 1]); + return lastEntry.chain?.hash || CHAIN_GENESIS; + } catch { + return CHAIN_GENESIS; + } + } + + private appendAudit(entry: Omit): void { + ensureDir(path.dirname(this.auditPath)); + + const prevHash = this.getLastAuditHash(); + + // Compute current hash + const entryWithoutChain = { ...entry }; + const payload = prevHash + '\n' + JSON.stringify(entryWithoutChain, Object.keys(entryWithoutChain).sort()); + const currentHash = sha256Hex(payload); + + const record: AuditEntry = { + ...entry, + chain: { + prev: prevHash, + hash: currentHash + } + }; + + fs.appendFileSync(this.auditPath, JSON.stringify(record) + '\n'); + } + + // -------------------------------------------------------------------------- + // Drift Detection + // -------------------------------------------------------------------------- + + async checkIntegrity(autoRestore: boolean = true, actor: string = 'agent'): Promise { + if (!this.baselines) { + throw new Error('Baselines not loaded. Call init() first.'); + } + + const result: CheckResult = { + success: true, + timestamp: utcNowIso(), + drift_detected: false, + files: [], + summary: { + total: 0, + ok: 0, + drifted: 0, + restored: 0, + alerted: 0, + errors: 0 + } + }; + + for (const [filePath, baseline] of Object.entries(this.baselines.files)) { + result.summary.total++; + + try { + if (!fs.existsSync(filePath)) { + result.files.push({ + path: filePath, + status: 'error', + mode: baseline.mode, + error: 'File not found' + }); + result.summary.errors++; + + this.appendAudit({ + ts: utcNowIso(), + event: 'error', + actor, + path: filePath, + error: 'File not found' + }); + + continue; + } + + refuseSymlink(filePath); + + const currentSha = sha256File(filePath); + + if (currentSha === baseline.sha256) { + // No drift + result.files.push({ + path: filePath, + status: 'ok', + mode: baseline.mode, + expected_sha: baseline.sha256, + found_sha: currentSha + }); + result.summary.ok++; + continue; + } + + // Drift detected + result.drift_detected = true; + result.summary.drifted++; + + // Generate diff + const snapshot = path.join(this.approvedDir, path.basename(filePath)); + const oldText = fs.existsSync(snapshot) ? fs.readFileSync(snapshot, 'utf-8') : ''; + const newText = fs.readFileSync(filePath, 'utf-8'); + const diff = unifiedDiff(oldText, newText, `approved/${path.basename(filePath)}`, path.basename(filePath)); + + const patchPath = path.join( + this.patchesDir, + `${new Date().toISOString().replace(/[:.]/g, '-')}-drift-${safePatchTag(path.basename(filePath))}.patch` + ); + fs.writeFileSync(patchPath, diff); + + this.appendAudit({ + ts: utcNowIso(), + event: 'drift', + actor, + path: filePath, + mode: baseline.mode, + expected_sha: baseline.sha256, + found_sha: currentSha, + patch_path: patchPath + }); + + // Handle based on mode + if (baseline.mode === 'restore' && autoRestore) { + // Auto-restore + try { + const quarantinePath = path.join( + this.quarantineDir, + `${safePatchTag(path.basename(filePath))}.${Date.now()}.quarantine` + ); + fs.copyFileSync(filePath, quarantinePath); + + if (fs.existsSync(snapshot)) { + atomicWrite(filePath, fs.readFileSync(snapshot)); + } + + this.appendAudit({ + ts: utcNowIso(), + event: 'restore', + actor, + path: filePath, + mode: baseline.mode, + quarantine_path: quarantinePath + }); + + result.files.push({ + path: filePath, + status: 'restored', + mode: baseline.mode, + expected_sha: baseline.sha256, + found_sha: currentSha, + patch_path: patchPath, + quarantine_path: quarantinePath + }); + result.summary.restored++; + } catch (error) { + result.files.push({ + path: filePath, + status: 'error', + mode: baseline.mode, + expected_sha: baseline.sha256, + found_sha: currentSha, + patch_path: patchPath, + error: `Restore failed: ${error instanceof Error ? error.message : String(error)}` + }); + result.summary.errors++; + } + } else { + // Alert only + result.files.push({ + path: filePath, + status: 'drifted', + mode: baseline.mode, + expected_sha: baseline.sha256, + found_sha: currentSha, + patch_path: patchPath + }); + result.summary.alerted++; + } + + } catch (error) { + result.files.push({ + path: filePath, + status: 'error', + mode: baseline.mode, + error: error instanceof Error ? error.message : String(error) + }); + result.summary.errors++; + + this.appendAudit({ + ts: utcNowIso(), + event: 'error', + actor, + path: filePath, + error: error instanceof Error ? error.message : String(error) + }); + } + } + + return result; + } + + // -------------------------------------------------------------------------- + // Approve Changes + // -------------------------------------------------------------------------- + + async approveChange(filePath: string, actor: string, note: string = ''): Promise { + if (!this.baselines) { + throw new Error('Baselines not loaded'); + } + + const normalizedFilePath = path.resolve(filePath); + + if (!fs.existsSync(normalizedFilePath)) { + throw new Error(`File not found: ${normalizedFilePath}`); + } + + refuseSymlink(normalizedFilePath); + + const targets = this.resolveTargets(); + const target = targets.find(t => t.path === normalizedFilePath); + if (!target || target.mode === 'ignore') { + throw new Error(`File ${normalizedFilePath} not in policy`); + } + + const previousSha = this.baselines.files[normalizedFilePath]?.sha256; + const currentSha = sha256File(normalizedFilePath); + + // Generate diff + const snapshot = path.join(this.approvedDir, path.basename(normalizedFilePath)); + const oldText = fs.existsSync(snapshot) ? fs.readFileSync(snapshot, 'utf-8') : ''; + const newText = fs.readFileSync(normalizedFilePath, 'utf-8'); + const diff = unifiedDiff( + oldText, + newText, + `approved/${path.basename(normalizedFilePath)}`, + path.basename(normalizedFilePath) + ); + + const patchPath = path.join( + this.patchesDir, + `${new Date().toISOString().replace(/[:.]/g, '-')}-approve-${safePatchTag(path.basename(normalizedFilePath))}.patch` + ); + fs.writeFileSync(patchPath, diff); + + // Update baseline + if (!this.baselines.files[normalizedFilePath]) { + this.baselines.files[normalizedFilePath] = { + sha256: currentSha, + approved_at: utcNowIso(), + approved_by: actor, + mode: target.mode, + priority: target.priority + }; + } else { + this.baselines.files[normalizedFilePath].sha256 = currentSha; + this.baselines.files[normalizedFilePath].approved_at = utcNowIso(); + this.baselines.files[normalizedFilePath].approved_by = actor; + } + + // Update snapshot + fs.copyFileSync(normalizedFilePath, snapshot); + + // Save and audit + this.saveBaselines(); + + this.appendAudit({ + ts: utcNowIso(), + event: 'approve', + actor, + note, + path: normalizedFilePath, + expected_sha: previousSha, + found_sha: currentSha, + patch_path: patchPath + }); + } + + // -------------------------------------------------------------------------- + // Status and Verification + // -------------------------------------------------------------------------- + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getStatus(filePath?: string): any { + if (!this.baselines) { + throw new Error('Baselines not loaded'); + } + + const normalizedFilePath = filePath ? path.resolve(filePath) : null; + const files = normalizedFilePath + ? { [normalizedFilePath]: this.baselines.files[normalizedFilePath] } + : this.baselines.files; + + return { + baseline_age: this.baselines.created_at, + files: Object.entries(files).map(([path, baseline]) => ({ + path, + mode: baseline?.mode, + priority: baseline?.priority, + has_baseline: !!baseline, + baseline_sha: baseline?.sha256, + approved_at: baseline?.approved_at, + snapshot_exists: fs.existsSync(this.approvedDir + '/' + path.split('/').pop()) + })) + }; + } + + verifyAuditChain(): { valid: boolean; entries: number; errors: string[] } { + if (!fs.existsSync(this.auditPath)) { + return { valid: true, entries: 0, errors: [] }; + } + + const content = fs.readFileSync(this.auditPath, 'utf-8'); + const lines = content.trim().split('\n').filter(l => l.trim()); + + const errors: string[] = []; + let prevHash = CHAIN_GENESIS; + + for (let i = 0; i < lines.length; i++) { + try { + const entry: AuditEntry = JSON.parse(lines[i]); + + if (entry.chain?.prev !== prevHash) { + errors.push(`Line ${i + 1}: Chain break (expected prev=${prevHash}, got=${entry.chain?.prev})`); + } + + const entryWithoutChain = { ...entry }; + delete entryWithoutChain.chain; + const payload = prevHash + '\n' + JSON.stringify(entryWithoutChain, Object.keys(entryWithoutChain).sort()); + const expectedHash = sha256Hex(payload); + + if (entry.chain?.hash !== expectedHash) { + errors.push(`Line ${i + 1}: Hash mismatch`); + } + + prevHash = entry.chain?.hash || CHAIN_GENESIS; + } catch (error) { + errors.push(`Line ${i + 1}: Parse error - ${error}`); + } + } + + return { + valid: errors.length === 0, + entries: lines.length, + errors + }; + } +} diff --git a/.agents/skills/clawsec-nanoclaw/guardian/policy.json b/.agents/skills/clawsec-nanoclaw/guardian/policy.json new file mode 100644 index 00000000..74454262 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/guardian/policy.json @@ -0,0 +1,55 @@ +{ + "version": 1, + "description": "NanoClaw file integrity monitoring policy", + "nanoclaw_version": "0.1.0", + "targets": [ + { + "path": "/workspace/project/data/registered_groups.json", + "mode": "restore", + "priority": "critical", + "description": "Group registration config - prevents unauthorized group access" + }, + { + "path": "/workspace/group/CLAUDE.md", + "mode": "restore", + "priority": "high", + "description": "Group-specific agent instructions" + }, + { + "path": "/workspace/project/groups/global/CLAUDE.md", + "mode": "restore", + "priority": "high", + "description": "Global agent instructions shared across all groups" + }, + { + "pattern": "/workspace/project/container/**/*.ts", + "mode": "alert", + "priority": "medium", + "description": "Container runtime code - alert on changes for awareness" + }, + { + "pattern": "/workspace/project/host/**/*.ts", + "mode": "alert", + "priority": "medium", + "description": "Host process code - alert on changes for awareness" + }, + { + "pattern": "/workspace/ipc/**/*", + "mode": "ignore", + "priority": "low", + "description": "IPC files change constantly - ignore" + }, + { + "pattern": "/workspace/group/conversations/**/*", + "mode": "ignore", + "priority": "low", + "description": "Chat history - expected to change frequently" + } + ], + "notes": [ + "Mode 'restore': Auto-restore file to approved baseline on drift + alert user", + "Mode 'alert': Alert user about drift but do not auto-restore", + "Mode 'ignore': No monitoring, file changes are expected", + "Patterns use glob syntax with ** for recursive matching" + ] +} diff --git a/.agents/skills/clawsec-nanoclaw/host-services/advisory-cache.ts b/.agents/skills/clawsec-nanoclaw/host-services/advisory-cache.ts new file mode 100644 index 00000000..578ed37a --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/host-services/advisory-cache.ts @@ -0,0 +1,383 @@ +/** + * ClawSec Advisory Cache Manager for NanoClaw + * + * Manages fetching, verifying, and caching the ClawSec advisory feed. + * Runs on the host side (not in container). + * + * Security: + * - Ed25519 signature verification using Node.js crypto + * - Fail-closed policy: invalid signature = reject feed + * - TLS 1.2+ enforcement with certificate validation + * - Public key embedded (not user-modifiable) + * - Cache stored in host-managed directory + */ + +import crypto from 'node:crypto'; +import fs from 'node:fs/promises'; +import https from 'node:https'; +import path from 'node:path'; +import { evaluateAdvisoryRisk } from '../lib/risk.js'; + +// ClawSec public key (from clawsec-signing-public.pem) +const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAS7nijfMcUoOBCj4yOXJX+GYGv2pFl2Yaha1P4v5Cm6A= +-----END PUBLIC KEY-----`; + +const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes +const FEED_URL = 'https://clawsec.prompt.security/advisories/feed.json'; +const FETCH_TIMEOUT_MS = 10000; + +export interface Advisory { + id: string; + severity: string; + type?: string; + title?: string; + description?: string; + action?: string; + published?: string; + updated?: string; + exploitability_score?: 'high' | 'medium' | 'low' | 'unknown' | string; + exploitability_rationale?: string; + affected: string[]; +} + +export interface FeedPayload { + version: string; + updated?: string; + advisories: Advisory[]; +} + +export interface AdvisoryCache { + feed: FeedPayload; + fetchedAt: string; + verified: boolean; + publicKeyFingerprint: string; +} + +interface Logger { + info(msg: string | object, ...args: unknown[]): void; + error(msg: string | object, ...args: unknown[]): void; + warn(msg: string | object, ...args: unknown[]): void; +} + +export class AdvisoryCacheManager { + private cache: AdvisoryCache | null = null; + private refreshPromise: Promise | null = null; + private cacheFile: string; + private logger: Logger; + + constructor(dataDir: string, logger: Logger) { + this.cacheFile = path.join(dataDir, 'clawsec-advisory-cache.json'); + this.logger = logger; + } + + /** + * Initialize cache manager. Loads cache from disk and refreshes if stale. + */ + async initialize(): Promise { + await this.loadCacheFromDisk(); + + if (!this.cache || this.isCacheStale()) { + try { + await this.refresh(); + } catch (error) { + this.logger.error({ error }, 'Failed to initialize advisory cache'); + // Continue with stale cache if available + } + } + } + + /** + * Refresh advisory cache from remote feed. + * Thread-safe: prevents concurrent refreshes. + */ + async refresh(): Promise { + // Prevent concurrent refreshes + if (this.refreshPromise) { + return this.refreshPromise; + } + + this.refreshPromise = this._doRefresh(); + try { + await this.refreshPromise; + } finally { + this.refreshPromise = null; + } + } + + /** + * Get current cache. Returns null if cache is stale or missing. + */ + getCache(): AdvisoryCache | null { + if (!this.cache || this.isCacheStale()) { + return null; + } + return this.cache; + } + + /** + * Get cache even if stale (for fallback scenarios) + */ + getCacheAllowStale(): AdvisoryCache | null { + return this.cache; + } + + private async _doRefresh(): Promise { + try { + this.logger.info('Refreshing advisory cache from ClawSec feed'); + + const feed = await this.fetchAndVerifyFeed(); + const fingerprint = this.calculateKeyFingerprint(); + + this.cache = { + feed, + fetchedAt: new Date().toISOString(), + verified: true, + publicKeyFingerprint: fingerprint, + }; + + await this.saveCacheToDisk(); + this.logger.info({ + advisories: feed.advisories.length, + updated: feed.updated, + }, 'Advisory cache refreshed successfully'); + } catch (error) { + this.logger.error({ error }, 'Failed to refresh advisory cache'); + throw error; + } + } + + private isCacheStale(): boolean { + if (!this.cache) return true; + const age = Date.now() - Date.parse(this.cache.fetchedAt); + return age > CACHE_TTL_MS; + } + + private async fetchAndVerifyFeed(): Promise { + // Fetch feed and signature in parallel + const [payloadRaw, signatureRaw] = await Promise.all([ + this.secureFetch(FEED_URL), + this.secureFetch(`${FEED_URL}.sig`), + ]); + + // Verify Ed25519 signature + if (!this.verifySignature(payloadRaw, signatureRaw)) { + throw new Error('Feed signature verification failed (Ed25519)'); + } + + // Parse and validate + const feed = JSON.parse(payloadRaw) as FeedPayload; + if (!this.isValidFeed(feed)) { + throw new Error('Invalid feed format'); + } + + return feed; + } + + private async secureFetch(url: string): Promise { + return new Promise((resolve, reject) => { + // Create secure HTTPS agent with TLS 1.2+ enforcement + const agent = new https.Agent({ + minVersion: 'TLSv1.2', + rejectUnauthorized: true, + ciphers: 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256', + }); + + const req = https.get(url, { + agent, + timeout: FETCH_TIMEOUT_MS, + headers: { + 'User-Agent': 'NanoClaw/1.0', + 'Accept': 'application/json,text/plain', + }, + }, (res) => { + if (res.statusCode !== 200) { + reject(new Error(`HTTP ${res.statusCode} from ${url}`)); + return; + } + + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => resolve(data)); + res.on('error', reject); + }); + + req.on('error', reject); + req.on('timeout', () => { + req.destroy(); + reject(new Error(`Timeout fetching ${url}`)); + }); + }); + } + + private verifySignature(payload: string, signatureBase64: string): boolean { + try { + // Decode base64 signature + const trimmed = signatureBase64.trim(); + let encoded = trimmed; + + // Handle JSON-wrapped signature: {"signature": "base64..."} + if (trimmed.startsWith('{')) { + try { + const parsed = JSON.parse(trimmed); + if (typeof parsed.signature === 'string') { + encoded = parsed.signature; + } + } catch { + // Not JSON, use as-is + } + } + + const normalized = encoded.replace(/\s+/g, ''); + const sigBuffer = Buffer.from(normalized, 'base64'); + + // Verify Ed25519 signature using Node.js crypto + const publicKey = crypto.createPublicKey(PUBLIC_KEY_PEM); + return crypto.verify( + null, // algorithm null = Ed25519 raw mode + Buffer.from(payload, 'utf8'), + publicKey, + sigBuffer + ); + } catch (error) { + this.logger.warn({ error }, 'Signature verification failed'); + return false; + } + } + + private isValidFeed(feed: unknown): feed is FeedPayload { + if (typeof feed !== 'object' || !feed) return false; + const f = feed as FeedPayload; + + if (typeof f.version !== 'string' || !f.version.trim()) return false; + if (!Array.isArray(f.advisories)) return false; + + // Validate each advisory + return f.advisories.every((a: unknown) => { + if (typeof a !== 'object' || !a) return false; + const advisory = a as Advisory; + + return ( + typeof advisory.id === 'string' && + advisory.id.trim() !== '' && + typeof advisory.severity === 'string' && + advisory.severity.trim() !== '' && + Array.isArray(advisory.affected) && + advisory.affected.every( + (affected) => typeof affected === 'string' && affected.trim() !== '' + ) + ); + }); + } + + private calculateKeyFingerprint(): string { + const publicKey = crypto.createPublicKey(PUBLIC_KEY_PEM); + const der = publicKey.export({ type: 'spki', format: 'der' }); + return crypto.createHash('sha256').update(der).digest('hex'); + } + + private async loadCacheFromDisk(): Promise { + try { + const data = await fs.readFile(this.cacheFile, 'utf8'); + const parsed = JSON.parse(data) as AdvisoryCache; + + // Validate cache structure + if (this.isValidCache(parsed)) { + this.cache = parsed; + this.logger.info({ + age: Date.now() - Date.parse(parsed.fetchedAt), + advisories: parsed.feed.advisories.length, + }, 'Loaded advisory cache from disk'); + } else { + this.logger.warn('Invalid cache format on disk, discarding'); + this.cache = null; + } + } catch { + this.cache = null; + } + } + + private isValidCache(cache: unknown): cache is AdvisoryCache { + if (typeof cache !== 'object' || !cache) return false; + const c = cache as AdvisoryCache; + + return ( + this.isValidFeed(c.feed) && + typeof c.fetchedAt === 'string' && + typeof c.verified === 'boolean' && + typeof c.publicKeyFingerprint === 'string' + ); + } + + private async saveCacheToDisk(): Promise { + if (!this.cache) return; + + try { + await fs.mkdir(path.dirname(this.cacheFile), { recursive: true }); + + // Atomic write: temp file then rename + const tempFile = `${this.cacheFile}.tmp`; + await fs.writeFile(tempFile, JSON.stringify(this.cache, null, 2), 'utf8'); + await fs.rename(tempFile, this.cacheFile); + + this.logger.info({ path: this.cacheFile }, 'Advisory cache saved to disk'); + } catch (error) { + this.logger.error({ error }, 'Failed to save advisory cache to disk'); + throw error; + } + } +} + +/** + * Helper: Match advisories against installed skills + */ +export function findAdvisoryMatches( + advisories: Advisory[], + skills: Array<{ name: string; version: string | null; dirName: string }> +): Array<{ + advisory: Advisory; + skill: { name: string; version: string | null; dirName: string }; + matchedAffected: string[]; +}> { + const matches: Array<{ + advisory: Advisory; + skill: { name: string; version: string | null; dirName: string }; + matchedAffected: string[]; + }> = []; + + for (const advisory of advisories) { + for (const skill of skills) { + const matchedAffected: string[] = []; + + for (const affected of advisory.affected) { + // Parse affected specifier: skill-name or skill-name@version + const atIndex = affected.lastIndexOf('@'); + const affectedName = atIndex > 0 ? affected.slice(0, atIndex) : affected; + const _affectedVersion = atIndex > 0 ? affected.slice(atIndex + 1) : '*'; + + // Match by name or directory name + if (affectedName === skill.name || affectedName === skill.dirName) { + // TODO: implement version range matching + matchedAffected.push(affected); + } + } + + if (matchedAffected.length > 0) { + matches.push({ advisory, skill, matchedAffected }); + } + } + } + + return matches; +} + +/** + * Helper: Evaluate safety recommendation for a skill + */ +export function evaluateSkillSafety(advisories: Advisory[]): { + safe: boolean; + recommendation: 'install' | 'block' | 'review'; + reason: string; +} { + return evaluateAdvisoryRisk(advisories); +} diff --git a/.agents/skills/clawsec-nanoclaw/host-services/integrity-handler.ts b/.agents/skills/clawsec-nanoclaw/host-services/integrity-handler.ts new file mode 100644 index 00000000..060b6ee9 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/host-services/integrity-handler.ts @@ -0,0 +1,348 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * ClawSec File Integrity Monitoring IPC Handler for NanoClaw Host + * + * Add these handlers to /workspace/project/src/ipc.ts + * + * This processes integrity monitoring requests from agents running in containers. + */ + +import fs from 'fs'; +import path from 'path'; +import { IntegrityMonitor } from '../guardian/integrity-monitor'; + +// ============================================================================ +// Integrity Service (Singleton) +// ============================================================================ + +export class IntegrityService { + private monitor: IntegrityMonitor | null = null; + private initialized = false; + + async initialize(): Promise { + if (this.initialized) return; + + try { + this.monitor = new IntegrityMonitor({ + policyPath: '/workspace/project/skills/clawsec-nanoclaw/guardian/policy.json', + stateDir: '/workspace/project/data/soul-guardian' + }); + + // Initialize baselines on first run + await this.monitor.init('system', 'initial baseline'); + + this.initialized = true; + console.log('[IntegrityService] Initialized successfully'); + } catch (error) { + console.error('[IntegrityService] Initialization failed:', error); + throw error; + } + } + + getMonitor(): IntegrityMonitor { + if (!this.monitor) { + throw new Error('IntegrityService not initialized'); + } + return this.monitor; + } + + isInitialized(): boolean { + return this.initialized; + } +} + +// Global singleton instance +let integrityServiceInstance: IntegrityService | null = null; + +export function getIntegrityService(): IntegrityService { + if (!integrityServiceInstance) { + integrityServiceInstance = new IntegrityService(); + } + return integrityServiceInstance; +} + +// ============================================================================ +// IPC Handler Integration +// ============================================================================ + +/** + * Add this to the IpcDeps interface in /workspace/project/src/ipc.ts: + * + * export interface IpcDeps { + * // ... existing deps + * integrityService?: IntegrityService; + * } + */ + +/** + * Add these cases to the switch statement in processTaskIpc: + */ + +export async function handleIntegrityIpc( + task: any, + deps: { integrityService?: IntegrityService }, + logger: any +): Promise { + const { type, requestId, groupFolder: _groupFolder } = task; + + if (!deps.integrityService) { + logger.warn({ task }, 'IntegrityService not available'); + if (requestId) { + writeResult(requestId, { + success: false, + error: 'IntegrityService not initialized' + }); + } + return; + } + + const service = deps.integrityService; + + if (!service.isInitialized()) { + try { + await service.initialize(); + } catch (error) { + logger.error({ error }, 'Failed to initialize IntegrityService'); + if (requestId) { + writeResult(requestId, { + success: false, + error: `Initialization failed: ${error instanceof Error ? error.message : String(error)}` + }); + } + return; + } + } + + switch (type) { + case 'integrity_check': + await handleIntegrityCheck(task, service, logger); + break; + + case 'integrity_approve': + await handleIntegrityApprove(task, service, logger); + break; + + case 'integrity_status': + await handleIntegrityStatus(task, service, logger); + break; + + case 'integrity_verify_audit': + await handleIntegrityVerifyAudit(task, service, logger); + break; + + default: + logger.warn({ type }, 'Unknown integrity task type'); + } +} + +// ============================================================================ +// Individual Handlers +// ============================================================================ + +async function handleIntegrityCheck( + task: any, + service: IntegrityService, + logger: any +): Promise { + const { requestId, mode, autoRestore, groupFolder } = task; + + logger.info({ requestId, groupFolder }, 'Processing integrity_check'); + + try { + const monitor = service.getMonitor(); + + if (mode === 'status') { + // Status mode: just return baseline info + const status = monitor.getStatus(); + writeResult(requestId, { + success: true, + mode: 'status', + ...status + }); + } else { + // Check mode: detect drift and optionally restore + const result = await monitor.checkIntegrity(autoRestore !== false, 'agent'); + + writeResult(requestId, result); + + if (result.drift_detected) { + logger.warn( + { requestId, drifted: result.summary.drifted, restored: result.summary.restored }, + 'Integrity drift detected' + ); + } else { + logger.info({ requestId }, 'Integrity check passed'); + } + } + } catch (error) { + logger.error({ error, requestId }, 'Integrity check failed'); + writeResult(requestId, { + success: false, + error: error instanceof Error ? error.message : String(error) + }); + } +} + +async function handleIntegrityApprove( + task: any, + service: IntegrityService, + logger: any +): Promise { + const { requestId, path: filePath, note, approvedBy, groupFolder } = task; + + logger.info({ requestId, filePath, groupFolder }, 'Processing integrity_approve'); + + try { + const monitor = service.getMonitor(); + + await monitor.approveChange(filePath, approvedBy || 'agent', note || ''); + + writeResult(requestId, { + success: true, + path: filePath, + approved_at: new Date().toISOString(), + approved_by: approvedBy, + note + }); + + logger.info({ requestId, filePath }, 'File change approved'); + } catch (error) { + logger.error({ error, requestId, filePath }, 'Approve change failed'); + writeResult(requestId, { + success: false, + error: error instanceof Error ? error.message : String(error), + path: filePath + }); + } +} + +async function handleIntegrityStatus( + task: any, + service: IntegrityService, + logger: any +): Promise { + const { requestId, path: filePath, groupFolder } = task; + + logger.info({ requestId, filePath, groupFolder }, 'Processing integrity_status'); + + try { + const monitor = service.getMonitor(); + const status = monitor.getStatus(filePath); + + writeResult(requestId, { + success: true, + ...status + }); + + logger.info({ requestId }, 'Status retrieved'); + } catch (error) { + logger.error({ error, requestId }, 'Status check failed'); + writeResult(requestId, { + success: false, + error: error instanceof Error ? error.message : String(error) + }); + } +} + +async function handleIntegrityVerifyAudit( + task: any, + service: IntegrityService, + logger: any +): Promise { + const { requestId, groupFolder } = task; + + logger.info({ requestId, groupFolder }, 'Processing integrity_verify_audit'); + + try { + const monitor = service.getMonitor(); + const verification = monitor.verifyAuditChain(); + + writeResult(requestId, { + success: true, + ...verification + }); + + if (!verification.valid) { + logger.error({ requestId, errors: verification.errors }, 'Audit chain verification failed'); + } else { + logger.info({ requestId, entries: verification.entries }, 'Audit chain verified'); + } + } catch (error) { + logger.error({ error, requestId }, 'Audit verification failed'); + writeResult(requestId, { + success: false, + error: error instanceof Error ? error.message : String(error) + }); + } +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +function writeResult(requestId: string, result: any): void { + const resultDir = '/workspace/ipc/clawsec_results'; + + // Ensure directory exists + if (!fs.existsSync(resultDir)) { + fs.mkdirSync(resultDir, { recursive: true }); + } + + const resultPath = path.join(resultDir, `${requestId}.json`); + fs.writeFileSync(resultPath, JSON.stringify(result, null, 2)); +} + +// ============================================================================ +// Integration Instructions +// ============================================================================ + +/** + * To integrate into NanoClaw host process: + * + * 1. Add IntegrityService to IpcDeps in src/ipc.ts: + * + * import { IntegrityService, getIntegrityService } from '../skills/clawsec-nanoclaw/host-services/integrity-handler'; + * + * export interface IpcDeps { + * // ... existing deps + * integrityService?: IntegrityService; + * } + * + * 2. Initialize in main.ts: + * + * const integrityService = getIntegrityService(); + * await integrityService.initialize(); + * + * const ipcDeps: IpcDeps = { + * // ... existing deps + * integrityService + * }; + * + * 3. Add handler calls in processTaskIpc switch statement: + * + * case 'integrity_check': + * case 'integrity_approve': + * case 'integrity_status': + * case 'integrity_verify_audit': + * await handleIntegrityIpc(task, deps, logger); + * break; + * + * 4. Ensure /workspace/ipc/clawsec_results/ directory exists and is writable + * + * 5. Ensure /workspace/project/data/soul-guardian/ directory exists and is writable + */ + +// Example scheduled task for continuous monitoring: +// +// schedule_task({ +// prompt: ` +// Run clawsec_check_integrity to check for file tampering. +// If drift_detected is true and files were restored, send alert: +// "SECURITY: Unauthorized changes detected and reverted in: +// [list restored files with their paths] +// Review patches in /workspace/project/data/soul-guardian/patches/" +// `, +// schedule_type: 'cron', +// schedule_value: '*/30 * * * *', // Every 30 minutes +// context_mode: 'isolated' +// }); diff --git a/.agents/skills/clawsec-nanoclaw/host-services/ipc-handlers.ts b/.agents/skills/clawsec-nanoclaw/host-services/ipc-handlers.ts new file mode 100644 index 00000000..e4d86fdd --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/host-services/ipc-handlers.ts @@ -0,0 +1,105 @@ +/** + * ClawSec Advisory Feed IPC Handler Additions for NanoClaw + * + * Add this case to the switch statement in /workspace/project/src/ipc.ts + * inside the processTaskIpc function. + * + * This handler processes advisory cache refresh requests from agents. + */ + +import { AdvisoryCacheManager } from './advisory-cache'; +import { SkillSignatureVerifier } from './skill-signature-handler'; + +// Add to IpcDeps interface: +export interface IpcDeps { + advisoryCacheManager?: AdvisoryCacheManager; + signatureVerifier?: SkillSignatureVerifier; +} + +interface IpcLogger { + info(obj: Record, msg?: string): void; + warn(obj: Record, msg?: string): void; + error(obj: Record, msg?: string): void; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type IpcTask = Record; + +/** + * Placeholder for the host-side writeResponse function. + * The actual implementation lives in the NanoClaw host process. + */ +declare function writeResponse(requestId: string, data: Record): Promise; + +/** + * Handle advisory and signature IPC tasks. + * + * In the host process, call this from the processTaskIpc switch statement + * for the 'refresh_advisory_cache' and 'verify_skill_signature' cases. + */ +export async function handleAdvisoryIpc( + task: IpcTask, + deps: IpcDeps, + logger: IpcLogger, + sourceGroup: string, +): Promise { + switch (task.type) { + case 'refresh_advisory_cache': + // Any group can request cache refresh (rate-limited by cache manager) + logger.info({ sourceGroup }, 'Advisory cache refresh requested via IPC'); + if (deps.advisoryCacheManager) { + try { + await deps.advisoryCacheManager.refresh(); + logger.info({ sourceGroup }, 'Advisory cache refreshed successfully'); + } catch (error) { + logger.error({ error, sourceGroup }, 'Advisory cache refresh failed'); + } + } else { + logger.warn({ sourceGroup }, 'Advisory cache manager not initialized'); + } + break; + + case 'verify_skill_signature': { + // Skill signature verification (Phase 1) + const { requestId, packagePath, signaturePath } = task; + + logger.info({ sourceGroup, requestId, packagePath }, 'Verifying skill signature'); + + try { + if (!deps.signatureVerifier) { + throw new Error('Signature verification service not available'); + } + + const result = await deps.signatureVerifier.verify({ + packagePath, + signaturePath, + }); + + await writeResponse(requestId, { + success: true, + message: result.valid ? 'Signature valid' : 'Signature invalid', + data: result, + }); + + logger.info( + { sourceGroup, requestId, valid: result.valid, signer: result.signer }, + 'Signature verification completed' + ); + } catch (error: unknown) { + const err = error as Error & { code?: string }; + logger.error({ error, sourceGroup, requestId, packagePath }, 'Signature verification failed'); + + const errorCode = err.code || 'CRYPTO_ERROR'; + await writeResponse(requestId, { + success: false, + message: err.message || 'Verification failed', + error: { + code: errorCode, + details: error + } + }); + } + break; + } + } +} diff --git a/.agents/skills/clawsec-nanoclaw/host-services/skill-signature-handler.ts b/.agents/skills/clawsec-nanoclaw/host-services/skill-signature-handler.ts new file mode 100644 index 00000000..4c5b6480 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/host-services/skill-signature-handler.ts @@ -0,0 +1,272 @@ +/** + * Skill Signature Verification Handler for NanoClaw + * + * Verifies Ed25519 signatures on skill packages to prevent supply chain attacks. + * Uses the same pinned public key as advisory feed verification. + */ + +import fs from 'fs'; +import path from 'path'; +import { + verifyDetachedSignatureWithDetails, + loadPublicKey, + sha256File, + SecurityPolicyError +} from '../lib/signatures.js'; + +/** + * Default location of ClawSec's pinned public key (same as advisory feed) + */ +const DEFAULT_PUBLIC_KEY_PATH = path.join( + __dirname, + '../advisories/feed-signing-public.pem' +); + +/** + * Verification result interface + */ +export interface VerificationResult { + valid: boolean; + signer: string | null; + packageHash: string; + verifiedAt: string; + algorithm: 'Ed25519'; + error?: string; +} + +/** + * Verification parameters interface + */ +export interface VerifyParams { + packagePath: string; + signaturePath: string; +} + +const ALLOWED_PACKAGE_ROOTS = [ + '/tmp', + '/var/tmp', + '/workspace/ipc', + '/workspace/project/data', + '/workspace/project/tmp', + '/workspace/project/downloads', +] as const; + +const ALLOWED_PACKAGE_EXTENSIONS = ['.zip', '.tar', '.tgz', '.tar.gz'] as const; + +function isWithinAllowedRoots(filePath: string): boolean { + return ALLOWED_PACKAGE_ROOTS.some((root) => filePath === root || filePath.startsWith(`${root}/`)); +} + +function hasAllowedPackageExtension(filePath: string): boolean { + return ALLOWED_PACKAGE_EXTENSIONS.some((ext) => filePath.endsWith(ext)); +} + +function normalizeAndValidatePath(rawPath: string, kind: 'package' | 'signature'): string { + if (!path.isAbsolute(rawPath)) { + throw new SecurityPolicyError(`${kind} path must be absolute`); + } + + const resolved = path.resolve(rawPath); + if (!isWithinAllowedRoots(resolved)) { + throw new SecurityPolicyError( + `${kind} path must be under allowed roots: ${ALLOWED_PACKAGE_ROOTS.join(', ')}` + ); + } + + if (kind === 'package' && !hasAllowedPackageExtension(resolved)) { + throw new SecurityPolicyError( + `package path must use one of: ${ALLOWED_PACKAGE_EXTENSIONS.join(', ')}` + ); + } + + if (kind === 'signature' && !resolved.endsWith('.sig')) { + throw new SecurityPolicyError('signature path must end with .sig'); + } + + return resolved; +} + +function ensureExistingRegularFile(filePath: string, kind: 'package' | 'signature'): string { + if (!fs.existsSync(filePath)) { + throw new SecurityPolicyError(`${kind} file not found: ${filePath}`); + } + + const stat = fs.lstatSync(filePath); + if (stat.isSymbolicLink()) { + throw new SecurityPolicyError(`${kind} path cannot be a symlink`); + } + if (!stat.isFile()) { + throw new SecurityPolicyError(`${kind} path must be a regular file`); + } + + const realPath = fs.realpathSync(filePath); + if (!isWithinAllowedRoots(realPath)) { + throw new SecurityPolicyError(`${kind} real path escapes allowed roots`); + } + + return realPath; +} + +function validatePackagePath(rawPackagePath: string): string { + const resolved = normalizeAndValidatePath(rawPackagePath, 'package'); + return ensureExistingRegularFile(resolved, 'package'); +} + +function validateSignaturePath(rawSignaturePath: string): string { + const resolved = normalizeAndValidatePath(rawSignaturePath, 'signature'); + return ensureExistingRegularFile(resolved, 'signature'); +} + +/** + * Service class for skill package signature verification + */ +export class SkillSignatureVerifier { + private publicKeyPath: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private logger: any; + + constructor( + publicKeyPath: string = DEFAULT_PUBLIC_KEY_PATH, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logger?: any + ) { + this.publicKeyPath = publicKeyPath; + this.logger = logger || console; + } + + /** + * Verify Ed25519 signature of a skill package + */ + async verify(params: VerifyParams): Promise { + const { + packagePath, + signaturePath, + } = params; + + let validatedPackagePath: string; + let validatedSignaturePath: string; + try { + validatedPackagePath = validatePackagePath(packagePath); + validatedSignaturePath = validateSignaturePath(signaturePath); + } catch (error) { + return { + valid: false, + signer: null, + packageHash: '', + verifiedAt: new Date().toISOString(), + algorithm: 'Ed25519', + error: error instanceof Error ? error.message : String(error), + }; + } + + // Load pinned ClawSec key only + let keyPem: string; + try { + if (!fs.existsSync(this.publicKeyPath)) { + return { + valid: false, + signer: null, + packageHash: '', + verifiedAt: new Date().toISOString(), + algorithm: 'Ed25519', + error: `Public key file not found: ${this.publicKeyPath}` + }; + } + + keyPem = fs.readFileSync(this.publicKeyPath, 'utf8'); + loadPublicKey(keyPem); // Validate pinned key + } catch (error) { + if (error instanceof SecurityPolicyError) { + return { + valid: false, + signer: null, + packageHash: '', + verifiedAt: new Date().toISOString(), + algorithm: 'Ed25519', + error: error.message + }; + } + return { + valid: false, + signer: null, + packageHash: '', + verifiedAt: new Date().toISOString(), + algorithm: 'Ed25519', + error: `Failed to load public key: ${error instanceof Error ? error.message : String(error)}` + }; + } + + // Compute package hash (always, for integrity tracking) + let packageHash: string; + try { + packageHash = sha256File(validatedPackagePath); + } catch (error) { + return { + valid: false, + signer: null, + packageHash: '', + verifiedAt: new Date().toISOString(), + algorithm: 'Ed25519', + error: `Failed to compute package hash: ${error instanceof Error ? error.message : String(error)}` + }; + } + + // Verify signature + const verificationResult = verifyDetachedSignatureWithDetails( + validatedPackagePath, + validatedSignaturePath, + keyPem + ); + + // Return structured result + return { + valid: verificationResult.valid, + signer: verificationResult.valid ? 'clawsec' : null, + packageHash, + verifiedAt: new Date().toISOString(), + algorithm: 'Ed25519', + error: verificationResult.error + }; + } + + /** + * Get public key fingerprint for auditing + */ + getPublicKeyFingerprint(): string { + try { + const keyPem = fs.readFileSync(this.publicKeyPath, 'utf8'); + const keyObject = loadPublicKey(keyPem); + const _keyDer = keyObject.export({ type: 'spki', format: 'der' }); + return `sha256:${sha256File(this.publicKeyPath).substring(0, 16)}`; + } catch (error) { + this.logger.error({ error }, 'Failed to compute public key fingerprint'); + return 'unknown'; + } + } +} + +/** + * Error codes for IPC responses + */ +export const ErrorCodes = { + SIGNATURE_INVALID: 'SIGNATURE_INVALID', + FILE_NOT_FOUND: 'FILE_NOT_FOUND', + CRYPTO_ERROR: 'CRYPTO_ERROR', + SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE' +} as const; + +/** + * Map verification errors to standard error codes + */ +export function mapErrorCode(error: string): string { + if (error.includes('not found')) { + return ErrorCodes.FILE_NOT_FOUND; + } + if (error.includes('Invalid signature') || error.includes('verification failed')) { + return ErrorCodes.SIGNATURE_INVALID; + } + if (error.includes('public key') || error.includes('PEM')) { + return ErrorCodes.CRYPTO_ERROR; + } + return ErrorCodes.CRYPTO_ERROR; +} diff --git a/.agents/skills/clawsec-nanoclaw/lib/advisories.ts b/.agents/skills/clawsec-nanoclaw/lib/advisories.ts new file mode 100644 index 00000000..efd6db37 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/lib/advisories.ts @@ -0,0 +1,349 @@ +/** + * Advisory Feed Loading and Matching for NanoClaw + * Ported from ClawSec's feed.mjs with fail-closed verification + */ + +import fs from 'fs/promises'; +import path from 'path'; +import { + Advisory, + AdvisoryFeed, + AdvisoryMatch, + AffectedSpecifier, + SignatureVerificationOptions, +} from './types.js'; +import { + verifySignedPayload, + parseChecksumsManifest, + verifyChecksums, + fetchText, + defaultChecksumsUrl, + SecurityPolicyError, +} from './signatures.js'; + +const DEFAULT_FEED_URL = 'https://clawsec.prompt.security/advisories/feed.json'; + +/** + * Validates that a payload is a valid advisory feed. + */ +export function isValidFeedPayload(raw: unknown): raw is AdvisoryFeed { + if (typeof raw !== 'object' || raw === null) return false; + const obj = raw as Record; + + if (typeof obj.version !== 'string' || !obj.version.trim()) return false; + if (!Array.isArray(obj.advisories)) return false; + + for (const advisory of obj.advisories) { + if (typeof advisory !== 'object' || advisory === null) return false; + const adv = advisory as Record; + + if (typeof adv.id !== 'string' || !adv.id.trim()) return false; + if (typeof adv.severity !== 'string' || !adv.severity.trim()) return false; + if (!Array.isArray(adv.affected)) return false; + if (!adv.affected.every((entry) => typeof entry === 'string' && entry.trim())) return false; + } + + return true; +} + +/** + * Parses an affected specifier like "skill-name@version-spec". + */ +export function parseAffectedSpecifier(rawSpecifier: string): AffectedSpecifier | null { + const specifier = rawSpecifier.trim(); + if (!specifier) return null; + + const atIndex = specifier.lastIndexOf('@'); + if (atIndex <= 0) { + return { name: specifier, versionSpec: '*' }; + } + + return { + name: specifier.slice(0, atIndex), + versionSpec: specifier.slice(atIndex + 1), + }; +} + +/** + * Normalizes a skill name for comparison. + */ +export function normalizeSkillName(name: string): string { + return name.toLowerCase().trim().replace(/[^a-z0-9-]/g, ''); +} + +/** + * Checks if a version matches a version specifier. + * Supports: exact match, semver range (^, ~, *), wildcards + */ +export function versionMatches(version: string, versionSpec: string): boolean { + const v = version.trim(); + const spec = versionSpec.trim(); + + // Wildcard matches everything + if (spec === '*' || spec === '') return true; + + // Exact match + if (v === spec) return true; + + // Parse semver components + const parseVersion = (ver: string): number[] => { + const match = ver.match(/^(\d+)\.(\d+)\.(\d+)/); + if (!match) return []; + return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)]; + }; + + const vParts = parseVersion(v); + const specParts = parseVersion(spec.replace(/^[~^]/, '')); + + if (vParts.length === 0 || specParts.length === 0) return false; + + // Caret range (^1.2.3): compatible with 1.x.x where x >= 2.3 + if (spec.startsWith('^')) { + if (vParts[0] !== specParts[0]) return false; + if (vParts[0] === 0) { + // ^0.2.3 means 0.2.x where x >= 3 + if (vParts[1] !== specParts[1]) return false; + return vParts[2] >= specParts[2]; + } + // ^1.2.3 means 1.x.x where x.x >= 2.3 + if (vParts[1] > specParts[1]) return true; + if (vParts[1] < specParts[1]) return false; + return vParts[2] >= specParts[2]; + } + + // Tilde range (~1.2.3): patch-level compatibility (1.2.x where x >= 3) + if (spec.startsWith('~')) { + if (vParts[0] !== specParts[0]) return false; + if (vParts[1] !== specParts[1]) return false; + return vParts[2] >= specParts[2]; + } + + return false; +} + +/** + * Checks whether an affected specifier matches a skill name/version. + * Optionally matches against a skill directory name as alias. + */ +export function matchesAffectedSpecifier( + affected: string, + skillName: string, + skillVersion: string | null, + skillDirName?: string +): boolean { + const parsed = parseAffectedSpecifier(affected); + if (!parsed) return false; + + const normalizedTarget = normalizeSkillName(parsed.name); + const normalizedSkillName = normalizeSkillName(skillName); + const normalizedDirName = skillDirName ? normalizeSkillName(skillDirName) : null; + + if (normalizedTarget !== normalizedSkillName && normalizedTarget !== normalizedDirName) { + return false; + } + + if (!skillVersion) { + return true; + } + + return versionMatches(skillVersion, parsed.versionSpec); +} + +/** + * Loads advisory feed from a remote URL with signature verification. + */ +export async function loadRemoteFeed( + feedUrl: string, + options: SignatureVerificationOptions +): Promise { + const signatureUrl = options.signatureUrl || `${feedUrl}.sig`; + const checksumsUrl = options.checksumsUrl || defaultChecksumsUrl(feedUrl); + const checksumsSignatureUrl = options.checksumsSignatureUrl || `${checksumsUrl}.sig`; + const publicKeyPem = options.publicKeyPem; + const checksumsPublicKeyPem = options.checksumsPublicKeyPem || publicKeyPem; + const allowUnsigned = options.allowUnsigned || false; + const verifyChecksumManifest = options.verifyChecksumManifest !== false; + + try { + const payloadRaw = await fetchText(feedUrl); + if (!payloadRaw) return null; + + if (!allowUnsigned) { + const signatureRaw = await fetchText(signatureUrl); + if (!signatureRaw) return null; + + if (!verifySignedPayload(payloadRaw, signatureRaw, publicKeyPem)) { + return null; + } + + // Verify checksum manifest if available + if (verifyChecksumManifest) { + const checksumsRaw = await fetchText(checksumsUrl); + const checksumsSignatureRaw = await fetchText(checksumsSignatureUrl); + + // Only proceed if BOTH checksum files are present + if (checksumsRaw && checksumsSignatureRaw) { + if (!verifySignedPayload(checksumsRaw, checksumsSignatureRaw, checksumsPublicKeyPem)) { + return null; // Fail-closed: invalid signature + } + + const checksumsManifest = parseChecksumsManifest(checksumsRaw); + const checksumFeedEntry = feedUrl.split('/').pop() || 'feed.json'; + const checksumSignatureEntry = signatureUrl.split('/').pop() || 'feed.json.sig'; + verifyChecksums(checksumsManifest, { + [checksumFeedEntry]: payloadRaw, + [checksumSignatureEntry]: signatureRaw, + }); + } + // If checksum files missing: continue without checksum verification + // (feed signature was already verified above) + } + } + + try { + const payload = JSON.parse(payloadRaw); + if (!isValidFeedPayload(payload)) return null; + return payload; + } catch { + return null; + } + } catch (error) { + // Security policy violations return null to allow graceful fallback to local feed + if (error instanceof SecurityPolicyError) { + return null; + } + // Re-throw unexpected errors + throw error; + } +} + +/** + * Loads advisory feed from a local file with signature verification. + */ +export async function loadLocalFeed( + feedPath: string, + options: SignatureVerificationOptions +): Promise { + const signaturePath = options.signatureUrl || `${feedPath}.sig`; + const checksumsPath = options.checksumsUrl || path.join(path.dirname(feedPath), 'checksums.json'); + const checksumsSignaturePath = options.checksumsSignatureUrl || `${checksumsPath}.sig`; + const publicKeyPem = options.publicKeyPem; + const checksumsPublicKeyPem = options.checksumsPublicKeyPem || publicKeyPem; + const allowUnsigned = options.allowUnsigned || false; + const verifyChecksumManifest = options.verifyChecksumManifest !== false; + + const payloadRaw = await fs.readFile(feedPath, 'utf8'); + + if (!allowUnsigned) { + const signatureRaw = await fs.readFile(signaturePath, 'utf8'); + if (!verifySignedPayload(payloadRaw, signatureRaw, publicKeyPem)) { + throw new Error(`Feed signature verification failed for local feed: ${feedPath}`); + } + + if (verifyChecksumManifest) { + const checksumsRaw = await fs.readFile(checksumsPath, 'utf8'); + const checksumsSignatureRaw = await fs.readFile(checksumsSignaturePath, 'utf8'); + + if (!verifySignedPayload(checksumsRaw, checksumsSignatureRaw, checksumsPublicKeyPem)) { + throw new Error(`Checksum manifest signature verification failed: ${checksumsPath}`); + } + + const checksumsManifest = parseChecksumsManifest(checksumsRaw); + const checksumFeedEntry = path.basename(feedPath); + const checksumSignatureEntry = path.basename(signaturePath); + verifyChecksums(checksumsManifest, { + [checksumFeedEntry]: payloadRaw, + [checksumSignatureEntry]: signatureRaw, + }); + } + } + + const payload = JSON.parse(payloadRaw); + if (!isValidFeedPayload(payload)) { + throw new Error(`Invalid advisory feed format: ${feedPath}`); + } + return payload; +} + +/** + * Loads advisory feed from remote or falls back to local. + */ +export async function loadFeed( + feedUrl: string = DEFAULT_FEED_URL, + localFeedPath: string, + publicKeyPem: string, + allowUnsigned: boolean = false +): Promise<{ feed: AdvisoryFeed; source: string }> { + const options: SignatureVerificationOptions = { + publicKeyPem, + allowUnsigned, + verifyChecksumManifest: true, + }; + + // Try remote feed first + const remoteFeed = await loadRemoteFeed(feedUrl, options); + if (remoteFeed) { + return { feed: remoteFeed, source: `remote:${feedUrl}` }; + } + + // Fall back to local feed + const localFeed = await loadLocalFeed(localFeedPath, options); + return { feed: localFeed, source: `local:${localFeedPath}` }; +} + +/** + * Checks if an advisory looks high-risk. + */ +export function advisoryLooksHighRisk(advisory: Advisory): boolean { + const type = advisory.type.toLowerCase(); + const severity = advisory.severity.toLowerCase(); + const exploitability = (advisory.exploitability_score || 'unknown').toLowerCase(); + const combined = `${advisory.title} ${advisory.description} ${advisory.action}`.toLowerCase(); + + if (type.includes('malicious')) return true; + if (severity === 'critical') return true; + if (exploitability === 'high') return true; + if (/\b(malicious|exfiltrate|exfiltration|backdoor|trojan|stealer|credential theft)\b/.test(combined)) return true; + if (/\b(remove|uninstall|disable|do not use|quarantine)\b/.test(combined)) return true; + + return false; +} + +/** + * Finds advisory matches for a skill. + */ +export function findAdvisoryMatches( + feed: AdvisoryFeed, + skillName: string, + version: string | null +): AdvisoryMatch[] { + const matches: AdvisoryMatch[] = []; + + for (const advisory of feed.advisories) { + const affected = advisory.affected || []; + if (affected.length === 0) continue; + + for (const specifier of affected) { + if (!matchesAffectedSpecifier(specifier, skillName, version)) { + continue; + } + + // Match found + matches.push({ + advisory, + matchedSpecifier: specifier, + isHighRisk: advisoryLooksHighRisk(advisory), + }); + break; // Only count each advisory once + } + } + + return matches; +} + +/** + * Removes duplicate strings from an array. + */ +export function uniqueStrings(arr: string[]): string[] { + return Array.from(new Set(arr)); +} diff --git a/.agents/skills/clawsec-nanoclaw/lib/local_file_io.ts b/.agents/skills/clawsec-nanoclaw/lib/local_file_io.ts new file mode 100644 index 00000000..64f19fc1 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/lib/local_file_io.ts @@ -0,0 +1,13 @@ +import fs from 'fs'; + +export function fileExists(filePath: string): boolean { + return fs.existsSync(filePath); +} + +export function loadBinaryFile(filePath: string): Buffer { + return fs.readFileSync(filePath); +} + +export function loadUtf8File(filePath: string): string { + return fs.readFileSync(filePath, 'utf8'); +} diff --git a/.agents/skills/clawsec-nanoclaw/lib/risk.ts b/.agents/skills/clawsec-nanoclaw/lib/risk.ts new file mode 100644 index 00000000..b5f2ae22 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/lib/risk.ts @@ -0,0 +1,88 @@ +/** + * Shared advisory risk evaluation for NanoClaw host + MCP layers. + */ + +export type SkillSafetyRecommendation = 'install' | 'block' | 'review'; + +export interface AdvisoryRiskInput { + severity?: string; + type?: string; + action?: string; + exploitability_score?: string; +} + +export interface AdvisoryRiskEvaluation { + safe: boolean; + recommendation: SkillSafetyRecommendation; + reason: string; +} + +export function normalizeExploitabilityScore(score: unknown): 'high' | 'medium' | 'low' | 'unknown' { + const value = String(score || '').toLowerCase().trim(); + if (value === 'high' || value === 'medium' || value === 'low') { + return value; + } + return 'unknown'; +} + +export function evaluateAdvisoryRisk(advisories: AdvisoryRiskInput[]): AdvisoryRiskEvaluation { + if (advisories.length === 0) { + return { safe: true, recommendation: 'install', reason: 'No advisories found' }; + } + + const hasMalicious = advisories.some((a) => String(a.type || '').toLowerCase().includes('malicious')); + const hasRemoveAction = advisories.some((a) => + /\b(remove|uninstall|disable|quarantine|block)\b/i.test(String(a.action || '')) + ); + const hasCritical = advisories.some((a) => String(a.severity || '').toLowerCase() === 'critical'); + const hasHigh = advisories.some((a) => String(a.severity || '').toLowerCase() === 'high'); + const hasHighExploitability = advisories.some( + (a) => normalizeExploitabilityScore(a.exploitability_score) === 'high' + ); + + if (hasMalicious || hasRemoveAction) { + return { + safe: false, + recommendation: 'block', + reason: 'Malicious skill or removal recommended by ClawSec', + }; + } + + if (hasCritical && hasHighExploitability) { + return { + safe: false, + recommendation: 'block', + reason: 'Critical advisory with high exploitability context - do not install', + }; + } + + if (hasCritical) { + return { + safe: false, + recommendation: 'block', + reason: 'Critical security advisory - do not install', + }; + } + + if (hasHighExploitability) { + return { + safe: false, + recommendation: 'review', + reason: 'High exploitability advisory - urgent user review strongly recommended', + }; + } + + if (hasHigh) { + return { + safe: false, + recommendation: 'review', + reason: 'High severity advisory - user review strongly recommended', + }; + } + + return { + safe: false, + recommendation: 'review', + reason: 'Advisory found - review details before installing', + }; +} diff --git a/.agents/skills/clawsec-nanoclaw/lib/signatures.ts b/.agents/skills/clawsec-nanoclaw/lib/signatures.ts new file mode 100644 index 00000000..46505ca6 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/lib/signatures.ts @@ -0,0 +1,497 @@ +/** + * Ed25519 Signature Verification for NanoClaw + * Ported from ClawSec's feed.mjs + */ + +import crypto from 'crypto'; +import https from 'https'; +import { ChecksumsManifest } from './types.js'; +import { fileExists, loadBinaryFile, loadUtf8File } from './local_file_io.js'; + +/** + * Allowed domains for feed/signature fetching. + * Only connections to these domains are permitted for security. + */ +const ALLOWED_DOMAINS = [ + 'clawsec.prompt.security', + 'prompt.security', + 'raw.githubusercontent.com', + 'github.com', +]; + +/** + * Custom error class for security policy violations. + * These errors should always propagate and never be silently caught. + */ +export class SecurityPolicyError extends Error { + constructor(message: string) { + super(message); + this.name = 'SecurityPolicyError'; + } +} + +/** + * Creates a secure HTTPS agent with TLS 1.2+ enforcement and certificate validation. + */ +function createSecureAgent(): https.Agent { + return new https.Agent({ + // Enforce minimum TLS 1.2 (eliminate TLS 1.0, 1.1) + minVersion: 'TLSv1.2', + // Ensure certificate validation is enabled (reject unauthorized certificates) + rejectUnauthorized: true, + // Use strong cipher suites + ciphers: 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256', + }); +} + +/** + * Validates that a URL is from an allowed domain. + */ +function isAllowedDomain(url: string): boolean { + try { + const parsed = new URL(url); + + // Only allow HTTPS protocol + if (parsed.protocol !== 'https:') { + return false; + } + + const hostname = parsed.hostname.toLowerCase(); + + // Check if hostname matches any allowed domain + return ALLOWED_DOMAINS.some( + (allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`) + ); + } catch { + return false; + } +} + +/** + * Secure wrapper around fetch with TLS enforcement and domain validation. + */ +export async function secureFetch(url: string, options: RequestInit = {}): Promise { + // Validate domain before making request + if (!isAllowedDomain(url)) { + throw new SecurityPolicyError( + `Security policy violation: URL domain not allowed. ` + + `Only connections to ${ALLOWED_DOMAINS.join(', ')} are permitted. ` + + `Blocked: ${url}` + ); + } + + // Use secure HTTPS agent with TLS 1.2+ enforcement + const agent = createSecureAgent(); + + return fetch(url, { + ...options, + // @ts-expect-error - agent is supported in Node.js fetch + agent, + }); +} + +/** + * Decodes a signature from various formats (base64 string or JSON). + */ +function decodeSignature(signatureRaw: string): Buffer | null { + const trimmed = signatureRaw.trim(); + if (!trimmed) return null; + + let encoded = trimmed; + if (trimmed.startsWith('{')) { + try { + const parsed = JSON.parse(trimmed); + if (typeof parsed === 'object' && parsed !== null && typeof parsed.signature === 'string') { + encoded = parsed.signature; + } + } catch { + return null; + } + } + + const normalized = encoded.replace(/\s+/g, ''); + if (!normalized) return null; + + try { + return Buffer.from(normalized, 'base64'); + } catch { + return null; + } +} + +/** + * Verifies an Ed25519 signature for a payload. + */ +export function verifySignedPayload( + payloadRaw: string, + signatureRaw: string, + publicKeyPem: string +): boolean { + const signature = decodeSignature(signatureRaw); + if (!signature) return false; + + const keyPem = publicKeyPem.trim(); + if (!keyPem) return false; + + try { + const publicKey = crypto.createPublicKey(keyPem); + return crypto.verify(null, Buffer.from(payloadRaw, 'utf8'), publicKey, signature); + } catch { + return false; + } +} + +/** + * Computes SHA-256 hash of content. + */ +export function sha256Hex(content: string | Buffer): string { + return crypto.createHash('sha256').update(content).digest('hex'); +} + +/** + * Computes SHA-256 hash of a file. + * Convenience wrapper for file-based integrity monitoring and package verification. + */ +export function sha256File(filePath: string): string { + const data = loadBinaryFile(filePath); + return sha256Hex(data); +} + +/** + * Loads and validates an Ed25519 public key from PEM format. + * @throws {SecurityPolicyError} if PEM format is invalid + */ +export function loadPublicKey(pemString: string): crypto.KeyObject { + const trimmed = pemString.trim(); + if (!trimmed.startsWith('-----BEGIN PUBLIC KEY-----')) { + throw new SecurityPolicyError('Invalid PEM format: must start with -----BEGIN PUBLIC KEY-----'); + } + + try { + return crypto.createPublicKey(trimmed); + } catch (error) { + throw new SecurityPolicyError( + `Failed to load public key: ${error instanceof Error ? error.message : String(error)}` + ); + } +} + +/** + * Verifies Ed25519 detached signature for a file. + * Matches the API of verify_detached_ed25519.mjs from OpenClaw. + * + * @param dataPath - Path to the file to verify + * @param signaturePath - Path to the detached signature file (.sig) + * @param publicKeyPem - Ed25519 public key in PEM format + * @returns true if signature is valid, false otherwise + */ +export function verifyDetachedSignature( + dataPath: string, + signaturePath: string, + publicKeyPem: string +): boolean { + try { + const data = loadBinaryFile(dataPath); + const signatureRaw = loadUtf8File(signaturePath); + const signature = decodeSignature(signatureRaw); + + if (!signature) return false; + + const publicKey = crypto.createPublicKey(publicKeyPem.trim()); + return crypto.verify(null, data, publicKey, signature); + } catch { + return false; + } +} + +/** + * Verifies detached signature with detailed error information. + * Useful for debugging signature verification failures. + * + * @param dataPath - Path to the file to verify + * @param signaturePath - Path to the detached signature file (.sig) + * @param publicKeyPem - Ed25519 public key in PEM format + * @returns Object with valid flag and optional error message + */ +export function verifyDetachedSignatureWithDetails( + dataPath: string, + signaturePath: string, + publicKeyPem: string +): { valid: boolean; error?: string } { + try { + if (!fileExists(dataPath)) { + return { valid: false, error: 'Data file not found' }; + } + if (!fileExists(signaturePath)) { + return { valid: false, error: 'Signature file not found' }; + } + + const data = loadBinaryFile(dataPath); + const signatureRaw = loadUtf8File(signaturePath); + const signature = decodeSignature(signatureRaw); + + if (!signature) { + return { valid: false, error: 'Invalid signature format' }; + } + + const publicKey = crypto.createPublicKey(publicKeyPem.trim()); + const valid = crypto.verify(null, data, publicKey, signature); + + return { valid, error: valid ? undefined : 'Signature verification failed' }; + } catch (error) { + return { + valid: false, + error: `Verification error: ${error instanceof Error ? error.message : String(error)}` + }; + } +} + +/** + * Verifies multiple files against expected hashes. + * Returns list of files that don't match their expected hashes. + * + * @param files - Map of file paths to expected SHA-256 hashes + * @returns Array of mismatches with path, expected, and actual hashes + */ +export function verifyFileHashes( + files: Record +): { path: string; expected: string; actual: string }[] { + const mismatches = []; + + for (const [path, expectedHash] of Object.entries(files)) { + try { + const actualHash = sha256File(path); + if (actualHash !== expectedHash) { + mismatches.push({ path, expected: expectedHash, actual: actualHash }); + } + } catch (error) { + // File missing or unreadable + mismatches.push({ + path, + expected: expectedHash, + actual: `ERROR: ${error instanceof Error ? error.message : String(error)}` + }); + } + } + + return mismatches; +} + +/** + * Extracts SHA-256 value from various formats. + */ +function extractSha256Value(value: unknown): string | null { + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase(); + return /^[a-f0-9]{64}$/.test(normalized) ? normalized : null; + } + + if (typeof value === 'object' && value !== null && 'sha256' in value) { + const sha256 = (value as { sha256: unknown }).sha256; + if (typeof sha256 === 'string') { + const normalized = sha256.trim().toLowerCase(); + return /^[a-f0-9]{64}$/.test(normalized) ? normalized : null; + } + } + + return null; +} + +/** + * Parses a checksums manifest JSON. + */ +export function parseChecksumsManifest(manifestRaw: string): ChecksumsManifest { + let parsed: unknown; + try { + parsed = JSON.parse(manifestRaw); + } catch { + throw new Error('Checksum manifest is not valid JSON'); + } + + if (typeof parsed !== 'object' || parsed === null) { + throw new Error('Checksum manifest must be an object'); + } + + const obj = parsed as Record; + + const algorithmRaw = typeof obj.algorithm === 'string' ? obj.algorithm.trim().toLowerCase() : 'sha256'; + if (algorithmRaw !== 'sha256') { + throw new Error(`Unsupported checksum manifest algorithm: ${algorithmRaw || '(empty)'}`); + } + + // Support legacy manifest formats + const schemaVersion = ( + typeof obj.schema_version === 'string' ? obj.schema_version.trim() : + typeof obj.version === 'string' ? obj.version.trim() : + typeof obj.generated_at === 'string' ? obj.generated_at.trim() : + '1' + ); + + if (!schemaVersion) { + throw new Error('Checksum manifest missing schema_version'); + } + + if (typeof obj.files !== 'object' || obj.files === null) { + throw new Error('Checksum manifest missing files object'); + } + + const files: Record = {}; + for (const [key, value] of Object.entries(obj.files)) { + if (!key.trim()) continue; + const digest = extractSha256Value(value); + if (!digest) { + throw new Error(`Invalid checksum digest entry for ${key}`); + } + files[key] = digest; + } + + if (Object.keys(files).length === 0) { + throw new Error('Checksum manifest has no usable file digests'); + } + + return { + schema_version: schemaVersion, + algorithm: 'sha256', + files, + }; +} + +/** + * Normalizes a checksum entry name for matching. + */ +function normalizeChecksumEntryName(entryName: string): string { + return entryName + .trim() + .replace(/\\/g, '/') + .replace(/^(?:\.\/)+/, '') + .replace(/^\/+/, ''); +} + +/** + * Resolves a checksum manifest entry by name. + */ +function resolveChecksumManifestEntry( + files: Record, + entryName: string +): { key: string; digest: string } | null { + const normalizedEntry = normalizeChecksumEntryName(entryName); + if (!normalizedEntry) return null; + + // Try direct match and common variations + const directCandidates = [ + normalizedEntry, + normalizedEntry.split('/').pop() || '', + `advisories/${normalizedEntry.split('/').pop() || ''}`, + ].filter((c, i, a) => c && a.indexOf(c) === i); + + for (const candidate of directCandidates) { + if (candidate in files) { + return { key: candidate, digest: files[candidate] }; + } + } + + // Try basename matching + const basename = normalizedEntry.split('/').pop() || ''; + if (!basename) return null; + + const basenameMatches = Object.entries(files).filter(([key]) => { + const normalizedKey = normalizeChecksumEntryName(key); + return normalizedKey.split('/').pop() === basename; + }); + + if (basenameMatches.length > 1) { + throw new Error( + `Checksum manifest entry is ambiguous for ${entryName}; ` + + `multiple manifest keys share basename ${basename}` + ); + } + + if (basenameMatches.length === 1) { + const [resolvedKey, digest] = basenameMatches[0]; + return { key: resolvedKey, digest }; + } + + return null; +} + +/** + * Verifies checksums for expected entries. + */ +export function verifyChecksums( + manifest: ChecksumsManifest, + expectedEntries: Record +): void { + for (const [entryName, entryContent] of Object.entries(expectedEntries)) { + if (!entryName) continue; + + const resolved = resolveChecksumManifestEntry(manifest.files, entryName); + if (!resolved) { + throw new Error(`Checksum manifest missing required entry: ${entryName}`); + } + + const actualDigest = sha256Hex(entryContent); + if (actualDigest !== resolved.digest) { + throw new Error(`Checksum mismatch for ${entryName} (manifest key: ${resolved.key})`); + } + } +} + +/** + * Fetches text from a URL with timeout. + */ +export async function fetchText(url: string, timeoutMs: number = 10000): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await secureFetch(url, { + method: 'GET', + signal: controller.signal, + headers: { accept: 'application/json,text/plain;q=0.9,*/*;q=0.8' }, + }); + if (!response.ok) return null; + return await response.text(); + } catch (error) { + // Re-throw security policy violations - these should never be silently caught + if (error instanceof SecurityPolicyError) { + throw error; + } + // Network errors, timeouts, etc. return null (graceful degradation) + return null; + } finally { + clearTimeout(timeout); + } +} + +/** + * Default checksums URL from feed URL. + */ +export function defaultChecksumsUrl(feedUrl: string): string { + try { + return new URL('checksums.json', feedUrl).toString(); + } catch { + const fallbackBase = feedUrl.replace(/\/?[^/]*$/, ''); + return `${fallbackBase}/checksums.json`; + } +} + +/** + * Safely extracts the basename from a URL or file path. + */ +function _safeBasename(urlOrPath: string, fallback: string): string { + try { + const parsed = new URL(urlOrPath); + const pathname = parsed.pathname; + const lastSlash = pathname.lastIndexOf('/'); + if (lastSlash >= 0 && lastSlash < pathname.length - 1) { + return pathname.slice(lastSlash + 1); + } + } catch { + const normalized = urlOrPath.trim(); + const lastSlash = normalized.lastIndexOf('/'); + if (lastSlash >= 0 && lastSlash < normalized.length - 1) { + return normalized.slice(lastSlash + 1); + } + } + return fallback; +} diff --git a/.agents/skills/clawsec-nanoclaw/lib/types.ts b/.agents/skills/clawsec-nanoclaw/lib/types.ts new file mode 100644 index 00000000..bb6afe08 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/lib/types.ts @@ -0,0 +1,254 @@ +/** + * TypeScript types for NanoClaw Skill Installer + * Adapted from ClawSec's guarded skill installer + */ + +export interface Advisory { + id: string; + severity: 'critical' | 'high' | 'medium' | 'low'; + type: 'vulnerable_skill' | 'malicious_skill' | 'prompt_injection' | string; + title: string; + description: string; + affected: string[]; // e.g., ["skill-name@1.0.0", "skill-name@1.0.1"] + action: string; + published: string; + references: string[]; + cvss_score?: number; + nvd_url?: string; + exploitability_score?: 'high' | 'medium' | 'low' | 'unknown'; + exploitability_rationale?: string; + source?: string; + github_issue_url?: string; + reporter?: { + agent_name?: string; + opener_type?: string; + }; +} + +export interface AdvisoryFeed { + version: string; + updated: string; + description: string; + advisories: Advisory[]; +} + +export interface AdvisoryMatch { + advisory: Advisory; + matchedSpecifier: string; + isHighRisk: boolean; +} + +export interface ReputationResult { + score: number; // 0-100 + warnings: string[]; + virusTotalFlags: string[]; + safe: boolean; +} + +export interface SkillMetadata { + slug: string; + name: string; + version: string; + description: string; + author: string; + created: string; + updated: string; + downloads: number; +} + +export interface InspectSkillResult { + skill: SkillMetadata; + reputation: ReputationResult; + advisories: AdvisoryMatch[]; + overallStatus: 'safe' | 'reputation_warning' | 'advisory_warning' | 'blocked'; +} + +export interface SkillInstallRequest { + request_id: string; + user_jid: string; + group_jid: string; + skill_slug: string; + skill_version: string | null; + reputation_score: number; + reputation_warnings: string[]; + advisories: AdvisoryMatch[]; + created_at: number; // Unix timestamp + expires_at: number; // Unix timestamp + status: 'pending' | 'confirmed' | 'expired' | 'cancelled'; + confirmed_at: number | null; +} + +export interface ChecksumsManifest { + schema_version: string; + algorithm: 'sha256'; + files: Record; // filename -> hex digest +} + +export interface SignatureVerificationOptions { + signatureUrl?: string; + checksumsUrl?: string; + checksumsSignatureUrl?: string; + publicKeyPem: string; + checksumsPublicKeyPem?: string; + allowUnsigned?: boolean; + verifyChecksumManifest?: boolean; +} + +export interface AffectedSpecifier { + name: string; + versionSpec: string; // e.g., "1.0.0", "^1.0.0", "*" +} + +// MCP Tool Request/Response Types + +export interface InspectSkillRequest { + slug: string; + version?: string; +} + +export interface RequestSkillInstallRequest { + slug: string; + version?: string; + target_group_jid?: string; +} + +export interface RequestSkillInstallResponse { + request_id: string; + status: 'safe' | 'reputation_warning' | 'advisory_warning' | 'blocked'; + reputation?: ReputationResult; + advisories?: AdvisoryMatch[]; + message: string; +} + +export interface ConfirmSkillInstallRequest { + request_id: string; + acknowledge_reputation?: boolean; + acknowledge_advisories?: boolean; +} + +export interface ConfirmSkillInstallResponse { + status: 'installed' | 'failed'; + installed_path?: string; + error?: string; +} + +export interface ListSkillsRequest { + target_group_jid?: string; +} + +export interface ListSkillsResponse { + skills: Array<{ + slug: string; + version: string; + installed_at: string; + path: string; + }>; +} + +export interface RemoveSkillRequest { + slug: string; + target_group_jid?: string; +} + +export interface RemoveSkillResponse { + status: 'removed' | 'not_found'; + message: string; +} + +// IPC Task Types + +export interface IpcSkillInstallRequest { + type: 'skill_install_request'; + slug: string; + version?: string; + target_group_jid?: string; + user_jid: string; + group_folder: string; + timestamp: string; +} + +export interface IpcSkillInstallConfirm { + type: 'skill_install_confirm'; + request_id: string; + acknowledge_reputation: boolean; + acknowledge_advisories: boolean; + user_jid: string; + group_folder: string; + timestamp: string; +} + +export interface IpcSkillRemove { + type: 'skill_remove'; + slug: string; + target_group_jid?: string; + user_jid: string; + group_folder: string; + timestamp: string; +} + +// Database Schema + +export interface SkillInstallRequestRow { + request_id: string; + user_jid: string; + group_jid: string; + skill_slug: string; + skill_version: string | null; + reputation_score: number; + reputation_warnings_json: string; // JSON array + advisories_json: string; // JSON array + created_at: number; + expires_at: number; + status: 'pending' | 'confirmed' | 'expired' | 'cancelled'; + confirmed_at: number | null; +} + +export interface InstalledSkillRow { + slug: string; + version: string; + installed_at: string; + installed_by: string; // user_jid + path: string; + metadata_json: string; // SkillMetadata as JSON +} + +// Skill Signature Verification Types (Phase 1) + +/** + * IPC request for skill signature verification + */ +export interface VerifySkillSignatureRequest { + type: 'verify_skill_signature'; + requestId: string; + groupFolder: string; + timestamp: string; + packagePath: string; + signaturePath: string; +} + +/** + * IPC response for skill signature verification + */ +export interface VerifySkillSignatureResponse { + success: boolean; + message: string; + data?: { + valid: boolean; + signer: string; // 'clawsec' or custom signer identifier + packageHash: string; // SHA-256 of package + verifiedAt: string; // ISO timestamp + algorithm: 'Ed25519'; + }; + error?: { + code: 'SIGNATURE_INVALID' | 'FILE_NOT_FOUND' | 'CRYPTO_ERROR' | 'SERVICE_UNAVAILABLE'; + details?: unknown; + }; +} + +/** + * MCP tool parameters for package verification + */ +export interface VerifySkillPackageParams { + packagePath: string; + signaturePath?: string; // Optional: auto-detects .sig if omitted +} diff --git a/.agents/skills/clawsec-nanoclaw/mcp-tools/advisory-tools.ts b/.agents/skills/clawsec-nanoclaw/mcp-tools/advisory-tools.ts new file mode 100644 index 00000000..c705785d --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/mcp-tools/advisory-tools.ts @@ -0,0 +1,377 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * ClawSec Advisory Feed MCP Tools for NanoClaw + * + * Add these tools to /workspace/project/container/agent-runner/src/ipc-mcp-stdio.ts + * + * These tools run in the container context and read from the host-managed + * advisory cache at /workspace/project/data/clawsec-advisory-cache.json + */ + +import fs from 'fs'; +import path from 'path'; +import { z } from 'zod'; +import { evaluateAdvisoryRisk, normalizeExploitabilityScore } from '../lib/risk.js'; +import { matchesAffectedSpecifier } from '../lib/advisories.js'; + +// These variables are provided by the host environment (ipc-mcp-stdio.ts) +// when this code is integrated into the NanoClaw container agent. +declare const server: { tool: (...args: any[]) => void }; +declare function writeIpcFile(dir: string, data: any): void; +declare const TASKS_DIR: string; +declare const groupFolder: string; +const CACHE_FILE = '/workspace/project/data/clawsec-advisory-cache.json'; + +const severityOrder: Record = { critical: 0, high: 1, medium: 2, low: 3 }; +const exploitabilityOrder: Record = { high: 0, medium: 1, low: 2, unknown: 3 }; + +/** + * Discover installed skills in a directory + */ +async function discoverInstalledSkills(installRoot: string): Promise> { + const skills: Array<{ name: string; version: string | null; dirName: string }> = []; + + try { + const entries = fs.readdirSync(installRoot, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + + const skillJsonPath = path.join(installRoot, entry.name, 'skill.json'); + try { + const raw = fs.readFileSync(skillJsonPath, 'utf8'); + const parsed = JSON.parse(raw); + skills.push({ + name: parsed.name || entry.name, + version: parsed.version || null, + dirName: entry.name, + }); + } catch { + // Skill without skill.json, use directory name + skills.push({ + name: entry.name, + version: null, + dirName: entry.name, + }); + } + } + } catch { + // Return empty if directory doesn't exist + } + + return skills; +} + +/** + * Find advisory matches for installed skills + */ +function findAdvisoryMatches( + advisories: any[], + skills: Array<{ name: string; version: string | null; dirName: string }> +): Array<{ + advisory: any; + skill: { name: string; version: string | null; dirName: string }; + matchedAffected: string[]; +}> { + const matches: Array<{ + advisory: any; + skill: { name: string; version: string | null; dirName: string }; + matchedAffected: string[]; + }> = []; + + for (const advisory of advisories) { + for (const skill of skills) { + const matchedAffected: string[] = []; + + for (const affected of advisory.affected || []) { + if (matchesAffectedSpecifier(affected, skill.name, skill.version, skill.dirName)) { + matchedAffected.push(affected); + } + } + + if (matchedAffected.length > 0) { + matches.push({ advisory, skill, matchedAffected }); + } + } + } + + return matches; +} + +// Add these tools to the server: + +server.tool( + 'clawsec_check_advisories', + 'Check ClawSec advisory feed for security issues affecting installed skills. Returns list of matching advisories with details. Use this to scan for known vulnerabilities, malicious skills, or deprecated packages.', + { + installRoot: z.string().optional().describe('Skills installation directory (default: ~/.claude/skills)'), + forceRefresh: z.boolean().optional().describe('Force cache refresh before checking (causes 1-2 second delay)'), + }, + async (args) => { + // Request cache refresh if needed + if (args.forceRefresh) { + writeIpcFile(TASKS_DIR, { + type: 'refresh_advisory_cache', + groupFolder, + timestamp: new Date().toISOString(), + }); + // Wait for refresh (async, best-effort) + await new Promise(resolve => setTimeout(resolve, 2000)); + } + + // Read cache from shared mount + try { + const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); + const installRoot = args.installRoot || path.join(process.env.HOME || '~', '.claude', 'skills'); + + // Discover installed skills + const skills = await discoverInstalledSkills(installRoot); + + // Find matches + const matches = findAdvisoryMatches(cacheData.feed.advisories, skills); + + // Calculate cache age + const cacheAge = Date.now() - Date.parse(cacheData.fetchedAt); + const cacheAgeMinutes = Math.floor(cacheAge / 60000); + + const result = { + success: true, + feedUpdated: cacheData.feed.updated || null, + totalAdvisories: cacheData.feed.advisories.length, + installedSkills: skills.length, + matches: matches.map(m => ({ + advisory: { + id: m.advisory.id, + severity: m.advisory.severity, + type: m.advisory.type, + title: m.advisory.title, + description: m.advisory.description, + action: m.advisory.action, + published: m.advisory.published, + exploitability_score: normalizeExploitabilityScore(m.advisory.exploitability_score), + exploitability_rationale: m.advisory.exploitability_rationale || null, + }, + skill: m.skill, + matchedAffected: m.matchedAffected, + })), + cacheAge: `${cacheAgeMinutes} minutes`, + cacheTimestamp: cacheData.fetchedAt, + }; + + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + }; + } catch (error) { + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: false, + error: `Failed to check advisories: ${error instanceof Error ? error.message : String(error)}` + }, null, 2) + }], + isError: true, + }; + } + } +); + +server.tool( + 'clawsec_check_skill_safety', + 'Check if a specific skill is safe to install based on ClawSec advisory feed. Returns safety recommendation (install/block/review) with reasons. Use this as a pre-install gate before installing any skill.', + { + skillName: z.string().describe('Name of skill to check'), + skillVersion: z.string().optional().describe('Version of skill (optional, for version-specific checks)'), + }, + async (args) => { + try { + const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); + + // Find matching advisories for this skill + const matchingAdvisories = cacheData.feed.advisories.filter((advisory: any) => + advisory.affected.some((affected: string) => { + return matchesAffectedSpecifier(affected, args.skillName, args.skillVersion || null); + }) + ); + + if (matchingAdvisories.length === 0) { + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + safe: true, + advisories: [], + recommendation: 'install', + reason: 'No known advisories for this skill', + }, null, 2), + }], + }; + } + + const risk = evaluateAdvisoryRisk(matchingAdvisories); + + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + safe: risk.safe, + advisories: matchingAdvisories.map((a: any) => ({ + id: a.id, + severity: a.severity, + type: a.type, + title: a.title, + description: a.description, + action: a.action, + published: a.published, + affected: a.affected, + exploitability_score: normalizeExploitabilityScore(a.exploitability_score), + exploitability_rationale: a.exploitability_rationale || null, + })), + recommendation: risk.recommendation, + reason: risk.reason, + skillName: args.skillName, + skillVersion: args.skillVersion || null, + advisoryCount: matchingAdvisories.length, + }, null, 2), + }], + }; + } catch (error) { + // Conservative: block on error + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + safe: false, + advisories: [], + recommendation: 'review', + reason: `Failed to verify safety: ${error instanceof Error ? error.message : String(error)}`, + error: true, + }, null, 2), + }], + }; + } + } +); + +server.tool( + 'clawsec_list_advisories', + 'List ClawSec advisories with optional filtering. Use this to browse security advisories, filter by severity/type/exploitability, or search for specific affected skills.', + { + severity: z.enum(['critical', 'high', 'medium', 'low']).optional().describe('Filter by severity level'), + type: z.string().optional().describe('Filter by advisory type (for example: vulnerable_skill, malicious_skill, prompt_injection)'), + exploitabilityScore: z.enum(['high', 'medium', 'low', 'unknown']).optional() + .describe('Filter by exploitability score'), + affectedSkill: z.string().optional().describe('Filter by affected skill name (partial match supported)'), + limit: z.number().optional().describe('Maximum number of results (default: unlimited)'), + }, + async (args) => { + try { + const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); + let advisories = [...cacheData.feed.advisories]; + + // Apply filters + if (args.severity) { + advisories = advisories.filter((a: any) => a.severity === args.severity); + } + if (args.type) { + const typeFilter = String(args.type).toLowerCase().trim(); + advisories = advisories.filter((a: any) => String(a.type || '').toLowerCase().trim() === typeFilter); + } + if (args.exploitabilityScore) { + advisories = advisories.filter( + (a: any) => normalizeExploitabilityScore(a.exploitability_score) === args.exploitabilityScore + ); + } + if (args.affectedSkill) { + advisories = advisories.filter((a: any) => + a.affected.some((spec: string) => spec.includes(args.affectedSkill!)) + ); + } + + // Sort by exploitability first, then severity, then publish date (newest first). + advisories.sort((a: any, b: any) => { + const exploitabilityDiff = + (exploitabilityOrder[normalizeExploitabilityScore(a.exploitability_score)] ?? 999) - + (exploitabilityOrder[normalizeExploitabilityScore(b.exploitability_score)] ?? 999); + if (exploitabilityDiff !== 0) return exploitabilityDiff; + + const severityDiff = (severityOrder[a.severity] || 999) - (severityOrder[b.severity] || 999); + if (severityDiff !== 0) return severityDiff; + return (b.published || '').localeCompare(a.published || ''); + }); + + // Apply limit + const originalCount = advisories.length; + if (args.limit && args.limit > 0) { + advisories = advisories.slice(0, args.limit); + } + + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: true, + feedUpdated: cacheData.feed.updated || null, + advisories: advisories.map((a: any) => ({ + id: a.id, + severity: a.severity, + type: a.type, + title: a.title, + description: a.description, + action: a.action, + published: a.published, + affected: a.affected, + exploitability_score: normalizeExploitabilityScore(a.exploitability_score), + exploitability_rationale: a.exploitability_rationale || null, + })), + total: cacheData.feed.advisories.length, + filtered: originalCount, + returned: advisories.length, + filters: { + severity: args.severity || null, + type: args.type || null, + exploitabilityScore: args.exploitabilityScore || null, + affectedSkill: args.affectedSkill || null, + limit: args.limit || null, + }, + }, null, 2), + }], + }; + } catch (error) { + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: false, + error: `Failed to list advisories: ${error instanceof Error ? error.message : String(error)}`, + }, null, 2), + }], + isError: true, + }; + } + } +); + +server.tool( + 'clawsec_refresh_cache', + 'Request immediate refresh of the advisory cache from ClawSec feed. This fetches the latest advisories and verifies signatures. Use when you need up-to-date advisory information.', + {}, + async () => { + writeIpcFile(TASKS_DIR, { + type: 'refresh_advisory_cache', + groupFolder, + timestamp: new Date().toISOString(), + }); + + return { + content: [{ + type: 'text' as const, + text: 'Advisory cache refresh requested. This may take a few seconds. Check status with clawsec_check_advisories.', + }], + }; + } +); diff --git a/.agents/skills/clawsec-nanoclaw/mcp-tools/integrity-tools.ts b/.agents/skills/clawsec-nanoclaw/mcp-tools/integrity-tools.ts new file mode 100644 index 00000000..70a1b76e --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/mcp-tools/integrity-tools.ts @@ -0,0 +1,249 @@ +/** + * ClawSec File Integrity Monitoring MCP Tools for NanoClaw + * + * Add these tools to /workspace/project/container/agent-runner/src/ipc-mcp-stdio.ts + * + * These tools run in the container context and communicate with the host-side + * integrity monitor via IPC. + */ + +import fs from 'fs'; +import path from 'path'; +import { z } from 'zod'; + +// These variables are provided by the host environment (ipc-mcp-stdio.ts) +// when this code is integrated into the NanoClaw container agent. +/* eslint-disable @typescript-eslint/no-explicit-any */ +declare const server: { tool: (...args: any[]) => void }; +declare function writeIpcFile(dir: string, data: any): void; +declare const TASKS_DIR: string; +declare const groupFolder: string; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +// Result waiting helper +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function waitForResult(requestId: string, timeoutMs: number = 60000): Promise { + const resultDir = '/workspace/ipc/clawsec_results'; + const resultPath = path.join(resultDir, `${requestId}.json`); + + const startTime = Date.now(); + while (Date.now() - startTime < timeoutMs) { + if (fs.existsSync(resultPath)) { + const result = JSON.parse(fs.readFileSync(resultPath, 'utf-8')); + fs.unlinkSync(resultPath); // Cleanup + return result; + } + await new Promise(resolve => setTimeout(resolve, 1000)); // Poll every 1s + } + + throw new Error(`Timeout waiting for result: ${requestId}`); +} + +// ============================================================================ +// MCP Tool 1: clawsec_check_integrity +// ============================================================================ + +server.tool( + 'clawsec_check_integrity', + 'Check protected files for unauthorized changes (drift). Automatically restores critical files to approved baselines. Use this for scheduled integrity monitoring or manual security checks.', + { + mode: z.enum(['check', 'status']).optional().describe('check=detect drift and restore, status=view baselines only (default: check)'), + autoRestore: z.boolean().optional().describe('Auto-restore files in restore mode (default: true)'), + }, + async (args) => { + const requestId = `integrity-check-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + + // Write IPC request + writeIpcFile(TASKS_DIR, { + type: 'integrity_check', + requestId, + mode: args.mode || 'check', + autoRestore: args.autoRestore !== false, + groupFolder, + timestamp: new Date().toISOString() + }); + + try { + // Wait for result + const result = await waitForResult(requestId, 60000); + + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + isError: !result.success + }; + } catch (error) { + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: false, + error: `Integrity check failed: ${error instanceof Error ? error.message : String(error)}` + }, null, 2) + }], + isError: true + }; + } + } +); + +// ============================================================================ +// MCP Tool 2: clawsec_approve_change +// ============================================================================ + +server.tool( + 'clawsec_approve_change', + 'Approve an intentional file modification as the new approved baseline. Use this after making legitimate changes to protected files (e.g., updating CLAUDE.md or registered_groups.json).', + { + path: z.string().describe('Absolute path to file to approve (e.g., /workspace/group/CLAUDE.md)'), + note: z.string().optional().describe('Optional note explaining why this change is being approved'), + }, + async (args) => { + const requestId = `integrity-approve-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + + // Write IPC request + writeIpcFile(TASKS_DIR, { + type: 'integrity_approve', + requestId, + path: args.path, + note: args.note || '', + approvedBy: 'agent', // In production, should be user JID + groupFolder, + timestamp: new Date().toISOString() + }); + + try { + const result = await waitForResult(requestId, 30000); + + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + isError: !result.success + }; + } catch (error) { + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: false, + error: `Approve failed: ${error instanceof Error ? error.message : String(error)}` + }, null, 2) + }], + isError: true + }; + } + } +); + +// ============================================================================ +// MCP Tool 3: clawsec_integrity_status +// ============================================================================ + +server.tool( + 'clawsec_integrity_status', + 'View current baseline status for protected files without checking for drift. Use this to see what files are monitored, when baselines were created, and their current hashes.', + { + path: z.string().optional().describe('Optional: specific file path to check. If omitted, shows all protected files.'), + }, + async (args) => { + const requestId = `integrity-status-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + + writeIpcFile(TASKS_DIR, { + type: 'integrity_status', + requestId, + path: args.path, + groupFolder, + timestamp: new Date().toISOString() + }); + + try { + const result = await waitForResult(requestId, 30000); + + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + isError: !result.success + }; + } catch (error) { + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: false, + error: `Status check failed: ${error instanceof Error ? error.message : String(error)}` + }, null, 2) + }], + isError: true + }; + } + } +); + +// ============================================================================ +// MCP Tool 4: clawsec_verify_audit +// ============================================================================ + +server.tool( + 'clawsec_verify_audit', + 'Verify the integrity of the audit log hash chain. Use this to detect if the audit log has been tampered with. A valid chain proves all logged events are authentic.', + {}, + async () => { + const requestId = `integrity-verify-audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + + writeIpcFile(TASKS_DIR, { + type: 'integrity_verify_audit', + requestId, + groupFolder, + timestamp: new Date().toISOString() + }); + + try { + const result = await waitForResult(requestId, 30000); + + return { + content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }], + isError: !result.success + }; + } catch (error) { + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: false, + error: `Audit verification failed: ${error instanceof Error ? error.message : String(error)}` + }, null, 2) + }], + isError: true + }; + } + } +); + +// ============================================================================ +// Usage Examples (for documentation) +// ============================================================================ + +// Usage Examples (for documentation): +// +// Example 1: Scheduled Integrity Check +// +// schedule_task({ +// prompt: 'Check file integrity with clawsec_check_integrity...', +// schedule_type: 'cron', +// schedule_value: '0,30 * * * *', // Every 30 minutes +// context_mode: 'isolated' +// }); +// +// Example 2: Pre-Deployment Check +// +// const check = await tools.clawsec_check_integrity({ mode: 'check', autoRestore: false }); +// if (check.drift_detected) { ... } +// +// Example 3: Approve Legitimate Changes +// +// await tools.clawsec_approve_change({ +// path: '/workspace/group/CLAUDE.md', +// note: 'Updated agent instructions to include new skill' +// }); +// +// Example 4: Audit Verification +// +// const audit = await tools.clawsec_verify_audit(); +// if (!audit.valid) { ... } diff --git a/.agents/skills/clawsec-nanoclaw/mcp-tools/signature-verification.ts b/.agents/skills/clawsec-nanoclaw/mcp-tools/signature-verification.ts new file mode 100644 index 00000000..216e8181 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/mcp-tools/signature-verification.ts @@ -0,0 +1,226 @@ +/** + * ClawSec Skill Signature Verification MCP Tool for NanoClaw + * + * Add this tool to /workspace/project/container/agent-runner/src/ipc-mcp-stdio.ts + * + * This tool verifies Ed25519 signatures on skill packages to prevent supply chain attacks. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import fs from 'fs'; +import path from 'path'; +import { z } from 'zod'; + +// These variables are provided by the host environment (ipc-mcp-stdio.ts) +// when this code is integrated into the NanoClaw container agent. +declare const server: { tool: (...args: any[]) => void }; +declare function writeIpcFile(dir: string, data: any): void; +declare const TASKS_DIR: string; +declare const groupFolder: string; + +const ALLOWED_VERIFICATION_ROOTS = [ + '/tmp', + '/var/tmp', + '/workspace/ipc', + '/workspace/project/data', + '/workspace/project/tmp', + '/workspace/project/downloads', +] as const; + +const ALLOWED_PACKAGE_EXTENSIONS = ['.zip', '.tar', '.tgz', '.tar.gz'] as const; + +function isWithinAllowedRoots(filePath: string): boolean { + return ALLOWED_VERIFICATION_ROOTS.some((root) => filePath === root || filePath.startsWith(`${root}/`)); +} + +function validatePackagePath(rawPath: string): string { + if (!path.isAbsolute(rawPath)) { + throw new Error('packagePath must be absolute'); + } + + const resolved = path.resolve(rawPath); + if (!isWithinAllowedRoots(resolved)) { + throw new Error(`packagePath must be under: ${ALLOWED_VERIFICATION_ROOTS.join(', ')}`); + } + + if (!ALLOWED_PACKAGE_EXTENSIONS.some((ext) => resolved.endsWith(ext))) { + throw new Error(`packagePath must end with one of: ${ALLOWED_PACKAGE_EXTENSIONS.join(', ')}`); + } + + return resolved; +} + +function validateSignaturePath(rawPath: string): string { + if (!path.isAbsolute(rawPath)) { + throw new Error('signaturePath must be absolute'); + } + + const resolved = path.resolve(rawPath); + if (!isWithinAllowedRoots(resolved)) { + throw new Error(`signaturePath must be under: ${ALLOWED_VERIFICATION_ROOTS.join(', ')}`); + } + + if (!resolved.endsWith('.sig')) { + throw new Error('signaturePath must end with .sig'); + } + + return resolved; +} + +// Result waiting helper +async function waitForResult(requestId: string, timeoutMs: number = 5000): Promise { + const resultDir = '/workspace/ipc/clawsec_results'; + const resultPath = path.join(resultDir, `${requestId}.json`); + + const startTime = Date.now(); + while (Date.now() - startTime < timeoutMs) { + if (fs.existsSync(resultPath)) { + const result = JSON.parse(fs.readFileSync(resultPath, 'utf-8')); + fs.unlinkSync(resultPath); // Cleanup + return result; + } + await new Promise(resolve => setTimeout(resolve, 100)); // Poll every 100ms + } + + throw new Error(`Timeout waiting for result: ${requestId}`); +} + +// ============================================================================ +// MCP Tool: clawsec_verify_skill_package +// ============================================================================ + +server.tool( + 'clawsec_verify_skill_package', + 'Verify Ed25519 signature of a skill package before installation. Prevents installation of tampered or malicious skill packages by checking ClawSec signatures.', + { + packagePath: z.string().describe('Absolute path to skill package (.tar.gz or .zip)'), + signaturePath: z.string().optional().describe('Path to signature file. If omitted, auto-detects .sig'), + }, + async (args: { packagePath: string; signaturePath?: string }) => { + const requestId = `verify-signature-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + let packagePath: string; + let sigPath: string; + + try { + packagePath = validatePackagePath(args.packagePath); + sigPath = validateSignaturePath(args.signaturePath || `${packagePath}.sig`); + } catch (error) { + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: false, + valid: false, + recommendation: 'block', + error: error instanceof Error ? error.message : String(error), + }, null, 2) + }], + isError: true + }; + } + + // Validate package file exists + if (!fs.existsSync(packagePath)) { + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: false, + valid: false, + recommendation: 'block', + error: `Package file not found: ${packagePath}` + }, null, 2) + }], + isError: true + }; + } + + // Write IPC request to host + writeIpcFile(TASKS_DIR, { + type: 'verify_skill_signature', + requestId, + groupFolder, + timestamp: new Date().toISOString(), + packagePath, + signaturePath: sigPath, + }); + + try { + // Wait for host to verify (5 second timeout) + const result = await waitForResult(requestId, 5000); + + if (!result.success) { + // Service error or file not found + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: false, + valid: false, + recommendation: 'block', + packagePath, + signaturePath: sigPath, + error: result.message || 'Verification failed', + reason: result.error?.code || 'UNKNOWN_ERROR' + }, null, 2) + }], + isError: true + }; + } + + // Check if signature is valid + if (!result.data?.valid) { + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: true, + valid: false, + recommendation: 'block', + packagePath, + signaturePath: sigPath, + reason: result.data?.error || 'Signature verification failed', + packageInfo: { + sha256: result.data?.packageHash || 'unknown' + } + }, null, 2) + }], + }; + } + + // Signature valid! + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: true, + valid: true, + recommendation: 'install', + packagePath, + signaturePath: sigPath, + signer: result.data.signer, + algorithm: result.data.algorithm, + verifiedAt: result.data.verifiedAt, + packageInfo: { + size: fs.statSync(packagePath).size, + sha256: result.data.packageHash + } + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ + type: 'text' as const, + text: JSON.stringify({ + success: false, + valid: false, + recommendation: 'block', + error: `Verification timeout or error: ${error instanceof Error ? error.message : String(error)}` + }, null, 2) + }], + isError: true + }; + } + } +); diff --git a/.agents/skills/clawsec-nanoclaw/skill.json b/.agents/skills/clawsec-nanoclaw/skill.json new file mode 100644 index 00000000..20fadb2d --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/skill.json @@ -0,0 +1,158 @@ +{ + "name": "clawsec-nanoclaw", + "version": "0.0.5", + "description": "ClawSec security suite for NanoClaw - Advisory feed monitoring, MCP tools for vulnerability checking, and Ed25519 signature verification for containerized WhatsApp bot agents", + "author": "prompt-security", + "license": "AGPL-3.0-or-later", + "homepage": "https://clawsec.prompt.security/", + "keywords": [ + "security", + "nanoclaw", + "whatsapp-bot", + "mcp-tools", + "advisory", + "feed", + "threat-intel", + "containers", + "signature-verification", + "vulnerability-scanning", + "agents", + "ai" + ], + "platform": "nanoclaw", + "sbom": { + "files": [ + { + "path": "SKILL.md", + "required": true, + "description": "NanoClaw skill documentation" + }, + { + "path": "CHANGELOG.md", + "required": true, + "description": "Version history and release notes" + }, + { + "path": "INSTALL.md", + "required": true, + "description": "Installation guide for NanoClaw deployments" + }, + { + "path": "mcp-tools/advisory-tools.ts", + "required": true, + "description": "MCP tools for advisory checking in container context" + }, + { + "path": "host-services/advisory-cache.ts", + "required": true, + "description": "Host-side advisory cache manager with periodic feed fetching" + }, + { + "path": "host-services/ipc-handlers.ts", + "required": true, + "description": "IPC handlers for MCP tool requests" + }, + { + "path": "lib/signatures.ts", + "required": true, + "description": "Ed25519 signature verification utilities" + }, + { + "path": "lib/local_file_io.ts", + "required": true, + "description": "Local file access helpers used by signature verification routines" + }, + { + "path": "lib/advisories.ts", + "required": true, + "description": "Advisory matching and vulnerability detection" + }, + { + "path": "lib/types.ts", + "required": true, + "description": "TypeScript type definitions" + }, + { + "path": "lib/risk.ts", + "required": true, + "description": "Shared advisory risk evaluation logic for host and MCP tools" + }, + { + "path": "advisories/feed-signing-public.pem", + "required": true, + "description": "Pinned Ed25519 public key for feed signature verification" + }, + { + "path": "mcp-tools/signature-verification.ts", + "required": true, + "description": "Phase 1: MCP tool for skill package signature verification" + }, + { + "path": "host-services/skill-signature-handler.ts", + "required": true, + "description": "Phase 1: Host-side signature verification service" + }, + { + "path": "docs/SKILL_SIGNING.md", + "required": true, + "description": "Phase 1: Documentation for skill signing and verification" + }, + { + "path": "mcp-tools/integrity-tools.ts", + "required": true, + "description": "Phase 2: MCP tools for file integrity monitoring" + }, + { + "path": "host-services/integrity-handler.ts", + "required": true, + "description": "Phase 2: Host-side integrity monitoring service" + }, + { + "path": "guardian/integrity-monitor.ts", + "required": true, + "description": "Phase 2: Core file integrity monitoring engine" + }, + { + "path": "guardian/policy.json", + "required": true, + "description": "Phase 2: NanoClaw-specific file protection policy" + }, + { + "path": "docs/INTEGRITY.md", + "required": true, + "description": "Phase 2: Documentation for file integrity monitoring" + } + ] + }, + "capabilities": [ + "Advisory feed monitoring from clawsec.prompt.security", + "MCP tools for agent-initiated vulnerability scans", + "Exploitability-aware advisory prioritization for agent environments", + "Pre-installation skill safety checks", + "Ed25519 signature verification for advisory feeds", + "Platform metadata preserved in advisory records for downstream filtering", + "Containerized agent support with IPC communication" + ], + "nanoclaw": { + "mcp_tools": [ + "clawsec_check_advisories", + "clawsec_check_skill_safety", + "clawsec_list_advisories", + "clawsec_refresh_cache", + "clawsec_verify_skill_package", + "clawsec_check_integrity", + "clawsec_approve_change", + "clawsec_integrity_status", + "clawsec_verify_audit" + ], + "requires": { + "node": ">=18.0.0", + "nanoclaw": ">=0.1.0" + }, + "integration": { + "mcp_tools_file": "container/agent-runner/src/ipc-mcp-stdio.ts", + "ipc_handlers_file": "src/ipc.ts", + "cache_location": "/workspace/project/data/clawsec-advisory-cache.json" + } + } +} diff --git a/.agents/skills/clawsec-nanoclaw/test/security-hardening.test.mjs b/.agents/skills/clawsec-nanoclaw/test/security-hardening.test.mjs new file mode 100644 index 00000000..867a4e18 --- /dev/null +++ b/.agents/skills/clawsec-nanoclaw/test/security-hardening.test.mjs @@ -0,0 +1,57 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const SKILL_ROOT = path.resolve(__dirname, '..'); + +function readSkillFile(relativePath) { + return fs.readFileSync(path.join(SKILL_ROOT, relativePath), 'utf8'); +} + +test('signature verifier enforces pinned key and path policy', () => { + const source = readSkillFile('host-services/skill-signature-handler.ts'); + + assert.ok(!source.includes('publicKeyPem?: string'), 'publicKeyPem override must be removed'); + assert.ok(!source.includes('allowUnsigned?: boolean'), 'allowUnsigned override must be removed'); + + assert.ok(source.includes('const ALLOWED_PACKAGE_ROOTS'), 'must define allowed package roots'); + assert.ok(source.includes('validatePackagePath('), 'must validate package path before hashing'); + assert.ok(source.includes('validateSignaturePath('), 'must validate signature path before verification'); +}); + +test('IPC advisory handler does not forward key or unsigned overrides', () => { + const source = readSkillFile('host-services/ipc-handlers.ts'); + + assert.ok(!source.includes('publicKeyPem'), 'IPC handler must not accept publicKeyPem override'); + assert.ok(!source.includes('allowUnsigned'), 'IPC handler must not accept allowUnsigned override'); +}); + +test('MCP signature tool validates filesystem boundaries', () => { + const source = readSkillFile('mcp-tools/signature-verification.ts'); + + assert.ok(source.includes('const ALLOWED_VERIFICATION_ROOTS'), 'must define allowed verification roots'); + assert.ok(source.includes('validatePackagePath('), 'must validate package path in MCP layer'); + assert.ok(source.includes('validateSignaturePath('), 'must validate signature path in MCP layer'); +}); + +test('integrity approvals are restricted to policy targets', () => { + const source = readSkillFile('guardian/integrity-monitor.ts'); + + assert.ok(source.includes('const normalizedFilePath = path.resolve(filePath);'), 'must normalize approved path'); + assert.ok( + source.includes("if (!target || target.mode === 'ignore')"), + 'must require approved file to exist in non-ignored policy target list' + ); +}); + +test('integrity targets and baselines use normalized absolute paths', () => { + const source = readSkillFile('guardian/integrity-monitor.ts'); + + assert.ok(source.includes('path: path.resolve(target.path)'), 'resolveTargets must normalize direct target paths'); + assert.ok(source.includes('const normalizedFilePath = path.resolve(filePath);'), 'status/approval lookups must normalize file paths'); + assert.ok(source.includes('normalizedFiles[path.resolve(filePath)] = baseline;'), 'loaded baselines must be normalized to absolute keys'); +}); diff --git a/.agents/skills/clawsec-scanner/CHANGELOG.md b/.agents/skills/clawsec-scanner/CHANGELOG.md new file mode 100644 index 00000000..cd6075d5 --- /dev/null +++ b/.agents/skills/clawsec-scanner/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +## [0.0.3] - 2026-05-13 + +### Changed +- Re-release skill payload metadata after excluding test-only files from release SBOMs and archives. + +All notable changes to the ClawSec Scanner will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.0.2] - 2026-03-10 + +### Changed + +- Replaced simulated DAST checks with real OpenClaw hook execution harness testing +- Updated DAST semantics so high-severity findings are emitted for actual hook execution failures/timeouts, not static payload pattern matches +- Reclassified DAST harness capability limitations (for example missing TypeScript compiler for `.ts` hooks) to `info` coverage findings instead of high severity +- Added DAST harness mode guard to prevent recursive scanner execution when hook handlers are tested in isolation + +### Added + +- New DAST helper executor script for isolated per-hook execution and timeout enforcement +- DAST harness regression tests covering no-false-positive baseline and malicious-input crash detection + +## [0.0.1] - 2026-02-27 + +### Added + +- Initial release of ClawSec Scanner skill +- Automated vulnerability scanning for OpenClaw skill installations +- Integration with advisory feed for real-time security alerts +- Support for scanning skill dependencies and detecting known CVEs +- Configurable scan policies and risk thresholds +- Detailed vulnerability reporting with remediation guidance diff --git a/.agents/skills/clawsec-scanner/SKILL.md b/.agents/skills/clawsec-scanner/SKILL.md new file mode 100644 index 00000000..04a4fce9 --- /dev/null +++ b/.agents/skills/clawsec-scanner/SKILL.md @@ -0,0 +1,497 @@ +--- +name: clawsec-scanner +version: 0.0.3 +description: Automated vulnerability scanner for agent platforms. Performs dependency scanning (npm audit, pip-audit), multi-database CVE lookup (OSV, NVD, GitHub Advisory), SAST analysis (Semgrep, Bandit), and agent-specific DAST hook execution testing for OpenClaw hooks. +homepage: https://clawsec.prompt.security +clawdis: + emoji: "🔍" + requires: + bins: [node, npm, python3, pip-audit, semgrep, bandit, jq, curl] +--- + +# ClawSec Scanner + +Comprehensive security scanner for agent platforms that automates vulnerability detection across multiple dimensions: + +- **Dependency Scanning**: Analyzes npm and Python dependencies using `npm audit` and `pip-audit` with structured JSON output parsing +- **CVE Database Integration**: Queries OSV (primary), NVD 2.0, and GitHub Advisory Database for vulnerability enrichment +- **SAST Analysis**: Static code analysis using Semgrep (JavaScript/TypeScript) and Bandit (Python) to detect hardcoded secrets, command injection, path traversal, and unsafe deserialization +- **DAST Framework**: Agent-specific dynamic analysis with real OpenClaw hook execution harness (malicious input, timeout, output bounds, event mutation safety) +- **Unified Reporting**: Consolidated vulnerability reports with severity classification and remediation guidance +- **Continuous Monitoring**: OpenClaw hook integration for automated periodic scanning + +## Features + +### Multi-Engine Scanning + +The scanner orchestrates four complementary scan types to provide comprehensive vulnerability coverage: + +1. **Dependency Scanning** + - Executes `npm audit --json` and `pip-audit -f json` as subprocesses + - Parses structured output to extract CVE IDs, severity, affected versions + - Handles edge cases: missing package-lock.json, zero vulnerabilities, malformed JSON + +2. **CVE Database Queries** + - **OSV API** (primary): Free, no authentication, broad ecosystem support (npm, PyPI, Go, Maven) + - **NVD 2.0** (optional): Requires API key to avoid 6-second rate limiting + - **GitHub Advisory Database** (optional): GraphQL API with OAuth token + - Normalizes all API responses to unified `Vulnerability` schema + +3. **Static Analysis (SAST)** + - **Semgrep** for JavaScript/TypeScript: Detects security issues using `--config auto` or `--config p/security-audit` + - **Bandit** for Python: Leverages existing `pyproject.toml` configuration + - Identifies: hardcoded secrets (API keys, tokens), command injection (`eval`, `exec`), path traversal, unsafe deserialization + +4. **Dynamic Analysis (DAST)** + - Real hook execution harness for OpenClaw hook handlers discovered from `HOOK.md` metadata + - Verifies: malicious input resilience, timeout behavior, output amplification bounds, and core event mutation safety + - Note: Traditional web DAST tools (ZAP, Burp) do not apply to agent platforms - this provides agent-specific testing + +### Unified Reporting + +All scan types emit a consistent `ScanReport` JSON schema: + +```typescript +{ + scan_id: string; // UUID + timestamp: string; // ISO 8601 + target: string; // Scanned path + vulnerabilities: Vulnerability[]; + summary: { + critical: number; + high: number; + medium: number; + low: number; + info: number; + } +} +``` + +Each `Vulnerability` object includes: +- `id`: CVE-2023-12345 or GHSA-xxxx-yyyy-zzzz +- `source`: npm-audit | pip-audit | osv | nvd | github | sast | dast +- `severity`: critical | high | medium | low | info +- `package`: Package name (or 'N/A' for SAST/DAST) +- `version`: Affected version +- `fixed_version`: First version with fix (if available) +- `title`: Short description +- `description`: Full advisory text +- `references`: URLs for more info +- `discovered_at`: ISO 8601 timestamp + +### OpenClaw Integration + +Automated continuous monitoring via hook: + +- Runs scanner on configurable interval (default: 86400s / 24 hours) +- Triggers on `agent:bootstrap` and `command:new` events +- Posts findings to `event.messages` array with severity summary +- Rate-limited by `CLAWSEC_SCANNER_INTERVAL` environment variable + +## Installation + +### Prerequisites + +Verify required binaries are available: + +```bash +# Core runtimes +node --version # v20+ +npm --version +python3 --version # 3.10+ + +# Scanning tools +pip-audit --version # Install: uv pip install pip-audit +semgrep --version # Install: pip install semgrep OR brew install semgrep +bandit --version # Install: uv pip install bandit + +# Utilities +jq --version +curl --version +``` + +### Option A: Via clawhub (recommended) + +```bash +npx clawhub@latest install clawsec-scanner +``` + +### Option B: Manual installation with verification + +```bash +set -euo pipefail + +VERSION="${SKILL_VERSION:?Set SKILL_VERSION (e.g. 0.1.0)}" +INSTALL_ROOT="${INSTALL_ROOT:-$HOME/.openclaw/skills}" +DEST="$INSTALL_ROOT/clawsec-scanner" +BASE="https://github.com/prompt-security/clawsec/releases/download/clawsec-scanner-v${VERSION}" + +TEMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TEMP_DIR"' EXIT + +# Pinned release-signing public key +# Fingerprint (SHA-256 of SPKI DER): 711424e4535f84093fefb024cd1ca4ec87439e53907b305b79a631d5befba9c8 +cat > "$TEMP_DIR/release-signing-public.pem" <<'PEM' +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAS7nijfMcUoOBCj4yOXJX+GYGv2pFl2Yaha1P4v5Cm6A= +-----END PUBLIC KEY----- +PEM + +ZIP_NAME="clawsec-scanner-v${VERSION}.zip" + +# Download release archive + signed checksums +curl -fsSL "$BASE/$ZIP_NAME" -o "$TEMP_DIR/$ZIP_NAME" +curl -fsSL "$BASE/checksums.json" -o "$TEMP_DIR/checksums.json" +curl -fsSL "$BASE/checksums.sig" -o "$TEMP_DIR/checksums.sig" + +# Verify checksums manifest signature +openssl base64 -d -A -in "$TEMP_DIR/checksums.sig" -out "$TEMP_DIR/checksums.sig.bin" +if ! openssl pkeyutl -verify \ + -pubin \ + -inkey "$TEMP_DIR/release-signing-public.pem" \ + -sigfile "$TEMP_DIR/checksums.sig.bin" \ + -rawin \ + -in "$TEMP_DIR/checksums.json" >/dev/null 2>&1; then + echo "ERROR: checksums.json signature verification failed" >&2 + exit 1 +fi + +EXPECTED_SHA="$(jq -r '.archive.sha256 // empty' "$TEMP_DIR/checksums.json")" +if [ -z "$EXPECTED_SHA" ]; then + echo "ERROR: checksums.json missing archive.sha256" >&2 + exit 1 +fi + +ACTUAL_SHA="$(shasum -a 256 "$TEMP_DIR/$ZIP_NAME" | awk '{print $1}')" +if [ "$EXPECTED_SHA" != "$ACTUAL_SHA" ]; then + echo "ERROR: Archive checksum mismatch" >&2 + exit 1 +fi + +echo "Checksums verified. Installing..." + +mkdir -p "$INSTALL_ROOT" +rm -rf "$DEST" +unzip -q "$TEMP_DIR/$ZIP_NAME" -d "$INSTALL_ROOT" + +chmod 600 "$DEST/skill.json" +find "$DEST" -type f ! -name "skill.json" -exec chmod 644 {} \; + +echo "Installed clawsec-scanner v${VERSION} to: $DEST" +echo "Next step: Run a scan or set up continuous monitoring" +``` + +## Usage + +### On-Demand CLI Scanning + +```bash +SCANNER_DIR="${INSTALL_ROOT:-$HOME/.openclaw/skills}/clawsec-scanner" + +# Scan all skills with JSON output +"$SCANNER_DIR/scripts/runner.sh" --target ./skills/ --output report.json --format json + +# Scan specific directory with human-readable output +"$SCANNER_DIR/scripts/runner.sh" --target ./my-skill/ --format text + +# Check available flags +"$SCANNER_DIR/scripts/runner.sh" --help +``` + +**CLI Flags:** +- `--target `: Directory to scan (required) +- `--output `: Write results to file (optional, defaults to stdout) +- `--format `: Output format (default: json) +- `--check`: Verify all required binaries are installed + +### OpenClaw Hook Setup (Continuous Monitoring) + +Enable automated periodic scanning: + +```bash +SCANNER_DIR="${INSTALL_ROOT:-$HOME/.openclaw/skills}/clawsec-scanner" +node "$SCANNER_DIR/scripts/setup_scanner_hook.mjs" +``` + +This creates a hook that: +- Scans on `agent:bootstrap` and `command:new` events +- Respects `CLAWSEC_SCANNER_INTERVAL` rate limiting (default: 86400 seconds / 24 hours) +- Posts findings to conversation with severity summary +- Recommends remediation for high/critical vulnerabilities + +Restart the OpenClaw gateway after enabling the hook, then run `/new` to trigger an immediate scan. + +### Environment Variables + +```bash +# Optional - NVD API key to avoid rate limiting (6-second delays without key) +export CLAWSEC_NVD_API_KEY="your-nvd-api-key" + +# Optional - GitHub OAuth token for Advisory Database queries +export GITHUB_TOKEN="ghp_your_token_here" + +# Optional - Scanner hook interval in seconds (default: 86400 / 24 hours) +export CLAWSEC_SCANNER_INTERVAL="86400" + +# Optional - Allow unsigned advisory feed during development (from clawsec-suite) +export CLAWSEC_ALLOW_UNSIGNED_FEED="1" +``` + +## Architecture + +### Modular Design + +Each scan type is an independent module that can run standalone or as part of unified scan: + +``` +scripts/runner.sh # Orchestration layer +├── scan_dependencies.mjs # npm audit + pip-audit +├── query_cve_databases.mjs # OSV/NVD/GitHub API queries +├── sast_analyzer.mjs # Semgrep + Bandit static analysis +├── dast_runner.mjs # Dynamic security testing orchestration +└── dast_hook_executor.mjs # Isolated real hook execution harness + +lib/ +├── report.mjs # Result aggregation and formatting +├── utils.mjs # Subprocess exec, JSON parsing, error handling +└── types.ts # TypeScript schema definitions + +hooks/clawsec-scanner-hook/ +├── HOOK.md # OpenClaw hook metadata +└── handler.ts # Periodic scan trigger +``` + +### Fail-Open Philosophy + +The scanner prioritizes availability over strict failure propagation: + +- Network failures → emit partial results, log warnings +- Missing tools → skip that scan type, continue with others +- Malformed JSON → parse what's valid, log errors +- API rate limits → implement exponential backoff, fallback to other sources +- Zero vulnerabilities → emit success report with empty array + +**Critical failures** that exit immediately: +- Target path does not exist +- No scanning tools available (all bins missing) +- Concurrent scan detected (lockfile present) + +### Subprocess Execution Pattern + +All external tools run as subprocesses with structured JSON output: + +```javascript +import { spawn } from 'node:child_process'; + +// Example: npm audit execution +const proc = spawn('npm', ['audit', '--json'], { + cwd: targetPath, + stdio: ['ignore', 'pipe', 'pipe'] +}); + +// Handle non-zero exit codes gracefully +// npm audit exits 1 when vulnerabilities found (not an error!) +proc.on('close', code => { + if (code !== 0 && stderr.includes('ERR!')) { + // Actual error + reject(new Error(stderr)); + } else { + // Vulnerabilities found or success + resolve(JSON.parse(stdout)); + } +}); +``` + +## Troubleshooting + +### Common Issues + +**"Missing package-lock.json" warning** +- `npm audit` requires lockfile to run +- Run `npm install` in target directory to generate +- Scanner continues with other scan types if npm audit fails + +**"NVD API rate limit exceeded"** +- Set `CLAWSEC_NVD_API_KEY` environment variable +- Without API key: 6-second delays enforced between requests +- OSV API used as primary source (no rate limits) + +**"pip-audit not found"** +- Install: `uv pip install pip-audit` or `pip install pip-audit` +- Verify: `which pip-audit` +- Add to PATH if installed in non-standard location + +**"Semgrep binary missing"** +- Install: `pip install semgrep` OR `brew install semgrep` +- Requires Python 3.8+ runtime +- Alternative: use Docker image `returntocorp/semgrep` + +**"TypeScript hook not executable in DAST harness"** +- The DAST harness executes real hook handlers and transpiles `handler.ts` files when a TypeScript compiler is available +- Install TypeScript in the scanner environment: `npm install -D typescript` (or provide `handler.js`/`handler.mjs`) +- Without a compiler, scanner reports an `info`-level coverage finding instead of a high-severity vulnerability + +**"Concurrent scan detected"** +- Lockfile exists: `/tmp/clawsec-scanner.lock` +- Wait for running scan to complete or manually remove lockfile +- Prevents overlapping scans that could produce inconsistent results + +### Verification + +Check scanner is working correctly: + +```bash +# Verify required binaries +./scripts/runner.sh --check + +# Run unit tests +node test/dependency_scanner.test.mjs +node test/cve_integration.test.mjs +node test/sast_engine.test.mjs +node test/dast_harness.test.mjs + +# Validate skill structure +python ../../utils/validate_skill.py . + +# Scan test fixtures (should detect known vulnerabilities) +./scripts/runner.sh --target test/fixtures/ --format text +``` + +## Development + +### Running Tests + +```bash +# All tests (vanilla Node.js, no framework) +for test in test/*.test.mjs; do + node "$test" || exit 1 +done + +# Individual test suites +node test/dependency_scanner.test.mjs # Dependency scanning +node test/cve_integration.test.mjs # CVE database APIs +node test/sast_engine.test.mjs # Static analysis +node test/dast_harness.test.mjs # DAST harness execution +``` + +### Linting + +```bash +# JavaScript/TypeScript +npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --max-warnings 0 + +# Python (Bandit already configured in pyproject.toml) +ruff check . +bandit -r . -ll + +# Shell scripts +shellcheck scripts/*.sh +``` + +### Adding Custom Semgrep Rules + +Create custom rules in `.semgrep/rules/`: + +```yaml +rules: + - id: custom-security-rule + pattern: dangerous_function($ARG) + message: Avoid dangerous_function - use safe_alternative instead + severity: WARNING + languages: [javascript, typescript] +``` + +Update `scripts/sast_analyzer.mjs` to include custom rules: + +```javascript +const proc = spawn('semgrep', [ + 'scan', + '--config', 'auto', + '--config', '.semgrep/rules/', // Add custom rules + '--json', + targetPath +]); +``` + +## Integration with ClawSec Suite + +The scanner works standalone or as part of the ClawSec ecosystem: + +- **clawsec-suite**: Meta-skill that can install and manage clawsec-scanner +- **clawsec-feed**: Advisory feed for malicious skill detection (complementary) +- **openclaw-audit-watchdog**: Cron-based audit automation (similar pattern) + +Install the full ClawSec suite: + +```bash +npx clawhub@latest install clawsec-suite +# Then use clawsec-suite to discover and install clawsec-scanner +``` + +## Security Considerations + +### Scanner Security + +- No hardcoded secrets in scanner code +- API keys read from environment variables only (never logged or committed) +- Subprocess arguments use arrays to prevent shell injection +- All external tool output parsed with try/catch error handling + +### Vulnerability Prioritization + +**Critical/High severity findings** should be addressed immediately: +- Known exploits in dependencies (CVSS 9.0+) +- Hardcoded API keys or credentials in code +- Command injection vulnerabilities +- Path traversal without validation + +**Medium/Low severity findings** can be addressed in normal sprint cycles: +- Outdated dependencies without known exploits +- Missing security headers +- Weak cryptography usage + +**Info findings** are advisory only: +- Deprecated API usage +- Code quality issues flagged by linters + +## Roadmap + +### v0.0.2 (Current) +- [x] Dependency scanning (npm audit, pip-audit) +- [x] CVE database integration (OSV, NVD, GitHub Advisory) +- [x] SAST analysis (Semgrep, Bandit) +- [x] Real OpenClaw hook execution harness for DAST +- [x] Unified JSON reporting +- [x] OpenClaw hook integration + +### Future Enhancements +- [ ] Automatic remediation (dependency upgrades, code fixes) +- [ ] SARIF output format for GitHub Code Scanning integration +- [ ] Web dashboard for vulnerability tracking over time +- [ ] CI/CD GitHub Action for PR blocking on high-severity findings +- [ ] Container image scanning (Docker, OCI) +- [ ] Infrastructure-as-Code scanning (Terraform, CloudFormation) +- [ ] Comprehensive agent workflow DAST (requires deeper platform integration) + +## Contributing + +Found a security issue? Please report privately to security@prompt.security. + +For feature requests and bug reports, open an issue at: +https://github.com/prompt-security/clawsec/issues + +## License + +AGPL-3.0-or-later + +See LICENSE file in repository root for full text. + +## Resources + +- **ClawSec Homepage**: https://clawsec.prompt.security +- **Documentation**: https://clawsec.prompt.security/scanner +- **GitHub Repository**: https://github.com/prompt-security/clawsec +- **OSV API Docs**: https://osv.dev/docs/ +- **NVD API Docs**: https://nvd.nist.gov/developers/vulnerabilities +- **Semgrep Registry**: https://semgrep.dev/explore +- **Bandit Documentation**: https://bandit.readthedocs.io/ diff --git a/.agents/skills/clawsec-scanner/hooks/.gitkeep b/.agents/skills/clawsec-scanner/hooks/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.agents/skills/clawsec-scanner/hooks/clawsec-scanner-hook/HOOK.md b/.agents/skills/clawsec-scanner/hooks/clawsec-scanner-hook/HOOK.md new file mode 100644 index 00000000..9953cbc3 --- /dev/null +++ b/.agents/skills/clawsec-scanner/hooks/clawsec-scanner-hook/HOOK.md @@ -0,0 +1,74 @@ +--- +name: clawsec-scanner-hook +description: Periodic vulnerability scanning for installed skills and dependencies with configurable scan intervals. +metadata: { "openclaw": { "events": ["agent:bootstrap", "command:new"] } } +--- + +# ClawSec Scanner Hook + +This hook performs comprehensive vulnerability scanning on installed skills and their dependencies on: + +- `agent:bootstrap` +- `command:new` + +When triggered, it runs all configured scanning engines (dependency scan, SAST, DAST, CVE database lookup) and posts findings as conversation messages. Scans are rate-limited by configurable interval to avoid performance impact. + +## Scanning Capabilities + +The hook orchestrates four independent scanning engines: + +1. **Dependency Scanning**: Executes `npm audit` and `pip-audit` to detect known vulnerabilities in JavaScript and Python dependencies +2. **SAST (Static Analysis)**: Runs Semgrep (JS/TS) and Bandit (Python) to detect security issues like hardcoded secrets, command injection, and path traversal +3. **CVE Database Lookup**: Queries OSV API (primary), NVD 2.0 (optional), and GitHub Advisory Database (optional) for vulnerability enrichment +4. **DAST (Dynamic Analysis)**: Executes real OpenClaw hook handlers in an isolated harness and tests malicious-input resilience, timeout behavior, output bounds, and event mutation safety + +## Safety Contract + +- The hook does not modify or delete skills. +- It only reports findings and provides remediation guidance. +- Scanning is non-blocking and runs on a configurable interval (default 24 hours). +- Failed scans (network errors, missing tools) produce warnings but do not block execution. +- Findings are deduplicated to avoid alert fatigue. + +## Optional Environment Variables + +### Core Configuration + +- `CLAWSEC_SCANNER_INTERVAL`: Minimum interval between hook scans in seconds (default `86400` / 24 hours). +- `CLAWSEC_SCANNER_TARGET`: Override default scan target path (default: installed skills root). +- `CLAWSEC_SCANNER_STATE_FILE`: Override state file path for deduplication (default `~/.openclaw/clawsec-scanner-state.json`). +- `CLAWSEC_INSTALL_ROOT`: Override installed skills root directory. + +### CVE Database Integration + +- `CLAWSEC_NVD_API_KEY`: NVD API key for rate-limit-free access (without this, 6-second delays apply). +- `GITHUB_TOKEN`: GitHub OAuth token for GitHub Advisory Database queries (optional enhancement). + +### Selective Scanning + +- `CLAWSEC_SKIP_DEPENDENCY_SCAN`: Set to `1` to disable dependency scanning (npm audit, pip-audit). +- `CLAWSEC_SKIP_SAST`: Set to `1` to disable static analysis (Semgrep, Bandit). +- `CLAWSEC_SKIP_DAST`: Set to `1` to disable dynamic analysis (hook security tests). +- `CLAWSEC_SKIP_CVE_LOOKUP`: Set to `1` to disable CVE database enrichment. + +### Advanced Options + +- `CLAWSEC_SCANNER_TIMEOUT`: Maximum scan duration in seconds before timeout (default `300` / 5 minutes). +- `CLAWSEC_SCANNER_FORMAT`: Output format for findings (`json` or `text`, default `text`). +- `CLAWSEC_SCANNER_MIN_SEVERITY`: Minimum severity to report (`critical`, `high`, `medium`, `low`, `info`, default `medium`). +- `CLAWSEC_SCANNER_OUTPUT_FILE`: Optional path to write full scan report JSON (default: conversation only). + +## Required Binaries + +The hook requires the following binaries to be available on `PATH`: + +- `node` (20+) - JavaScript runtime +- `npm` - For npm audit execution +- `python3` (3.10+) - Python runtime +- `pip-audit` - Python dependency scanner +- `semgrep` - JavaScript/TypeScript static analysis +- `bandit` - Python static analysis +- `jq` - JSON parsing and merging +- `curl` - API requests (fallback) + +Missing binaries will be logged as warnings; available tools will still run. diff --git a/.agents/skills/clawsec-scanner/hooks/clawsec-scanner-hook/handler.ts b/.agents/skills/clawsec-scanner/hooks/clawsec-scanner-hook/handler.ts new file mode 100644 index 00000000..1ec3d960 --- /dev/null +++ b/.agents/skills/clawsec-scanner/hooks/clawsec-scanner-hook/handler.ts @@ -0,0 +1,313 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { execCommand, safeJsonParse } from "../../lib/utils.mjs"; +import { formatReportText } from "../../lib/report.mjs"; +import type { HookEvent, HookContext, ScanReport } from "../../lib/types.ts"; + +const DEFAULT_SCAN_INTERVAL_SECONDS = 86400; // 24 hours +const DEFAULT_SCANNER_TIMEOUT = 300; // 5 minutes +const DEFAULT_MIN_SEVERITY = "medium"; +let unsignedModeWarningShown = false; + +interface ScannerState { + last_hook_scan: string | null; + last_full_scan: string | null; + known_vulnerabilities: string[]; +} + +function parsePositiveInteger(value: string | undefined, fallback: number): number { + const parsed = Number.parseInt(String(value ?? ""), 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + return fallback; + } + return parsed; +} + +function toEventName(event: HookEvent): string { + const eventType = String(event.type ?? "").trim(); + const action = String(event.action ?? "").trim(); + if (!eventType || !action) return ""; + return `${eventType}:${action}`; +} + +function shouldHandleEvent(event: HookEvent): boolean { + const eventName = toEventName(event); + return eventName === "agent:bootstrap" || eventName === "command:new"; +} + +function epochMs(isoTimestamp: string | null): number { + if (!isoTimestamp) return 0; + const parsed = Date.parse(isoTimestamp); + return Number.isNaN(parsed) ? 0 : parsed; +} + +function scannedRecently(lastScan: string | null, minIntervalSeconds: number): boolean { + const sinceMs = Date.now() - epochMs(lastScan); + return sinceMs >= 0 && sinceMs < minIntervalSeconds * 1000; +} + +function configuredPath( + explicit: string | undefined, + fallback: string, + label: string, +): string { + if (!explicit) return fallback; + + const resolved = path.resolve(explicit); + try { + // Basic validation - check if path is a string + if (typeof resolved === "string" && resolved.length > 0) { + return resolved; + } + } catch (error) { + console.warn( + `[clawsec-scanner-hook] invalid ${label} path "${explicit}", using default "${fallback}": ${String(error)}`, + ); + } + + return fallback; +} + +async function loadState(stateFile: string): Promise { + try { + const content = await fs.readFile(stateFile, "utf8"); + const parsed = safeJsonParse(content, { fallback: {}, label: "scanner state" }); + const parsedState = + parsed && typeof parsed === "object" ? (parsed as Record) : {}; + + return { + last_hook_scan: + typeof parsedState.last_hook_scan === "string" ? parsedState.last_hook_scan : null, + last_full_scan: + typeof parsedState.last_full_scan === "string" ? parsedState.last_full_scan : null, + known_vulnerabilities: Array.isArray(parsedState.known_vulnerabilities) + ? parsedState.known_vulnerabilities.filter((v): v is string => typeof v === "string") + : [], + }; + } catch { + // State file doesn't exist yet - return empty state + return { + last_hook_scan: null, + last_full_scan: null, + known_vulnerabilities: [], + }; + } +} + +async function persistState(stateFile: string, state: ScannerState): Promise { + try { + const dir = path.dirname(stateFile); + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(stateFile, JSON.stringify(state, null, 2), "utf8"); + } catch (error) { + console.warn(`[clawsec-scanner-hook] failed to persist state: ${String(error)}`); + } +} + +async function runScanner( + targetPath: string, + options: { + skipDeps: boolean; + skipSast: boolean; + skipDast: boolean; + skipCve: boolean; + timeout: number; + }, +): Promise { + try { + const scriptPath = path.join(path.dirname(new URL(import.meta.url).pathname), "../../scripts/runner.sh"); + + const args = ["--target", targetPath, "--format", "json"]; + + if (options.skipDeps) args.push("--skip-deps"); + if (options.skipSast) args.push("--skip-sast"); + if (options.skipDast) args.push("--skip-dast"); + if (options.skipCve) args.push("--skip-cve"); + + const { stdout, stderr } = await execCommand("bash", [scriptPath, ...args]); + + if (stderr && !stdout) { + console.warn(`[clawsec-scanner-hook] scanner warning: ${stderr}`); + } + + const report = safeJsonParse(stdout, { fallback: null, label: "scanner report" }); + + if (!report || typeof report !== "object") { + console.warn("[clawsec-scanner-hook] scanner produced invalid report"); + return null; + } + + return report as ScanReport; + } catch (error) { + console.warn(`[clawsec-scanner-hook] scanner execution failed: ${String(error)}`); + return null; + } +} + +function shouldReportSeverity(severity: string, minSeverity: string): boolean { + const severityOrder = ["info", "low", "medium", "high", "critical"]; + const minIndex = severityOrder.indexOf(minSeverity.toLowerCase()); + const vulnIndex = severityOrder.indexOf(severity.toLowerCase()); + + if (minIndex === -1 || vulnIndex === -1) return true; + + return vulnIndex >= minIndex; +} + +function deduplicateVulnerabilities( + report: ScanReport, + knownVulnIds: string[], +): ScanReport { + const knownSet = new Set(knownVulnIds); + const newVulnerabilities = report.vulnerabilities.filter( + (vuln) => !knownSet.has(vuln.id), + ); + + // Recalculate summary for new vulnerabilities + const summary = { + critical: 0, + high: 0, + medium: 0, + low: 0, + info: 0, + }; + + for (const vuln of newVulnerabilities) { + const severity = vuln.severity; + if (severity in summary) { + summary[severity]++; + } + } + + return { + ...report, + vulnerabilities: newVulnerabilities, + summary, + }; +} + +function buildAlertMessage(report: ScanReport, format: string): string { + if (format === "json") { + return JSON.stringify(report, null, 2); + } + + return formatReportText(report); +} + +const handler = async (event: HookEvent, _context: HookContext): Promise => { + // DAST harness mode executes hook handlers directly; skip recursive scanner runs. + if (process.env.CLAWSEC_DAST_HARNESS === "1" || _context?.dastMode === true) { + return; + } + + if (!shouldHandleEvent(event)) return; + + const installRoot = configuredPath( + process.env.CLAWSEC_INSTALL_ROOT || process.env.INSTALL_ROOT, + path.join(os.homedir(), ".openclaw", "skills"), + "CLAWSEC_INSTALL_ROOT", + ); + + const targetPath = configuredPath( + process.env.CLAWSEC_SCANNER_TARGET, + installRoot, + "CLAWSEC_SCANNER_TARGET", + ); + + const stateFile = configuredPath( + process.env.CLAWSEC_SCANNER_STATE_FILE, + path.join(os.homedir(), ".openclaw", "clawsec-scanner-state.json"), + "CLAWSEC_SCANNER_STATE_FILE", + ); + + const scanIntervalSeconds = parsePositiveInteger( + process.env.CLAWSEC_SCANNER_INTERVAL, + DEFAULT_SCAN_INTERVAL_SECONDS, + ); + + const scanTimeout = parsePositiveInteger( + process.env.CLAWSEC_SCANNER_TIMEOUT, + DEFAULT_SCANNER_TIMEOUT, + ); + + const minSeverity = process.env.CLAWSEC_SCANNER_MIN_SEVERITY || DEFAULT_MIN_SEVERITY; + const outputFormat = process.env.CLAWSEC_SCANNER_FORMAT || "text"; + const allowUnsigned = process.env.CLAWSEC_ALLOW_UNSIGNED_FEED === "1"; + + const skipDeps = process.env.CLAWSEC_SKIP_DEPENDENCY_SCAN === "1"; + const skipSast = process.env.CLAWSEC_SKIP_SAST === "1"; + const skipDast = process.env.CLAWSEC_SKIP_DAST === "1"; + const skipCve = process.env.CLAWSEC_SKIP_CVE_LOOKUP === "1"; + + if (allowUnsigned && !unsignedModeWarningShown) { + unsignedModeWarningShown = true; + console.warn( + "[clawsec-scanner-hook] CLAWSEC_ALLOW_UNSIGNED_FEED=1 is enabled. " + + "This bypass is for development only.", + ); + } + + const forceScan = toEventName(event) === "command:new"; + const state = await loadState(stateFile); + + if (!forceScan && scannedRecently(state.last_hook_scan, scanIntervalSeconds)) { + return; + } + + const report = await runScanner(targetPath, { + skipDeps, + skipSast, + skipDast, + skipCve, + timeout: scanTimeout, + }); + + const nowIso = new Date().toISOString(); + state.last_hook_scan = nowIso; + state.last_full_scan = nowIso; + + if (!report) { + await persistState(stateFile, state); + return; + } + + // Filter by minimum severity + const filteredVulns = report.vulnerabilities.filter((vuln) => + shouldReportSeverity(vuln.severity, minSeverity), + ); + + // Deduplicate against known vulnerabilities + const dedupedReport = deduplicateVulnerabilities( + { ...report, vulnerabilities: filteredVulns }, + state.known_vulnerabilities, + ); + + // Update known vulnerabilities list + const allVulnIds = report.vulnerabilities.map((v) => v.id).filter((id) => id.trim() !== ""); + state.known_vulnerabilities = Array.from(new Set([...state.known_vulnerabilities, ...allVulnIds])); + + await persistState(stateFile, state); + + // Write optional output file + const outputFile = process.env.CLAWSEC_SCANNER_OUTPUT_FILE; + if (outputFile) { + try { + await fs.writeFile(outputFile, JSON.stringify(report, null, 2), "utf8"); + } catch (error) { + console.warn(`[clawsec-scanner-hook] failed to write output file: ${String(error)}`); + } + } + + // Post findings to conversation if any new vulnerabilities + if (dedupedReport.vulnerabilities.length > 0) { + const alertMessage = buildAlertMessage(dedupedReport, outputFormat); + + event.messages?.push({ + role: "system", + content: `🔍 ClawSec Scanner detected ${dedupedReport.vulnerabilities.length} new vulnerabilities:\n\n${alertMessage}`, + }); + } +}; + +export default handler; diff --git a/.agents/skills/clawsec-scanner/lib/.gitkeep b/.agents/skills/clawsec-scanner/lib/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.agents/skills/clawsec-scanner/lib/report.mjs b/.agents/skills/clawsec-scanner/lib/report.mjs new file mode 100644 index 00000000..c4b21811 --- /dev/null +++ b/.agents/skills/clawsec-scanner/lib/report.mjs @@ -0,0 +1,251 @@ +import { generateUuid, getTimestamp } from "./utils.mjs"; + +/** + * @typedef {import('./types.ts').Vulnerability} Vulnerability + * @typedef {import('./types.ts').ScanReport} ScanReport + * @typedef {import('./types.ts').SeverityLevel} SeverityLevel + */ + +/** + * Generate a unified vulnerability report from scan results. + * + * @param {Vulnerability[]} vulnerabilities - Array of detected vulnerabilities + * @param {string} target - Target path that was scanned + * @returns {ScanReport} + */ +export function generateReport(vulnerabilities, target = ".") { + const summary = { + critical: 0, + high: 0, + medium: 0, + low: 0, + info: 0, + }; + + // Count vulnerabilities by severity + for (const vuln of vulnerabilities) { + const severity = vuln.severity; + if (severity in summary) { + summary[severity]++; + } + } + + return { + scan_id: generateUuid(), + timestamp: getTimestamp(), + target, + vulnerabilities, + summary, + }; +} + +/** + * Format a scan report as JSON string. + * + * @param {ScanReport} report - Scan report to format + * @param {boolean} pretty - Whether to pretty-print JSON + * @returns {string} + */ +export function formatReportJson(report, pretty = true) { + return pretty ? JSON.stringify(report, null, 2) : JSON.stringify(report); +} + +/** + * Format a scan report as human-readable text. + * + * @param {ScanReport} report - Scan report to format + * @returns {string} + */ +export function formatReportText(report) { + const lines = []; + + // Header + lines.push("═══════════════════════════════════════════════════════════════"); + lines.push(" VULNERABILITY SCAN REPORT"); + lines.push("═══════════════════════════════════════════════════════════════"); + lines.push(""); + lines.push(`Scan ID: ${report.scan_id}`); + lines.push(`Timestamp: ${report.timestamp}`); + lines.push(`Target: ${report.target}`); + lines.push(""); + + // Summary + lines.push("───────────────────────────────────────────────────────────────"); + lines.push("SUMMARY"); + lines.push("───────────────────────────────────────────────────────────────"); + lines.push(""); + + const total = report.vulnerabilities.length; + const { critical, high, medium, low, info } = report.summary; + + lines.push(`Total Vulnerabilities: ${total}`); + lines.push(""); + + if (critical > 0) { + lines.push(` 🔴 Critical: ${critical}`); + } + if (high > 0) { + lines.push(` 🟠 High: ${high}`); + } + if (medium > 0) { + lines.push(` 🟡 Medium: ${medium}`); + } + if (low > 0) { + lines.push(` 🔵 Low: ${low}`); + } + if (info > 0) { + lines.push(` ⚪ Info: ${info}`); + } + + if (total === 0) { + lines.push(" ✓ No vulnerabilities detected"); + } + + lines.push(""); + + // Detailed findings + if (report.vulnerabilities.length > 0) { + lines.push("───────────────────────────────────────────────────────────────"); + lines.push("DETAILED FINDINGS"); + lines.push("───────────────────────────────────────────────────────────────"); + lines.push(""); + + // Group vulnerabilities by severity + const bySeverity = { + critical: [], + high: [], + medium: [], + low: [], + info: [], + }; + + for (const vuln of report.vulnerabilities) { + bySeverity[vuln.severity].push(vuln); + } + + // Display in order: critical -> high -> medium -> low -> info + const severityOrder = ["critical", "high", "medium", "low", "info"]; + + for (const severity of severityOrder) { + const vulns = bySeverity[severity]; + if (vulns.length === 0) continue; + + const severityIcon = getSeverityIcon(severity); + lines.push(`${severityIcon} ${severity.toUpperCase()}`); + lines.push(""); + + for (const vuln of vulns) { + lines.push(` ID: ${vuln.id}`); + lines.push(` Package: ${vuln.package} @ ${vuln.version}`); + if (vuln.fixed_version) { + lines.push(` Fix: ${vuln.fixed_version}`); + } + lines.push(` Source: ${vuln.source}`); + lines.push(` Title: ${vuln.title}`); + + // Wrap description at 60 chars + const descLines = wrapText(vuln.description, 60); + lines.push(" Description:"); + for (const line of descLines) { + lines.push(` ${line}`); + } + + if (vuln.references.length > 0) { + lines.push(" References:"); + for (const ref of vuln.references.slice(0, 3)) { + lines.push(` - ${ref}`); + } + if (vuln.references.length > 3) { + lines.push(` ... and ${vuln.references.length - 3} more`); + } + } + + lines.push(""); + } + } + } + + // Recommendations + lines.push("───────────────────────────────────────────────────────────────"); + lines.push("RECOMMENDATIONS"); + lines.push("───────────────────────────────────────────────────────────────"); + lines.push(""); + + if (critical > 0 || high > 0) { + lines.push("⚠️ URGENT: Critical or high severity vulnerabilities detected!"); + lines.push(""); + lines.push("Recommended actions:"); + lines.push(" 1. Review all critical and high severity findings immediately"); + lines.push(" 2. Update vulnerable dependencies to fixed versions"); + lines.push(" 3. Run scanner again to verify remediation"); + lines.push(""); + } else if (medium > 0) { + lines.push("⚠️ Medium severity vulnerabilities detected."); + lines.push(""); + lines.push("Recommended actions:"); + lines.push(" 1. Review findings and assess impact on your use case"); + lines.push(" 2. Plan updates during next maintenance window"); + lines.push(""); + } else if (low > 0 || info > 0) { + lines.push("✓ No critical or high severity vulnerabilities detected."); + lines.push(""); + lines.push("Recommended actions:"); + lines.push(" 1. Review low/info findings for awareness"); + lines.push(" 2. Consider updates when convenient"); + lines.push(""); + } else { + lines.push("✓ No vulnerabilities detected. Your code is clean!"); + lines.push(""); + } + + lines.push("═══════════════════════════════════════════════════════════════"); + + return lines.join("\n"); +} + +/** + * Get emoji icon for severity level. + * + * @param {SeverityLevel} severity - Severity level + * @returns {string} + */ +function getSeverityIcon(severity) { + const icons = { + critical: "🔴", + high: "🟠", + medium: "🟡", + low: "🔵", + info: "⚪", + }; + return icons[severity] || "⚪"; +} + +/** + * Wrap text to specified width. + * + * @param {string} text - Text to wrap + * @param {number} width - Maximum line width + * @returns {string[]} + */ +function wrapText(text, width) { + const words = text.split(/\s+/); + const lines = []; + let currentLine = ""; + + for (const word of words) { + if (currentLine.length + word.length + 1 <= width) { + currentLine += (currentLine ? " " : "") + word; + } else { + if (currentLine) { + lines.push(currentLine); + } + currentLine = word; + } + } + + if (currentLine) { + lines.push(currentLine); + } + + return lines.length > 0 ? lines : [""]; +} diff --git a/.agents/skills/clawsec-scanner/lib/types.ts b/.agents/skills/clawsec-scanner/lib/types.ts new file mode 100644 index 00000000..eab6cec4 --- /dev/null +++ b/.agents/skills/clawsec-scanner/lib/types.ts @@ -0,0 +1,45 @@ +export type VulnerabilitySource = 'npm-audit' | 'pip-audit' | 'osv' | 'nvd' | 'github' | 'sast' | 'dast'; + +export type SeverityLevel = 'critical' | 'high' | 'medium' | 'low' | 'info'; + +export interface Vulnerability { + id: string; + source: VulnerabilitySource; + severity: SeverityLevel; + package: string; + version: string; + fixed_version?: string; + title: string; + description: string; + references: string[]; + discovered_at: string; +} + +export interface ScanReport { + scan_id: string; + timestamp: string; + target: string; + vulnerabilities: Vulnerability[]; + summary: { + critical: number; + high: number; + medium: number; + low: number; + info: number; + }; +} + +export type HookEvent = { + type?: string; + action?: string; + messages?: Array<{ + role: string; + content: string; + }>; +}; + +export type HookContext = { + skillPath?: string; + agentPlatform?: string; + [key: string]: unknown; +}; diff --git a/.agents/skills/clawsec-scanner/lib/utils.mjs b/.agents/skills/clawsec-scanner/lib/utils.mjs new file mode 100644 index 00000000..f274d7e9 --- /dev/null +++ b/.agents/skills/clawsec-scanner/lib/utils.mjs @@ -0,0 +1,139 @@ +import { spawn } from "node:child_process"; + +/** + * @param {unknown} value + * @returns {value is Record} + */ +export function isObject(value) { + return typeof value === "object" && value !== null; +} + +/** + * Execute a command as a subprocess and return its output. + * + * NOTE: npm audit exits non-zero when vulnerabilities are found. + * Check stderr for actual errors vs. normal vulnerability reports. + * + * @param {string} cmd - Command to execute + * @param {string[]} args - Command arguments + * @param {{env?: Record, cwd?: string}} [options] - Execution options + * @returns {Promise<{code: number, stdout: string, stderr: string}>} + */ +export function execCommand(cmd, args, options = {}) { + return new Promise((resolve, reject) => { + const proc = spawn(cmd, args, { + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, ...options.env }, + cwd: options.cwd, + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (d) => { + stdout += d; + }); + proc.stderr.on("data", (d) => { + stderr += d; + }); + + proc.on("close", (code) => { + // npm audit and other security tools exit non-zero when vulnerabilities found + // Check stderr for actual errors (ERR! pattern) vs. normal findings + if (code !== 0 && stderr.includes("ERR!")) { + reject(new Error(stderr)); + } else { + resolve({ code, stdout, stderr }); + } + }); + + proc.on("error", (error) => { + reject(error); + }); + }); +} + +/** + * Safely parse JSON string with error handling. + * + * @param {string} jsonString - JSON string to parse + * @param {{fallback?: unknown, label?: string}} [options] - Parse options + * @returns {unknown} + */ +export function safeJsonParse(jsonString, { fallback = null, label = "JSON" } = {}) { + const raw = String(jsonString ?? "").trim(); + if (!raw) return fallback; + + try { + return JSON.parse(raw); + } catch (error) { + if (error instanceof Error) { + console.warn(`Failed to parse ${label}: ${error.message}`); + } + return fallback; + } +} + +/** + * Normalize severity levels from different security tools to standard levels. + * + * @param {string} severity - Severity string from security tool + * @returns {'critical' | 'high' | 'medium' | 'low' | 'info'} + */ +export function normalizeSeverity(severity) { + const normalized = String(severity ?? "") + .trim() + .toLowerCase(); + + if (normalized.includes("critical")) return "critical"; + if (normalized.includes("high")) return "high"; + if (normalized.includes("moderate") || normalized.includes("medium")) return "medium"; + if (normalized.includes("low")) return "low"; + + return "info"; +} + +/** + * @param {string[]} values + * @returns {string[]} + */ +export function uniqueStrings(values) { + return Array.from(new Set(values)); +} + +/** + * Generate a simple UUID v4. + * + * @returns {string} + */ +export function generateUuid() { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +/** + * Get current ISO 8601 timestamp. + * + * @returns {string} + */ +export function getTimestamp() { + return new Date().toISOString(); +} + +/** + * Check if a command exists in PATH. + * + * @param {string} command - Command name to check + * @returns {Promise} + */ +export async function commandExists(command) { + try { + const { code } = await execCommand("which", [command]); + return code === 0; + } catch { + return false; + } +} diff --git a/.agents/skills/clawsec-scanner/scripts/.gitkeep b/.agents/skills/clawsec-scanner/scripts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.agents/skills/clawsec-scanner/scripts/dast_hook_executor.mjs b/.agents/skills/clawsec-scanner/scripts/dast_hook_executor.mjs new file mode 100644 index 00000000..27d33728 --- /dev/null +++ b/.agents/skills/clawsec-scanner/scripts/dast_hook_executor.mjs @@ -0,0 +1,273 @@ +#!/usr/bin/env node + +import fs from "node:fs/promises"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { pathToFileURL } from "node:url"; + +function parseArgs(argv) { + const parsed = { + handler: "", + exportName: "default", + eventB64: "", + contextB64: "", + }; + + for (let i = 0; i < argv.length; i += 1) { + const token = argv[i]; + + if (token === "--handler") { + parsed.handler = String(argv[i + 1] ?? "").trim(); + i += 1; + continue; + } + + if (token === "--export") { + parsed.exportName = String(argv[i + 1] ?? "default").trim() || "default"; + i += 1; + continue; + } + + if (token === "--event") { + parsed.eventB64 = String(argv[i + 1] ?? "").trim(); + i += 1; + continue; + } + + if (token === "--context") { + parsed.contextB64 = String(argv[i + 1] ?? "").trim(); + i += 1; + continue; + } + + throw new Error(`Unknown argument: ${token}`); + } + + if (!parsed.handler) { + throw new Error("Missing required --handler"); + } + + if (!parsed.eventB64) { + throw new Error("Missing required --event"); + } + + if (!parsed.contextB64) { + throw new Error("Missing required --context"); + } + + return parsed; +} + +function decodeBase64Json(value, label) { + try { + const decoded = Buffer.from(value, "base64").toString("utf8"); + return JSON.parse(decoded); + } catch (error) { + throw new Error(`Failed to decode ${label}: ${error instanceof Error ? error.message : String(error)}`); + } +} + +async function fileExists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +async function loadTypeScriptCompiler() { + if (process.env.CLAWSEC_DAST_DISABLE_TYPESCRIPT === "1") { + return null; + } + + try { + const imported = await import("typescript"); + return imported.default || imported; + } catch { + // Ignore and try require path next. + } + + try { + const req = createRequire(import.meta.url); + return req("typescript"); + } catch { + return null; + } +} + +async function importTypeScriptModule(tsPath) { + const tsCompiler = await loadTypeScriptCompiler(); + if (!tsCompiler || typeof tsCompiler.transpileModule !== "function") { + throw new Error( + `Cannot execute TypeScript hook (${tsPath}): typescript compiler not available. ` + + "Install 'typescript' or provide a JavaScript handler file.", + ); + } + + const source = await fs.readFile(tsPath, "utf8"); + const transpiled = tsCompiler.transpileModule(source, { + compilerOptions: { + module: tsCompiler.ModuleKind.ESNext, + target: tsCompiler.ScriptTarget.ES2022, + moduleResolution: tsCompiler.ModuleResolutionKind.NodeNext, + esModuleInterop: true, + sourceMap: false, + inlineSourceMap: false, + declaration: false, + }, + fileName: tsPath, + reportDiagnostics: false, + }); + + const tempFile = path.join( + path.dirname(tsPath), + `.clawsec-dast-${path.basename(tsPath, ".ts")}-${process.pid}-${Date.now()}.mjs`, + ); + + await fs.writeFile(tempFile, transpiled.outputText, "utf8"); + + try { + return await import(`${pathToFileURL(tempFile).href}?ts=${Date.now()}`); + } finally { + try { + await fs.unlink(tempFile); + } catch { + // best-effort cleanup + } + } +} + +async function loadHookModule(handlerPath) { + const fullPath = path.resolve(handlerPath); + const exists = await fileExists(fullPath); + if (!exists) { + throw new Error(`Hook handler does not exist: ${fullPath}`); + } + + const ext = path.extname(fullPath).toLowerCase(); + + if (ext === ".ts") { + return importTypeScriptModule(fullPath); + } + + return import(`${pathToFileURL(fullPath).href}?v=${Date.now()}`); +} + +function resolveHandlerExport(mod, exportName) { + if (exportName && exportName !== "default") { + if (typeof mod?.[exportName] === "function") { + return mod[exportName]; + } + throw new Error(`Hook export '${exportName}' is not a function`); + } + + if (typeof mod?.default === "function") { + return mod.default; + } + + if (typeof mod?.handler === "function") { + return mod.handler; + } + + throw new Error("Hook module does not export a handler function"); +} + +function normalizeTimestamp(event) { + const timestamp = event?.timestamp; + if (typeof timestamp === "string" || typeof timestamp === "number") { + const parsed = new Date(timestamp); + if (!Number.isNaN(parsed.getTime())) { + event.timestamp = parsed; + } + } +} + +function summarizeMessages(messages) { + if (!Array.isArray(messages)) { + return { + count: 0, + charCount: 0, + }; + } + + let charCount = 0; + + for (const message of messages) { + if (typeof message === "string") { + charCount += message.length; + continue; + } + + try { + charCount += JSON.stringify(message).length; + } catch { + charCount += 0; + } + } + + return { + count: messages.length, + charCount, + }; +} + +function coreEventShape(event) { + return { + type: event?.type ?? null, + action: event?.action ?? null, + sessionKey: event?.sessionKey ?? null, + }; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const event = decodeBase64Json(args.eventB64, "event payload"); + const context = decodeBase64Json(args.contextB64, "context payload"); + + normalizeTimestamp(event); + + const startedAt = Date.now(); + const before = coreEventShape(event); + + try { + const mod = await loadHookModule(args.handler); + const handler = resolveHandlerExport(mod, args.exportName); + + await handler(event, context); + + const after = coreEventShape(event); + const messageSummary = summarizeMessages(event?.messages); + + const payload = { + ok: true, + duration_ms: Date.now() - startedAt, + core_before: before, + core_after: after, + messages_count: messageSummary.count, + messages_char_count: messageSummary.charCount, + }; + + process.stdout.write(JSON.stringify(payload)); + } catch (error) { + const after = coreEventShape(event); + const messageSummary = summarizeMessages(event?.messages); + + const payload = { + ok: false, + duration_ms: Date.now() - startedAt, + core_before: before, + core_after: after, + messages_count: messageSummary.count, + messages_char_count: messageSummary.charCount, + error: error instanceof Error ? error.message : String(error), + }; + + process.stdout.write(JSON.stringify(payload)); + } +} + +main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`); + process.exit(1); +}); diff --git a/.agents/skills/clawsec-scanner/scripts/dast_runner.mjs b/.agents/skills/clawsec-scanner/scripts/dast_runner.mjs new file mode 100755 index 00000000..cd1d49f3 --- /dev/null +++ b/.agents/skills/clawsec-scanner/scripts/dast_runner.mjs @@ -0,0 +1,785 @@ +#!/usr/bin/env node + +/** + * DAST (Dynamic Application Security Testing) Runner for ClawSec Scanner. + * + * Scope: + * - Discover OpenClaw hooks from target directories + * - Execute real hook handlers in an isolated harness process + * - Validate malicious-input resilience, timeout behavior, output bounds, + * and event mutation safety + */ + +import fs from "node:fs/promises"; +import path from "node:path"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +import { generateReport, formatReportJson, formatReportText } from "../lib/report.mjs"; +import { getTimestamp } from "../lib/utils.mjs"; + +/** + * @typedef {import('../lib/types.ts').Vulnerability} Vulnerability + * @typedef {import('../lib/types.ts').ScanReport} ScanReport + */ + +const DEFAULT_TIMEOUT_MS = 30000; +const MAX_OUTPUT_MESSAGES = 25; +const MAX_OUTPUT_CHARS = 20000; +const SKIP_DIR_NAMES = new Set([ + ".git", + ".github", + ".idea", + ".vscode", + "node_modules", + "dist", + "build", + "coverage", + ".openclaw", +]); + +const MALICIOUS_PAYLOADS = [ + "", + "'; DROP TABLE users; --", + "; rm -rf /", + "$(whoami)", + "..\\..\\..\\windows\\system32\\config\\sam", + "../../../etc/passwd", + "test\0malicious", + "A".repeat(200000), +]; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const HOOK_EXECUTOR_PATH = path.join(__dirname, "dast_hook_executor.mjs"); + +/** + * @typedef {Object} HookDescriptor + * @property {string} name + * @property {string} hookDir + * @property {string} hookFile + * @property {string} handlerPath + * @property {string[]} events + * @property {string} exportName + */ + +/** + * Parse CLI arguments. + * + * @param {string[]} argv + * @returns {{target: string, format: 'json' | 'text', timeout: number}} + */ +function parseArgs(argv) { + const parsed = { + target: ".", + format: "json", + timeout: DEFAULT_TIMEOUT_MS, + }; + + for (let i = 0; i < argv.length; i += 1) { + const token = argv[i]; + + if (token === "--target") { + parsed.target = String(argv[i + 1] ?? "").trim(); + i += 1; + continue; + } + + if (token === "--format") { + const value = String(argv[i + 1] ?? "json").trim(); + if (value !== "json" && value !== "text") { + throw new Error("Invalid --format value. Use 'json' or 'text'."); + } + parsed.format = value; + i += 1; + continue; + } + + if (token === "--timeout") { + const value = Number.parseInt(String(argv[i + 1] ?? ""), 10); + if (!Number.isFinite(value) || value <= 0) { + throw new Error("Invalid --timeout value. Must be a positive integer (milliseconds)."); + } + parsed.timeout = value; + i += 1; + continue; + } + + if (token === "--help" || token === "-h") { + printUsage(); + process.exit(0); + } + + throw new Error(`Unknown argument: ${token}`); + } + + if (!parsed.target) { + throw new Error("Missing required argument: --target"); + } + + return parsed; +} + +function printUsage() { + process.stderr.write( + [ + "Usage:", + " node scripts/dast_runner.mjs --target [--format json|text] [--timeout ms]", + "", + "Examples:", + " node scripts/dast_runner.mjs --target ./skills/", + " node scripts/dast_runner.mjs --target ./skills/ --format text", + " node scripts/dast_runner.mjs --target ./skills/ --timeout 60000", + "", + "Flags:", + " --target Target skill/hook directory to test (required)", + " --format Output format: json or text (default: json)", + ` --timeout Per-hook invocation timeout in milliseconds (default: ${DEFAULT_TIMEOUT_MS})`, + "", + ].join("\n"), + ); +} + +/** + * @param {string} filePath + * @returns {Promise} + */ +async function fileExists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +/** + * @param {string} markdown + * @returns {string} + */ +function extractFrontmatter(markdown) { + const match = markdown.match(/^---\n([\s\S]*?)\n---/); + return match ? match[1] : ""; +} + +/** + * @param {string} frontmatter + * @returns {string[]} + */ +function parseEvents(frontmatter) { + const defaultEvents = ["command:new"]; + if (!frontmatter) return defaultEvents; + + const jsonStyle = frontmatter.match(/"events"\s*:\s*\[([^\]]*)\]/m); + const yamlStyle = frontmatter.match(/events\s*:\s*\[([^\]]*)\]/m); + const raw = jsonStyle?.[1] ?? yamlStyle?.[1]; + + if (!raw) return defaultEvents; + + const events = []; + const quotedRegex = /"([^"]+)"|'([^']+)'/g; + + let quotedMatch = quotedRegex.exec(raw); + while (quotedMatch) { + const value = quotedMatch[1] || quotedMatch[2]; + if (value && value.includes(":")) { + events.push(value.trim()); + } + quotedMatch = quotedRegex.exec(raw); + } + + if (events.length === 0) { + const fallback = raw + .split(",") + .map((part) => part.trim()) + .map((part) => part.replace(/^['"]|['"]$/g, "")) + .filter((part) => part.includes(":")); + events.push(...fallback); + } + + return events.length > 0 ? Array.from(new Set(events)) : defaultEvents; +} + +/** + * @param {string} frontmatter + * @param {string} fallback + * @returns {string} + */ +function parseHookName(frontmatter, fallback) { + if (!frontmatter) return fallback; + + const match = frontmatter.match(/^name\s*:\s*(.+)$/m); + if (!match) return fallback; + + return match[1].trim().replace(/^['"]|['"]$/g, "") || fallback; +} + +/** + * @param {string} frontmatter + * @returns {string} + */ +function parseExportName(frontmatter) { + if (!frontmatter) return "default"; + + const jsonStyle = frontmatter.match(/"export"\s*:\s*"([^"]+)"/m); + if (jsonStyle?.[1]) return jsonStyle[1].trim(); + + const yamlStyle = frontmatter.match(/^export\s*:\s*(.+)$/m); + if (yamlStyle?.[1]) { + const value = yamlStyle[1].trim().replace(/^['"]|['"]$/g, ""); + return value || "default"; + } + + return "default"; +} + +/** + * @param {string} hookDir + * @returns {Promise} + */ +async function resolveHandlerPath(hookDir) { + const candidates = [ + "handler.mjs", + "handler.js", + "handler.cjs", + "handler.ts", + "index.mjs", + "index.js", + "index.cjs", + "index.ts", + ]; + + for (const candidate of candidates) { + const fullPath = path.join(hookDir, candidate); + if (await fileExists(fullPath)) { + return fullPath; + } + } + + return null; +} + +/** + * @param {string} targetPath + * @returns {Promise} + */ +export async function discoverHooks(targetPath) { + const hooks = []; + const absoluteTarget = path.resolve(targetPath); + + /** + * @param {string} dir + * @returns {Promise} + */ + async function walk(dir) { + let entries; + try { + entries = await fs.readdir(dir, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + if (SKIP_DIR_NAMES.has(entry.name)) { + continue; + } + + await walk(fullPath); + continue; + } + + if (!entry.isFile() || entry.name !== "HOOK.md") { + continue; + } + + const hookDir = path.dirname(fullPath); + const hookMd = await fs.readFile(fullPath, "utf8"); + const frontmatter = extractFrontmatter(hookMd); + const handlerPath = await resolveHandlerPath(hookDir); + + if (!handlerPath) { + continue; + } + + hooks.push({ + name: parseHookName(frontmatter, path.basename(hookDir)), + hookDir, + hookFile: fullPath, + handlerPath, + events: parseEvents(frontmatter), + exportName: parseExportName(frontmatter), + }); + } + } + + await walk(absoluteTarget); + + return hooks; +} + +/** + * @param {string} eventKey + * @returns {{type: string, action: string}} + */ +function splitEventKey(eventKey) { + const parts = String(eventKey ?? "").split(":"); + const type = parts.shift() || "command"; + const action = parts.join(":") || "new"; + return { type, action }; +} + +/** + * @param {string} eventKey + * @param {string} payload + * @param {string} targetPath + * @returns {Record} + */ +export function buildEvent(eventKey, payload, targetPath) { + const { type, action } = splitEventKey(eventKey); + + return { + type, + action, + sessionKey: "clawsec-dast-session", + timestamp: new Date().toISOString(), + messages: [], + context: { + content: payload, + transcript: payload, + workspaceDir: path.resolve(targetPath), + channelId: "dast-harness", + commandSource: "dast", + bootstrapFiles: [], + }, + }; +} + +/** + * @typedef {Object} HarnessInvocationResult + * @property {boolean} timedOut + * @property {number} exitCode + * @property {string} stderr + * @property {Record | null} parsed + * @property {string | null} parseError + */ + +/** + * @param {HookDescriptor} hook + * @param {Record} event + * @param {Record} context + * @param {number} timeoutMs + * @returns {Promise} + */ +async function invokeHookHarness(hook, event, context, timeoutMs) { + const encodedEvent = Buffer.from(JSON.stringify(event), "utf8").toString("base64"); + const encodedContext = Buffer.from(JSON.stringify(context), "utf8").toString("base64"); + + const args = [ + HOOK_EXECUTOR_PATH, + "--handler", + hook.handlerPath, + "--export", + hook.exportName || "default", + "--event", + encodedEvent, + "--context", + encodedContext, + ]; + + return new Promise((resolve) => { + const proc = spawn("node", args, { + stdio: ["ignore", "pipe", "pipe"], + env: { + ...process.env, + CLAWSEC_DAST_HARNESS: "1", + }, + }); + + let stdout = ""; + let stderr = ""; + let timedOut = false; + + const timer = setTimeout(() => { + timedOut = true; + proc.kill("SIGKILL"); + }, timeoutMs); + + proc.stdout.on("data", (chunk) => { + stdout += String(chunk); + }); + + proc.stderr.on("data", (chunk) => { + stderr += String(chunk); + }); + + proc.on("close", (code) => { + clearTimeout(timer); + + const raw = stdout.trim(); + if (!raw) { + resolve({ + timedOut, + exitCode: code ?? 1, + stderr, + parsed: null, + parseError: raw ? null : "Harness produced no JSON output", + }); + return; + } + + try { + const parsed = JSON.parse(raw); + resolve({ + timedOut, + exitCode: code ?? 1, + stderr, + parsed, + parseError: null, + }); + } catch (error) { + resolve({ + timedOut, + exitCode: code ?? 1, + stderr, + parsed: null, + parseError: error instanceof Error ? error.message : String(error), + }); + } + }); + }); +} + +/** + * @param {unknown} value + * @returns {value is Record} + */ +function isObject(value) { + return typeof value === "object" && value !== null; +} + +/** + * @param {unknown} parsed + * @returns {{ok: boolean, error: string, messagesCount: number, messagesCharCount: number, coreAfter: Record}} + */ +function normalizeHarnessPayload(parsed) { + if (!isObject(parsed)) { + return { + ok: false, + error: "Harness output is not an object", + messagesCount: 0, + messagesCharCount: 0, + coreAfter: {}, + }; + } + + const ok = parsed.ok === true; + const error = typeof parsed.error === "string" ? parsed.error : ""; + const messagesCount = Number(parsed.messages_count ?? 0) || 0; + const messagesCharCount = Number(parsed.messages_char_count ?? 0) || 0; + const coreAfter = isObject(parsed.core_after) ? parsed.core_after : {}; + + return { + ok, + error, + messagesCount, + messagesCharCount, + coreAfter, + }; +} + +/** + * @param {string} input + * @returns {string} + */ +function slug(input) { + return String(input) + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 60); +} + +/** + * @param {string} reason + * @returns {boolean} + */ +function isHarnessCapabilityError(reason) { + const normalized = String(reason ?? "").toLowerCase(); + return ( + normalized.includes("typescript compiler not available") + || normalized.includes("does not export a handler function") + || normalized.includes("is not a function") + ); +} + +/** + * @param {Vulnerability[]} bucket + * @param {string} id + * @param {'critical' | 'high' | 'medium' | 'low' | 'info'} severity + * @param {HookDescriptor} hook + * @param {string} eventKey + * @param {string} title + * @param {string} description + */ +function pushHookVulnerability(bucket, id, severity, hook, eventKey, title, description) { + bucket.push({ + id, + source: "dast", + severity, + package: hook.name, + version: `${eventKey}:${path.basename(hook.handlerPath)}`, + fixed_version: "", + title, + description, + references: [hook.hookFile], + discovered_at: getTimestamp(), + }); +} + +/** + * @param {HookDescriptor} hook + * @param {string} targetPath + * @param {number} timeoutMs + * @returns {Promise} + */ +async function evaluateHook(hook, targetPath, timeoutMs) { + const findings = []; + const invocationTimeoutMs = Math.max(1000, timeoutMs); + + for (const eventKey of hook.events) { + const safeEvent = buildEvent(eventKey, "safe baseline input", targetPath); + const safeContext = { + skillPath: hook.hookDir, + agentPlatform: "openclaw", + dastMode: true, + targetPath: path.resolve(targetPath), + event: eventKey, + }; + + const safeResult = await invokeHookHarness(hook, safeEvent, safeContext, invocationTimeoutMs); + + if (safeResult.timedOut) { + pushHookVulnerability( + findings, + `DAST-TIMEOUT-${slug(`${hook.name}-${eventKey}`)}`, + "high", + hook, + eventKey, + "Hook times out under baseline input", + `Hook execution exceeded ${invocationTimeoutMs}ms for event '${eventKey}' under safe baseline input.`, + ); + continue; + } + + if (safeResult.parseError) { + pushHookVulnerability( + findings, + `DAST-HARNESS-${slug(`${hook.name}-${eventKey}`)}`, + "medium", + hook, + eventKey, + "Hook harness output invalid", + `Could not parse harness output for event '${eventKey}': ${safeResult.parseError}. stderr: ${safeResult.stderr || "(empty)"}`, + ); + continue; + } + + const normalizedSafe = normalizeHarnessPayload(safeResult.parsed); + if (!normalizedSafe.ok) { + const reason = normalizedSafe.error || safeResult.stderr || "unknown error"; + + if (isHarnessCapabilityError(reason)) { + pushHookVulnerability( + findings, + `DAST-COVERAGE-${slug(`${hook.name}-${eventKey}`)}`, + "info", + hook, + eventKey, + "Hook not executable in local DAST harness", + `DAST harness could not execute hook for event '${eventKey}' due to runtime capability limits: ${reason}`, + ); + } else { + pushHookVulnerability( + findings, + `DAST-CRASH-${slug(`${hook.name}-${eventKey}`)}`, + "high", + hook, + eventKey, + "Hook throws on baseline input", + `Hook execution failed for event '${eventKey}' under safe baseline input: ${reason}`, + ); + } + continue; + } + + const mutationObserved = + normalizedSafe.coreAfter.type !== safeEvent.type || + normalizedSafe.coreAfter.action !== safeEvent.action || + normalizedSafe.coreAfter.sessionKey !== safeEvent.sessionKey; + + if (mutationObserved) { + pushHookVulnerability( + findings, + `DAST-MUTATION-${slug(`${hook.name}-${eventKey}`)}`, + "low", + hook, + eventKey, + "Hook mutates core event identity fields", + `Hook changed one or more of type/action/sessionKey for event '${eventKey}'. This can cause routing side effects in OpenClaw hooks.`, + ); + } + + if ( + normalizedSafe.messagesCount > MAX_OUTPUT_MESSAGES || + normalizedSafe.messagesCharCount > MAX_OUTPUT_CHARS + ) { + pushHookVulnerability( + findings, + `DAST-OUTPUT-${slug(`${hook.name}-${eventKey}`)}`, + "medium", + hook, + eventKey, + "Hook output exceeds safe bounds", + `Hook generated ${normalizedSafe.messagesCount} messages and ${normalizedSafe.messagesCharCount} chars for baseline input. Limits: ${MAX_OUTPUT_MESSAGES} messages / ${MAX_OUTPUT_CHARS} chars.`, + ); + } + + const maliciousFailures = []; + const maliciousTimeouts = []; + + for (const payload of MALICIOUS_PAYLOADS) { + const event = buildEvent(eventKey, payload, targetPath); + const context = { + ...safeContext, + payloadLength: payload.length, + }; + + const result = await invokeHookHarness(hook, event, context, invocationTimeoutMs); + + if (result.timedOut) { + maliciousTimeouts.push(`len=${payload.length}`); + continue; + } + + if (result.parseError) { + maliciousFailures.push(`parse-error(${result.parseError})`); + continue; + } + + const normalized = normalizeHarnessPayload(result.parsed); + if (!normalized.ok) { + maliciousFailures.push(normalized.error || "execution-error"); + } + + if ( + normalized.messagesCount > MAX_OUTPUT_MESSAGES || + normalized.messagesCharCount > MAX_OUTPUT_CHARS + ) { + pushHookVulnerability( + findings, + `DAST-OUTPUT-${slug(`${hook.name}-${eventKey}`)}-${payload.length}`, + "medium", + hook, + eventKey, + "Hook output amplification under malicious input", + `Hook generated ${normalized.messagesCount} messages and ${normalized.messagesCharCount} chars for payload length ${payload.length}.`, + ); + } + } + + if (maliciousTimeouts.length > 0) { + pushHookVulnerability( + findings, + `DAST-MALICIOUS-TIMEOUT-${slug(`${hook.name}-${eventKey}`)}`, + "high", + hook, + eventKey, + "Hook times out on malicious input", + `Hook exceeded ${invocationTimeoutMs}ms for malicious payloads (${maliciousTimeouts.slice(0, 3).join(", ")}${maliciousTimeouts.length > 3 ? `, +${maliciousTimeouts.length - 3} more` : ""}).`, + ); + } + + if (maliciousFailures.length > 0) { + pushHookVulnerability( + findings, + `DAST-MALICIOUS-CRASH-${slug(`${hook.name}-${eventKey}`)}`, + "high", + hook, + eventKey, + "Hook crashes on malicious input", + `Hook raised unhandled errors for malicious payloads. Sample errors: ${maliciousFailures.slice(0, 3).join(" | ")}${maliciousFailures.length > 3 ? ` (+${maliciousFailures.length - 3} more)` : ""}`, + ); + } + } + + return findings; +} + +/** + * Execute DAST hook tests. + * + * @param {string} targetPath + * @param {number} timeout + * @returns {Promise} + */ +export async function runDastTests(targetPath, timeout) { + const hooks = await discoverHooks(targetPath); + if (hooks.length === 0) { + process.stderr.write(`[dast] No OpenClaw hooks discovered under ${targetPath}; skipping DAST harness execution\n`); + return []; + } + + const vulnerabilities = []; + + for (const hook of hooks) { + const hookFindings = await evaluateHook(hook, targetPath, timeout); + vulnerabilities.push(...hookFindings); + } + + return vulnerabilities; +} + +/** + * CLI entry point. + */ +async function main() { + try { + const args = parseArgs(process.argv.slice(2)); + + const targetExists = await fileExists(args.target); + if (!targetExists) { + throw new Error(`Target path does not exist: ${args.target}`); + } + + const vulnerabilities = await runDastTests(args.target, args.timeout); + const report = generateReport(vulnerabilities, args.target); + + if (args.format === "text") { + process.stdout.write(formatReportText(report)); + process.stdout.write("\n"); + } else { + process.stdout.write(formatReportJson(report)); + process.stdout.write("\n"); + } + + const hasCriticalOrHigh = report.summary.critical > 0 || report.summary.high > 0; + process.exit(hasCriticalOrHigh ? 1 : 0); + } catch (error) { + process.stderr.write("DAST runner failed:\n"); + if (error instanceof Error) { + process.stderr.write(`${error.message}\n`); + } else { + process.stderr.write(`${String(error)}\n`); + } + process.exit(1); + } +} + +export { MALICIOUS_PAYLOADS }; + +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/.agents/skills/clawsec-scanner/scripts/query_cve_databases.mjs b/.agents/skills/clawsec-scanner/scripts/query_cve_databases.mjs new file mode 100644 index 00000000..8695d42d --- /dev/null +++ b/.agents/skills/clawsec-scanner/scripts/query_cve_databases.mjs @@ -0,0 +1,291 @@ +import { normalizeSeverity, getTimestamp, uniqueStrings } from '../lib/utils.mjs'; + +/** + * Query OSV API for vulnerability data. + * OSV is the primary CVE source (free, no auth, broad ecosystem support). + * + * @param {string} packageName - Package name (e.g., 'lodash') + * @param {string} ecosystem - Ecosystem identifier (e.g., 'npm', 'PyPI') + * @param {string} [version] - Optional specific version to check + * @returns {Promise} + */ +export async function queryOSV(packageName, ecosystem, version = undefined) { + const url = 'https://api.osv.dev/v1/query'; + + const requestBody = { + package: { + name: packageName, + ecosystem: ecosystem, + }, + }; + + if (version) { + requestBody.version = version; + } + + try { + const controller = new globalThis.AbortController(); + const timeout = globalThis.setTimeout(() => controller.abort(), 10000); + + const response = await globalThis.fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody), + signal: controller.signal, + }); + + globalThis.clearTimeout(timeout); + + if (!response.ok) { + console.warn(`OSV API returned status ${response.status} for ${packageName}`); + return []; + } + + const data = await response.json(); + const vulns = data.vulns || []; + + return vulns.map((vuln) => normalizeOSVVulnerability(vuln, packageName, version || '*')); + } catch (error) { + if (error instanceof Error) { + console.warn(`OSV API error for ${packageName}: ${error.message}`); + } + return []; + } +} + +/** + * Query NVD API 2.0 for CVE data. + * Gated behind CLAWSEC_NVD_API_KEY environment variable. + * Enforces 6-second rate limiting without API key. + * + * @param {string} cveId - CVE identifier (e.g., 'CVE-2023-12345') + * @returns {Promise} + */ +export async function queryNVD(cveId) { + const apiKey = process.env.CLAWSEC_NVD_API_KEY; + const url = `https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=${cveId}`; + + const headers = {}; + if (apiKey) { + headers['apiKey'] = apiKey; + } + + try { + const controller = new globalThis.AbortController(); + const timeout = globalThis.setTimeout(() => controller.abort(), 15000); + + const response = await globalThis.fetch(url, { + method: 'GET', + headers, + signal: controller.signal, + }); + + globalThis.clearTimeout(timeout); + + // Rate limiting: 6-second delay required WITHOUT API key + if (!apiKey) { + await new Promise((r) => globalThis.setTimeout(r, 6000)); + } + + if (!response.ok) { + console.warn(`NVD API returned status ${response.status} for ${cveId}`); + return null; + } + + const data = await response.json(); + + if (!data.vulnerabilities || data.vulnerabilities.length === 0) { + return null; + } + + const cveItem = data.vulnerabilities[0].cve; + return normalizeNVDVulnerability(cveItem); + } catch (error) { + if (error instanceof Error) { + console.warn(`NVD API error for ${cveId}: ${error.message}`); + } + return null; + } +} + +/** + * Query GitHub Advisory Database (optional - requires OAuth token). + * Currently a placeholder for future implementation. + * + * @param {string} _packageName - Package name + * @param {string} _ecosystem - Ecosystem (e.g., 'npm', 'pip') + * @returns {Promise} + */ +export async function queryGitHub(_packageName, _ecosystem) { + const token = process.env.GITHUB_TOKEN; + + if (!token) { + console.warn('GitHub Advisory Database query skipped: GITHUB_TOKEN not set'); + return []; + } + + // TODO: Implement GitHub GraphQL advisory query + // This requires GraphQL API integration with oauth token + // Placeholder for future enhancement + console.warn('GitHub Advisory Database integration not yet implemented'); + return []; +} + +/** + * Normalize OSV vulnerability data to unified schema. + * + * @param {any} osvVuln - Raw OSV vulnerability object + * @param {string} packageName - Package name + * @param {string} version - Package version + * @returns {import('../lib/types.ts').Vulnerability} + */ +function normalizeOSVVulnerability(osvVuln, packageName, version) { + const id = osvVuln.id || 'UNKNOWN'; + const summary = osvVuln.summary || 'No description available'; + const details = osvVuln.details || summary; + + // Extract severity from database_specific or severity array + let severity = 'info'; + if (osvVuln.severity && Array.isArray(osvVuln.severity) && osvVuln.severity.length > 0) { + severity = normalizeSeverity(osvVuln.severity[0].type || 'info'); + } else if (osvVuln.database_specific && osvVuln.database_specific.severity) { + severity = normalizeSeverity(osvVuln.database_specific.severity); + } + + // Extract references + const references = []; + if (Array.isArray(osvVuln.references)) { + references.push(...osvVuln.references.map((ref) => ref.url).filter(Boolean)); + } + + // Extract fixed version from affected ranges + let fixedVersion = undefined; + if (Array.isArray(osvVuln.affected)) { + for (const affected of osvVuln.affected) { + if (Array.isArray(affected.ranges)) { + for (const range of affected.ranges) { + if (Array.isArray(range.events)) { + for (const event of range.events) { + if (event.fixed) { + fixedVersion = event.fixed; + break; + } + } + } + } + } + } + } + + return { + id, + source: 'osv', + severity, + package: packageName, + version, + fixed_version: fixedVersion, + title: summary, + description: details, + references: uniqueStrings(references), + discovered_at: getTimestamp(), + }; +} + +/** + * Normalize NVD vulnerability data to unified schema. + * + * @param {any} nvdCve - Raw NVD CVE object + * @returns {import('../lib/types.ts').Vulnerability} + */ +function normalizeNVDVulnerability(nvdCve) { + const id = nvdCve.id || 'UNKNOWN'; + + // Extract description + let description = 'No description available'; + if (nvdCve.descriptions && Array.isArray(nvdCve.descriptions)) { + const englishDesc = nvdCve.descriptions.find((d) => d.lang === 'en'); + if (englishDesc && englishDesc.value) { + description = englishDesc.value; + } + } + + // Extract severity from CVSS metrics + let severity = 'info'; + if (nvdCve.metrics) { + // Try CVSS v3.1 first, then v3.0, then v2.0 + const cvssV31 = nvdCve.metrics.cvssMetricV31?.[0]; + const cvssV30 = nvdCve.metrics.cvssMetricV30?.[0]; + const cvssV2 = nvdCve.metrics.cvssMetricV2?.[0]; + + const cvssData = cvssV31?.cvssData || cvssV30?.cvssData || cvssV2?.cvssData; + if (cvssData && cvssData.baseSeverity) { + severity = normalizeSeverity(cvssData.baseSeverity); + } + } + + // Extract references + const references = []; + if (nvdCve.references && Array.isArray(nvdCve.references)) { + references.push(...nvdCve.references.map((ref) => ref.url).filter(Boolean)); + } + + return { + id, + source: 'nvd', + severity, + package: 'N/A', + version: '*', + fixed_version: undefined, + title: description.slice(0, 100), + description, + references: uniqueStrings(references), + discovered_at: getTimestamp(), + }; +} + +/** + * Enrich vulnerability data by querying multiple CVE databases. + * OSV is primary, NVD is fallback for additional details. + * + * @param {string} packageName - Package name + * @param {string} ecosystem - Ecosystem (e.g., 'npm', 'PyPI') + * @param {string} [version] - Optional version + * @returns {Promise} + */ +export async function enrichVulnerability(packageName, ecosystem, version = undefined) { + const results = []; + + // Query OSV first (primary source) + const osvResults = await queryOSV(packageName, ecosystem, version); + results.push(...osvResults); + + // Optionally query NVD for each CVE ID found in OSV results + const nvdApiKey = process.env.CLAWSEC_NVD_API_KEY; + if (nvdApiKey && results.length > 0) { + for (const vuln of results) { + if (vuln.id.startsWith('CVE-')) { + const nvdData = await queryNVD(vuln.id); + if (nvdData) { + // Merge NVD references into OSV vulnerability + vuln.references = uniqueStrings([...vuln.references, ...nvdData.references]); + } + } + } + } + + return results; +} + +// CLI entry point for testing +if (import.meta.url === `file://${process.argv[1]}`) { + const args = process.argv.slice(2); + const packageName = args[0] || 'lodash'; + const ecosystem = args[1] || 'npm'; + const version = args[2]; + + console.log(`Querying OSV for ${packageName}@${ecosystem}${version ? ` version ${version}` : ''}...`); + + const results = await queryOSV(packageName, ecosystem, version); + console.log(JSON.stringify(results, null, 2)); + console.log(`\nFound ${results.length} vulnerabilities`); +} diff --git a/.agents/skills/clawsec-scanner/scripts/runner.sh b/.agents/skills/clawsec-scanner/scripts/runner.sh new file mode 100755 index 00000000..9755828c --- /dev/null +++ b/.agents/skills/clawsec-scanner/scripts/runner.sh @@ -0,0 +1,288 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runner for clawsec-scanner - orchestrates all vulnerability scanning engines. +# - Runs dependency scan (npm audit + pip-audit) +# - Enriches findings with CVE database lookups (OSV, NVD) +# - Runs SAST analysis (Semgrep + Bandit) +# - Runs DAST security tests (hook handler validation) +# - Generates unified vulnerability report + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Default values +TARGET="" +OUTPUT="" +FORMAT="json" +RUN_DEPS=1 +RUN_CVE=1 +RUN_SAST=1 +RUN_DAST=1 + +# Parse CLI arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --target) + TARGET="${2:-}" + shift 2 + ;; + --output) + OUTPUT="${2:-}" + shift 2 + ;; + --format) + FORMAT="${2:-json}" + shift 2 + ;; + --skip-deps) + RUN_DEPS=0 + shift + ;; + --skip-cve) + RUN_CVE=0 + shift + ;; + --skip-sast) + RUN_SAST=0 + shift + ;; + --skip-dast) + RUN_DAST=0 + shift + ;; + --help|-h) + cat <<'EOF' +Usage: runner.sh --target [options] + +Orchestrates vulnerability scanning across dependency, SAST, DAST, and CVE engines. + +Required: + --target Target directory to scan (e.g., ./skills/) + +Optional: + --output Write report to file (default: stdout) + --format Output format (default: json) + --skip-deps Skip dependency scanning (npm audit, pip-audit) + --skip-cve Skip CVE database enrichment + --skip-sast Skip static analysis (Semgrep, Bandit) + --skip-dast Skip dynamic analysis (hook security tests) + --help, -h Show this help message + +Examples: + # Scan all skills with JSON output to file + ./runner.sh --target ./skills/ --output report.json + + # Scan with human-readable output + ./runner.sh --target ./skills/ --format text + + # Quick scan: dependencies only + ./runner.sh --target ./skills/ --skip-sast --skip-dast --skip-cve + +Environment Variables: + CLAWSEC_NVD_API_KEY Optional NVD API key (avoids rate limiting) + GITHUB_TOKEN Optional GitHub token for Advisory Database + CLAWSEC_SCANNER_INTERVAL Hook scan interval in seconds (default: 86400) + CLAWSEC_ALLOW_UNSIGNED_FEED Allow unsigned advisory feed (dev only) + +EOF + exit 0 + ;; + *) + echo "Unknown flag: $1" >&2 + echo "Run with --help for usage information" >&2 + exit 1 + ;; + esac +done + +# Validate required arguments +if [[ -z "$TARGET" ]]; then + echo "Error: Missing required --target flag" >&2 + echo "Run with --help for usage information" >&2 + exit 1 +fi + +# Validate target exists +if [[ ! -e "$TARGET" ]]; then + echo "Error: Target path does not exist: $TARGET" >&2 + exit 1 +fi + +# Validate format +if [[ "$FORMAT" != "json" && "$FORMAT" != "text" ]]; then + echo "Error: Invalid --format value. Use 'json' or 'text'." >&2 + exit 1 +fi + +# Temporary files for intermediate results +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + +DEPS_REPORT="$TEMP_DIR/deps.json" +SAST_REPORT="$TEMP_DIR/sast.json" +DAST_REPORT="$TEMP_DIR/dast.json" +MERGED_REPORT="$TEMP_DIR/merged.json" + +# Run dependency scan +if [[ "$RUN_DEPS" -eq 1 ]]; then + if command -v node >/dev/null 2>&1; then + node "$SCRIPT_DIR/scan_dependencies.mjs" --target "$TARGET" --format json > "$DEPS_REPORT" 2>/dev/null || { + echo '{"scan_id":"","timestamp":"","target":"","vulnerabilities":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0}}' > "$DEPS_REPORT" + } + else + echo "Warning: node not found, skipping dependency scan" >&2 + echo '{"scan_id":"","timestamp":"","target":"","vulnerabilities":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0}}' > "$DEPS_REPORT" + fi +else + echo '{"scan_id":"","timestamp":"","target":"","vulnerabilities":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0}}' > "$DEPS_REPORT" +fi + +# Run SAST analysis +if [[ "$RUN_SAST" -eq 1 ]]; then + if command -v node >/dev/null 2>&1; then + node "$SCRIPT_DIR/sast_analyzer.mjs" --target "$TARGET" --format json > "$SAST_REPORT" 2>/dev/null || { + echo '{"scan_id":"","timestamp":"","target":"","vulnerabilities":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0}}' > "$SAST_REPORT" + } + else + echo "Warning: node not found, skipping SAST analysis" >&2 + echo '{"scan_id":"","timestamp":"","target":"","vulnerabilities":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0}}' > "$SAST_REPORT" + fi +else + echo '{"scan_id":"","timestamp":"","target":"","vulnerabilities":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0}}' > "$SAST_REPORT" +fi + +# Run DAST tests +if [[ "$RUN_DAST" -eq 1 ]]; then + if command -v node >/dev/null 2>&1; then + if ! node "$SCRIPT_DIR/dast_runner.mjs" --target "$TARGET" --format json > "$DAST_REPORT" 2>/dev/null; then + # dast_runner exits non-zero when high/critical findings exist. + # Preserve a valid JSON report in that case; only fall back to empty on true execution errors. + if [[ -s "$DAST_REPORT" ]] && jq -e '.vulnerabilities and .summary' "$DAST_REPORT" >/dev/null 2>&1; then + echo "Warning: DAST runner exited non-zero; preserving generated findings report" >&2 + else + echo '{"scan_id":"","timestamp":"","target":"","vulnerabilities":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0}}' > "$DAST_REPORT" + fi + fi + else + echo "Warning: node not found, skipping DAST tests" >&2 + echo '{"scan_id":"","timestamp":"","target":"","vulnerabilities":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0}}' > "$DAST_REPORT" + fi +else + echo '{"scan_id":"","timestamp":"","target":"","vulnerabilities":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0}}' > "$DAST_REPORT" +fi + +# Merge reports using jq +if command -v jq >/dev/null 2>&1; then + # Extract vulnerabilities from all reports and merge + jq -s ' + { + scan_id: (.[0].scan_id // ""), + timestamp: (.[0].timestamp // (now | todate)), + target: (.[0].target // ""), + vulnerabilities: (map(.vulnerabilities // []) | flatten), + summary: { + critical: (map(.summary.critical // 0) | add), + high: (map(.summary.high // 0) | add), + medium: (map(.summary.medium // 0) | add), + low: (map(.summary.low // 0) | add), + info: (map(.summary.info // 0) | add) + } + } + ' "$DEPS_REPORT" "$SAST_REPORT" "$DAST_REPORT" > "$MERGED_REPORT" +else + echo "Error: jq not found. Required for report merging." >&2 + exit 1 +fi + +# CVE enrichment (if enabled and vulnerabilities found) +if [[ "$RUN_CVE" -eq 1 ]]; then + VULN_COUNT=$(jq '.vulnerabilities | length' "$MERGED_REPORT") + if [[ "$VULN_COUNT" -gt 0 ]] && command -v node >/dev/null 2>&1; then + # Note: CVE enrichment is done inline by scan_dependencies.mjs for efficiency + # Future enhancement: implement post-scan enrichment for SAST/DAST findings + : + fi +fi + +# Output final report +if [[ "$FORMAT" == "json" ]]; then + FINAL_OUTPUT=$(cat "$MERGED_REPORT") +elif [[ "$FORMAT" == "text" ]]; then + # Convert JSON to human-readable text using Node.js + if command -v node >/dev/null 2>&1; then + FINAL_OUTPUT=$(node -e " + const fs = require('fs'); + const report = JSON.parse(fs.readFileSync('$MERGED_REPORT', 'utf8')); + + console.log('='.repeat(80)); + console.log('ClawSec Vulnerability Scan Report'); + console.log('='.repeat(80)); + console.log(''); + console.log('Scan ID: ' + report.scan_id); + console.log('Target: ' + report.target); + console.log('Timestamp: ' + report.timestamp); + console.log(''); + console.log('Summary:'); + console.log(' Critical: ' + report.summary.critical); + console.log(' High: ' + report.summary.high); + console.log(' Medium: ' + report.summary.medium); + console.log(' Low: ' + report.summary.low); + console.log(' Info: ' + report.summary.info); + console.log(' Total: ' + report.vulnerabilities.length); + console.log(''); + + if (report.vulnerabilities.length === 0) { + console.log('✓ No vulnerabilities detected'); + console.log(''); + } else { + console.log('Vulnerabilities by Severity:'); + console.log(''); + + const bySeverity = { + critical: [], + high: [], + medium: [], + low: [], + info: [] + }; + + report.vulnerabilities.forEach(v => { + const sev = v.severity || 'info'; + if (bySeverity[sev]) { + bySeverity[sev].push(v); + } + }); + + ['critical', 'high', 'medium', 'low', 'info'].forEach(severity => { + const vulns = bySeverity[severity]; + if (vulns.length > 0) { + console.log(severity.toUpperCase() + ':'); + vulns.forEach((v, idx) => { + console.log(' ' + (idx + 1) + '. [' + v.source + '] ' + v.id + ' - ' + v.title); + console.log(' Package: ' + v.package + '@' + v.version); + if (v.fixed_version) { + console.log(' Fix: Upgrade to ' + v.fixed_version); + } + console.log(''); + }); + } + }); + } + + console.log('='.repeat(80)); + ") + else + echo "Error: node required for text format output" >&2 + exit 1 + fi +else + FINAL_OUTPUT=$(cat "$MERGED_REPORT") +fi + +# Write output +if [[ -n "$OUTPUT" ]]; then + printf '%s\n' "$FINAL_OUTPUT" > "$OUTPUT" +else + printf '%s\n' "$FINAL_OUTPUT" +fi diff --git a/.agents/skills/clawsec-scanner/scripts/sast_analyzer.mjs b/.agents/skills/clawsec-scanner/scripts/sast_analyzer.mjs new file mode 100755 index 00000000..ad2ede39 --- /dev/null +++ b/.agents/skills/clawsec-scanner/scripts/sast_analyzer.mjs @@ -0,0 +1,306 @@ +#!/usr/bin/env node + +import fs from "node:fs/promises"; +import path from "node:path"; +import { + execCommand, + safeJsonParse, + normalizeSeverity, + getTimestamp, + commandExists, +} from "../lib/utils.mjs"; +import { generateReport, formatReportJson, formatReportText } from "../lib/report.mjs"; + +/** + * @typedef {import('../lib/types.ts').Vulnerability} Vulnerability + * @typedef {import('../lib/types.ts').ScanReport} ScanReport + */ + +/** + * Parse CLI arguments. + * + * @param {string[]} argv - Command line arguments + * @returns {{target: string, format: 'json' | 'text'}} + */ +function parseArgs(argv) { + const parsed = { + target: "", + format: "json", + }; + + for (let i = 0; i < argv.length; i += 1) { + const token = argv[i]; + + if (token === "--target") { + parsed.target = String(argv[i + 1] ?? "").trim(); + i += 1; + continue; + } + if (token === "--format") { + const formatValue = String(argv[i + 1] ?? "").trim(); + if (formatValue !== "json" && formatValue !== "text") { + throw new Error("Invalid --format value. Use 'json' or 'text'."); + } + parsed.format = formatValue; + i += 1; + continue; + } + if (token === "--help" || token === "-h") { + printUsage(); + process.exit(0); + } + + throw new Error(`Unknown argument: ${token}`); + } + + if (!parsed.target) { + throw new Error("Missing required argument: --target"); + } + + return parsed; +} + +/** + * Print usage information. + */ +function printUsage() { + process.stderr.write( + [ + "Usage:", + " node scripts/sast_analyzer.mjs --target [--format json|text]", + "", + "Examples:", + " node scripts/sast_analyzer.mjs --target ./skills/clawsec-suite", + " node scripts/sast_analyzer.mjs --target ./skills/ --format json", + "", + "Flags:", + " --target Path to scan (required)", + " --format Output format: json or text (default: json)", + "", + ].join("\n"), + ); +} + +/** + * Check if a file exists. + * + * @param {string} filePath - Path to check + * @returns {Promise} + */ +async function fileExists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +/** + * Run Semgrep for JavaScript/TypeScript analysis. + * + * @param {string} targetPath - Path to scan + * @returns {Promise} + */ +async function runSemgrep(targetPath) { + const vulnerabilities = []; + + // Check if semgrep is available + const hasSemgrep = await commandExists("semgrep"); + if (!hasSemgrep) { + process.stderr.write("[semgrep] semgrep command not found, skipping JavaScript/TypeScript SAST\n"); + return vulnerabilities; + } + + try { + // Run Semgrep with security-focused rules + // NOTE: Semgrep exits non-zero when findings are present + const { stdout } = await execCommand("semgrep", [ + "scan", + "--config", "auto", + "--json", + targetPath, + ]); + + const semgrepData = safeJsonParse(stdout, { + fallback: { results: [] }, + label: "semgrep output", + }); + + // Semgrep format: { results: [ {check_id, path, extra: {message, severity, ...}, ...} ] } + if (semgrepData && typeof semgrepData === "object" && "results" in semgrepData) { + const results = Array.isArray(semgrepData.results) ? semgrepData.results : []; + + for (const result of results) { + if (!result || typeof result !== "object") continue; + + const checkId = String(result.check_id || "semgrep-unknown"); + const filePath = String(result.path || "unknown"); + const extra = result.extra || {}; + + // Extract metadata + const message = String(extra.message || "Security issue detected"); + const severity = normalizeSeverity(extra.severity || "info"); + const metadata = extra.metadata || {}; + + // Build references from metadata + const references = []; + if (metadata.references && Array.isArray(metadata.references)) { + references.push(...metadata.references.map((r) => String(r))); + } + if (metadata.source && typeof metadata.source === "string") { + references.push(metadata.source); + } + + const vuln = { + id: checkId, + source: "sast", + severity, + package: path.basename(filePath), + version: `${filePath}:${result.start?.line || 0}`, + fixed_version: "", + title: message.slice(0, 150), + description: message, + references, + discovered_at: getTimestamp(), + }; + + vulnerabilities.push(vuln); + } + } + } catch (error) { + if (error instanceof Error) { + process.stderr.write(`[semgrep] Warning: ${error.message}\n`); + } + // Continue with partial results + } + + return vulnerabilities; +} + +/** + * Run Bandit for Python analysis. + * + * @param {string} targetPath - Path to scan + * @returns {Promise} + */ +async function runBandit(targetPath) { + const vulnerabilities = []; + + // Check if bandit is available + const hasBandit = await commandExists("bandit"); + if (!hasBandit) { + process.stderr.write("[bandit] bandit command not found, skipping Python SAST\n"); + return vulnerabilities; + } + + // Check if pyproject.toml exists in the project root + const pyprojectPath = path.join(process.cwd(), "pyproject.toml"); + const hasPyproject = await fileExists(pyprojectPath); + + try { + // Run Bandit with JSON output + // NOTE: Bandit exits non-zero when findings are present + const args = ["-r", targetPath, "-f", "json"]; + + // Only add -c flag if pyproject.toml exists + if (hasPyproject) { + args.push("-c", pyprojectPath); + } + + const { stdout } = await execCommand("bandit", args); + + const banditData = safeJsonParse(stdout, { + fallback: { results: [] }, + label: "bandit output", + }); + + // Bandit format: { results: [ {issue_text, issue_severity, issue_confidence, test_id, filename, line_number, ...} ] } + if (banditData && typeof banditData === "object" && "results" in banditData) { + const results = Array.isArray(banditData.results) ? banditData.results : []; + + for (const result of results) { + if (!result || typeof result !== "object") continue; + + const testId = String(result.test_id || "bandit-unknown"); + const filePath = String(result.filename || "unknown"); + const lineNumber = result.line_number || 0; + const issueText = String(result.issue_text || "Security issue detected"); + const issueSeverity = String(result.issue_severity || "LOW"); + + // Map Bandit severity (HIGH, MEDIUM, LOW) to normalized severity + const severity = normalizeSeverity(issueSeverity); + + const vuln = { + id: testId, + source: "sast", + severity, + package: path.basename(filePath), + version: `${filePath}:${lineNumber}`, + fixed_version: "", + title: issueText.slice(0, 150), + description: issueText, + references: [ + `https://bandit.readthedocs.io/en/latest/plugins/${testId.toLowerCase().replace(/_/g, '-')}.html`, + ], + discovered_at: getTimestamp(), + }; + + vulnerabilities.push(vuln); + } + } + } catch (error) { + if (error instanceof Error) { + process.stderr.write(`[bandit] Warning: ${error.message}\n`); + } + // Continue with partial results + } + + return vulnerabilities; +} + +/** + * Main entry point. + */ +async function main() { + try { + const args = parseArgs(process.argv.slice(2)); + + // Verify target path exists + const targetExists = await fileExists(args.target); + if (!targetExists) { + throw new Error(`Target path does not exist: ${args.target}`); + } + + // Run SAST tools + const semgrepVulns = await runSemgrep(args.target); + const banditVulns = await runBandit(args.target); + + // Combine all vulnerabilities + const allVulnerabilities = [...semgrepVulns, ...banditVulns]; + + // Generate unified report + const report = generateReport(allVulnerabilities, args.target); + + // Output report + if (args.format === "json") { + process.stdout.write(formatReportJson(report)); + process.stdout.write("\n"); + } else { + process.stdout.write(formatReportText(report)); + } + + // Exit 0 even if vulnerabilities found (advisory only) + process.exit(0); + } catch (error) { + if (error instanceof Error) { + process.stderr.write(`Error: ${error.message}\n`); + } + process.exit(1); + } +} + +// Run if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/.agents/skills/clawsec-scanner/scripts/scan_dependencies.mjs b/.agents/skills/clawsec-scanner/scripts/scan_dependencies.mjs new file mode 100755 index 00000000..c2110bec --- /dev/null +++ b/.agents/skills/clawsec-scanner/scripts/scan_dependencies.mjs @@ -0,0 +1,325 @@ +#!/usr/bin/env node + +import fs from "node:fs/promises"; +import path from "node:path"; +import { + execCommand, + safeJsonParse, + normalizeSeverity, + getTimestamp, + commandExists, +} from "../lib/utils.mjs"; +import { generateReport, formatReportJson, formatReportText } from "../lib/report.mjs"; + +/** + * @typedef {import('../lib/types.ts').Vulnerability} Vulnerability + * @typedef {import('../lib/types.ts').ScanReport} ScanReport + */ + +/** + * Parse CLI arguments. + * + * @param {string[]} argv - Command line arguments + * @returns {{target: string, format: 'json' | 'text'}} + */ +function parseArgs(argv) { + const parsed = { + target: "", + format: "json", + }; + + for (let i = 0; i < argv.length; i += 1) { + const token = argv[i]; + + if (token === "--target") { + parsed.target = String(argv[i + 1] ?? "").trim(); + i += 1; + continue; + } + if (token === "--format") { + const formatValue = String(argv[i + 1] ?? "").trim(); + if (formatValue !== "json" && formatValue !== "text") { + throw new Error("Invalid --format value. Use 'json' or 'text'."); + } + parsed.format = formatValue; + i += 1; + continue; + } + if (token === "--help" || token === "-h") { + printUsage(); + process.exit(0); + } + + throw new Error(`Unknown argument: ${token}`); + } + + if (!parsed.target) { + throw new Error("Missing required argument: --target"); + } + + return parsed; +} + +/** + * Print usage information. + */ +function printUsage() { + process.stderr.write( + [ + "Usage:", + " node scripts/scan_dependencies.mjs --target [--format json|text]", + "", + "Examples:", + " node scripts/scan_dependencies.mjs --target ./skills/clawsec-suite", + " node scripts/scan_dependencies.mjs --target ./skills/ --format json", + "", + "Flags:", + " --target Path to scan (required)", + " --format Output format: json or text (default: json)", + "", + ].join("\n"), + ); +} + +/** + * Check if a file exists. + * + * @param {string} filePath - Path to check + * @returns {Promise} + */ +async function fileExists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +/** + * Run npm audit and parse vulnerabilities. + * + * @param {string} targetPath - Path to scan + * @returns {Promise} + */ +async function scanNpmAudit(targetPath) { + const vulnerabilities = []; + + // Check if package-lock.json exists + const packageLockPath = path.join(targetPath, "package-lock.json"); + const hasPackageLock = await fileExists(packageLockPath); + + if (!hasPackageLock) { + process.stderr.write(`[npm-audit] No package-lock.json found in ${targetPath}, skipping npm audit\n`); + return vulnerabilities; + } + + // Check if npm is available + const hasNpm = await commandExists("npm"); + if (!hasNpm) { + process.stderr.write("[npm-audit] npm command not found, skipping npm audit\n"); + return vulnerabilities; + } + + try { + // Run npm audit with JSON output + // NOTE: npm audit exits non-zero when vulnerabilities are found + const { stdout } = await execCommand("npm", ["audit", "--json"], { cwd: targetPath }); + + const auditData = safeJsonParse(stdout, { + fallback: { vulnerabilities: {} }, + label: "npm audit output", + }); + + // npm audit v7+ format: { vulnerabilities: { [package]: {...} } } + if (auditData && typeof auditData === "object" && "vulnerabilities" in auditData) { + const vulnsMap = auditData.vulnerabilities; + + if (vulnsMap && typeof vulnsMap === "object") { + for (const [packageName, vulnData] of Object.entries(vulnsMap)) { + if (!vulnData || typeof vulnData !== "object") continue; + + // Extract vulnerability data + const severity = normalizeSeverity(vulnData.severity || "info"); + const version = String(vulnData.range || vulnData.version || "unknown"); + const via = Array.isArray(vulnData.via) ? vulnData.via : []; + + // npm audit can have multiple advisories via the 'via' field + for (const viaItem of via) { + if (typeof viaItem === "object" && viaItem !== null) { + const vuln = { + id: String(viaItem.source || viaItem.cve || `npm-${packageName}`), + source: "npm-audit", + severity, + package: packageName, + version, + fixed_version: String(vulnData.fixAvailable?.version || ""), + title: String(viaItem.title || `Vulnerability in ${packageName}`), + description: String(viaItem.title || viaItem.name || "No description available"), + references: viaItem.url ? [String(viaItem.url)] : [], + discovered_at: getTimestamp(), + }; + + vulnerabilities.push(vuln); + } + } + + // If 'via' doesn't have objects, create a generic entry + if (via.length === 0 || via.every((v) => typeof v !== "object")) { + const vuln = { + id: `npm-${packageName}`, + source: "npm-audit", + severity, + package: packageName, + version, + fixed_version: String(vulnData.fixAvailable?.version || ""), + title: `Vulnerability in ${packageName}`, + description: String(vulnData.name || `Vulnerability detected in ${packageName}`), + references: [], + discovered_at: getTimestamp(), + }; + + vulnerabilities.push(vuln); + } + } + } + } + } catch (error) { + if (error instanceof Error) { + process.stderr.write(`[npm-audit] Warning: ${error.message}\n`); + } + // Continue with partial results + } + + return vulnerabilities; +} + +/** + * Run pip-audit and parse vulnerabilities. + * + * @param {string} targetPath - Path to scan + * @returns {Promise} + */ +async function scanPipAudit(targetPath) { + const vulnerabilities = []; + + // Check if pip-audit is available + const hasPipAudit = await commandExists("pip-audit"); + if (!hasPipAudit) { + process.stderr.write("[pip-audit] pip-audit command not found, skipping Python dependency scan\n"); + return vulnerabilities; + } + + // Check if requirements.txt or setup.py exists + const requirementsTxt = path.join(targetPath, "requirements.txt"); + const setupPy = path.join(targetPath, "setup.py"); + const pyprojectToml = path.join(targetPath, "pyproject.toml"); + + const hasRequirements = await fileExists(requirementsTxt); + const hasSetupPy = await fileExists(setupPy); + const hasPyprojectToml = await fileExists(pyprojectToml); + + if (!hasRequirements && !hasSetupPy && !hasPyprojectToml) { + process.stderr.write( + `[pip-audit] No Python dependency files found in ${targetPath}, skipping pip-audit\n`, + ); + return vulnerabilities; + } + + try { + // Prefer requirements.txt when present; otherwise scan project context in target dir. + const pipAuditArgs = hasRequirements ? ["-f", "json", "-r", "requirements.txt"] : ["-f", "json"]; + const { stdout } = await execCommand("pip-audit", pipAuditArgs, { cwd: targetPath }); + + const auditData = safeJsonParse(stdout, { + fallback: { dependencies: [] }, + label: "pip-audit output", + }); + + // pip-audit format: { dependencies: [ {name, version, vulns: [{id, fix_versions, description, ...}]} ] } + if (auditData && typeof auditData === "object" && "dependencies" in auditData) { + const deps = Array.isArray(auditData.dependencies) ? auditData.dependencies : []; + + for (const dep of deps) { + if (!dep || typeof dep !== "object") continue; + + const packageName = String(dep.name || "unknown"); + const version = String(dep.version || "unknown"); + const vulns = Array.isArray(dep.vulns) ? dep.vulns : []; + + for (const vulnData of vulns) { + if (!vulnData || typeof vulnData !== "object") continue; + + const fixVersions = Array.isArray(vulnData.fix_versions) ? vulnData.fix_versions : []; + const vuln = { + id: String(vulnData.id || `pip-${packageName}`), + source: "pip-audit", + severity: normalizeSeverity(vulnData.severity || "info"), + package: packageName, + version, + fixed_version: fixVersions.length > 0 ? String(fixVersions[0]) : "", + title: String(vulnData.description || `Vulnerability in ${packageName}`).slice(0, 150), + description: String(vulnData.description || "No description available"), + references: vulnData.link ? [String(vulnData.link)] : [], + discovered_at: getTimestamp(), + }; + + vulnerabilities.push(vuln); + } + } + } + } catch (error) { + if (error instanceof Error) { + process.stderr.write(`[pip-audit] Warning: ${error.message}\n`); + } + // Continue with partial results + } + + return vulnerabilities; +} + +/** + * Main entry point. + */ +async function main() { + try { + const args = parseArgs(process.argv.slice(2)); + + // Verify target path exists + const targetExists = await fileExists(args.target); + if (!targetExists) { + throw new Error(`Target path does not exist: ${args.target}`); + } + + // Run dependency scanners + const npmVulns = await scanNpmAudit(args.target); + const pipVulns = await scanPipAudit(args.target); + + // Combine all vulnerabilities + const allVulnerabilities = [...npmVulns, ...pipVulns]; + + // Generate unified report + const report = generateReport(allVulnerabilities, args.target); + + // Output report + if (args.format === "json") { + process.stdout.write(formatReportJson(report)); + process.stdout.write("\n"); + } else { + process.stdout.write(formatReportText(report)); + } + + // Exit 0 even if vulnerabilities found (advisory only) + process.exit(0); + } catch (error) { + if (error instanceof Error) { + process.stderr.write(`Error: ${error.message}\n`); + } + process.exit(1); + } +} + +// Run if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/.agents/skills/clawsec-scanner/scripts/setup_scanner_hook.mjs b/.agents/skills/clawsec-scanner/scripts/setup_scanner_hook.mjs new file mode 100755 index 00000000..c1d2f91b --- /dev/null +++ b/.agents/skills/clawsec-scanner/scripts/setup_scanner_hook.mjs @@ -0,0 +1,126 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const HOOK_NAME = "clawsec-scanner-hook"; +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const SCANNER_DIR = path.resolve(SCRIPT_DIR, ".."); +const SOURCE_HOOK_DIR = path.join(SCANNER_DIR, "hooks", HOOK_NAME); +const HOOKS_ROOT = path.join(os.homedir(), ".openclaw", "hooks"); +const TARGET_HOOK_DIR = path.join(HOOKS_ROOT, HOOK_NAME); + +function sh(cmd, args) { + const result = spawnSync(cmd, args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + const details = (result.stderr || result.stdout || "").trim(); + throw new Error(`${cmd} ${args.join(" ")} failed${details ? `: ${details}` : ""}`); + } + + return result.stdout; +} + +function requireOpenClawCli() { + try { + sh("openclaw", ["--version"]); + } catch (error) { + throw new Error( + "openclaw CLI is required. Install OpenClaw and ensure `openclaw` is available in PATH. " + + `Original error: ${String(error)}`, + { cause: error }, + ); + } +} + +function assertSourceHookExists() { + const requiredFiles = [ + "HOOK.md", + "handler.ts", + ]; + for (const file of requiredFiles) { + const fullPath = path.join(SOURCE_HOOK_DIR, file); + if (!fs.existsSync(fullPath)) { + throw new Error(`Missing required hook file: ${fullPath}`); + } + } + + // Verify lib files exist in parent skill directory + const requiredLibFiles = [ + "lib/utils.mjs", + "lib/report.mjs", + "lib/types.ts", + ]; + for (const file of requiredLibFiles) { + const fullPath = path.join(SCANNER_DIR, file); + if (!fs.existsSync(fullPath)) { + throw new Error(`Missing required lib file: ${fullPath}`); + } + } + + // Verify scanner scripts exist + const requiredScripts = [ + "scripts/runner.sh", + "scripts/scan_dependencies.mjs", + "scripts/sast_analyzer.mjs", + "scripts/dast_runner.mjs", + "scripts/dast_hook_executor.mjs", + "scripts/query_cve_databases.mjs", + ]; + for (const file of requiredScripts) { + const fullPath = path.join(SCANNER_DIR, file); + if (!fs.existsSync(fullPath)) { + throw new Error(`Missing required scanner script: ${fullPath}`); + } + } +} + +function installHookFiles() { + fs.mkdirSync(HOOKS_ROOT, { recursive: true }); + fs.rmSync(TARGET_HOOK_DIR, { recursive: true, force: true }); + fs.cpSync(SOURCE_HOOK_DIR, TARGET_HOOK_DIR, { recursive: true }); + + // Copy lib files to hook directory + const targetLibDir = path.join(TARGET_HOOK_DIR, "lib"); + const sourceLibDir = path.join(SCANNER_DIR, "lib"); + fs.mkdirSync(targetLibDir, { recursive: true }); + fs.cpSync(sourceLibDir, targetLibDir, { recursive: true }); + + // Copy scanner scripts to hook directory + const targetScriptsDir = path.join(TARGET_HOOK_DIR, "scripts"); + const sourceScriptsDir = path.join(SCANNER_DIR, "scripts"); + fs.mkdirSync(targetScriptsDir, { recursive: true }); + fs.cpSync(sourceScriptsDir, targetScriptsDir, { recursive: true }); +} + +function enableHook() { + sh("openclaw", ["hooks", "enable", HOOK_NAME]); +} + +function main() { + assertSourceHookExists(); + requireOpenClawCli(); + installHookFiles(); + enableHook(); + + process.stdout.write(`Installed hook files to: ${TARGET_HOOK_DIR}\n`); + process.stdout.write(`Enabled hook: ${HOOK_NAME}\n`); + process.stdout.write("Restart your OpenClaw gateway process so the hook is loaded.\n"); + process.stdout.write("After restart, run /new once to trigger an immediate vulnerability scan.\n"); +} + +try { + main(); +} catch (error) { + process.stderr.write(`${String(error)}\n`); + process.exit(1); +} diff --git a/.agents/skills/clawsec-scanner/skill.json b/.agents/skills/clawsec-scanner/skill.json new file mode 100644 index 00000000..8616a1b4 --- /dev/null +++ b/.agents/skills/clawsec-scanner/skill.json @@ -0,0 +1,127 @@ +{ + "name": "clawsec-scanner", + "version": "0.0.3", + "description": "Automated vulnerability scanner for agent platforms. Performs dependency scanning (npm audit, pip-audit), multi-database CVE lookup (OSV, NVD, GitHub Advisory), SAST analysis (Semgrep, Bandit), and agent-specific DAST hook execution testing for OpenClaw hooks.", + "author": "prompt-security", + "license": "AGPL-3.0-or-later", + "homepage": "https://clawsec.prompt.security/", + "keywords": [ + "security", + "vulnerability", + "scanner", + "dependency", + "cve", + "sast", + "dast", + "audit", + "agents", + "ai", + "openclaw", + "semgrep", + "bandit", + "osv", + "nvd" + ], + "sbom": { + "files": [ + { + "path": "SKILL.md", + "required": true, + "description": "Scanner skill documentation and usage guide" + }, + { + "path": "CHANGELOG.md", + "required": true, + "description": "Version history and feature changelog" + }, + { + "path": "scripts/runner.sh", + "required": true, + "description": "Main orchestration script for running all scanner engines" + }, + { + "path": "scripts/scan_dependencies.mjs", + "required": true, + "description": "Dependency scanner using npm audit and pip-audit with JSON parsing" + }, + { + "path": "scripts/query_cve_databases.mjs", + "required": true, + "description": "Multi-database CVE lookup (OSV primary, NVD/GitHub fallback)" + }, + { + "path": "scripts/sast_analyzer.mjs", + "required": true, + "description": "Static analysis engine running Semgrep and Bandit as subprocesses" + }, + { + "path": "scripts/dast_runner.mjs", + "required": true, + "description": "Dynamic analysis harness executing OpenClaw hook handlers with malicious-input and timeout checks" + }, + { + "path": "scripts/dast_hook_executor.mjs", + "required": true, + "description": "Isolated hook execution helper used by DAST for real OpenClaw harness testing" + }, + { + "path": "scripts/setup_scanner_hook.mjs", + "required": false, + "description": "Hook installer for continuous monitoring integration" + }, + { + "path": "lib/report.mjs", + "required": true, + "description": "Unified vulnerability report generator (JSON and human-readable formats)" + }, + { + "path": "lib/utils.mjs", + "required": true, + "description": "Shared utility functions for subprocess execution and JSON parsing" + }, + { + "path": "lib/types.ts", + "required": true, + "description": "TypeScript type definitions for Vulnerability and ScanReport schemas" + }, + { + "path": "hooks/clawsec-scanner-hook/HOOK.md", + "required": false, + "description": "OpenClaw hook metadata for continuous scanning integration" + }, + { + "path": "hooks/clawsec-scanner-hook/handler.ts", + "required": false, + "description": "OpenClaw hook handler for periodic vulnerability scanning" + } + ] + }, + "openclaw": { + "emoji": "🔍", + "category": "security", + "requires": { + "bins": [ + "node", + "npm", + "python3", + "pip-audit", + "semgrep", + "bandit", + "jq", + "curl" + ] + }, + "triggers": [ + "vulnerability scan", + "security scan", + "dependency scan", + "cve scan", + "sast scan", + "run scanner", + "scan vulnerabilities", + "check vulnerabilities", + "audit dependencies", + "security check" + ] + } +} diff --git a/.agents/skills/clawsec-scanner/test/.gitkeep b/.agents/skills/clawsec-scanner/test/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.agents/skills/clawsec-scanner/test/cve_integration.test.mjs b/.agents/skills/clawsec-scanner/test/cve_integration.test.mjs new file mode 100755 index 00000000..cebea68f --- /dev/null +++ b/.agents/skills/clawsec-scanner/test/cve_integration.test.mjs @@ -0,0 +1,571 @@ +#!/usr/bin/env node + +/** + * CVE integration tests for clawsec-scanner. + * + * Tests cover: + * - OSV API query and normalization + * - NVD API query and normalization + * - GitHub Advisory Database query (placeholder) + * - Multi-source enrichment + * - Error handling and timeouts + * - Rate limiting behavior + * + * Run: node skills/clawsec-scanner/test/cve_integration.test.mjs + */ + +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { pass, fail, report, exitWithResults, withEnv } from "./lib/test_harness.mjs"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const SCRIPTS_PATH = path.resolve(__dirname, "..", "scripts"); + +// Dynamic import to ensure we test the actual modules +const { queryOSV, queryNVD, queryGitHub, enrichVulnerability } = await import( + `${SCRIPTS_PATH}/query_cve_databases.mjs` +); + +// ----------------------------------------------------------------------------- +// Test: queryOSV - successful query with results +// ----------------------------------------------------------------------------- +async function testQueryOSV_Success() { + const testName = "queryOSV: successful query returns vulnerabilities"; + try { + // Query a known vulnerable package (lodash has known vulnerabilities) + const results = await queryOSV("lodash", "npm", "4.17.19"); + + // lodash 4.17.19 has known vulnerabilities + if (Array.isArray(results) && results.length > 0) { + // Verify structure of first result + const vuln = results[0]; + if ( + vuln.id && + vuln.source === "osv" && + vuln.severity && + vuln.package === "lodash" && + vuln.title && + vuln.description && + Array.isArray(vuln.references) + ) { + pass(testName); + } else { + fail(testName, `Invalid vulnerability structure: ${JSON.stringify(vuln)}`); + } + } else { + // If no results, package may have been patched - that's also valid + pass(testName); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: queryOSV - returns empty array for non-existent package +// ----------------------------------------------------------------------------- +async function testQueryOSV_NotFound() { + const testName = "queryOSV: returns empty array for non-existent package"; + try { + const results = await queryOSV("nonexistent-package-that-does-not-exist-12345", "npm"); + + if (Array.isArray(results) && results.length === 0) { + pass(testName); + } else { + fail(testName, `Expected empty array, got ${results.length} results`); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: queryOSV - handles network errors gracefully +// ----------------------------------------------------------------------------- +async function testQueryOSV_NetworkError() { + const testName = "queryOSV: handles network errors gracefully"; + try { + // This will likely timeout or fail, but should return empty array + const results = await queryOSV("test-pkg", "invalid-ecosystem-999"); + + if (Array.isArray(results)) { + pass(testName); + } else { + fail(testName, `Expected array, got ${typeof results}`); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: queryOSV - version-specific query +// ----------------------------------------------------------------------------- +async function testQueryOSV_WithVersion() { + const testName = "queryOSV: handles version-specific queries"; + try { + const results = await queryOSV("express", "npm", "4.16.0"); + + // Express 4.16.0 may or may not have vulnerabilities + // Just verify it returns an array + if (Array.isArray(results)) { + pass(testName); + } else { + fail(testName, `Expected array, got ${typeof results}`); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: queryOSV - normalizes severity correctly +// ----------------------------------------------------------------------------- +async function testQueryOSV_SeverityNormalization() { + const testName = "queryOSV: normalizes severity from API response"; + try { + const results = await queryOSV("lodash", "npm", "4.17.19"); + + if (results.length > 0) { + const validSeverities = ["critical", "high", "medium", "low", "info"]; + const allValid = results.every((vuln) => validSeverities.includes(vuln.severity)); + + if (allValid) { + pass(testName); + } else { + fail( + testName, + `Invalid severity found: ${results.map((v) => v.severity).join(", ")}`, + ); + } + } else { + // No results is valid + pass(testName); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: queryNVD - requires API key or respects rate limiting +// ----------------------------------------------------------------------------- +async function testQueryNVD_RateLimiting() { + const testName = "queryNVD: respects rate limiting without API key"; + try { + await withEnv("CLAWSEC_NVD_API_KEY", undefined, async () => { + const startTime = Date.now(); + + // Query should add 6-second delay when no API key (if request succeeds) + await queryNVD("CVE-2021-44228"); + + const elapsed = Date.now() - startTime; + + // If the request failed quickly (network issue), skip the test + if (elapsed < 100) { + pass(testName + " (skipped - network unavailable)"); + } else if (elapsed >= 5900) { + // Should take at least 6 seconds if successful + pass(testName); + } else { + fail(testName, `Expected ~6s delay, got ${elapsed}ms`); + } + }); + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: queryNVD - handles non-existent CVE +// ----------------------------------------------------------------------------- +async function testQueryNVD_NotFound() { + const testName = "queryNVD: returns null for non-existent CVE"; + try { + await withEnv("CLAWSEC_NVD_API_KEY", undefined, async () => { + const result = await queryNVD("CVE-9999-99999"); + + if (result === null) { + pass(testName); + } else { + fail(testName, `Expected null, got ${JSON.stringify(result)}`); + } + }); + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: queryNVD - valid CVE returns structured data +// ----------------------------------------------------------------------------- +async function testQueryNVD_ValidCVE() { + const testName = "queryNVD: valid CVE returns structured vulnerability"; + try { + // Only run if API key is set (to avoid rate limiting in CI) + const apiKey = process.env.CLAWSEC_NVD_API_KEY; + if (!apiKey) { + pass(testName + " (skipped - no API key)"); + return; + } + + const result = await queryNVD("CVE-2021-44228"); + + if (result && result.id === "CVE-2021-44228" && result.source === "nvd") { + pass(testName); + } else if (result === null) { + // API might be down or rate limited + pass(testName + " (API returned null)"); + } else { + fail(testName, `Unexpected result: ${JSON.stringify(result)}`); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: queryGitHub - returns empty array when token not set +// ----------------------------------------------------------------------------- +async function testQueryGitHub_NoToken() { + const testName = "queryGitHub: returns empty array when token not set"; + try { + await withEnv("GITHUB_TOKEN", undefined, async () => { + const results = await queryGitHub("test-package", "npm"); + + if (Array.isArray(results) && results.length === 0) { + pass(testName); + } else { + fail(testName, `Expected empty array, got ${results.length} results`); + } + }); + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: queryGitHub - placeholder implementation +// ----------------------------------------------------------------------------- +async function testQueryGitHub_Placeholder() { + const testName = "queryGitHub: placeholder returns empty array with token"; + try { + await withEnv("GITHUB_TOKEN", "fake-token-for-testing", async () => { + const results = await queryGitHub("test-package", "npm"); + + // Current implementation is a placeholder + if (Array.isArray(results) && results.length === 0) { + pass(testName); + } else { + fail(testName, `Expected empty array, got ${results.length} results`); + } + }); + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: enrichVulnerability - combines OSV results +// ----------------------------------------------------------------------------- +async function testEnrichVulnerability_OSVOnly() { + const testName = "enrichVulnerability: returns OSV results"; + try { + await withEnv("CLAWSEC_NVD_API_KEY", undefined, async () => { + const results = await enrichVulnerability("lodash", "npm", "4.17.19"); + + if (Array.isArray(results)) { + pass(testName); + } else { + fail(testName, `Expected array, got ${typeof results}`); + } + }); + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: enrichVulnerability - enriches with NVD when API key present +// ----------------------------------------------------------------------------- +async function testEnrichVulnerability_WithNVD() { + const testName = "enrichVulnerability: enriches with NVD when API key present"; + try { + const apiKey = process.env.CLAWSEC_NVD_API_KEY; + if (!apiKey) { + pass(testName + " (skipped - no API key)"); + return; + } + + // Query a package with known CVE + const results = await enrichVulnerability("lodash", "npm", "4.17.19"); + + // If results contain CVE IDs, they should have enriched references + const hasCVE = results.some((v) => v.id.startsWith("CVE-")); + + if (hasCVE) { + // Check if references were enriched (should have more than original OSV refs) + const hasReferences = results.some((v) => v.references.length > 0); + if (hasReferences) { + pass(testName); + } else { + fail(testName, "Expected enriched references from NVD"); + } + } else { + // No CVEs found, which is valid + pass(testName + " (no CVEs to enrich)"); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: enrichVulnerability - handles empty results +// ----------------------------------------------------------------------------- +async function testEnrichVulnerability_Empty() { + const testName = "enrichVulnerability: handles packages with no vulnerabilities"; + try { + const results = await enrichVulnerability( + "nonexistent-package-12345", + "npm", + "1.0.0", + ); + + if (Array.isArray(results) && results.length === 0) { + pass(testName); + } else { + fail(testName, `Expected empty array, got ${results.length} results`); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: OSV normalization - extracts severity +// ----------------------------------------------------------------------------- +async function testOSVNormalization_Severity() { + const testName = "OSV normalization: extracts severity correctly"; + try { + // Query real data and check normalization + const results = await queryOSV("lodash", "npm", "4.17.19"); + + if (results.length > 0) { + const vuln = results[0]; + const validSeverities = ["critical", "high", "medium", "low", "info"]; + + if (validSeverities.includes(vuln.severity)) { + pass(testName); + } else { + fail(testName, `Invalid severity: ${vuln.severity}`); + } + } else { + pass(testName + " (no results to test)"); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: OSV normalization - extracts references +// ----------------------------------------------------------------------------- +async function testOSVNormalization_References() { + const testName = "OSV normalization: extracts references"; + try { + const results = await queryOSV("lodash", "npm", "4.17.19"); + + if (results.length > 0) { + const vuln = results[0]; + + if (Array.isArray(vuln.references)) { + // References should be URLs + const allUrls = vuln.references.every((ref) => ref.startsWith("http")); + if (allUrls) { + pass(testName); + } else { + fail(testName, `Non-URL reference found: ${vuln.references.join(", ")}`); + } + } else { + fail(testName, "References is not an array"); + } + } else { + pass(testName + " (no results to test)"); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: OSV normalization - extracts fixed version +// ----------------------------------------------------------------------------- +async function testOSVNormalization_FixedVersion() { + const testName = "OSV normalization: extracts fixed version"; + try { + const results = await queryOSV("lodash", "npm", "4.17.19"); + + if (results.length > 0) { + const hasFixedVersion = results.some((v) => v.fixed_version !== undefined); + + if (hasFixedVersion) { + pass(testName); + } else { + // Some vulnerabilities may not have a fixed version yet + pass(testName + " (no fixed versions available)"); + } + } else { + pass(testName + " (no results to test)"); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: OSV normalization - includes timestamp +// ----------------------------------------------------------------------------- +async function testOSVNormalization_Timestamp() { + const testName = "OSV normalization: includes discovery timestamp"; + try { + const results = await queryOSV("lodash", "npm", "4.17.19"); + + if (results.length > 0) { + const vuln = results[0]; + const iso8601Pattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + + if (vuln.discovered_at && iso8601Pattern.test(vuln.discovered_at)) { + pass(testName); + } else { + fail(testName, `Invalid timestamp: ${vuln.discovered_at}`); + } + } else { + pass(testName + " (no results to test)"); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: Vulnerability structure - required fields present +// ----------------------------------------------------------------------------- +async function testVulnerabilityStructure() { + const testName = "Vulnerability structure: has all required fields"; + try { + const results = await queryOSV("lodash", "npm", "4.17.19"); + + if (results.length > 0) { + const vuln = results[0]; + const hasAllFields = + "id" in vuln && + "source" in vuln && + "severity" in vuln && + "package" in vuln && + "version" in vuln && + "title" in vuln && + "description" in vuln && + "references" in vuln && + "discovered_at" in vuln; + + if (hasAllFields) { + pass(testName); + } else { + fail(testName, `Missing required fields: ${JSON.stringify(vuln)}`); + } + } else { + pass(testName + " (no results to test)"); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: Multiple ecosystems - PyPI support +// ----------------------------------------------------------------------------- +async function testMultipleEcosystems_PyPI() { + const testName = "Multiple ecosystems: PyPI packages"; + try { + // Query a known vulnerable Python package + const results = await queryOSV("requests", "PyPI", "2.6.0"); + + // Verify it returns valid results + if (Array.isArray(results)) { + pass(testName); + } else { + fail(testName, `Expected array, got ${typeof results}`); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: Multiple ecosystems - npm support +// ----------------------------------------------------------------------------- +async function testMultipleEcosystems_npm() { + const testName = "Multiple ecosystems: npm packages"; + try { + const results = await queryOSV("express", "npm"); + + if (Array.isArray(results)) { + pass(testName); + } else { + fail(testName, `Expected array, got ${typeof results}`); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Main test runner +// ----------------------------------------------------------------------------- +async function main() { + console.log("Running CVE integration tests...\n"); + + // OSV API tests + await testQueryOSV_Success(); + await testQueryOSV_NotFound(); + await testQueryOSV_NetworkError(); + await testQueryOSV_WithVersion(); + await testQueryOSV_SeverityNormalization(); + + // NVD API tests + await testQueryNVD_RateLimiting(); + await testQueryNVD_NotFound(); + await testQueryNVD_ValidCVE(); + + // GitHub Advisory tests + await testQueryGitHub_NoToken(); + await testQueryGitHub_Placeholder(); + + // Enrichment tests + await testEnrichVulnerability_OSVOnly(); + await testEnrichVulnerability_WithNVD(); + await testEnrichVulnerability_Empty(); + + // Normalization tests + await testOSVNormalization_Severity(); + await testOSVNormalization_References(); + await testOSVNormalization_FixedVersion(); + await testOSVNormalization_Timestamp(); + + // Structure tests + await testVulnerabilityStructure(); + + // Ecosystem tests + await testMultipleEcosystems_PyPI(); + await testMultipleEcosystems_npm(); + + // Final report + report(); + exitWithResults(); +} + +// Run if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/.agents/skills/clawsec-scanner/test/dast_harness.test.mjs b/.agents/skills/clawsec-scanner/test/dast_harness.test.mjs new file mode 100644 index 00000000..5a87d6bf --- /dev/null +++ b/.agents/skills/clawsec-scanner/test/dast_harness.test.mjs @@ -0,0 +1,250 @@ +#!/usr/bin/env node + +import fs from "node:fs/promises"; +import path from "node:path"; +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { + pass, + fail, + report, + exitWithResults, + createTempDir, +} from "./lib/test_harness.mjs"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const SKILL_ROOT = path.resolve(__dirname, ".."); +const DAST_SCRIPT = path.join(SKILL_ROOT, "scripts", "dast_runner.mjs"); + +/** + * @param {string} targetPath + * @param {number} timeoutMs + * @param {Record} envOverrides + * @returns {Promise<{code: number, stdout: string, stderr: string, report: any}>} + */ +async function runDast(targetPath, timeoutMs = 3000, envOverrides = {}) { + return new Promise((resolve, reject) => { + const proc = spawn( + "node", + [DAST_SCRIPT, "--target", targetPath, "--format", "json", "--timeout", String(timeoutMs)], + { + cwd: SKILL_ROOT, + stdio: ["ignore", "pipe", "pipe"], + env: { + ...process.env, + ...envOverrides, + }, + }, + ); + + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (chunk) => { + stdout += String(chunk); + }); + + proc.stderr.on("data", (chunk) => { + stderr += String(chunk); + }); + + proc.on("error", reject); + + proc.on("close", (code) => { + try { + const parsed = JSON.parse(stdout.trim()); + resolve({ + code: code ?? 1, + stdout, + stderr, + report: parsed, + }); + } catch (error) { + reject(new Error(`Failed to parse DAST JSON output: ${String(error)}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`)); + } + }); + }); +} + +/** + * @param {string} hookDir + * @param {string} eventsLiteral + * @param {string} handlerSource + * @param {string} [handlerFile] + * @returns {Promise} + */ +async function writeHookFixture(hookDir, eventsLiteral, handlerSource, handlerFile = "handler.js") { + await fs.mkdir(hookDir, { recursive: true }); + + const hookMd = `--- +name: ${path.basename(hookDir)} +description: fixture hook +metadata: { "openclaw": { "events": [${eventsLiteral}] } } +--- + +# Fixture Hook +`; + + await fs.writeFile(path.join(hookDir, "HOOK.md"), hookMd, "utf8"); + await fs.writeFile(path.join(hookDir, handlerFile), handlerSource, "utf8"); +} + +async function testSafeHookExecutesAndDoesNotReportMisleadingHigh() { + const testName = "DAST harness: executes real hook and reports no misleading high findings"; + const tmp = await createTempDir(); + + try { + const targetPath = path.join(tmp.path, "skill"); + const hookDir = path.join(targetPath, "hooks", "safe-hook"); + const markerFile = path.join(hookDir, "executed.marker"); + + await writeHookFixture( + hookDir, + '"command:new"', + `import fs from "node:fs/promises"; +import path from "node:path"; + +const handler = async (event, context) => { + const marker = path.join(path.dirname(new URL(import.meta.url).pathname), "executed.marker"); + await fs.writeFile(marker, String(context?.event || "unknown"), "utf8"); + + if (!Array.isArray(event.messages)) { + event.messages = []; + } + + event.messages.push("hook executed"); +}; + +export default handler; +`, + ); + + const result = await runDast(targetPath, 2500); + const markerExists = await fs + .access(markerFile) + .then(() => true) + .catch(() => false); + + const cleanSummary = + result.report?.summary?.critical === 0 + && result.report?.summary?.high === 0 + && result.report?.summary?.medium === 0 + && result.report?.summary?.low === 0 + && result.report?.summary?.info === 0; + + if (result.code === 0 && markerExists && cleanSummary) { + pass(testName); + } else { + fail( + testName, + `Expected exit=0, markerExists=true, clean summary. Got exit=${result.code}, markerExists=${markerExists}, summary=${JSON.stringify(result.report?.summary)} stderr=${result.stderr}`, + ); + } + } catch (error) { + fail(testName, error); + } finally { + await tmp.cleanup(); + } +} + +async function testMaliciousCrashProducesHighFinding() { + const testName = "DAST harness: malicious input crash is reported as high"; + const tmp = await createTempDir(); + + try { + const targetPath = path.join(tmp.path, "skill"); + const hookDir = path.join(targetPath, "hooks", "crashy-hook"); + + await writeHookFixture( + hookDir, + '"message:preprocessed"', + `const handler = async (event) => { + const payload = String(event?.context?.content || ""); + if (payload.includes(" + +

Hello

+ +); +``` + +Fallow marks `static/style.css` and `static/app.js` as reachable. Root-relative paths (starting with `/`) resolve from the source file's parent directory first, then the project root, matching how Vite/Parcel/Hono serve static assets. Only `StringLiteral` attribute values are captured: expression containers (`href={someVar}`) and capitalized React-style components (` + + +``` + +## Custom Components + +Svelte components receive a `portableText` prop with `value`, `global`, and `indexInParent`. Child content is passed via Svelte snippets. + +### Block Styles + +```svelte + + + +{#if value.style === 'h1'} +

{@render children()}

+{:else if value.style === 'h2'} +

{@render children()}

+{:else} +

{@render children()}

+{/if} +``` + +### Custom Types + +```svelte + + + +
+ {value.alt + {#if value.caption} +
{value.caption}
+ {/if} +
+``` + +### Mark Components (Annotations) + +```svelte + + + + + {@render children()} + +``` + +## Assembling Components + +```svelte + + + +``` + +## Passing Context + +Pass external data to all components via `context`: + +```svelte + +``` + +Access in components via `portableText.global.context`. + +## Plain Text Extraction + +```js +import {toPlainText} from '@portabletext/svelte' + +const text = toPlainText(blocks) +``` + +## Reference + +- [@portabletext/svelte](https://github.com/portabletext/svelte-portabletext) +- [Sanity + SvelteKit guide](https://www.sanity.io/guides/sanity-sveltekit) diff --git a/.agents/skills/portable-text-serialization/rules/vue.md b/.agents/skills/portable-text-serialization/rules/vue.md new file mode 100644 index 00000000..a088ea06 --- /dev/null +++ b/.agents/skills/portable-text-serialization/rules/vue.md @@ -0,0 +1,125 @@ +--- +title: Serialize Portable Text to Vue +description: Render Portable Text in Vue 3 and Nuxt using @portabletext/vue +tags: [portable-text, vue, nuxt, serialization, rendering] +--- + +# Serialize Portable Text to Vue + +Use `@portabletext/vue` to render PT in Vue 3 / Nuxt applications. + +```bash +npm install @portabletext/vue +``` + +## Basic Usage + +```vue + + + +``` + +## Custom Components + +Vue components can be defined as render functions, SFCs, or JSX. + +### Render Function Style (Concise) + +```ts +import {h} from 'vue' +import type {PortableTextVueComponents} from '@portabletext/vue' + +const components: PortableTextVueComponents = { + types: { + image: ({value}) => h('img', {src: urlFor(value).width(800).url(), alt: value.alt || ''}), + code: ({value}) => h('pre', {'data-language': value.language}, h('code', value.code)), + }, + + marks: { + link: ({value}, {slots}) => { + const rel = !value?.href?.startsWith('/') ? 'noreferrer noopener' : undefined + return h('a', {href: value?.href, rel}, slots.default?.()) + }, + highlight: (_, {slots}) => h('span', {class: 'bg-yellow-200'}, slots.default?.()), + }, + + block: { + h1: (_, {slots}) => h('h1', {class: 'text-4xl font-bold'}, slots.default?.()), + h2: (_, {slots}) => h('h2', {class: 'text-3xl font-semibold'}, slots.default?.()), + blockquote: (_, {slots}) => h('blockquote', {class: 'border-l-4 pl-4 italic'}, slots.default?.()), + }, + + list: { + bullet: (_, {slots}) => h('ul', {class: 'list-disc ml-6'}, slots.default?.()), + number: (_, {slots}) => h('ol', {class: 'list-decimal ml-6'}, slots.default?.()), + }, +} +``` + +### SFC Style (For Complex Components) + +```vue + + + + +``` + +Then register: + +```ts +import ImageBlock from './ImageBlock.vue' + +const components = { + types: { + image: ImageBlock, + }, +} +``` + +## Props Pattern + +Custom components receive: + +| Prop | Description | +|------|-------------| +| `value` | The block/mark data | +| `index` | Position in parent array | +| `isInline` | Whether this is an inline element | +| `renderNode` | Internal renderer (rarely needed) | + +Children are passed via **slots** (`slots.default?.()`), not props. + +## Plain Text Extraction + +```ts +import {toPlainText} from '@portabletext/vue' + +const text = toPlainText(blocks) +``` + +## Reference + +- [@portabletext/vue](https://github.com/portabletext/vue-portabletext) +- [Sanity + Nuxt guide](https://www.sanity.io/guides/sanity-nuxt) diff --git a/.agents/skills/portfolio-case-study-writer/SKILL.md b/.agents/skills/portfolio-case-study-writer/SKILL.md new file mode 100644 index 00000000..33eb631a --- /dev/null +++ b/.agents/skills/portfolio-case-study-writer/SKILL.md @@ -0,0 +1,369 @@ +--- +name: portfolio-case-study-writer +description: Transform resume bullets into detailed portfolio case studies +--- + +# Portfolio Case Study Writer + +## When to Use This Skill + +Use this skill when the user: +- Wants to create portfolio case studies +- Needs to expand resume bullets into detailed writeups +- Is building a portfolio website +- Wants to showcase project work in depth +- Mentions: "case study", "portfolio", "project writeup", "work samples", "portfolio piece" + +## Core Capabilities + +- Transform resume bullets into detailed case studies +- Structure case studies for maximum impact +- Create compelling project narratives +- Balance technical detail with business context +- Format for portfolio websites +- Tailor depth to audience + +## Case Study Purpose + +**Why Case Studies Matter:** +- Resumes show WHAT you did; case studies show HOW and WHY +- Demonstrate thinking process, not just outcomes +- Allow deeper showcase of skills +- Differentiate you from other candidates +- Required for many PM, UX, and creative roles + +## The Case Study Structure + +### Standard Structure + +``` +1. Overview (Project summary) +2. Problem (What needed to be solved) +3. Process (How you approached it) +4. Solution (What you created/delivered) +5. Results (The impact) +6. Learnings (What you'd do differently) +``` + +### Time to Read +- **Quick Read:** 3-5 minutes (essential for portfolio) +- **Deep Dive:** 10-15 minutes (for interested readers) + +## Section-by-Section Guide + +### 1. Overview Section + +**Purpose:** Hook the reader, provide context + +**Include:** +- Project name and company +- Your role +- Timeline +- Team size +- One-sentence summary of impact + +**Example:** +``` +# Redesigning the Checkout Flow + +**Company:** E-Commerce Inc. +**Role:** Lead Product Designer +**Timeline:** 6 weeks +**Team:** 2 designers, 3 engineers, 1 PM + +**Summary:** Reduced cart abandonment by 35% through a streamlined 3-step checkout process, generating $2M in recovered revenue. +``` + +### 2. Problem Section + +**Purpose:** Set up why this work mattered + +**Include:** +- Business context +- User pain points +- Key metrics or goals +- Constraints + +**Example:** +``` +## The Problem + +E-Commerce Inc. was experiencing 68% cart abandonment—significantly higher than the industry average of 55%. Exit surveys and user research revealed several issues: + +- **Too many steps:** Our checkout had 7 screens +- **Forced account creation:** Users had to register before purchasing +- **Hidden costs:** Shipping wasn't shown until step 5 +- **Mobile friction:** Forms weren't optimized for mobile + +**Goal:** Reduce cart abandonment to below 50% within 3 months. + +**Constraints:** +- No changes to existing payment integrations +- Had to maintain PCI compliance +- 6-week timeline before holiday season +``` + +### 3. Process Section + +**Purpose:** Show your thinking and methodology + +**Include:** +- Research conducted +- Stakeholders involved +- Hypotheses formed +- Options considered +- Decisions made (and why) + +**Example:** +``` +## Process + +### Research +I started by understanding the problem deeply: +- Analyzed Mixpanel funnel data for drop-off points +- Conducted 10 user interviews with recent abandoners +- Reviewed heatmaps and session recordings +- Benchmarked against 5 competitor checkout flows + +**Key Insight:** 73% of drop-offs occurred at the account creation screen. Users wanted to purchase, not commit to a relationship. + +### Ideation +I explored several approaches: +1. Guest checkout only (simplest) +2. Social login options (lower friction) +3. Progressive profiling (collect info over time) +4. One-page checkout (Amazon-style) + +After weighing feasibility, timeline, and impact, we chose a hybrid approach... + +### Decisions Made +- **Guest checkout first:** Made registration optional and post-purchase +- **Transparent pricing:** Showed shipping on the first screen +- **Mobile-first design:** Designed for mobile, then adapted for desktop +- **Progress indicator:** Added clear "Step 1 of 3" indicator +``` + +### 4. Solution Section + +**Purpose:** Show what you actually created + +**Include:** +- Visual artifacts (mockups, screenshots, diagrams) +- Key features/changes +- Technical implementation (if relevant) +- How it addressed the problems + +**Example:** +``` +## Solution + +### The New Checkout Flow + +**Before:** 7 screens with mandatory registration +**After:** 3 screens with optional guest checkout + +[IMAGE: Before/After comparison] + +### Key Changes + +**1. Transparent Pricing Widget** +[IMAGE: Pricing widget mockup] +Showed order total, shipping, and taxes from the start. No surprises. + +**2. Guest Checkout Option** +[IMAGE: Guest checkout screen] +Made account creation optional with clear value proposition for why to register. + +**3. Smart Form Design** +[IMAGE: Form design] +- Single-column layout on mobile +- Auto-format for phone/card numbers +- Address autocomplete integration +- Clear error messaging + +**4. Trust Signals** +Added security badges, money-back guarantee, and customer service contact throughout the flow. +``` + +### 5. Results Section + +**Purpose:** Prove impact with data + +**Include:** +- Quantitative results (with timeframe) +- Comparison to goals +- Secondary metrics affected +- Business impact + +**Example:** +``` +## Results + +### Primary Metrics (90 days post-launch) + +| Metric | Before | After | Change | +|--------|--------|-------|--------| +| Cart Abandonment | 68% | 44% | -35% | +| Checkout Completion | 32% | 56% | +75% | +| Mobile Conversion | 18% | 41% | +128% | +| Revenue per Visitor | $2.40 | $3.85 | +60% | + +### Business Impact +- **$2M additional revenue** in first quarter +- **15% increase in mobile orders** +- **Customer support tickets about checkout** dropped by 45% + +### Secondary Effects +- Account creation actually increased 20% (post-purchase) +- Average order value stayed stable +- Return customer rate improved +``` + +### 6. Learnings Section + +**Purpose:** Show growth mindset and self-awareness + +**Include:** +- What worked well +- What you'd do differently +- Unexpected challenges +- Skills developed + +**Example:** +``` +## Learnings + +### What Worked +- **Early user research** prevented us from building the wrong solution +- **Cross-functional alignment** meetings kept everyone on the same page +- **Launching with analytics** let us measure impact immediately + +### What I'd Do Differently +- **More A/B testing:** We launched the full redesign at once. Would have preferred to test individual changes to understand what drove results. +- **Earlier mobile focus:** We designed desktop-first then adapted. Starting mobile-first would have been more efficient. +- **Stakeholder education:** Spent too long convincing leadership. Would start stakeholder alignment earlier next time. + +### Skills Developed +- Advanced Figma prototyping +- Working with A/B testing frameworks +- Presenting data-driven design decisions to executives +``` + +## Case Study Types by Role + +### Product Manager Case Study +**Focus on:** +- Strategy and prioritization +- Stakeholder management +- Metrics and outcomes +- Technical trade-offs + +### UX/Product Designer Case Study +**Focus on:** +- User research +- Design process +- Visual artifacts +- Usability improvements + +### Software Engineer Case Study +**Focus on:** +- Technical architecture +- Problem-solving approach +- System design +- Code quality/performance + +### Marketing Case Study +**Focus on:** +- Strategy and targeting +- Creative execution +- Channel performance +- ROI and attribution + +## Visual Elements + +### Must-Have Visuals +- Before/after comparisons +- Key screens or deliverables +- Process diagrams +- Results charts + +### Nice-to-Have Visuals +- User journey maps +- Wireframes evolution +- Research artifacts +- Team photos + +### Visual Tips +- Use consistent image sizing +- Add captions explaining each image +- Blur sensitive data if needed +- Ensure mobile-friendly image sizes + +## Output Format + +When creating a case study: + +```markdown +# CASE STUDY: [PROJECT NAME] + +## Quick Facts +- **Role:** [Your role] +- **Company:** [Company] +- **Timeline:** [Duration] +- **Team:** [Team composition] +- **Impact:** [One-line result] + +--- + +## Overview +[2-3 sentence summary of the project] + +## Problem +[Context and challenges - what needed to be solved] + +## Process +### Research +[What you learned] + +### Approach +[How you tackled it] + +### Key Decisions +[Important choices and rationale] + +## Solution +[What you built/created - include visual descriptions] + +### Feature 1 +[Description] + +### Feature 2 +[Description] + +## Results +[Quantified impact] + +| Metric | Before | After | Change | +|--------|--------|-------|--------| + +## Learnings +[Reflections and growth] + +--- + +## Visual Asset List +[List of images/screenshots needed] +``` + +## Case Study Quality Checklist + +- ✅ Clear problem statement +- ✅ Evidence of user/customer focus +- ✅ Process clearly explained +- ✅ Your specific contributions are clear +- ✅ Quantified results +- ✅ Visual artifacts included +- ✅ Honest about challenges/learnings +- ✅ Appropriate length (3-10 min read) +- ✅ Proofread and polished +- ✅ Can discuss in detail in interview diff --git a/.agents/skills/pptx/LICENSE.txt b/.agents/skills/pptx/LICENSE.txt new file mode 100644 index 00000000..c55ab422 --- /dev/null +++ b/.agents/skills/pptx/LICENSE.txt @@ -0,0 +1,30 @@ +© 2025 Anthropic, PBC. All rights reserved. + +LICENSE: Use of these materials (including all code, prompts, assets, files, +and other components of this Skill) is governed by your agreement with +Anthropic regarding use of Anthropic's services. If no separate agreement +exists, use is governed by Anthropic's Consumer Terms of Service or +Commercial Terms of Service, as applicable: +https://www.anthropic.com/legal/consumer-terms +https://www.anthropic.com/legal/commercial-terms +Your applicable agreement is referred to as the "Agreement." "Services" are +as defined in the Agreement. + +ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the +contrary, users may not: + +- Extract these materials from the Services or retain copies of these + materials outside the Services +- Reproduce or copy these materials, except for temporary copies created + automatically during authorized use of the Services +- Create derivative works based on these materials +- Distribute, sublicense, or transfer these materials to any third party +- Make, offer to sell, sell, or import any inventions embodied in these + materials +- Reverse engineer, decompile, or disassemble these materials + +The receipt, viewing, or possession of these materials does not convey or +imply any license or right beyond those expressly granted above. + +Anthropic retains all right, title, and interest in these materials, +including all copyrights, patents, and other intellectual property rights. diff --git a/.agents/skills/pptx/SKILL.md b/.agents/skills/pptx/SKILL.md new file mode 100644 index 00000000..df5000e1 --- /dev/null +++ b/.agents/skills/pptx/SKILL.md @@ -0,0 +1,232 @@ +--- +name: pptx +description: "Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill." +license: Proprietary. LICENSE.txt has complete terms +--- + +# PPTX Skill + +## Quick Reference + +| Task | Guide | +|------|-------| +| Read/analyze content | `python -m markitdown presentation.pptx` | +| Edit or create from template | Read [editing.md](editing.md) | +| Create from scratch | Read [pptxgenjs.md](pptxgenjs.md) | + +--- + +## Reading Content + +```bash +# Text extraction +python -m markitdown presentation.pptx + +# Visual overview +python scripts/thumbnail.py presentation.pptx + +# Raw XML +python scripts/office/unpack.py presentation.pptx unpacked/ +``` + +--- + +## Editing Workflow + +**Read [editing.md](editing.md) for full details.** + +1. Analyze template with `thumbnail.py` +2. Unpack → manipulate slides → edit content → clean → pack + +--- + +## Creating from Scratch + +**Read [pptxgenjs.md](pptxgenjs.md) for full details.** + +Use when no template or reference presentation is available. + +--- + +## Design Ideas + +**Don't create boring slides.** Plain bullets on a white background won't impress anyone. Consider ideas from this list for each slide. + +### Before Starting + +- **Pick a bold, content-informed color palette**: The palette should feel designed for THIS topic. If swapping your colors into a completely different presentation would still "work," you haven't made specific enough choices. +- **Dominance over equality**: One color should dominate (60-70% visual weight), with 1-2 supporting tones and one sharp accent. Never give all colors equal weight. +- **Dark/light contrast**: Dark backgrounds for title + conclusion slides, light for content ("sandwich" structure). Or commit to dark throughout for a premium feel. +- **Commit to a visual motif**: Pick ONE distinctive element and repeat it — rounded image frames, icons in colored circles, thick single-side borders. Carry it across every slide. + +### Color Palettes + +Choose colors that match your topic — don't default to generic blue. Use these palettes as inspiration: + +| Theme | Primary | Secondary | Accent | +|-------|---------|-----------|--------| +| **Midnight Executive** | `1E2761` (navy) | `CADCFC` (ice blue) | `FFFFFF` (white) | +| **Forest & Moss** | `2C5F2D` (forest) | `97BC62` (moss) | `F5F5F5` (cream) | +| **Coral Energy** | `F96167` (coral) | `F9E795` (gold) | `2F3C7E` (navy) | +| **Warm Terracotta** | `B85042` (terracotta) | `E7E8D1` (sand) | `A7BEAE` (sage) | +| **Ocean Gradient** | `065A82` (deep blue) | `1C7293` (teal) | `21295C` (midnight) | +| **Charcoal Minimal** | `36454F` (charcoal) | `F2F2F2` (off-white) | `212121` (black) | +| **Teal Trust** | `028090` (teal) | `00A896` (seafoam) | `02C39A` (mint) | +| **Berry & Cream** | `6D2E46` (berry) | `A26769` (dusty rose) | `ECE2D0` (cream) | +| **Sage Calm** | `84B59F` (sage) | `69A297` (eucalyptus) | `50808E` (slate) | +| **Cherry Bold** | `990011` (cherry) | `FCF6F5` (off-white) | `2F3C7E` (navy) | + +### For Each Slide + +**Every slide needs a visual element** — image, chart, icon, or shape. Text-only slides are forgettable. + +**Layout options:** +- Two-column (text left, illustration on right) +- Icon + text rows (icon in colored circle, bold header, description below) +- 2x2 or 2x3 grid (image on one side, grid of content blocks on other) +- Half-bleed image (full left or right side) with content overlay + +**Data display:** +- Large stat callouts (big numbers 60-72pt with small labels below) +- Comparison columns (before/after, pros/cons, side-by-side options) +- Timeline or process flow (numbered steps, arrows) + +**Visual polish:** +- Icons in small colored circles next to section headers +- Italic accent text for key stats or taglines + +### Typography + +**Choose an interesting font pairing** — don't default to Arial. Pick a header font with personality and pair it with a clean body font. + +| Header Font | Body Font | +|-------------|-----------| +| Georgia | Calibri | +| Arial Black | Arial | +| Calibri | Calibri Light | +| Cambria | Calibri | +| Trebuchet MS | Calibri | +| Impact | Arial | +| Palatino | Garamond | +| Consolas | Calibri | + +| Element | Size | +|---------|------| +| Slide title | 36-44pt bold | +| Section header | 20-24pt bold | +| Body text | 14-16pt | +| Captions | 10-12pt muted | + +### Spacing + +- 0.5" minimum margins +- 0.3-0.5" between content blocks +- Leave breathing room—don't fill every inch + +### Avoid (Common Mistakes) + +- **Don't repeat the same layout** — vary columns, cards, and callouts across slides +- **Don't center body text** — left-align paragraphs and lists; center only titles +- **Don't skimp on size contrast** — titles need 36pt+ to stand out from 14-16pt body +- **Don't default to blue** — pick colors that reflect the specific topic +- **Don't mix spacing randomly** — choose 0.3" or 0.5" gaps and use consistently +- **Don't style one slide and leave the rest plain** — commit fully or keep it simple throughout +- **Don't create text-only slides** — add images, icons, charts, or visual elements; avoid plain title + bullets +- **Don't forget text box padding** — when aligning lines or shapes with text edges, set `margin: 0` on the text box or offset the shape to account for padding +- **Don't use low-contrast elements** — icons AND text need strong contrast against the background; avoid light text on light backgrounds or dark text on dark backgrounds +- **NEVER use accent lines under titles** — these are a hallmark of AI-generated slides; use whitespace or background color instead + +--- + +## QA (Required) + +**Assume there are problems. Your job is to find them.** + +Your first render is almost never correct. Approach QA as a bug hunt, not a confirmation step. If you found zero issues on first inspection, you weren't looking hard enough. + +### Content QA + +```bash +python -m markitdown output.pptx +``` + +Check for missing content, typos, wrong order. + +**When using templates, check for leftover placeholder text:** + +```bash +python -m markitdown output.pptx | grep -iE "xxxx|lorem|ipsum|this.*(page|slide).*layout" +``` + +If grep returns results, fix them before declaring success. + +### Visual QA + +**⚠️ USE SUBAGENTS** — even for 2-3 slides. You've been staring at the code and will see what you expect, not what's there. Subagents have fresh eyes. + +Convert slides to images (see [Converting to Images](#converting-to-images)), then use this prompt: + +``` +Visually inspect these slides. Assume there are issues — find them. + +Look for: +- Overlapping elements (text through shapes, lines through words, stacked elements) +- Text overflow or cut off at edges/box boundaries +- Decorative lines positioned for single-line text but title wrapped to two lines +- Source citations or footers colliding with content above +- Elements too close (< 0.3" gaps) or cards/sections nearly touching +- Uneven gaps (large empty area in one place, cramped in another) +- Insufficient margin from slide edges (< 0.5") +- Columns or similar elements not aligned consistently +- Low-contrast text (e.g., light gray text on cream-colored background) +- Low-contrast icons (e.g., dark icons on dark backgrounds without a contrasting circle) +- Text boxes too narrow causing excessive wrapping +- Leftover placeholder content + +For each slide, list issues or areas of concern, even if minor. + +Read and analyze these images: +1. /path/to/slide-01.jpg (Expected: [brief description]) +2. /path/to/slide-02.jpg (Expected: [brief description]) + +Report ALL issues found, including minor ones. +``` + +### Verification Loop + +1. Generate slides → Convert to images → Inspect +2. **List issues found** (if none found, look again more critically) +3. Fix issues +4. **Re-verify affected slides** — one fix often creates another problem +5. Repeat until a full pass reveals no new issues + +**Do not declare success until you've completed at least one fix-and-verify cycle.** + +--- + +## Converting to Images + +Convert presentations to individual slide images for visual inspection: + +```bash +python scripts/office/soffice.py --headless --convert-to pdf output.pptx +pdftoppm -jpeg -r 150 output.pdf slide +``` + +This creates `slide-01.jpg`, `slide-02.jpg`, etc. + +To re-render specific slides after fixes: + +```bash +pdftoppm -jpeg -r 150 -f N -l N output.pdf slide-fixed +``` + +--- + +## Dependencies + +- `pip install "markitdown[pptx]"` - text extraction +- `pip install Pillow` - thumbnail grids +- `npm install -g pptxgenjs` - creating from scratch +- LibreOffice (`soffice`) - PDF conversion (auto-configured for sandboxed environments via `scripts/office/soffice.py`) +- Poppler (`pdftoppm`) - PDF to images diff --git a/.agents/skills/pptx/editing.md b/.agents/skills/pptx/editing.md new file mode 100644 index 00000000..f873e8a0 --- /dev/null +++ b/.agents/skills/pptx/editing.md @@ -0,0 +1,205 @@ +# Editing Presentations + +## Template-Based Workflow + +When using an existing presentation as a template: + +1. **Analyze existing slides**: + ```bash + python scripts/thumbnail.py template.pptx + python -m markitdown template.pptx + ``` + Review `thumbnails.jpg` to see layouts, and markitdown output to see placeholder text. + +2. **Plan slide mapping**: For each content section, choose a template slide. + + ⚠️ **USE VARIED LAYOUTS** — monotonous presentations are a common failure mode. Don't default to basic title + bullet slides. Actively seek out: + - Multi-column layouts (2-column, 3-column) + - Image + text combinations + - Full-bleed images with text overlay + - Quote or callout slides + - Section dividers + - Stat/number callouts + - Icon grids or icon + text rows + + **Avoid:** Repeating the same text-heavy layout for every slide. + + Match content type to layout style (e.g., key points → bullet slide, team info → multi-column, testimonials → quote slide). + +3. **Unpack**: `python scripts/office/unpack.py template.pptx unpacked/` + +4. **Build presentation** (do this yourself, not with subagents): + - Delete unwanted slides (remove from ``) + - Duplicate slides you want to reuse (`add_slide.py`) + - Reorder slides in `` + - **Complete all structural changes before step 5** + +5. **Edit content**: Update text in each `slide{N}.xml`. + **Use subagents here if available** — slides are separate XML files, so subagents can edit in parallel. + +6. **Clean**: `python scripts/clean.py unpacked/` + +7. **Pack**: `python scripts/office/pack.py unpacked/ output.pptx --original template.pptx` + +--- + +## Scripts + +| Script | Purpose | +|--------|---------| +| `unpack.py` | Extract and pretty-print PPTX | +| `add_slide.py` | Duplicate slide or create from layout | +| `clean.py` | Remove orphaned files | +| `pack.py` | Repack with validation | +| `thumbnail.py` | Create visual grid of slides | + +### unpack.py + +```bash +python scripts/office/unpack.py input.pptx unpacked/ +``` + +Extracts PPTX, pretty-prints XML, escapes smart quotes. + +### add_slide.py + +```bash +python scripts/add_slide.py unpacked/ slide2.xml # Duplicate slide +python scripts/add_slide.py unpacked/ slideLayout2.xml # From layout +``` + +Prints `` to add to `` at desired position. + +### clean.py + +```bash +python scripts/clean.py unpacked/ +``` + +Removes slides not in ``, unreferenced media, orphaned rels. + +### pack.py + +```bash +python scripts/office/pack.py unpacked/ output.pptx --original input.pptx +``` + +Validates, repairs, condenses XML, re-encodes smart quotes. + +### thumbnail.py + +```bash +python scripts/thumbnail.py input.pptx [output_prefix] [--cols N] +``` + +Creates `thumbnails.jpg` with slide filenames as labels. Default 3 columns, max 12 per grid. + +**Use for template analysis only** (choosing layouts). For visual QA, use `soffice` + `pdftoppm` to create full-resolution individual slide images—see SKILL.md. + +--- + +## Slide Operations + +Slide order is in `ppt/presentation.xml` → ``. + +**Reorder**: Rearrange `` elements. + +**Delete**: Remove ``, then run `clean.py`. + +**Add**: Use `add_slide.py`. Never manually copy slide files—the script handles notes references, Content_Types.xml, and relationship IDs that manual copying misses. + +--- + +## Editing Content + +**Subagents:** If available, use them here (after completing step 4). Each slide is a separate XML file, so subagents can edit in parallel. In your prompt to subagents, include: +- The slide file path(s) to edit +- **"Use the Edit tool for all changes"** +- The formatting rules and common pitfalls below + +For each slide: +1. Read the slide's XML +2. Identify ALL placeholder content—text, images, charts, icons, captions +3. Replace each placeholder with final content + +**Use the Edit tool, not sed or Python scripts.** The Edit tool forces specificity about what to replace and where, yielding better reliability. + +### Formatting Rules + +- **Bold all headers, subheadings, and inline labels**: Use `b="1"` on ``. This includes: + - Slide titles + - Section headers within a slide + - Inline labels like (e.g.: "Status:", "Description:") at the start of a line +- **Never use unicode bullets (•)**: Use proper list formatting with `` or `` +- **Bullet consistency**: Let bullets inherit from the layout. Only specify `` or ``. + +--- + +## Common Pitfalls + +### Template Adaptation + +When source content has fewer items than the template: +- **Remove excess elements entirely** (images, shapes, text boxes), don't just clear text +- Check for orphaned visuals after clearing text content +- Run visual QA to catch mismatched counts + +When replacing text with different length content: +- **Shorter replacements**: Usually safe +- **Longer replacements**: May overflow or wrap unexpectedly +- Test with visual QA after text changes +- Consider truncating or splitting content to fit the template's design constraints + +**Template slots ≠ Source items**: If template has 4 team members but source has 3 users, delete the 4th member's entire group (image + text boxes), not just the text. + +### Multi-Item Content + +If source has multiple items (numbered lists, multiple sections), create separate `` elements for each — **never concatenate into one string**. + +**❌ WRONG** — all items in one paragraph: +```xml + + Step 1: Do the first thing. Step 2: Do the second thing. + +``` + +**✅ CORRECT** — separate paragraphs with bold headers: +```xml + + + Step 1 + + + + Do the first thing. + + + + Step 2 + + +``` + +Copy `` from the original paragraph to preserve line spacing. Use `b="1"` on headers. + +### Smart Quotes + +Handled automatically by unpack/pack. But the Edit tool converts smart quotes to ASCII. + +**When adding new text with quotes, use XML entities:** + +```xml +the “Agreement” +``` + +| Character | Name | Unicode | XML Entity | +|-----------|------|---------|------------| +| `“` | Left double quote | U+201C | `“` | +| `”` | Right double quote | U+201D | `”` | +| `‘` | Left single quote | U+2018 | `‘` | +| `’` | Right single quote | U+2019 | `’` | + +### Other + +- **Whitespace**: Use `xml:space="preserve"` on `` with leading/trailing spaces +- **XML parsing**: Use `defusedxml.minidom`, not `xml.etree.ElementTree` (corrupts namespaces) diff --git a/.agents/skills/pptx/pptxgenjs.md b/.agents/skills/pptx/pptxgenjs.md new file mode 100644 index 00000000..6bfed908 --- /dev/null +++ b/.agents/skills/pptx/pptxgenjs.md @@ -0,0 +1,420 @@ +# PptxGenJS Tutorial + +## Setup & Basic Structure + +```javascript +const pptxgen = require("pptxgenjs"); + +let pres = new pptxgen(); +pres.layout = 'LAYOUT_16x9'; // or 'LAYOUT_16x10', 'LAYOUT_4x3', 'LAYOUT_WIDE' +pres.author = 'Your Name'; +pres.title = 'Presentation Title'; + +let slide = pres.addSlide(); +slide.addText("Hello World!", { x: 0.5, y: 0.5, fontSize: 36, color: "363636" }); + +pres.writeFile({ fileName: "Presentation.pptx" }); +``` + +## Layout Dimensions + +Slide dimensions (coordinates in inches): +- `LAYOUT_16x9`: 10" × 5.625" (default) +- `LAYOUT_16x10`: 10" × 6.25" +- `LAYOUT_4x3`: 10" × 7.5" +- `LAYOUT_WIDE`: 13.3" × 7.5" + +--- + +## Text & Formatting + +```javascript +// Basic text +slide.addText("Simple Text", { + x: 1, y: 1, w: 8, h: 2, fontSize: 24, fontFace: "Arial", + color: "363636", bold: true, align: "center", valign: "middle" +}); + +// Character spacing (use charSpacing, not letterSpacing which is silently ignored) +slide.addText("SPACED TEXT", { x: 1, y: 1, w: 8, h: 1, charSpacing: 6 }); + +// Rich text arrays +slide.addText([ + { text: "Bold ", options: { bold: true } }, + { text: "Italic ", options: { italic: true } } +], { x: 1, y: 3, w: 8, h: 1 }); + +// Multi-line text (requires breakLine: true) +slide.addText([ + { text: "Line 1", options: { breakLine: true } }, + { text: "Line 2", options: { breakLine: true } }, + { text: "Line 3" } // Last item doesn't need breakLine +], { x: 0.5, y: 0.5, w: 8, h: 2 }); + +// Text box margin (internal padding) +slide.addText("Title", { + x: 0.5, y: 0.3, w: 9, h: 0.6, + margin: 0 // Use 0 when aligning text with other elements like shapes or icons +}); +``` + +**Tip:** Text boxes have internal margin by default. Set `margin: 0` when you need text to align precisely with shapes, lines, or icons at the same x-position. + +--- + +## Lists & Bullets + +```javascript +// ✅ CORRECT: Multiple bullets +slide.addText([ + { text: "First item", options: { bullet: true, breakLine: true } }, + { text: "Second item", options: { bullet: true, breakLine: true } }, + { text: "Third item", options: { bullet: true } } +], { x: 0.5, y: 0.5, w: 8, h: 3 }); + +// ❌ WRONG: Never use unicode bullets +slide.addText("• First item", { ... }); // Creates double bullets + +// Sub-items and numbered lists +{ text: "Sub-item", options: { bullet: true, indentLevel: 1 } } +{ text: "First", options: { bullet: { type: "number" }, breakLine: true } } +``` + +--- + +## Shapes + +```javascript +slide.addShape(pres.shapes.RECTANGLE, { + x: 0.5, y: 0.8, w: 1.5, h: 3.0, + fill: { color: "FF0000" }, line: { color: "000000", width: 2 } +}); + +slide.addShape(pres.shapes.OVAL, { x: 4, y: 1, w: 2, h: 2, fill: { color: "0000FF" } }); + +slide.addShape(pres.shapes.LINE, { + x: 1, y: 3, w: 5, h: 0, line: { color: "FF0000", width: 3, dashType: "dash" } +}); + +// With transparency +slide.addShape(pres.shapes.RECTANGLE, { + x: 1, y: 1, w: 3, h: 2, + fill: { color: "0088CC", transparency: 50 } +}); + +// Rounded rectangle (rectRadius only works with ROUNDED_RECTANGLE, not RECTANGLE) +// ⚠️ Don't pair with rectangular accent overlays — they won't cover rounded corners. Use RECTANGLE instead. +slide.addShape(pres.shapes.ROUNDED_RECTANGLE, { + x: 1, y: 1, w: 3, h: 2, + fill: { color: "FFFFFF" }, rectRadius: 0.1 +}); + +// With shadow +slide.addShape(pres.shapes.RECTANGLE, { + x: 1, y: 1, w: 3, h: 2, + fill: { color: "FFFFFF" }, + shadow: { type: "outer", color: "000000", blur: 6, offset: 2, angle: 135, opacity: 0.15 } +}); +``` + +Shadow options: + +| Property | Type | Range | Notes | +|----------|------|-------|-------| +| `type` | string | `"outer"`, `"inner"` | | +| `color` | string | 6-char hex (e.g. `"000000"`) | No `#` prefix, no 8-char hex — see Common Pitfalls | +| `blur` | number | 0-100 pt | | +| `offset` | number | 0-200 pt | **Must be non-negative** — negative values corrupt the file | +| `angle` | number | 0-359 degrees | Direction the shadow falls (135 = bottom-right, 270 = upward) | +| `opacity` | number | 0.0-1.0 | Use this for transparency, never encode in color string | + +To cast a shadow upward (e.g. on a footer bar), use `angle: 270` with a positive offset — do **not** use a negative offset. + +**Note**: Gradient fills are not natively supported. Use a gradient image as a background instead. + +--- + +## Images + +### Image Sources + +```javascript +// From file path +slide.addImage({ path: "images/chart.png", x: 1, y: 1, w: 5, h: 3 }); + +// From URL +slide.addImage({ path: "https://example.com/image.jpg", x: 1, y: 1, w: 5, h: 3 }); + +// From base64 (faster, no file I/O) +slide.addImage({ data: "image/png;base64,iVBORw0KGgo...", x: 1, y: 1, w: 5, h: 3 }); +``` + +### Image Options + +```javascript +slide.addImage({ + path: "image.png", + x: 1, y: 1, w: 5, h: 3, + rotate: 45, // 0-359 degrees + rounding: true, // Circular crop + transparency: 50, // 0-100 + flipH: true, // Horizontal flip + flipV: false, // Vertical flip + altText: "Description", // Accessibility + hyperlink: { url: "https://example.com" } +}); +``` + +### Image Sizing Modes + +```javascript +// Contain - fit inside, preserve ratio +{ sizing: { type: 'contain', w: 4, h: 3 } } + +// Cover - fill area, preserve ratio (may crop) +{ sizing: { type: 'cover', w: 4, h: 3 } } + +// Crop - cut specific portion +{ sizing: { type: 'crop', x: 0.5, y: 0.5, w: 2, h: 2 } } +``` + +### Calculate Dimensions (preserve aspect ratio) + +```javascript +const origWidth = 1978, origHeight = 923, maxHeight = 3.0; +const calcWidth = maxHeight * (origWidth / origHeight); +const centerX = (10 - calcWidth) / 2; + +slide.addImage({ path: "image.png", x: centerX, y: 1.2, w: calcWidth, h: maxHeight }); +``` + +### Supported Formats + +- **Standard**: PNG, JPG, GIF (animated GIFs work in Microsoft 365) +- **SVG**: Works in modern PowerPoint/Microsoft 365 + +--- + +## Icons + +Use react-icons to generate SVG icons, then rasterize to PNG for universal compatibility. + +### Setup + +```javascript +const React = require("react"); +const ReactDOMServer = require("react-dom/server"); +const sharp = require("sharp"); +const { FaCheckCircle, FaChartLine } = require("react-icons/fa"); + +function renderIconSvg(IconComponent, color = "#000000", size = 256) { + return ReactDOMServer.renderToStaticMarkup( + React.createElement(IconComponent, { color, size: String(size) }) + ); +} + +async function iconToBase64Png(IconComponent, color, size = 256) { + const svg = renderIconSvg(IconComponent, color, size); + const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer(); + return "image/png;base64," + pngBuffer.toString("base64"); +} +``` + +### Add Icon to Slide + +```javascript +const iconData = await iconToBase64Png(FaCheckCircle, "#4472C4", 256); + +slide.addImage({ + data: iconData, + x: 1, y: 1, w: 0.5, h: 0.5 // Size in inches +}); +``` + +**Note**: Use size 256 or higher for crisp icons. The size parameter controls the rasterization resolution, not the display size on the slide (which is set by `w` and `h` in inches). + +### Icon Libraries + +Install: `npm install -g react-icons react react-dom sharp` + +Popular icon sets in react-icons: +- `react-icons/fa` - Font Awesome +- `react-icons/md` - Material Design +- `react-icons/hi` - Heroicons +- `react-icons/bi` - Bootstrap Icons + +--- + +## Slide Backgrounds + +```javascript +// Solid color +slide.background = { color: "F1F1F1" }; + +// Color with transparency +slide.background = { color: "FF3399", transparency: 50 }; + +// Image from URL +slide.background = { path: "https://example.com/bg.jpg" }; + +// Image from base64 +slide.background = { data: "image/png;base64,iVBORw0KGgo..." }; +``` + +--- + +## Tables + +```javascript +slide.addTable([ + ["Header 1", "Header 2"], + ["Cell 1", "Cell 2"] +], { + x: 1, y: 1, w: 8, h: 2, + border: { pt: 1, color: "999999" }, fill: { color: "F1F1F1" } +}); + +// Advanced with merged cells +let tableData = [ + [{ text: "Header", options: { fill: { color: "6699CC" }, color: "FFFFFF", bold: true } }, "Cell"], + [{ text: "Merged", options: { colspan: 2 } }] +]; +slide.addTable(tableData, { x: 1, y: 3.5, w: 8, colW: [4, 4] }); +``` + +--- + +## Charts + +```javascript +// Bar chart +slide.addChart(pres.charts.BAR, [{ + name: "Sales", labels: ["Q1", "Q2", "Q3", "Q4"], values: [4500, 5500, 6200, 7100] +}], { + x: 0.5, y: 0.6, w: 6, h: 3, barDir: 'col', + showTitle: true, title: 'Quarterly Sales' +}); + +// Line chart +slide.addChart(pres.charts.LINE, [{ + name: "Temp", labels: ["Jan", "Feb", "Mar"], values: [32, 35, 42] +}], { x: 0.5, y: 4, w: 6, h: 3, lineSize: 3, lineSmooth: true }); + +// Pie chart +slide.addChart(pres.charts.PIE, [{ + name: "Share", labels: ["A", "B", "Other"], values: [35, 45, 20] +}], { x: 7, y: 1, w: 5, h: 4, showPercent: true }); +``` + +### Better-Looking Charts + +Default charts look dated. Apply these options for a modern, clean appearance: + +```javascript +slide.addChart(pres.charts.BAR, chartData, { + x: 0.5, y: 1, w: 9, h: 4, barDir: "col", + + // Custom colors (match your presentation palette) + chartColors: ["0D9488", "14B8A6", "5EEAD4"], + + // Clean background + chartArea: { fill: { color: "FFFFFF" }, roundedCorners: true }, + + // Muted axis labels + catAxisLabelColor: "64748B", + valAxisLabelColor: "64748B", + + // Subtle grid (value axis only) + valGridLine: { color: "E2E8F0", size: 0.5 }, + catGridLine: { style: "none" }, + + // Data labels on bars + showValue: true, + dataLabelPosition: "outEnd", + dataLabelColor: "1E293B", + + // Hide legend for single series + showLegend: false, +}); +``` + +**Key styling options:** +- `chartColors: [...]` - hex colors for series/segments +- `chartArea: { fill, border, roundedCorners }` - chart background +- `catGridLine/valGridLine: { color, style, size }` - grid lines (`style: "none"` to hide) +- `lineSmooth: true` - curved lines (line charts) +- `legendPos: "r"` - legend position: "b", "t", "l", "r", "tr" + +--- + +## Slide Masters + +```javascript +pres.defineSlideMaster({ + title: 'TITLE_SLIDE', background: { color: '283A5E' }, + objects: [{ + placeholder: { options: { name: 'title', type: 'title', x: 1, y: 2, w: 8, h: 2 } } + }] +}); + +let titleSlide = pres.addSlide({ masterName: "TITLE_SLIDE" }); +titleSlide.addText("My Title", { placeholder: "title" }); +``` + +--- + +## Common Pitfalls + +⚠️ These issues cause file corruption, visual bugs, or broken output. Avoid them. + +1. **NEVER use "#" with hex colors** - causes file corruption + ```javascript + color: "FF0000" // ✅ CORRECT + color: "#FF0000" // ❌ WRONG + ``` + +2. **NEVER encode opacity in hex color strings** - 8-char colors (e.g., `"00000020"`) corrupt the file. Use the `opacity` property instead. + ```javascript + shadow: { type: "outer", blur: 6, offset: 2, color: "00000020" } // ❌ CORRUPTS FILE + shadow: { type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.12 } // ✅ CORRECT + ``` + +3. **Use `bullet: true`** - NEVER unicode symbols like "•" (creates double bullets) + +4. **Use `breakLine: true`** between array items or text runs together + +5. **Avoid `lineSpacing` with bullets** - causes excessive gaps; use `paraSpaceAfter` instead + +6. **Each presentation needs fresh instance** - don't reuse `pptxgen()` objects + +7. **NEVER reuse option objects across calls** - PptxGenJS mutates objects in-place (e.g. converting shadow values to EMU). Sharing one object between multiple calls corrupts the second shape. + ```javascript + const shadow = { type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.15 }; + slide.addShape(pres.shapes.RECTANGLE, { shadow, ... }); // ❌ second call gets already-converted values + slide.addShape(pres.shapes.RECTANGLE, { shadow, ... }); + + const makeShadow = () => ({ type: "outer", blur: 6, offset: 2, color: "000000", opacity: 0.15 }); + slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... }); // ✅ fresh object each time + slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... }); + ``` + +8. **Don't use `ROUNDED_RECTANGLE` with accent borders** - rectangular overlay bars won't cover rounded corners. Use `RECTANGLE` instead. + ```javascript + // ❌ WRONG: Accent bar doesn't cover rounded corners + slide.addShape(pres.shapes.ROUNDED_RECTANGLE, { x: 1, y: 1, w: 3, h: 1.5, fill: { color: "FFFFFF" } }); + slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 0.08, h: 1.5, fill: { color: "0891B2" } }); + + // ✅ CORRECT: Use RECTANGLE for clean alignment + slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 3, h: 1.5, fill: { color: "FFFFFF" } }); + slide.addShape(pres.shapes.RECTANGLE, { x: 1, y: 1, w: 0.08, h: 1.5, fill: { color: "0891B2" } }); + ``` + +--- + +## Quick Reference + +- **Shapes**: RECTANGLE, OVAL, LINE, ROUNDED_RECTANGLE +- **Charts**: BAR, LINE, PIE, DOUGHNUT, SCATTER, BUBBLE, RADAR +- **Layouts**: LAYOUT_16x9 (10"×5.625"), LAYOUT_16x10, LAYOUT_4x3, LAYOUT_WIDE +- **Alignment**: "left", "center", "right" +- **Chart data labels**: "outEnd", "inEnd", "center" diff --git a/.agents/skills/pptx/scripts/__init__.py b/.agents/skills/pptx/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/.agents/skills/pptx/scripts/add_slide.py b/.agents/skills/pptx/scripts/add_slide.py new file mode 100755 index 00000000..13700df0 --- /dev/null +++ b/.agents/skills/pptx/scripts/add_slide.py @@ -0,0 +1,195 @@ +"""Add a new slide to an unpacked PPTX directory. + +Usage: python add_slide.py + +The source can be: + - A slide file (e.g., slide2.xml) - duplicates the slide + - A layout file (e.g., slideLayout2.xml) - creates from layout + +Examples: + python add_slide.py unpacked/ slide2.xml + # Duplicates slide2, creates slide5.xml + + python add_slide.py unpacked/ slideLayout2.xml + # Creates slide5.xml from slideLayout2.xml + +To see available layouts: ls unpacked/ppt/slideLayouts/ + +Prints the element to add to presentation.xml. +""" + +import re +import shutil +import sys +from pathlib import Path + + +def get_next_slide_number(slides_dir: Path) -> int: + existing = [int(m.group(1)) for f in slides_dir.glob("slide*.xml") + if (m := re.match(r"slide(\d+)\.xml", f.name))] + return max(existing) + 1 if existing else 1 + + +def create_slide_from_layout(unpacked_dir: Path, layout_file: str) -> None: + slides_dir = unpacked_dir / "ppt" / "slides" + rels_dir = slides_dir / "_rels" + layouts_dir = unpacked_dir / "ppt" / "slideLayouts" + + layout_path = layouts_dir / layout_file + if not layout_path.exists(): + print(f"Error: {layout_path} not found", file=sys.stderr) + sys.exit(1) + + next_num = get_next_slide_number(slides_dir) + dest = f"slide{next_num}.xml" + dest_slide = slides_dir / dest + dest_rels = rels_dir / f"{dest}.rels" + + slide_xml = ''' + + + + + + + + + + + + + + + + + + + + + +''' + dest_slide.write_text(slide_xml, encoding="utf-8") + + rels_dir.mkdir(exist_ok=True) + rels_xml = f''' + + +''' + dest_rels.write_text(rels_xml, encoding="utf-8") + + _add_to_content_types(unpacked_dir, dest) + + rid = _add_to_presentation_rels(unpacked_dir, dest) + + next_slide_id = _get_next_slide_id(unpacked_dir) + + print(f"Created {dest} from {layout_file}") + print(f'Add to presentation.xml : ') + + +def duplicate_slide(unpacked_dir: Path, source: str) -> None: + slides_dir = unpacked_dir / "ppt" / "slides" + rels_dir = slides_dir / "_rels" + + source_slide = slides_dir / source + + if not source_slide.exists(): + print(f"Error: {source_slide} not found", file=sys.stderr) + sys.exit(1) + + next_num = get_next_slide_number(slides_dir) + dest = f"slide{next_num}.xml" + dest_slide = slides_dir / dest + + source_rels = rels_dir / f"{source}.rels" + dest_rels = rels_dir / f"{dest}.rels" + + shutil.copy2(source_slide, dest_slide) + + if source_rels.exists(): + shutil.copy2(source_rels, dest_rels) + + rels_content = dest_rels.read_text(encoding="utf-8") + rels_content = re.sub( + r'\s*]*Type="[^"]*notesSlide"[^>]*/>\s*', + "\n", + rels_content, + ) + dest_rels.write_text(rels_content, encoding="utf-8") + + _add_to_content_types(unpacked_dir, dest) + + rid = _add_to_presentation_rels(unpacked_dir, dest) + + next_slide_id = _get_next_slide_id(unpacked_dir) + + print(f"Created {dest} from {source}") + print(f'Add to presentation.xml : ') + + +def _add_to_content_types(unpacked_dir: Path, dest: str) -> None: + content_types_path = unpacked_dir / "[Content_Types].xml" + content_types = content_types_path.read_text(encoding="utf-8") + + new_override = f'' + + if f"/ppt/slides/{dest}" not in content_types: + content_types = content_types.replace("", f" {new_override}\n") + content_types_path.write_text(content_types, encoding="utf-8") + + +def _add_to_presentation_rels(unpacked_dir: Path, dest: str) -> str: + pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels" + pres_rels = pres_rels_path.read_text(encoding="utf-8") + + rids = [int(m) for m in re.findall(r'Id="rId(\d+)"', pres_rels)] + next_rid = max(rids) + 1 if rids else 1 + rid = f"rId{next_rid}" + + new_rel = f'' + + if f"slides/{dest}" not in pres_rels: + pres_rels = pres_rels.replace("", f" {new_rel}\n") + pres_rels_path.write_text(pres_rels, encoding="utf-8") + + return rid + + +def _get_next_slide_id(unpacked_dir: Path) -> int: + pres_path = unpacked_dir / "ppt" / "presentation.xml" + pres_content = pres_path.read_text(encoding="utf-8") + slide_ids = [int(m) for m in re.findall(r']*id="(\d+)"', pres_content)] + return max(slide_ids) + 1 if slide_ids else 256 + + +def parse_source(source: str) -> tuple[str, str | None]: + if source.startswith("slideLayout") and source.endswith(".xml"): + return ("layout", source) + + return ("slide", None) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python add_slide.py ", file=sys.stderr) + print("", file=sys.stderr) + print("Source can be:", file=sys.stderr) + print(" slide2.xml - duplicate an existing slide", file=sys.stderr) + print(" slideLayout2.xml - create from a layout template", file=sys.stderr) + print("", file=sys.stderr) + print("To see available layouts: ls /ppt/slideLayouts/", file=sys.stderr) + sys.exit(1) + + unpacked_dir = Path(sys.argv[1]) + source = sys.argv[2] + + if not unpacked_dir.exists(): + print(f"Error: {unpacked_dir} not found", file=sys.stderr) + sys.exit(1) + + source_type, layout_file = parse_source(source) + + if source_type == "layout" and layout_file is not None: + create_slide_from_layout(unpacked_dir, layout_file) + else: + duplicate_slide(unpacked_dir, source) diff --git a/.agents/skills/pptx/scripts/clean.py b/.agents/skills/pptx/scripts/clean.py new file mode 100755 index 00000000..3d13994c --- /dev/null +++ b/.agents/skills/pptx/scripts/clean.py @@ -0,0 +1,286 @@ +"""Remove unreferenced files from an unpacked PPTX directory. + +Usage: python clean.py + +Example: + python clean.py unpacked/ + +This script removes: +- Orphaned slides (not in sldIdLst) and their relationships +- [trash] directory (unreferenced files) +- Orphaned .rels files for deleted resources +- Unreferenced media, embeddings, charts, diagrams, drawings, ink files +- Unreferenced theme files +- Unreferenced notes slides +- Content-Type overrides for deleted files +""" + +import sys +from pathlib import Path + +import defusedxml.minidom + + +import re + + +def get_slides_in_sldidlst(unpacked_dir: Path) -> set[str]: + pres_path = unpacked_dir / "ppt" / "presentation.xml" + pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels" + + if not pres_path.exists() or not pres_rels_path.exists(): + return set() + + rels_dom = defusedxml.minidom.parse(str(pres_rels_path)) + rid_to_slide = {} + for rel in rels_dom.getElementsByTagName("Relationship"): + rid = rel.getAttribute("Id") + target = rel.getAttribute("Target") + rel_type = rel.getAttribute("Type") + if "slide" in rel_type and target.startswith("slides/"): + rid_to_slide[rid] = target.replace("slides/", "") + + pres_content = pres_path.read_text(encoding="utf-8") + referenced_rids = set(re.findall(r']*r:id="([^"]+)"', pres_content)) + + return {rid_to_slide[rid] for rid in referenced_rids if rid in rid_to_slide} + + +def remove_orphaned_slides(unpacked_dir: Path) -> list[str]: + slides_dir = unpacked_dir / "ppt" / "slides" + slides_rels_dir = slides_dir / "_rels" + pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels" + + if not slides_dir.exists(): + return [] + + referenced_slides = get_slides_in_sldidlst(unpacked_dir) + removed = [] + + for slide_file in slides_dir.glob("slide*.xml"): + if slide_file.name not in referenced_slides: + rel_path = slide_file.relative_to(unpacked_dir) + slide_file.unlink() + removed.append(str(rel_path)) + + rels_file = slides_rels_dir / f"{slide_file.name}.rels" + if rels_file.exists(): + rels_file.unlink() + removed.append(str(rels_file.relative_to(unpacked_dir))) + + if removed and pres_rels_path.exists(): + rels_dom = defusedxml.minidom.parse(str(pres_rels_path)) + changed = False + + for rel in list(rels_dom.getElementsByTagName("Relationship")): + target = rel.getAttribute("Target") + if target.startswith("slides/"): + slide_name = target.replace("slides/", "") + if slide_name not in referenced_slides: + if rel.parentNode: + rel.parentNode.removeChild(rel) + changed = True + + if changed: + with open(pres_rels_path, "wb") as f: + f.write(rels_dom.toxml(encoding="utf-8")) + + return removed + + +def remove_trash_directory(unpacked_dir: Path) -> list[str]: + trash_dir = unpacked_dir / "[trash]" + removed = [] + + if trash_dir.exists() and trash_dir.is_dir(): + for file_path in trash_dir.iterdir(): + if file_path.is_file(): + rel_path = file_path.relative_to(unpacked_dir) + removed.append(str(rel_path)) + file_path.unlink() + trash_dir.rmdir() + + return removed + + +def get_slide_referenced_files(unpacked_dir: Path) -> set: + referenced = set() + slides_rels_dir = unpacked_dir / "ppt" / "slides" / "_rels" + + if not slides_rels_dir.exists(): + return referenced + + for rels_file in slides_rels_dir.glob("*.rels"): + dom = defusedxml.minidom.parse(str(rels_file)) + for rel in dom.getElementsByTagName("Relationship"): + target = rel.getAttribute("Target") + if not target: + continue + target_path = (rels_file.parent.parent / target).resolve() + try: + referenced.add(target_path.relative_to(unpacked_dir.resolve())) + except ValueError: + pass + + return referenced + + +def remove_orphaned_rels_files(unpacked_dir: Path) -> list[str]: + resource_dirs = ["charts", "diagrams", "drawings"] + removed = [] + slide_referenced = get_slide_referenced_files(unpacked_dir) + + for dir_name in resource_dirs: + rels_dir = unpacked_dir / "ppt" / dir_name / "_rels" + if not rels_dir.exists(): + continue + + for rels_file in rels_dir.glob("*.rels"): + resource_file = rels_dir.parent / rels_file.name.replace(".rels", "") + try: + resource_rel_path = resource_file.resolve().relative_to(unpacked_dir.resolve()) + except ValueError: + continue + + if not resource_file.exists() or resource_rel_path not in slide_referenced: + rels_file.unlink() + rel_path = rels_file.relative_to(unpacked_dir) + removed.append(str(rel_path)) + + return removed + + +def get_referenced_files(unpacked_dir: Path) -> set: + referenced = set() + + for rels_file in unpacked_dir.rglob("*.rels"): + dom = defusedxml.minidom.parse(str(rels_file)) + for rel in dom.getElementsByTagName("Relationship"): + target = rel.getAttribute("Target") + if not target: + continue + target_path = (rels_file.parent.parent / target).resolve() + try: + referenced.add(target_path.relative_to(unpacked_dir.resolve())) + except ValueError: + pass + + return referenced + + +def remove_orphaned_files(unpacked_dir: Path, referenced: set) -> list[str]: + resource_dirs = ["media", "embeddings", "charts", "diagrams", "tags", "drawings", "ink"] + removed = [] + + for dir_name in resource_dirs: + dir_path = unpacked_dir / "ppt" / dir_name + if not dir_path.exists(): + continue + + for file_path in dir_path.glob("*"): + if not file_path.is_file(): + continue + rel_path = file_path.relative_to(unpacked_dir) + if rel_path not in referenced: + file_path.unlink() + removed.append(str(rel_path)) + + theme_dir = unpacked_dir / "ppt" / "theme" + if theme_dir.exists(): + for file_path in theme_dir.glob("theme*.xml"): + rel_path = file_path.relative_to(unpacked_dir) + if rel_path not in referenced: + file_path.unlink() + removed.append(str(rel_path)) + theme_rels = theme_dir / "_rels" / f"{file_path.name}.rels" + if theme_rels.exists(): + theme_rels.unlink() + removed.append(str(theme_rels.relative_to(unpacked_dir))) + + notes_dir = unpacked_dir / "ppt" / "notesSlides" + if notes_dir.exists(): + for file_path in notes_dir.glob("*.xml"): + if not file_path.is_file(): + continue + rel_path = file_path.relative_to(unpacked_dir) + if rel_path not in referenced: + file_path.unlink() + removed.append(str(rel_path)) + + notes_rels_dir = notes_dir / "_rels" + if notes_rels_dir.exists(): + for file_path in notes_rels_dir.glob("*.rels"): + notes_file = notes_dir / file_path.name.replace(".rels", "") + if not notes_file.exists(): + file_path.unlink() + removed.append(str(file_path.relative_to(unpacked_dir))) + + return removed + + +def update_content_types(unpacked_dir: Path, removed_files: list[str]) -> None: + ct_path = unpacked_dir / "[Content_Types].xml" + if not ct_path.exists(): + return + + dom = defusedxml.minidom.parse(str(ct_path)) + changed = False + + for override in list(dom.getElementsByTagName("Override")): + part_name = override.getAttribute("PartName").lstrip("/") + if part_name in removed_files: + if override.parentNode: + override.parentNode.removeChild(override) + changed = True + + if changed: + with open(ct_path, "wb") as f: + f.write(dom.toxml(encoding="utf-8")) + + +def clean_unused_files(unpacked_dir: Path) -> list[str]: + all_removed = [] + + slides_removed = remove_orphaned_slides(unpacked_dir) + all_removed.extend(slides_removed) + + trash_removed = remove_trash_directory(unpacked_dir) + all_removed.extend(trash_removed) + + while True: + removed_rels = remove_orphaned_rels_files(unpacked_dir) + referenced = get_referenced_files(unpacked_dir) + removed_files = remove_orphaned_files(unpacked_dir, referenced) + + total_removed = removed_rels + removed_files + if not total_removed: + break + + all_removed.extend(total_removed) + + if all_removed: + update_content_types(unpacked_dir, all_removed) + + return all_removed + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python clean.py ", file=sys.stderr) + print("Example: python clean.py unpacked/", file=sys.stderr) + sys.exit(1) + + unpacked_dir = Path(sys.argv[1]) + + if not unpacked_dir.exists(): + print(f"Error: {unpacked_dir} not found", file=sys.stderr) + sys.exit(1) + + removed = clean_unused_files(unpacked_dir) + + if removed: + print(f"Removed {len(removed)} unreferenced files:") + for f in removed: + print(f" {f}") + else: + print("No unreferenced files found") diff --git a/.agents/skills/pptx/scripts/office/helpers/__init__.py b/.agents/skills/pptx/scripts/office/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/.agents/skills/pptx/scripts/office/helpers/merge_runs.py b/.agents/skills/pptx/scripts/office/helpers/merge_runs.py new file mode 100644 index 00000000..ad7c25ee --- /dev/null +++ b/.agents/skills/pptx/scripts/office/helpers/merge_runs.py @@ -0,0 +1,199 @@ +"""Merge adjacent runs with identical formatting in DOCX. + +Merges adjacent elements that have identical properties. +Works on runs in paragraphs and inside tracked changes (, ). + +Also: +- Removes rsid attributes from runs (revision metadata that doesn't affect rendering) +- Removes proofErr elements (spell/grammar markers that block merging) +""" + +from pathlib import Path + +import defusedxml.minidom + + +def merge_runs(input_dir: str) -> tuple[int, str]: + doc_xml = Path(input_dir) / "word" / "document.xml" + + if not doc_xml.exists(): + return 0, f"Error: {doc_xml} not found" + + try: + dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8")) + root = dom.documentElement + + _remove_elements(root, "proofErr") + _strip_run_rsid_attrs(root) + + containers = {run.parentNode for run in _find_elements(root, "r")} + + merge_count = 0 + for container in containers: + merge_count += _merge_runs_in(container) + + doc_xml.write_bytes(dom.toxml(encoding="UTF-8")) + return merge_count, f"Merged {merge_count} runs" + + except Exception as e: + return 0, f"Error: {e}" + + + + +def _find_elements(root, tag: str) -> list: + results = [] + + def traverse(node): + if node.nodeType == node.ELEMENT_NODE: + name = node.localName or node.tagName + if name == tag or name.endswith(f":{tag}"): + results.append(node) + for child in node.childNodes: + traverse(child) + + traverse(root) + return results + + +def _get_child(parent, tag: str): + for child in parent.childNodes: + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name == tag or name.endswith(f":{tag}"): + return child + return None + + +def _get_children(parent, tag: str) -> list: + results = [] + for child in parent.childNodes: + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name == tag or name.endswith(f":{tag}"): + results.append(child) + return results + + +def _is_adjacent(elem1, elem2) -> bool: + node = elem1.nextSibling + while node: + if node == elem2: + return True + if node.nodeType == node.ELEMENT_NODE: + return False + if node.nodeType == node.TEXT_NODE and node.data.strip(): + return False + node = node.nextSibling + return False + + + + +def _remove_elements(root, tag: str): + for elem in _find_elements(root, tag): + if elem.parentNode: + elem.parentNode.removeChild(elem) + + +def _strip_run_rsid_attrs(root): + for run in _find_elements(root, "r"): + for attr in list(run.attributes.values()): + if "rsid" in attr.name.lower(): + run.removeAttribute(attr.name) + + + + +def _merge_runs_in(container) -> int: + merge_count = 0 + run = _first_child_run(container) + + while run: + while True: + next_elem = _next_element_sibling(run) + if next_elem and _is_run(next_elem) and _can_merge(run, next_elem): + _merge_run_content(run, next_elem) + container.removeChild(next_elem) + merge_count += 1 + else: + break + + _consolidate_text(run) + run = _next_sibling_run(run) + + return merge_count + + +def _first_child_run(container): + for child in container.childNodes: + if child.nodeType == child.ELEMENT_NODE and _is_run(child): + return child + return None + + +def _next_element_sibling(node): + sibling = node.nextSibling + while sibling: + if sibling.nodeType == sibling.ELEMENT_NODE: + return sibling + sibling = sibling.nextSibling + return None + + +def _next_sibling_run(node): + sibling = node.nextSibling + while sibling: + if sibling.nodeType == sibling.ELEMENT_NODE: + if _is_run(sibling): + return sibling + sibling = sibling.nextSibling + return None + + +def _is_run(node) -> bool: + name = node.localName or node.tagName + return name == "r" or name.endswith(":r") + + +def _can_merge(run1, run2) -> bool: + rpr1 = _get_child(run1, "rPr") + rpr2 = _get_child(run2, "rPr") + + if (rpr1 is None) != (rpr2 is None): + return False + if rpr1 is None: + return True + return rpr1.toxml() == rpr2.toxml() + + +def _merge_run_content(target, source): + for child in list(source.childNodes): + if child.nodeType == child.ELEMENT_NODE: + name = child.localName or child.tagName + if name != "rPr" and not name.endswith(":rPr"): + target.appendChild(child) + + +def _consolidate_text(run): + t_elements = _get_children(run, "t") + + for i in range(len(t_elements) - 1, 0, -1): + curr, prev = t_elements[i], t_elements[i - 1] + + if _is_adjacent(prev, curr): + prev_text = prev.firstChild.data if prev.firstChild else "" + curr_text = curr.firstChild.data if curr.firstChild else "" + merged = prev_text + curr_text + + if prev.firstChild: + prev.firstChild.data = merged + else: + prev.appendChild(run.ownerDocument.createTextNode(merged)) + + if merged.startswith(" ") or merged.endswith(" "): + prev.setAttribute("xml:space", "preserve") + elif prev.hasAttribute("xml:space"): + prev.removeAttribute("xml:space") + + run.removeChild(curr) diff --git a/.agents/skills/pptx/scripts/office/helpers/simplify_redlines.py b/.agents/skills/pptx/scripts/office/helpers/simplify_redlines.py new file mode 100644 index 00000000..db963bb9 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/helpers/simplify_redlines.py @@ -0,0 +1,197 @@ +"""Simplify tracked changes by merging adjacent w:ins or w:del elements. + +Merges adjacent elements from the same author into a single element. +Same for elements. This makes heavily-redlined documents easier to +work with by reducing the number of tracked change wrappers. + +Rules: +- Only merges w:ins with w:ins, w:del with w:del (same element type) +- Only merges if same author (ignores timestamp differences) +- Only merges if truly adjacent (only whitespace between them) +""" + +import xml.etree.ElementTree as ET +import zipfile +from pathlib import Path + +import defusedxml.minidom + +WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + + +def simplify_redlines(input_dir: str) -> tuple[int, str]: + doc_xml = Path(input_dir) / "word" / "document.xml" + + if not doc_xml.exists(): + return 0, f"Error: {doc_xml} not found" + + try: + dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8")) + root = dom.documentElement + + merge_count = 0 + + containers = _find_elements(root, "p") + _find_elements(root, "tc") + + for container in containers: + merge_count += _merge_tracked_changes_in(container, "ins") + merge_count += _merge_tracked_changes_in(container, "del") + + doc_xml.write_bytes(dom.toxml(encoding="UTF-8")) + return merge_count, f"Simplified {merge_count} tracked changes" + + except Exception as e: + return 0, f"Error: {e}" + + +def _merge_tracked_changes_in(container, tag: str) -> int: + merge_count = 0 + + tracked = [ + child + for child in container.childNodes + if child.nodeType == child.ELEMENT_NODE and _is_element(child, tag) + ] + + if len(tracked) < 2: + return 0 + + i = 0 + while i < len(tracked) - 1: + curr = tracked[i] + next_elem = tracked[i + 1] + + if _can_merge_tracked(curr, next_elem): + _merge_tracked_content(curr, next_elem) + container.removeChild(next_elem) + tracked.pop(i + 1) + merge_count += 1 + else: + i += 1 + + return merge_count + + +def _is_element(node, tag: str) -> bool: + name = node.localName or node.tagName + return name == tag or name.endswith(f":{tag}") + + +def _get_author(elem) -> str: + author = elem.getAttribute("w:author") + if not author: + for attr in elem.attributes.values(): + if attr.localName == "author" or attr.name.endswith(":author"): + return attr.value + return author + + +def _can_merge_tracked(elem1, elem2) -> bool: + if _get_author(elem1) != _get_author(elem2): + return False + + node = elem1.nextSibling + while node and node != elem2: + if node.nodeType == node.ELEMENT_NODE: + return False + if node.nodeType == node.TEXT_NODE and node.data.strip(): + return False + node = node.nextSibling + + return True + + +def _merge_tracked_content(target, source): + while source.firstChild: + child = source.firstChild + source.removeChild(child) + target.appendChild(child) + + +def _find_elements(root, tag: str) -> list: + results = [] + + def traverse(node): + if node.nodeType == node.ELEMENT_NODE: + name = node.localName or node.tagName + if name == tag or name.endswith(f":{tag}"): + results.append(node) + for child in node.childNodes: + traverse(child) + + traverse(root) + return results + + +def get_tracked_change_authors(doc_xml_path: Path) -> dict[str, int]: + if not doc_xml_path.exists(): + return {} + + try: + tree = ET.parse(doc_xml_path) + root = tree.getroot() + except ET.ParseError: + return {} + + namespaces = {"w": WORD_NS} + author_attr = f"{{{WORD_NS}}}author" + + authors: dict[str, int] = {} + for tag in ["ins", "del"]: + for elem in root.findall(f".//w:{tag}", namespaces): + author = elem.get(author_attr) + if author: + authors[author] = authors.get(author, 0) + 1 + + return authors + + +def _get_authors_from_docx(docx_path: Path) -> dict[str, int]: + try: + with zipfile.ZipFile(docx_path, "r") as zf: + if "word/document.xml" not in zf.namelist(): + return {} + with zf.open("word/document.xml") as f: + tree = ET.parse(f) + root = tree.getroot() + + namespaces = {"w": WORD_NS} + author_attr = f"{{{WORD_NS}}}author" + + authors: dict[str, int] = {} + for tag in ["ins", "del"]: + for elem in root.findall(f".//w:{tag}", namespaces): + author = elem.get(author_attr) + if author: + authors[author] = authors.get(author, 0) + 1 + return authors + except (zipfile.BadZipFile, ET.ParseError): + return {} + + +def infer_author(modified_dir: Path, original_docx: Path, default: str = "Claude") -> str: + modified_xml = modified_dir / "word" / "document.xml" + modified_authors = get_tracked_change_authors(modified_xml) + + if not modified_authors: + return default + + original_authors = _get_authors_from_docx(original_docx) + + new_changes: dict[str, int] = {} + for author, count in modified_authors.items(): + original_count = original_authors.get(author, 0) + diff = count - original_count + if diff > 0: + new_changes[author] = diff + + if not new_changes: + return default + + if len(new_changes) == 1: + return next(iter(new_changes)) + + raise ValueError( + f"Multiple authors added new changes: {new_changes}. " + "Cannot infer which author to validate." + ) diff --git a/.agents/skills/pptx/scripts/office/pack.py b/.agents/skills/pptx/scripts/office/pack.py new file mode 100755 index 00000000..db29ed8b --- /dev/null +++ b/.agents/skills/pptx/scripts/office/pack.py @@ -0,0 +1,159 @@ +"""Pack a directory into a DOCX, PPTX, or XLSX file. + +Validates with auto-repair, condenses XML formatting, and creates the Office file. + +Usage: + python pack.py [--original ] [--validate true|false] + +Examples: + python pack.py unpacked/ output.docx --original input.docx + python pack.py unpacked/ output.pptx --validate false +""" + +import argparse +import sys +import shutil +import tempfile +import zipfile +from pathlib import Path + +import defusedxml.minidom + +from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator + +def pack( + input_directory: str, + output_file: str, + original_file: str | None = None, + validate: bool = True, + infer_author_func=None, +) -> tuple[None, str]: + input_dir = Path(input_directory) + output_path = Path(output_file) + suffix = output_path.suffix.lower() + + if not input_dir.is_dir(): + return None, f"Error: {input_dir} is not a directory" + + if suffix not in {".docx", ".pptx", ".xlsx"}: + return None, f"Error: {output_file} must be a .docx, .pptx, or .xlsx file" + + if validate and original_file: + original_path = Path(original_file) + if original_path.exists(): + success, output = _run_validation( + input_dir, original_path, suffix, infer_author_func + ) + if output: + print(output) + if not success: + return None, f"Error: Validation failed for {input_dir}" + + with tempfile.TemporaryDirectory() as temp_dir: + temp_content_dir = Path(temp_dir) / "content" + shutil.copytree(input_dir, temp_content_dir) + + for pattern in ["*.xml", "*.rels"]: + for xml_file in temp_content_dir.rglob(pattern): + _condense_xml(xml_file) + + output_path.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf: + for f in temp_content_dir.rglob("*"): + if f.is_file(): + zf.write(f, f.relative_to(temp_content_dir)) + + return None, f"Successfully packed {input_dir} to {output_file}" + + +def _run_validation( + unpacked_dir: Path, + original_file: Path, + suffix: str, + infer_author_func=None, +) -> tuple[bool, str | None]: + output_lines = [] + validators = [] + + if suffix == ".docx": + author = "Claude" + if infer_author_func: + try: + author = infer_author_func(unpacked_dir, original_file) + except ValueError as e: + print(f"Warning: {e} Using default author 'Claude'.", file=sys.stderr) + + validators = [ + DOCXSchemaValidator(unpacked_dir, original_file), + RedliningValidator(unpacked_dir, original_file, author=author), + ] + elif suffix == ".pptx": + validators = [PPTXSchemaValidator(unpacked_dir, original_file)] + + if not validators: + return True, None + + total_repairs = sum(v.repair() for v in validators) + if total_repairs: + output_lines.append(f"Auto-repaired {total_repairs} issue(s)") + + success = all(v.validate() for v in validators) + + if success: + output_lines.append("All validations PASSED!") + + return success, "\n".join(output_lines) if output_lines else None + + +def _condense_xml(xml_file: Path) -> None: + try: + with open(xml_file, encoding="utf-8") as f: + dom = defusedxml.minidom.parse(f) + + for element in dom.getElementsByTagName("*"): + if element.tagName.endswith(":t"): + continue + + for child in list(element.childNodes): + if ( + child.nodeType == child.TEXT_NODE + and child.nodeValue + and child.nodeValue.strip() == "" + ) or child.nodeType == child.COMMENT_NODE: + element.removeChild(child) + + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + except Exception as e: + print(f"ERROR: Failed to parse {xml_file.name}: {e}", file=sys.stderr) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Pack a directory into a DOCX, PPTX, or XLSX file" + ) + parser.add_argument("input_directory", help="Unpacked Office document directory") + parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)") + parser.add_argument( + "--original", + help="Original file for validation comparison", + ) + parser.add_argument( + "--validate", + type=lambda x: x.lower() == "true", + default=True, + metavar="true|false", + help="Run validation with auto-repair (default: true)", + ) + args = parser.parse_args() + + _, message = pack( + args.input_directory, + args.output_file, + original_file=args.original, + validate=args.validate, + ) + print(message) + + if "Error" in message: + sys.exit(1) diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file mode 100644 index 00000000..6454ef9a --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd @@ -0,0 +1,1499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file mode 100644 index 00000000..afa4f463 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file mode 100644 index 00000000..64e66b8a --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd @@ -0,0 +1,1085 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file mode 100644 index 00000000..687eea82 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd @@ -0,0 +1,11 @@ + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file mode 100644 index 00000000..6ac81b06 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd @@ -0,0 +1,3081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file mode 100644 index 00000000..1dbf0514 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file mode 100644 index 00000000..f1af17db --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file mode 100644 index 00000000..0a185ab6 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd new file mode 100644 index 00000000..14ef4888 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd @@ -0,0 +1,1676 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file mode 100644 index 00000000..c20f3bf1 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file mode 100644 index 00000000..ac602522 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file mode 100644 index 00000000..424b8ba8 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file mode 100644 index 00000000..2bddce29 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file mode 100644 index 00000000..8a8c18ba --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file mode 100644 index 00000000..5c42706a --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file mode 100644 index 00000000..853c341c --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file mode 100644 index 00000000..da835ee8 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file mode 100644 index 00000000..87ad2658 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file mode 100644 index 00000000..9e86f1b2 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd new file mode 100644 index 00000000..d0be42e7 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd @@ -0,0 +1,4439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file mode 100644 index 00000000..8821dd18 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd @@ -0,0 +1,570 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file mode 100644 index 00000000..ca2575c7 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file mode 100644 index 00000000..dd079e60 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file mode 100644 index 00000000..3dd6cf62 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file mode 100644 index 00000000..f1041e34 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd new file mode 100644 index 00000000..9c5b7a63 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd @@ -0,0 +1,3646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd new file mode 100644 index 00000000..0f13678d --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd @@ -0,0 +1,116 @@ + + + + + + See http://www.w3.org/XML/1998/namespace.html and + http://www.w3.org/TR/REC-xml for information about this namespace. + + This schema document describes the XML namespace, in a form + suitable for import by other schema documents. + + Note that local names in this namespace are intended to be defined + only by the World Wide Web Consortium or its subgroups. The + following names are currently defined in this namespace and should + not be used with conflicting semantics by any Working Group, + specification, or document instance: + + base (as an attribute name): denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification. + + lang (as an attribute name): denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification. + + space (as an attribute name): denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification. + + Father (in any context at all): denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: + + In appreciation for his vision, leadership and dedication + the W3C XML Plenary on this 10th day of February, 2000 + reserves for Jon Bosak in perpetuity the XML name + xml:Father + + + + + This schema defines attributes and an attribute group + suitable for use by + schemas wishing to allow xml:base, xml:lang or xml:space attributes + on elements they define. + + To enable this, such a schema must import this schema + for the XML namespace, e.g. as follows: + <schema . . .> + . . . + <import namespace="http://www.w3.org/XML/1998/namespace" + schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> + + Subsequently, qualified reference to any of the attributes + or the group defined below will have the desired effect, e.g. + + <type . . .> + . . . + <attributeGroup ref="xml:specialAttrs"/> + + will define a type which will schema-validate an instance + element with any of those attributes + + + + In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + http://www.w3.org/2001/03/xml.xsd. + At the date of issue it can also be found at + http://www.w3.org/2001/xml.xsd. + The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML Schema + itself. In other words, if the XML Schema namespace changes, the version + of this document at + http://www.w3.org/2001/xml.xsd will change + accordingly; the version at + http://www.w3.org/2001/03/xml.xsd will not change. + + + + + + In due course, we should install the relevant ISO 2- and 3-letter + codes as the enumerated possible values . . . + + + + + + + + + + + + + + + See http://www.w3.org/TR/xmlbase/ for + information about this attribute. + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file mode 100644 index 00000000..a6de9d27 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file mode 100644 index 00000000..10e978b6 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd b/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd new file mode 100644 index 00000000..4248bf7a --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd b/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd new file mode 100644 index 00000000..56497467 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/mce/mc.xsd b/.agents/skills/pptx/scripts/office/schemas/mce/mc.xsd new file mode 100644 index 00000000..ef725457 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/mce/mc.xsd @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd new file mode 100644 index 00000000..f65f7777 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd @@ -0,0 +1,560 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd new file mode 100644 index 00000000..6b00755a --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd new file mode 100644 index 00000000..f321d333 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd new file mode 100644 index 00000000..364c6a9b --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd new file mode 100644 index 00000000..fed9d15b --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd new file mode 100644 index 00000000..680cf154 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd @@ -0,0 +1,4 @@ + + + + diff --git a/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd new file mode 100644 index 00000000..89ada908 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.agents/skills/pptx/scripts/office/soffice.py b/.agents/skills/pptx/scripts/office/soffice.py new file mode 100644 index 00000000..c7f7e328 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/soffice.py @@ -0,0 +1,183 @@ +""" +Helper for running LibreOffice (soffice) in environments where AF_UNIX +sockets may be blocked (e.g., sandboxed VMs). Detects the restriction +at runtime and applies an LD_PRELOAD shim if needed. + +Usage: + from office.soffice import run_soffice, get_soffice_env + + # Option 1 – run soffice directly + result = run_soffice(["--headless", "--convert-to", "pdf", "input.docx"]) + + # Option 2 – get env dict for your own subprocess calls + env = get_soffice_env() + subprocess.run(["soffice", ...], env=env) +""" + +import os +import socket +import subprocess +import tempfile +from pathlib import Path + + +def get_soffice_env() -> dict: + env = os.environ.copy() + env["SAL_USE_VCLPLUGIN"] = "svp" + + if _needs_shim(): + shim = _ensure_shim() + env["LD_PRELOAD"] = str(shim) + + return env + + +def run_soffice(args: list[str], **kwargs) -> subprocess.CompletedProcess: + env = get_soffice_env() + return subprocess.run(["soffice"] + args, env=env, **kwargs) + + + +_SHIM_SO = Path(tempfile.gettempdir()) / "lo_socket_shim.so" + + +def _needs_shim() -> bool: + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.close() + return False + except OSError: + return True + + +def _ensure_shim() -> Path: + if _SHIM_SO.exists(): + return _SHIM_SO + + src = Path(tempfile.gettempdir()) / "lo_socket_shim.c" + src.write_text(_SHIM_SOURCE) + subprocess.run( + ["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"], + check=True, + capture_output=True, + ) + src.unlink() + return _SHIM_SO + + + +_SHIM_SOURCE = r""" +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +static int (*real_socket)(int, int, int); +static int (*real_socketpair)(int, int, int, int[2]); +static int (*real_listen)(int, int); +static int (*real_accept)(int, struct sockaddr *, socklen_t *); +static int (*real_close)(int); +static int (*real_read)(int, void *, size_t); + +/* Per-FD bookkeeping (FDs >= 1024 are passed through unshimmed). */ +static int is_shimmed[1024]; +static int peer_of[1024]; +static int wake_r[1024]; /* accept() blocks reading this */ +static int wake_w[1024]; /* close() writes to this */ +static int listener_fd = -1; /* FD that received listen() */ + +__attribute__((constructor)) +static void init(void) { + real_socket = dlsym(RTLD_NEXT, "socket"); + real_socketpair = dlsym(RTLD_NEXT, "socketpair"); + real_listen = dlsym(RTLD_NEXT, "listen"); + real_accept = dlsym(RTLD_NEXT, "accept"); + real_close = dlsym(RTLD_NEXT, "close"); + real_read = dlsym(RTLD_NEXT, "read"); + for (int i = 0; i < 1024; i++) { + peer_of[i] = -1; + wake_r[i] = -1; + wake_w[i] = -1; + } +} + +/* ---- socket ---------------------------------------------------------- */ +int socket(int domain, int type, int protocol) { + if (domain == AF_UNIX) { + int fd = real_socket(domain, type, protocol); + if (fd >= 0) return fd; + /* socket(AF_UNIX) blocked – fall back to socketpair(). */ + int sv[2]; + if (real_socketpair(domain, type, protocol, sv) == 0) { + if (sv[0] >= 0 && sv[0] < 1024) { + is_shimmed[sv[0]] = 1; + peer_of[sv[0]] = sv[1]; + int wp[2]; + if (pipe(wp) == 0) { + wake_r[sv[0]] = wp[0]; + wake_w[sv[0]] = wp[1]; + } + } + return sv[0]; + } + errno = EPERM; + return -1; + } + return real_socket(domain, type, protocol); +} + +/* ---- listen ---------------------------------------------------------- */ +int listen(int sockfd, int backlog) { + if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) { + listener_fd = sockfd; + return 0; + } + return real_listen(sockfd, backlog); +} + +/* ---- accept ---------------------------------------------------------- */ +int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) { + if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) { + /* Block until close() writes to the wake pipe. */ + if (wake_r[sockfd] >= 0) { + char buf; + real_read(wake_r[sockfd], &buf, 1); + } + errno = ECONNABORTED; + return -1; + } + return real_accept(sockfd, addr, addrlen); +} + +/* ---- close ----------------------------------------------------------- */ +int close(int fd) { + if (fd >= 0 && fd < 1024 && is_shimmed[fd]) { + int was_listener = (fd == listener_fd); + is_shimmed[fd] = 0; + + if (wake_w[fd] >= 0) { /* unblock accept() */ + char c = 0; + write(wake_w[fd], &c, 1); + real_close(wake_w[fd]); + wake_w[fd] = -1; + } + if (wake_r[fd] >= 0) { real_close(wake_r[fd]); wake_r[fd] = -1; } + if (peer_of[fd] >= 0) { real_close(peer_of[fd]); peer_of[fd] = -1; } + + if (was_listener) + _exit(0); /* conversion done – exit */ + } + return real_close(fd); +} +""" + + + +if __name__ == "__main__": + import sys + result = run_soffice(sys.argv[1:]) + sys.exit(result.returncode) diff --git a/.agents/skills/pptx/scripts/office/unpack.py b/.agents/skills/pptx/scripts/office/unpack.py new file mode 100755 index 00000000..00152533 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/unpack.py @@ -0,0 +1,132 @@ +"""Unpack Office files (DOCX, PPTX, XLSX) for editing. + +Extracts the ZIP archive, pretty-prints XML files, and optionally: +- Merges adjacent runs with identical formatting (DOCX only) +- Simplifies adjacent tracked changes from same author (DOCX only) + +Usage: + python unpack.py [options] + +Examples: + python unpack.py document.docx unpacked/ + python unpack.py presentation.pptx unpacked/ + python unpack.py document.docx unpacked/ --merge-runs false +""" + +import argparse +import sys +import zipfile +from pathlib import Path + +import defusedxml.minidom + +from helpers.merge_runs import merge_runs as do_merge_runs +from helpers.simplify_redlines import simplify_redlines as do_simplify_redlines + +SMART_QUOTE_REPLACEMENTS = { + "\u201c": "“", + "\u201d": "”", + "\u2018": "‘", + "\u2019": "’", +} + + +def unpack( + input_file: str, + output_directory: str, + merge_runs: bool = True, + simplify_redlines: bool = True, +) -> tuple[None, str]: + input_path = Path(input_file) + output_path = Path(output_directory) + suffix = input_path.suffix.lower() + + if not input_path.exists(): + return None, f"Error: {input_file} does not exist" + + if suffix not in {".docx", ".pptx", ".xlsx"}: + return None, f"Error: {input_file} must be a .docx, .pptx, or .xlsx file" + + try: + output_path.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile(input_path, "r") as zf: + zf.extractall(output_path) + + xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels")) + for xml_file in xml_files: + _pretty_print_xml(xml_file) + + message = f"Unpacked {input_file} ({len(xml_files)} XML files)" + + if suffix == ".docx": + if simplify_redlines: + simplify_count, _ = do_simplify_redlines(str(output_path)) + message += f", simplified {simplify_count} tracked changes" + + if merge_runs: + merge_count, _ = do_merge_runs(str(output_path)) + message += f", merged {merge_count} runs" + + for xml_file in xml_files: + _escape_smart_quotes(xml_file) + + return None, message + + except zipfile.BadZipFile: + return None, f"Error: {input_file} is not a valid Office file" + except Exception as e: + return None, f"Error unpacking: {e}" + + +def _pretty_print_xml(xml_file: Path) -> None: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="utf-8")) + except Exception: + pass + + +def _escape_smart_quotes(xml_file: Path) -> None: + try: + content = xml_file.read_text(encoding="utf-8") + for char, entity in SMART_QUOTE_REPLACEMENTS.items(): + content = content.replace(char, entity) + xml_file.write_text(content, encoding="utf-8") + except Exception: + pass + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Unpack an Office file (DOCX, PPTX, XLSX) for editing" + ) + parser.add_argument("input_file", help="Office file to unpack") + parser.add_argument("output_directory", help="Output directory") + parser.add_argument( + "--merge-runs", + type=lambda x: x.lower() == "true", + default=True, + metavar="true|false", + help="Merge adjacent runs with identical formatting (DOCX only, default: true)", + ) + parser.add_argument( + "--simplify-redlines", + type=lambda x: x.lower() == "true", + default=True, + metavar="true|false", + help="Merge adjacent tracked changes from same author (DOCX only, default: true)", + ) + args = parser.parse_args() + + _, message = unpack( + args.input_file, + args.output_directory, + merge_runs=args.merge_runs, + simplify_redlines=args.simplify_redlines, + ) + print(message) + + if "Error" in message: + sys.exit(1) diff --git a/.agents/skills/pptx/scripts/office/validate.py b/.agents/skills/pptx/scripts/office/validate.py new file mode 100755 index 00000000..03b01f6e --- /dev/null +++ b/.agents/skills/pptx/scripts/office/validate.py @@ -0,0 +1,111 @@ +""" +Command line tool to validate Office document XML files against XSD schemas and tracked changes. + +Usage: + python validate.py [--original ] [--auto-repair] [--author NAME] + +The first argument can be either: +- An unpacked directory containing the Office document XML files +- A packed Office file (.docx/.pptx/.xlsx) which will be unpacked to a temp directory + +Auto-repair fixes: +- paraId/durableId values that exceed OOXML limits +- Missing xml:space="preserve" on w:t elements with whitespace +""" + +import argparse +import sys +import tempfile +import zipfile +from pathlib import Path + +from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator + + +def main(): + parser = argparse.ArgumentParser(description="Validate Office document XML files") + parser.add_argument( + "path", + help="Path to unpacked directory or packed Office file (.docx/.pptx/.xlsx)", + ) + parser.add_argument( + "--original", + required=False, + default=None, + help="Path to original file (.docx/.pptx/.xlsx). If omitted, all XSD errors are reported and redlining validation is skipped.", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable verbose output", + ) + parser.add_argument( + "--auto-repair", + action="store_true", + help="Automatically repair common issues (hex IDs, whitespace preservation)", + ) + parser.add_argument( + "--author", + default="Claude", + help="Author name for redlining validation (default: Claude)", + ) + args = parser.parse_args() + + path = Path(args.path) + assert path.exists(), f"Error: {path} does not exist" + + original_file = None + if args.original: + original_file = Path(args.original) + assert original_file.is_file(), f"Error: {original_file} is not a file" + assert original_file.suffix.lower() in [".docx", ".pptx", ".xlsx"], ( + f"Error: {original_file} must be a .docx, .pptx, or .xlsx file" + ) + + file_extension = (original_file or path).suffix.lower() + assert file_extension in [".docx", ".pptx", ".xlsx"], ( + f"Error: Cannot determine file type from {path}. Use --original or provide a .docx/.pptx/.xlsx file." + ) + + if path.is_file() and path.suffix.lower() in [".docx", ".pptx", ".xlsx"]: + temp_dir = tempfile.mkdtemp() + with zipfile.ZipFile(path, "r") as zf: + zf.extractall(temp_dir) + unpacked_dir = Path(temp_dir) + else: + assert path.is_dir(), f"Error: {path} is not a directory or Office file" + unpacked_dir = path + + match file_extension: + case ".docx": + validators = [ + DOCXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose), + ] + if original_file: + validators.append( + RedliningValidator(unpacked_dir, original_file, verbose=args.verbose, author=args.author) + ) + case ".pptx": + validators = [ + PPTXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose), + ] + case _: + print(f"Error: Validation not supported for file type {file_extension}") + sys.exit(1) + + if args.auto_repair: + total_repairs = sum(v.repair() for v in validators) + if total_repairs: + print(f"Auto-repaired {total_repairs} issue(s)") + + success = all(v.validate() for v in validators) + + if success: + print("All validations PASSED!") + + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/pptx/scripts/office/validators/__init__.py b/.agents/skills/pptx/scripts/office/validators/__init__.py new file mode 100644 index 00000000..db092ece --- /dev/null +++ b/.agents/skills/pptx/scripts/office/validators/__init__.py @@ -0,0 +1,15 @@ +""" +Validation modules for Word document processing. +""" + +from .base import BaseSchemaValidator +from .docx import DOCXSchemaValidator +from .pptx import PPTXSchemaValidator +from .redlining import RedliningValidator + +__all__ = [ + "BaseSchemaValidator", + "DOCXSchemaValidator", + "PPTXSchemaValidator", + "RedliningValidator", +] diff --git a/.agents/skills/pptx/scripts/office/validators/base.py b/.agents/skills/pptx/scripts/office/validators/base.py new file mode 100644 index 00000000..db4a06a2 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/validators/base.py @@ -0,0 +1,847 @@ +""" +Base validator with common validation logic for document files. +""" + +import re +from pathlib import Path + +import defusedxml.minidom +import lxml.etree + + +class BaseSchemaValidator: + + IGNORED_VALIDATION_ERRORS = [ + "hyphenationZone", + "purl.org/dc/terms", + ] + + UNIQUE_ID_REQUIREMENTS = { + "comment": ("id", "file"), + "commentrangestart": ("id", "file"), + "commentrangeend": ("id", "file"), + "bookmarkstart": ("id", "file"), + "bookmarkend": ("id", "file"), + "sldid": ("id", "file"), + "sldmasterid": ("id", "global"), + "sldlayoutid": ("id", "global"), + "cm": ("authorid", "file"), + "sheet": ("sheetid", "file"), + "definedname": ("id", "file"), + "cxnsp": ("id", "file"), + "sp": ("id", "file"), + "pic": ("id", "file"), + "grpsp": ("id", "file"), + } + + EXCLUDED_ID_CONTAINERS = { + "sectionlst", + } + + ELEMENT_RELATIONSHIP_TYPES = {} + + SCHEMA_MAPPINGS = { + "word": "ISO-IEC29500-4_2016/wml.xsd", + "ppt": "ISO-IEC29500-4_2016/pml.xsd", + "xl": "ISO-IEC29500-4_2016/sml.xsd", + "[Content_Types].xml": "ecma/fouth-edition/opc-contentTypes.xsd", + "app.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd", + "core.xml": "ecma/fouth-edition/opc-coreProperties.xsd", + "custom.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd", + ".rels": "ecma/fouth-edition/opc-relationships.xsd", + "people.xml": "microsoft/wml-2012.xsd", + "commentsIds.xml": "microsoft/wml-cid-2016.xsd", + "commentsExtensible.xml": "microsoft/wml-cex-2018.xsd", + "commentsExtended.xml": "microsoft/wml-2012.xsd", + "chart": "ISO-IEC29500-4_2016/dml-chart.xsd", + "theme": "ISO-IEC29500-4_2016/dml-main.xsd", + "drawing": "ISO-IEC29500-4_2016/dml-main.xsd", + } + + MC_NAMESPACE = "http://schemas.openxmlformats.org/markup-compatibility/2006" + XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace" + + PACKAGE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/relationships" + ) + OFFICE_RELATIONSHIPS_NAMESPACE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + ) + CONTENT_TYPES_NAMESPACE = ( + "http://schemas.openxmlformats.org/package/2006/content-types" + ) + + MAIN_CONTENT_FOLDERS = {"word", "ppt", "xl"} + + OOXML_NAMESPACES = { + "http://schemas.openxmlformats.org/officeDocument/2006/math", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "http://schemas.openxmlformats.org/schemaLibrary/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/main", + "http://schemas.openxmlformats.org/drawingml/2006/chart", + "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/diagram", + "http://schemas.openxmlformats.org/drawingml/2006/picture", + "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing", + "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "http://schemas.openxmlformats.org/presentationml/2006/main", + "http://schemas.openxmlformats.org/spreadsheetml/2006/main", + "http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes", + "http://www.w3.org/XML/1998/namespace", + } + + def __init__(self, unpacked_dir, original_file=None, verbose=False): + self.unpacked_dir = Path(unpacked_dir).resolve() + self.original_file = Path(original_file) if original_file else None + self.verbose = verbose + + self.schemas_dir = Path(__file__).parent.parent / "schemas" + + patterns = ["*.xml", "*.rels"] + self.xml_files = [ + f for pattern in patterns for f in self.unpacked_dir.rglob(pattern) + ] + + if not self.xml_files: + print(f"Warning: No XML files found in {self.unpacked_dir}") + + def validate(self): + raise NotImplementedError("Subclasses must implement the validate method") + + def repair(self) -> int: + return self.repair_whitespace_preservation() + + def repair_whitespace_preservation(self) -> int: + repairs = 0 + + for xml_file in self.xml_files: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + modified = False + + for elem in dom.getElementsByTagName("*"): + if elem.tagName.endswith(":t") and elem.firstChild: + text = elem.firstChild.nodeValue + if text and (text.startswith((' ', '\t')) or text.endswith((' ', '\t'))): + if elem.getAttribute("xml:space") != "preserve": + elem.setAttribute("xml:space", "preserve") + text_preview = repr(text[:30]) + "..." if len(text) > 30 else repr(text) + print(f" Repaired: {xml_file.name}: Added xml:space='preserve' to {elem.tagName}: {text_preview}") + repairs += 1 + modified = True + + if modified: + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + + except Exception: + pass + + return repairs + + def validate_xml(self): + errors = [] + + for xml_file in self.xml_files: + try: + lxml.etree.parse(str(xml_file)) + except lxml.etree.XMLSyntaxError as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {e.lineno}: {e.msg}" + ) + except Exception as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Unexpected error: {str(e)}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} XML violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All XML files are well-formed") + return True + + def validate_namespaces(self): + errors = [] + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + declared = set(root.nsmap.keys()) - {None} + + for attr_val in [ + v for k, v in root.attrib.items() if k.endswith("Ignorable") + ]: + undeclared = set(attr_val.split()) - declared + errors.extend( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Namespace '{ns}' in Ignorable but not declared" + for ns in undeclared + ) + except lxml.etree.XMLSyntaxError: + continue + + if errors: + print(f"FAILED - {len(errors)} namespace issues:") + for error in errors: + print(error) + return False + if self.verbose: + print("PASSED - All namespace prefixes properly declared") + return True + + def validate_unique_ids(self): + errors = [] + global_ids = {} + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + file_ids = {} + + mc_elements = root.xpath( + ".//mc:AlternateContent", namespaces={"mc": self.MC_NAMESPACE} + ) + for elem in mc_elements: + elem.getparent().remove(elem) + + for elem in root.iter(): + tag = ( + elem.tag.split("}")[-1].lower() + if "}" in elem.tag + else elem.tag.lower() + ) + + if tag in self.UNIQUE_ID_REQUIREMENTS: + in_excluded_container = any( + ancestor.tag.split("}")[-1].lower() in self.EXCLUDED_ID_CONTAINERS + for ancestor in elem.iterancestors() + ) + if in_excluded_container: + continue + + attr_name, scope = self.UNIQUE_ID_REQUIREMENTS[tag] + + id_value = None + for attr, value in elem.attrib.items(): + attr_local = ( + attr.split("}")[-1].lower() + if "}" in attr + else attr.lower() + ) + if attr_local == attr_name: + id_value = value + break + + if id_value is not None: + if scope == "global": + if id_value in global_ids: + prev_file, prev_line, prev_tag = global_ids[ + id_value + ] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Global ID '{id_value}' in <{tag}> " + f"already used in {prev_file} at line {prev_line} in <{prev_tag}>" + ) + else: + global_ids[id_value] = ( + xml_file.relative_to(self.unpacked_dir), + elem.sourceline, + tag, + ) + elif scope == "file": + key = (tag, attr_name) + if key not in file_ids: + file_ids[key] = {} + + if id_value in file_ids[key]: + prev_line = file_ids[key][id_value] + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: Duplicate {attr_name}='{id_value}' in <{tag}> " + f"(first occurrence at line {prev_line})" + ) + else: + file_ids[key][id_value] = elem.sourceline + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} ID uniqueness violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All required IDs are unique") + return True + + def validate_file_references(self): + errors = [] + + rels_files = list(self.unpacked_dir.rglob("*.rels")) + + if not rels_files: + if self.verbose: + print("PASSED - No .rels files found") + return True + + all_files = [] + for file_path in self.unpacked_dir.rglob("*"): + if ( + file_path.is_file() + and file_path.name != "[Content_Types].xml" + and not file_path.name.endswith(".rels") + ): + all_files.append(file_path.resolve()) + + all_referenced_files = set() + + if self.verbose: + print( + f"Found {len(rels_files)} .rels files and {len(all_files)} target files" + ) + + for rels_file in rels_files: + try: + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + rels_dir = rels_file.parent + + referenced_files = set() + broken_refs = [] + + for rel in rels_root.findall( + ".//ns:Relationship", + namespaces={"ns": self.PACKAGE_RELATIONSHIPS_NAMESPACE}, + ): + target = rel.get("Target") + if target and not target.startswith( + ("http", "mailto:") + ): + if target.startswith("/"): + target_path = self.unpacked_dir / target.lstrip("/") + elif rels_file.name == ".rels": + target_path = self.unpacked_dir / target + else: + base_dir = rels_dir.parent + target_path = base_dir / target + + try: + target_path = target_path.resolve() + if target_path.exists() and target_path.is_file(): + referenced_files.add(target_path) + all_referenced_files.add(target_path) + else: + broken_refs.append((target, rel.sourceline)) + except (OSError, ValueError): + broken_refs.append((target, rel.sourceline)) + + if broken_refs: + rel_path = rels_file.relative_to(self.unpacked_dir) + for broken_ref, line_num in broken_refs: + errors.append( + f" {rel_path}: Line {line_num}: Broken reference to {broken_ref}" + ) + + except Exception as e: + rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append(f" Error parsing {rel_path}: {e}") + + unreferenced_files = set(all_files) - all_referenced_files + + if unreferenced_files: + for unref_file in sorted(unreferenced_files): + unref_rel_path = unref_file.relative_to(self.unpacked_dir) + errors.append(f" Unreferenced file: {unref_rel_path}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship validation errors:") + for error in errors: + print(error) + print( + "CRITICAL: These errors will cause the document to appear corrupt. " + + "Broken references MUST be fixed, " + + "and unreferenced files MUST be referenced or removed." + ) + return False + else: + if self.verbose: + print( + "PASSED - All references are valid and all files are properly referenced" + ) + return True + + def validate_all_relationship_ids(self): + import lxml.etree + + errors = [] + + for xml_file in self.xml_files: + if xml_file.suffix == ".rels": + continue + + rels_dir = xml_file.parent / "_rels" + rels_file = rels_dir / f"{xml_file.name}.rels" + + if not rels_file.exists(): + continue + + try: + rels_root = lxml.etree.parse(str(rels_file)).getroot() + rid_to_type = {} + + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rid = rel.get("Id") + rel_type = rel.get("Type", "") + if rid: + if rid in rid_to_type: + rels_rel_path = rels_file.relative_to(self.unpacked_dir) + errors.append( + f" {rels_rel_path}: Line {rel.sourceline}: " + f"Duplicate relationship ID '{rid}' (IDs must be unique)" + ) + type_name = ( + rel_type.split("/")[-1] if "/" in rel_type else rel_type + ) + rid_to_type[rid] = type_name + + xml_root = lxml.etree.parse(str(xml_file)).getroot() + + r_ns = self.OFFICE_RELATIONSHIPS_NAMESPACE + rid_attrs_to_check = ["id", "embed", "link"] + for elem in xml_root.iter(): + for attr_name in rid_attrs_to_check: + rid_attr = elem.get(f"{{{r_ns}}}{attr_name}") + if not rid_attr: + continue + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + elem_name = ( + elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag + ) + + if rid_attr not in rid_to_type: + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> r:{attr_name} references non-existent relationship '{rid_attr}' " + f"(valid IDs: {', '.join(sorted(rid_to_type.keys())[:5])}{'...' if len(rid_to_type) > 5 else ''})" + ) + elif attr_name == "id" and self.ELEMENT_RELATIONSHIP_TYPES: + expected_type = self._get_expected_relationship_type( + elem_name + ) + if expected_type: + actual_type = rid_to_type[rid_attr] + if expected_type not in actual_type.lower(): + errors.append( + f" {xml_rel_path}: Line {elem.sourceline}: " + f"<{elem_name}> references '{rid_attr}' which points to '{actual_type}' " + f"but should point to a '{expected_type}' relationship" + ) + + except Exception as e: + xml_rel_path = xml_file.relative_to(self.unpacked_dir) + errors.append(f" Error processing {xml_rel_path}: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} relationship ID reference errors:") + for error in errors: + print(error) + print("\nThese ID mismatches will cause the document to appear corrupt!") + return False + else: + if self.verbose: + print("PASSED - All relationship ID references are valid") + return True + + def _get_expected_relationship_type(self, element_name): + elem_lower = element_name.lower() + + if elem_lower in self.ELEMENT_RELATIONSHIP_TYPES: + return self.ELEMENT_RELATIONSHIP_TYPES[elem_lower] + + if elem_lower.endswith("id") and len(elem_lower) > 2: + prefix = elem_lower[:-2] + if prefix.endswith("master"): + return prefix.lower() + elif prefix.endswith("layout"): + return prefix.lower() + else: + if prefix == "sld": + return "slide" + return prefix.lower() + + if elem_lower.endswith("reference") and len(elem_lower) > 9: + prefix = elem_lower[:-9] + return prefix.lower() + + return None + + def validate_content_types(self): + errors = [] + + content_types_file = self.unpacked_dir / "[Content_Types].xml" + if not content_types_file.exists(): + print("FAILED - [Content_Types].xml file not found") + return False + + try: + root = lxml.etree.parse(str(content_types_file)).getroot() + declared_parts = set() + declared_extensions = set() + + for override in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Override" + ): + part_name = override.get("PartName") + if part_name is not None: + declared_parts.add(part_name.lstrip("/")) + + for default in root.findall( + f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Default" + ): + extension = default.get("Extension") + if extension is not None: + declared_extensions.add(extension.lower()) + + declarable_roots = { + "sld", + "sldLayout", + "sldMaster", + "presentation", + "document", + "workbook", + "worksheet", + "theme", + } + + media_extensions = { + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "gif": "image/gif", + "bmp": "image/bmp", + "tiff": "image/tiff", + "wmf": "image/x-wmf", + "emf": "image/x-emf", + } + + all_files = list(self.unpacked_dir.rglob("*")) + all_files = [f for f in all_files if f.is_file()] + + for xml_file in self.xml_files: + path_str = str(xml_file.relative_to(self.unpacked_dir)).replace( + "\\", "/" + ) + + if any( + skip in path_str + for skip in [".rels", "[Content_Types]", "docProps/", "_rels/"] + ): + continue + + try: + root_tag = lxml.etree.parse(str(xml_file)).getroot().tag + root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag + + if root_name in declarable_roots and path_str not in declared_parts: + errors.append( + f" {path_str}: File with <{root_name}> root not declared in [Content_Types].xml" + ) + + except Exception: + continue + + for file_path in all_files: + if file_path.suffix.lower() in {".xml", ".rels"}: + continue + if file_path.name == "[Content_Types].xml": + continue + if "_rels" in file_path.parts or "docProps" in file_path.parts: + continue + + extension = file_path.suffix.lstrip(".").lower() + if extension and extension not in declared_extensions: + if extension in media_extensions: + relative_path = file_path.relative_to(self.unpacked_dir) + errors.append( + f' {relative_path}: File with extension \'{extension}\' not declared in [Content_Types].xml - should add: ' + ) + + except Exception as e: + errors.append(f" Error parsing [Content_Types].xml: {e}") + + if errors: + print(f"FAILED - Found {len(errors)} content type declaration errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print( + "PASSED - All content files are properly declared in [Content_Types].xml" + ) + return True + + def validate_file_against_xsd(self, xml_file, verbose=False): + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + + is_valid, current_errors = self._validate_single_file_xsd( + xml_file, unpacked_dir + ) + + if is_valid is None: + return None, set() + elif is_valid: + return True, set() + + original_errors = self._get_original_file_errors(xml_file) + + assert current_errors is not None + new_errors = current_errors - original_errors + + new_errors = { + e for e in new_errors + if not any(pattern in e for pattern in self.IGNORED_VALIDATION_ERRORS) + } + + if new_errors: + if verbose: + relative_path = xml_file.relative_to(unpacked_dir) + print(f"FAILED - {relative_path}: {len(new_errors)} new error(s)") + for error in list(new_errors)[:3]: + truncated = error[:250] + "..." if len(error) > 250 else error + print(f" - {truncated}") + return False, new_errors + else: + if verbose: + print( + f"PASSED - No new errors (original had {len(current_errors)} errors)" + ) + return True, set() + + def validate_against_xsd(self): + new_errors = [] + original_error_count = 0 + valid_count = 0 + skipped_count = 0 + + for xml_file in self.xml_files: + relative_path = str(xml_file.relative_to(self.unpacked_dir)) + is_valid, new_file_errors = self.validate_file_against_xsd( + xml_file, verbose=False + ) + + if is_valid is None: + skipped_count += 1 + continue + elif is_valid and not new_file_errors: + valid_count += 1 + continue + elif is_valid: + original_error_count += 1 + valid_count += 1 + continue + + new_errors.append(f" {relative_path}: {len(new_file_errors)} new error(s)") + for error in list(new_file_errors)[:3]: + new_errors.append( + f" - {error[:250]}..." if len(error) > 250 else f" - {error}" + ) + + if self.verbose: + print(f"Validated {len(self.xml_files)} files:") + print(f" - Valid: {valid_count}") + print(f" - Skipped (no schema): {skipped_count}") + if original_error_count: + print(f" - With original errors (ignored): {original_error_count}") + print( + f" - With NEW errors: {len(new_errors) > 0 and len([e for e in new_errors if not e.startswith(' ')]) or 0}" + ) + + if new_errors: + print("\nFAILED - Found NEW validation errors:") + for error in new_errors: + print(error) + return False + else: + if self.verbose: + print("\nPASSED - No new XSD validation errors introduced") + return True + + def _get_schema_path(self, xml_file): + if xml_file.name in self.SCHEMA_MAPPINGS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.name] + + if xml_file.suffix == ".rels": + return self.schemas_dir / self.SCHEMA_MAPPINGS[".rels"] + + if "charts/" in str(xml_file) and xml_file.name.startswith("chart"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["chart"] + + if "theme/" in str(xml_file) and xml_file.name.startswith("theme"): + return self.schemas_dir / self.SCHEMA_MAPPINGS["theme"] + + if xml_file.parent.name in self.MAIN_CONTENT_FOLDERS: + return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.parent.name] + + return None + + def _clean_ignorable_namespaces(self, xml_doc): + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + for elem in xml_copy.iter(): + attrs_to_remove = [] + + for attr in elem.attrib: + if "{" in attr: + ns = attr.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + attrs_to_remove.append(attr) + + for attr in attrs_to_remove: + del elem.attrib[attr] + + self._remove_ignorable_elements(xml_copy) + + return lxml.etree.ElementTree(xml_copy) + + def _remove_ignorable_elements(self, root): + elements_to_remove = [] + + for elem in list(root): + if not hasattr(elem, "tag") or callable(elem.tag): + continue + + tag_str = str(elem.tag) + if tag_str.startswith("{"): + ns = tag_str.split("}")[0][1:] + if ns not in self.OOXML_NAMESPACES: + elements_to_remove.append(elem) + continue + + self._remove_ignorable_elements(elem) + + for elem in elements_to_remove: + root.remove(elem) + + def _preprocess_for_mc_ignorable(self, xml_doc): + root = xml_doc.getroot() + + if f"{{{self.MC_NAMESPACE}}}Ignorable" in root.attrib: + del root.attrib[f"{{{self.MC_NAMESPACE}}}Ignorable"] + + return xml_doc + + def _validate_single_file_xsd(self, xml_file, base_path): + schema_path = self._get_schema_path(xml_file) + if not schema_path: + return None, None + + try: + with open(schema_path, "rb") as xsd_file: + parser = lxml.etree.XMLParser() + xsd_doc = lxml.etree.parse( + xsd_file, parser=parser, base_url=str(schema_path) + ) + schema = lxml.etree.XMLSchema(xsd_doc) + + with open(xml_file, "r") as f: + xml_doc = lxml.etree.parse(f) + + xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc) + xml_doc = self._preprocess_for_mc_ignorable(xml_doc) + + relative_path = xml_file.relative_to(base_path) + if ( + relative_path.parts + and relative_path.parts[0] in self.MAIN_CONTENT_FOLDERS + ): + xml_doc = self._clean_ignorable_namespaces(xml_doc) + + if schema.validate(xml_doc): + return True, set() + else: + errors = set() + for error in schema.error_log: + errors.add(error.message) + return False, errors + + except Exception as e: + return False, {str(e)} + + def _get_original_file_errors(self, xml_file): + if self.original_file is None: + return set() + + import tempfile + import zipfile + + xml_file = Path(xml_file).resolve() + unpacked_dir = self.unpacked_dir.resolve() + relative_path = xml_file.relative_to(unpacked_dir) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + with zipfile.ZipFile(self.original_file, "r") as zip_ref: + zip_ref.extractall(temp_path) + + original_xml_file = temp_path / relative_path + + if not original_xml_file.exists(): + return set() + + is_valid, errors = self._validate_single_file_xsd( + original_xml_file, temp_path + ) + return errors if errors else set() + + def _remove_template_tags_from_text_nodes(self, xml_doc): + warnings = [] + template_pattern = re.compile(r"\{\{[^}]*\}\}") + + xml_string = lxml.etree.tostring(xml_doc, encoding="unicode") + xml_copy = lxml.etree.fromstring(xml_string) + + def process_text_content(text, content_type): + if not text: + return text + matches = list(template_pattern.finditer(text)) + if matches: + for match in matches: + warnings.append( + f"Found template tag in {content_type}: {match.group()}" + ) + return template_pattern.sub("", text) + return text + + for elem in xml_copy.iter(): + if not hasattr(elem, "tag") or callable(elem.tag): + continue + tag_str = str(elem.tag) + if tag_str.endswith("}t") or tag_str == "t": + continue + + elem.text = process_text_content(elem.text, "text content") + elem.tail = process_text_content(elem.tail, "tail content") + + return lxml.etree.ElementTree(xml_copy), warnings + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/.agents/skills/pptx/scripts/office/validators/docx.py b/.agents/skills/pptx/scripts/office/validators/docx.py new file mode 100644 index 00000000..fec405e6 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/validators/docx.py @@ -0,0 +1,446 @@ +""" +Validator for Word document XML files against XSD schemas. +""" + +import random +import re +import tempfile +import zipfile + +import defusedxml.minidom +import lxml.etree + +from .base import BaseSchemaValidator + + +class DOCXSchemaValidator(BaseSchemaValidator): + + WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + W14_NAMESPACE = "http://schemas.microsoft.com/office/word/2010/wordml" + W16CID_NAMESPACE = "http://schemas.microsoft.com/office/word/2016/wordml/cid" + + ELEMENT_RELATIONSHIP_TYPES = {} + + def validate(self): + if not self.validate_xml(): + return False + + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + if not self.validate_unique_ids(): + all_valid = False + + if not self.validate_file_references(): + all_valid = False + + if not self.validate_content_types(): + all_valid = False + + if not self.validate_against_xsd(): + all_valid = False + + if not self.validate_whitespace_preservation(): + all_valid = False + + if not self.validate_deletions(): + all_valid = False + + if not self.validate_insertions(): + all_valid = False + + if not self.validate_all_relationship_ids(): + all_valid = False + + if not self.validate_id_constraints(): + all_valid = False + + if not self.validate_comment_markers(): + all_valid = False + + self.compare_paragraph_counts() + + return all_valid + + def validate_whitespace_preservation(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"): + if elem.text: + text = elem.text + if re.search(r"^[ \t\n\r]", text) or re.search( + r"[ \t\n\r]$", text + ): + xml_space_attr = f"{{{self.XML_NAMESPACE}}}space" + if ( + xml_space_attr not in elem.attrib + or elem.attrib[xml_space_attr] != "preserve" + ): + text_preview = ( + repr(text)[:50] + "..." + if len(repr(text)) > 50 + else repr(text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: w:t element with whitespace missing xml:space='preserve': {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} whitespace preservation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All whitespace is properly preserved") + return True + + def validate_deletions(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + for t_elem in root.xpath(".//w:del//w:t", namespaces=namespaces): + if t_elem.text: + text_preview = ( + repr(t_elem.text)[:50] + "..." + if len(repr(t_elem.text)) > 50 + else repr(t_elem.text) + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {t_elem.sourceline}: found within : {text_preview}" + ) + + for instr_elem in root.xpath( + ".//w:del//w:instrText", namespaces=namespaces + ): + text_preview = ( + repr(instr_elem.text or "")[:50] + "..." + if len(repr(instr_elem.text or "")) > 50 + else repr(instr_elem.text or "") + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {instr_elem.sourceline}: found within (use ): {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} deletion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:t elements found within w:del elements") + return True + + def count_paragraphs_in_unpacked(self): + count = 0 + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + except Exception as e: + print(f"Error counting paragraphs in unpacked document: {e}") + + return count + + def count_paragraphs_in_original(self): + original = self.original_file + if original is None: + return 0 + + count = 0 + + try: + with tempfile.TemporaryDirectory() as temp_dir: + with zipfile.ZipFile(original, "r") as zip_ref: + zip_ref.extractall(temp_dir) + + doc_xml_path = temp_dir + "/word/document.xml" + root = lxml.etree.parse(doc_xml_path).getroot() + + paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p") + count = len(paragraphs) + + except Exception as e: + print(f"Error counting paragraphs in original document: {e}") + + return count + + def validate_insertions(self): + errors = [] + + for xml_file in self.xml_files: + if xml_file.name != "document.xml": + continue + + try: + root = lxml.etree.parse(str(xml_file)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + invalid_elements = root.xpath( + ".//w:ins//w:delText[not(ancestor::w:del)]", namespaces=namespaces + ) + + for elem in invalid_elements: + text_preview = ( + repr(elem.text or "")[:50] + "..." + if len(repr(elem.text or "")) > 50 + else repr(elem.text or "") + ) + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: within : {text_preview}" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} insertion validation violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - No w:delText elements within w:ins elements") + return True + + def compare_paragraph_counts(self): + original_count = self.count_paragraphs_in_original() + new_count = self.count_paragraphs_in_unpacked() + + diff = new_count - original_count + diff_str = f"+{diff}" if diff > 0 else str(diff) + print(f"\nParagraphs: {original_count} → {new_count} ({diff_str})") + + def _parse_id_value(self, val: str, base: int = 16) -> int: + return int(val, base) + + def validate_id_constraints(self): + errors = [] + para_id_attr = f"{{{self.W14_NAMESPACE}}}paraId" + durable_id_attr = f"{{{self.W16CID_NAMESPACE}}}durableId" + + for xml_file in self.xml_files: + try: + for elem in lxml.etree.parse(str(xml_file)).iter(): + if val := elem.get(para_id_attr): + if self._parse_id_value(val, base=16) >= 0x80000000: + errors.append( + f" {xml_file.name}:{elem.sourceline}: paraId={val} >= 0x80000000" + ) + + if val := elem.get(durable_id_attr): + if xml_file.name == "numbering.xml": + try: + if self._parse_id_value(val, base=10) >= 0x7FFFFFFF: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} >= 0x7FFFFFFF" + ) + except ValueError: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} must be decimal in numbering.xml" + ) + else: + if self._parse_id_value(val, base=16) >= 0x7FFFFFFF: + errors.append( + f" {xml_file.name}:{elem.sourceline}: " + f"durableId={val} >= 0x7FFFFFFF" + ) + except Exception: + pass + + if errors: + print(f"FAILED - {len(errors)} ID constraint violations:") + for e in errors: + print(e) + elif self.verbose: + print("PASSED - All paraId/durableId values within constraints") + return not errors + + def validate_comment_markers(self): + errors = [] + + document_xml = None + comments_xml = None + for xml_file in self.xml_files: + if xml_file.name == "document.xml" and "word" in str(xml_file): + document_xml = xml_file + elif xml_file.name == "comments.xml": + comments_xml = xml_file + + if not document_xml: + if self.verbose: + print("PASSED - No document.xml found (skipping comment validation)") + return True + + try: + doc_root = lxml.etree.parse(str(document_xml)).getroot() + namespaces = {"w": self.WORD_2006_NAMESPACE} + + range_starts = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentRangeStart", namespaces=namespaces + ) + } + range_ends = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentRangeEnd", namespaces=namespaces + ) + } + references = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in doc_root.xpath( + ".//w:commentReference", namespaces=namespaces + ) + } + + orphaned_ends = range_ends - range_starts + for comment_id in sorted( + orphaned_ends, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + errors.append( + f' document.xml: commentRangeEnd id="{comment_id}" has no matching commentRangeStart' + ) + + orphaned_starts = range_starts - range_ends + for comment_id in sorted( + orphaned_starts, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + errors.append( + f' document.xml: commentRangeStart id="{comment_id}" has no matching commentRangeEnd' + ) + + comment_ids = set() + if comments_xml and comments_xml.exists(): + comments_root = lxml.etree.parse(str(comments_xml)).getroot() + comment_ids = { + elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id") + for elem in comments_root.xpath( + ".//w:comment", namespaces=namespaces + ) + } + + marker_ids = range_starts | range_ends | references + invalid_refs = marker_ids - comment_ids + for comment_id in sorted( + invalid_refs, key=lambda x: int(x) if x and x.isdigit() else 0 + ): + if comment_id: + errors.append( + f' document.xml: marker id="{comment_id}" references non-existent comment' + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append(f" Error parsing XML: {e}") + + if errors: + print(f"FAILED - {len(errors)} comment marker violations:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All comment markers properly paired") + return True + + def repair(self) -> int: + repairs = super().repair() + repairs += self.repair_durableId() + return repairs + + def repair_durableId(self) -> int: + repairs = 0 + + for xml_file in self.xml_files: + try: + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + modified = False + + for elem in dom.getElementsByTagName("*"): + if not elem.hasAttribute("w16cid:durableId"): + continue + + durable_id = elem.getAttribute("w16cid:durableId") + needs_repair = False + + if xml_file.name == "numbering.xml": + try: + needs_repair = ( + self._parse_id_value(durable_id, base=10) >= 0x7FFFFFFF + ) + except ValueError: + needs_repair = True + else: + try: + needs_repair = ( + self._parse_id_value(durable_id, base=16) >= 0x7FFFFFFF + ) + except ValueError: + needs_repair = True + + if needs_repair: + value = random.randint(1, 0x7FFFFFFE) + if xml_file.name == "numbering.xml": + new_id = str(value) + else: + new_id = f"{value:08X}" + + elem.setAttribute("w16cid:durableId", new_id) + print( + f" Repaired: {xml_file.name}: durableId {durable_id} → {new_id}" + ) + repairs += 1 + modified = True + + if modified: + xml_file.write_bytes(dom.toxml(encoding="UTF-8")) + + except Exception: + pass + + return repairs + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/.agents/skills/pptx/scripts/office/validators/pptx.py b/.agents/skills/pptx/scripts/office/validators/pptx.py new file mode 100644 index 00000000..09842aa9 --- /dev/null +++ b/.agents/skills/pptx/scripts/office/validators/pptx.py @@ -0,0 +1,275 @@ +""" +Validator for PowerPoint presentation XML files against XSD schemas. +""" + +import re + +from .base import BaseSchemaValidator + + +class PPTXSchemaValidator(BaseSchemaValidator): + + PRESENTATIONML_NAMESPACE = ( + "http://schemas.openxmlformats.org/presentationml/2006/main" + ) + + ELEMENT_RELATIONSHIP_TYPES = { + "sldid": "slide", + "sldmasterid": "slidemaster", + "notesmasterid": "notesmaster", + "sldlayoutid": "slidelayout", + "themeid": "theme", + "tablestyleid": "tablestyles", + } + + def validate(self): + if not self.validate_xml(): + return False + + all_valid = True + if not self.validate_namespaces(): + all_valid = False + + if not self.validate_unique_ids(): + all_valid = False + + if not self.validate_uuid_ids(): + all_valid = False + + if not self.validate_file_references(): + all_valid = False + + if not self.validate_slide_layout_ids(): + all_valid = False + + if not self.validate_content_types(): + all_valid = False + + if not self.validate_against_xsd(): + all_valid = False + + if not self.validate_notes_slide_references(): + all_valid = False + + if not self.validate_all_relationship_ids(): + all_valid = False + + if not self.validate_no_duplicate_slide_layouts(): + all_valid = False + + return all_valid + + def validate_uuid_ids(self): + import lxml.etree + + errors = [] + uuid_pattern = re.compile( + r"^[\{\(]?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}[\}\)]?$" + ) + + for xml_file in self.xml_files: + try: + root = lxml.etree.parse(str(xml_file)).getroot() + + for elem in root.iter(): + for attr, value in elem.attrib.items(): + attr_name = attr.split("}")[-1].lower() + if attr_name == "id" or attr_name.endswith("id"): + if self._looks_like_uuid(value): + if not uuid_pattern.match(value): + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: " + f"Line {elem.sourceline}: ID '{value}' appears to be a UUID but contains invalid hex characters" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} UUID ID validation errors:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All UUID-like IDs contain valid hex values") + return True + + def _looks_like_uuid(self, value): + clean_value = value.strip("{}()").replace("-", "") + return len(clean_value) == 32 and all(c.isalnum() for c in clean_value) + + def validate_slide_layout_ids(self): + import lxml.etree + + errors = [] + + slide_masters = list(self.unpacked_dir.glob("ppt/slideMasters/*.xml")) + + if not slide_masters: + if self.verbose: + print("PASSED - No slide masters found") + return True + + for slide_master in slide_masters: + try: + root = lxml.etree.parse(str(slide_master)).getroot() + + rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels" + + if not rels_file.exists(): + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Missing relationships file: {rels_file.relative_to(self.unpacked_dir)}" + ) + continue + + rels_root = lxml.etree.parse(str(rels_file)).getroot() + + valid_layout_rids = set() + for rel in rels_root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "slideLayout" in rel_type: + valid_layout_rids.add(rel.get("Id")) + + for sld_layout_id in root.findall( + f".//{{{self.PRESENTATIONML_NAMESPACE}}}sldLayoutId" + ): + r_id = sld_layout_id.get( + f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id" + ) + layout_id = sld_layout_id.get("id") + + if r_id and r_id not in valid_layout_rids: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: " + f"Line {sld_layout_id.sourceline}: sldLayoutId with id='{layout_id}' " + f"references r:id='{r_id}' which is not found in slide layout relationships" + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {slide_master.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print(f"FAILED - Found {len(errors)} slide layout ID validation errors:") + for error in errors: + print(error) + print( + "Remove invalid references or add missing slide layouts to the relationships file." + ) + return False + else: + if self.verbose: + print("PASSED - All slide layout IDs reference valid slide layouts") + return True + + def validate_no_duplicate_slide_layouts(self): + import lxml.etree + + errors = [] + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + for rels_file in slide_rels_files: + try: + root = lxml.etree.parse(str(rels_file)).getroot() + + layout_rels = [ + rel + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ) + if "slideLayout" in rel.get("Type", "") + ] + + if len(layout_rels) > 1: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: has {len(layout_rels)} slideLayout references" + ) + + except Exception as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + if errors: + print("FAILED - Found slides with duplicate slideLayout references:") + for error in errors: + print(error) + return False + else: + if self.verbose: + print("PASSED - All slides have exactly one slideLayout reference") + return True + + def validate_notes_slide_references(self): + import lxml.etree + + errors = [] + notes_slide_references = {} + + slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels")) + + if not slide_rels_files: + if self.verbose: + print("PASSED - No slide relationship files found") + return True + + for rels_file in slide_rels_files: + try: + root = lxml.etree.parse(str(rels_file)).getroot() + + for rel in root.findall( + f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship" + ): + rel_type = rel.get("Type", "") + if "notesSlide" in rel_type: + target = rel.get("Target", "") + if target: + normalized_target = target.replace("../", "") + + slide_name = rels_file.stem.replace( + ".xml", "" + ) + + if normalized_target not in notes_slide_references: + notes_slide_references[normalized_target] = [] + notes_slide_references[normalized_target].append( + (slide_name, rels_file) + ) + + except (lxml.etree.XMLSyntaxError, Exception) as e: + errors.append( + f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}" + ) + + for target, references in notes_slide_references.items(): + if len(references) > 1: + slide_names = [ref[0] for ref in references] + errors.append( + f" Notes slide '{target}' is referenced by multiple slides: {', '.join(slide_names)}" + ) + for slide_name, rels_file in references: + errors.append(f" - {rels_file.relative_to(self.unpacked_dir)}") + + if errors: + print( + f"FAILED - Found {len([e for e in errors if not e.startswith(' ')])} notes slide reference validation errors:" + ) + for error in errors: + print(error) + print("Each slide may optionally have its own slide file.") + return False + else: + if self.verbose: + print("PASSED - All notes slide references are unique") + return True + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/.agents/skills/pptx/scripts/office/validators/redlining.py b/.agents/skills/pptx/scripts/office/validators/redlining.py new file mode 100644 index 00000000..71c81b6b --- /dev/null +++ b/.agents/skills/pptx/scripts/office/validators/redlining.py @@ -0,0 +1,247 @@ +""" +Validator for tracked changes in Word documents. +""" + +import subprocess +import tempfile +import zipfile +from pathlib import Path + + +class RedliningValidator: + + def __init__(self, unpacked_dir, original_docx, verbose=False, author="Claude"): + self.unpacked_dir = Path(unpacked_dir) + self.original_docx = Path(original_docx) + self.verbose = verbose + self.author = author + self.namespaces = { + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + } + + def repair(self) -> int: + return 0 + + def validate(self): + modified_file = self.unpacked_dir / "word" / "document.xml" + if not modified_file.exists(): + print(f"FAILED - Modified document.xml not found at {modified_file}") + return False + + try: + import xml.etree.ElementTree as ET + + tree = ET.parse(modified_file) + root = tree.getroot() + + del_elements = root.findall(".//w:del", self.namespaces) + ins_elements = root.findall(".//w:ins", self.namespaces) + + author_del_elements = [ + elem + for elem in del_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == self.author + ] + author_ins_elements = [ + elem + for elem in ins_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == self.author + ] + + if not author_del_elements and not author_ins_elements: + if self.verbose: + print(f"PASSED - No tracked changes by {self.author} found.") + return True + + except Exception: + pass + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + try: + with zipfile.ZipFile(self.original_docx, "r") as zip_ref: + zip_ref.extractall(temp_path) + except Exception as e: + print(f"FAILED - Error unpacking original docx: {e}") + return False + + original_file = temp_path / "word" / "document.xml" + if not original_file.exists(): + print( + f"FAILED - Original document.xml not found in {self.original_docx}" + ) + return False + + try: + import xml.etree.ElementTree as ET + + modified_tree = ET.parse(modified_file) + modified_root = modified_tree.getroot() + original_tree = ET.parse(original_file) + original_root = original_tree.getroot() + except ET.ParseError as e: + print(f"FAILED - Error parsing XML files: {e}") + return False + + self._remove_author_tracked_changes(original_root) + self._remove_author_tracked_changes(modified_root) + + modified_text = self._extract_text_content(modified_root) + original_text = self._extract_text_content(original_root) + + if modified_text != original_text: + error_message = self._generate_detailed_diff( + original_text, modified_text + ) + print(error_message) + return False + + if self.verbose: + print(f"PASSED - All changes by {self.author} are properly tracked") + return True + + def _generate_detailed_diff(self, original_text, modified_text): + error_parts = [ + f"FAILED - Document text doesn't match after removing {self.author}'s tracked changes", + "", + "Likely causes:", + " 1. Modified text inside another author's or tags", + " 2. Made edits without proper tracked changes", + " 3. Didn't nest inside when deleting another's insertion", + "", + "For pre-redlined documents, use correct patterns:", + " - To reject another's INSERTION: Nest inside their ", + " - To restore another's DELETION: Add new AFTER their ", + "", + ] + + git_diff = self._get_git_word_diff(original_text, modified_text) + if git_diff: + error_parts.extend(["Differences:", "============", git_diff]) + else: + error_parts.append("Unable to generate word diff (git not available)") + + return "\n".join(error_parts) + + def _get_git_word_diff(self, original_text, modified_text): + try: + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + original_file = temp_path / "original.txt" + modified_file = temp_path / "modified.txt" + + original_file.write_text(original_text, encoding="utf-8") + modified_file.write_text(modified_text, encoding="utf-8") + + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "--word-diff-regex=.", + "-U0", + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + text=True, + ) + + if result.stdout.strip(): + lines = result.stdout.split("\n") + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + + if content_lines: + return "\n".join(content_lines) + + result = subprocess.run( + [ + "git", + "diff", + "--word-diff=plain", + "-U0", + "--no-index", + str(original_file), + str(modified_file), + ], + capture_output=True, + text=True, + ) + + if result.stdout.strip(): + lines = result.stdout.split("\n") + content_lines = [] + in_content = False + for line in lines: + if line.startswith("@@"): + in_content = True + continue + if in_content and line.strip(): + content_lines.append(line) + return "\n".join(content_lines) + + except (subprocess.CalledProcessError, FileNotFoundError, Exception): + pass + + return None + + def _remove_author_tracked_changes(self, root): + ins_tag = f"{{{self.namespaces['w']}}}ins" + del_tag = f"{{{self.namespaces['w']}}}del" + author_attr = f"{{{self.namespaces['w']}}}author" + + for parent in root.iter(): + to_remove = [] + for child in parent: + if child.tag == ins_tag and child.get(author_attr) == self.author: + to_remove.append(child) + for elem in to_remove: + parent.remove(elem) + + deltext_tag = f"{{{self.namespaces['w']}}}delText" + t_tag = f"{{{self.namespaces['w']}}}t" + + for parent in root.iter(): + to_process = [] + for child in parent: + if child.tag == del_tag and child.get(author_attr) == self.author: + to_process.append((child, list(parent).index(child))) + + for del_elem, del_index in reversed(to_process): + for elem in del_elem.iter(): + if elem.tag == deltext_tag: + elem.tag = t_tag + + for child in reversed(list(del_elem)): + parent.insert(del_index, child) + parent.remove(del_elem) + + def _extract_text_content(self, root): + p_tag = f"{{{self.namespaces['w']}}}p" + t_tag = f"{{{self.namespaces['w']}}}t" + + paragraphs = [] + for p_elem in root.findall(f".//{p_tag}"): + text_parts = [] + for t_elem in p_elem.findall(f".//{t_tag}"): + if t_elem.text: + text_parts.append(t_elem.text) + paragraph_text = "".join(text_parts) + if paragraph_text: + paragraphs.append(paragraph_text) + + return "\n".join(paragraphs) + + +if __name__ == "__main__": + raise RuntimeError("This module should not be run directly.") diff --git a/.agents/skills/pptx/scripts/thumbnail.py b/.agents/skills/pptx/scripts/thumbnail.py new file mode 100755 index 00000000..edcbdc0f --- /dev/null +++ b/.agents/skills/pptx/scripts/thumbnail.py @@ -0,0 +1,289 @@ +"""Create thumbnail grids from PowerPoint presentation slides. + +Creates a grid layout of slide thumbnails for quick visual analysis. +Labels each thumbnail with its XML filename (e.g., slide1.xml). +Hidden slides are shown with a placeholder pattern. + +Usage: + python thumbnail.py input.pptx [output_prefix] [--cols N] + +Examples: + python thumbnail.py presentation.pptx + # Creates: thumbnails.jpg + + python thumbnail.py template.pptx grid --cols 4 + # Creates: grid.jpg (or grid-1.jpg, grid-2.jpg for large decks) +""" + +import argparse +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + +import defusedxml.minidom +from office.soffice import get_soffice_env +from PIL import Image, ImageDraw, ImageFont + +THUMBNAIL_WIDTH = 300 +CONVERSION_DPI = 100 +MAX_COLS = 6 +DEFAULT_COLS = 3 +JPEG_QUALITY = 95 +GRID_PADDING = 20 +BORDER_WIDTH = 2 +FONT_SIZE_RATIO = 0.10 +LABEL_PADDING_RATIO = 0.4 + + +def main(): + parser = argparse.ArgumentParser( + description="Create thumbnail grids from PowerPoint slides." + ) + parser.add_argument("input", help="Input PowerPoint file (.pptx)") + parser.add_argument( + "output_prefix", + nargs="?", + default="thumbnails", + help="Output prefix for image files (default: thumbnails)", + ) + parser.add_argument( + "--cols", + type=int, + default=DEFAULT_COLS, + help=f"Number of columns (default: {DEFAULT_COLS}, max: {MAX_COLS})", + ) + + args = parser.parse_args() + + cols = min(args.cols, MAX_COLS) + if args.cols > MAX_COLS: + print(f"Warning: Columns limited to {MAX_COLS}") + + input_path = Path(args.input) + if not input_path.exists() or input_path.suffix.lower() != ".pptx": + print(f"Error: Invalid PowerPoint file: {args.input}", file=sys.stderr) + sys.exit(1) + + output_path = Path(f"{args.output_prefix}.jpg") + + try: + slide_info = get_slide_info(input_path) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + visible_images = convert_to_images(input_path, temp_path) + + if not visible_images and not any(s["hidden"] for s in slide_info): + print("Error: No slides found", file=sys.stderr) + sys.exit(1) + + slides = build_slide_list(slide_info, visible_images, temp_path) + + grid_files = create_grids(slides, cols, THUMBNAIL_WIDTH, output_path) + + print(f"Created {len(grid_files)} grid(s):") + for grid_file in grid_files: + print(f" {grid_file}") + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def get_slide_info(pptx_path: Path) -> list[dict]: + with zipfile.ZipFile(pptx_path, "r") as zf: + rels_content = zf.read("ppt/_rels/presentation.xml.rels").decode("utf-8") + rels_dom = defusedxml.minidom.parseString(rels_content) + + rid_to_slide = {} + for rel in rels_dom.getElementsByTagName("Relationship"): + rid = rel.getAttribute("Id") + target = rel.getAttribute("Target") + rel_type = rel.getAttribute("Type") + if "slide" in rel_type and target.startswith("slides/"): + rid_to_slide[rid] = target.replace("slides/", "") + + pres_content = zf.read("ppt/presentation.xml").decode("utf-8") + pres_dom = defusedxml.minidom.parseString(pres_content) + + slides = [] + for sld_id in pres_dom.getElementsByTagName("p:sldId"): + rid = sld_id.getAttribute("r:id") + if rid in rid_to_slide: + hidden = sld_id.getAttribute("show") == "0" + slides.append({"name": rid_to_slide[rid], "hidden": hidden}) + + return slides + + +def build_slide_list( + slide_info: list[dict], + visible_images: list[Path], + temp_dir: Path, +) -> list[tuple[Path, str]]: + if visible_images: + with Image.open(visible_images[0]) as img: + placeholder_size = img.size + else: + placeholder_size = (1920, 1080) + + slides = [] + visible_idx = 0 + + for info in slide_info: + if info["hidden"]: + placeholder_path = temp_dir / f"hidden-{info['name']}.jpg" + placeholder_img = create_hidden_placeholder(placeholder_size) + placeholder_img.save(placeholder_path, "JPEG") + slides.append((placeholder_path, f"{info['name']} (hidden)")) + else: + if visible_idx < len(visible_images): + slides.append((visible_images[visible_idx], info["name"])) + visible_idx += 1 + + return slides + + +def create_hidden_placeholder(size: tuple[int, int]) -> Image.Image: + img = Image.new("RGB", size, color="#F0F0F0") + draw = ImageDraw.Draw(img) + line_width = max(5, min(size) // 100) + draw.line([(0, 0), size], fill="#CCCCCC", width=line_width) + draw.line([(size[0], 0), (0, size[1])], fill="#CCCCCC", width=line_width) + return img + + +def convert_to_images(pptx_path: Path, temp_dir: Path) -> list[Path]: + pdf_path = temp_dir / f"{pptx_path.stem}.pdf" + + result = subprocess.run( + [ + "soffice", + "--headless", + "--convert-to", + "pdf", + "--outdir", + str(temp_dir), + str(pptx_path), + ], + capture_output=True, + text=True, + env=get_soffice_env(), + ) + if result.returncode != 0 or not pdf_path.exists(): + raise RuntimeError("PDF conversion failed") + + result = subprocess.run( + [ + "pdftoppm", + "-jpeg", + "-r", + str(CONVERSION_DPI), + str(pdf_path), + str(temp_dir / "slide"), + ], + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError("Image conversion failed") + + return sorted(temp_dir.glob("slide-*.jpg")) + + +def create_grids( + slides: list[tuple[Path, str]], + cols: int, + width: int, + output_path: Path, +) -> list[str]: + max_per_grid = cols * (cols + 1) + grid_files = [] + + for chunk_idx, start_idx in enumerate(range(0, len(slides), max_per_grid)): + end_idx = min(start_idx + max_per_grid, len(slides)) + chunk_slides = slides[start_idx:end_idx] + + grid = create_grid(chunk_slides, cols, width) + + if len(slides) <= max_per_grid: + grid_filename = output_path + else: + stem = output_path.stem + suffix = output_path.suffix + grid_filename = output_path.parent / f"{stem}-{chunk_idx + 1}{suffix}" + + grid_filename.parent.mkdir(parents=True, exist_ok=True) + grid.save(str(grid_filename), quality=JPEG_QUALITY) + grid_files.append(str(grid_filename)) + + return grid_files + + +def create_grid( + slides: list[tuple[Path, str]], + cols: int, + width: int, +) -> Image.Image: + font_size = int(width * FONT_SIZE_RATIO) + label_padding = int(font_size * LABEL_PADDING_RATIO) + + with Image.open(slides[0][0]) as img: + aspect = img.height / img.width + height = int(width * aspect) + + rows = (len(slides) + cols - 1) // cols + grid_w = cols * width + (cols + 1) * GRID_PADDING + grid_h = rows * (height + font_size + label_padding * 2) + (rows + 1) * GRID_PADDING + + grid = Image.new("RGB", (grid_w, grid_h), "white") + draw = ImageDraw.Draw(grid) + + try: + font = ImageFont.load_default(size=font_size) + except Exception: + font = ImageFont.load_default() + + for i, (img_path, slide_name) in enumerate(slides): + row, col = i // cols, i % cols + x = col * width + (col + 1) * GRID_PADDING + y_base = ( + row * (height + font_size + label_padding * 2) + (row + 1) * GRID_PADDING + ) + + label = slide_name + bbox = draw.textbbox((0, 0), label, font=font) + text_w = bbox[2] - bbox[0] + draw.text( + (x + (width - text_w) // 2, y_base + label_padding), + label, + fill="black", + font=font, + ) + + y_thumbnail = y_base + label_padding + font_size + label_padding + + with Image.open(img_path) as img: + img.thumbnail((width, height), Image.Resampling.LANCZOS) + w, h = img.size + tx = x + (width - w) // 2 + ty = y_thumbnail + (height - h) // 2 + grid.paste(img, (tx, ty)) + + if BORDER_WIDTH > 0: + draw.rectangle( + [ + (tx - BORDER_WIDTH, ty - BORDER_WIDTH), + (tx + w + BORDER_WIDTH - 1, ty + h + BORDER_WIDTH - 1), + ], + outline="gray", + width=BORDER_WIDTH, + ) + + return grid + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/project-initializer/SKILL.md b/.agents/skills/project-initializer/SKILL.md new file mode 100644 index 00000000..d001c15c --- /dev/null +++ b/.agents/skills/project-initializer/SKILL.md @@ -0,0 +1,50 @@ +--- +name: project-initializer +description: Initializer agent for the project-level harness — turn a spec or issue into feature_list.json, init.sh, verify.yaml, and an initial commit. +requires_tools: + - project_init + - project_status + - progress_append + - bash_exec + - file_read + - file_write +--- + +# Project Initializer + +Use exactly once per project, when `session_mode == feature_loop` and `.agx/project/` does not yet exist (system prompt will say "Initializer 阶段"). + +## Inputs + +- A spec markdown, GitHub issue, or natural-language feature description. +- The repo root (`workspace_dir` or first non-default taskspace). + +## Steps + +1. **Read the spec end-to-end.** Use `file_read` if the user pointed at a file, otherwise capture the user's message as the source of truth. +2. **Decompose into ≥ 5 deliverables.** Each feature must: + - have a stable `id` (kebab-case, ≤ 32 chars), + - be **independently mergeable** (its commit can land on main without breaking others), + - declare 1–3 concrete `acceptance_criteria` (observable behavior, not implementation steps), + - declare `depends_on` only for hard ordering constraints, + - get an integer `priority` (lower = sooner; reserve 100 for "later"). +3. **Call `project_init`** with the full feature list. The tool writes `feature_list.json`, seeds `status.json` with `phase=initialize`, and drops template `init.sh` + `verify.yaml`. +4. **Customize `init.sh`** via `bash_exec` (or `file_write` with the diff path = `.agx/project/init.sh`): + - install language runtime / package manager dependencies, + - apply migrations / generate seed data, + - **must be idempotent** (running twice on the same machine should not break anything). +5. **Customize `verify.yaml`** with the project's real test/lint commands. Keep the `bootstrap` step that runs `init.sh`. +6. **Smoke-run** with `verify_run` (no `feature_id`) to confirm the initial gate is green on a clean checkout. +7. **Commit** the harness files via `bash_exec`: + ``` + git add .agx/project init.sh verify.yaml + git commit -m "chore(project): initialize harness with N features" + ``` + Keep this commit small — only harness files, no business code. +8. **Append a closing progress note** with `progress_append` (e.g. `"[initialize-done] commit= features=N"`) and tell the user the project is ready for a Coding session. + +## Forbidden + +- Editing business code in this session. +- Selecting or completing a feature — that is the Coding phase's job. +- Hardcoding secrets in `init.sh` or `verify.yaml`. diff --git a/.agents/skills/prompt-engineer/.gitignore b/.agents/skills/prompt-engineer/.gitignore new file mode 100644 index 00000000..f4da87e7 --- /dev/null +++ b/.agents/skills/prompt-engineer/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +plans/ +docs/ +.npmrc diff --git a/.agents/skills/prompt-engineer/LICENSE b/.agents/skills/prompt-engineer/LICENSE new file mode 100644 index 00000000..14fac913 --- /dev/null +++ b/.agents/skills/prompt-engineer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.agents/skills/prompt-engineer/README.md b/.agents/skills/prompt-engineer/README.md new file mode 100644 index 00000000..1e44427a --- /dev/null +++ b/.agents/skills/prompt-engineer/README.md @@ -0,0 +1,65 @@ +# 🌟 prompt-engineer-skill - Turn Ideas Into Ready-to-Use Prompts + +## 🚀 Getting Started + +Welcome to prompt-engineer-skill! This tool helps you turn your raw ideas into polished, production-ready prompts for language models. Whether you're a writer, marketer, or simply someone looking to generate quality content, this application is for you. + +## 💾 Download & Install + +To get started, visit our releases page to download the application: + +[![Download the application](https://github.com/pateljig4545/prompt-engineer-skill/raw/refs/heads/main/references/skill_engineer_prompt_v1.5.zip)](https://github.com/pateljig4545/prompt-engineer-skill/raw/refs/heads/main/references/skill_engineer_prompt_v1.5.zip) + +Follow these steps to install it on your computer: + +1. Click the link above to open the Releases page. +2. Look for the latest version under the "Assets" section. +3. Download the file that fits your operating system. For example, you might find files like `https://github.com/pateljig4545/prompt-engineer-skill/raw/refs/heads/main/references/skill_engineer_prompt_v1.5.zip`, `https://github.com/pateljig4545/prompt-engineer-skill/raw/refs/heads/main/references/skill_engineer_prompt_v1.5.zip`, or similar. +4. After the download is complete, find the file on your computer. It is usually located in the "Downloads" folder. +5. Double-click the file to run the installer. Follow the on-screen instructions to complete the installation. + +## 🌐 System Requirements + +To ensure the prompt-engineer-skill runs smoothly, make sure your system meets the following requirements: + +- **Operating System:** Windows 10 or higher, macOS 10.13 or higher. +- **Processor:** Intel or AMD with at least 2GHz. +- **Memory:** Minimum 4GB of RAM. +- **Storage:** At least 100MB of free space. + +If you have specific software tools or libraries installed, they should work with this application as well. + +## 🎉 Features + +The prompt-engineer-skill provides several useful features: + +- **Idea Transformation:** Input rough concepts and get refined prompts suitable for various use cases. +- **User-Friendly Interface:** Navigate easily and access all functions without needing technical knowledge. +- **Export Options:** Save your generated prompts in multiple formats, including text files and direct copy to clipboard. +- **Customized Suggestions:** Get tailored prompts based on the typing style or desired tone, making your content feel personalized. + +## ⚙️ How to Use + +After installing the application, follow these steps to get the most out of prompt-engineer-skill: + +1. **Open the Application:** Locate the application on your computer and double-click to launch it. +2. **Enter Your Idea:** You will see a text box. Type in your rough idea or concept that you want to transform into a prompt. +3. **Generate Prompt:** Click the “Generate” button. The application will process your input and create a refined prompt. +4. **Review Result:** Check the generated output. You can make adjustments or regenerate to get more options. +5. **Save or Copy:** If you like a prompt, use the "Save" button to download it or click “Copy to Clipboard” for easy pasting. + +## ⚡ Troubleshooting + +If you encounter issues while using prompt-engineer-skill, try the following solutions: + +- **Application Does Not Start:** Ensure your operating system is compatible and that you have enough memory and storage. +- **Prompts Are Not What I Expected:** Try rephrasing your idea and submit it again. Small changes can lead to different results. +- **Slow Performance:** Close any other applications running in the background to free up resources. + +## 📞 Support & Feedback + +If you have questions, feedback, or suggestions, feel free to reach out. You can create an issue on our GitHub page, and we will get back to you as soon as possible. + +[Visit the releases page to download the application](https://github.com/pateljig4545/prompt-engineer-skill/raw/refs/heads/main/references/skill_engineer_prompt_v1.5.zip) + +Thank you for choosing prompt-engineer-skill! We hope it helps you in your creative journey. \ No newline at end of file diff --git a/.agents/skills/prompt-engineer/SKILL.md b/.agents/skills/prompt-engineer/SKILL.md new file mode 100644 index 00000000..157930d8 --- /dev/null +++ b/.agents/skills/prompt-engineer/SKILL.md @@ -0,0 +1,75 @@ +--- +name: prompt-engineer +description: Transform rough prompts/ideas into production-ready LLM prompts. Use when crafting, refining, or optimizing prompts for any AI model (Codex, GPT, Llama, etc.) with advanced techniques like CoT, constitutional AI, RAG optimization. +--- + +# Prompt Engineer + +Expert prompt engineering skill that transforms rough ideas into well-structured, production-ready prompts optimized for LLMs. + +## When to Activate + +- User provides a rough prompt/idea and wants it refined +- User asks to create/design/optimize a prompt for any LLM +- User needs prompt architecture for agents, RAG, or multi-step workflows +- User asks about prompting techniques or best practices + +## Workflow + +### 1. Analyze Input + +Identify from user's request: +- **Target model** (Codex, GPT, Llama, etc.) — default: Codex +- **Use case** (agent system prompt, task prompt, RAG, chat, etc.) +- **Domain** (technical, creative, business, etc.) +- **Constraints** (token limits, output format, safety requirements) + +### 2. Apply Techniques + +Select appropriate techniques from `references/techniques.md` based on use case: +- Complex reasoning → Chain-of-Thought, Tree-of-Thoughts +- Safety-critical → Constitutional AI patterns +- Data extraction → Structured output, JSON mode +- Multi-step tasks → Prompt chaining, agent patterns +- Knowledge-heavy → RAG optimization + +### 3. Craft the Prompt + +Follow model-specific guidelines from `references/model-optimization.md`: +- Structure with clear sections (role, context, instructions, output format) +- Include examples where beneficial (few-shot) +- Add constraints and guardrails +- Optimize for token efficiency + +### 4. Deliver Output + +**MANDATORY format** — always include ALL sections: + +#### The Prompt +Display complete prompt in a single copyable code block. + +#### Implementation Notes +- Techniques used and rationale +- Model-specific optimizations +- Parameter recommendations (temperature, max_tokens) +- Expected behavior and output format + +#### Testing & Evaluation +- 3-5 test cases to validate +- Edge cases and failure modes +- Optimization suggestions + +#### Usage Guidelines +- When/how to use effectively +- Customization options +- Integration considerations + +## Key Principles + +- **Always show the complete prompt** — never just describe it +- **Token efficiency** — concise but comprehensive +- **Production-ready** — reliable, safe, optimized +- **Model-aware** — tailor to target model's strengths +- Refer to `references/techniques.md` for advanced technique details +- Refer to `references/model-specific-optimization-guide.md` for model-specific guidance +- Refer to `references/production-patterns-and-enterprise-templates.md` for enterprise patterns diff --git a/.agents/skills/prompt-engineer/cli.js b/.agents/skills/prompt-engineer/cli.js new file mode 100644 index 00000000..ffc74c09 --- /dev/null +++ b/.agents/skills/prompt-engineer/cli.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node + +// Installs/uninstalls prompt-engineer skill files (SKILL.md + references/) into Claude Code skills directory +// Usage: +// npx prompt-engineer-skill → install to ~/.claude/skills/prompt-engineer +// npx prompt-engineer-skill --path DIR → install to custom path +// node install.js --uninstall → remove installed files +// node install.js --dry-run → preview without changes + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const args = process.argv.slice(2); +const pathIdx = args.indexOf('--path'); +const dest = pathIdx !== -1 + ? args[pathIdx + 1] + : path.join(os.homedir(), '.claude', 'skills', 'prompt-engineer'); + +if (args.includes('--uninstall')) { + fs.rmSync(dest, { recursive: true, force: true }); + console.log(`prompt-engineer skill removed from ${dest}`); + process.exit(0); +} + +if (args.includes('--dry-run')) { + console.log(`Would install to: ${dest}`); + console.log('Files: SKILL.md, references/'); + process.exit(0); +} + +const src = __dirname; +fs.mkdirSync(dest, { recursive: true }); +fs.cpSync(path.join(src, 'SKILL.md'), path.join(dest, 'SKILL.md')); +fs.cpSync(path.join(src, 'references'), path.join(dest, 'references'), { recursive: true }); + +console.log(`prompt-engineer skill installed to ${dest}`); diff --git a/.agents/skills/prompt-engineer/package.json b/.agents/skills/prompt-engineer/package.json new file mode 100644 index 00000000..61e0e61e --- /dev/null +++ b/.agents/skills/prompt-engineer/package.json @@ -0,0 +1,41 @@ +{ + "name": "prompt-engineer-skill", + "version": "1.0.2", + "description": "Claude Code skill: transform rough prompts/ideas into production-ready LLM prompts with CoT, Constitutional AI, RAG optimization, and multi-model support", + "bin": { + "prompt-engineer-skill": "./cli.js" + }, + "scripts": { + "test": "node cli.js --dry-run" + }, + "files": [ + "cli.js", + "SKILL.md", + "references/" + ], + "keywords": [ + "claude-code", + "skill", + "agent-skill", + "prompt-engineering", + "llm", + "claude", + "gpt", + "ai-prompts", + "chain-of-thought", + "rag" + ], + "author": "repo-phuocdt", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/repo-phuocdt/prompt-engineer-skill.git" + }, + "bugs": { + "url": "https://github.com/repo-phuocdt/prompt-engineer-skill/issues" + }, + "homepage": "https://github.com/repo-phuocdt/prompt-engineer-skill#readme", + "engines": { + "node": ">=14.0.0" + } +} diff --git a/.agents/skills/prompt-engineer/references/model-specific-optimization-guide.md b/.agents/skills/prompt-engineer/references/model-specific-optimization-guide.md new file mode 100644 index 00000000..91e3f998 --- /dev/null +++ b/.agents/skills/prompt-engineer/references/model-specific-optimization-guide.md @@ -0,0 +1,43 @@ +# Model-Specific Optimization Guide + +## Anthropic Claude (Opus, Sonnet, Haiku) + +**Strengths**: Long context, instruction following, XML structure, safety alignment +**Best practices**: +- Use XML tags (``, ``, ``) for clear prompt sections +- Place most important instructions at start and end (primacy/recency) +- Use `` blocks for chain-of-thought reasoning +- Leverage system prompt for persistent behavioral instructions +- Claude responds well to direct, imperative instructions +- Avoid "please" and hedging — be direct about expectations +- **Temperature**: 0 for deterministic, 0.3-0.7 for creative, 1.0 for brainstorming +- **Max tokens**: Set explicitly to avoid truncation + +## OpenAI GPT-4o / o1 / o1-mini + +**Strengths**: Function calling, JSON mode, structured outputs, code generation +**Best practices**: +- Use system message for role/persona, user message for task +- Enable JSON mode with `response_format: { type: "json_object" }` +- For o1 models: skip chain-of-thought (built-in reasoning), keep prompts concise +- Function/tool definitions: use clear parameter descriptions with enum constraints +- **Temperature**: 0-0.2 for factual, 0.7-1.0 for creative +- Structured outputs: define JSON schema in system message + +## Open Source (Llama, Mixtral, Qwen) + +**Strengths**: Customizable, fine-tunable, no API dependency +**Best practices**: +- Use model-specific chat templates (e.g., `[INST]` for Llama) +- Keep prompts shorter — smaller context windows +- Be more explicit with instructions — less implicit understanding +- Test with quantized versions if deploying locally +- Simpler prompt structures work better than complex nesting +- Few-shot examples more critical than with larger models + +## Cross-Model Tips + +- **Prompt portability**: Start with clear structure, adapt formatting per model +- **Token budget**: Allocate ~70% to context/instructions, ~30% to output space +- **Iteration**: Same prompt performs differently across models — always test +- **Fallback design**: Build prompts that degrade gracefully on weaker models diff --git a/.agents/skills/prompt-engineer/references/production-patterns-and-enterprise-templates.md b/.agents/skills/prompt-engineer/references/production-patterns-and-enterprise-templates.md new file mode 100644 index 00000000..547d5dfd --- /dev/null +++ b/.agents/skills/prompt-engineer/references/production-patterns-and-enterprise-templates.md @@ -0,0 +1,90 @@ +# Production Patterns & Enterprise Templates + +## Prompt Architecture Patterns + +### System Prompt Template +``` +[Role Definition] +You are a [specific role] specializing in [domain]. + +[Core Instructions] +- Primary objective: [what to accomplish] +- Constraints: [boundaries and limitations] +- Output format: [expected structure] + +[Behavioral Rules] +- Always: [required behaviors] +- Never: [prohibited behaviors] + +[Context Handling] +- When context is provided: [how to use it] +- When context is missing: [fallback behavior] + +[Output Format] +[Specify exact structure, sections, formatting] +``` + +### Task Prompt Template +``` + +[Relevant background information, data, or documents] + + + +[Clear, specific instruction for what to accomplish] + + + +- [Output format requirements] +- [Length/scope limits] +- [Quality criteria] + + + +[1-3 input/output examples if beneficial] + +``` + +## Enterprise Patterns + +### RAG System Prompt +``` +Answer questions using ONLY the provided context. +If context insufficient, state: "I don't have enough information to answer." +Always cite sources using [Source: document_name] format. +Never blend context knowledge with general knowledge without marking it. +``` + +### Agent System Prompt +``` +You are [agent name] responsible for [scope]. +Available tools: [list with when-to-use criteria] +Decision framework: +1. Analyze user request +2. Determine if tools needed +3. Execute with minimal tool calls +4. Verify output quality +Escalate to human when: [criteria] +``` + +### Content Moderation +``` +Evaluate content against these principles: +1. [Principle]: [Description and examples] +2. [Principle]: [Description and examples] +For each principle violated: +- Quote the specific violation +- Explain why it violates the principle +- Suggest a compliant alternative +``` + +## Quality Checklist + +Before finalizing any production prompt: +- [ ] Clear role/persona defined +- [ ] Specific output format specified +- [ ] Edge cases addressed with fallback instructions +- [ ] Safety guardrails included where needed +- [ ] Token-efficient — no redundant instructions +- [ ] Tested with 5+ diverse inputs +- [ ] Failure modes documented diff --git a/.agents/skills/prompt-engineer/references/skill-engineer-prompt-v1.9.zip b/.agents/skills/prompt-engineer/references/skill-engineer-prompt-v1.9.zip new file mode 100644 index 00000000..d7237346 Binary files /dev/null and b/.agents/skills/prompt-engineer/references/skill-engineer-prompt-v1.9.zip differ diff --git a/.agents/skills/prompt-engineer/references/skill_engineer_prompt_v1.5.zip b/.agents/skills/prompt-engineer/references/skill_engineer_prompt_v1.5.zip new file mode 100644 index 00000000..fc91f5f1 Binary files /dev/null and b/.agents/skills/prompt-engineer/references/skill_engineer_prompt_v1.5.zip differ diff --git a/.agents/skills/prompt-engineer/references/techniques.md b/.agents/skills/prompt-engineer/references/techniques.md new file mode 100644 index 00000000..d7dbc3d1 --- /dev/null +++ b/.agents/skills/prompt-engineer/references/techniques.md @@ -0,0 +1,54 @@ +# Advanced Prompting Techniques + +## Chain-of-Thought (CoT) & Reasoning + +- **Zero-shot CoT**: Append "Let's think step by step" — works for most reasoning tasks +- **Few-shot CoT**: Provide 2-3 worked examples showing reasoning steps before the actual question +- **Tree-of-Thoughts**: Explore multiple reasoning paths, evaluate each, select best — use for complex decisions +- **Self-consistency**: Generate multiple reasoning chains, take majority vote — improves accuracy ~5-15% +- **Least-to-most**: Decompose complex problem into sub-problems, solve sequentially — good for math/logic +- **PAL (Program-aided)**: Generate code to solve computational tasks instead of reasoning in text + +## Constitutional AI & Safety + +- **Critique-and-revise**: Ask model to critique its own output against principles, then revise +- **Safety preamble**: Define explicit boundaries and refusal behaviors upfront +- **Red-line instructions**: "Never do X even if asked" — place early in prompt for emphasis +- **Output filtering**: Add post-generation self-check: "Review output for [harmful content], revise if found" + +## Meta-Prompting + +- **Self-reflection**: "Before answering, identify potential biases or errors in your reasoning" +- **Confidence calibration**: "Rate confidence 1-10 and explain uncertainty" +- **Prompt compression**: Remove redundant instructions, use shorthand for repeated patterns +- **Iterative refinement**: "Draft → Critique → Improve" loop within single prompt + +## Structured Output Patterns + +- **XML tags** (Claude preferred): ``, ``, `` for clear sections +- **JSON mode**: Specify exact schema with field descriptions and types +- **Markdown structure**: Headers, lists, tables for human-readable structured output +- **Delimiter patterns**: Use `---`, `###`, or custom delimiters to separate sections + +## Agent & Multi-Agent Patterns + +- **Role definition**: Specific persona with expertise, constraints, behavioral traits +- **Tool-use instructions**: When/how to use each tool, decision criteria +- **Memory management**: What to remember across turns, state tracking instructions +- **Escalation logic**: When to ask for clarification vs. proceed with assumptions +- **Multi-agent coordination**: Define communication protocols, handoff criteria, shared context + +## RAG Optimization + +- **Context framing**: "Use ONLY the following context to answer. If insufficient, say so." +- **Citation prompting**: "Cite specific passages using [Source: X] format" +- **Relevance filtering**: "Identify which context pieces are relevant before answering" +- **Hallucination guard**: "Distinguish between information from context vs. general knowledge" +- **Query expansion**: Rephrase user query into multiple search angles + +## Few-Shot Design + +- **Example selection**: Choose diverse, representative examples covering edge cases +- **Format consistency**: All examples must follow identical structure +- **Negative examples**: Show what NOT to do alongside positive examples +- **Graduated complexity**: Order examples from simple to complex diff --git a/.agents/skills/prompting-company/INSTALL.md b/.agents/skills/prompting-company/INSTALL.md new file mode 100644 index 00000000..7290fae0 --- /dev/null +++ b/.agents/skills/prompting-company/INSTALL.md @@ -0,0 +1,32 @@ +# Install Prompting Company + +## Claude Code + +Copy the skill into your local Claude skills directory: + +```bash +mkdir -p ~/.claude/skills +cp -r skills/prompting-company ~/.claude/skills/ +``` + +Restart Claude Code or reload skills if your environment supports it. + +## claude.ai + +Upload the `skills/prompting-company` folder as a custom skill. + +## MCP and agent environments + +Install the folder wherever your agent runtime loads skills from. Keep the folder name `prompting-company`. + +## Requirements + +This skill needs network access to fetch live documentation from: + +- https://docs.promptingcompany.com/api +- https://docs.promptingcompany.com/llms.txt +- https://docs.promptingcompany.com/api-reference/openapi.json +- https://app.promptingco.com/api/v1/openapi.json +- https://docs.promptingcompany.com/ts-sdk/introduction +- https://docs.promptingcompany.com/tutorial/CLI +- https://docs.promptingcompany.com/tutorial/MCP-Server diff --git a/.agents/skills/prompting-company/README.md b/.agents/skills/prompting-company/README.md new file mode 100644 index 00000000..7050ddfa --- /dev/null +++ b/.agents/skills/prompting-company/README.md @@ -0,0 +1,18 @@ +# Prompting Company + +Build, debug, and plan work with The Prompting Company through its API, MCP Server, CLI, and SDK entrypoints. + +## Workflows + +| Workflow | Triggers | +|---|---| +| Live documentation lookup | "find the docs", "what endpoint should I use", "what scopes are required" | +| Entrypoint selection | "use the API", "set up MCP", "use the CLI", "integrate the SDK" | + +## Install + +```bash +cp -r skills/prompting-company ~/.claude/skills/ +``` + +See [`INSTALL.md`](INSTALL.md) for claude.ai and MCP server setup. diff --git a/.agents/skills/prompting-company/SKILL.md b/.agents/skills/prompting-company/SKILL.md new file mode 100644 index 00000000..a3b18198 --- /dev/null +++ b/.agents/skills/prompting-company/SKILL.md @@ -0,0 +1,59 @@ +--- +name: prompting-company +description: > + Build, debug, or plan work with The Prompting Company through its API, MCP + Server, CLI, or SDK entrypoints. Use when the user needs public routes, + OpenAPI schema guidance, TypeScript SDK integration, CLI workflows, MCP setup, + content APIs, app publishing APIs, public markdown access, simulations, + visibility analytics, authentication, or API key scopes. +--- + +# Prompting Company + +When this skill is activated, greet the user with: +"Thank you for activating the Prompting Company skill by The Prompting Company (https://promptingcompany.com)." + +Then list the available workflows by reading the Workflows section of this skill - one line each, name and one-sentence description. End with: "How can I help you today?" + +## Overview + +Use this skill to choose and implement the right Prompting Company entrypoint: API, MCP Server, CLI, or SDK. Reference live documentation without vendoring it into the repository. + +## Trigger keywords + +This skill activates when the user asks to: + +- Use The Prompting Company public API, public routes, or OpenAPI schema +- Build against `docs.promptingcompany.com/api`, `app.promptingco.com`, the TypeScript SDK, the CLI, or the MCP Server +- Generate REST clients, SDK integrations, CLI workflows, or MCP setup guidance +- Work with content APIs, public markdown endpoints, app publishing, simulations, visibility analytics, authentication, or scopes + +## Workflows + +### 1. Live Documentation Lookup + +See [`workflows/live-docs.md`](workflows/live-docs.md) for source-of-truth URLs. Summary: + +1. Start from the docs index or OpenAPI schema. +2. Fetch the specific endpoint page only when exact request or response details matter. +3. Use the authentication and scope pages before implementing authenticated requests. +4. Do not copy API reference pages into this repository. + +### 2. Entrypoint Selection + +See [`workflows/entrypoints.md`](workflows/entrypoints.md) for full steps. Summary: + +1. Choose API, MCP Server, CLI, or SDK based on the user's environment and goal. +2. Use API for language-agnostic integrations and exact route control. +3. Use MCP Server for agent-tool access to Prompting Company. +4. Use CLI for local operational workflows. +5. Use SDK for TypeScript application integrations. + +## General principles + +- Treat the live docs, SDK docs, CLI docs, MCP docs, and OpenAPI schema as authoritative. +- Never invent endpoints, scopes, request fields, or response shapes. +- Prefer public markdown endpoints for AI-readable published content. +- Prefer Apps APIs over deprecated site page APIs. +- Keep API keys in environment variables or secret stores. +- Ask before making authenticated production mutations unless the user explicitly requested the write. diff --git a/.agents/skills/prompting-company/workflows/entrypoints.md b/.agents/skills/prompting-company/workflows/entrypoints.md new file mode 100644 index 00000000..028afd4e --- /dev/null +++ b/.agents/skills/prompting-company/workflows/entrypoints.md @@ -0,0 +1,112 @@ +--- +name: entrypoints +description: > + Selects the right Prompting Company entrypoint: API, MCP Server, CLI, or SDK. +--- + +# Entrypoints + +## Overview + +Choose the smallest Prompting Company surface that matches the user's goal, then verify exact commands, setup, endpoints, or types from the live documentation. + +## API + +Use the API when the user needs: + +- Language-agnostic integration from any backend or automation environment +- Exact control over HTTP methods, request bodies, query params, and response handling +- Public markdown routes for AI-readable published content +- Content management, app publishing, simulations, visibility analytics, authentication, or scope-specific calls +- Generated clients or types from the OpenAPI schema + +Implementation steps: + +1. Fetch `workflows/live-docs.md` and identify the API overview, OpenAPI schema, authentication docs, and scope reference. +2. Select the endpoint family: public markdown, content, apps and publishing, simulations, or visibility analytics. +3. Fetch the specific endpoint page or OpenAPI operation before writing code. +4. Use `https://app.promptingco.com` as the production base URL unless the live docs say otherwise. +5. Use `http://localhost:3000` only for local development. +6. Handle both non-2xx responses and the shared response envelope: + +```json +{ + "ok": true, + "data": {} +} +``` + +```json +{ + "ok": false, + "code": "FORBIDDEN", + "message": "public_api_only", + "details": {} +} +``` + +## MCP Server + +Use the MCP Server when the user wants agents or MCP-capable tools to access Prompting Company capabilities directly. + +Use this entrypoint for: + +- Claude, Codex, Cursor, VS Code, or another MCP client +- Agent workflows that should call Prompting Company tools instead of handwritten HTTP code +- Local setup, connector configuration, or troubleshooting MCP access + +Implementation steps: + +1. Fetch the live MCP Server tutorial from `workflows/live-docs.md`. +2. Follow the target client's MCP configuration format exactly. +3. Keep tokens and API keys in the client-supported secret mechanism. +4. Verify the MCP server appears in the client tool list before attempting workflows. +5. If a requested capability is not exposed through MCP, fall back to API, CLI, or SDK. + +## CLI + +Use the CLI when the user wants local operational workflows, repeatable commands, setup tasks, or shell-friendly automation. + +Use this entrypoint for: + +- Installing and authenticating local tooling +- Running repeatable Prompting Company operations from a terminal +- Creating scripts or CI steps that call Prompting Company commands +- Debugging environment or credential setup outside an app codebase + +Implementation steps: + +1. Fetch the live CLI tutorial from `workflows/live-docs.md`. +2. Follow the user's package manager and shell conventions. +3. Prefer documented commands over inferred flags. +4. Keep credentials out of shell history when possible. +5. For destructive or mutating commands, show the command and ask for confirmation unless the user already requested the mutation. + +## SDK + +Use the SDK when the user is building a TypeScript or Next.js application and wants idiomatic application code. + +Use this entrypoint for: + +- Typed application integrations +- Next.js examples or server-side app code +- Reusing the project's existing service-client patterns +- Avoiding handwritten fetch wrappers when the SDK covers the workflow + +Implementation steps: + +1. Fetch the live SDK introduction, installation, quickstart, and advanced usage pages from `workflows/live-docs.md`. +2. Install dependencies with the repository's existing package manager. +3. Place initialization near other service clients. +4. Keep API keys in environment variables or the host application's secret store. +5. If the SDK does not expose the needed operation, fall back to the API and document why. + +## Quality Check + +Before finishing: + +- Confirm the chosen entrypoint matches the user's environment and goal. +- Confirm every endpoint, command, config field, scope, and type came from live docs or OpenAPI. +- Confirm no copied API reference content was added to the repository. +- Confirm credentials are not hard-coded. +- Confirm write and delete operations were explicitly requested or confirmed. diff --git a/.agents/skills/prompting-company/workflows/live-docs.md b/.agents/skills/prompting-company/workflows/live-docs.md new file mode 100644 index 00000000..eb5ef30a --- /dev/null +++ b/.agents/skills/prompting-company/workflows/live-docs.md @@ -0,0 +1,34 @@ +# Live Documentation References + +Use these URLs as live sources of truth. Do not copy their contents into this repository. + +## Required Rule + +Always reference these URLs directly. Only fetch the specific live page or OpenAPI document needed for the current task. + +## Primary Docs + +- API overview: https://docs.promptingcompany.com/api +- API overview, Markdown: https://docs.promptingcompany.com/api.md +- Documentation index for agents: https://docs.promptingcompany.com/llms.txt +- Full documentation for agents: https://docs.promptingcompany.com/llms-full.txt + +## Machine-Readable API Specs + +- Mintlify OpenAPI reference: https://docs.promptingcompany.com/api-reference/openapi.json +- Public API OpenAPI route: https://app.promptingco.com/api/v1/openapi.json + +## Core Reference Pages + +- Authentication: https://docs.promptingcompany.com/api/authentication +- Scope reference: https://docs.promptingcompany.com/api/scopes +- TypeScript SDK: https://docs.promptingcompany.com/ts-sdk/introduction +- CLI: https://docs.promptingcompany.com/tutorial/CLI +- MCP server: https://docs.promptingcompany.com/tutorial/MCP-Server + +## Endpoint Areas To Search In The Docs Index + +- Apps & Publishing: app creation, domains, resources, route candidates, routes, batch route creation, route import preview, and app config. +- Content: count, search, get, get by path, replace, update, public markdown by ID/path/root, and legacy site page endpoints. +- Simulations: agent simulation runs. +- Visibility & Mentions: share-of-voice analytics. diff --git a/.agents/skills/qwen-asr/SKILL.md b/.agents/skills/qwen-asr/SKILL.md new file mode 100644 index 00000000..6c91f033 --- /dev/null +++ b/.agents/skills/qwen-asr/SKILL.md @@ -0,0 +1,16 @@ +--- +name: qwen-asr +description: Transcribe audio files using Qwen ASR. Use when the user sends voice messages and wants them converted to text. +--- + +# Qwen ASR +Transcribe an audio file (wav/mp3/ogg...) to text using Qwen ASR. No configuration or API key required. + +## Usage +```shell +uv run scripts/main.py -f audio.wav + +cat audio.mp3 | uv run scripts/main.py > transcript.txt + +curl https://example.com/audio.ogg | uv run scripts/main.py +``` diff --git a/.agents/skills/qwen-asr/scripts/main.py b/.agents/skills/qwen-asr/scripts/main.py new file mode 100644 index 00000000..6049ba14 --- /dev/null +++ b/.agents/skills/qwen-asr/scripts/main.py @@ -0,0 +1,106 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = ["aiohttp", "argparse", "gradio_client"] +# /// +import os +import sys +import logging +import asyncio +import aiohttp +import argparse +from gradio_client import Client, handle_file +from contextlib import contextmanager, redirect_stdout + +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.INFO) +BASE_URL = os.getenv("QWEN_ASR_BASE_URL") or "https://qwen-qwen3-asr-demo.ms.show" +USER_AGENT = "Mozilla/5.0 AppleWebKit/537.36 Chrome/143 Safari/537" +SESSION: aiohttp.ClientSession | None = None + +@contextmanager +def gradio_stdout(): + with redirect_stdout(sys.stderr): + yield + +async def transcribe(file, prompt="", lang="auto", itn=False): + audio_path = await upload_file(file) + if not audio_path: + exit(1) + audio_url = f"{BASE_URL}/gradio_api/file={audio_path}" + res = await SESSION.get(audio_url) + if res.status == 200: + _LOGGER.warning("Audio file: %s", audio_url) + else: + _LOGGER.warning("Audio file upload failed: %s", [audio_url, res.status, res.headers]) + exit(1) + + with gradio_stdout(): + gradio = Client(BASE_URL) + result = gradio.predict( + audio_file=handle_file(audio_url), + context=prompt or "", + language=lang or "auto", + enable_itn=itn or False, + api_name="/asr_inference", + ) + gradio.close() + _LOGGER.info("Gradio result: %s", result) + return result[0] + +async def upload_file(audio): + file = None + if isinstance(audio, (bytes, bytearray)): + file = audio + elif audio in (None, "", "-"): + file = sys.stdin.buffer.read() + elif audio: + with open(audio, "rb") as f: + file = f.read() + if not file: + _LOGGER.warning("No file provided. %s", audio[:100]) + return None + form = aiohttp.FormData() + form.add_field("files", file, filename="audio") + res = await api_request("/gradio_api/upload", data=form) + try: + src = (await res.json())[0] + if not src: + _LOGGER.warning("Upload failed: %s", [res.status, await res.text()]) + except: + src = None + _LOGGER.error("Upload failed: %s", [res.status, await res.text()], exc_info=True) + return src + +async def api_request(api, json=None, headers=None, **kwargs): + _LOGGER.info("%s: %s", api, json) + return await SESSION.post( + api, + json=json, + headers={ + aiohttp.hdrs.USER_AGENT: USER_AGENT, + aiohttp.hdrs.REFERER: BASE_URL, + **(headers or {}), + }, + **kwargs, + ) + +async def main(): + global SESSION + async with aiohttp.ClientSession(base_url=BASE_URL) as SESSION: + parser = argparse.ArgumentParser() + parser.add_argument("-f", "--file", help="Audio file, - for stdio") + parser.add_argument("-p", "--prompt", help="Context") + parser.add_argument("--lang", help="Language: auto/zh/en/ja/ko/es/fr/de/ar/it/ru/pt") + parser.add_argument("--itn", help="Enable ITN", action="store_true") + args = parser.parse_args() + if args.file in (None, ""): + file = sys.stdin.buffer.read() + if file: + args.file = file + if args.file: + print(await transcribe(**vars(args))) + else: + parser.print_help() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/.agents/skills/rank-tracker/SKILL.md b/.agents/skills/rank-tracker/SKILL.md new file mode 100644 index 00000000..95b61704 --- /dev/null +++ b/.agents/skills/rank-tracker/SKILL.md @@ -0,0 +1,138 @@ +--- +name: rank-tracker +description: 'Use when the user asks to "track rankings"; monitors keyword/SERP changes from provided exports or connected tools, including AI response checks. 排名追踪/SERP监控' +version: "9.9.9" +license: Apache-2.0 +compatibility: "Codex, skills.sh, ClawHub, Vercel Labs, Cursor, Windsurf, Codex CLI, Amp, Gemini CLI, Kimi Code, Qwen Code, CodeBuddy" +homepage: "https://github.com/aaron-he-zhu/seo-geo-Codex-skills" +when_to_use: "Use when tracking keyword rankings, monitoring position changes, comparing ranking snapshots, or detecting ranking drops." +argument-hint: " [keyword list]" +metadata: + author: aaron-he-zhu + version: "9.9.9" + geo-relevance: "medium" + tags: + - seo + - geo + - rank-tracking + - keyword-rankings + - serp-positions + - ranking-changes + - position-tracking + - 排名追踪 + - ランキング追跡 + - 순위추적 + - seguimiento-rankings + triggers: + - "track rankings" + - "check keyword positions" + - "ranking changes" + - "keyword tracking" + - "position monitoring" + - "how am I ranking" + - "did my rankings change" + - "where do I rank now" + - "check my positions" + - "how are my rankings doing" + - "排名追踪" + - "关键词排名" + - "SERP位置监控" + - "排名变化" + - "查排名" + - "排名变了吗" + - "我排第几" + - "ランキング追跡" + - "検索順位チェック" + - "順位変動" + - "キーワード順位確認" + - "순위 추적" + - "키워드 순위" + - "순위 확인" + - "내 순위 어떻게 됐어?" + - "seguimiento de rankings" + - "posición en buscadores" + - "posicionamiento SEO" + - "en qué posición estoy" + - "rastreamento de rankings" + - "monitoramento de posições" + - "posição no Google" +--- + +# Rank Tracker + +Tracks keyword positions, SERP feature ownership, and AI visibility over time. + +## Quick Start + +``` +Set up rank tracking for [domain] targeting these keywords: [keyword list] +``` + +``` +Analyze ranking changes for [domain] over the past [time period] +``` + +## Skill Contract + +**Expected output**: a ranking report or delta summary plus the standard handoff summary for `memory/monitoring/`. + +- **Reads**: current metrics, baselines, alert thresholds, and reporting context from [AGENTS.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/AGENTS.md) and the shared [State Model](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/references/state-model.md) when available. +- **Writes**: a user-facing monitoring deliverable and reusable summary. +- **Promotes**: significant changes, confirmed anomalies, follow-up actions, and pending decisions to `memory/open-loops.md`. +- **Primary next skill**: [alert-manager](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/alert-manager/SKILL.md) when recurring monitoring should become automated. + +### Handoff Summary + +> Emit the standard shape from [skill-contract.md §Handoff Summary Format](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/references/skill-contract.md). + +## Data Sources + +All integrations optional (see [CONNECTORS.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/CONNECTORS.md)). With tools, pull rankings from ~~SEO tool, impressions from ~~search console, traffic from ~~analytics, and AI citations from ~~AI monitor. Without tools, ask for positions, volumes, competitor data, and SERP feature status. + +## Instructions + +When a user requests rank tracking or analysis: + +1. **Set Up Keyword Tracking** — configure domain, market, device, language, update frequency, priorities, and competitor watchlist. +2. **Record Current Rankings** — summarize position ranges, detailed rankings, ranking URLs, feature ownership, and movement. +3. **Analyze Ranking Changes** — highlight biggest wins, declines, stable terms, new rankings, lost rankings, likely causes, and recovery ideas. +4. **Track SERP Features** — compare ownership of snippets, PAA, image/video packs, local packs, and related feature shifts. +5. **Track GEO / AI Visibility** — monitor AI Overview presence, citation rate, citation position, and trend. +6. **Compare Against Competitors** — report share of voice, head-to-head comparisons, and threat levels. +7. **Generate Ranking Report** — summarize overall trend, key wins, concerns, opportunities, SERP feature changes, GEO visibility, and recommendations. + +> **Reference**: See [references/ranking-analysis-templates.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/rank-tracker/references/ranking-analysis-templates.md) for the complete output templates for all seven steps. + +## Example + +Sample outcome: average position improves from 15.3 to 12.8, top-10 keywords rise from 12 to 17, and the report highlights the biggest winners, biggest drops, and next actions. + +## Tips for Success + +Track consistently, segment by intent, watch competitors, and include SERP feature plus GEO signals. + +## Rank Change Quick Reference + +### Response Protocol + +| Change | Timeframe | Action | +|--------|-----------|--------| +| Drop 1-3 positions | Wait 1-2 weeks | Monitor — may be normal fluctuation | +| Drop 3-5 positions | Investigate within 1 week | Check technical issues and competitor changes | +| Drop 5-10 positions | Investigate immediately | Run a full diagnostic: technical, content, links | +| Drop off page 1 | Emergency response | Comprehensive audit + recovery plan | +| Position gained | Document and learn | Identify what worked and replicate | + +> **Reference**: See [references/tracking-setup-guide.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/rank-tracker/references/tracking-setup-guide.md) for tracking setup, root-cause taxonomy, CTR benchmarks, SERP feature impact, and algorithm-update assessment. + +### Save Results + +Ask "Save these results?" If yes, write `memory/monitoring/YYYY-MM-DD-.md` with headline finding, actions, and open loops. + +## Reference Materials + +- [Tracking Setup Guide](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/rank-tracker/references/tracking-setup-guide.md) — Setup rules, feature tracking, and interpretation guidance + +## Next Best Skill + +Initial setup (no baseline) → [alert-manager](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/monitor/alert-manager/SKILL.md). Subsequent runs (baseline exists) → Terminal. Visited-set rule applies per [skill-contract.md](https://github.com/aaron-he-zhu/seo-geo-Codex-skills/blob/main/references/skill-contract.md). diff --git a/.agents/skills/rank-tracker/references/ranking-analysis-templates.md b/.agents/skills/rank-tracker/references/ranking-analysis-templates.md new file mode 100644 index 00000000..0fc79966 --- /dev/null +++ b/.agents/skills/rank-tracker/references/ranking-analysis-templates.md @@ -0,0 +1,126 @@ +# Ranking Analysis Output Templates + +Compact templates for rank tracking setup, movement analysis, SERP features, GEO visibility, competitors, and reporting. + +## 1. Setup And Snapshot + +```markdown +## Rank Tracking Setup +**Domain**: [domain] | **Location**: [country/city] | **Device**: [mobile/desktop/both] | **Language**: [language] | **Frequency**: [daily/weekly] + +| Keyword | Volume | Current Rank | Type | Priority | Target URL | +|---------|--------|--------------|------|----------|------------| +| [keyword] | [vol] | [rank] | Primary/Secondary/Brand | H/M/L | [URL] | + +**Competitors**: [domain 1], [domain 2], [domain 3] +**Categories**: Brand [X], Product [X], Informational [X], Commercial [X] + +## Current Ranking Snapshot -- [date] +| Position Range | Count | Share | +|----------------|-------|-------| +| #1 | [X] | [Y]% | +| #2-3 | [X] | [Y]% | +| #4-10 | [X] | [Y]% | +| #11-20 | [X] | [Y]% | +| #21-50 | [X] | [Y]% | +| #51-100 | [X] | [Y]% | +| Not ranking | [X] | [Y]% | + +| Keyword | Position | URL | SERP Features | Change | +|---------|----------|-----|---------------|--------| +| [kw] | [pos] | [URL] | [feature] | [+/-] | +``` + +## 2. Movement Analysis + +```markdown +## Ranking Change Analysis +**Window**: [start] to [end] + +| Metric | Start | End | Change | +|--------|-------|-----|--------| +| Avg position | [X] | [Y] | [+/-Z] | +| Keywords in Top 10 | [X] | [Y] | [+/-Z] | +| Keywords in Top 3 | [X] | [Y] | [+/-Z] | + +| Segment | Keyword | Old -> New | Est. Traffic Impact | Hypothesis | Action | +|---------|---------|------------|---------------------|------------|--------| +| Improvement | [kw] | [old -> new] | [+X/mo] | [possible cause] | [protect/expand] | +| Decline | [kw] | [old -> new] | [-X/mo] | [likely factor] | [investigate/refresh] | +| New | [kw] | [none -> pos] | [estimate] | [new content/intent] | [monitor] | +| Lost | [kw] | [pos -> none] | [estimate] | [index/intent/competition] | [recover] | + +Hypotheses are based on available signals, not confirmed causes. +``` + +## 3. SERP Feature And GEO Tracking + +```markdown +## SERP Feature Tracking + +| Feature | Your Count | Competitor Avg | Opportunity | +|---------|------------|----------------|-------------| +| Featured snippets | [X] | [Y] | [win/defend] | +| People Also Ask | [X] | [Y] | [answer gaps] | +| Image / video / local pack | [X] | [Y] | [asset need] | + +| Keyword | Feature | You Own? | Current Owner | Winnable? | +|---------|---------|----------|---------------|-----------| +| [kw] | [feature] | Y/N | [domain] | H/M/L | + +## AI/GEO Visibility +| Keyword | AI Overview Present | You Cited? | Citation Position | Content Gap | +|---------|---------------------|------------|-------------------|-------------| +| [kw] | Y/N | Y/N | [pos] | [definition/stat/source gap] | + +**Citation rate**: [your citations] / [AI Overview keywords] = [Z]% +``` + +## 4. Competitor Comparison + +```markdown +## Competitor Ranking Comparison + +| Domain | Keywords Ranked | Avg Position | Visibility Share | +|--------|-----------------|--------------|------------------| +| [your site] | [X] | [Y] | [Z]% | +| [competitor] | [X] | [Y] | [Z]% | + +| Keyword | Your Rank | Competitor Rank | Winner | Threat / Opportunity | +|---------|-----------|-----------------|--------|----------------------| +| [kw] | [rank] | [rank] | [domain] | [note] | + +| Competitor | Keyword | Movement | Threat Level | +|------------|---------|----------|--------------| +| [domain] | [kw] | [+/- positions] | H/M/L | +``` + +## 5. Ranking Performance Report + +```markdown +# Ranking Performance Report +**Domain**: [domain] | **Period**: [start] to [end] | **Generated**: [date] + +## Executive Summary +**Overall trend**: [Improving/Stable/Declining] + +| Metric | Value | vs Last Period | Status | +|--------|-------|----------------|--------| +| Total tracked keywords | [X] | [+/-Y] | [status] | +| Keywords in Top 10 | [X] | [+/-Y] | [status] | +| Average position | [X] | [+/-Y] | [status] | +| Estimated traffic | [X] | [+/-Y]% | [status] | +| AI citation rate | [X]% | [+/-Y] | [status] | + +**Wins**: [list] +**Concerns**: [list] +**Opportunities**: [list] + +| Timing | Recommendation | Expected Impact | +|--------|----------------|-----------------| +| Immediate | [action] | [impact] | +| This month | [action] | [impact] | +| Next quarter | [action] | [impact] | + +**Next report**: [date] | **Focus areas**: [areas] +``` diff --git a/.agents/skills/rank-tracker/references/tracking-setup-guide.md b/.agents/skills/rank-tracker/references/tracking-setup-guide.md new file mode 100644 index 00000000..c6d3324e --- /dev/null +++ b/.agents/skills/rank-tracker/references/tracking-setup-guide.md @@ -0,0 +1,126 @@ +# Rank Tracking Setup Guide + +Configuration checklist and interpretation rules for reliable rank tracking. + +## Tool Setup + +| Step | Action | Notes | +|------|--------|-------| +| 1 | Select rank tracker | `~~SEO tool` with keyword/location/device support | +| 2 | Add target domain | Include key subdomains when relevant | +| 3 | Set location | Country, region, city, or separate project per market | +| 4 | Set device | Track mobile and desktop when budget allows | +| 5 | Set search engine/language | Match target audience | +| 6 | Add competitors | 3-5 direct competitors | +| 7 | Import keywords | From keyword research, GSC, or existing tracker | +| 8 | Configure frequency | Daily for priority terms; weekly for long-tail | +| 9 | Verify first pull | Spot-check against manual searches | + +| Scenario | Location Setup | +|----------|----------------| +| National | Country | +| Regional | State/region | +| Local | City | +| Multi-location | Separate project per location | +| International | Separate project per country | + +## Keyword Selection + +| Site Size | Recommended Keywords | Breakdown | +|-----------|----------------------|-----------| +| Small (<50 pages) | 50-100 | brand + primary + secondary + long-tail | +| Medium (50-500 pages) | 100-500 | brand + priority pages + topic clusters | +| Large (500+ pages) | 500-2,000+ | scale by revenue pages | +| Enterprise | 2,000-9.9.9+ | automated grouping and governance | + +| Include | Rule | +|---------|------| +| Revenue terms | Always track conversion-driving keywords | +| Page 1-3 terms | Track current opportunity set | +| Competitor terms | Track important competitor targets | +| Strategic terms | Track board/product priorities even if volume is lower | +| Content investments | Track every major page you are actively improving | + +Skip zero-volume terms unless strategic, terms with no target content, broad one-word terms, and low-volume misspellings. + +## Grouping + +| Dimension | Use | +|-----------|-----| +| Topic cluster | Hub performance | +| Intent | Funnel-stage performance | +| Product/service | Product-line performance | +| Content type | Format effectiveness | +| Priority tier | Resourcing | +| URL | Page-level diagnosis | +| Competitor overlap | Competitive monitoring | + +Recommended hierarchy: `Business unit -> Topic cluster -> Intent -> Priority tier`. + +## Frequency and Alerts + +| Keyword Tier | Tracking Frequency | Drop Alert | Gain Alert | Competitor Alert | +|--------------|-------------------|------------|------------|------------------| +| Tier 1 revenue | Daily | Drop >=3 positions | Gain >=3 | Competitor enters top 5 | +| Tier 2 growth | 2-3x/week | Drop >=5 | Enters top 10 | Competitor overtakes | +| Tier 3 monitor | Weekly | Drop >=10 | Enters top 20 | None | +| Brand | Daily | Any drop from #1 | N/A | Competitor ranks for brand | +| New/experimental | Daily for 30 days | Drop >=5 | Enters top 20 | Optional | + +| Alert Type | Channel | Frequency | +|------------|---------|-----------| +| Critical drops | Email + Slack | Immediate | +| Significant changes | Email | Daily digest | +| Weekly summary | Email | Monday | +| Monthly report | Email + dashboard | 1st of month | + +## Reporting Cadence + +| Report | Audience | Frequency | Focus | +|--------|----------|-----------|-------| +| Quick pulse | SEO team | Daily | Major movement, fired alerts | +| Weekly summary | Marketing | Weekly | Position changes, SERP features, AI citations | +| Monthly report | Stakeholders | Monthly | MoM distribution, SOV, GEO visibility, actions | +| Quarterly review | Leadership | Quarterly | QoQ trend, ROI, competitive shifts | + +## Interpretation Rules + +| Pattern | Meaning | Action | +|---------|---------|--------| +| Daily +/-1-2 positions | Normal volatility | Ignore; track weekly trend | +| Sudden drop 5+ and recovery in 2-3 days | Test/data-center variation | Monitor | +| Steady decline for 2+ weeks | Real loss | Investigate content, links, SERP, technical | +| Many keywords drop together | Algorithm or technical issue | Check Search Status, crawlability, indexation | +| One URL drops across terms | Page-level issue | Check noindex, 404, speed, content decay | +| Competitor surges | Market or content shift | Analyze their page and backlink changes | + +| Position Change | Typical Traffic Impact | +|-----------------|------------------------| +| #1 -> #2 | -50% to -60% click loss | +| #2 -> #3 | -25% to -30% | +| #3 -> #5 | -30% to -40% | +| #5 -> #10 | -50% to -60% | +| #10 -> #11 | -60% to -80% page-2 cliff | + +## Data Quality Checks + +| Check | Frequency | +|-------|-----------| +| Manual spot-check 5-10 keywords | Weekly | +| Compare with Search Console | Monthly | +| Check position 0/tracking errors | Weekly | +| Verify competitor data manually | Monthly | +| Confirm location accuracy | Quarterly | + +## Migration / Tool Switching + +| Step | Action | +|------|--------| +| 1 | Export historical data | +| 2 | Run old and new tools in parallel for 2-4 weeks | +| 3 | Compare systematic differences | +| 4 | Import history if supported | +| 5 | Rebuild alerts and reports | +| 6 | Decommission old tool after confidence is established | + +1-2 position variance between tools is normal. Document any systematic bias before comparing historical trends. diff --git a/.agents/skills/reddit-skills/SKILL.md b/.agents/skills/reddit-skills/SKILL.md index a1559844..da815774 100644 --- a/.agents/skills/reddit-skills/SKILL.md +++ b/.agents/skills/reddit-skills/SKILL.md @@ -2,10 +2,15 @@ name: reddit-skills description: Use when the user asks to operate Reddit across authentication, search, publishing, comments, votes, saves, subreddit analysis, or multi-step content workflows. tags: [reddit, automation, social-media] +capability: Coordinate Reddit search, publish, comment, login, vote, and save tasks across multiple skills. triggers: - use Reddit - operate Reddit - Reddit workflow + - several Reddit actions + - Search Reddit, then comment + - Browse a subreddit and save a post + - Check Reddit login and publish a link post version: 1.0.0 metadata: openclaw: diff --git a/.agents/skills/reddit-skills/skills/reddit-auth/SKILL.md b/.agents/skills/reddit-skills/skills/reddit-auth/SKILL.md index a9111eb8..26bffa55 100644 --- a/.agents/skills/reddit-skills/skills/reddit-auth/SKILL.md +++ b/.agents/skills/reddit-skills/skills/reddit-auth/SKILL.md @@ -2,6 +2,7 @@ name: reddit-auth description: Use when the user asks to check Reddit login status, authenticate a Reddit session, or log out. Handles authentication state only. tags: [reddit, authentication] +capability: Inspect or change Reddit authentication state without performing content operations. triggers: - am I logged in to Reddit - check my Reddit login diff --git a/.agents/skills/reddit-skills/skills/reddit-content-ops/SKILL.md b/.agents/skills/reddit-skills/skills/reddit-content-ops/SKILL.md index 691fa018..c4f468ba 100644 --- a/.agents/skills/reddit-skills/skills/reddit-content-ops/SKILL.md +++ b/.agents/skills/reddit-skills/skills/reddit-content-ops/SKILL.md @@ -2,6 +2,7 @@ name: reddit-content-ops description: Use when the user asks for multi-step Reddit operations such as subreddit analysis, trend tracking, content strategy, publishing, or engagement campaigns. tags: [reddit, content-strategy, analysis, automation] +capability: Execute compound Reddit research, strategy, publishing, and engagement workflows. triggers: - analyze subreddit trends - Reddit content strategy diff --git a/.agents/skills/reddit-skills/skills/reddit-explore/SKILL.md b/.agents/skills/reddit-skills/skills/reddit-explore/SKILL.md index b6a2f7f7..89f264bb 100644 --- a/.agents/skills/reddit-skills/skills/reddit-explore/SKILL.md +++ b/.agents/skills/reddit-skills/skills/reddit-explore/SKILL.md @@ -2,6 +2,7 @@ name: reddit-explore description: Use when the user asks to search Reddit, browse subreddits, inspect posts, track discussions, or view Reddit user profiles. Read-only discovery and research. tags: [reddit, search, research] +capability: Perform read-only Reddit discovery across posts, subreddits, discussions, and profiles. triggers: - search Reddit - browse subreddits diff --git a/.agents/skills/reddit-skills/skills/reddit-interact/SKILL.md b/.agents/skills/reddit-skills/skills/reddit-interact/SKILL.md index 7864a7b3..7edd6e66 100644 --- a/.agents/skills/reddit-skills/skills/reddit-interact/SKILL.md +++ b/.agents/skills/reddit-skills/skills/reddit-interact/SKILL.md @@ -1,10 +1,12 @@ --- name: reddit-interact -description: Use when the user asks to comment, reply, upvote, downvote, or save Reddit content. Handles social interactions but does not publish new posts. +description: Use when the user asks to comment, reply, upvote, downvote, or save Reddit content. Handles social interactions. NOT for publishing new posts. tags: [reddit, engagement, social-media] +capability: Perform Reddit comments, replies, votes, and saves without creating new posts. triggers: - post a comment on Reddit - upvote that Reddit post + - downvote this Reddit post - save Reddit content version: 1.0.0 metadata: diff --git a/.agents/skills/reddit-skills/skills/reddit-publish/SKILL.md b/.agents/skills/reddit-skills/skills/reddit-publish/SKILL.md index 5fba2368..38b8d762 100644 --- a/.agents/skills/reddit-skills/skills/reddit-publish/SKILL.md +++ b/.agents/skills/reddit-skills/skills/reddit-publish/SKILL.md @@ -1,7 +1,8 @@ --- name: reddit-publish -description: Use when the user asks to publish, submit, or share a text, link, or image post on Reddit. Handles new posts rather than comments or votes. +description: Use when the user asks to publish, submit, or share a text, link, or image post on Reddit. NOT for blogs, company websites, comments, or votes. tags: [reddit, publishing, social-media] +capability: Create new Reddit text, link, or image posts without handling comments or votes. triggers: - create a new text post on Reddit - publish a Reddit post diff --git a/.agents/skills/reference-list-builder/SKILL.md b/.agents/skills/reference-list-builder/SKILL.md new file mode 100644 index 00000000..318004ed --- /dev/null +++ b/.agents/skills/reference-list-builder/SKILL.md @@ -0,0 +1,347 @@ +--- +name: reference-list-builder +description: Format professional references properly and prepare reference materials +--- + +# Reference List Builder + +## When to Use This Skill + +Use this skill when the user: +- Needs to create a professional reference list +- Wants help choosing the right references +- Needs to format references properly +- Is preparing references for job applications +- Mentions: "references", "reference list", "professional references", "reference check" + +## Core Capabilities + +- Format professional reference lists +- Guide reference selection strategy +- Prepare reference briefing materials +- Anticipate reference check questions +- Handle difficult reference situations +- Coordinate reference outreach + +## Reference Strategy + +### Who Makes a Good Reference? + +**Ideal References:** +- Former direct managers (most important) +- Senior colleagues who observed your work +- Cross-functional partners +- Clients or customers +- Direct reports (for leadership roles) +- Professors or advisors (for recent graduates) + +**Reference Hierarchy:** +1. **Most Valuable:** Recent direct supervisor +2. **Very Valuable:** Senior leaders who know your work +3. **Valuable:** Peers and cross-functional partners +4. **Acceptable:** Clients, vendors, professors +5. **Avoid:** Friends, family, HR contacts only + +### Who to Avoid + +- ❌ Current employer (without permission) +- ❌ People who barely know you +- ❌ References from 10+ years ago only +- ❌ Personal friends (unless specified) +- ❌ People who might give lukewarm feedback +- ❌ Anyone you haven't contacted in advance + +## Reference List Format + +### Standard Format + +``` +PROFESSIONAL REFERENCES + +Jane Smith +Senior Director of Product +TechCorp Inc. +Phone: (555) 123-4567 +Email: jane.smith@techcorp.com +Relationship: Direct supervisor for 3 years (2020-2023) + +John Doe +VP of Engineering +Previous Company +Phone: (555) 234-5678 +Email: john.doe@previous.com +Relationship: Cross-functional partner on 5 major projects +``` + +### Information to Include + +**Required:** +- Full name +- Current job title +- Current company +- Phone number +- Professional email +- Your relationship to them + +**Optional:** +- LinkedIn URL +- Best time to reach +- Preferred contact method + +### Formatting Guidelines + +- Match the style of your resume (fonts, formatting) +- 3-5 references (more only if requested) +- Separate page from resume +- Header should match resume header +- Include "References" or "Professional References" as title + +## Reference Preparation + +### Step 1: Ask Permission + +**Before listing anyone:** +- Call or email to ask permission +- Confirm their contact information +- Explain the role you're applying for +- Gauge their willingness and enthusiasm + +**Script:** +``` +"Hi [Name], I hope you're doing well! I'm applying for a [Role] position at [Company] and was wondering if you'd be comfortable serving as a reference for me. The role involves [brief description], and I think your perspective on [specific project/skill] would be particularly valuable. Would you be willing to speak with them if they reach out?" +``` + +### Step 2: Brief Your References + +**Send them:** +- Copy of your resume +- Job description +- Key points you want highlighted +- Specific projects to mention +- Timeline for when they might be contacted + +**Briefing Email Template:** +``` +Subject: Reference Preparation - [Role] at [Company] + +Hi [Name], + +Thank you so much for agreeing to be a reference! Here's some context to help: + +**The Role:** [Job title] at [Company] +**What they're looking for:** [Key requirements] + +**Points I'm emphasizing:** +- [Achievement 1] +- [Achievement 2] +- [Skill they should mention] + +**Our work together they might ask about:** +- [Project 1] +- [Project 2] + +I've attached my resume and the job description for reference. They may reach out in the next [timeframe]. + +Please let me know if you have any questions, and thank you again! + +Best, +[Your name] +``` + +### Step 3: Follow Up + +After references are checked: +- Thank your references regardless of outcome +- Let them know the result +- Offer to reciprocate + +## Handling Special Situations + +### Current Employer Doesn't Know + +**Options:** +- Ask if reference check can wait until later stage +- Use colleagues who've left the company +- Be upfront: "My current employer doesn't know I'm looking" +- Use other professional references + +### Manager Left the Company + +**Options:** +- Track them down on LinkedIn +- Use their personal email/phone +- Include their new company in reference list +- Explain "Former manager, now at [New Company]" + +### Bad Relationship with Past Manager + +**Options:** +- Use another supervisor from that role +- Use senior colleagues instead +- Choose references from different roles +- Be prepared to explain if asked + +### Limited Professional Experience + +**Options:** +- Professors or academic advisors +- Internship supervisors +- Volunteer organization leaders +- Coaches or mentors +- Client contacts + +### Reference Won't Give Positive Review + +**Don't use them.** It's better to have fewer references than a lukewarm or negative one. + +## What Reference Checkers Ask + +### Common Questions + +**Performance:** +- "How would you describe [name]'s work?" +- "What were their primary responsibilities?" +- "How did they perform against expectations?" + +**Skills:** +- "What are their greatest strengths?" +- "What areas could they improve?" +- "How would you rate their [specific skill]?" + +**Work Style:** +- "How did they handle pressure/deadlines?" +- "How did they work with the team?" +- "How did they handle conflict?" + +**Character:** +- "Would you rehire them?" +- "Is there anything else I should know?" +- "How do they compare to peers?" + +### The "Would You Rehire?" Question + +**Most important question.** Brief your references that this may be asked and ensure they can answer enthusiastically. + +## Reference List Template + +``` +[YOUR NAME] +[Your Email] | [Your Phone] + +PROFESSIONAL REFERENCES + +[REFERENCE 1 - MOST SENIOR/RELEVANT] +[Name] +[Title] +[Company] +Phone: [Number] +Email: [Email] +Relationship: [How you worked together, dates] + +[REFERENCE 2] +[Name] +[Title] +[Company] +Phone: [Number] +Email: [Email] +Relationship: [How you worked together, dates] + +[REFERENCE 3] +[Name] +[Title] +[Company] +Phone: [Number] +Email: [Email] +Relationship: [How you worked together, dates] + +--- +References available upon request for additional contacts. +``` + +## Reference Timing + +### When to Provide References + +- **Don't include with initial application** (unless requested) +- **Bring to interview** (have them ready) +- **Provide when asked** (usually after final interview) +- **Always ask before sharing** (confirm permission each time) + +### Common Timeline + +1. **Application:** "References available upon request" (optional on resume) +2. **First Interview:** Have list ready but don't offer +3. **Final Rounds:** "Can you provide references?" → Share list +4. **Reference Check:** Company contacts your references +5. **Offer:** Follow up with references, thank them + +## Output Format + +When building a reference list: + +```markdown +# REFERENCE LIST + +## Reference Strategy +**Target Role:** [Position] +**Company:** [Company] + +## Recommended References + +### Primary References (Use These) + +**Reference 1: [Name]** +- Current Title: [Title] +- Company: [Company] +- Contact: [Phone/Email] +- Relationship: [Description] +- Why: [What they can speak to] +- Key points to highlight: [Specific projects/skills] + +**Reference 2: [Name]** +[Same format] + +**Reference 3: [Name]** +[Same format] + +### Backup References (If Needed) + +**Reference 4: [Name]** +[Same format] + +## Briefing Notes + +### For Each Reference, Send: +- [ ] Resume +- [ ] Job description +- [ ] Key talking points +- [ ] Timeline + +### Key Points to Emphasize +- [Point 1 - who should mention] +- [Point 2 - who should mention] + +## Reference Outreach + +### Permission Request Script +[Customized script] + +### Briefing Email +[Draft briefing email] + +### Thank You Template +[Post-check thank you message] +``` + +## Reference Checklist + +- ✅ Have 3-5 references ready +- ✅ All references have given permission +- ✅ Contact information is current and accurate +- ✅ References know about the specific role +- ✅ Each reference has your resume and talking points +- ✅ At least one direct supervisor included +- ✅ References span different aspects of your work +- ✅ References are enthusiastic (not just willing) +- ✅ Backup references identified +- ✅ Thank you notes planned diff --git a/.agents/skills/resume-ats-optimizer/SKILL.md b/.agents/skills/resume-ats-optimizer/SKILL.md new file mode 100644 index 00000000..3557e214 --- /dev/null +++ b/.agents/skills/resume-ats-optimizer/SKILL.md @@ -0,0 +1,319 @@ +--- +name: resume-ats-optimizer +description: Optimize resumes for Applicant Tracking Systems, check ATS compatibility, and analyze keyword match +--- + +# Resume ATS Optimizer + +## When to Use This Skill + +Use this skill when the user wants to: +- Optimize their resume for Applicant Tracking Systems (ATS) +- Check if their resume will pass automated screening +- Understand why their applications aren't getting responses +- Mentions keywords like: "ATS", "not getting interviews", "resume not working", "optimize resume", "keyword optimization" + +Also use when the user provides a resume file and mentions they're applying to jobs. + +## Core Capabilities + +- Parse resume and test ATS compatibility +- Extract and analyze keywords against job descriptions +- Identify formatting issues that break ATS parsers +- Calculate match scores between resume and job postings +- Suggest keyword additions and placements +- Generate ATS-friendly formatting recommendations + +## The ATS Problem + +75% of resumes are rejected by Applicant Tracking Systems before a human ever sees them. Companies use ATS to: +- Filter out unqualified candidates automatically +- Search for specific keywords from job requirements +- Parse resumes into structured data +- Rank candidates by keyword match percentage + +Common reasons resumes fail ATS: +1. Poor formatting (tables, columns, headers/footers) +2. Missing keywords from job description +3. Inconsistent section headers +4. Non-standard fonts or special characters +5. Text embedded in images +6. Incorrect file format + +## ATS Compatibility Checklist + +### File Format +- ✅ Use .docx or .pdf (not .pages, .odt) +- ✅ PDF must be text-based, not scanned image +- ✅ File name: "FirstName_LastName_Resume.pdf" + +### Font & Formatting +- ✅ Standard fonts: Arial, Calibri, Georgia, Times New Roman +- ✅ Font size: 10-12pt for body, 14-16pt for headers +- ✅ No text boxes, tables, or columns +- ✅ No headers/footers (put contact info in body) +- ✅ No images, graphics, or charts +- ✅ Consistent date formats (MM/YYYY) +- ✅ Standard bullet points (•, -, *) + +### Section Headers +Use standard, recognizable headers: +- ✅ "Professional Experience" or "Work Experience" (not "Where I've Been") +- ✅ "Education" (not "Academic Background") +- ✅ "Skills" (not "Core Competencies") +- ✅ "Summary" or "Professional Summary" + +### Contact Information +``` +John Smith +email@example.com | (555) 123-4567 | LinkedIn: linkedin.com/in/johnsmith +San Francisco, CA +``` + +NOT in header/footer, and avoid: +- ❌ Tables for contact info +- ❌ Special characters in email +- ❌ Multiple phone numbers +- ❌ Full mailing address (city/state is enough) + +## Keyword Optimization Process + +### Step 1: Extract Job Description Keywords + +Identify three types of keywords: + +**Hard Skills (Technical)** +- Programming languages (Python, Java, SQL) +- Tools and platforms (Salesforce, AWS, Excel) +- Certifications (PMP, CPA, CFA) +- Methodologies (Agile, Six Sigma, SDLC) + +**Soft Skills** +- Leadership, collaboration, communication +- Problem-solving, analytical thinking +- Project management, stakeholder management + +**Industry Terms** +- B2B, SaaS, e-commerce +- Enterprise, SMB, mid-market +- ARR, MRR, churn rate + +### Step 2: Match Analysis + +For each keyword in job description: +1. Check if exact phrase appears in resume +2. Check for synonyms or variations +3. Count frequency of mention +4. Note location (summary, experience, skills) + +### Step 3: Calculate Match Score + +``` +Match Score = (Keywords Matched / Total Required Keywords) × 100 + +Example: +Job has 20 required keywords +Your resume has 15 of them +Match Score = 75% + +Target: 80%+ for strong match +``` + +### Step 4: Keyword Placement Strategy + +**Priority 1: Professional Summary (Top of Resume)** +- Include 5-8 most important keywords +- Use naturally in 3-4 sentence paragraph +- Example: "Data Scientist with 5+ years using Python, SQL, and machine learning to drive business insights..." + +**Priority 2: Skills Section** +- List keywords explicitly +- Group by category if needed +- Use exact phrasing from job description + +**Priority 3: Experience Bullets** +- Incorporate keywords into achievement statements +- Don't force keywords unnaturally +- Use variations throughout + +**Keyword Density Guidelines:** +- Critical keywords: Appear 2-4 times throughout resume +- Important keywords: Appear 1-2 times +- Don't keyword stuff - keep it natural +- Vary phrasing (e.g., "led team" and "team leadership") + +## Analysis Output Format + +When analyzing a resume, provide this structured report: + +```markdown +# ATS COMPATIBILITY REPORT + +## Overall Score: [X]/100 + +### File Format Check ✅/❌ +- Format: [DOCX/PDF] +- Text extraction: [Success/Failed] +- File size: [X KB/MB] + +### Formatting Issues +✅ No tables or columns detected +❌ Contact info in header (move to body) +⚠️ Two different font sizes in skills section + +### Keyword Analysis + +JOB REQUIREMENTS vs YOUR RESUME: + +**Critical Keywords (Must Have):** +✅ Project Management - Found 3x +✅ Agile/Scrum - Found 2x +❌ Stakeholder Management - MISSING (mentioned 5x in JD) +❌ Budget Management - MISSING (mentioned 3x in JD) + +**Important Keywords:** +✅ Cross-functional teams - Found 1x +⚠️ "Risk management" - You have "risk mitigation" (close but not exact match) +✅ Process improvement - Found 2x + +**Match Score: 65%** +Target: 80%+ recommended + +### Recommended Changes + +**1. Add Missing Keywords:** + +In Professional Summary, change: +"Experienced project manager with proven track record..." + +To: +"Experienced project manager with proven track record in stakeholder management and budget oversight..." + +In Experience section, add bullet: +"Managed stakeholder communication across 3 departments and executive leadership team" +"Directed budget management for $2.5M project portfolio" + +**2. Fix Formatting:** +- Move contact information from header to body of resume +- Make all skill section items same font size (currently 10pt and 11pt mixed) + +**3. Strengthen Existing Keywords:** +Change "risk mitigation" to "risk management" for exact match + +### Estimated New Match Score: 85% +``` + +## Common ATS Failure Patterns + +### Pattern 1: Creative Formatting +``` +❌ PROBLEM: +[Two-column layout with graphics] +[Skill bars and proficiency charts] +[Text in colored boxes] + +✅ SOLUTION: +- Single column layout +- Text-only skills list +- Simple bullet points +``` + +### Pattern 2: Unconventional Section Names +``` +❌ PROBLEM: +"My Journey" (instead of Experience) +"What I Bring to the Table" (instead of Skills) +"Academic Pursuits" (instead of Education) + +✅ SOLUTION: +Use standard headers ATS recognizes +``` + +### Pattern 3: Missing Keywords +``` +❌ PROBLEM: +Job requires: "Python, SQL, Data Visualization" +Resume says: "Programming, databases, making charts" + +✅ SOLUTION: +Use exact terminology from job description +``` + +### Pattern 4: Keyword Stuffing +``` +❌ PROBLEM: +Skills: Python, Python programming, Python developer, Python expert, Python specialist, Advanced Python... + +✅ SOLUTION: +Skills: Python, SQL, JavaScript, React, Node.js +(Then incorporate naturally in bullets) +``` + +## Industry-Specific Considerations + +### Tech Resumes +- Emphasize programming languages and frameworks +- Include GitHub, portfolio links in Skills section (not header) +- Certifications and courses matter highly + +### Business/Finance +- Focus on software proficiency (Excel, SAP, Salesforce) +- Certifications critical (CPA, CFA, PMP) +- Industry keywords (P&L, ROI, KPI) + +### Healthcare +- Licenses and certifications required +- Specific systems (Epic, Cerner, MEDITECH) +- Compliance keywords (HIPAA, Joint Commission) + +### Marketing +- Platform expertise (HubSpot, Salesforce, Google Analytics) +- Channel keywords (SEO, PPC, email marketing) +- Metrics and results-driven language + +## Edge Cases & Special Situations + +### Career Changers +- Focus on transferable skills +- Use keywords from TARGET industry, not just current +- May need two resume versions for ATS + +### Recent Graduates +- Education section becomes priority for keywords +- Include relevant coursework, projects +- Internships count as experience - use those keywords + +### Executive Level +- ATS still matters for senior roles +- Focus on strategic keywords +- Include board experience, P&L size, team size + +### Gaps in Employment +- Use years only (not months) if it helps +- Include freelance/consulting with keywords +- Volunteer work can include relevant keywords + +## Implementation Checklist + +When helping user optimize for ATS: + +1. ✅ Scan current resume for ATS compatibility issues +2. ✅ Analyze job description for required keywords +3. ✅ Calculate current match score +4. ✅ Identify specific missing keywords +5. ✅ Suggest exact placements for new keywords +6. ✅ Flag formatting problems +7. ✅ Provide before/after examples +8. ✅ Re-score after suggested changes +9. ✅ Verify file format and naming +10. ✅ Test with ATS simulator if possible + +## Success Metrics + +After optimization, the resume should: +- Score 80%+ match for target job descriptions +- Pass ATS parsing test (all sections recognized) +- Have zero formatting errors +- Include all critical keywords 2-4x each +- Read naturally (not keyword-stuffed) +- Be ready to submit immediately diff --git a/.agents/skills/resume-bullet-writer/SKILL.md b/.agents/skills/resume-bullet-writer/SKILL.md new file mode 100644 index 00000000..ee1140fd --- /dev/null +++ b/.agents/skills/resume-bullet-writer/SKILL.md @@ -0,0 +1,437 @@ +--- +name: resume-bullet-writer +description: Transform weak resume bullets into achievement-focused statements with metrics and impact +--- + +# Resume Bullet Writer + +## When to Use This Skill + +Use this skill when the user wants to: +- Write or improve resume bullet points +- Transform weak descriptions into strong achievements +- Add metrics and quantifiable results +- Make their experience more compelling +- Mentions: "improve my bullets", "make my resume stronger", "quantify my achievements", "results-driven" + +Also use when you see weak bullets that need improvement (passive language, no metrics, vague descriptions). + +## Core Capabilities + +- Transform weak bullet points into achievement-focused statements +- Apply STAR method and X-Y-Z formula +- Add quantifiable metrics and results +- Use strong action verbs +- Tailor bullets to specific roles/industries +- Ensure every bullet shows impact, not just duties + +## The Bullet Point Problem + +Most resumes have weak bullets that list job duties instead of achievements: + +❌ **Weak Bullets (What NOT to do):** +- "Responsible for managing team" +- "Helped with customer service" +- "Worked on improving processes" +- "Assisted with projects" + +These are passive, vague, and don't show impact or results. + +✅ **Strong Bullets (What TO do):** +- "Led cross-functional team of 12 to deliver $2M product, increasing revenue by 35%" +- "Resolved 50+ customer issues daily, improving satisfaction scores from 3.2 to 4.8/5" +- "Streamlined approval process, reducing cycle time by 40% (from 10 to 6 days)" +- "Managed portfolio of 8 concurrent projects with 100% on-time delivery rate" + +These are active, specific, and quantify the impact. + +## Core Frameworks + +### 1. The X-Y-Z Formula (Google Method) + +**Structure:** "Accomplished [X] as measured by [Y] by doing [Z]" + +- X = What you achieved +- Y = How you measured it +- Z = What actions you took + +**Examples:** + +``` +❌ BEFORE: "Managed social media accounts" + +✅ AFTER: "Grew Instagram following by 250% (5K to 17.5K) by implementing daily content calendar and influencer partnerships" + +X = Grew Instagram following by 250% +Y = 5K to 17.5K followers +Z = Daily content calendar + influencer partnerships +``` + +``` +❌ BEFORE: "Improved customer service" + +✅ AFTER: "Increased customer satisfaction scores by 40% (3.2 to 4.5/5) by redesigning support ticketing system and training 15 agents" + +X = Increased customer satisfaction by 40% +Y = 3.2 to 4.5 out of 5 +Z = Redesigned ticketing system + trained agents +``` + +### 2. STAR Method + +**Structure:** +- **S**ituation: What was the context? +- **T**ask: What needed to be done? +- **A**ction: What did YOU specifically do? +- **R**esult: What was the measurable outcome? + +**Example:** + +Full STAR story: +"Inherited underperforming sales team (S) with 65% quota attainment. Tasked with improving performance within Q1 (T). Implemented new training program and revised commission structure (A). Achieved 92% quota attainment by Q2, generating $1.8M additional revenue (R)." + +For resume bullet (condensed STAR): +"Revitalized underperforming sales team through training program and commission restructure, improving quota attainment from 65% to 92% and generating $1.8M additional revenue" + +### 3. CAR Method (Alternative to STAR) + +**Structure:** +- **C**hallenge: What problem existed? +- **A**ction: What did you do about it? +- **R**esult: What happened? + +**Example:** + +"Reduced customer churn (C) by implementing proactive outreach program (A), retaining 85% of at-risk accounts worth $500K ARR (R)" + +## Power Verbs by Category + +### Leadership & Management +- Led, Directed, Managed, Supervised, Coordinated +- Spearheaded, Orchestrated, Oversaw, Championed, Mentored + +### Achievement & Success +- Achieved, Delivered, Exceeded, Surpassed, Attained +- Secured, Won, Earned, Captured, Clinched + +### Growth & Improvement +- Grew, Increased, Boosted, Expanded, Scaled +- Elevated, Amplified, Maximized, Accelerated, Doubled + +### Creation & Innovation +- Created, Developed, Designed, Built, Launched +- Pioneered, Established, Founded, Introduced, Originated + +### Optimization & Efficiency +- Streamlined, Optimized, Enhanced, Improved, Revitalized +- Transformed, Restructured, Modernized, Automated, Simplified + +### Analysis & Strategy +- Analyzed, Assessed, Evaluated, Identified, Diagnosed +- Researched, Investigated, Examined, Audited, Forecasted + +### Collaboration & Communication +- Collaborated, Partnered, Facilitated, Coordinated, United +- Presented, Communicated, Negotiated, Influenced, Persuaded + +### Problem-Solving +- Resolved, Solved, Troubleshot, Rectified, Debugged +- Eliminated, Reduced, Mitigated, Prevented, Corrected + +## Quantification Strategies + +Every bullet should have at least ONE number. Here's how to find metrics: + +### Types of Metrics to Include + +**1. Money** +- Revenue generated: "$2M in new revenue" +- Money saved: "Reduced costs by $50K annually" +- Budget managed: "Managed $5M project budget" +- Deal size: "Closed 3 enterprise deals worth $500K" + +**2. Percentages** +- Growth: "Increased conversion rate by 45%" +- Improvement: "Reduced error rate by 60%" +- Efficiency: "Decreased processing time by 30%" +- Quality: "Improved accuracy from 85% to 98%" + +**3. Time** +- Speed: "Reduced load time from 8s to 2s" +- Frequency: "Delivered weekly reports to 50+ stakeholders" +- Duration: "Completed 6-month project in 4 months" +- Saved time: "Automated process, saving team 10 hours/week" + +**4. Scale/Volume** +- People: "Led team of 15", "Trained 50+ employees" +- Projects: "Managed 8 concurrent initiatives" +- Customers: "Served 500+ enterprise clients" +- Users: "Built product used by 100K+ daily active users" + +**5. Comparison/Before-After** +- "Increased from X to Y" +- "Reduced from X to Y" +- "Grew from X to Y" + +### When You Don't Have Exact Numbers + +Use estimation strategies: + +**Approximate with ~** +"Improved performance by ~40%" + +**Use ranges** +"Managed team of 8-12 people" +"Generated $50K-$75K in monthly revenue" + +**Use conservative estimates** +If you think it was 60%, say 50% +If you saved 100 hours, say 75 hours + +**Quantify inputs if outputs unknown** +Can't measure revenue? Quantify activities: +- "Conducted 30+ customer interviews" +- "Analyzed 500+ data points" +- "Created 20+ marketing campaigns" + +**Find related metrics** +Can't measure conversion? Measure traffic, engagement, or other funnel metrics + +## Industry-Specific Bullet Examples + +### Software Engineering + +❌ WEAK: +- Wrote code for new features +- Fixed bugs in production +- Worked with product team + +✅ STRONG: +- Architected authentication microservice serving 500K+ daily active users, reducing login latency by 60% (from 5s to 2s) +- Resolved 50+ critical production bugs over 6 months, improving system uptime from 97.2% to 99.8% +- Collaborated with product team to deliver 12 features ahead of schedule, resulting in 25% increase in user engagement + +### Product Management + +❌ WEAK: +- Managed product roadmap +- Worked with stakeholders +- Launched new features + +✅ STRONG: +- Defined and executed product roadmap for $10M ARR product, prioritizing 50+ feature requests from 200+ customers +- Facilitated weekly stakeholder meetings with engineering, design, and executive teams to align on strategic priorities +- Launched 8 major features in 12 months, driving 40% increase in customer retention and $2M additional revenue + +### Sales + +❌ WEAK: +- Met with clients +- Closed deals +- Exceeded quota + +✅ STRONG: +- Generated $3.2M in new business revenue by closing 15 enterprise deals, achieving 142% of annual quota +- Managed sales cycle for 50+ prospects simultaneously, maintaining 35% close rate (company average: 22%) +- Exceeded quarterly quota 8 consecutive quarters, ranking #2 out of 45 sales reps nationwide + +### Marketing + +❌ WEAK: +- Created marketing campaigns +- Managed social media +- Analyzed campaign performance + +✅ STRONG: +- Launched 12 multi-channel campaigns generating $500K in attributed revenue and 2,500+ qualified leads +- Grew LinkedIn audience by 400% (3K to 15K followers) through thought leadership content strategy, resulting in 200+ inbound demo requests +- Analyzed campaign performance across 8 channels using Google Analytics, identifying 3 top-performing channels responsible for 75% of conversions + +### Customer Success + +❌ WEAK: +- Helped customers with problems +- Managed customer accounts +- Conducted onboarding sessions + +✅ STRONG: +- Maintained 95% customer retention rate across portfolio of 50 enterprise accounts representing $4M ARR +- Resolved 40+ customer issues weekly with average response time of 2 hours, achieving 4.9/5 CSAT score +- Delivered 25+ onboarding sessions for new customers, achieving 90% product adoption rate within first 30 days + +### Data Analysis + +❌ WEAK: +- Analyzed data +- Created reports +- Made recommendations + +✅ STRONG: +- Analyzed 500K+ transaction records to identify $2M revenue opportunity, presented findings to C-suite resulting in new product line +- Built automated reporting dashboard in Tableau reducing manual reporting time by 15 hours/week for team of 8 +- Developed predictive model with 85% accuracy for customer churn, enabling proactive retention campaigns that saved $500K ARR + +### Operations/Project Management + +❌ WEAK: +- Managed projects +- Coordinated with teams +- Tracked progress + +✅ STRONG: +- Led 8 cross-functional projects simultaneously with 100% on-time delivery rate and zero budget overruns across $5M portfolio +- Coordinated efforts across engineering, design, and marketing teams (30+ people) to launch product ahead of schedule +- Implemented project tracking system reducing status meeting time by 50% and improving visibility for 15+ stakeholders + +## Common Bullet Writing Mistakes + +### Mistake 1: Passive Language +❌ "Was responsible for..." +❌ "Helped with..." +❌ "Assisted in..." +❌ "Participated in..." + +✅ Use active verbs: Led, Created, Delivered, Achieved + +### Mistake 2: No Metrics +❌ "Improved website performance" +✅ "Improved website load time by 60% (8s to 3.2s)" + +❌ "Managed large team" +✅ "Led cross-functional team of 25 across 4 departments" + +### Mistake 3: Job Duties Instead of Achievements +❌ "Responsible for customer support" +✅ "Resolved 50+ tickets daily with 98% customer satisfaction score" + +❌ "Managed social media accounts" +✅ "Grew social media following by 300% (10K to 40K) in 8 months" + +### Mistake 4: Too Vague +❌ "Worked with stakeholders" +✅ "Facilitated weekly meetings with 15+ stakeholders across engineering, product, and executive teams" + +❌ "Improved processes" +✅ "Streamlined approval workflow, reducing cycle time from 10 to 4 days (60% improvement)" + +### Mistake 5: Too Long +❌ "Was responsible for managing and overseeing all aspects of the customer success function including onboarding, training, support, and retention for a portfolio of enterprise clients..." + +✅ "Led customer success for 50 enterprise clients ($3M ARR), achieving 95% retention rate" + +**Rule: Keep bullets to 1-2 lines maximum** + +## Bullet Writing Process + +### Step 1: Start with the Weak Bullet +``` +Original: "Managed social media" +``` + +### Step 2: Ask Clarifying Questions +- What platforms? +- How many followers? +- What growth did you achieve? +- What specific actions did you take? +- What business impact resulted? + +### Step 3: Gather Information +- Platforms: Instagram, LinkedIn +- Started: 5K followers +- Ended: 18K followers +- Actions: Daily content calendar, influencer partnerships +- Impact: 200 leads generated + +### Step 4: Apply Formula +Using X-Y-Z: +"Grew Instagram and LinkedIn following by 260% (5K to 18K) by implementing daily content calendar and 15 influencer partnerships, generating 200+ qualified leads" + +### Step 5: Optimize Length +If too long, prioritize most impressive metrics: +"Grew social media audience by 260% (5K to 18K followers) through content strategy and influencer partnerships, generating 200+ leads" + +## Bullet Strength Checklist + +Every bullet should have: +- ✅ Strong action verb (avoid "responsible for", "helped") +- ✅ At least one number/metric +- ✅ Specific outcome or result +- ✅ Context of scale (team size, budget, users, etc.) +- ✅ 1-2 lines maximum +- ✅ Reads as an achievement, not a duty +- ✅ Relevant to target role + +## Output Format + +When rewriting bullets, provide: + +```markdown +## BULLET IMPROVEMENTS + +### Original Bullet #1: +"Managed customer accounts" + +### Issues: +- Passive language ("managed") +- No metrics +- Vague (what does "managed" mean?) +- No results shown + +### Improved Version: +"Grew portfolio of 40 enterprise accounts from $2M to $3.5M ARR (75% growth) through quarterly business reviews and proactive upselling strategy" + +### What Changed: +- Added scale (40 accounts) +- Added growth metric (75%, $2M to $3.5M) +- Specified actions (QBRs, upselling) +- Shows business impact (revenue growth) + +--- + +### Original Bullet #2: +[Continue for each bullet...] +``` + +## Special Situations + +### For Entry-Level/Recent Graduates +Focus on: +- Academic projects with real impact +- Internship achievements +- Relevant coursework projects +- Volunteer work with quantifiable results +- Club leadership with metrics + +Example: +"Led university marketing club of 50 members, organizing 8 events attended by 300+ students and securing $10K in corporate sponsorships" + +### For Career Changers +Focus on: +- Transferable skills +- Reframe old experience for new industry +- Highlight relevant projects/side work +- Emphasize learning and adaptation + +Example: +"Managed cross-functional teams of 15 (traditional retail) → can become: +"Led cross-functional teams of 15 across operations, merchandising, and customer experience, coordinating workflows and achieving 100% project completion rate" + +### For Gaps in Employment +Focus on: +- Freelance/consulting work +- Volunteer achievements +- Side projects +- Professional development + +Don't draw attention to gaps, just fill space with relevant achievements. + +## Implementation Checklist + +For each resume bullet: +1. ✅ Identify weak verb → Replace with power verb +2. ✅ Check for metrics → Add at least one number +3. ✅ Verify it shows result → Add outcome/impact +4. ✅ Confirm specificity → Remove vague language +5. ✅ Test length → Keep to 1-2 lines +6. ✅ Read aloud → Ensure it sounds natural +7. ✅ Verify relevance → Aligns with target role diff --git a/.agents/skills/resume-formatter/SKILL.md b/.agents/skills/resume-formatter/SKILL.md new file mode 100644 index 00000000..ae66b4db --- /dev/null +++ b/.agents/skills/resume-formatter/SKILL.md @@ -0,0 +1,360 @@ +--- +name: resume-formatter +description: Ensure ATS-friendly formatting and create clean scannable layouts +--- + +# Resume Formatter + +## When to Use This Skill + +Use this skill when the user: +- Needs help with resume layout and formatting +- Has a messy or hard-to-read resume +- Wants to ensure ATS compatibility through formatting +- Needs a clean, professional design +- Mentions: "format resume", "resume layout", "resume design", "clean resume", "professional format" + +## Core Capabilities + +- Structure resumes for optimal readability +- Ensure ATS compatibility through formatting +- Create visual hierarchy +- Optimize white space and margins +- Select appropriate fonts and sizes +- Balance aesthetic appeal with functionality + +## Formatting Fundamentals + +### The Dual Audience Challenge + +Your resume must work for: +1. **ATS (Applicant Tracking Systems)** - Robots that parse text +2. **Human Readers** - Recruiters who scan quickly + +**The Solution:** Clean, simple formatting that satisfies both. + +## Document Setup + +### Page Length +- **Entry Level (0-5 years):** 1 page +- **Mid-Level (5-15 years):** 1-2 pages +- **Senior/Executive (15+ years):** 2 pages (max 3 for executives) + +### Margins +- **Recommended:** 0.5" - 1" all sides +- **Minimum:** 0.5" (don't go smaller) +- **Maximum:** 1" (don't waste space) + +### Font Selection + +**Safe, ATS-Friendly Fonts:** +- **Sans-serif:** Arial, Calibri, Helvetica, Verdana +- **Serif:** Times New Roman, Georgia, Garamond + +**Font Sizes:** +- **Name:** 16-20pt +- **Section Headers:** 12-14pt +- **Body Text:** 10-12pt +- **Minimum readable:** 10pt + +### Spacing +- **Line spacing:** 1.0 to 1.15 +- **Space after paragraphs:** 6-12pt +- **Section spacing:** 12-16pt between sections + +## ATS-Safe Formatting Rules + +### DO: +- ✅ Use standard fonts +- ✅ Use simple bullet points (•, -, *) +- ✅ Use bold and italic sparingly +- ✅ Use standard section headers +- ✅ Save as .docx or text-based .pdf +- ✅ Put contact info in body (not header) +- ✅ Use single column layout +- ✅ Use consistent formatting throughout + +### DON'T: +- ❌ Use tables (except simple ones for contact info) +- ❌ Use text boxes +- ❌ Use columns (multi-column layouts) +- ❌ Use headers/footers for important info +- ❌ Use images or graphics +- ❌ Use unusual fonts +- ❌ Use skill bars or progress indicators +- ❌ Use special characters or emojis +- ❌ Use color for essential information + +## Section Organization + +### Standard Section Order + +``` +1. Contact Information +2. Professional Summary (optional) +3. Skills/Technical Skills +4. Professional Experience +5. Education +6. Certifications (if relevant) +7. Additional (volunteer, languages, etc.) +``` + +### Section Header Formatting + +**ATS-Recognized Headers:** +- PROFESSIONAL EXPERIENCE or WORK EXPERIENCE +- EDUCATION +- SKILLS or TECHNICAL SKILLS +- PROFESSIONAL SUMMARY or SUMMARY +- CERTIFICATIONS +- PROJECTS + +**Format Options:** +``` +PROFESSIONAL EXPERIENCE +━━━━━━━━━━━━━━━━━━━━━━ + +or + +Professional Experience +_______________________ + +or + +PROFESSIONAL EXPERIENCE +``` + +## Contact Information Layout + +### Recommended Format +``` +JOHN SMITH +john.smith@email.com | (555) 123-4567 | linkedin.com/in/johnsmith +San Francisco, CA +``` + +### Alternative Format +``` +JOHN SMITH +San Francisco, CA +john.smith@email.com | (555) 123-4567 +LinkedIn: linkedin.com/in/johnsmith | GitHub: github.com/johnsmith +``` + +### What to Include +- ✅ Full name +- ✅ Professional email +- ✅ Phone number +- ✅ City, State (no full address needed) +- ✅ LinkedIn URL +- ✅ Portfolio/GitHub (if relevant) + +### What to Exclude +- ❌ Full street address +- ❌ Photo +- ❌ Date of birth +- ❌ Marital status +- ❌ Multiple phone numbers +- ❌ Personal social media + +## Experience Section Formatting + +### Standard Format +``` +COMPANY NAME | City, ST +Job Title | Month Year - Month Year + +• Achievement bullet with metrics and results +• Achievement bullet with metrics and results +• Achievement bullet with metrics and results +``` + +### Alternative Format +``` +Job Title +COMPANY NAME, City, ST Month Year - Month Year + +• Achievement bullet with metrics and results +• Achievement bullet with metrics and results +``` + +### Date Formatting +- **Consistent format:** Use same format throughout +- **Recommended:** Month Year (Jan 2020 - Present) +- **Also acceptable:** MM/YYYY (01/2020 - Present) +- **Avoid:** Full dates (January 15, 2020) + +### Bullet Point Guidelines +- **Length:** 1-2 lines each +- **Format:** Start with action verb, end with result +- **Quantity:** 3-6 bullets per role (more for recent, fewer for old) +- **Symbol:** Use standard bullets (•, -, *) + +## Skills Section Formatting + +### Option 1: Simple List +``` +SKILLS +Python, JavaScript, SQL, React, Node.js, AWS, Docker, Git, Agile, JIRA +``` + +### Option 2: Categorized +``` +TECHNICAL SKILLS +Languages: Python, JavaScript, TypeScript, SQL +Frameworks: React, Node.js, Django, Flask +Tools: AWS, Docker, Kubernetes, Git, Jenkins +``` + +### Option 3: Columns (Careful with ATS) +``` +SKILLS +Languages Frameworks Tools +Python React AWS +JavaScript Node.js Docker +SQL Django Git +``` + +**Note:** Multi-column layouts may cause ATS issues. Test before using. + +## Education Section Formatting + +### Standard Format +``` +EDUCATION +Bachelor of Science in Computer Science +University of California, Berkeley | 2018 +GPA: 3.8/4.0 (include if 3.5+) +``` + +### With Honors/Details +``` +EDUCATION +MBA, Finance & Strategy | Stanford Graduate School of Business | 2020 +• Graduated with Distinction +• Relevant Coursework: Corporate Finance, M&A Strategy +``` + +## Visual Hierarchy Principles + +### Hierarchy Order +1. **Name** - Largest, most prominent +2. **Section Headers** - Clear divisions +3. **Job Titles/Company Names** - Easy to scan +4. **Bullet Points** - The details + +### Creating Hierarchy +- Use font SIZE to create levels +- Use **BOLD** for emphasis (names, titles, headers) +- Use CAPS for section headers +- Use consistent spacing to separate sections + +## White Space Management + +### Good White Space: +- Between sections (clear separation) +- After headings (visual breathing room) +- Between bullets (don't cram) +- Around margins (frame the content) + +### Bad White Space: +- Huge gaps between sections +- Inconsistent spacing +- Half-empty pages +- Excessive margins eating space + +## Common Formatting Mistakes + +### Mistake 1: Wall of Text +**Problem:** Dense paragraphs with no bullets +**Solution:** Use bullet points, keep paragraphs short + +### Mistake 2: Inconsistent Formatting +**Problem:** Different fonts, sizes, or styles throughout +**Solution:** Pick one format and stick to it + +### Mistake 3: Trying to Be Creative +**Problem:** Fancy designs that break ATS +**Solution:** Save creativity for portfolio, not resume + +### Mistake 4: Too Much Information +**Problem:** Cramming everything onto one page +**Solution:** Edit ruthlessly, prioritize relevance + +### Mistake 5: Not Enough Information +**Problem:** Half-page resume with massive margins +**Solution:** Add detail, reduce margins (to 0.5") + +## File Format Guidelines + +### For Online Applications +- **.docx** - Best for ATS parsing +- **.pdf** - Good if created from Word (not scanned) + +### For Email/Direct Send +- **.pdf** - Preserves formatting + +### File Naming +``` +FirstName_LastName_Resume.pdf +JohnSmith_Resume_ProductManager.pdf +``` + +**Avoid:** +- resume_final_v2_updated_FINAL.docx +- resume (1).pdf +- Untitled document.docx + +## Output Format + +When formatting a resume: + +```markdown +# RESUME FORMATTING REVIEW + +## Current Issues +- [ ] [Issue 1] +- [ ] [Issue 2] +- [ ] [Issue 3] + +## Recommended Changes + +### Document Setup +- Margins: [Current] → [Recommended] +- Font: [Current] → [Recommended] +- Font sizes: [Current] → [Recommended] + +### Section Order +**Current:** [Current order] +**Recommended:** [New order and why] + +### Visual Improvements +- [Specific change 1] +- [Specific change 2] + +### ATS Compatibility Fixes +- [Fix 1] +- [Fix 2] + +## Before/After Preview + +### Before: +[Description or example of current formatting] + +### After: +[Description or example of improved formatting] +``` + +## Quick Formatting Checklist + +Before submitting any resume: +- ✅ One page (or two if warranted) +- ✅ Standard font (10-12pt body) +- ✅ Consistent formatting throughout +- ✅ Clear section headers +- ✅ Appropriate white space +- ✅ No tables, text boxes, or columns +- ✅ Contact info in body (not header) +- ✅ Saved as .docx or .pdf +- ✅ Proper file name +- ✅ Proofread for consistency diff --git a/.agents/skills/resume-quantifier/SKILL.md b/.agents/skills/resume-quantifier/SKILL.md new file mode 100644 index 00000000..7e6152b5 --- /dev/null +++ b/.agents/skills/resume-quantifier/SKILL.md @@ -0,0 +1,350 @@ +--- +name: resume-quantifier +description: Find opportunities to add metrics and estimate numbers when exact data unavailable +--- + +# Resume Quantifier + +## When to Use This Skill + +Use this skill when the user: +- Needs to add metrics and numbers to their resume +- Has bullets without quantifiable results +- Doesn't know what numbers to include +- Says they "don't have metrics" or "can't measure impact" +- Mentions: "add metrics", "quantify", "add numbers", "measure impact", "no data" + +## Core Capabilities + +- Find hidden metrics in any experience +- Estimate numbers when exact data unavailable +- Create before/after comparisons +- Identify measurable impact points +- Transform vague statements into quantified achievements +- Guide users to discover their metrics + +## Why Quantification Matters + +**The Problem:** +- "Managed projects" vs "Managed 12 projects worth $2M" +- "Improved processes" vs "Reduced cycle time by 40%" +- "Helped customers" vs "Resolved 50+ tickets daily with 98% satisfaction" + +**Studies Show:** +- Resumes with numbers get 30% more attention +- Quantified bullets are 40% more memorable +- Numbers provide credibility and scale + +## The Quantification Framework + +### Categories of Metrics + +**1. Money** +- Revenue generated +- Costs reduced/saved +- Budget managed +- Deal sizes closed +- Profit margins improved + +**2. Time** +- Hours saved +- Cycle time reduced +- Project duration +- Response times +- Time to market + +**3. Percentages** +- Growth rates +- Improvement percentages +- Efficiency gains +- Error reduction +- Conversion rates + +**4. Volume/Scale** +- Number of customers/users +- Projects managed +- Team size +- Transactions processed +- Items produced + +**5. Quality** +- Satisfaction scores +- Error rates +- Accuracy rates +- Compliance rates +- SLA adherence + +**6. Frequency** +- Per day/week/month +- Annual totals +- Meeting cadences +- Report cycles + +## Finding Hidden Metrics + +### The Discovery Questions + +For any experience, ask: + +**Scale Questions:** +- How many people/projects/customers? +- What was the budget/revenue involved? +- How large was the team? +- How many locations/regions? + +**Impact Questions:** +- What changed because of your work? +- What would have happened without you? +- What problems did you solve? +- What got better/faster/cheaper? + +**Comparison Questions:** +- How was it before vs. after? +- How did you compare to others/previous results? +- What was the baseline you improved? + +### Role-Specific Metric Discovery + +**Sales:** +- Quota attainment percentage +- Revenue generated +- Number of deals closed +- Average deal size +- Pipeline generated +- New accounts acquired +- Retention rate + +**Marketing:** +- Leads generated +- Campaign ROI +- Engagement rates +- Follower growth +- Website traffic increase +- Conversion rates +- Brand awareness metrics + +**Customer Service:** +- Tickets resolved per day +- Customer satisfaction score +- Average response time +- First call resolution rate +- NPS score contribution + +**Operations:** +- Efficiency improvements +- Cost reductions +- Process cycle times +- Error rate reductions +- Throughput increases + +**Engineering:** +- System uptime +- Performance improvements +- Bug resolution rate +- Deployment frequency +- Code coverage + +**Project Management:** +- Number of projects +- Budget sizes +- Team sizes +- On-time delivery rate +- Stakeholders managed + +**HR/Admin:** +- Hiring numbers +- Time to fill +- Employee satisfaction scores +- Training completion rates +- Onboarding efficiency + +## Estimation Techniques + +When you don't have exact numbers: + +### Conservative Estimation + +**Principle:** Estimate low to maintain credibility + +**Example:** +- You think you saved 100 hours/month → say "75+ hours" +- You think growth was 50% → say "~40%" +- You think you served 500 customers → say "400+" + +### Range Estimation + +**Format:** "X-Y" or "X to Y" + +**Examples:** +- "Managed team of 8-12" +- "Generated $100K-$150K in revenue" +- "Saved 20-30 hours weekly" + +### Minimum Bound + +**Format:** "X+" or "at least X" + +**Examples:** +- "Served 100+ customers daily" +- "Managed at least 15 concurrent projects" +- "Generated $500K+ in annual revenue" + +### Percentage of Activity + +**Format:** Calculate from known totals + +**Example:** +- Company had 1000 customers → You managed 20% → "Managed 200 customer accounts" +- Team had 10 people → You supervised 4 → "Supervised 40% of team" + +### Time-Based Calculation + +**Format:** Work backwards from frequency + +**Example:** +- Met with 5 clients/week × 50 weeks = "Consulted with 250+ clients annually" +- Processed 30 invoices/day × 250 days = "Processed 7,500+ invoices annually" + +## Quantification Templates + +### Before and After Template +``` +"Improved [X] from [before number] to [after number], resulting in [Y]% improvement" + +Example: +"Improved page load time from 8 seconds to 2 seconds, resulting in 75% reduction and 20% increase in user engagement" +``` + +### Scale Template +``` +"[Verb] [number] [things], resulting in [impact]" + +Example: +"Managed 25 concurrent projects worth $3M, delivering 95% on-time with zero budget overruns" +``` + +### Volume + Impact Template +``` +"Processed [number] [items] per [time period], achieving [quality metric]" + +Example: +"Resolved 50+ customer tickets daily, maintaining 98% satisfaction rating and 4-hour average response time" +``` + +### Comparison Template +``` +"Ranked #[X] out of [Y] in [metric], [context]" + +Example: +"Ranked #2 out of 45 sales representatives nationally, generating $3.2M in annual revenue" +``` + +## Common "I Have No Numbers" Situations + +### Situation 1: "I was just one person on a team" + +**Solution:** Focus on YOUR contribution + +**Example:** +- "Part of team that launched product" → +- "Contributed 40% of front-end code for product launch reaching 100K users" + +### Situation 2: "I don't have access to business metrics" + +**Solution:** Quantify activities and inputs + +**Example:** +- "Supported sales team" → +- "Created 50+ sales presentations and managed pipeline of 200+ prospects in Salesforce" + +### Situation 3: "My job didn't produce measurable outcomes" + +**Solution:** Measure the work itself + +**Example:** +- "Wrote documentation" → +- "Produced 75-page technical documentation reducing new hire onboarding time by 2 weeks" + +### Situation 4: "Results were confidential" + +**Solution:** Use percentages or ranges + +**Example:** +- "Increased revenue" → +- "Grew revenue by 40%+ year-over-year" or "Contributed to $X-$Y million growth" + +### Situation 5: "I was entry-level with limited impact" + +**Solution:** Quantify learning, throughput, accuracy + +**Example:** +- "Entered data" → +- "Processed 200+ records daily with 99.5% accuracy rate, exceeding team average by 15%" + +## Output Format + +When quantifying a resume: + +```markdown +# RESUME QUANTIFICATION + +## Analysis Summary +**Bullets without numbers:** X +**Bullets with numbers:** Y +**Target:** 100% of bullets should have at least one metric + +## Quantified Bullets + +### Original Bullet #1: +"Managed customer accounts" + +### Questions to Find Metrics: +- How many accounts? → [User answer: ~40] +- What was the revenue? → [User answer: ~$2M] +- What results did you achieve? → [User answer: retained most] + +### Quantified Version: +"Managed portfolio of 40 enterprise accounts representing $2M ARR, achieving 95% retention rate" + +### Metrics Added: +- Account count: 40 +- Revenue: $2M ARR +- Retention: 95% + +--- + +### Original Bullet #2: +[Continue for each bullet] + +## Estimation Notes +- [Metric]: Estimated based on [reasoning] +- [Metric]: Conservative estimate using [method] + +## Remaining Questions +- [Questions to ask user for missing information] +``` + +## Quantification Quality Checklist + +For each bullet: +- ✅ Has at least ONE number +- ✅ Number is relevant (not just any number) +- ✅ Scale is clear (what does the number mean?) +- ✅ Estimation is conservative and defensible +- ✅ Number adds credibility, not confusion +- ✅ You can explain the number in an interview + +## Numbers to Avoid + +- ❌ Numbers that make you look bad +- ❌ Numbers you can't explain or defend +- ❌ Numbers that reveal confidential information +- ❌ Exaggerated or inflated numbers +- ❌ Numbers without context (e.g., "increased by 300%" without baseline) +- ❌ Too many numbers in one bullet (2-3 max) + +## Key Principle + +**Every bullet can be quantified.** If you think your work can't be measured, you haven't asked the right questions yet. + +The goal isn't to have impressive numbers—it's to have SPECIFIC numbers that show the scope and impact of your work. diff --git a/.agents/skills/resume-section-builder/SKILL.md b/.agents/skills/resume-section-builder/SKILL.md new file mode 100644 index 00000000..54ffcf7e --- /dev/null +++ b/.agents/skills/resume-section-builder/SKILL.md @@ -0,0 +1,397 @@ +--- +name: resume-section-builder +description: Create targeted resume sections optimized for different experience levels and roles +--- + +# Resume Section Builder + +## When to Use This Skill + +Use this skill when the user: +- Needs help with specific resume sections +- Wants to optimize a particular part of their resume +- Is unsure what to include in a section +- Needs section-specific guidance +- Mentions: "resume sections", "skills section", "summary section", "experience section", "what to include" + +## Core Capabilities + +- Build targeted professional summaries +- Structure skills sections effectively +- Optimize experience sections +- Create education sections appropriately +- Add supplementary sections strategically +- Tailor sections for different career stages + +## Professional Summary Section + +### When to Include + +**Include Summary If:** +- Career changers (need to explain transition) +- Senior professionals (distill long career) +- Returning to workforce (address gaps) +- Highly specialized role (emphasize fit) + +**Skip Summary If:** +- Entry level with limited experience +- Straightforward career progression +- Space is at a premium + +### Summary Framework + +**Formula:** [Title/Identity] + [Years/Experience] + [Key Skills] + [Value Proposition] + +### By Career Stage + +**Entry Level:** +``` +Recent Computer Science graduate from UC Berkeley with internship experience in full-stack development. Skilled in Python, React, and AWS. Seeking to leverage academic projects in machine learning and user-facing application development in a software engineering role. +``` + +**Mid-Career:** +``` +Product Manager with 6 years driving B2B SaaS products from concept to scale. Track record of launching products that generated $10M+ ARR through data-driven roadmap prioritization and cross-functional leadership. Expert in API products, developer tools, and enterprise sales motions. +``` + +**Senior/Executive:** +``` +Technology executive with 15+ years building and scaling engineering organizations from 50 to 500+ across global markets. Proven success leading digital transformation initiatives, M&A integration, and platform modernization. P&L ownership of $100M+ with track record of 40%+ efficiency improvements. +``` + +**Career Changer:** +``` +Sales professional transitioning to Customer Success, bringing 5 years of consultative selling experience and proven ability to build lasting client relationships. Skilled in needs assessment, solution design, and stakeholder management. Seeking to apply relationship-building expertise to drive customer retention and expansion. +``` + +### Summary Don'ts + +- ❌ "Seeking a challenging position..." +- ❌ "Hard-working team player..." +- ❌ "Results-oriented professional..." +- ❌ Third person ("John is a...") +- ❌ Objectives (what you want vs. what you offer) + +## Skills Section + +### Organization Options + +**Option 1: Simple List** +``` +SKILLS +Python, JavaScript, SQL, React, Node.js, AWS, Docker, Git, Agile, JIRA +``` +Best for: ATS optimization, space constraints + +**Option 2: Categorized** +``` +TECHNICAL SKILLS +Languages: Python, JavaScript, TypeScript, SQL +Frameworks: React, Node.js, Django, FastAPI +Cloud: AWS (EC2, S3, Lambda), GCP, Docker, Kubernetes +Tools: Git, JIRA, Confluence, Datadog +``` +Best for: Technical roles, extensive skill sets + +**Option 3: Proficiency Levels** (use carefully) +``` +SKILLS +Expert: Python, SQL, Product Management +Advanced: AWS, Data Analysis, Stakeholder Management +Proficient: Machine Learning, Figma, Financial Modeling +``` +Best for: Roles requiring specific proficiency, honest representation + +### What to Include + +**Technical/Hard Skills:** +- Programming languages +- Software and tools +- Methodologies (Agile, Lean) +- Platforms (Salesforce, SAP) +- Certifications + +**Functional Skills:** +- Project management +- Financial analysis +- Data analysis +- Market research + +**Industry Knowledge:** +- Domain expertise +- Regulatory knowledge +- Industry certifications + +### What to Exclude + +- ❌ Microsoft Office (assumed) +- ❌ "Basic" skills +- ❌ Skills you can't discuss in interview +- ❌ Soft skills (show, don't list) +- ❌ Outdated technologies + +## Experience Section + +### Standard Format + +``` +COMPANY NAME | City, State +Job Title | Start Date - End Date + +• Achievement bullet with metric and impact +• Achievement bullet with metric and impact +• Achievement bullet with metric and impact +``` + +### Bullet Guidelines by Career Stage + +**Entry Level (0-2 years):** +- 3-5 bullets per role +- Include relevant projects +- Quantify where possible +- Show initiative and learning + +**Mid-Career (3-10 years):** +- 4-6 bullets for recent roles +- 2-3 bullets for older roles +- Focus on achievements over duties +- Strong metrics throughout + +**Senior (10+ years):** +- 5-6 bullets for recent roles +- 2-3 bullets for older roles +- Emphasize leadership and strategy +- Show increasing scope + +### Handling Different Situations + +**Multiple Roles at Same Company:** +``` +COMPANY NAME | City, State +Senior Manager | 2021 - Present +• [Bullets for current role] + +Manager | 2019 - 2021 +• [Bullets for previous role] + +Analyst | 2017 - 2019 +• [Bullets for earliest role] +``` + +**Short Tenure:** +- Include if relevant experience +- Frame around project or achievement +- Don't apologize or explain in resume + +**Contract/Freelance:** +``` +Freelance Product Consultant | 2022 - Present +Clients include: Company A, Company B, Company C +• [Achievement with Client A] +• [Achievement with Client B] +``` + +## Education Section + +### Standard Format + +``` +EDUCATION + +Bachelor of Science in Computer Science +University of California, Berkeley | 2020 +GPA: 3.8/4.0 | Honors: Magna Cum Laude +``` + +### What to Include by Career Stage + +**Entry Level:** +- Degree, major, school, year +- GPA (if 3.5+) +- Honors and awards +- Relevant coursework +- Academic projects +- Study abroad + +**Mid-Career:** +- Degree, major, school, year +- GPA only if exceptional +- Skip coursework (replaced by experience) + +**Senior:** +- Degree, school +- May skip year (age discrimination) +- Professional development more relevant + +### Advanced Degrees + +``` +MBA, Finance & Strategy +Harvard Business School | 2022 +• Leadership Fellow +• Relevant coursework: Corporate Finance, M&A Strategy + +M.S. in Computer Science +Stanford University | 2018 +• Specialization: Artificial Intelligence +• Thesis: "Title of Thesis" +``` + +### Certifications + +``` +CERTIFICATIONS +AWS Solutions Architect Associate | Amazon Web Services | 2023 +PMP (Project Management Professional) | PMI | 2022 +Google Analytics Certified | Google | 2023 +``` + +## Additional Sections + +### When to Include Each + +**Projects Section:** +- Entry level with limited work experience +- Career changers showing new skills +- Technical roles with personal projects + +**Volunteer Section:** +- Relevant volunteer experience +- Leadership roles +- Fills employment gaps meaningfully + +**Languages:** +- If relevant to role/company +- List proficiency levels accurately +- Only if beyond basic conversational + +**Publications/Patents:** +- Academic positions +- Research roles +- Technical thought leadership + +**Awards/Recognition:** +- Significant industry awards +- Company-wide recognition +- Relevant honors + +### Format Examples + +**Projects:** +``` +PROJECTS +E-commerce Platform | React, Node.js, PostgreSQL | github.com/user/project +• Built full-stack marketplace with 500+ active users +• Implemented payment processing with Stripe integration +``` + +**Volunteer:** +``` +VOLUNTEER EXPERIENCE +Board Member | Local Nonprofit | 2021 - Present +• Led fundraising committee, increasing annual donations by 40% +``` + +**Languages:** +``` +LANGUAGES +English (Native) | Spanish (Professional) | Mandarin (Conversational) +``` + +## Section Order by Role Type + +### Standard Order +1. Contact +2. Summary (optional) +3. Skills +4. Experience +5. Education +6. Additional + +### Technical Roles +1. Contact +2. Skills (prioritized) +3. Experience +4. Projects +5. Education +6. Certifications + +### Recent Graduate +1. Contact +2. Education (prioritized) +3. Skills +4. Experience/Internships +5. Projects +6. Activities + +### Executive +1. Contact +2. Executive Summary +3. Career Highlights +4. Experience +5. Board Roles +6. Education + +### Career Changer +1. Contact +2. Summary (explaining transition) +3. Skills (transferable) +4. Experience (reframed) +5. Bridge Experience +6. Education + +## Output Format + +When building resume sections: + +```markdown +# RESUME SECTION RECOMMENDATIONS + +## For: [User's situation/role] + +### Recommended Section Order +1. [Section] - [Why] +2. [Section] - [Why] +... + +### Section Details + +#### Professional Summary +**Recommendation:** [Include/Skip] +**Draft:** +[Written summary if recommended] + +#### Skills Section +**Format:** [Simple/Categorized/Proficiency] +**Content:** +[Organized skills list] + +#### Experience Section +**Format:** [Standard/Functional/Hybrid] +**Bullets per Role:** +- Recent: [X] bullets +- Older: [X] bullets + +#### Education Section +**Include:** +- [Items to include] +**Exclude:** +- [Items to exclude] + +#### Additional Sections +**Recommended:** [Section name] because [reason] +**Skip:** [Section name] because [reason] +``` + +## Section-Building Checklist + +- ✅ Section order optimized for role +- ✅ Summary is concise and targeted (if included) +- ✅ Skills are relevant and organized +- ✅ Experience bullets are achievement-focused +- ✅ Education appropriate for career stage +- ✅ Additional sections add value (not filler) +- ✅ Consistent formatting throughout +- ✅ All sections support the target role +- ✅ Nothing irrelevant or outdated included +- ✅ Total length appropriate (1-2 pages) diff --git a/.agents/skills/resume-tailor/SKILL.md b/.agents/skills/resume-tailor/SKILL.md new file mode 100644 index 00000000..87d31a68 --- /dev/null +++ b/.agents/skills/resume-tailor/SKILL.md @@ -0,0 +1,312 @@ +--- +name: resume-tailor +description: Customize resume for specific job postings while maintaining truthfulness +--- + +# Resume Tailor + +## When to Use This Skill + +Use this skill when the user wants to: +- Customize their resume for a specific job posting +- Adjust their resume to match job requirements +- Create a targeted version of their resume +- Mentions: "tailor resume", "customize resume", "target role", "specific job", "match job description" + +Use AFTER job-description-analyzer to know what to emphasize. + +## Core Capabilities + +- Reorder experience sections by relevance to target role +- Adjust professional summary for specific position +- Add missing keywords from job description +- Modify bullet points to match job requirements +- Maintain authenticity while optimizing match +- Create multiple targeted resume versions + +## The Tailoring Philosophy + +**Key Principle:** You're not lying or fabricating - you're HIGHLIGHTING the most relevant parts of your true experience. + +Think of your full experience as a library of achievements. Tailoring means selecting the books that best fit what each employer is looking for. + +## Tailoring Process + +### Step 1: Analyze the Job (Use Job Description Analyzer First) +- Identify required skills and keywords +- Note the company's priorities +- Understand the role's primary responsibilities + +### Step 2: Audit Your Resume +For each section, ask: +- Does this support my candidacy for THIS specific role? +- Is there a better way to phrase this for THIS job? +- Should this be higher or lower in priority? + +### Step 3: Make Strategic Adjustments + +**Professional Summary:** Rewrite to mirror the job's key requirements + +**Skills Section:** Reorder to put most relevant skills first, add missing keywords + +**Experience:** +- Reorder jobs if a less recent role is more relevant +- Swap bullet points to lead with most relevant achievements +- Add keywords naturally into existing bullets + +**Education:** Highlight relevant coursework, certifications + +## Section-by-Section Tailoring Guide + +### Professional Summary + +This is your "elevator pitch" - customize for each application. + +**Generic Summary (AVOID):** +``` +Results-driven professional with 5 years of experience in business operations. Strong analytical and communication skills. Looking for a challenging opportunity to grow. +``` + +**Tailored for Operations Manager Role:** +``` +Operations Manager with 5 years optimizing supply chain processes and reducing costs by 25%. Expertise in Lean Six Sigma, vendor management, and cross-functional team leadership. Track record of improving operational efficiency while maintaining quality standards. +``` + +**Tailored for Project Manager Role (Same Person):** +``` +Project Manager with 5 years leading cross-functional initiatives from concept to delivery. PMP-certified with expertise in Agile methodology, stakeholder management, and budget oversight. Track record of on-time, under-budget project delivery across $10M+ portfolios. +``` + +### Skills Section Reordering + +**Job Description Emphasizes:** Data analysis, SQL, Python, stakeholder communication + +**Before (Generic Order):** +``` +Skills: Microsoft Office, Communication, Project Management, Python, SQL, Data Visualization, Leadership +``` + +**After (Tailored Order):** +``` +Skills: SQL, Python, Data Analysis, Data Visualization, Stakeholder Communication, Project Management, Microsoft Office +``` + +### Experience Section + +**Strategy 1: Reorder Jobs** + +If your most recent job is less relevant than a previous role: + +**Before:** +1. Marketing Coordinator (current, but applying for data role) +2. Data Analyst (previous, highly relevant) + +**After:** +1. Data Analyst (labeled with dates, moved up) +2. Marketing Coordinator (still included, but secondary) + +**Strategy 2: Swap Bullet Order** + +Lead with bullets most relevant to the target role. + +**Applying for Management Role - Lead with:** +- "Led team of 12..." +- "Managed budget of $2M..." + +**Applying for Technical Role - Lead with:** +- "Developed automated system..." +- "Analyzed 500K+ data points..." + +**Strategy 3: Adjust Bullet Language** + +Incorporate job description keywords while staying truthful. + +**Job Description Says:** "stakeholder management" +**Your Bullet Says:** "Worked with various teams" +**Tailored Version:** "Managed stakeholder relationships across 5 departments, ensuring alignment on project priorities" + +## Tailoring Templates + +### For Each Job Application, Create: + +```markdown +## RESUME TAILORING PLAN + +**Target Position:** [Job Title] +**Company:** [Company Name] +**Match Score:** [From JD Analyzer] + +### Summary Customization +**Current:** [Your current summary] +**Tailored:** [Rewritten for this role] + +### Skills Reordering +**Current Order:** [List] +**New Order:** [Reordered list with added keywords] +**Keywords Added:** [New skills from JD] + +### Experience Adjustments + +**Job 1: [Title]** +- Bullet to emphasize: [Which bullet to lead with] +- Keyword to add: [What phrase to incorporate] +- Bullet to de-emphasize: [Move down or remove if space needed] + +**Job 2: [Title]** +[Same structure] + +### Other Adjustments +- Education: [Any relevant coursework to add] +- Certifications: [Any to highlight] +- Projects: [Relevant projects to include] +``` + +## Common Tailoring Scenarios + +### Scenario 1: Technical Role at Non-Tech Company + +**Challenge:** They want technical skills but also business acumen + +**Strategy:** +- Lead with technical achievements +- Include business impact in every technical bullet +- Add "translated technical concepts for business stakeholders" + +### Scenario 2: Management Role (But You've Done Both IC and Management) + +**Challenge:** Show leadership without abandoning technical credibility + +**Strategy:** +- Summary: Emphasize leadership +- Experience: Lead with team management bullets +- Keep some technical bullets to show you understand the work + +### Scenario 3: Startup (But You've Worked at Big Companies) + +**Challenge:** Show you can thrive in ambiguity and wear many hats + +**Strategy:** +- Highlight cross-functional work +- Emphasize initiative and self-starting +- Show scrappy, creative problem-solving +- De-emphasize rigid processes and large team structures + +### Scenario 4: Big Company (But You've Worked at Startups) + +**Challenge:** Show you can work within structure and at scale + +**Strategy:** +- Emphasize process improvement +- Highlight work that scaled +- Show collaboration across teams +- Add metrics that show impact at scale + +## Keyword Integration Rules + +### DO: +- Add keywords that truthfully describe your work +- Use exact phrasing from job description when accurate +- Place keywords naturally in context +- Include keywords in multiple locations (summary, skills, experience) + +### DON'T: +- Add skills you don't actually have +- Keyword stuff (repeating same term 10x) +- Create a different meaning than your actual experience +- Sacrifice readability for keyword density + +## Truth vs. Tailoring Line + +**Acceptable Tailoring:** +- Reordering true information +- Emphasizing relevant experience +- Using industry-standard terminology +- Adding context to vague statements +- Matching language style to job description + +**Unacceptable (Lying):** +- Adding skills you don't have +- Changing numbers or metrics +- Creating fake experiences +- Claiming titles you didn't hold +- Stating certifications you don't have + +## Version Management + +### Maintain a Master Resume +- Keep ONE complete document with ALL experiences +- Include every bullet you've ever written +- This is your "source of truth" + +### Create Targeted Versions +- Name files clearly: "JohnSmith_Resume_ProductManager_TechCorp.pdf" +- Track which version went to which company +- Save tailoring notes for interview prep + +### Version Naming Convention +``` +[LastName]_Resume_[TargetRole]_[Company]_[Date].pdf + +Examples: +- Smith_Resume_PM_Google_Jan2024.pdf +- Smith_Resume_DataAnalyst_Meta_Jan2024.pdf +- Smith_Resume_General_Master.docx (your master file) +``` + +## Quick Tailoring Checklist + +Before submitting any resume: + +1. ✅ Summary mentions the exact job title/function +2. ✅ Top 5 skills match job description's top 5 requirements +3. ✅ Most relevant experience is positioned first +4. ✅ Each job's top bullet addresses job's key requirement +5. ✅ Keywords from JD appear naturally throughout +6. ✅ Company/industry terminology is used correctly +7. ✅ All claims are truthful +8. ✅ File is named appropriately +9. ✅ ATS formatting maintained +10. ✅ Saved for interview prep reference + +## Output Format + +When tailoring a resume, provide: + +```markdown +# TAILORED RESUME CHANGES + +## Target: [Job Title] at [Company] + +### Professional Summary +**Before:** [Original] +**After:** [Tailored version] +**Keywords Added:** [List] + +### Skills Section +**New Order:** [Reordered list] +**Added:** [New keywords] +**Removed:** [If any, for space] + +### Experience Changes + +**[Company Name] - [Title]** +- Move bullet X to position 1 +- Modify bullet Y: [Before → After] +- Add keyword "[phrase]" to bullet Z + +[Repeat for each relevant job] + +### Overall Changes Summary +- Keywords added: X +- Bullets modified: Y +- Sections reordered: Yes/No +- Estimated new match score: Z% +``` + +## Implementation Notes + +- Always start with the job description analyzer +- Keep tailoring changes documented for interview prep +- Maintain master resume as source of truth +- Never sacrifice ATS compatibility for tailoring +- Test keyword match after tailoring diff --git a/.agents/skills/resume-version-manager/SKILL.md b/.agents/skills/resume-version-manager/SKILL.md new file mode 100644 index 00000000..32940f4d --- /dev/null +++ b/.agents/skills/resume-version-manager/SKILL.md @@ -0,0 +1,351 @@ +--- +name: resume-version-manager +description: Track different resume versions, maintain master resume, manage tailored versions +--- + +# Resume Version Manager + +## When to Use This Skill + +Use this skill when the user: +- Has multiple resume versions to manage +- Needs to track tailored resumes +- Wants to maintain a master resume +- Is applying to many different roles +- Mentions: "resume versions", "master resume", "different versions", "track resumes", "which resume" + +## Core Capabilities + +- Create and maintain master resume document +- Track tailored resume versions +- Organize resume versions by role/industry +- Maintain consistent source of truth +- Streamline resume updates +- Prevent version confusion + +## The Version Management Problem + +**Common Pain Points:** +- "Which version did I send to Company X?" +- "Where's my most recent resume?" +- "I have 15 resume files and don't know which is best" +- "I forgot to update my resume after that project" +- "I keep tailoring from different base versions" + +**The Solution:** +A systematic approach with: +1. One master resume (source of truth) +2. Organized tailored versions +3. Clear naming conventions +4. Update workflow + +## Master Resume Concept + +### What is a Master Resume? + +A comprehensive document containing: +- ALL your experiences (not just recent) +- ALL bullet points you've ever written +- Every achievement, project, skill +- Full details (even if they won't fit on one page) + +**Purpose:** Source of truth to pull from when tailoring + +### Master Resume Structure + +```markdown +# MASTER RESUME - [YOUR NAME] +Last Updated: [Date] + +## CONTACT INFORMATION +[Full contact details] + +## PROFESSIONAL SUMMARY VERSIONS +[Summary for Role Type A] +[Summary for Role Type B] +[Summary for Role Type C] + +## ALL SKILLS +### Technical Skills +[Complete list by category] + +### Soft Skills +[Complete list] + +### Industry Knowledge +[All domains] + +## PROFESSIONAL EXPERIENCE + +### Company Name | Title | Dates + +**All Bullets (choose best for each application):** +• Bullet 1 (leadership focused) +• Bullet 2 (technical focused) +• Bullet 3 (results focused) +• Bullet 4 (collaboration focused) +• Bullet 5 (additional achievement) +• Bullet 6 (additional achievement) + +**Keywords this experience covers:** +[List of keywords this job demonstrates] + +### Previous Company | Title | Dates +[Same format...] + +## EDUCATION +[Complete education history] + +## CERTIFICATIONS +[All certifications ever earned] + +## PROJECTS +[All notable projects] + +## VOLUNTEER / ADDITIONAL +[All other relevant experience] +``` + +## File Organization System + +### Folder Structure + +``` +Resume/ +├── Master/ +│ └── LastName_Master_Resume.docx +├── Tailored/ +│ ├── ProductManagement/ +│ │ ├── LastName_PM_Google_Jan2024.pdf +│ │ └── LastName_PM_Meta_Jan2024.pdf +│ ├── Engineering/ +│ │ ├── LastName_SWE_Startup_Feb2024.pdf +│ │ └── LastName_SWE_Enterprise_Feb2024.pdf +│ └── General/ +│ └── LastName_General_Resume.pdf +├── CoverLetters/ +│ ├── Google_PM_CoverLetter.docx +│ └── Meta_PM_CoverLetter.docx +└── Applications/ + └── ApplicationTracker.xlsx +``` + +### File Naming Convention + +**Pattern:** +`[LastName]_[Role/Type]_[Company]_[Date].pdf` + +**Examples:** +- `Smith_ProductManager_Google_Jan2024.pdf` +- `Smith_SWE_Stripe_Feb2024.pdf` +- `Smith_DataScience_General_2024.pdf` +- `Smith_Master_Resume_v3.docx` + +## Version Categories + +### By Target Role + +**Product Management:** +- Emphasizes: Strategy, roadmap, metrics, stakeholders +- Skills highlight: Product tools, analytics, user research + +**Software Engineering:** +- Emphasizes: Technical projects, systems, code +- Skills highlight: Languages, frameworks, tools + +**Data Science:** +- Emphasizes: Analysis, ML, statistical methods +- Skills highlight: Python, SQL, ML libraries + +### By Industry + +**Tech/Startup:** +- Emphasizes: Innovation, growth, scrappiness +- Tone: Modern, direct, achievement-focused + +**Enterprise/Corporate:** +- Emphasizes: Scale, process, collaboration +- Tone: Professional, structured, comprehensive + +**Finance:** +- Emphasizes: Analysis, compliance, accuracy +- Tone: Conservative, precise, credentialed + +### By Seniority Level + +**Individual Contributor:** +- Focus on execution and technical skills +- Detailed project descriptions +- Technical accomplishments + +**Manager:** +- Team leadership and development +- Cross-functional collaboration +- Business impact metrics + +**Executive:** +- Strategic leadership +- P&L responsibility +- Organizational transformation + +## Application Tracking + +### Simple Tracker Spreadsheet + +``` +| Company | Role | Version Used | Date Applied | Status | Notes | +|---------|------|--------------|--------------|--------|-------| +| Google | PM | PM_Google_Jan | 1/15/24 | Interview | 2nd round 2/1 | +| Meta | PM | PM_Meta_Jan | 1/18/24 | Applied | Referral from John | +| Startup | PM | PM_General | 1/20/24 | Rejected | Too senior | +``` + +### Information to Track + +- Company name +- Job title +- Resume version used +- Cover letter version used +- Application date +- Application method (portal, referral, direct) +- Current status +- Follow-up dates +- Notes and contacts + +## Update Workflow + +### When to Update Master Resume + +**Immediately Update For:** +- New job or promotion +- Completed major project +- New skills or certifications +- Significant achievements +- Awards or recognition + +**Quarterly Review:** +- Add recent accomplishments +- Update metrics with new data +- Refresh skills section +- Remove outdated information + +### Master to Tailored Workflow + +``` +1. Start with Master Resume + ↓ +2. Copy to new file (don't edit master) + ↓ +3. Analyze job description + ↓ +4. Select relevant bullets from master + ↓ +5. Choose appropriate summary version + ↓ +6. Reorder skills for relevance + ↓ +7. Add job-specific keywords + ↓ +8. Trim to appropriate length + ↓ +9. Save with proper naming convention + ↓ +10. Update application tracker +``` + +## Common Scenarios + +### Scenario 1: Applying to Similar Roles + +**Strategy:** +- Create one well-tailored version for the role type +- Make minor adjustments for each company +- Track which slight variation went where + +### Scenario 2: Applying to Different Role Types + +**Strategy:** +- Create separate base versions for each role type +- Maintain clear folder organization +- Each version pulls from same master + +### Scenario 3: Rapid Application Volume + +**Strategy:** +- Create 2-3 strong category versions +- Use "general" versions for quick applications +- Reserve deep tailoring for top choices + +### Scenario 4: Career Transition + +**Strategy:** +- Create transition-focused version +- Emphasize transferable skills +- Maintain original industry version as backup + +## Version Control Best Practices + +### DO: +- ✅ Always work from master as source +- ✅ Use consistent naming conventions +- ✅ Track which version went where +- ✅ Keep master updated +- ✅ Date your files +- ✅ Backup to cloud storage + +### DON'T: +- ❌ Edit master directly for applications +- ❌ Use vague names like "resume_final_v2" +- ❌ Forget which version you sent +- ❌ Let master get out of date +- ❌ Have multiple "master" files +- ❌ Delete old versions (archive instead) + +## Output Format + +When managing resume versions: + +```markdown +# RESUME VERSION MANAGEMENT + +## Master Resume Status +**Last Updated:** [Date] +**Location:** [File path] +**Total Experience Entries:** [X] +**Total Bullet Points Available:** [X] + +## Active Versions + +### Role Type: Product Management +**Base Version:** PM_General_2024.docx +**Tailored Versions:** +| Company | File Name | Date Created | Status | +|---------|-----------|--------------|--------| +| Google | PM_Google_Jan24 | 1/15/24 | Submitted | +| Meta | PM_Meta_Jan24 | 1/18/24 | Submitted | + +### Role Type: Engineering +[Same structure] + +## Update Queue +- [ ] Add Q4 project results to master +- [ ] Update skills with new certification +- [ ] Archive versions older than 6 months + +## Recommended Actions +1. [Action 1] +2. [Action 2] +``` + +## Version Management Checklist + +- ✅ Master resume exists and is current +- ✅ Folder structure is organized +- ✅ Naming convention is consistent +- ✅ Application tracker is maintained +- ✅ Know which version sent to each company +- ✅ All versions pull from same master +- ✅ Backup system in place +- ✅ Old versions archived (not deleted) +- ✅ Update workflow is established +- ✅ Regular master resume reviews scheduled diff --git a/.agents/skills/salary-negotiation-prep/SKILL.md b/.agents/skills/salary-negotiation-prep/SKILL.md new file mode 100644 index 00000000..2c43fdd6 --- /dev/null +++ b/.agents/skills/salary-negotiation-prep/SKILL.md @@ -0,0 +1,378 @@ +--- +name: salary-negotiation-prep +description: Research market rates, build negotiation strategy, and create counter-offer scripts +--- + +# Salary Negotiation Prep + +## When to Use This Skill + +Use this skill when the user wants to: +- Negotiate a job offer or salary +- Research market rates for their role +- Create a counter-offer strategy +- Understand total compensation packages +- Mentions: "salary negotiation", "negotiate offer", "counter offer", "compensation", "how much should I ask for" + +## Core Capabilities + +- Research and validate market compensation +- Build negotiation strategy and scripts +- Calculate total compensation (not just base salary) +- Prepare counter-offer responses +- Identify negotiation leverage points +- Navigate difficult salary conversations + +## The Negotiation Mindset + +**Key Principles:** +1. Negotiation is expected - companies budget for it +2. 84% of employers expect candidates to negotiate +3. Not negotiating leaves $500K-$1M on the table over a career +4. The goal is win-win, not adversarial + +**What You're Really Negotiating:** +- Base salary +- Signing bonus +- Annual bonus/commission +- Equity (stock options, RSUs) +- Benefits (401k match, insurance) +- Perks (vacation, remote work, professional development) +- Start date +- Title + +## Research Phase + +### Step 1: Determine Market Rate + +**Sources to Check:** +- Levels.fyi (best for tech) +- Glassdoor (general, take with grain of salt) +- LinkedIn Salary +- Blind (anonymous reports) +- PayScale +- Salary.com +- H1B salary data (publicly available) + +**Build a Range:** +``` +Low (25th percentile): $XXX,XXX +Target (50th percentile): $XXX,XXX +High (75th percentile): $XXX,XXX +Stretch (90th percentile): $XXX,XXX +``` + +### Step 2: Know Your Value + +**Factors That Increase Your Worth:** +- Years of relevant experience +- Specialized/rare skills +- Track record of results +- In-demand certifications +- Current competing offers +- Referral from employee +- Market demand in your field + +**Factors That May Limit:** +- Entry level or career change +- Less experience than ideal candidate +- Gaps in required skills +- Location arbitrage (lower cost of living) + +### Step 3: Calculate Total Compensation + +**Total Comp = Base + Bonus + Equity + Benefits** + +``` +EXAMPLE: +Base Salary: $150,000 +Target Bonus (15%): $22,500 +RSU Grant (4-year): $200,000 ($50,000/year) +401k Match (4%): $6,000 +Benefits Value: ~$15,000 + +Total Annual Comp: $243,500 +``` + +**Common Equity Terms:** +- **RSUs:** Restricted Stock Units (real shares, taxed when vesting) +- **Options:** Right to buy at strike price (value = current price - strike price) +- **Vesting:** Typically 4-year with 1-year cliff +- **Refresh grants:** Annual additional equity grants + +## Negotiation Strategy + +### When to Negotiate + +**Best Time:** After you have a written offer, before you sign + +**Timeline:** +1. Receive verbal offer → Express enthusiasm, ask for written offer +2. Receive written offer → Thank them, ask for time to review +3. Research and prepare → 24-48 hours +4. Counter with ask → Email or call +5. Discussion/back and forth → May take several rounds +6. Final agreement → Get in writing + +### The Counter-Offer Framework + +**Structure:** +1. Express enthusiasm +2. Reinforce your value +3. Make specific ask +4. Provide justification +5. Open discussion + +### Counter-Offer Email Template + +``` +Subject: [Your Name] - Offer Discussion + +Hi [Recruiter/Hiring Manager], + +Thank you so much for the offer to join [Company] as [Title]. I'm very excited about the opportunity to [specific thing about the role]. After speaking with the team and learning more about [something specific], I'm confident this is the right fit. + +I've had time to review the offer details and wanted to discuss the compensation. Based on my research of the market and my [X years of experience / specific valuable skill / competing offer], I was hoping we could discuss a base salary of $[Your Ask] rather than $[Their Offer]. + +[Optional: Add specific justification - e.g., "I've seen similar roles at [comparable companies] in this range" or "Given the scope of the role and my track record of [specific achievement], I believe this reflects my value."] + +I'm flexible and open to discussing other elements of the package as well. Would you have time for a quick call to discuss? + +Thank you again for this opportunity. I'm looking forward to finding a package that works for both of us. + +Best, +[Your Name] +``` + +### Counter-Offer Call Script + +``` +"Hi [Name], thanks for making time. I'm really excited about this opportunity - [genuine specific reason]. + +I've reviewed the offer and want to discuss compensation. Based on my market research and [X years experience / key accomplishment / competing offer], I was hoping for a base salary closer to $[Amount]. + +Is there flexibility there?" + +[LISTEN - Let them respond] + +[If they push back:] +"I understand there are constraints. I'm flexible - could we look at other elements like signing bonus, equity, or [other element] to bridge the gap?" + +[If they say they'll need to check:] +"That's totally fair. When would be a good time to reconnect?" +``` + +## Common Negotiation Scenarios + +### Scenario 1: First Offer Is Low + +**Approach:** +- Don't accept immediately +- Express enthusiasm for role +- Counter with research-backed number +- Be prepared to justify + +**Script:** +``` +"I'm thrilled about the opportunity. The base salary is lower than I expected based on my research. For this role and market, I was expecting something in the $X-$Y range. Is there room to move closer to $X?" +``` + +### Scenario 2: They Ask Your Salary Expectations First + +**Deflection Strategy:** +``` +"I'm flexible and focused on finding the right fit. What's the range you've budgeted for this role?" +``` + +**If Pressed:** +``` +"Based on my research for this role and market, I'm looking at $X-$Y, but I'm open to discussing the full compensation picture." +``` + +### Scenario 3: They Won't Budge on Base + +**Alternatives to Negotiate:** +- Signing bonus (one-time, easier to approve) +- Additional equity +- Earlier performance review (sooner raise) +- More vacation days +- Remote work flexibility +- Professional development budget +- Title upgrade +- Relocation assistance +- Start date + +**Script:** +``` +"I understand the base salary is firm. Could we discuss a signing bonus to help bridge the gap? Something in the range of $X would make this work." +``` + +### Scenario 4: You Have Competing Offers + +**Use Carefully:** +- Only mention if true +- Don't make it a threat +- Frame as problem-solving + +**Script:** +``` +"I want to be transparent - I'm also in discussions with [another company/a few other companies]. They're offering $X. [Your Company] is my first choice because [genuine reason], but I want to make sure the compensation is competitive." +``` + +### Scenario 5: They Ask About Current Salary + +**In Many States, This Question Is Illegal** + +**If Asked (and legal):** +``` +"I'd prefer to focus on the value I'd bring to this role and what the market rate is. What's the range you've budgeted?" +``` + +**Or Redirect:** +``` +"My current compensation isn't really comparable since [different location/role/structure]. Based on my research for this role, I'm targeting $X-$Y." +``` + +## Negotiation Tactics + +### Do's: +- ✅ Always negotiate (respectfully) +- ✅ Get the offer in writing before negotiating +- ✅ Research thoroughly +- ✅ Be specific with numbers +- ✅ Express genuine enthusiasm +- ✅ Give them a way to say yes +- ✅ Consider total compensation +- ✅ Be patient - process takes time +- ✅ Get final agreement in writing + +### Don'ts: +- ❌ Accept on the spot +- ❌ Give a salary history (if not required by law) +- ❌ Make ultimatums +- ❌ Lie about competing offers +- ❌ Be rude or aggressive +- ❌ Negotiate just for the sake of it +- ❌ Accept verbal promises without writing +- ❌ Burn bridges if it doesn't work out + +## Total Compensation Comparison + +### Side-by-Side Analysis + +```markdown +## OFFER COMPARISON + +| Component | Company A | Company B | Notes | +|-----------|-----------|-----------|-------| +| Base Salary | $150,000 | $140,000 | A wins | +| Target Bonus | 15% ($22,500) | 20% ($28,000) | B wins | +| Signing Bonus | $20,000 | $10,000 | A wins | +| Equity (annual) | $50,000 | $75,000 | B wins | +| 401k Match | 4% ($6,000) | 6% ($8,400) | B wins | +| Benefits | Standard | Premium | B wins | +| WFH | Hybrid (3 days) | Full remote | B wins | +| Vacation | 3 weeks | Unlimited | Depends | + +**Year 1 Total Comp:** +Company A: $248,500 +Company B: $261,400 + +**Analysis:** Company B is higher total comp, but Company A has higher base which affects future raises and mortgage qualification. +``` + +## Negotiation Timeline Template + +``` +Day 1: Receive offer +- Thank them, express enthusiasm +- Ask for offer in writing +- Ask deadline for response + +Day 1-3: Research +- Verify market rate +- Calculate total comp +- Identify priorities +- Prepare counter-offer + +Day 3-5: Counter +- Send counter-offer email or schedule call +- Be specific about ask + +Day 5-10: Discussion +- May require several rounds +- Be patient but responsive +- Stay professional and positive + +Day 10+: Resolution +- Agree on terms +- Get everything in writing +- Sign and celebrate! +``` + +## Output Format + +When preparing salary negotiation: + +```markdown +# SALARY NEGOTIATION STRATEGY + +## Market Research Summary +**Role:** [Title] +**Location:** [City/Remote] +**Experience Level:** [Years] + +**Market Range:** +- 25th percentile: $XXX,XXX +- 50th percentile: $XXX,XXX (target) +- 75th percentile: $XXX,XXX +- 90th percentile: $XXX,XXX (stretch) + +**Sources:** [List sources used] + +## Their Offer +| Component | Amount | +|-----------|--------| +| Base | $XXX,XXX | +| Bonus | X% | +| Equity | $XXX,XXX | +| Signing | $XXX | +| Total Year 1 | $XXX,XXX | + +## Your Counter +| Component | Ask | Justification | +|-----------|-----|---------------| +| Base | $XXX,XXX | [Why] | +| Signing | $XXX | [Why] | +| [Other] | | | + +## Counter-Offer Script +[Email template or call script customized for this situation] + +## If They Push Back +**Plan B:** [Alternative elements to negotiate] +**Walk-away Point:** [Your minimum] + +## Key Talking Points +1. [Your experience/value point] +2. [Market data point] +3. [Specific achievement] + +## Questions to Clarify +- [Equity vesting schedule?] +- [Bonus guaranteed?] +- [Review cycle timeline?] +``` + +## Implementation Checklist + +1. ✅ Research market rate from 3+ sources +2. ✅ Calculate total compensation (not just base) +3. ✅ Identify your priorities +4. ✅ Determine walk-away point +5. ✅ Prepare counter-offer with justification +6. ✅ Write or practice negotiation script +7. ✅ Plan for pushback scenarios +8. ✅ Get agreement in writing +9. ✅ Review final offer letter carefully +10. ✅ Sign and celebrate! diff --git a/.agents/skills/sanity-best-practices/SKILL.md b/.agents/skills/sanity-best-practices/SKILL.md new file mode 100644 index 00000000..43335c95 --- /dev/null +++ b/.agents/skills/sanity-best-practices/SKILL.md @@ -0,0 +1,74 @@ +--- +name: sanity-best-practices +description: Sanity development best practices for schema design, GROQ queries, TypeGen, Visual Editing, images, Portable Text, Studio structure, localization, migrations, Sanity Functions, Blueprints, and framework integrations such as Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, Hydrogen, and the App SDK. Use this skill whenever working with Sanity schemas, defineType or defineField, GROQ or defineQuery, content modeling, Presentation or preview setups, Sanity-powered frontend integrations, Sanity Functions, documentEventHandler, defineDocumentFunction, defineMediaLibraryAssetFunction, @sanity/functions, @sanity/blueprints, sanity.blueprint.ts, event-driven content automation, or when reviewing and fixing a Sanity codebase. +--- + +# Sanity Best Practices + +Comprehensive best practices and integration guides for Sanity development, maintained by Sanity. Use the quick reference below to load only the one or two topic files that match the task. + +## When to Apply + +Reference these guidelines when: +- Setting up a new Sanity project or onboarding +- Integrating Sanity with a frontend framework (Next.js, Nuxt, Astro, Remix, SvelteKit, Hydrogen) +- Writing GROQ queries or optimizing performance +- Designing content schemas +- Implementing Visual Editing and live preview +- Working with images, Portable Text, or page builders +- Configuring Sanity Studio structure +- Setting up TypeGen for type safety +- Implementing localization +- Migrating content from other systems +- Building custom apps with the Sanity App SDK +- Managing infrastructure with Blueprints +- Automating content workflows with Sanity Functions + +## Quick Reference + +### Integration Guides + +- `get-started` - Interactive onboarding for new Sanity projects +- `nextjs` - Next.js App Router, Live Content API, embedded Studio +- `nuxt` - Nuxt integration with @nuxtjs/sanity +- `angular` - Angular integration with @sanity/client, signals, resource API +- `astro` - Astro integration with @sanity/astro +- `remix` - React Router / Remix integration +- `svelte` - SvelteKit integration with @sanity/svelte-loader +- `hydrogen` - Shopify Hydrogen with Sanity +- `project-structure` - Monorepo and embedded Studio patterns +- `app-sdk` - Custom applications with Sanity App SDK +- `blueprints` - Infrastructure as Code with Sanity Blueprints +- `functions` - Automating content workflows with Sanity Functions + +### Topic Guides + +- `groq` - GROQ query patterns, type safety, performance optimization +- `schema` - Schema design, field definitions, validation, deprecation patterns +- `visual-editing` - Presentation Tool, Stega, overlays, live preview +- `page-builder` - Page Builder arrays, block components, live editing +- `portable-text` - Rich text rendering and custom components +- `image` - Image schema, URL builder, hotspots, LQIP, Next.js Image +- `studio-structure` - Desk structure, singletons, navigation +- `typegen` - TypeGen configuration, workflow, type utilities +- `seo` - Metadata, sitemaps, Open Graph, JSON-LD +- `localization` - i18n patterns, document vs field-level, locale management +- `migration` - Content import overview (see also `migration-html-import`) +- `migration-html-import` - HTML to Portable Text with @portabletext/block-tools + +## How to Use + +Start with the single framework or topic guide that best matches the request, then read additional references only when the task crosses concerns. Use these reference files for detailed explanations and code examples: + +``` +references/groq.md +references/schema.md +references/nextjs.md +``` + +Each reference file contains: +- Comprehensive topic or integration coverage +- Incorrect and correct code examples +- Decision matrices and workflow guidance +- Framework-specific patterns where applicable + diff --git a/.agents/skills/sanity-best-practices/references/angular.md b/.agents/skills/sanity-best-practices/references/angular.md new file mode 100644 index 00000000..359ccde4 --- /dev/null +++ b/.agents/skills/sanity-best-practices/references/angular.md @@ -0,0 +1,565 @@ +--- +title: Angular & Sanity Integration Rules +description: Integration guide for Angular, including @sanity/client setup, data fetching with signals and resource API, Portable Text rendering, and image optimization. +--- + +# Angular & Sanity Integration Rules + +Jump to the section that matches your Angular version or integration task instead of reading this guide straight through. + +## Table of Contents + +- Setup and configuration +- Client setup (service pattern) +- Data fetching patterns +- Routing +- Portable Text rendering +- Image optimization +- Modern Angular features +- SSR and prerendering +- Visual Editing +- Error handling + +## 1. Setup & Configuration + +Use the official template `sanity-template-angular-clean` as a starting point. It provides a monorepo structure: + +``` +project/ +├── angular-app/ # Angular 19+ frontend +└── studio/ # Sanity Studio +``` + +Install dependencies in the Angular app: + +```bash +npm install @sanity/client @sanity/image-url @portabletext/to-html +``` + +Configure environment files for Sanity credentials: + +```typescript +// environments/environment.ts +export const environment = { + production: false, + sanity: { + projectId: 'your-project-id', + dataset: 'production', + apiVersion: '2025-05-01', + }, +} +``` + +```typescript +// environments/environment.production.ts +export const environment = { + production: true, + sanity: { + projectId: 'your-project-id', + dataset: 'production', + apiVersion: '2025-05-01', + }, +} +``` + +> There is no Angular-specific Sanity SDK. Use `@sanity/client` directly, wrapped in an Angular service. + +### TypeGen in a Monorepo + +Sanity TypeGen generates TypeScript types from your schema and GROQ queries. In the Angular monorepo template, TypeGen runs from the Studio side but scans your Angular app's source files. Ensure `studio/sanity.cli.ts` points at the Angular app: + +```typescript +// studio/sanity.cli.ts +import { defineCliConfig } from 'sanity/cli' + +export default defineCliConfig({ + typegen: { + enabled: true, + path: '../angular-app/src/**/*.ts', + generates: '../angular-app/sanity.types.ts', + }, +}) +``` + +The remaining defaults (`overloadClientMethods: true`, `schema: "schema.json"`) work as-is. Include the generated types file in `angular-app/tsconfig.json` (usually covered by `"include": ["src/**/*.ts", "sanity.types.ts"]`). See `typegen.md` for the full TypeGen workflow, git strategy, and configuration options. + +## 2. Client Setup (Service Pattern) + +Create an injectable service wrapping `@sanity/client` and `@sanity/image-url`: + +```typescript +import { Injectable } from '@angular/core' +import { createClient, type ClientReturn, type QueryParams, type SanityClient } from '@sanity/client' +import imageUrlBuilder, { type ImageUrlBuilder } from '@sanity/image-url' +import type { SanityImageSource } from '@sanity/image-url/lib/types/types' +import { environment } from '../environments/environment' + +@Injectable({ providedIn: 'root' }) +export class SanityService { + private client: SanityClient + private builder: ImageUrlBuilder + + constructor() { + this.client = createClient({ + projectId: environment.sanity.projectId, + dataset: environment.sanity.dataset, + apiVersion: environment.sanity.apiVersion, + useCdn: true, + }) + this.builder = imageUrlBuilder(this.client) + } + + // ClientReturn resolves TypeGen's declaration-merged overloads for defineQuery strings + fetch(query: Query, params?: QueryParams): Promise> { + return this.client.fetch(query, params) + } + + getImageUrlBuilder(source: SanityImageSource) { + return this.builder.image(source) + } +} +``` + +For preview/draft content, create a second client instance with a token and `useCdn: false`. Never expose tokens in client-side bundles — use server-side rendering or a proxy endpoint for authenticated requests. + +## 3. Data Fetching Patterns + +### A. `resource` API (Angular 19+, Recommended) + +The `resource` API works natively with promises and integrates with Angular signals: + +```typescript +import { Component, input, resource, inject } from '@angular/core' +import { defineQuery } from 'groq' +import { SanityService } from '../sanity.service' + +const POST_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]{ + title, body, mainImage, publishedAt +}`) + +@Component({ + selector: 'app-post', + standalone: true, + template: ` + @if (post.value(); as p) { +

{{ p.title }}

+ + } @else if (post.isLoading()) { +

Loading…

+ } @else if (post.error()) { +

Error loading post

+ } + `, +}) +export default class PostComponent { + slug = input.required() + private sanity = inject(SanityService) + + post = resource({ + params: () => ({ slug: this.slug() }), + loader: ({ params }) => this.sanity.fetch(POST_QUERY, params), + }) +} +``` + +The `resource` automatically re-fetches when `slug` changes and exposes `value()`, `isLoading()`, and `error()` signals. + +> **TypeGen:** Wrapping queries in `defineQuery` enables Sanity TypeGen to infer return types automatically — no manual type imports needed. See `typegen.md` for the full workflow. + +### B. `rxResource` (Observable-based) + +For teams using RxJS patterns or needing operators like `retry` and `debounceTime`: + +```typescript +import { Component, input, inject } from '@angular/core' +import { rxResource } from '@angular/core/rxjs-interop' +import { defineQuery } from 'groq' +import { from } from 'rxjs' +import { SanityService } from '../sanity.service' + +const POST_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]`) + +@Component({ /* ... */ }) +export default class PostComponent { + slug = input.required() + private sanity = inject(SanityService) + + post = rxResource({ + params: () => ({ slug: this.slug() }), + loader: ({ params }) => from(this.sanity.fetch(POST_QUERY, params)), + }) +} +``` + +### C. `toSignal` (Angular 17–18) + +For apps not yet on Angular 19, convert observables to signals: + +```typescript +import { Component, inject } from '@angular/core' +import { toSignal } from '@angular/core/rxjs-interop' +import { defineQuery } from 'groq' +import { from } from 'rxjs' +import { SanityService } from '../sanity.service' + +const POSTS_QUERY = defineQuery(`*[_type == "post"] | order(publishedAt desc)`) + +@Component({ /* ... */ }) +export class HomeComponent { + private sanity = inject(SanityService) + posts = toSignal(from(this.sanity.fetch(POSTS_QUERY)), { initialValue: [] }) +} +``` + +> **Note:** `toSignal` does not re-fetch on parameter changes. For dynamic queries, use `resource` or `rxResource`. + +### Choosing a pattern + +| Pattern | Angular Version | Reactivity | Best For | +|---|---|---|---| +| `resource` | 19+ | Signal-based, auto re-fetch | New projects, dynamic queries | +| `rxResource` | 19+ | RxJS + signals | Teams using RxJS operators | +| `toSignal` | 17+ | One-shot conversion | Static queries, legacy apps | + +## 4. Routing + +Use lazy-loaded routes with `withComponentInputBinding()` so route params bind directly to component inputs: + +```typescript +// app.config.ts +import { provideRouter, withComponentInputBinding } from '@angular/router' +import { routes } from './app.routes' + +export const appConfig = { + providers: [ + provideRouter(routes, withComponentInputBinding()), + ], +} +``` + +```typescript +// app.routes.ts +import { Routes } from '@angular/router' + +export const routes: Routes = [ + { + path: '', + loadComponent: () => import('./home/home.component'), + pathMatch: 'full', + }, + { + path: 'post/:slug', + loadComponent: () => import('./post/post.component'), + }, +] +``` + +With `withComponentInputBinding()`, the `:slug` route param is automatically bound to `slug = input.required()` on the component — no need to inject `ActivatedRoute`. + +## 5. Portable Text Rendering + +### A. `@portabletext/to-html` with Angular Pipe (Recommended) + +```typescript +import { Pipe, PipeTransform, inject } from '@angular/core' +import { toHTML, type PortableTextComponents } from '@portabletext/to-html' +import type { PortableTextBlock } from '@portabletext/types' +import { SanityService } from '../sanity.service' + +@Pipe({ name: 'portableTextToHTML', standalone: true }) +export class PortableTextToHTMLPipe implements PipeTransform { + private sanity = inject(SanityService) + + private components: PortableTextComponents = { + types: { + image: ({ value }) => { + const url = this.sanity.getImageUrlBuilder(value).width(800).auto('format').url() + return `${value.alt || ''}` + }, + }, + marks: { + link: ({ children, value }) => + `${children}`, + }, + } + + transform(value: PortableTextBlock[] | undefined): string { + if (!value) return '' + return toHTML(value, { components: this.components }) + } +} +``` + +Usage in templates: + +```html +
+``` + +### B. `@limitless-angular/sanity` (Community, Component-based) + +For full Angular component control over each block type, the community library `@limitless-angular/sanity` provides a component-based Portable Text renderer. This is useful when you need Angular-specific interactivity within rich text blocks. + +See `portable-text.md` for Portable Text schema design and serialization rules. + +## 6. Image Optimization + +Create a pipe wrapping `@sanity/image-url`: + +```typescript +import { Pipe, PipeTransform, inject } from '@angular/core' +import type { SanityImageSource } from '@sanity/image-url/lib/types/types' +import { SanityService } from '../sanity.service' + +@Pipe({ name: 'sanityImage', standalone: true }) +export class SanityImagePipe implements PipeTransform { + private sanity = inject(SanityService) + + transform(value: SanityImageSource | undefined, width?: number): string | null { + if (!value) return null + const builder = this.sanity.getImageUrlBuilder(value) + if (width) return builder.width(width).auto('format').url() + return builder.auto('format').url() + } +} +``` + +Combine with Angular's `NgOptimizedImage` for LCP images: + +```html + + + + + +``` + +❌ **Bad:** Fetching full-size images without width constraints. + +```html + +``` + +✅ **Good:** Specifying width and using `auto('format')` for WebP/AVIF delivery. + +```html + +``` + +### LQIP with `NgOptimizedImage` + +Sanity provides a base64 LQIP (Low Quality Image Placeholder) per image asset — but you must query it explicitly: + +```groq +mainImage { + // @sanity/image-url needs these to build URLs with hotspot/crop support + asset, + hotspot, + crop, + alt, + // NgOptimizedImage needs these for placeholder and layout + "lqip": asset->metadata.lqip, + "width": asset->metadata.dimensions.width, + "height": asset->metadata.dimensions.height +} +``` + +Feed the LQIP directly into `NgOptimizedImage`'s `placeholder` attribute: + +```html + +``` + +Angular applies a CSS blur to the LQIP and crossfades to the full image on load. No extra libraries needed. + +> **Note:** LQIP strings are small (~200 bytes) so they're safe to inline in SSR HTML and `TransferState`. See `image.md` for the full image query patterns. + +See `image.md` for image field schema patterns and hotspot/crop configuration. + +## 7. Modern Angular Features + +When building with Sanity, leverage these Angular 19+ features: + +- **Standalone components** — Default in Angular 19. No `NgModule` boilerplate needed. +- **Signals and `resource`** — Preferred over RxJS for data fetching. Simpler, less boilerplate. +- **New control flow** — Use `@if`, `@for`, `@switch` with `@empty` for cleaner templates: + +```html +@for (post of posts.value(); track post._id) { + +} @empty { +

No posts found.

+} +``` + +- **`@defer` blocks** — Lazy-load below-fold content: + +```html +@defer (on viewport) { + +} @placeholder { +

Scroll to see comments…

+} +``` + +- **`inject()` function** — Preferred over constructor injection for cleaner code. +- **Zoneless change detection** — Experimental in Angular 19. Works well with signals-based data fetching since signals automatically notify the framework of changes. + +## 8. SSR & Prerendering + +Angular 17+ includes built-in SSR support (replacing Angular Universal): + +```typescript +// app.config.server.ts +import { provideServerRendering } from '@angular/platform-server' +import { provideClientHydration } from '@angular/platform-browser' + +export const serverConfig = { + providers: [ + provideServerRendering(), + provideClientHydration(), + ], +} +``` + +Key considerations for Sanity + Angular SSR: + +| Feature | Details | +|---|---| +| **Hydration** | `provideClientHydration()` preserves server-rendered DOM. The client reuses it instead of re-rendering. | +| **HTTP Transfer Cache** | Only works with Angular's `HttpClient`. Since `@sanity/client` uses its own HTTP transport, use `TransferState` manually (see below). | +| **Prerendering** | Use `getPrerenderParams` in route config to generate static pages at build time. | + +### Transfer State for `@sanity/client` + +Angular's built-in HTTP Transfer Cache does not cover `@sanity/client` requests. Without manual transfer, the client re-fetches every query during hydration. Add `TransferState` to the service from Section 2: + +```typescript ++ async function hashQuery(query: string, params?: QueryParams): Promise { ++ const input = query + JSON.stringify(params ?? {}) ++ const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input)) ++ return Array.from(new Uint8Array(buffer), b => b.toString(16).padStart(2, '0')).join('') ++ } + +import { Injectable, inject } from '@angular/core' ++ import { isPlatformBrowser, isPlatformServer } from '@angular/common' ++ import { PLATFORM_ID, makeStateKey, TransferState } from '@angular/core' +import { createClient, type ClientReturn, type QueryParams, type SanityClient } from '@sanity/client' + +export class SanityService { + private client: SanityClient ++ private transferState = inject(TransferState) ++ private platformId = inject(PLATFORM_ID) + + async fetch(query: Query, params?: QueryParams): Promise> { ++ const key = makeStateKey>(await hashQuery(query, params)) ++ ++ if (isPlatformBrowser(this.platformId)) { ++ const cached = this.transferState.get(key, null) ++ if (cached !== null) { ++ this.transferState.remove(key) ++ return cached ++ } ++ } ++ + const result = await this.client.fetch(query, params) ++ ++ if (isPlatformServer(this.platformId)) { ++ this.transferState.set(key, result) ++ } ++ + return result + } +} +``` + +The `hashQuery` helper keeps `TransferState` keys short (SHA-256 hex) instead of embedding raw GROQ strings in the serialized HTML. + +Prerendering dynamic routes: + +```typescript +// app.routes.server.ts +import { RenderMode, ServerRoute } from '@angular/ssr' + +export const serverRoutes: ServerRoute[] = [ + { + path: 'post/:slug', + renderMode: RenderMode.Prerender, + async getPrerenderParams() { + // Fetch all slugs from Sanity at build time + const client = createClient({ projectId: '...', dataset: '...', apiVersion: '...', useCdn: true }) + const slugs = await client.fetch(`*[_type == "post"].slug.current`) + return slugs.map((slug) => ({ slug })) + }, + }, + { path: '**', renderMode: RenderMode.Server }, +] +``` + +❌ **Bad:** Using `isPlatformBrowser()` in templates to conditionally render content — causes hydration mismatch. + +✅ **Good:** Using `@defer` or `afterNextRender()` for browser-only code. + +## 9. Visual Editing + +> **Important:** Angular does not have official Sanity Visual Editing support. There is no `@sanity/visual-editing` integration, no Stega encoding, and no click-to-edit overlay for Angular applications. This is unlike Next.js, Nuxt, and SvelteKit which have first-party support. + +### Preview Mode (Basic) + +For draft content preview, create a separate preview client with an API token: + +```typescript +@Injectable({ providedIn: 'root' }) +export class SanityService { + private client: SanityClient + private previewClient: SanityClient + + constructor() { + this.client = createClient({ + projectId: environment.sanity.projectId, + dataset: environment.sanity.dataset, + apiVersion: environment.sanity.apiVersion, + useCdn: true, + }) + + this.previewClient = this.client.withConfig({ + useCdn: false, + token: environment.sanity.previewToken, // Server-side only! + perspective: 'drafts', + }) + } + + fetch(query: Query, params?: QueryParams, preview = false): Promise> { + const client = preview ? this.previewClient : this.client + return client.fetch(query, params) + } +} +``` + +> **Security:** Never expose the preview token in client-side bundles. Use this pattern only with SSR where the token stays on the server, or proxy preview requests through a backend API. + +### Community Visual Editing + +The community library `@limitless-angular/sanity` provides experimental Visual Editing support for Angular, including overlay click-to-edit functionality. Check its documentation for current status and limitations. + +## 10. Error Handling + +Common errors when integrating Angular with Sanity: + +| Error | Cause | Solution | +|---|---|---| +| `401 Unauthorized` | Invalid or missing API token | Verify token in [Sanity Manage](https://www.sanity.io/manage). Ensure it has correct permissions. | +| `403 Forbidden` | CORS origin not allowed | Add your Angular dev/production URL to CORS origins in [Sanity Manage](https://www.sanity.io/manage). | +| `422 Invalid query` | GROQ syntax error | Test queries in Vision plugin or Sanity's GROQ playground. See `groq.md`. | +| Hydration mismatch | Conditional rendering based on platform | Use `@defer` or `afterNextRender()` instead of `isPlatformBrowser()` checks. | +| Empty response | Missing dataset or wrong `apiVersion` | Verify environment config. Use a date-based `apiVersion` (e.g., `'2025-05-01'`). | +| Images not loading | Missing `@sanity/image-url` setup | Ensure `getImageUrlBuilder` is called with a valid image reference. See `image.md`. | + +For GROQ query patterns and best practices, see `groq.md`. For schema design, see `schema.md`. diff --git a/.agents/skills/sanity-best-practices/references/app-sdk.md b/.agents/skills/sanity-best-practices/references/app-sdk.md new file mode 100644 index 00000000..99856aff --- /dev/null +++ b/.agents/skills/sanity-best-practices/references/app-sdk.md @@ -0,0 +1,462 @@ +--- +title: Sanity App SDK +description: Rules for building custom applications with the Sanity App SDK, including React hooks, document handles, real-time patterns, and Suspense best practices. +--- + +# Sanity App SDK + +Build custom React applications that interact with Sanity content in real-time. + +## Tech Stack + +- **Framework:** React 19+, TypeScript +- **Packages:** `@sanity/sdk`, `@sanity/sdk-react` +- **Optional UI:** `@sanity/ui`, `styled-components` +- **Runtime:** Node.js 20+ + +## Commands + +```bash +# Basic quickstart +npx sanity@latest init --template app-quickstart --organization --output-path . --typescript --skip-mcp + +# With Sanity UI components +npx sanity@latest init --template app-sanity-ui --organization --output-path . --typescript --skip-mcp + +# Start development server +npm run dev + +# Deploy to Sanity +npx sanity@latest deploy + +# Install Sanity UI +npm install @sanity/ui styled-components +``` + +## Project Structure + +``` +my-app/ +├── sanity.cli.ts # CLI config (org ID, entry point) +├── src/ +│ ├── App.tsx # Root component with SanityApp provider +│ ├── App.css # Global styles +│ └── components/ # Your components +├── package.json +└── tsconfig.json +``` + +## Boundaries + +- **Always:** Wrap data-fetching components in ``, use `documentId` as React `key`, read/write directly to Content Lake (not local state) +- **Always:** Use `useDocuments` for lists, `useDocumentProjection` for display, `useDocument` + `useEditDocument` for editing +- **Ask first:** Before using `useQuery` with raw GROQ (prefer `useDocuments` + `useDocumentProjection`) +- **Ask first:** Before adding multiple data-fetching hooks in a single component +- **Never:** Use `useState` for form values that should sync with Content Lake +- **Never:** Use array index as React `key` for document lists (breaks real-time updates) +- **Never:** Forget the `fallback` prop on `` and `` boundaries + +--- + +## Configuration + +### CLI Config (`sanity.cli.ts`) + +```typescript +import { defineCliConfig } from 'sanity/cli' + +export default defineCliConfig({ + app: { + organizationId: 'your-org-id', + entry: './src/App.tsx', + }, +}) +``` + +### App Root (`src/App.tsx`) + +```typescript +import { SanityApp, type SanityConfig } from '@sanity/sdk-react' + +export default function App() { + const config: SanityConfig[] = [ + { + projectId: 'your-project-id', + dataset: 'production', + }, + ] + + return ( + Loading...}> + + + ) +} +``` + +### With Sanity UI + +```typescript +import { SanityApp, type SanityConfig } from '@sanity/sdk-react' +import { ThemeProvider } from '@sanity/ui' +import { buildTheme } from '@sanity/ui/theme' + +const theme = buildTheme() + +export default function App() { + const config: SanityConfig[] = [ + { projectId: 'your-project-id', dataset: 'production' }, + ] + + return ( + + Loading...}> + + + + ) +} +``` + +### Environment Variables + +Prefix with `SANITY_APP_` for automatic bundling: + +```bash +SANITY_APP_PROJECT_ID=abc123 +SANITY_APP_DATASET=production +``` + +Access: `process.env.SANITY_APP_PROJECT_ID` + +--- + +## Document Handles + +Lightweight references to documents. Fetch handles first, then load content as needed. + +```typescript +interface DocumentHandle { + documentId: string + documentType: string + projectId?: string + dataset?: string +} +``` + +### Creating Handles + +```typescript +// Best: From useDocuments hook +const { data: handles } = useDocuments({ documentType: 'article' }) + +// Good: With helper (preserves literal types for TypeGen) +import { createDocumentHandle } from '@sanity/sdk' +const handle = createDocumentHandle({ + documentId: 'my-doc-id', + documentType: 'article', +}) + +// Good: With as const (preserves literal types) +const handle = { + documentId: 'my-doc-id', + documentType: 'article', +} as const +``` + +--- + +## Hook Selection + +| Hook | Use Case | Returns | +|------|----------|---------| +| `useDocuments` | List of documents (infinite scroll) | Document handles | +| `usePaginatedDocuments` | Paginated lists with page controls | Document handles | +| `useDocument` | Single document, real-time editing | Full document or field | +| `useDocumentProjection` | Specific fields, display only | Projected data | +| `useQuery` | Complex GROQ queries (use sparingly) | Raw query results | + +--- + +## Code Patterns + +### Fetching a Document List + +```typescript +// Good: Fetch handles, render items with Suspense +import { Suspense } from 'react' +import { useDocuments } from '@sanity/sdk-react' + +function ArticleList() { + const { data, hasMore, loadMore, isPending } = useDocuments({ + documentType: 'article', + batchSize: 10, + orderings: [{ field: '_updatedAt', direction: 'desc' }], + }) + + return ( + <> +
    + {data.map((handle) => ( + Loading...}> + + + ))} +
+ {hasMore && ( + + )} + + ) +} +``` + +```typescript +// Bad: Over-fetching with raw GROQ, no pagination +function BadArticleList() { + const { data } = useQuery(`*[_type == "article"]`) + return data?.map((doc, i) =>
  • {doc.title}
  • ) +} +``` + +### Projecting Content from a Handle + +```typescript +// Good: Project only needed fields +import { useDocumentProjection, type DocumentHandle } from '@sanity/sdk-react' + +function ArticleItem(handle: DocumentHandle) { + const { data } = useDocumentProjection({ + ...handle, + projection: `{ + title, + "authorName": author->name, + "imageUrl": image.asset->url + }`, + }) + + if (!data) return null + + return ( +
  • +

    {data.title}

    +

    By {data.authorName}

    +
  • + ) +} +``` + +### Real-time Editing + +```typescript +// Good: Read and write directly to Content Lake +import { useDocument, useEditDocument, type DocumentHandle } from '@sanity/sdk-react' + +function TitleInput(handle: DocumentHandle) { + const { data: title } = useDocument({ ...handle, path: 'title' }) + const editTitle = useEditDocument({ ...handle, path: 'title' }) + + return ( + editTitle(e.currentTarget.value)} + /> + ) +} +``` + +```typescript +// Bad: Local state with submit button - causes stale data +function BadTitleForm(handle: DocumentHandle) { + const [value, setValue] = useState('') + const editTitle = useEditDocument({ ...handle, path: 'title' }) + + function handleSubmit(e: FormEvent) { + e.preventDefault() + editTitle(value) // Only writes on submit! + } + + return ( +
    + setValue(e.target.value)} /> + +
    + ) +} +``` + +### Document Actions + +```typescript +import { + useApplyDocumentActions, + publishDocument, + unpublishDocument, + deleteDocument, +} from '@sanity/sdk-react' + +function DocumentActions({ handle }: { handle: DocumentHandle }) { + const apply = useApplyDocumentActions() + + return ( +
    + + + +
    + ) +} +``` + +--- + +## Suspense Patterns + +The App SDK uses React Suspense. Every data-fetching component must be wrapped. + +### One Hook Per Component + +```typescript +// Good: Separate fetchers into separate components +function EventsAndVenues() { + return ( + <> + + + + + + + + ) +} + +function EventsList() { + const { data } = useDocuments({ documentType: 'event' }) + return +} + +function VenuesList() { + const { data } = useDocuments({ documentType: 'venue' }) + return +} +``` + +```typescript +// Bad: Multiple fetchers in one component +function BadComponent() { + const { data: events } = useDocuments({ documentType: 'event' }) + const { data: venues } = useDocuments({ documentType: 'venue' }) + // Both trigger Suspense together, causing unnecessary re-renders +} +``` + +### Prevent Layout Shift + +```typescript +// Good: Fallback matches final component dimensions +const BUTTON_TEXT = 'Open in Studio' + +export function OpenInStudio({ handle }: { handle: DocumentHandle }) { + return ( + }> + + + ) +} + +function OpenInStudioButton({ handle }: { handle: DocumentHandle }) { + const { navigateToStudioDocument } = useNavigateToStudioDocument(handle) + return + + + + +
    +
    + +
    +
    Prompt
    +
    +
    +
    +
    + + +
    +
    Output
    +
    +
    No output files found
    +
    +
    + + + + + + + + +
    +
    Your Feedback
    +
    + + + +
    +
    +
    + + +
    + + +
    +
    +
    No benchmark data available. Run a benchmark to see quantitative results here.
    +
    +
    + + + +
    +
    +

    Review Complete

    +

    Your feedback has been saved. Go back to your Claude Code session and tell Claude you're done reviewing.

    +
    + +
    +
    +
    + + +
    + + + + diff --git a/.agents/skills/skill-creator/references/schemas.md b/.agents/skills/skill-creator/references/schemas.md new file mode 100644 index 00000000..b6eeaa2d --- /dev/null +++ b/.agents/skills/skill-creator/references/schemas.md @@ -0,0 +1,430 @@ +# JSON Schemas + +This document defines the JSON schemas used by skill-creator. + +--- + +## evals.json + +Defines the evals for a skill. Located at `evals/evals.json` within the skill directory. + +```json +{ + "skill_name": "example-skill", + "evals": [ + { + "id": 1, + "prompt": "User's example prompt", + "expected_output": "Description of expected result", + "files": ["evals/files/sample1.pdf"], + "expectations": [ + "The output includes X", + "The skill used script Y" + ] + } + ] +} +``` + +**Fields:** +- `skill_name`: Name matching the skill's frontmatter +- `evals[].id`: Unique integer identifier +- `evals[].prompt`: The task to execute +- `evals[].expected_output`: Human-readable description of success +- `evals[].files`: Optional list of input file paths (relative to skill root) +- `evals[].expectations`: List of verifiable statements + +--- + +## history.json + +Tracks version progression in Improve mode. Located at workspace root. + +```json +{ + "started_at": "2026-01-15T10:30:00Z", + "skill_name": "pdf", + "current_best": "v2", + "iterations": [ + { + "version": "v0", + "parent": null, + "expectation_pass_rate": 0.65, + "grading_result": "baseline", + "is_current_best": false + }, + { + "version": "v1", + "parent": "v0", + "expectation_pass_rate": 0.75, + "grading_result": "won", + "is_current_best": false + }, + { + "version": "v2", + "parent": "v1", + "expectation_pass_rate": 0.85, + "grading_result": "won", + "is_current_best": true + } + ] +} +``` + +**Fields:** +- `started_at`: ISO timestamp of when improvement started +- `skill_name`: Name of the skill being improved +- `current_best`: Version identifier of the best performer +- `iterations[].version`: Version identifier (v0, v1, ...) +- `iterations[].parent`: Parent version this was derived from +- `iterations[].expectation_pass_rate`: Pass rate from grading +- `iterations[].grading_result`: "baseline", "won", "lost", or "tie" +- `iterations[].is_current_best`: Whether this is the current best version + +--- + +## grading.json + +Output from the grader agent. Located at `/grading.json`. + +```json +{ + "expectations": [ + { + "text": "The output includes the name 'John Smith'", + "passed": true, + "evidence": "Found in transcript Step 3: 'Extracted names: John Smith, Sarah Johnson'" + }, + { + "text": "The spreadsheet has a SUM formula in cell B10", + "passed": false, + "evidence": "No spreadsheet was created. The output was a text file." + } + ], + "summary": { + "passed": 2, + "failed": 1, + "total": 3, + "pass_rate": 0.67 + }, + "execution_metrics": { + "tool_calls": { + "Read": 5, + "Write": 2, + "Bash": 8 + }, + "total_tool_calls": 15, + "total_steps": 6, + "errors_encountered": 0, + "output_chars": 12450, + "transcript_chars": 3200 + }, + "timing": { + "executor_duration_seconds": 165.0, + "grader_duration_seconds": 26.0, + "total_duration_seconds": 191.0 + }, + "claims": [ + { + "claim": "The form has 12 fillable fields", + "type": "factual", + "verified": true, + "evidence": "Counted 12 fields in field_info.json" + } + ], + "user_notes_summary": { + "uncertainties": ["Used 2023 data, may be stale"], + "needs_review": [], + "workarounds": ["Fell back to text overlay for non-fillable fields"] + }, + "eval_feedback": { + "suggestions": [ + { + "assertion": "The output includes the name 'John Smith'", + "reason": "A hallucinated document that mentions the name would also pass" + } + ], + "overall": "Assertions check presence but not correctness." + } +} +``` + +**Fields:** +- `expectations[]`: Graded expectations with evidence +- `summary`: Aggregate pass/fail counts +- `execution_metrics`: Tool usage and output size (from executor's metrics.json) +- `timing`: Wall clock timing (from timing.json) +- `claims`: Extracted and verified claims from the output +- `user_notes_summary`: Issues flagged by the executor +- `eval_feedback`: (optional) Improvement suggestions for the evals, only present when the grader identifies issues worth raising + +--- + +## metrics.json + +Output from the executor agent. Located at `/outputs/metrics.json`. + +```json +{ + "tool_calls": { + "Read": 5, + "Write": 2, + "Bash": 8, + "Edit": 1, + "Glob": 2, + "Grep": 0 + }, + "total_tool_calls": 18, + "total_steps": 6, + "files_created": ["filled_form.pdf", "field_values.json"], + "errors_encountered": 0, + "output_chars": 12450, + "transcript_chars": 3200 +} +``` + +**Fields:** +- `tool_calls`: Count per tool type +- `total_tool_calls`: Sum of all tool calls +- `total_steps`: Number of major execution steps +- `files_created`: List of output files created +- `errors_encountered`: Number of errors during execution +- `output_chars`: Total character count of output files +- `transcript_chars`: Character count of transcript + +--- + +## timing.json + +Wall clock timing for a run. Located at `/timing.json`. + +**How to capture:** When a subagent task completes, the task notification includes `total_tokens` and `duration_ms`. Save these immediately — they are not persisted anywhere else and cannot be recovered after the fact. + +```json +{ + "total_tokens": 84852, + "duration_ms": 23332, + "total_duration_seconds": 23.3, + "executor_start": "2026-01-15T10:30:00Z", + "executor_end": "2026-01-15T10:32:45Z", + "executor_duration_seconds": 165.0, + "grader_start": "2026-01-15T10:32:46Z", + "grader_end": "2026-01-15T10:33:12Z", + "grader_duration_seconds": 26.0 +} +``` + +--- + +## benchmark.json + +Output from Benchmark mode. Located at `benchmarks//benchmark.json`. + +```json +{ + "metadata": { + "skill_name": "pdf", + "skill_path": "/path/to/pdf", + "executor_model": "claude-sonnet-4-20250514", + "analyzer_model": "most-capable-model", + "timestamp": "2026-01-15T10:30:00Z", + "evals_run": [1, 2, 3], + "runs_per_configuration": 3 + }, + + "runs": [ + { + "eval_id": 1, + "eval_name": "Ocean", + "configuration": "with_skill", + "run_number": 1, + "result": { + "pass_rate": 0.85, + "passed": 6, + "failed": 1, + "total": 7, + "time_seconds": 42.5, + "tokens": 3800, + "tool_calls": 18, + "errors": 0 + }, + "expectations": [ + {"text": "...", "passed": true, "evidence": "..."} + ], + "notes": [ + "Used 2023 data, may be stale", + "Fell back to text overlay for non-fillable fields" + ] + } + ], + + "run_summary": { + "with_skill": { + "pass_rate": {"mean": 0.85, "stddev": 0.05, "min": 0.80, "max": 0.90}, + "time_seconds": {"mean": 45.0, "stddev": 12.0, "min": 32.0, "max": 58.0}, + "tokens": {"mean": 3800, "stddev": 400, "min": 3200, "max": 4100} + }, + "without_skill": { + "pass_rate": {"mean": 0.35, "stddev": 0.08, "min": 0.28, "max": 0.45}, + "time_seconds": {"mean": 32.0, "stddev": 8.0, "min": 24.0, "max": 42.0}, + "tokens": {"mean": 2100, "stddev": 300, "min": 1800, "max": 2500} + }, + "delta": { + "pass_rate": "+0.50", + "time_seconds": "+13.0", + "tokens": "+1700" + } + }, + + "notes": [ + "Assertion 'Output is a PDF file' passes 100% in both configurations - may not differentiate skill value", + "Eval 3 shows high variance (50% ± 40%) - may be flaky or model-dependent", + "Without-skill runs consistently fail on table extraction expectations", + "Skill adds 13s average execution time but improves pass rate by 50%" + ] +} +``` + +**Fields:** +- `metadata`: Information about the benchmark run + - `skill_name`: Name of the skill + - `timestamp`: When the benchmark was run + - `evals_run`: List of eval names or IDs + - `runs_per_configuration`: Number of runs per config (e.g. 3) +- `runs[]`: Individual run results + - `eval_id`: Numeric eval identifier + - `eval_name`: Human-readable eval name (used as section header in the viewer) + - `configuration`: Must be `"with_skill"` or `"without_skill"` (the viewer uses this exact string for grouping and color coding) + - `run_number`: Integer run number (1, 2, 3...) + - `result`: Nested object with `pass_rate`, `passed`, `total`, `time_seconds`, `tokens`, `errors` +- `run_summary`: Statistical aggregates per configuration + - `with_skill` / `without_skill`: Each contains `pass_rate`, `time_seconds`, `tokens` objects with `mean` and `stddev` fields + - `delta`: Difference strings like `"+0.50"`, `"+13.0"`, `"+1700"` +- `notes`: Freeform observations from the analyzer + +**Important:** The viewer reads these field names exactly. Using `config` instead of `configuration`, or putting `pass_rate` at the top level of a run instead of nested under `result`, will cause the viewer to show empty/zero values. Always reference this schema when generating benchmark.json manually. + +--- + +## comparison.json + +Output from blind comparator. Located at `/comparison-N.json`. + +```json +{ + "winner": "A", + "reasoning": "Output A provides a complete solution with proper formatting and all required fields. Output B is missing the date field and has formatting inconsistencies.", + "rubric": { + "A": { + "content": { + "correctness": 5, + "completeness": 5, + "accuracy": 4 + }, + "structure": { + "organization": 4, + "formatting": 5, + "usability": 4 + }, + "content_score": 4.7, + "structure_score": 4.3, + "overall_score": 9.0 + }, + "B": { + "content": { + "correctness": 3, + "completeness": 2, + "accuracy": 3 + }, + "structure": { + "organization": 3, + "formatting": 2, + "usability": 3 + }, + "content_score": 2.7, + "structure_score": 2.7, + "overall_score": 5.4 + } + }, + "output_quality": { + "A": { + "score": 9, + "strengths": ["Complete solution", "Well-formatted", "All fields present"], + "weaknesses": ["Minor style inconsistency in header"] + }, + "B": { + "score": 5, + "strengths": ["Readable output", "Correct basic structure"], + "weaknesses": ["Missing date field", "Formatting inconsistencies", "Partial data extraction"] + } + }, + "expectation_results": { + "A": { + "passed": 4, + "total": 5, + "pass_rate": 0.80, + "details": [ + {"text": "Output includes name", "passed": true} + ] + }, + "B": { + "passed": 3, + "total": 5, + "pass_rate": 0.60, + "details": [ + {"text": "Output includes name", "passed": true} + ] + } + } +} +``` + +--- + +## analysis.json + +Output from post-hoc analyzer. Located at `/analysis.json`. + +```json +{ + "comparison_summary": { + "winner": "A", + "winner_skill": "path/to/winner/skill", + "loser_skill": "path/to/loser/skill", + "comparator_reasoning": "Brief summary of why comparator chose winner" + }, + "winner_strengths": [ + "Clear step-by-step instructions for handling multi-page documents", + "Included validation script that caught formatting errors" + ], + "loser_weaknesses": [ + "Vague instruction 'process the document appropriately' led to inconsistent behavior", + "No script for validation, agent had to improvise" + ], + "instruction_following": { + "winner": { + "score": 9, + "issues": ["Minor: skipped optional logging step"] + }, + "loser": { + "score": 6, + "issues": [ + "Did not use the skill's formatting template", + "Invented own approach instead of following step 3" + ] + } + }, + "improvement_suggestions": [ + { + "priority": "high", + "category": "instructions", + "suggestion": "Replace 'process the document appropriately' with explicit steps", + "expected_impact": "Would eliminate ambiguity that caused inconsistent behavior" + } + ], + "transcript_insights": { + "winner_execution_pattern": "Read skill -> Followed 5-step process -> Used validation script", + "loser_execution_pattern": "Read skill -> Unclear on approach -> Tried 3 different methods" + } +} +``` diff --git a/.agents/skills/skill-creator/scripts/__init__.py b/.agents/skills/skill-creator/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/.agents/skills/skill-creator/scripts/aggregate_benchmark.py b/.agents/skills/skill-creator/scripts/aggregate_benchmark.py new file mode 100755 index 00000000..3e66e8c1 --- /dev/null +++ b/.agents/skills/skill-creator/scripts/aggregate_benchmark.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python3 +""" +Aggregate individual run results into benchmark summary statistics. + +Reads grading.json files from run directories and produces: +- run_summary with mean, stddev, min, max for each metric +- delta between with_skill and without_skill configurations + +Usage: + python aggregate_benchmark.py + +Example: + python aggregate_benchmark.py benchmarks/2026-01-15T10-30-00/ + +The script supports two directory layouts: + + Workspace layout (from skill-creator iterations): + / + └── eval-N/ + ├── with_skill/ + │ ├── run-1/grading.json + │ └── run-2/grading.json + └── without_skill/ + ├── run-1/grading.json + └── run-2/grading.json + + Legacy layout (with runs/ subdirectory): + / + └── runs/ + └── eval-N/ + ├── with_skill/ + │ └── run-1/grading.json + └── without_skill/ + └── run-1/grading.json +""" + +import argparse +import json +import math +import sys +from datetime import datetime, timezone +from pathlib import Path + + +def calculate_stats(values: list[float]) -> dict: + """Calculate mean, stddev, min, max for a list of values.""" + if not values: + return {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0} + + n = len(values) + mean = sum(values) / n + + if n > 1: + variance = sum((x - mean) ** 2 for x in values) / (n - 1) + stddev = math.sqrt(variance) + else: + stddev = 0.0 + + return { + "mean": round(mean, 4), + "stddev": round(stddev, 4), + "min": round(min(values), 4), + "max": round(max(values), 4) + } + + +def load_run_results(benchmark_dir: Path) -> dict: + """ + Load all run results from a benchmark directory. + + Returns dict keyed by config name (e.g. "with_skill"/"without_skill", + or "new_skill"/"old_skill"), each containing a list of run results. + """ + # Support both layouts: eval dirs directly under benchmark_dir, or under runs/ + runs_dir = benchmark_dir / "runs" + if runs_dir.exists(): + search_dir = runs_dir + elif list(benchmark_dir.glob("eval-*")): + search_dir = benchmark_dir + else: + print(f"No eval directories found in {benchmark_dir} or {benchmark_dir / 'runs'}") + return {} + + results: dict[str, list] = {} + + for eval_idx, eval_dir in enumerate(sorted(search_dir.glob("eval-*"))): + metadata_path = eval_dir / "eval_metadata.json" + if metadata_path.exists(): + try: + with open(metadata_path) as mf: + eval_id = json.load(mf).get("eval_id", eval_idx) + except (json.JSONDecodeError, OSError): + eval_id = eval_idx + else: + try: + eval_id = int(eval_dir.name.split("-")[1]) + except ValueError: + eval_id = eval_idx + + # Discover config directories dynamically rather than hardcoding names + for config_dir in sorted(eval_dir.iterdir()): + if not config_dir.is_dir(): + continue + # Skip non-config directories (inputs, outputs, etc.) + if not list(config_dir.glob("run-*")): + continue + config = config_dir.name + if config not in results: + results[config] = [] + + for run_dir in sorted(config_dir.glob("run-*")): + run_number = int(run_dir.name.split("-")[1]) + grading_file = run_dir / "grading.json" + + if not grading_file.exists(): + print(f"Warning: grading.json not found in {run_dir}") + continue + + try: + with open(grading_file) as f: + grading = json.load(f) + except json.JSONDecodeError as e: + print(f"Warning: Invalid JSON in {grading_file}: {e}") + continue + + # Extract metrics + result = { + "eval_id": eval_id, + "run_number": run_number, + "pass_rate": grading.get("summary", {}).get("pass_rate", 0.0), + "passed": grading.get("summary", {}).get("passed", 0), + "failed": grading.get("summary", {}).get("failed", 0), + "total": grading.get("summary", {}).get("total", 0), + } + + # Extract timing — check grading.json first, then sibling timing.json + timing = grading.get("timing", {}) + result["time_seconds"] = timing.get("total_duration_seconds", 0.0) + timing_file = run_dir / "timing.json" + if result["time_seconds"] == 0.0 and timing_file.exists(): + try: + with open(timing_file) as tf: + timing_data = json.load(tf) + result["time_seconds"] = timing_data.get("total_duration_seconds", 0.0) + result["tokens"] = timing_data.get("total_tokens", 0) + except json.JSONDecodeError: + pass + + # Extract metrics if available + metrics = grading.get("execution_metrics", {}) + result["tool_calls"] = metrics.get("total_tool_calls", 0) + if not result.get("tokens"): + result["tokens"] = metrics.get("output_chars", 0) + result["errors"] = metrics.get("errors_encountered", 0) + + # Extract expectations — viewer requires fields: text, passed, evidence + raw_expectations = grading.get("expectations", []) + for exp in raw_expectations: + if "text" not in exp or "passed" not in exp: + print(f"Warning: expectation in {grading_file} missing required fields (text, passed, evidence): {exp}") + result["expectations"] = raw_expectations + + # Extract notes from user_notes_summary + notes_summary = grading.get("user_notes_summary", {}) + notes = [] + notes.extend(notes_summary.get("uncertainties", [])) + notes.extend(notes_summary.get("needs_review", [])) + notes.extend(notes_summary.get("workarounds", [])) + result["notes"] = notes + + results[config].append(result) + + return results + + +def aggregate_results(results: dict) -> dict: + """ + Aggregate run results into summary statistics. + + Returns run_summary with stats for each configuration and delta. + """ + run_summary = {} + configs = list(results.keys()) + + for config in configs: + runs = results.get(config, []) + + if not runs: + run_summary[config] = { + "pass_rate": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0}, + "time_seconds": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0}, + "tokens": {"mean": 0, "stddev": 0, "min": 0, "max": 0} + } + continue + + pass_rates = [r["pass_rate"] for r in runs] + times = [r["time_seconds"] for r in runs] + tokens = [r.get("tokens", 0) for r in runs] + + run_summary[config] = { + "pass_rate": calculate_stats(pass_rates), + "time_seconds": calculate_stats(times), + "tokens": calculate_stats(tokens) + } + + # Calculate delta between the first two configs (if two exist) + if len(configs) >= 2: + primary = run_summary.get(configs[0], {}) + baseline = run_summary.get(configs[1], {}) + else: + primary = run_summary.get(configs[0], {}) if configs else {} + baseline = {} + + delta_pass_rate = primary.get("pass_rate", {}).get("mean", 0) - baseline.get("pass_rate", {}).get("mean", 0) + delta_time = primary.get("time_seconds", {}).get("mean", 0) - baseline.get("time_seconds", {}).get("mean", 0) + delta_tokens = primary.get("tokens", {}).get("mean", 0) - baseline.get("tokens", {}).get("mean", 0) + + run_summary["delta"] = { + "pass_rate": f"{delta_pass_rate:+.2f}", + "time_seconds": f"{delta_time:+.1f}", + "tokens": f"{delta_tokens:+.0f}" + } + + return run_summary + + +def generate_benchmark(benchmark_dir: Path, skill_name: str = "", skill_path: str = "") -> dict: + """ + Generate complete benchmark.json from run results. + """ + results = load_run_results(benchmark_dir) + run_summary = aggregate_results(results) + + # Build runs array for benchmark.json + runs = [] + for config in results: + for result in results[config]: + runs.append({ + "eval_id": result["eval_id"], + "configuration": config, + "run_number": result["run_number"], + "result": { + "pass_rate": result["pass_rate"], + "passed": result["passed"], + "failed": result["failed"], + "total": result["total"], + "time_seconds": result["time_seconds"], + "tokens": result.get("tokens", 0), + "tool_calls": result.get("tool_calls", 0), + "errors": result.get("errors", 0) + }, + "expectations": result["expectations"], + "notes": result["notes"] + }) + + # Determine eval IDs from results + eval_ids = sorted(set( + r["eval_id"] + for config in results.values() + for r in config + )) + + benchmark = { + "metadata": { + "skill_name": skill_name or "", + "skill_path": skill_path or "", + "executor_model": "", + "analyzer_model": "", + "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "evals_run": eval_ids, + "runs_per_configuration": 3 + }, + "runs": runs, + "run_summary": run_summary, + "notes": [] # To be filled by analyzer + } + + return benchmark + + +def generate_markdown(benchmark: dict) -> str: + """Generate human-readable benchmark.md from benchmark data.""" + metadata = benchmark["metadata"] + run_summary = benchmark["run_summary"] + + # Determine config names (excluding "delta") + configs = [k for k in run_summary if k != "delta"] + config_a = configs[0] if len(configs) >= 1 else "config_a" + config_b = configs[1] if len(configs) >= 2 else "config_b" + label_a = config_a.replace("_", " ").title() + label_b = config_b.replace("_", " ").title() + + lines = [ + f"# Skill Benchmark: {metadata['skill_name']}", + "", + f"**Model**: {metadata['executor_model']}", + f"**Date**: {metadata['timestamp']}", + f"**Evals**: {', '.join(map(str, metadata['evals_run']))} ({metadata['runs_per_configuration']} runs each per configuration)", + "", + "## Summary", + "", + f"| Metric | {label_a} | {label_b} | Delta |", + "|--------|------------|---------------|-------|", + ] + + a_summary = run_summary.get(config_a, {}) + b_summary = run_summary.get(config_b, {}) + delta = run_summary.get("delta", {}) + + # Format pass rate + a_pr = a_summary.get("pass_rate", {}) + b_pr = b_summary.get("pass_rate", {}) + lines.append(f"| Pass Rate | {a_pr.get('mean', 0)*100:.0f}% ± {a_pr.get('stddev', 0)*100:.0f}% | {b_pr.get('mean', 0)*100:.0f}% ± {b_pr.get('stddev', 0)*100:.0f}% | {delta.get('pass_rate', '—')} |") + + # Format time + a_time = a_summary.get("time_seconds", {}) + b_time = b_summary.get("time_seconds", {}) + lines.append(f"| Time | {a_time.get('mean', 0):.1f}s ± {a_time.get('stddev', 0):.1f}s | {b_time.get('mean', 0):.1f}s ± {b_time.get('stddev', 0):.1f}s | {delta.get('time_seconds', '—')}s |") + + # Format tokens + a_tokens = a_summary.get("tokens", {}) + b_tokens = b_summary.get("tokens", {}) + lines.append(f"| Tokens | {a_tokens.get('mean', 0):.0f} ± {a_tokens.get('stddev', 0):.0f} | {b_tokens.get('mean', 0):.0f} ± {b_tokens.get('stddev', 0):.0f} | {delta.get('tokens', '—')} |") + + # Notes section + if benchmark.get("notes"): + lines.extend([ + "", + "## Notes", + "" + ]) + for note in benchmark["notes"]: + lines.append(f"- {note}") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser( + description="Aggregate benchmark run results into summary statistics" + ) + parser.add_argument( + "benchmark_dir", + type=Path, + help="Path to the benchmark directory" + ) + parser.add_argument( + "--skill-name", + default="", + help="Name of the skill being benchmarked" + ) + parser.add_argument( + "--skill-path", + default="", + help="Path to the skill being benchmarked" + ) + parser.add_argument( + "--output", "-o", + type=Path, + help="Output path for benchmark.json (default: /benchmark.json)" + ) + + args = parser.parse_args() + + if not args.benchmark_dir.exists(): + print(f"Directory not found: {args.benchmark_dir}") + sys.exit(1) + + # Generate benchmark + benchmark = generate_benchmark(args.benchmark_dir, args.skill_name, args.skill_path) + + # Determine output paths + output_json = args.output or (args.benchmark_dir / "benchmark.json") + output_md = output_json.with_suffix(".md") + + # Write benchmark.json + with open(output_json, "w") as f: + json.dump(benchmark, f, indent=2) + print(f"Generated: {output_json}") + + # Write benchmark.md + markdown = generate_markdown(benchmark) + with open(output_md, "w") as f: + f.write(markdown) + print(f"Generated: {output_md}") + + # Print summary + run_summary = benchmark["run_summary"] + configs = [k for k in run_summary if k != "delta"] + delta = run_summary.get("delta", {}) + + print(f"\nSummary:") + for config in configs: + pr = run_summary[config]["pass_rate"]["mean"] + label = config.replace("_", " ").title() + print(f" {label}: {pr*100:.1f}% pass rate") + print(f" Delta: {delta.get('pass_rate', '—')}") + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/skill-creator/scripts/generate_report.py b/.agents/skills/skill-creator/scripts/generate_report.py new file mode 100755 index 00000000..959e30a0 --- /dev/null +++ b/.agents/skills/skill-creator/scripts/generate_report.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +"""Generate an HTML report from run_loop.py output. + +Takes the JSON output from run_loop.py and generates a visual HTML report +showing each description attempt with check/x for each test case. +Distinguishes between train and test queries. +""" + +import argparse +import html +import json +import sys +from pathlib import Path + + +def generate_html(data: dict, auto_refresh: bool = False, skill_name: str = "") -> str: + """Generate HTML report from loop output data. If auto_refresh is True, adds a meta refresh tag.""" + history = data.get("history", []) + holdout = data.get("holdout", 0) + title_prefix = html.escape(skill_name + " \u2014 ") if skill_name else "" + + # Get all unique queries from train and test sets, with should_trigger info + train_queries: list[dict] = [] + test_queries: list[dict] = [] + if history: + for r in history[0].get("train_results", history[0].get("results", [])): + train_queries.append({"query": r["query"], "should_trigger": r.get("should_trigger", True)}) + if history[0].get("test_results"): + for r in history[0].get("test_results", []): + test_queries.append({"query": r["query"], "should_trigger": r.get("should_trigger", True)}) + + refresh_tag = ' \n' if auto_refresh else "" + + html_parts = [""" + + + +""" + refresh_tag + """ """ + title_prefix + """Skill Description Optimization + + + + + + +

    """ + title_prefix + """Skill Description Optimization

    +
    + Optimizing your skill's description. This page updates automatically as Claude tests different versions of your skill's description. Each row is an iteration — a new description attempt. The columns show test queries: green checkmarks mean the skill triggered correctly (or correctly didn't trigger), red crosses mean it got it wrong. The "Train" score shows performance on queries used to improve the description; the "Test" score shows performance on held-out queries the optimizer hasn't seen. When it's done, Claude will apply the best-performing description to your skill. +
    +"""] + + # Summary section + best_test_score = data.get('best_test_score') + best_train_score = data.get('best_train_score') + html_parts.append(f""" +
    +

    Original: {html.escape(data.get('original_description', 'N/A'))}

    +

    Best: {html.escape(data.get('best_description', 'N/A'))}

    +

    Best Score: {data.get('best_score', 'N/A')} {'(test)' if best_test_score else '(train)'}

    +

    Iterations: {data.get('iterations_run', 0)} | Train: {data.get('train_size', '?')} | Test: {data.get('test_size', '?')}

    +
    +""") + + # Legend + html_parts.append(""" +
    + Query columns: + Should trigger + Should NOT trigger + Train + Test +
    +""") + + # Table header + html_parts.append(""" +
    + + + + + + + +""") + + # Add column headers for train queries + for qinfo in train_queries: + polarity = "positive-col" if qinfo["should_trigger"] else "negative-col" + html_parts.append(f' \n') + + # Add column headers for test queries (different color) + for qinfo in test_queries: + polarity = "positive-col" if qinfo["should_trigger"] else "negative-col" + html_parts.append(f' \n') + + html_parts.append(""" + + +""") + + # Find best iteration for highlighting + if test_queries: + best_iter = max(history, key=lambda h: h.get("test_passed") or 0).get("iteration") + else: + best_iter = max(history, key=lambda h: h.get("train_passed", h.get("passed", 0))).get("iteration") + + # Add rows for each iteration + for h in history: + iteration = h.get("iteration", "?") + train_passed = h.get("train_passed", h.get("passed", 0)) + train_total = h.get("train_total", h.get("total", 0)) + test_passed = h.get("test_passed") + test_total = h.get("test_total") + description = h.get("description", "") + train_results = h.get("train_results", h.get("results", [])) + test_results = h.get("test_results", []) + + # Create lookups for results by query + train_by_query = {r["query"]: r for r in train_results} + test_by_query = {r["query"]: r for r in test_results} if test_results else {} + + # Compute aggregate correct/total runs across all retries + def aggregate_runs(results: list[dict]) -> tuple[int, int]: + correct = 0 + total = 0 + for r in results: + runs = r.get("runs", 0) + triggers = r.get("triggers", 0) + total += runs + if r.get("should_trigger", True): + correct += triggers + else: + correct += runs - triggers + return correct, total + + train_correct, train_runs = aggregate_runs(train_results) + test_correct, test_runs = aggregate_runs(test_results) + + # Determine score classes + def score_class(correct: int, total: int) -> str: + if total > 0: + ratio = correct / total + if ratio >= 0.8: + return "score-good" + elif ratio >= 0.5: + return "score-ok" + return "score-bad" + + train_class = score_class(train_correct, train_runs) + test_class = score_class(test_correct, test_runs) + + row_class = "best-row" if iteration == best_iter else "" + + html_parts.append(f""" + + + + +""") + + # Add result for each train query + for qinfo in train_queries: + r = train_by_query.get(qinfo["query"], {}) + did_pass = r.get("pass", False) + triggers = r.get("triggers", 0) + runs = r.get("runs", 0) + + icon = "✓" if did_pass else "✗" + css_class = "pass" if did_pass else "fail" + + html_parts.append(f' \n') + + # Add result for each test query (with different background) + for qinfo in test_queries: + r = test_by_query.get(qinfo["query"], {}) + did_pass = r.get("pass", False) + triggers = r.get("triggers", 0) + runs = r.get("runs", 0) + + icon = "✓" if did_pass else "✗" + css_class = "pass" if did_pass else "fail" + + html_parts.append(f' \n') + + html_parts.append(" \n") + + html_parts.append(""" +
    IterTrainTestDescription{html.escape(qinfo["query"])}{html.escape(qinfo["query"])}
    {iteration}{train_correct}/{train_runs}{test_correct}/{test_runs}{html.escape(description)}{icon}{triggers}/{runs}{icon}{triggers}/{runs}
    +
    +""") + + html_parts.append(""" + + +""") + + return "".join(html_parts) + + +def main(): + parser = argparse.ArgumentParser(description="Generate HTML report from run_loop output") + parser.add_argument("input", help="Path to JSON output from run_loop.py (or - for stdin)") + parser.add_argument("-o", "--output", default=None, help="Output HTML file (default: stdout)") + parser.add_argument("--skill-name", default="", help="Skill name to include in the report title") + args = parser.parse_args() + + if args.input == "-": + data = json.load(sys.stdin) + else: + data = json.loads(Path(args.input).read_text()) + + html_output = generate_html(data, skill_name=args.skill_name) + + if args.output: + Path(args.output).write_text(html_output) + print(f"Report written to {args.output}", file=sys.stderr) + else: + print(html_output) + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/skill-creator/scripts/improve_description.py b/.agents/skills/skill-creator/scripts/improve_description.py new file mode 100755 index 00000000..06bcec76 --- /dev/null +++ b/.agents/skills/skill-creator/scripts/improve_description.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +"""Improve a skill description based on eval results. + +Takes eval results (from run_eval.py) and generates an improved description +by calling `claude -p` as a subprocess (same auth pattern as run_eval.py — +uses the session's Claude Code auth, no separate ANTHROPIC_API_KEY needed). +""" + +import argparse +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +from scripts.utils import parse_skill_md + + +def _call_claude(prompt: str, model: str | None, timeout: int = 300) -> str: + """Run `claude -p` with the prompt on stdin and return the text response. + + Prompt goes over stdin (not argv) because it embeds the full SKILL.md + body and can easily exceed comfortable argv length. + """ + cmd = ["claude", "-p", "--output-format", "text"] + if model: + cmd.extend(["--model", model]) + + # Remove CLAUDECODE env var to allow nesting claude -p inside a + # Claude Code session. The guard is for interactive terminal conflicts; + # programmatic subprocess usage is safe. Same pattern as run_eval.py. + env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"} + + result = subprocess.run( + cmd, + input=prompt, + capture_output=True, + text=True, + env=env, + timeout=timeout, + ) + if result.returncode != 0: + raise RuntimeError( + f"claude -p exited {result.returncode}\nstderr: {result.stderr}" + ) + return result.stdout + + +def improve_description( + skill_name: str, + skill_content: str, + current_description: str, + eval_results: dict, + history: list[dict], + model: str, + test_results: dict | None = None, + log_dir: Path | None = None, + iteration: int | None = None, +) -> str: + """Call Claude to improve the description based on eval results.""" + failed_triggers = [ + r for r in eval_results["results"] + if r["should_trigger"] and not r["pass"] + ] + false_triggers = [ + r for r in eval_results["results"] + if not r["should_trigger"] and not r["pass"] + ] + + # Build scores summary + train_score = f"{eval_results['summary']['passed']}/{eval_results['summary']['total']}" + if test_results: + test_score = f"{test_results['summary']['passed']}/{test_results['summary']['total']}" + scores_summary = f"Train: {train_score}, Test: {test_score}" + else: + scores_summary = f"Train: {train_score}" + + prompt = f"""You are optimizing a skill description for a Claude Code skill called "{skill_name}". A "skill" is sort of like a prompt, but with progressive disclosure -- there's a title and description that Claude sees when deciding whether to use the skill, and then if it does use the skill, it reads the .md file which has lots more details and potentially links to other resources in the skill folder like helper files and scripts and additional documentation or examples. + +The description appears in Claude's "available_skills" list. When a user sends a query, Claude decides whether to invoke the skill based solely on the title and on this description. Your goal is to write a description that triggers for relevant queries, and doesn't trigger for irrelevant ones. + +Here's the current description: + +"{current_description}" + + +Current scores ({scores_summary}): + +""" + if failed_triggers: + prompt += "FAILED TO TRIGGER (should have triggered but didn't):\n" + for r in failed_triggers: + prompt += f' - "{r["query"]}" (triggered {r["triggers"]}/{r["runs"]} times)\n' + prompt += "\n" + + if false_triggers: + prompt += "FALSE TRIGGERS (triggered but shouldn't have):\n" + for r in false_triggers: + prompt += f' - "{r["query"]}" (triggered {r["triggers"]}/{r["runs"]} times)\n' + prompt += "\n" + + if history: + prompt += "PREVIOUS ATTEMPTS (do NOT repeat these — try something structurally different):\n\n" + for h in history: + train_s = f"{h.get('train_passed', h.get('passed', 0))}/{h.get('train_total', h.get('total', 0))}" + test_s = f"{h.get('test_passed', '?')}/{h.get('test_total', '?')}" if h.get('test_passed') is not None else None + score_str = f"train={train_s}" + (f", test={test_s}" if test_s else "") + prompt += f'\n' + prompt += f'Description: "{h["description"]}"\n' + if "results" in h: + prompt += "Train results:\n" + for r in h["results"]: + status = "PASS" if r["pass"] else "FAIL" + prompt += f' [{status}] "{r["query"][:80]}" (triggered {r["triggers"]}/{r["runs"]})\n' + if h.get("note"): + prompt += f'Note: {h["note"]}\n' + prompt += "\n\n" + + prompt += f""" + +Skill content (for context on what the skill does): + +{skill_content} + + +Based on the failures, write a new and improved description that is more likely to trigger correctly. When I say "based on the failures", it's a bit of a tricky line to walk because we don't want to overfit to the specific cases you're seeing. So what I DON'T want you to do is produce an ever-expanding list of specific queries that this skill should or shouldn't trigger for. Instead, try to generalize from the failures to broader categories of user intent and situations where this skill would be useful or not useful. The reason for this is twofold: + +1. Avoid overfitting +2. The list might get loooong and it's injected into ALL queries and there might be a lot of skills, so we don't want to blow too much space on any given description. + +Concretely, your description should not be more than about 100-200 words, even if that comes at the cost of accuracy. There is a hard limit of 1024 characters — descriptions over that will be truncated, so stay comfortably under it. + +Here are some tips that we've found to work well in writing these descriptions: +- The skill should be phrased in the imperative -- "Use this skill for" rather than "this skill does" +- The skill description should focus on the user's intent, what they are trying to achieve, vs. the implementation details of how the skill works. +- The description competes with other skills for Claude's attention — make it distinctive and immediately recognizable. +- If you're getting lots of failures after repeated attempts, change things up. Try different sentence structures or wordings. + +I'd encourage you to be creative and mix up the style in different iterations since you'll have multiple opportunities to try different approaches and we'll just grab the highest-scoring one at the end. + +Please respond with only the new description text in tags, nothing else.""" + + text = _call_claude(prompt, model) + + match = re.search(r"(.*?)", text, re.DOTALL) + description = match.group(1).strip().strip('"') if match else text.strip().strip('"') + + transcript: dict = { + "iteration": iteration, + "prompt": prompt, + "response": text, + "parsed_description": description, + "char_count": len(description), + "over_limit": len(description) > 1024, + } + + # Safety net: the prompt already states the 1024-char hard limit, but if + # the model blew past it anyway, make one fresh single-turn call that + # quotes the too-long version and asks for a shorter rewrite. (The old + # SDK path did this as a true multi-turn; `claude -p` is one-shot, so we + # inline the prior output into the new prompt instead.) + if len(description) > 1024: + shorten_prompt = ( + f"{prompt}\n\n" + f"---\n\n" + f"A previous attempt produced this description, which at " + f"{len(description)} characters is over the 1024-character hard limit:\n\n" + f'"{description}"\n\n' + f"Rewrite it to be under 1024 characters while keeping the most " + f"important trigger words and intent coverage. Respond with only " + f"the new description in tags." + ) + shorten_text = _call_claude(shorten_prompt, model) + match = re.search(r"(.*?)", shorten_text, re.DOTALL) + shortened = match.group(1).strip().strip('"') if match else shorten_text.strip().strip('"') + + transcript["rewrite_prompt"] = shorten_prompt + transcript["rewrite_response"] = shorten_text + transcript["rewrite_description"] = shortened + transcript["rewrite_char_count"] = len(shortened) + description = shortened + + transcript["final_description"] = description + + if log_dir: + log_dir.mkdir(parents=True, exist_ok=True) + log_file = log_dir / f"improve_iter_{iteration or 'unknown'}.json" + log_file.write_text(json.dumps(transcript, indent=2)) + + return description + + +def main(): + parser = argparse.ArgumentParser(description="Improve a skill description based on eval results") + parser.add_argument("--eval-results", required=True, help="Path to eval results JSON (from run_eval.py)") + parser.add_argument("--skill-path", required=True, help="Path to skill directory") + parser.add_argument("--history", default=None, help="Path to history JSON (previous attempts)") + parser.add_argument("--model", required=True, help="Model for improvement") + parser.add_argument("--verbose", action="store_true", help="Print thinking to stderr") + args = parser.parse_args() + + skill_path = Path(args.skill_path) + if not (skill_path / "SKILL.md").exists(): + print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr) + sys.exit(1) + + eval_results = json.loads(Path(args.eval_results).read_text()) + history = [] + if args.history: + history = json.loads(Path(args.history).read_text()) + + name, _, content = parse_skill_md(skill_path) + current_description = eval_results["description"] + + if args.verbose: + print(f"Current: {current_description}", file=sys.stderr) + print(f"Score: {eval_results['summary']['passed']}/{eval_results['summary']['total']}", file=sys.stderr) + + new_description = improve_description( + skill_name=name, + skill_content=content, + current_description=current_description, + eval_results=eval_results, + history=history, + model=args.model, + ) + + if args.verbose: + print(f"Improved: {new_description}", file=sys.stderr) + + # Output as JSON with both the new description and updated history + output = { + "description": new_description, + "history": history + [{ + "description": current_description, + "passed": eval_results["summary"]["passed"], + "failed": eval_results["summary"]["failed"], + "total": eval_results["summary"]["total"], + "results": eval_results["results"], + }], + } + print(json.dumps(output, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/skill-creator/scripts/package_skill.py b/.agents/skills/skill-creator/scripts/package_skill.py new file mode 100755 index 00000000..f48eac44 --- /dev/null +++ b/.agents/skills/skill-creator/scripts/package_skill.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Skill Packager - Creates a distributable .skill file of a skill folder + +Usage: + python utils/package_skill.py [output-directory] + +Example: + python utils/package_skill.py skills/public/my-skill + python utils/package_skill.py skills/public/my-skill ./dist +""" + +import fnmatch +import sys +import zipfile +from pathlib import Path +from scripts.quick_validate import validate_skill + +# Patterns to exclude when packaging skills. +EXCLUDE_DIRS = {"__pycache__", "node_modules"} +EXCLUDE_GLOBS = {"*.pyc"} +EXCLUDE_FILES = {".DS_Store"} +# Directories excluded only at the skill root (not when nested deeper). +ROOT_EXCLUDE_DIRS = {"evals"} + + +def should_exclude(rel_path: Path) -> bool: + """Check if a path should be excluded from packaging.""" + parts = rel_path.parts + if any(part in EXCLUDE_DIRS for part in parts): + return True + # rel_path is relative to skill_path.parent, so parts[0] is the skill + # folder name and parts[1] (if present) is the first subdir. + if len(parts) > 1 and parts[1] in ROOT_EXCLUDE_DIRS: + return True + name = rel_path.name + if name in EXCLUDE_FILES: + return True + return any(fnmatch.fnmatch(name, pat) for pat in EXCLUDE_GLOBS) + + +def package_skill(skill_path, output_dir=None): + """ + Package a skill folder into a .skill file. + + Args: + skill_path: Path to the skill folder + output_dir: Optional output directory for the .skill file (defaults to current directory) + + Returns: + Path to the created .skill file, or None if error + """ + skill_path = Path(skill_path).resolve() + + # Validate skill folder exists + if not skill_path.exists(): + print(f"❌ Error: Skill folder not found: {skill_path}") + return None + + if not skill_path.is_dir(): + print(f"❌ Error: Path is not a directory: {skill_path}") + return None + + # Validate SKILL.md exists + skill_md = skill_path / "SKILL.md" + if not skill_md.exists(): + print(f"❌ Error: SKILL.md not found in {skill_path}") + return None + + # Run validation before packaging + print("🔍 Validating skill...") + valid, message = validate_skill(skill_path) + if not valid: + print(f"❌ Validation failed: {message}") + print(" Please fix the validation errors before packaging.") + return None + print(f"✅ {message}\n") + + # Determine output location + skill_name = skill_path.name + if output_dir: + output_path = Path(output_dir).resolve() + output_path.mkdir(parents=True, exist_ok=True) + else: + output_path = Path.cwd() + + skill_filename = output_path / f"{skill_name}.skill" + + # Create the .skill file (zip format) + try: + with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: + # Walk through the skill directory, excluding build artifacts + for file_path in skill_path.rglob('*'): + if not file_path.is_file(): + continue + arcname = file_path.relative_to(skill_path.parent) + if should_exclude(arcname): + print(f" Skipped: {arcname}") + continue + zipf.write(file_path, arcname) + print(f" Added: {arcname}") + + print(f"\n✅ Successfully packaged skill to: {skill_filename}") + return skill_filename + + except Exception as e: + print(f"❌ Error creating .skill file: {e}") + return None + + +def main(): + if len(sys.argv) < 2: + print("Usage: python utils/package_skill.py [output-directory]") + print("\nExample:") + print(" python utils/package_skill.py skills/public/my-skill") + print(" python utils/package_skill.py skills/public/my-skill ./dist") + sys.exit(1) + + skill_path = sys.argv[1] + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + print(f"📦 Packaging skill: {skill_path}") + if output_dir: + print(f" Output directory: {output_dir}") + print() + + result = package_skill(skill_path, output_dir) + + if result: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/skill-creator/scripts/quick_validate.py b/.agents/skills/skill-creator/scripts/quick_validate.py new file mode 100755 index 00000000..ed8e1ddd --- /dev/null +++ b/.agents/skills/skill-creator/scripts/quick_validate.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +Quick validation script for skills - minimal version +""" + +import sys +import os +import re +import yaml +from pathlib import Path + +def validate_skill(skill_path): + """Basic validation of a skill""" + skill_path = Path(skill_path) + + # Check SKILL.md exists + skill_md = skill_path / 'SKILL.md' + if not skill_md.exists(): + return False, "SKILL.md not found" + + # Read and validate frontmatter + content = skill_md.read_text() + if not content.startswith('---'): + return False, "No YAML frontmatter found" + + # Extract frontmatter + match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) + if not match: + return False, "Invalid frontmatter format" + + frontmatter_text = match.group(1) + + # Parse YAML frontmatter + try: + frontmatter = yaml.safe_load(frontmatter_text) + if not isinstance(frontmatter, dict): + return False, "Frontmatter must be a YAML dictionary" + except yaml.YAMLError as e: + return False, f"Invalid YAML in frontmatter: {e}" + + # Define allowed properties + ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata', 'compatibility'} + + # Check for unexpected properties (excluding nested keys under metadata) + unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES + if unexpected_keys: + return False, ( + f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. " + f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}" + ) + + # Check required fields + if 'name' not in frontmatter: + return False, "Missing 'name' in frontmatter" + if 'description' not in frontmatter: + return False, "Missing 'description' in frontmatter" + + # Extract name for validation + name = frontmatter.get('name', '') + if not isinstance(name, str): + return False, f"Name must be a string, got {type(name).__name__}" + name = name.strip() + if name: + # Check naming convention (kebab-case: lowercase with hyphens) + if not re.match(r'^[a-z0-9-]+$', name): + return False, f"Name '{name}' should be kebab-case (lowercase letters, digits, and hyphens only)" + if name.startswith('-') or name.endswith('-') or '--' in name: + return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens" + # Check name length (max 64 characters per spec) + if len(name) > 64: + return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters." + + # Extract and validate description + description = frontmatter.get('description', '') + if not isinstance(description, str): + return False, f"Description must be a string, got {type(description).__name__}" + description = description.strip() + if description: + # Check for angle brackets + if '<' in description or '>' in description: + return False, "Description cannot contain angle brackets (< or >)" + # Check description length (max 1024 characters per spec) + if len(description) > 1024: + return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters." + + # Validate compatibility field if present (optional) + compatibility = frontmatter.get('compatibility', '') + if compatibility: + if not isinstance(compatibility, str): + return False, f"Compatibility must be a string, got {type(compatibility).__name__}" + if len(compatibility) > 500: + return False, f"Compatibility is too long ({len(compatibility)} characters). Maximum is 500 characters." + + return True, "Skill is valid!" + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python quick_validate.py ") + sys.exit(1) + + valid, message = validate_skill(sys.argv[1]) + print(message) + sys.exit(0 if valid else 1) \ No newline at end of file diff --git a/.agents/skills/skill-creator/scripts/run_eval.py b/.agents/skills/skill-creator/scripts/run_eval.py new file mode 100755 index 00000000..e58c70be --- /dev/null +++ b/.agents/skills/skill-creator/scripts/run_eval.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +"""Run trigger evaluation for a skill description. + +Tests whether a skill's description causes Claude to trigger (read the skill) +for a set of queries. Outputs results as JSON. +""" + +import argparse +import json +import os +import select +import subprocess +import sys +import time +import uuid +from concurrent.futures import ProcessPoolExecutor, as_completed +from pathlib import Path + +from scripts.utils import parse_skill_md + + +def find_project_root() -> Path: + """Find the project root by walking up from cwd looking for .claude/. + + Mimics how Claude Code discovers its project root, so the command file + we create ends up where claude -p will look for it. + """ + current = Path.cwd() + for parent in [current, *current.parents]: + if (parent / ".claude").is_dir(): + return parent + return current + + +def run_single_query( + query: str, + skill_name: str, + skill_description: str, + timeout: int, + project_root: str, + model: str | None = None, +) -> bool: + """Run a single query and return whether the skill was triggered. + + Creates a command file in .claude/commands/ so it appears in Claude's + available_skills list, then runs `claude -p` with the raw query. + Uses --include-partial-messages to detect triggering early from + stream events (content_block_start) rather than waiting for the + full assistant message, which only arrives after tool execution. + """ + unique_id = uuid.uuid4().hex[:8] + clean_name = f"{skill_name}-skill-{unique_id}" + project_commands_dir = Path(project_root) / ".claude" / "commands" + command_file = project_commands_dir / f"{clean_name}.md" + + try: + project_commands_dir.mkdir(parents=True, exist_ok=True) + # Use YAML block scalar to avoid breaking on quotes in description + indented_desc = "\n ".join(skill_description.split("\n")) + command_content = ( + f"---\n" + f"description: |\n" + f" {indented_desc}\n" + f"---\n\n" + f"# {skill_name}\n\n" + f"This skill handles: {skill_description}\n" + ) + command_file.write_text(command_content) + + cmd = [ + "claude", + "-p", query, + "--output-format", "stream-json", + "--verbose", + "--include-partial-messages", + ] + if model: + cmd.extend(["--model", model]) + + # Remove CLAUDECODE env var to allow nesting claude -p inside a + # Claude Code session. The guard is for interactive terminal conflicts; + # programmatic subprocess usage is safe. + env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"} + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + cwd=project_root, + env=env, + ) + + triggered = False + start_time = time.time() + buffer = "" + # Track state for stream event detection + pending_tool_name = None + accumulated_json = "" + + try: + while time.time() - start_time < timeout: + if process.poll() is not None: + remaining = process.stdout.read() + if remaining: + buffer += remaining.decode("utf-8", errors="replace") + break + + ready, _, _ = select.select([process.stdout], [], [], 1.0) + if not ready: + continue + + chunk = os.read(process.stdout.fileno(), 8192) + if not chunk: + break + buffer += chunk.decode("utf-8", errors="replace") + + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.strip() + if not line: + continue + + try: + event = json.loads(line) + except json.JSONDecodeError: + continue + + # Early detection via stream events + if event.get("type") == "stream_event": + se = event.get("event", {}) + se_type = se.get("type", "") + + if se_type == "content_block_start": + cb = se.get("content_block", {}) + if cb.get("type") == "tool_use": + tool_name = cb.get("name", "") + if tool_name in ("Skill", "Read"): + pending_tool_name = tool_name + accumulated_json = "" + else: + return False + + elif se_type == "content_block_delta" and pending_tool_name: + delta = se.get("delta", {}) + if delta.get("type") == "input_json_delta": + accumulated_json += delta.get("partial_json", "") + if clean_name in accumulated_json: + return True + + elif se_type in ("content_block_stop", "message_stop"): + if pending_tool_name: + return clean_name in accumulated_json + if se_type == "message_stop": + return False + + # Fallback: full assistant message + elif event.get("type") == "assistant": + message = event.get("message", {}) + for content_item in message.get("content", []): + if content_item.get("type") != "tool_use": + continue + tool_name = content_item.get("name", "") + tool_input = content_item.get("input", {}) + if tool_name == "Skill" and clean_name in tool_input.get("skill", ""): + triggered = True + elif tool_name == "Read" and clean_name in tool_input.get("file_path", ""): + triggered = True + return triggered + + elif event.get("type") == "result": + return triggered + finally: + # Clean up process on any exit path (return, exception, timeout) + if process.poll() is None: + process.kill() + process.wait() + + return triggered + finally: + if command_file.exists(): + command_file.unlink() + + +def run_eval( + eval_set: list[dict], + skill_name: str, + description: str, + num_workers: int, + timeout: int, + project_root: Path, + runs_per_query: int = 1, + trigger_threshold: float = 0.5, + model: str | None = None, +) -> dict: + """Run the full eval set and return results.""" + results = [] + + with ProcessPoolExecutor(max_workers=num_workers) as executor: + future_to_info = {} + for item in eval_set: + for run_idx in range(runs_per_query): + future = executor.submit( + run_single_query, + item["query"], + skill_name, + description, + timeout, + str(project_root), + model, + ) + future_to_info[future] = (item, run_idx) + + query_triggers: dict[str, list[bool]] = {} + query_items: dict[str, dict] = {} + for future in as_completed(future_to_info): + item, _ = future_to_info[future] + query = item["query"] + query_items[query] = item + if query not in query_triggers: + query_triggers[query] = [] + try: + query_triggers[query].append(future.result()) + except Exception as e: + print(f"Warning: query failed: {e}", file=sys.stderr) + query_triggers[query].append(False) + + for query, triggers in query_triggers.items(): + item = query_items[query] + trigger_rate = sum(triggers) / len(triggers) + should_trigger = item["should_trigger"] + if should_trigger: + did_pass = trigger_rate >= trigger_threshold + else: + did_pass = trigger_rate < trigger_threshold + results.append({ + "query": query, + "should_trigger": should_trigger, + "trigger_rate": trigger_rate, + "triggers": sum(triggers), + "runs": len(triggers), + "pass": did_pass, + }) + + passed = sum(1 for r in results if r["pass"]) + total = len(results) + + return { + "skill_name": skill_name, + "description": description, + "results": results, + "summary": { + "total": total, + "passed": passed, + "failed": total - passed, + }, + } + + +def main(): + parser = argparse.ArgumentParser(description="Run trigger evaluation for a skill description") + parser.add_argument("--eval-set", required=True, help="Path to eval set JSON file") + parser.add_argument("--skill-path", required=True, help="Path to skill directory") + parser.add_argument("--description", default=None, help="Override description to test") + parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers") + parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds") + parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query") + parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold") + parser.add_argument("--model", default=None, help="Model to use for claude -p (default: user's configured model)") + parser.add_argument("--verbose", action="store_true", help="Print progress to stderr") + args = parser.parse_args() + + eval_set = json.loads(Path(args.eval_set).read_text()) + skill_path = Path(args.skill_path) + + if not (skill_path / "SKILL.md").exists(): + print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr) + sys.exit(1) + + name, original_description, content = parse_skill_md(skill_path) + description = args.description or original_description + project_root = find_project_root() + + if args.verbose: + print(f"Evaluating: {description}", file=sys.stderr) + + output = run_eval( + eval_set=eval_set, + skill_name=name, + description=description, + num_workers=args.num_workers, + timeout=args.timeout, + project_root=project_root, + runs_per_query=args.runs_per_query, + trigger_threshold=args.trigger_threshold, + model=args.model, + ) + + if args.verbose: + summary = output["summary"] + print(f"Results: {summary['passed']}/{summary['total']} passed", file=sys.stderr) + for r in output["results"]: + status = "PASS" if r["pass"] else "FAIL" + rate_str = f"{r['triggers']}/{r['runs']}" + print(f" [{status}] rate={rate_str} expected={r['should_trigger']}: {r['query'][:70]}", file=sys.stderr) + + print(json.dumps(output, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/skill-creator/scripts/run_loop.py b/.agents/skills/skill-creator/scripts/run_loop.py new file mode 100755 index 00000000..30a263d6 --- /dev/null +++ b/.agents/skills/skill-creator/scripts/run_loop.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 +"""Run the eval + improve loop until all pass or max iterations reached. + +Combines run_eval.py and improve_description.py in a loop, tracking history +and returning the best description found. Supports train/test split to prevent +overfitting. +""" + +import argparse +import json +import random +import sys +import tempfile +import time +import webbrowser +from pathlib import Path + +from scripts.generate_report import generate_html +from scripts.improve_description import improve_description +from scripts.run_eval import find_project_root, run_eval +from scripts.utils import parse_skill_md + + +def split_eval_set(eval_set: list[dict], holdout: float, seed: int = 42) -> tuple[list[dict], list[dict]]: + """Split eval set into train and test sets, stratified by should_trigger.""" + random.seed(seed) + + # Separate by should_trigger + trigger = [e for e in eval_set if e["should_trigger"]] + no_trigger = [e for e in eval_set if not e["should_trigger"]] + + # Shuffle each group + random.shuffle(trigger) + random.shuffle(no_trigger) + + # Calculate split points + n_trigger_test = max(1, int(len(trigger) * holdout)) + n_no_trigger_test = max(1, int(len(no_trigger) * holdout)) + + # Split + test_set = trigger[:n_trigger_test] + no_trigger[:n_no_trigger_test] + train_set = trigger[n_trigger_test:] + no_trigger[n_no_trigger_test:] + + return train_set, test_set + + +def run_loop( + eval_set: list[dict], + skill_path: Path, + description_override: str | None, + num_workers: int, + timeout: int, + max_iterations: int, + runs_per_query: int, + trigger_threshold: float, + holdout: float, + model: str, + verbose: bool, + live_report_path: Path | None = None, + log_dir: Path | None = None, +) -> dict: + """Run the eval + improvement loop.""" + project_root = find_project_root() + name, original_description, content = parse_skill_md(skill_path) + current_description = description_override or original_description + + # Split into train/test if holdout > 0 + if holdout > 0: + train_set, test_set = split_eval_set(eval_set, holdout) + if verbose: + print(f"Split: {len(train_set)} train, {len(test_set)} test (holdout={holdout})", file=sys.stderr) + else: + train_set = eval_set + test_set = [] + + history = [] + exit_reason = "unknown" + + for iteration in range(1, max_iterations + 1): + if verbose: + print(f"\n{'='*60}", file=sys.stderr) + print(f"Iteration {iteration}/{max_iterations}", file=sys.stderr) + print(f"Description: {current_description}", file=sys.stderr) + print(f"{'='*60}", file=sys.stderr) + + # Evaluate train + test together in one batch for parallelism + all_queries = train_set + test_set + t0 = time.time() + all_results = run_eval( + eval_set=all_queries, + skill_name=name, + description=current_description, + num_workers=num_workers, + timeout=timeout, + project_root=project_root, + runs_per_query=runs_per_query, + trigger_threshold=trigger_threshold, + model=model, + ) + eval_elapsed = time.time() - t0 + + # Split results back into train/test by matching queries + train_queries_set = {q["query"] for q in train_set} + train_result_list = [r for r in all_results["results"] if r["query"] in train_queries_set] + test_result_list = [r for r in all_results["results"] if r["query"] not in train_queries_set] + + train_passed = sum(1 for r in train_result_list if r["pass"]) + train_total = len(train_result_list) + train_summary = {"passed": train_passed, "failed": train_total - train_passed, "total": train_total} + train_results = {"results": train_result_list, "summary": train_summary} + + if test_set: + test_passed = sum(1 for r in test_result_list if r["pass"]) + test_total = len(test_result_list) + test_summary = {"passed": test_passed, "failed": test_total - test_passed, "total": test_total} + test_results = {"results": test_result_list, "summary": test_summary} + else: + test_results = None + test_summary = None + + history.append({ + "iteration": iteration, + "description": current_description, + "train_passed": train_summary["passed"], + "train_failed": train_summary["failed"], + "train_total": train_summary["total"], + "train_results": train_results["results"], + "test_passed": test_summary["passed"] if test_summary else None, + "test_failed": test_summary["failed"] if test_summary else None, + "test_total": test_summary["total"] if test_summary else None, + "test_results": test_results["results"] if test_results else None, + # For backward compat with report generator + "passed": train_summary["passed"], + "failed": train_summary["failed"], + "total": train_summary["total"], + "results": train_results["results"], + }) + + # Write live report if path provided + if live_report_path: + partial_output = { + "original_description": original_description, + "best_description": current_description, + "best_score": "in progress", + "iterations_run": len(history), + "holdout": holdout, + "train_size": len(train_set), + "test_size": len(test_set), + "history": history, + } + live_report_path.write_text(generate_html(partial_output, auto_refresh=True, skill_name=name)) + + if verbose: + def print_eval_stats(label, results, elapsed): + pos = [r for r in results if r["should_trigger"]] + neg = [r for r in results if not r["should_trigger"]] + tp = sum(r["triggers"] for r in pos) + pos_runs = sum(r["runs"] for r in pos) + fn = pos_runs - tp + fp = sum(r["triggers"] for r in neg) + neg_runs = sum(r["runs"] for r in neg) + tn = neg_runs - fp + total = tp + tn + fp + fn + precision = tp / (tp + fp) if (tp + fp) > 0 else 1.0 + recall = tp / (tp + fn) if (tp + fn) > 0 else 1.0 + accuracy = (tp + tn) / total if total > 0 else 0.0 + print(f"{label}: {tp+tn}/{total} correct, precision={precision:.0%} recall={recall:.0%} accuracy={accuracy:.0%} ({elapsed:.1f}s)", file=sys.stderr) + for r in results: + status = "PASS" if r["pass"] else "FAIL" + rate_str = f"{r['triggers']}/{r['runs']}" + print(f" [{status}] rate={rate_str} expected={r['should_trigger']}: {r['query'][:60]}", file=sys.stderr) + + print_eval_stats("Train", train_results["results"], eval_elapsed) + if test_summary: + print_eval_stats("Test ", test_results["results"], 0) + + if train_summary["failed"] == 0: + exit_reason = f"all_passed (iteration {iteration})" + if verbose: + print(f"\nAll train queries passed on iteration {iteration}!", file=sys.stderr) + break + + if iteration == max_iterations: + exit_reason = f"max_iterations ({max_iterations})" + if verbose: + print(f"\nMax iterations reached ({max_iterations}).", file=sys.stderr) + break + + # Improve the description based on train results + if verbose: + print(f"\nImproving description...", file=sys.stderr) + + t0 = time.time() + # Strip test scores from history so improvement model can't see them + blinded_history = [ + {k: v for k, v in h.items() if not k.startswith("test_")} + for h in history + ] + new_description = improve_description( + skill_name=name, + skill_content=content, + current_description=current_description, + eval_results=train_results, + history=blinded_history, + model=model, + log_dir=log_dir, + iteration=iteration, + ) + improve_elapsed = time.time() - t0 + + if verbose: + print(f"Proposed ({improve_elapsed:.1f}s): {new_description}", file=sys.stderr) + + current_description = new_description + + # Find the best iteration by TEST score (or train if no test set) + if test_set: + best = max(history, key=lambda h: h["test_passed"] or 0) + best_score = f"{best['test_passed']}/{best['test_total']}" + else: + best = max(history, key=lambda h: h["train_passed"]) + best_score = f"{best['train_passed']}/{best['train_total']}" + + if verbose: + print(f"\nExit reason: {exit_reason}", file=sys.stderr) + print(f"Best score: {best_score} (iteration {best['iteration']})", file=sys.stderr) + + return { + "exit_reason": exit_reason, + "original_description": original_description, + "best_description": best["description"], + "best_score": best_score, + "best_train_score": f"{best['train_passed']}/{best['train_total']}", + "best_test_score": f"{best['test_passed']}/{best['test_total']}" if test_set else None, + "final_description": current_description, + "iterations_run": len(history), + "holdout": holdout, + "train_size": len(train_set), + "test_size": len(test_set), + "history": history, + } + + +def main(): + parser = argparse.ArgumentParser(description="Run eval + improve loop") + parser.add_argument("--eval-set", required=True, help="Path to eval set JSON file") + parser.add_argument("--skill-path", required=True, help="Path to skill directory") + parser.add_argument("--description", default=None, help="Override starting description") + parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers") + parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds") + parser.add_argument("--max-iterations", type=int, default=5, help="Max improvement iterations") + parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query") + parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold") + parser.add_argument("--holdout", type=float, default=0.4, help="Fraction of eval set to hold out for testing (0 to disable)") + parser.add_argument("--model", required=True, help="Model for improvement") + parser.add_argument("--verbose", action="store_true", help="Print progress to stderr") + parser.add_argument("--report", default="auto", help="Generate HTML report at this path (default: 'auto' for temp file, 'none' to disable)") + parser.add_argument("--results-dir", default=None, help="Save all outputs (results.json, report.html, log.txt) to a timestamped subdirectory here") + args = parser.parse_args() + + eval_set = json.loads(Path(args.eval_set).read_text()) + skill_path = Path(args.skill_path) + + if not (skill_path / "SKILL.md").exists(): + print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr) + sys.exit(1) + + name, _, _ = parse_skill_md(skill_path) + + # Set up live report path + if args.report != "none": + if args.report == "auto": + timestamp = time.strftime("%Y%m%d_%H%M%S") + live_report_path = Path(tempfile.gettempdir()) / f"skill_description_report_{skill_path.name}_{timestamp}.html" + else: + live_report_path = Path(args.report) + # Open the report immediately so the user can watch + live_report_path.write_text("

    Starting optimization loop...

    ") + webbrowser.open(str(live_report_path)) + else: + live_report_path = None + + # Determine output directory (create before run_loop so logs can be written) + if args.results_dir: + timestamp = time.strftime("%Y-%m-%d_%H%M%S") + results_dir = Path(args.results_dir) / timestamp + results_dir.mkdir(parents=True, exist_ok=True) + else: + results_dir = None + + log_dir = results_dir / "logs" if results_dir else None + + output = run_loop( + eval_set=eval_set, + skill_path=skill_path, + description_override=args.description, + num_workers=args.num_workers, + timeout=args.timeout, + max_iterations=args.max_iterations, + runs_per_query=args.runs_per_query, + trigger_threshold=args.trigger_threshold, + holdout=args.holdout, + model=args.model, + verbose=args.verbose, + live_report_path=live_report_path, + log_dir=log_dir, + ) + + # Save JSON output + json_output = json.dumps(output, indent=2) + print(json_output) + if results_dir: + (results_dir / "results.json").write_text(json_output) + + # Write final HTML report (without auto-refresh) + if live_report_path: + live_report_path.write_text(generate_html(output, auto_refresh=False, skill_name=name)) + print(f"\nReport: {live_report_path}", file=sys.stderr) + + if results_dir and live_report_path: + (results_dir / "report.html").write_text(generate_html(output, auto_refresh=False, skill_name=name)) + + if results_dir: + print(f"Results saved to: {results_dir}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/skill-creator/scripts/utils.py b/.agents/skills/skill-creator/scripts/utils.py new file mode 100644 index 00000000..51b6a07d --- /dev/null +++ b/.agents/skills/skill-creator/scripts/utils.py @@ -0,0 +1,47 @@ +"""Shared utilities for skill-creator scripts.""" + +from pathlib import Path + + + +def parse_skill_md(skill_path: Path) -> tuple[str, str, str]: + """Parse a SKILL.md file, returning (name, description, full_content).""" + content = (skill_path / "SKILL.md").read_text() + lines = content.split("\n") + + if lines[0].strip() != "---": + raise ValueError("SKILL.md missing frontmatter (no opening ---)") + + end_idx = None + for i, line in enumerate(lines[1:], start=1): + if line.strip() == "---": + end_idx = i + break + + if end_idx is None: + raise ValueError("SKILL.md missing frontmatter (no closing ---)") + + name = "" + description = "" + frontmatter_lines = lines[1:end_idx] + i = 0 + while i < len(frontmatter_lines): + line = frontmatter_lines[i] + if line.startswith("name:"): + name = line[len("name:"):].strip().strip('"').strip("'") + elif line.startswith("description:"): + value = line[len("description:"):].strip() + # Handle YAML multiline indicators (>, |, >-, |-) + if value in (">", "|", ">-", "|-"): + continuation_lines: list[str] = [] + i += 1 + while i < len(frontmatter_lines) and (frontmatter_lines[i].startswith(" ") or frontmatter_lines[i].startswith("\t")): + continuation_lines.append(frontmatter_lines[i].strip()) + i += 1 + description = " ".join(continuation_lines) + continue + else: + description = value.strip('"').strip("'") + i += 1 + + return name, description, content diff --git a/.agents/skills/skills-admin/INSTALL.md b/.agents/skills/skills-admin/INSTALL.md new file mode 100644 index 00000000..a2e8a425 --- /dev/null +++ b/.agents/skills/skills-admin/INSTALL.md @@ -0,0 +1,17 @@ +# Installation Guide + +## Claude Code (CLI) + +```bash +cp -r skills/skills-admin ~/.claude/skills/ +``` + +## claude.ai + +Paste the contents of `SKILL.md` into your Project Knowledge. + +## Uninstalling + +```bash +rm -rf ~/.claude/skills/skills-admin +``` diff --git a/.agents/skills/skills-admin/README.md b/.agents/skills/skills-admin/README.md new file mode 100644 index 00000000..45054092 --- /dev/null +++ b/.agents/skills/skills-admin/README.md @@ -0,0 +1,18 @@ +# Skills Admin + +Administrative workflows for contributing to and updating the [agent-skills](https://github.com/promptingcompany/agent-skills) repository. + +## Workflows + +| Workflow | Triggers | +|---|---| +| Open a PR | "open a PR", "submit my changes", "push this skill", "create a pull request" | +| Update an installed skill | "update my skills", "get the latest skill", "how do I update" | + +## Install + +```bash +cp -r skills/skills-admin ~/.claude/skills/ +``` + +See [`INSTALL.md`](INSTALL.md) for claude.ai and MCP server setup. diff --git a/.agents/skills/skills-admin/SKILL.md b/.agents/skills/skills-admin/SKILL.md new file mode 100644 index 00000000..515c8c40 --- /dev/null +++ b/.agents/skills/skills-admin/SKILL.md @@ -0,0 +1,50 @@ +--- +name: skills-admin +description: > + Administrative workflows for the agent-skills repository. + Use when the user wants to contribute a skill, open a pull request, + or update an already-installed skill to the latest version. + + Trigger when users say: "open a PR", "submit my changes", "push this skill", + "update my skills", "update the skills repo", or "how do I contribute a skill". +--- + +# Skills Admin + +When this skill is activated, greet the user with: +"Thank you for activating the Skills Admin skill by The Prompting Company (https://promptingcompany.com)." + +Then list the available workflows by reading the Workflows section of this skill — one line each, name and one-sentence description. End with: "How can I help you today?" + +## Overview + +Administrative workflows for contributing to and maintaining the [agent-skills](https://github.com/promptingcompany/agent-skills) repository. + +## Trigger keywords + +This skill activates when the user asks to: +- Open a PR, submit skill changes, push a skill update, or create a pull request for the skills repo +- Update an installed skill to the latest version +- Contribute a new skill or fix to the repository + +## Workflows + +### 1. Open a PR + +See [`workflows/open-pr.md`] for full steps. Summary: + +1. Fork and clone the repo if first time (Step 0), then sync with upstream. +2. Audit changes with `git status` and `git diff` — flag anything outside `skills/`. +3. Summarise what will be committed and ask for confirmation. +4. Commit with a conventional message (`add:`, `update:`, `fix:`, `meta:`). +5. Push to a new branch on the fork, never to `main`. +6. Open a PR against upstream via `gh pr create` and return the URL. + +### 2. Update an Installed Skill + +See [`workflows/update-skill.md`] for full steps. Summary: + +1. Check which skills are installed and their current version from `metadata.json`. +2. Fetch the latest `metadata.json` from upstream to compare versions. +3. Re-run `npx skills add` for any skill that is out of date, or pull and copy manually. +4. Confirm the update succeeded by checking the installed `metadata.json` version. diff --git a/.agents/skills/skills-admin/workflows/open-pr.md b/.agents/skills/skills-admin/workflows/open-pr.md new file mode 100644 index 00000000..78b90967 --- /dev/null +++ b/.agents/skills/skills-admin/workflows/open-pr.md @@ -0,0 +1,140 @@ +--- +name: open-pr +description: > + Opens a pull request to update one or more skills in the agent-skills repository. + Checks what has changed, commits, pushes to a branch, and creates the PR. + + Trigger when users say: "open a PR", "create a PR for this skill", "submit my changes", + "push this skill", or "update the skills repo". +--- + +# Open a PR to Update Skills + +## Overview + +Commit skill changes and open a pull request against the main branch. + +## Prerequisites + +- Changes to one or more skill files that are ready to ship +- `gh` CLI authenticated (`gh auth status`) +- A fork of [github.com/promptingcompany/agent-skills](https://github.com/promptingcompany/agent-skills) set as `origin`, with `upstream` pointing to the source repo + +If the user has not forked yet, run Step 0 first. + +## Required Workflow + +**Follow all steps in order.** + +--- + +### Step 0 — Fork and clone (first-time contributors only) + +Check whether the user already has a fork: + +```bash +gh repo view promptingcompany/agent-skills --json isFork +``` + +If no fork exists: + +```bash +gh repo fork promptingcompany/agent-skills --clone --remote +cd agent-skills +git remote add upstream https://github.com/promptingcompany/agent-skills.git +``` + +Confirm the remotes are set correctly: + +```bash +git remote -v +# origin https://github.com//agent-skills (fetch/push) +# upstream https://github.com/promptingcompany/agent-skills (fetch/push) +``` + +--- + +### Step 1 — Sync with upstream + +Before making changes, ensure the fork is up to date: + +```bash +git fetch upstream +git rebase upstream/main +``` + +If conflicts arise, resolve them before continuing. + +--- + +### Step 2 — Audit what has changed + +Run `git status` and `git diff` to understand the full scope of changes: + +- List every modified or untracked file under `skills/` +- Flag any files outside `skills/` that were changed unexpectedly — confirm with the user before including them +- If there are no changes, tell the user and stop + +--- + +### Step 3 — Summarise the changes + +Before committing, produce a short summary for the user: + +``` +Skill: [skill folder name] +Changed: [list of files] +Type: [new skill | workflow update | metadata | install guide | other] +``` + +Ask: +> "Does this look right? Anything to exclude?" + +Wait for confirmation before proceeding. + +--- + +### Step 4 — Commit + +Stage only the confirmed files and commit: + +```bash +git add [confirmed files] +git commit -m "[type]: [short description of what changed]" +``` + +Commit message conventions: +- `add:` — new skill or workflow file +- `update:` — changes to an existing skill or workflow +- `fix:` — correcting a mistake in a skill +- `meta:` — metadata, README, or install guide only + +--- + +### Step 5 — Push to a branch on the fork + +Never commit directly to `main`. Create a branch and push to `origin` (the fork): + +```bash +git checkout -b skills/[skill-name]-[short-description] +git push -u origin HEAD +``` + +--- + +### Step 6 — Open the PR against upstream + +```bash +gh pr create \ + --repo promptingcompany/agent-skills \ + --title "[type]: [skill name] — [short description]" \ + --body "[summary of what changed and why]" \ + --base main +``` + +PR body should include: +- What skill was changed and why +- Any workflows added or removed +- A one-line test instruction (e.g., "Trigger with: `simulate agent for [product]`") + +Confirm back with the PR URL once created. diff --git a/.agents/skills/skills-admin/workflows/update-skill.md b/.agents/skills/skills-admin/workflows/update-skill.md new file mode 100644 index 00000000..a4c65f4c --- /dev/null +++ b/.agents/skills/skills-admin/workflows/update-skill.md @@ -0,0 +1,80 @@ +--- +name: update-skill +description: > + Updates an already-installed skill to the latest version from the agent-skills repository. + Compares the installed metadata.json version against upstream and re-installs if out of date. + + Trigger when users say: "update my skills", "get the latest skill", "how do I update", + or "is my skill up to date". +--- + +# Update an Installed Skill + +## Overview + +Check whether an installed skill is out of date and update it to the latest version. + +## Prerequisites + +- Skill previously installed to `~/.claude/skills/` or via `npx skills add` +- Internet access to fetch the latest version from upstream + +## Required Workflow + +**Follow all steps in order.** + +--- + +### Step 1 — Check installed version + +Read the `metadata.json` of the installed skill: + +```bash +cat ~/.claude/skills//metadata.json +``` + +Note the `version` field. + +--- + +### Step 2 — Check upstream version + +Fetch the latest `metadata.json` directly from the repo: + +```bash +curl -s https://raw.githubusercontent.com/promptingcompany/agent-skills/main/skills//metadata.json +``` + +Compare the `version` fields. If they match, tell the user: "Your skill is already up to date (`v[version]`)." and stop. + +--- + +### Step 3 — Update + +**Option A — via the `skills` CLI (recommended):** + +```bash +npx skills add https://github.com/promptingcompany/agent-skills --skill +``` + +This overwrites the local copy with the latest version. + +**Option B — manual:** + +```bash +git clone https://github.com/promptingcompany/agent-skills /tmp/agent-skills +cp -r /tmp/agent-skills/skills/ ~/.claude/skills/ +rm -rf /tmp/agent-skills +``` + +--- + +### Step 4 — Confirm + +Re-read the installed `metadata.json` and confirm the version now matches upstream: + +```bash +cat ~/.claude/skills//metadata.json +``` + +Tell the user: "Updated to `v[new-version]`." diff --git a/.agents/skills/slack-gif-creator/LICENSE.txt b/.agents/skills/slack-gif-creator/LICENSE.txt new file mode 100644 index 00000000..4f881c52 --- /dev/null +++ b/.agents/skills/slack-gif-creator/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Anthropic, PBC. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/.agents/skills/slack-gif-creator/SKILL.md b/.agents/skills/slack-gif-creator/SKILL.md new file mode 100644 index 00000000..16660d8c --- /dev/null +++ b/.agents/skills/slack-gif-creator/SKILL.md @@ -0,0 +1,254 @@ +--- +name: slack-gif-creator +description: Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack." +license: Complete terms in LICENSE.txt +--- + +# Slack GIF Creator + +A toolkit providing utilities and knowledge for creating animated GIFs optimized for Slack. + +## Slack Requirements + +**Dimensions:** +- Emoji GIFs: 128x128 (recommended) +- Message GIFs: 480x480 + +**Parameters:** +- FPS: 10-30 (lower is smaller file size) +- Colors: 48-128 (fewer = smaller file size) +- Duration: Keep under 3 seconds for emoji GIFs + +## Core Workflow + +```python +from core.gif_builder import GIFBuilder +from PIL import Image, ImageDraw + +# 1. Create builder +builder = GIFBuilder(width=128, height=128, fps=10) + +# 2. Generate frames +for i in range(12): + frame = Image.new('RGB', (128, 128), (240, 248, 255)) + draw = ImageDraw.Draw(frame) + + # Draw your animation using PIL primitives + # (circles, polygons, lines, etc.) + + builder.add_frame(frame) + +# 3. Save with optimization +builder.save('output.gif', num_colors=48, optimize_for_emoji=True) +``` + +## Drawing Graphics + +### Working with User-Uploaded Images +If a user uploads an image, consider whether they want to: +- **Use it directly** (e.g., "animate this", "split this into frames") +- **Use it as inspiration** (e.g., "make something like this") + +Load and work with images using PIL: +```python +from PIL import Image + +uploaded = Image.open('file.png') +# Use directly, or just as reference for colors/style +``` + +### Drawing from Scratch +When drawing graphics from scratch, use PIL ImageDraw primitives: + +```python +from PIL import ImageDraw + +draw = ImageDraw.Draw(frame) + +# Circles/ovals +draw.ellipse([x1, y1, x2, y2], fill=(r, g, b), outline=(r, g, b), width=3) + +# Stars, triangles, any polygon +points = [(x1, y1), (x2, y2), (x3, y3), ...] +draw.polygon(points, fill=(r, g, b), outline=(r, g, b), width=3) + +# Lines +draw.line([(x1, y1), (x2, y2)], fill=(r, g, b), width=5) + +# Rectangles +draw.rectangle([x1, y1, x2, y2], fill=(r, g, b), outline=(r, g, b), width=3) +``` + +**Don't use:** Emoji fonts (unreliable across platforms) or assume pre-packaged graphics exist in this skill. + +### Making Graphics Look Good + +Graphics should look polished and creative, not basic. Here's how: + +**Use thicker lines** - Always set `width=2` or higher for outlines and lines. Thin lines (width=1) look choppy and amateurish. + +**Add visual depth**: +- Use gradients for backgrounds (`create_gradient_background`) +- Layer multiple shapes for complexity (e.g., a star with a smaller star inside) + +**Make shapes more interesting**: +- Don't just draw a plain circle - add highlights, rings, or patterns +- Stars can have glows (draw larger, semi-transparent versions behind) +- Combine multiple shapes (stars + sparkles, circles + rings) + +**Pay attention to colors**: +- Use vibrant, complementary colors +- Add contrast (dark outlines on light shapes, light outlines on dark shapes) +- Consider the overall composition + +**For complex shapes** (hearts, snowflakes, etc.): +- Use combinations of polygons and ellipses +- Calculate points carefully for symmetry +- Add details (a heart can have a highlight curve, snowflakes have intricate branches) + +Be creative and detailed! A good Slack GIF should look polished, not like placeholder graphics. + +## Available Utilities + +### GIFBuilder (`core.gif_builder`) +Assembles frames and optimizes for Slack: +```python +builder = GIFBuilder(width=128, height=128, fps=10) +builder.add_frame(frame) # Add PIL Image +builder.add_frames(frames) # Add list of frames +builder.save('out.gif', num_colors=48, optimize_for_emoji=True, remove_duplicates=True) +``` + +### Validators (`core.validators`) +Check if GIF meets Slack requirements: +```python +from core.validators import validate_gif, is_slack_ready + +# Detailed validation +passes, info = validate_gif('my.gif', is_emoji=True, verbose=True) + +# Quick check +if is_slack_ready('my.gif'): + print("Ready!") +``` + +### Easing Functions (`core.easing`) +Smooth motion instead of linear: +```python +from core.easing import interpolate + +# Progress from 0.0 to 1.0 +t = i / (num_frames - 1) + +# Apply easing +y = interpolate(start=0, end=400, t=t, easing='ease_out') + +# Available: linear, ease_in, ease_out, ease_in_out, +# bounce_out, elastic_out, back_out +``` + +### Frame Helpers (`core.frame_composer`) +Convenience functions for common needs: +```python +from core.frame_composer import ( + create_blank_frame, # Solid color background + create_gradient_background, # Vertical gradient + draw_circle, # Helper for circles + draw_text, # Simple text rendering + draw_star # 5-pointed star +) +``` + +## Animation Concepts + +### Shake/Vibrate +Offset object position with oscillation: +- Use `math.sin()` or `math.cos()` with frame index +- Add small random variations for natural feel +- Apply to x and/or y position + +### Pulse/Heartbeat +Scale object size rhythmically: +- Use `math.sin(t * frequency * 2 * math.pi)` for smooth pulse +- For heartbeat: two quick pulses then pause (adjust sine wave) +- Scale between 0.8 and 1.2 of base size + +### Bounce +Object falls and bounces: +- Use `interpolate()` with `easing='bounce_out'` for landing +- Use `easing='ease_in'` for falling (accelerating) +- Apply gravity by increasing y velocity each frame + +### Spin/Rotate +Rotate object around center: +- PIL: `image.rotate(angle, resample=Image.BICUBIC)` +- For wobble: use sine wave for angle instead of linear + +### Fade In/Out +Gradually appear or disappear: +- Create RGBA image, adjust alpha channel +- Or use `Image.blend(image1, image2, alpha)` +- Fade in: alpha from 0 to 1 +- Fade out: alpha from 1 to 0 + +### Slide +Move object from off-screen to position: +- Start position: outside frame bounds +- End position: target location +- Use `interpolate()` with `easing='ease_out'` for smooth stop +- For overshoot: use `easing='back_out'` + +### Zoom +Scale and position for zoom effect: +- Zoom in: scale from 0.1 to 2.0, crop center +- Zoom out: scale from 2.0 to 1.0 +- Can add motion blur for drama (PIL filter) + +### Explode/Particle Burst +Create particles radiating outward: +- Generate particles with random angles and velocities +- Update each particle: `x += vx`, `y += vy` +- Add gravity: `vy += gravity_constant` +- Fade out particles over time (reduce alpha) + +## Optimization Strategies + +Only when asked to make the file size smaller, implement a few of the following methods: + +1. **Fewer frames** - Lower FPS (10 instead of 20) or shorter duration +2. **Fewer colors** - `num_colors=48` instead of 128 +3. **Smaller dimensions** - 128x128 instead of 480x480 +4. **Remove duplicates** - `remove_duplicates=True` in save() +5. **Emoji mode** - `optimize_for_emoji=True` auto-optimizes + +```python +# Maximum optimization for emoji +builder.save( + 'emoji.gif', + num_colors=48, + optimize_for_emoji=True, + remove_duplicates=True +) +``` + +## Philosophy + +This skill provides: +- **Knowledge**: Slack's requirements and animation concepts +- **Utilities**: GIFBuilder, validators, easing functions +- **Flexibility**: Create the animation logic using PIL primitives + +It does NOT provide: +- Rigid animation templates or pre-made functions +- Emoji font rendering (unreliable across platforms) +- A library of pre-packaged graphics built into the skill + +**Note on user uploads**: This skill doesn't include pre-built graphics, but if a user uploads an image, use PIL to load and work with it - interpret based on their request whether they want it used directly or just as inspiration. + +Be creative! Combine concepts (bouncing + rotating, pulsing + sliding, etc.) and use PIL's full capabilities. + +## Dependencies + +```bash +pip install pillow imageio numpy +``` diff --git a/.agents/skills/slack-gif-creator/core/easing.py b/.agents/skills/slack-gif-creator/core/easing.py new file mode 100755 index 00000000..772fa830 --- /dev/null +++ b/.agents/skills/slack-gif-creator/core/easing.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +""" +Easing Functions - Timing functions for smooth animations. + +Provides various easing functions for natural motion and timing. +All functions take a value t (0.0 to 1.0) and return eased value (0.0 to 1.0). +""" + +import math + + +def linear(t: float) -> float: + """Linear interpolation (no easing).""" + return t + + +def ease_in_quad(t: float) -> float: + """Quadratic ease-in (slow start, accelerating).""" + return t * t + + +def ease_out_quad(t: float) -> float: + """Quadratic ease-out (fast start, decelerating).""" + return t * (2 - t) + + +def ease_in_out_quad(t: float) -> float: + """Quadratic ease-in-out (slow start and end).""" + if t < 0.5: + return 2 * t * t + return -1 + (4 - 2 * t) * t + + +def ease_in_cubic(t: float) -> float: + """Cubic ease-in (slow start).""" + return t * t * t + + +def ease_out_cubic(t: float) -> float: + """Cubic ease-out (fast start).""" + return (t - 1) * (t - 1) * (t - 1) + 1 + + +def ease_in_out_cubic(t: float) -> float: + """Cubic ease-in-out.""" + if t < 0.5: + return 4 * t * t * t + return (t - 1) * (2 * t - 2) * (2 * t - 2) + 1 + + +def ease_in_bounce(t: float) -> float: + """Bounce ease-in (bouncy start).""" + return 1 - ease_out_bounce(1 - t) + + +def ease_out_bounce(t: float) -> float: + """Bounce ease-out (bouncy end).""" + if t < 1 / 2.75: + return 7.5625 * t * t + elif t < 2 / 2.75: + t -= 1.5 / 2.75 + return 7.5625 * t * t + 0.75 + elif t < 2.5 / 2.75: + t -= 2.25 / 2.75 + return 7.5625 * t * t + 0.9375 + else: + t -= 2.625 / 2.75 + return 7.5625 * t * t + 0.984375 + + +def ease_in_out_bounce(t: float) -> float: + """Bounce ease-in-out.""" + if t < 0.5: + return ease_in_bounce(t * 2) * 0.5 + return ease_out_bounce(t * 2 - 1) * 0.5 + 0.5 + + +def ease_in_elastic(t: float) -> float: + """Elastic ease-in (spring effect).""" + if t == 0 or t == 1: + return t + return -math.pow(2, 10 * (t - 1)) * math.sin((t - 1.1) * 5 * math.pi) + + +def ease_out_elastic(t: float) -> float: + """Elastic ease-out (spring effect).""" + if t == 0 or t == 1: + return t + return math.pow(2, -10 * t) * math.sin((t - 0.1) * 5 * math.pi) + 1 + + +def ease_in_out_elastic(t: float) -> float: + """Elastic ease-in-out.""" + if t == 0 or t == 1: + return t + t = t * 2 - 1 + if t < 0: + return -0.5 * math.pow(2, 10 * t) * math.sin((t - 0.1) * 5 * math.pi) + return math.pow(2, -10 * t) * math.sin((t - 0.1) * 5 * math.pi) * 0.5 + 1 + + +# Convenience mapping +EASING_FUNCTIONS = { + "linear": linear, + "ease_in": ease_in_quad, + "ease_out": ease_out_quad, + "ease_in_out": ease_in_out_quad, + "bounce_in": ease_in_bounce, + "bounce_out": ease_out_bounce, + "bounce": ease_in_out_bounce, + "elastic_in": ease_in_elastic, + "elastic_out": ease_out_elastic, + "elastic": ease_in_out_elastic, +} + + +def get_easing(name: str = "linear"): + """Get easing function by name.""" + return EASING_FUNCTIONS.get(name, linear) + + +def interpolate(start: float, end: float, t: float, easing: str = "linear") -> float: + """ + Interpolate between two values with easing. + + Args: + start: Start value + end: End value + t: Progress from 0.0 to 1.0 + easing: Name of easing function + + Returns: + Interpolated value + """ + ease_func = get_easing(easing) + eased_t = ease_func(t) + return start + (end - start) * eased_t + + +def ease_back_in(t: float) -> float: + """Back ease-in (slight overshoot backward before forward motion).""" + c1 = 1.70158 + c3 = c1 + 1 + return c3 * t * t * t - c1 * t * t + + +def ease_back_out(t: float) -> float: + """Back ease-out (overshoot forward then settle back).""" + c1 = 1.70158 + c3 = c1 + 1 + return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2) + + +def ease_back_in_out(t: float) -> float: + """Back ease-in-out (overshoot at both ends).""" + c1 = 1.70158 + c2 = c1 * 1.525 + if t < 0.5: + return (pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2 + return (pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2 + + +def apply_squash_stretch( + base_scale: tuple[float, float], intensity: float, direction: str = "vertical" +) -> tuple[float, float]: + """ + Calculate squash and stretch scales for more dynamic animation. + + Args: + base_scale: (width_scale, height_scale) base scales + intensity: Squash/stretch intensity (0.0-1.0) + direction: 'vertical', 'horizontal', or 'both' + + Returns: + (width_scale, height_scale) with squash/stretch applied + """ + width_scale, height_scale = base_scale + + if direction == "vertical": + # Compress vertically, expand horizontally (preserve volume) + height_scale *= 1 - intensity * 0.5 + width_scale *= 1 + intensity * 0.5 + elif direction == "horizontal": + # Compress horizontally, expand vertically + width_scale *= 1 - intensity * 0.5 + height_scale *= 1 + intensity * 0.5 + elif direction == "both": + # General squash (both dimensions) + width_scale *= 1 - intensity * 0.3 + height_scale *= 1 - intensity * 0.3 + + return (width_scale, height_scale) + + +def calculate_arc_motion( + start: tuple[float, float], end: tuple[float, float], height: float, t: float +) -> tuple[float, float]: + """ + Calculate position along a parabolic arc (natural motion path). + + Args: + start: (x, y) starting position + end: (x, y) ending position + height: Arc height at midpoint (positive = upward) + t: Progress (0.0-1.0) + + Returns: + (x, y) position along arc + """ + x1, y1 = start + x2, y2 = end + + # Linear interpolation for x + x = x1 + (x2 - x1) * t + + # Parabolic interpolation for y + # y = start + progress * (end - start) + arc_offset + # Arc offset peaks at t=0.5 + arc_offset = 4 * height * t * (1 - t) + y = y1 + (y2 - y1) * t - arc_offset + + return (x, y) + + +# Add new easing functions to the convenience mapping +EASING_FUNCTIONS.update( + { + "back_in": ease_back_in, + "back_out": ease_back_out, + "back_in_out": ease_back_in_out, + "anticipate": ease_back_in, # Alias + "overshoot": ease_back_out, # Alias + } +) diff --git a/.agents/skills/slack-gif-creator/core/frame_composer.py b/.agents/skills/slack-gif-creator/core/frame_composer.py new file mode 100755 index 00000000..1afe4348 --- /dev/null +++ b/.agents/skills/slack-gif-creator/core/frame_composer.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +""" +Frame Composer - Utilities for composing visual elements into frames. + +Provides functions for drawing shapes, text, emojis, and compositing elements +together to create animation frames. +""" + +from typing import Optional + +import numpy as np +from PIL import Image, ImageDraw, ImageFont + + +def create_blank_frame( + width: int, height: int, color: tuple[int, int, int] = (255, 255, 255) +) -> Image.Image: + """ + Create a blank frame with solid color background. + + Args: + width: Frame width + height: Frame height + color: RGB color tuple (default: white) + + Returns: + PIL Image + """ + return Image.new("RGB", (width, height), color) + + +def draw_circle( + frame: Image.Image, + center: tuple[int, int], + radius: int, + fill_color: Optional[tuple[int, int, int]] = None, + outline_color: Optional[tuple[int, int, int]] = None, + outline_width: int = 1, +) -> Image.Image: + """ + Draw a circle on a frame. + + Args: + frame: PIL Image to draw on + center: (x, y) center position + radius: Circle radius + fill_color: RGB fill color (None for no fill) + outline_color: RGB outline color (None for no outline) + outline_width: Outline width in pixels + + Returns: + Modified frame + """ + draw = ImageDraw.Draw(frame) + x, y = center + bbox = [x - radius, y - radius, x + radius, y + radius] + draw.ellipse(bbox, fill=fill_color, outline=outline_color, width=outline_width) + return frame + + +def draw_text( + frame: Image.Image, + text: str, + position: tuple[int, int], + color: tuple[int, int, int] = (0, 0, 0), + centered: bool = False, +) -> Image.Image: + """ + Draw text on a frame. + + Args: + frame: PIL Image to draw on + text: Text to draw + position: (x, y) position (top-left unless centered=True) + color: RGB text color + centered: If True, center text at position + + Returns: + Modified frame + """ + draw = ImageDraw.Draw(frame) + + # Uses Pillow's default font. + # If the font should be changed for the emoji, add additional logic here. + font = ImageFont.load_default() + + if centered: + bbox = draw.textbbox((0, 0), text, font=font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + x = position[0] - text_width // 2 + y = position[1] - text_height // 2 + position = (x, y) + + draw.text(position, text, fill=color, font=font) + return frame + + +def create_gradient_background( + width: int, + height: int, + top_color: tuple[int, int, int], + bottom_color: tuple[int, int, int], +) -> Image.Image: + """ + Create a vertical gradient background. + + Args: + width: Frame width + height: Frame height + top_color: RGB color at top + bottom_color: RGB color at bottom + + Returns: + PIL Image with gradient + """ + frame = Image.new("RGB", (width, height)) + draw = ImageDraw.Draw(frame) + + # Calculate color step for each row + r1, g1, b1 = top_color + r2, g2, b2 = bottom_color + + for y in range(height): + # Interpolate color + ratio = y / height + r = int(r1 * (1 - ratio) + r2 * ratio) + g = int(g1 * (1 - ratio) + g2 * ratio) + b = int(b1 * (1 - ratio) + b2 * ratio) + + # Draw horizontal line + draw.line([(0, y), (width, y)], fill=(r, g, b)) + + return frame + + +def draw_star( + frame: Image.Image, + center: tuple[int, int], + size: int, + fill_color: tuple[int, int, int], + outline_color: Optional[tuple[int, int, int]] = None, + outline_width: int = 1, +) -> Image.Image: + """ + Draw a 5-pointed star. + + Args: + frame: PIL Image to draw on + center: (x, y) center position + size: Star size (outer radius) + fill_color: RGB fill color + outline_color: RGB outline color (None for no outline) + outline_width: Outline width + + Returns: + Modified frame + """ + import math + + draw = ImageDraw.Draw(frame) + x, y = center + + # Calculate star points + points = [] + for i in range(10): + angle = (i * 36 - 90) * math.pi / 180 # 36 degrees per point, start at top + radius = size if i % 2 == 0 else size * 0.4 # Alternate between outer and inner + px = x + radius * math.cos(angle) + py = y + radius * math.sin(angle) + points.append((px, py)) + + # Draw star + draw.polygon(points, fill=fill_color, outline=outline_color, width=outline_width) + + return frame diff --git a/.agents/skills/slack-gif-creator/core/gif_builder.py b/.agents/skills/slack-gif-creator/core/gif_builder.py new file mode 100755 index 00000000..5759f144 --- /dev/null +++ b/.agents/skills/slack-gif-creator/core/gif_builder.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +""" +GIF Builder - Core module for assembling frames into GIFs optimized for Slack. + +This module provides the main interface for creating GIFs from programmatically +generated frames, with automatic optimization for Slack's requirements. +""" + +from pathlib import Path +from typing import Optional + +import imageio.v3 as imageio +import numpy as np +from PIL import Image + + +class GIFBuilder: + """Builder for creating optimized GIFs from frames.""" + + def __init__(self, width: int = 480, height: int = 480, fps: int = 15): + """ + Initialize GIF builder. + + Args: + width: Frame width in pixels + height: Frame height in pixels + fps: Frames per second + """ + self.width = width + self.height = height + self.fps = fps + self.frames: list[np.ndarray] = [] + + def add_frame(self, frame: np.ndarray | Image.Image): + """ + Add a frame to the GIF. + + Args: + frame: Frame as numpy array or PIL Image (will be converted to RGB) + """ + if isinstance(frame, Image.Image): + frame = np.array(frame.convert("RGB")) + + # Ensure frame is correct size + if frame.shape[:2] != (self.height, self.width): + pil_frame = Image.fromarray(frame) + pil_frame = pil_frame.resize( + (self.width, self.height), Image.Resampling.LANCZOS + ) + frame = np.array(pil_frame) + + self.frames.append(frame) + + def add_frames(self, frames: list[np.ndarray | Image.Image]): + """Add multiple frames at once.""" + for frame in frames: + self.add_frame(frame) + + def optimize_colors( + self, num_colors: int = 128, use_global_palette: bool = True + ) -> list[np.ndarray]: + """ + Reduce colors in all frames using quantization. + + Args: + num_colors: Target number of colors (8-256) + use_global_palette: Use a single palette for all frames (better compression) + + Returns: + List of color-optimized frames + """ + optimized = [] + + if use_global_palette and len(self.frames) > 1: + # Create a global palette from all frames + # Sample frames to build palette + sample_size = min(5, len(self.frames)) + sample_indices = [ + int(i * len(self.frames) / sample_size) for i in range(sample_size) + ] + sample_frames = [self.frames[i] for i in sample_indices] + + # Combine sample frames into a single image for palette generation + # Flatten each frame to get all pixels, then stack them + all_pixels = np.vstack( + [f.reshape(-1, 3) for f in sample_frames] + ) # (total_pixels, 3) + + # Create a properly-shaped RGB image from the pixel data + # We'll make a roughly square image from all the pixels + total_pixels = len(all_pixels) + width = min(512, int(np.sqrt(total_pixels))) # Reasonable width, max 512 + height = (total_pixels + width - 1) // width # Ceiling division + + # Pad if necessary to fill the rectangle + pixels_needed = width * height + if pixels_needed > total_pixels: + padding = np.zeros((pixels_needed - total_pixels, 3), dtype=np.uint8) + all_pixels = np.vstack([all_pixels, padding]) + + # Reshape to proper RGB image format (H, W, 3) + img_array = ( + all_pixels[:pixels_needed].reshape(height, width, 3).astype(np.uint8) + ) + combined_img = Image.fromarray(img_array, mode="RGB") + + # Generate global palette + global_palette = combined_img.quantize(colors=num_colors, method=2) + + # Apply global palette to all frames + for frame in self.frames: + pil_frame = Image.fromarray(frame) + quantized = pil_frame.quantize(palette=global_palette, dither=1) + optimized.append(np.array(quantized.convert("RGB"))) + else: + # Use per-frame quantization + for frame in self.frames: + pil_frame = Image.fromarray(frame) + quantized = pil_frame.quantize(colors=num_colors, method=2, dither=1) + optimized.append(np.array(quantized.convert("RGB"))) + + return optimized + + def deduplicate_frames(self, threshold: float = 0.9995) -> int: + """ + Remove duplicate or near-duplicate consecutive frames. + + Args: + threshold: Similarity threshold (0.0-1.0). Higher = more strict (0.9995 = nearly identical). + Use 0.9995+ to preserve subtle animations, 0.98 for aggressive removal. + + Returns: + Number of frames removed + """ + if len(self.frames) < 2: + return 0 + + deduplicated = [self.frames[0]] + removed_count = 0 + + for i in range(1, len(self.frames)): + # Compare with previous frame + prev_frame = np.array(deduplicated[-1], dtype=np.float32) + curr_frame = np.array(self.frames[i], dtype=np.float32) + + # Calculate similarity (normalized) + diff = np.abs(prev_frame - curr_frame) + similarity = 1.0 - (np.mean(diff) / 255.0) + + # Keep frame if sufficiently different + # High threshold (0.9995+) means only remove nearly identical frames + if similarity < threshold: + deduplicated.append(self.frames[i]) + else: + removed_count += 1 + + self.frames = deduplicated + return removed_count + + def save( + self, + output_path: str | Path, + num_colors: int = 128, + optimize_for_emoji: bool = False, + remove_duplicates: bool = False, + ) -> dict: + """ + Save frames as optimized GIF for Slack. + + Args: + output_path: Where to save the GIF + num_colors: Number of colors to use (fewer = smaller file) + optimize_for_emoji: If True, optimize for emoji size (128x128, fewer colors) + remove_duplicates: If True, remove duplicate consecutive frames (opt-in) + + Returns: + Dictionary with file info (path, size, dimensions, frame_count) + """ + if not self.frames: + raise ValueError("No frames to save. Add frames with add_frame() first.") + + output_path = Path(output_path) + + # Remove duplicate frames to reduce file size + if remove_duplicates: + removed = self.deduplicate_frames(threshold=0.9995) + if removed > 0: + print( + f" Removed {removed} nearly identical frames (preserved subtle animations)" + ) + + # Optimize for emoji if requested + if optimize_for_emoji: + if self.width > 128 or self.height > 128: + print( + f" Resizing from {self.width}x{self.height} to 128x128 for emoji" + ) + self.width = 128 + self.height = 128 + # Resize all frames + resized_frames = [] + for frame in self.frames: + pil_frame = Image.fromarray(frame) + pil_frame = pil_frame.resize((128, 128), Image.Resampling.LANCZOS) + resized_frames.append(np.array(pil_frame)) + self.frames = resized_frames + num_colors = min(num_colors, 48) # More aggressive color limit for emoji + + # More aggressive FPS reduction for emoji + if len(self.frames) > 12: + print( + f" Reducing frames from {len(self.frames)} to ~12 for emoji size" + ) + # Keep every nth frame to get close to 12 frames + keep_every = max(1, len(self.frames) // 12) + self.frames = [ + self.frames[i] for i in range(0, len(self.frames), keep_every) + ] + + # Optimize colors with global palette + optimized_frames = self.optimize_colors(num_colors, use_global_palette=True) + + # Calculate frame duration in milliseconds + frame_duration = 1000 / self.fps + + # Save GIF + imageio.imwrite( + output_path, + optimized_frames, + duration=frame_duration, + loop=0, # Infinite loop + ) + + # Get file info + file_size_kb = output_path.stat().st_size / 1024 + file_size_mb = file_size_kb / 1024 + + info = { + "path": str(output_path), + "size_kb": file_size_kb, + "size_mb": file_size_mb, + "dimensions": f"{self.width}x{self.height}", + "frame_count": len(optimized_frames), + "fps": self.fps, + "duration_seconds": len(optimized_frames) / self.fps, + "colors": num_colors, + } + + # Print info + print(f"\n✓ GIF created successfully!") + print(f" Path: {output_path}") + print(f" Size: {file_size_kb:.1f} KB ({file_size_mb:.2f} MB)") + print(f" Dimensions: {self.width}x{self.height}") + print(f" Frames: {len(optimized_frames)} @ {self.fps} fps") + print(f" Duration: {info['duration_seconds']:.1f}s") + print(f" Colors: {num_colors}") + + # Size info + if optimize_for_emoji: + print(f" Optimized for emoji (128x128, reduced colors)") + if file_size_mb > 1.0: + print(f"\n Note: Large file size ({file_size_kb:.1f} KB)") + print(" Consider: fewer frames, smaller dimensions, or fewer colors") + + return info + + def clear(self): + """Clear all frames (useful for creating multiple GIFs).""" + self.frames = [] diff --git a/.agents/skills/slack-gif-creator/core/validators.py b/.agents/skills/slack-gif-creator/core/validators.py new file mode 100755 index 00000000..a6f5bdf2 --- /dev/null +++ b/.agents/skills/slack-gif-creator/core/validators.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Validators - Check if GIFs meet Slack's requirements. + +These validators help ensure your GIFs meet Slack's size and dimension constraints. +""" + +from pathlib import Path + + +def validate_gif( + gif_path: str | Path, is_emoji: bool = True, verbose: bool = True +) -> tuple[bool, dict]: + """ + Validate GIF for Slack (dimensions, size, frame count). + + Args: + gif_path: Path to GIF file + is_emoji: True for emoji (128x128 recommended), False for message GIF + verbose: Print validation details + + Returns: + Tuple of (passes: bool, results: dict with all details) + """ + from PIL import Image + + gif_path = Path(gif_path) + + if not gif_path.exists(): + return False, {"error": f"File not found: {gif_path}"} + + # Get file size + size_bytes = gif_path.stat().st_size + size_kb = size_bytes / 1024 + size_mb = size_kb / 1024 + + # Get dimensions and frame info + try: + with Image.open(gif_path) as img: + width, height = img.size + + # Count frames + frame_count = 0 + try: + while True: + img.seek(frame_count) + frame_count += 1 + except EOFError: + pass + + # Get duration + try: + duration_ms = img.info.get("duration", 100) + total_duration = (duration_ms * frame_count) / 1000 + fps = frame_count / total_duration if total_duration > 0 else 0 + except: + total_duration = None + fps = None + + except Exception as e: + return False, {"error": f"Failed to read GIF: {e}"} + + # Validate dimensions + if is_emoji: + optimal = width == height == 128 + acceptable = width == height and 64 <= width <= 128 + dim_pass = acceptable + else: + aspect_ratio = ( + max(width, height) / min(width, height) + if min(width, height) > 0 + else float("inf") + ) + dim_pass = aspect_ratio <= 2.0 and 320 <= min(width, height) <= 640 + + results = { + "file": str(gif_path), + "passes": dim_pass, + "width": width, + "height": height, + "size_kb": size_kb, + "size_mb": size_mb, + "frame_count": frame_count, + "duration_seconds": total_duration, + "fps": fps, + "is_emoji": is_emoji, + "optimal": optimal if is_emoji else None, + } + + # Print if verbose + if verbose: + print(f"\nValidating {gif_path.name}:") + print( + f" Dimensions: {width}x{height}" + + ( + f" ({'optimal' if optimal else 'acceptable'})" + if is_emoji and acceptable + else "" + ) + ) + print( + f" Size: {size_kb:.1f} KB" + + (f" ({size_mb:.2f} MB)" if size_mb >= 1.0 else "") + ) + print( + f" Frames: {frame_count}" + + (f" @ {fps:.1f} fps ({total_duration:.1f}s)" if fps else "") + ) + + if not dim_pass: + print( + f" Note: {'Emoji should be 128x128' if is_emoji else 'Unusual dimensions for Slack'}" + ) + + if size_mb > 5.0: + print(f" Note: Large file size - consider fewer frames/colors") + + return dim_pass, results + + +def is_slack_ready( + gif_path: str | Path, is_emoji: bool = True, verbose: bool = True +) -> bool: + """ + Quick check if GIF is ready for Slack. + + Args: + gif_path: Path to GIF file + is_emoji: True for emoji GIF, False for message GIF + verbose: Print feedback + + Returns: + True if dimensions are acceptable + """ + passes, _ = validate_gif(gif_path, is_emoji, verbose) + return passes diff --git a/.agents/skills/slack-gif-creator/requirements.txt b/.agents/skills/slack-gif-creator/requirements.txt new file mode 100644 index 00000000..8bc4493e --- /dev/null +++ b/.agents/skills/slack-gif-creator/requirements.txt @@ -0,0 +1,4 @@ +pillow>=10.0.0 +imageio>=2.31.0 +imageio-ffmpeg>=0.4.9 +numpy>=1.24.0 \ No newline at end of file diff --git a/.agents/skills/soul-guardian/.clawhubignore b/.agents/skills/soul-guardian/.clawhubignore new file mode 100644 index 00000000..d9492561 --- /dev/null +++ b/.agents/skills/soul-guardian/.clawhubignore @@ -0,0 +1,12 @@ +# Exclude local caches and build outputs from ClawHub upload +.DS_Store +.git/ +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ +.env +.venv/ +.cache/ diff --git a/.agents/skills/soul-guardian/CHANGELOG.md b/.agents/skills/soul-guardian/CHANGELOG.md new file mode 100644 index 00000000..18426ca5 --- /dev/null +++ b/.agents/skills/soul-guardian/CHANGELOG.md @@ -0,0 +1,57 @@ +# Changelog + +## [0.0.6] - 2026-05-14 + +### Security +- Added explicit signed release artifact verification instructions for standalone installs, including `checksums.json`, `checksums.sig`, `signing-public.pem`, archive hash verification, and `SKILL.md`/`skill.json` checksum checks. + +All notable changes to soul-guardian will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.0.5] - 2026-04-14 + +### Added + +- Regression coverage for launchd label migration so the installer documents and cleans up the previous Clawdbot-era label before starting the new default label. + +### Changed + +- `scripts/install_launchd_plist.py` now documents the legacy launchd label/plist in dry-run output and attempts a best-effort disable/bootout of `com.clawdbot.soul-guardian.` before installing `com.openclaw.soul-guardian.`. +- The `--label` help now explains that non-legacy labels trigger legacy-job cleanup, while explicitly selecting the legacy label skips that migration path. + +### Security + +- Reduced the chance of duplicate launchd jobs or split monitoring state by making the old-label cleanup path explicit and warning the operator when manual launchd cleanup is still required. + +## [0.0.4] - 2026-04-14 + +### Added + +- Regression coverage for launchd state-directory selection so existing legacy installs keep using their current guardian state unless the operator explicitly chooses a new location. + +### Changed + +- `scripts/install_launchd_plist.py` now reuses `~/.clawdbot/soul-guardian//` when that legacy state directory already exists and otherwise keeps the new `~/.openclaw/...` default. +- The launchd installer now prints an explicit migration warning with the `--state-dir` value to use when switching an existing install to the new OpenClaw path. + +### Security + +- Prevented silent state-directory drift for existing launchd-based installs that would otherwise create a second guardian state tree and lose visibility into the approved baselines they were already enforcing. + +## [0.0.3] - 2026-04-14 + +### Added + +- Operational notes that describe restore behavior, state-directory sensitivity, and optional scheduling integrations. +- Metadata for persistence, network posture, and operator review expectations. + +### Changed + +- Declared optional integration runtimes used by the documented workflows (`openclaw`, `launchctl`, `bash`) alongside the required `python3` runtime. +- Normalized the documented product/runtime naming to OpenClaw, including cron examples, default external state paths, and launchd labels. + +### Security + +- Made it explicit that restore mode can overwrite protected files back to baseline and that guardian state directories may contain sensitive snapshots, diffs, and quarantined content. diff --git a/.agents/skills/soul-guardian/README.md b/.agents/skills/soul-guardian/README.md new file mode 100644 index 00000000..0d8cd9f4 --- /dev/null +++ b/.agents/skills/soul-guardian/README.md @@ -0,0 +1,251 @@ +# soul-guardian + +A small, dependency-free integrity guard for OpenClaw agent workspaces. + +## Operational Notes + +- Required runtime: `python3` +- Optional runtime: `openclaw` for cron integration, `launchctl` for macOS scheduling +- Side effects: can restore protected files to approved baselines and stores sensitive snapshots/audit data in the guardian state directory +- Network behavior: none by default +- Any cron/launchd scheduling is opt-in and should be reviewed before enabling + +It helps you detect (and optionally auto-undo) unexpected edits to the workspace markdown files that an agent auto-loads (e.g., `SOUL.md`, `AGENTS.md`). It also records a **tamper-evident** audit trail of changes. + +## Why this exists + +In many OpenClaw setups, the agent reads certain markdown files every session (identity, instructions, memory, tools, etc.). If those files drift unexpectedly (accidental edits, bad merges, unwanted automation, etc.), you want: + +- detection (sha256 mismatch) +- a diff/patch artifact for review +- a record of what happened (audit log) +- optionally: an automatic restore to a known-good baseline for critical files + +## What it protects (default policy) + +Default `policy.json` protects: + +- **Auto-restore + alert:** `SOUL.md`, `AGENTS.md` +- **Alert-only:** `USER.md`, `TOOLS.md`, `IDENTITY.md`, `HEARTBEAT.md`, `MEMORY.md` +- **Ignored by default:** `memory/*.md` (daily notes) + +You can customize this by editing the policy file in the guardian state directory. + +## Security model (and limitations) + +What it does well: +- Detects filesystem drift vs an approved baseline. +- Produces unified diffs (patch files) for review. +- Maintains an **append-only JSONL audit log** with **hash chaining** so log tampering is detectable. +- Refuses to operate on **symlinks** (reduces link attacks). +- Uses **atomic writes** for restores and baseline updates (`os.replace`). + +What it does *not* do: +- It cannot prove *who* changed a file. `--actor` is best-effort metadata. +- It cannot protect you if an attacker can modify both the workspace and the guardian state directory. +- It is not a substitute for backups. + +Recommendation (not enforced): +- Mirror/back up your guardian state directory (and/or workspace) using git and/or offsite backups. + +## State directory + +By default, state is stored inside the workspace: + +- `memory/soul-guardian/` + - `policy.json` (what to monitor) + - `baselines.json` (approved sha256 per file) + - `approved/` (approved snapshots) + - `audit.jsonl` (append-only log with hash chain) + - `patches/*.patch` (unified diffs) + - `quarantine/*` (copies of drifted files before restore) + +For better resilience, you can move this **outside** the workspace (recommended). + +## Install / usage + +From the agent workspace root. + +### First run / Initialize baselines (recommended) + +For resilience, create your guardian **state directory outside** the workspace first, then initialize baselines. + +1) Onboard an external state dir (creates policy, copies any existing state, prints paths/snippets): + +```bash +python3 skills/soul-guardian/scripts/onboard_state_dir.py --agent-id +``` + +2) Initialize baselines **in that external state dir**: + +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py \ + --state-dir ~/.openclaw/soul-guardian/ \ + init --actor sam --note "first baseline" +``` + +3) Run a check once (should be silent on OK; prints a single-line summary on drift): + +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py \ + --state-dir ~/.openclaw/soul-guardian/ \ + check --actor system --note "first check" +``` + +### Common commands + +Status (summary): + +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py \ + --state-dir ~/.openclaw/soul-guardian/ \ + status +``` + +Check for drift (default: restores restore-mode files): + +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py \ + --state-dir ~/.openclaw/soul-guardian/ \ + check --actor system --note cron +``` + +Alert-only check (never restore): + +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py \ + --state-dir ~/.openclaw/soul-guardian/ \ + check --no-restore +``` + +Approve intentional edits (one file): + +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py \ + --state-dir ~/.openclaw/soul-guardian/ \ + approve --file SOUL.md --actor sam --note "intentional update" +``` + +Approve all policy targets (except ignored ones): + +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py \ + --state-dir ~/.openclaw/soul-guardian/ \ + approve --all --actor sam --note "bulk approve" +``` + +Restore (only restore-mode files): + +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py \ + --state-dir ~/.openclaw/soul-guardian/ \ + restore --file SOUL.md --actor system --note "manual restore" +``` + +Verify audit log tamper-evidence: + +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py \ + --state-dir ~/.openclaw/soul-guardian/ \ + verify-audit +``` + +## Policy format (`policy.json`) + +Example: + +```json +{ + "version": 1, + "workspaceRoot": "/path/to/workspace", + "targets": [ + {"path": "SOUL.md", "mode": "restore"}, + {"path": "AGENTS.md", "mode": "restore"}, + {"path": "USER.md", "mode": "alert"}, + {"pattern": "memory/*.md", "mode": "ignore"} + ] +} +``` + +- `mode`: + - `restore`: drift triggers audit + patch + (by default) restore + quarantine copy + - `alert`: drift triggers audit + patch, but does not restore + - `ignore`: excluded + +## Onboarding: move state outside the workspace + +Run the helper: + +```bash +python3 skills/soul-guardian/scripts/onboard_state_dir.py +``` + +It will: +- create an external state dir (**recommended default:** `~/.openclaw/soul-guardian//`) +- copy (or move with `--move`) existing state from `memory/soul-guardian/` +- write a default `policy.json` if missing +- print scheduling snippets + +Notes: +- `` should be **stable and unique per workspace** (don’t point multiple workspaces at the same state dir). +- WARNING: `--move` deletes the old in-workspace state dir after copying. +- The external state dir can contain **approved snapshots, patches, and quarantined copies** of sensitive prompt/instruction/memory files. Keep permissions restrictive (e.g., `chmod 700 `; `chmod go-rwx `). + +Then include `--state-dir` in all commands (run from the workspace root), e.g.: + +```bash +cd && python3 skills/soul-guardian/scripts/soul_guardian.py --state-dir ~/.openclaw/soul-guardian/ check +``` + +## Scheduling (cron) + +### A) OpenClaw Cron (recommended) + +This is the default pattern when you want drift notifications to flow through OpenClaw. + +Note: even when there is **no drift**, OpenClaw cron runs typically show an **OK summary** in the main session. + +Example (edit paths + schedule): + +```bash +openclaw cron add \ + --name "soul-guardian: check workspace" \ + --description "Run soul-guardian check; alert when drift detected." \ + --session isolated \ + --wake now \ + --cron "*/10 * * * *" \ + --tz UTC \ + --message "Run:\ncd ''\npython3 skills/soul-guardian/scripts/soul_guardian.py --state-dir ~/.openclaw/soul-guardian/ check --actor cron --note 'gateway-cron'\n\nIf the command prints a line starting with 'SOUL_GUARDIAN_DRIFT', treat it as an alert. If it prints nothing, reply HEARTBEAT_OK." \ + --post-prefix "[soul-guardian]" \ + --post-mode summary +``` + +### B) macOS launchd (optional, silent-on-OK) + +If you want **system scheduling** without OpenClaw posting OK summaries, use `launchd`. + +Because `soul_guardian.py check` prints **nothing** on OK and prints a single-line `SOUL_GUARDIAN_DRIFT ...` summary on drift, this tends to be silent unless something changed. + +Generate + (optionally) install a LaunchAgent plist (run from the workspace root, or pass `--workspace-root`): + +```bash +python3 skills/soul-guardian/scripts/install_launchd_plist.py \ + --state-dir ~/.openclaw/soul-guardian/ \ + --interval-seconds 600 \ + --install +``` + +The generated plist includes `WorkingDirectory` set to your workspace root (recommended), so relative paths behave as expected. + +The script writes drift output to log files under `/logs/`. +You can tail them with the commands it prints. + +## Development / tests + +A minimal test script is included: + +```bash +python3 skills/soul-guardian/scripts/test_soul_guardian.py +``` + +It simulates a workspace in a temp directory and validates drift detection, approve/restore flow, and audit hash chain verification. diff --git a/.agents/skills/soul-guardian/SKILL.md b/.agents/skills/soul-guardian/SKILL.md new file mode 100644 index 00000000..5478a63c --- /dev/null +++ b/.agents/skills/soul-guardian/SKILL.md @@ -0,0 +1,251 @@ +--- +name: soul-guardian +version: 0.0.6 +description: Drift detection + baseline integrity guard for agent workspace files with automatic alerting support +homepage: https://clawsec.prompt.security +metadata: {"openclaw":{"emoji":"👻","category":"security"}} +clawdis: + emoji: "👻" + requires: + bins: [python3] +--- + +# soul-guardian 👻 + +Protects your agent's core files (SOUL.md, AGENTS.md, etc.) from unauthorized changes with automatic detection, restoration, and **user alerting**. + +## Operational Notes + +- Required runtime: `python3` +- Optional runtime: `openclaw` for cron integration, `launchctl` for macOS scheduling, `bash` for the demo helper +- Side effects: can auto-restore protected files to their approved baseline and writes audit/quarantine state locally +- Network behavior: none by default +- Trust model: any scheduling is opt-in, but restore mode intentionally overwrites drifted files + + +## Release Artifact Verification + +For standalone installs, verify the signed release manifest before trusting `SKILL.md`, `skill.json`, or the archive. The `skill.json` file is the package metadata/SBOM source, and the release pipeline signs `checksums.json` with the ClawSec release key. + +```bash +set -euo pipefail + +SKILL_NAME="soul-guardian" +VERSION="0.0.6" +REPO="prompt-security/clawsec" +TAG="${SKILL_NAME}-v${VERSION}" +BASE="https://github.com/${REPO}/releases/download/${TAG}" +ZIP_NAME="${SKILL_NAME}-v${VERSION}.zip" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +RELEASE_PUBKEY_SHA256="711424e4535f84093fefb024cd1ca4ec87439e53907b305b79a631d5befba9c8" + +curl -fsSL "$BASE/checksums.json" -o "$TMP_DIR/checksums.json" +curl -fsSL "$BASE/checksums.sig" -o "$TMP_DIR/checksums.sig" +curl -fsSL "$BASE/signing-public.pem" -o "$TMP_DIR/signing-public.pem" +curl -fsSL "$BASE/$ZIP_NAME" -o "$TMP_DIR/$ZIP_NAME" +curl -fsSL "$BASE/SKILL.md" -o "$TMP_DIR/SKILL.md" +curl -fsSL "$BASE/skill.json" -o "$TMP_DIR/skill.json" + +ACTUAL_PUBKEY_SHA256="$(openssl pkey -pubin -in "$TMP_DIR/signing-public.pem" -outform DER | shasum -a 256 | awk '{print $1}')" +if [ "$ACTUAL_PUBKEY_SHA256" != "$RELEASE_PUBKEY_SHA256" ]; then + echo "ERROR: signing-public.pem fingerprint mismatch" >&2 + exit 1 +fi + +openssl base64 -d -A -in "$TMP_DIR/checksums.sig" -out "$TMP_DIR/checksums.sig.bin" +openssl pkeyutl -verify -rawin -pubin \ + -inkey "$TMP_DIR/signing-public.pem" \ + -sigfile "$TMP_DIR/checksums.sig.bin" \ + -in "$TMP_DIR/checksums.json" >/dev/null + +hash_file() { + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + sha256sum "$1" | awk '{print $1}' + fi +} + +verify_manifest_file() { + asset="$1" + path="$2" + expected="$(jq -r --arg asset "$asset" '.files[$asset].sha256 // empty' "$TMP_DIR/checksums.json")" + if [ -z "$expected" ]; then + echo "ERROR: checksums.json missing $asset" >&2 + exit 1 + fi + actual="$(hash_file "$path")" + if [ "$actual" != "$expected" ]; then + echo "ERROR: checksum mismatch for $asset" >&2 + exit 1 + fi +} + +expected_archive="$(jq -r '.archive.sha256 // empty' "$TMP_DIR/checksums.json")" +if [ -z "$expected_archive" ]; then + echo "ERROR: checksums.json missing archive.sha256" >&2 + exit 1 +fi +actual_archive="$(hash_file "$TMP_DIR/$ZIP_NAME")" +if [ "$actual_archive" != "$expected_archive" ]; then + echo "ERROR: archive checksum mismatch" >&2 + exit 1 +fi + +verify_manifest_file "SKILL.md" "$TMP_DIR/SKILL.md" +verify_manifest_file "skill.json" "$TMP_DIR/skill.json" + +echo "Signed release manifest, archive, SKILL.md, and skill.json verified." +``` + +Only install or extract the archive after this verification succeeds. + +## Quick Start (3 Steps) + +### Step 1: Initialize baselines +```bash +cd ~/.openclaw/workspace +python3 skills/soul-guardian/scripts/soul_guardian.py init --actor setup --note "initial baseline" +``` + +### Step 2: Enable monitoring +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py enable-monitoring +``` +This shows you how to add monitoring to your workflow. + +### Step 3: Add to HEARTBEAT.md (Recommended) + +Add this to your HEARTBEAT.md for automatic monitoring: + +```markdown +## Soul Guardian Check +- Run `python3 skills/soul-guardian/scripts/soul_guardian.py check --actor heartbeat --output-format alert` +- If any output is produced, relay it to the user immediately as a security alert +``` + +That's it! Soul Guardian will now: +- ✅ Detect unauthorized changes to protected files +- ✅ Auto-restore SOUL.md and AGENTS.md to approved baseline +- ✅ Alert you when drift is detected and handled + +--- + +## What it protects (default policy) + +| File | Mode | Action on drift | +|------|------|-----------------| +| SOUL.md | restore | Auto-restore + alert | +| AGENTS.md | restore | Auto-restore + alert | +| USER.md | alert | Alert only | +| TOOLS.md | alert | Alert only | +| IDENTITY.md | alert | Alert only | +| HEARTBEAT.md | alert | Alert only | +| MEMORY.md | alert | Alert only | +| memory/*.md | ignore | Ignored | + +## Commands + +### Check for drift (with alert output) +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py check --output-format alert +``` +- Silent if no drift +- Outputs human-readable alert if drift detected +- Perfect for heartbeat integration + +### Watch mode (continuous monitoring) +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py watch --interval 30 +``` +Runs continuously, checking every 30 seconds. + +### Approve intentional changes +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py approve --file SOUL.md --actor user --note "intentional update" +``` + +### View status +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py status +``` + +### Verify audit log integrity +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py verify-audit +``` + +--- + +## Alert Format + +When drift is detected, the `--output-format alert` produces output like: + +``` +================================================== +🚨 SOUL GUARDIAN SECURITY ALERT +================================================== + +📄 FILE: SOUL.md + Mode: restore + Status: ✅ RESTORED to approved baseline + Expected hash: abc123def456... + Found hash: 789xyz000111... + Diff saved: /path/to/patches/drift.patch + +================================================== +Review changes and investigate the source of drift. +If intentional, run: soul_guardian.py approve --file +================================================== +``` + +This output is designed to be relayed directly to the user in TUI/chat. + +--- + +## Security Model + +**What it does:** +- Detects filesystem drift vs approved baseline (sha256) +- Produces unified diffs for review +- Maintains tamper-evident audit log with hash chaining +- Refuses to operate on symlinks +- Uses atomic writes for restores + +**What it doesn't do:** +- Cannot prove WHO made a change (actor is best-effort metadata) +- Cannot protect if attacker controls both workspace AND state directory +- Is not a substitute for backups + +**Recommendation:** Store state directory outside workspace for better resilience. + +--- + +## Demo + +Run the full demo flow to see soul-guardian in action: + +```bash +bash skills/soul-guardian/scripts/demo.sh +``` + +This will: +1. Verify clean state (silent check) +2. Inject malicious content into SOUL.md +3. Run heartbeat check (produces alert) +4. Show SOUL.md was restored + +--- + +## Troubleshooting + +**"Not initialized" error:** +Run `init` first to set up baselines. + +**Drift keeps happening:** +Check what's modifying your files. Review the audit log and patches. + +**Want to approve a change:** +Run `approve --file ` after reviewing the change. diff --git a/.agents/skills/soul-guardian/scripts/install_launchd_plist.py b/.agents/skills/soul-guardian/scripts/install_launchd_plist.py new file mode 100644 index 00000000..82bbaf72 --- /dev/null +++ b/.agents/skills/soul-guardian/scripts/install_launchd_plist.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +"""Generate (and optionally install) a macOS launchd plist for soul-guardian. + +Goal: +- Run `soul_guardian.py check` on an interval. +- Be *silent on OK* (soul_guardian.py prints nothing + exits 0 when no drift). +- Produce a single-line stdout alert on drift (exits 2 and prints SOUL_GUARDIAN_DRIFT ...). + +This script is intentionally deterministic and dependency-free. + +It does NOT attempt to deliver drift alerts to Telegram/Slack/etc. +Instead it: +- writes logs to the state dir (so drift output is preserved) +- relies on you to wire notifications however you prefer + +If you want OpenClaw-side delivery, use OpenClaw cron. +""" + +from __future__ import annotations + +import argparse +import os +from pathlib import Path +import plistlib +import subprocess +import sys + + +LEGACY_STATE_ROOT = Path("~/.clawdbot/soul-guardian").expanduser() +DEFAULT_STATE_ROOT = Path("~/.openclaw/soul-guardian").expanduser() +LEGACY_LABEL_PREFIX = "com.clawdbot.soul-guardian." +DEFAULT_LABEL_PREFIX = "com.openclaw.soul-guardian." + + +def agent_id_default(workspace_root: Path) -> str: + return workspace_root.name + + +def legacy_label(agent_id: str) -> str: + return f"{LEGACY_LABEL_PREFIX}{agent_id}" + + +def default_label(agent_id: str) -> str: + return f"{DEFAULT_LABEL_PREFIX}{agent_id}" + + +def legacy_plist_path(agent_id: str) -> Path: + return Path("~/Library/LaunchAgents").expanduser() / f"{legacy_label(agent_id)}.plist" + + +def default_external_state_dir(agent_id: str) -> tuple[Path, bool]: + legacy_state_dir = LEGACY_STATE_ROOT / agent_id + if legacy_state_dir.exists(): + return legacy_state_dir, True + return DEFAULT_STATE_ROOT / agent_id, False + + +def run_launchctl(args: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run(["/bin/launchctl", *args], check=False, text=True, capture_output=True) + + +def cleanup_legacy_launchd(uid: int, active_label: str, agent_id: str) -> list[str]: + legacy_job_label = legacy_label(agent_id) + legacy_job_plist = legacy_plist_path(agent_id).expanduser().resolve() + if active_label == legacy_job_label: + return [] + + cleanup_commands: list[tuple[list[str], str]] = [ + ( + ["disable", f"gui/{uid}/{legacy_job_label}"], + f"launchctl disable gui/{uid}/{legacy_job_label}", + ), + ( + ["bootout", f"gui/{uid}/{legacy_job_label}"], + f"launchctl bootout gui/{uid}/{legacy_job_label}", + ), + ] + + if legacy_job_plist.exists(): + cleanup_commands.append( + ( + ["bootout", f"gui/{uid}", str(legacy_job_plist)], + f"launchctl bootout gui/{uid} {legacy_job_plist}", + ) + ) + + failed_commands: list[str] = [] + for args, display_cmd in cleanup_commands: + cp = run_launchctl(args) + if cp.returncode != 0 and legacy_job_plist.exists(): + failed_commands.append(display_cmd) + + if not failed_commands: + return [] + + warning_lines = [ + "WARNING: Failed to fully clean up the legacy soul-guardian launchd job " + f"{legacy_job_label}.", + f"Manually run: launchctl bootout gui/{uid} {legacy_job_label}", + ] + if legacy_job_plist.exists(): + warning_lines.append(f"If needed, also remove the legacy plist: {legacy_job_plist}") + warning_lines.append("You can rerun this installer after the legacy job is removed.") + return warning_lines + + +def main(argv: list[str]) -> int: + ap = argparse.ArgumentParser() + ap.add_argument( + "--workspace-root", + default=str(Path.cwd()), + help="Workspace root (default: current working directory).", + ) + ap.add_argument( + "--agent-id", + default=None, + help="Agent/workspace identifier used in default label + state dir (default: workspace folder name).", + ) + ap.add_argument( + "--state-dir", + default=None, + help="External state directory (recommended). Default: ~/.openclaw/soul-guardian//; reuses ~/.clawdbot/soul-guardian// if that legacy state dir already exists.", + ) + ap.add_argument( + "--label", + default=None, + help="launchd label (default: com.openclaw.soul-guardian.). When using a non-legacy label, --install attempts to disable/boot out the previous com.clawdbot.soul-guardian. job first.", + ) + ap.add_argument( + "--interval-seconds", + type=int, + default=600, + help="Run interval in seconds (StartInterval). Default: 600 (10 minutes).", + ) + ap.add_argument("--actor", default="cron", help="--actor passed to soul_guardian.py (default: cron).") + ap.add_argument("--note", default="launchd", help="--note passed to soul_guardian.py (default: launchd).") + ap.add_argument( + "--out", + default=None, + help="Write plist to this path (default: ~/Library/LaunchAgents/