"use strict";(globalThis.webpackChunkproject_public_docs=globalThis.webpackChunkproject_public_docs||[]).push([[887],{4884(e,n,s){s.r(n),s.d(n,{assets:()=>o,contentTitle:()=>t,default:()=>h,frontMatter:()=>d,metadata:()=>i,toc:()=>c});const i=JSON.parse('{"id":"reference/registry-spec","title":"CmdForge Registry Design","description":"Purpose","source":"@site/docs/reference/registry-spec.md","sourceDirName":"reference","slug":"/reference/registry-spec","permalink":"/rob/CmdForge/reference/registry-spec","draft":false,"unlisted":false,"tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"sidebar_label":"Registry API","sidebar_position":1,"format":"md"},"sidebar":"docs","previous":{"title":"Provider Setup","permalink":"/rob/CmdForge/reference/providers"},"next":{"title":"Meta-Tools","permalink":"/rob/CmdForge/reference/meta-tools"}}');var r=s(4848),l=s(8453);const d={sidebar_label:"Registry API",sidebar_position:1,format:"md"},t="CmdForge Registry Design",o={},c=[{value:"Purpose",id:"purpose",level:2},{value:"Terminology",id:"terminology",level:2},{value:"Diagram References",id:"diagram-references",level:2},{value:"System Overview",id:"system-overview",level:2},{value:"Pagination",id:"pagination",level:3},{value:"Input Constraints",id:"input-constraints",level:3},{value:"Sort Fields and Indexes",id:"sort-fields-and-indexes",level:3},{value:"Tags Endpoint",id:"tags-endpoint",level:3},{value:"Advanced Search",id:"advanced-search",level:3},{value:"API Version Compatibility",id:"api-version-compatibility",level:3},{value:"Source of Truth",id:"source-of-truth",level:2},{value:"Namespacing and Paths",id:"namespacing-and-paths",level:2},{value:"Namespace Identity",id:"namespace-identity",level:3},{value:"Tool Format (Registry == Local)",id:"tool-format-registry--local",level:2},{value:"Attribution and Source Fields",id:"attribution-and-source-fields",level:3},{value:"Collections",id:"collections",level:2},{value:"Collection Structure",id:"collection-structure",level:3},{value:"Collections API",id:"collections-api",level:3},{value:"CLI Commands",id:"cli-commands",level:3},{value:"Admin Collections API",id:"admin-collections-api",level:3},{value:"Versioning and Immutability",id:"versioning-and-immutability",level:2},{value:"Yank Policy",id:"yank-policy",level:3},{value:"Version Format",id:"version-format",level:3},{value:"Version Constraints",id:"version-constraints",level:3},{value:"Version Resolution Rules",id:"version-resolution-rules",level:3},{value:"Prerelease Handling",id:"prerelease-handling",level:3},{value:"Download Endpoint Version Selection",id:"download-endpoint-version-selection",level:3},{value:"Tool Resolution Order",id:"tool-resolution-order",level:2},{value:"Official Namespace",id:"official-namespace",level:3},{value:"Auto-Fetch Behavior",id:"auto-fetch-behavior",level:2},{value:"Wrapper Script Collisions",id:"wrapper-script-collisions",level:3},{value:"Project Manifest (cmdforge.yaml)",id:"project-manifest-cmdforgeyaml",level:2},{value:"CLI Config and Tokens",id:"cli-config-and-tokens",level:2},{value:"Publishing and Auth",id:"publishing-and-auth",level:2},{value:"Publish Idempotency and Edge Cases",id:"publish-idempotency-and-edge-cases",level:3},{value:"Publisher Registration",id:"publisher-registration",level:2},{value:"Authentication Security",id:"authentication-security",level:3},{value:"Token Scopes and Authorization",id:"token-scopes-and-authorization",level:3},{value:"Web Session Security",id:"web-session-security",level:3},{value:"CLI Commands Reference",id:"cli-commands-reference",level:2},{value:"Registry Commands",id:"registry-commands",level:3},{value:"Project Commands",id:"project-commands",level:3},{value:"Config Commands",id:"config-commands",level:3},{value:"Flags available on most commands",id:"flags-available-on-most-commands",level:3},{value:"Publish State Tracking",id:"publish-state-tracking",level:2},{value:"Local State Storage",id:"local-state-storage",level:3},{value:"Visual Indicators",id:"visual-indicators",level:3},{value:"Automatic Status Sync",id:"automatic-status-sync",level:3},{value:"Manual Sync",id:"manual-sync",level:3},{value:"Hash Computation",id:"hash-computation",level:3},{value:"API Endpoint",id:"api-endpoint",level:3},{value:"Webhooks and Security",id:"webhooks-and-security",level:2},{value:"HMAC Verification",id:"hmac-verification",level:3},{value:"Replay Protection",id:"replay-protection",level:3},{value:"Sync Job Locking",id:"sync-job-locking",level:3},{value:"Atomic Sync Strategy",id:"atomic-sync-strategy",level:3},{value:"Error Handling",id:"error-handling",level:3},{value:"Automated CI Validation",id:"automated-ci-validation",level:2},{value:"Registry Repository Structure",id:"registry-repository-structure",level:2},{value:"Download Stats",id:"download-stats",level:2},{value:"Counting Methodology",id:"counting-methodology",level:3},{value:"Client ID Generation",id:"client-id-generation",level:3},{value:"Privacy Considerations",id:"privacy-considerations",level:3},{value:"Async Stats Strategy",id:"async-stats-strategy",level:3},{value:"Search",id:"search",level:2},{value:"API Caching Strategy",id:"api-caching-strategy",level:2},{value:"Cache Headers",id:"cache-headers",level:3},{value:"ETag Implementation",id:"etag-implementation",level:3},{value:"DB vs Repo Read Strategy",id:"db-vs-repo-read-strategy",level:3},{value:"Staleness Detection",id:"staleness-detection",level:3},{value:"Error Model",id:"error-model",level:2},{value:"Response Envelopes",id:"response-envelopes",level:3},{value:"Error Codes",id:"error-codes",level:3},{value:"Error Scenarios and Fallbacks",id:"error-scenarios-and-fallbacks",level:2},{value:"CLI Error Handling",id:"cli-error-handling",level:3},{value:"Validation Failure Details",id:"validation-failure-details",level:3},{value:"Dependency Resolution Failures",id:"dependency-resolution-failures",level:3},{value:"Graceful Degradation",id:"graceful-degradation",level:3},{value:"UX Requirements (CLI/TUI)",id:"ux-requirements-clitui",level:2},{value:"Publishing UX",id:"publishing-ux",level:3},{value:"Progress Indicators",id:"progress-indicators",level:3},{value:"TUI Browse",id:"tui-browse",level:3},{value:"Project Initialization",id:"project-initialization",level:3},{value:"Accessibility",id:"accessibility",level:3},{value:"Offline Cache",id:"offline-cache",level:2},{value:"Index Integrity",id:"index-integrity",level:3},{value:"Web UI Vision",id:"web-ui-vision",level:2},{value:"README Security",id:"readme-security",level:3},{value:"Registry Curation System",id:"registry-curation-system",level:2},{value:"Roles and Permissions",id:"roles-and-permissions",level:3},{value:"Tool Visibility",id:"tool-visibility",level:3},{value:"Moderation Workflow",id:"moderation-workflow",level:3},{value:"Admin API Endpoints",id:"admin-api-endpoints",level:3},{value:"Ban Behavior",id:"ban-behavior",level:3},{value:"Report Resolution Actions",id:"report-resolution-actions",level:3},{value:"Audit Log",id:"audit-log",level:3},{value:"Web UI Admin Dashboard",id:"web-ui-admin-dashboard",level:3},{value:"Pending Tools Review Page",id:"pending-tools-review-page",level:4},{value:"Creating the First Admin",id:"creating-the-first-admin",level:3},{value:"Implementation Phases",id:"implementation-phases",level:2},{value:"Phase 1: Foundation",id:"phase-1-foundation",level:3},{value:"Phase 2: Core Backend",id:"phase-2-core-backend",level:3},{value:"Phase 3: CLI Commands",id:"phase-3-cli-commands",level:3},{value:"Phase 4: Publishing",id:"phase-4-publishing",level:3},{value:"Phase 5: Project Dependencies",id:"phase-5-project-dependencies",level:3},{value:"Phase 6: Smart Features",id:"phase-6-smart-features",level:3},{value:"Phase 7: Full Web UI",id:"phase-7-full-web-ui",level:3},{value:"Phase 8: Polish & Scale",id:"phase-8-polish--scale",level:3}];function a(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,l.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.header,{children:(0,r.jsx)(n.h1,{id:"cmdforge-registry-design",children:"CmdForge Registry Design"})}),"\n",(0,r.jsx)(n.h2,{id:"purpose",children:"Purpose"}),"\n",(0,r.jsx)(n.p,{children:"Build a centralized registry for CmdForge to enable discovery, publishing, dependency management, and future curation at scale."}),"\n",(0,r.jsx)(n.h2,{id:"terminology",children:"Terminology"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Term"}),(0,r.jsx)(n.th,{children:"Definition"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Tool definition"})}),(0,r.jsxs)(n.td,{children:["The full YAML file in the registry (",(0,r.jsx)(n.code,{children:"config.yaml"}),") containing name, steps, arguments, etc."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Tool config"})}),(0,r.jsx)(n.td,{children:"The configuration within a tool definition (arguments, steps, provider settings)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"cmdforge.yaml"})}),(0,r.jsx)(n.td,{children:"Project manifest file declaring tool dependencies and overrides"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"config.yaml"})}),(0,r.jsx)(n.td,{children:"The tool definition file, both in registry and when installed locally"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Owner"})}),(0,r.jsxs)(n.td,{children:["Immutable namespace slug identifying the publisher (e.g., ",(0,r.jsx)(n.code,{children:"rob"}),", ",(0,r.jsx)(n.code,{children:"alice"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Publisher"})}),(0,r.jsx)(n.td,{children:"A registered user who can publish tools to the registry"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.strong,{children:"Wrapper script"})}),(0,r.jsxs)(n.td,{children:["Auto-generated bash script in ",(0,r.jsx)(n.code,{children:"~/.local/bin/"})," that invokes a tool"]})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Canonical naming:"})," Use ",(0,r.jsx)(n.code,{children:"CmdForge-Registry"})," (capitalized, hyphenated) for the repository name."]}),"\n",(0,r.jsx)(n.h2,{id:"diagram-references",children:"Diagram References"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["System overview: ",(0,r.jsx)(n.code,{children:"discussions/diagrams/cmdforge-registry_rob_1.puml"})]}),"\n",(0,r.jsxs)(n.li,{children:["Data flows: ",(0,r.jsx)(n.code,{children:"discussions/diagrams/cmdforge-registry_rob_5.puml"})]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"system-overview",children:"System Overview"}),"\n",(0,r.jsxs)(n.p,{children:["Users interact via the CLI and a future Web UI. Both call a Registry API hosted at ",(0,r.jsx)(n.code,{children:"https://cmdforge.brrd.tech/api/v1"})," (future alias: ",(0,r.jsx)(n.code,{children:"cmdforge.brrd.tech/api/v1"}),"). The API syncs from a Gitea-backed registry repo and maintains a SQLite cache/search index."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Canonical API base path:"})," ",(0,r.jsx)(n.code,{children:"https://cmdforge.brrd.tech/api/v1"})]}),"\n",(0,r.jsxs)(n.p,{children:["All API endpoints are versioned under ",(0,r.jsx)(n.code,{children:"/api/v1"}),". When breaking changes are needed, a new version (",(0,r.jsx)(n.code,{children:"/api/v2"}),") will be introduced with deprecation notices."]}),"\n",(0,r.jsx)(n.p,{children:"Core API endpoints:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/tools"})}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"GET /api/v1/tools/search?q=..."})," (with advanced filtering)"]}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/tools/{owner}/{name}"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/tools/{owner}/{name}/versions"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/tools/{owner}/{name}/download?version=..."})}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"POST /api/v1/tools"})," (publish)"]}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/categories"})}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"GET /api/v1/tags"})," (list all tags with counts)"]}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/collections"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/collections/{name}"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/stats/popular"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"POST /api/v1/webhook/gitea"})}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"pagination",children:"Pagination"}),"\n",(0,r.jsx)(n.p,{children:"All list endpoints support pagination:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Parameter"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Max"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"page"})}),(0,r.jsx)(n.td,{children:"1"}),(0,r.jsx)(n.td,{children:"-"}),(0,r.jsx)(n.td,{children:"Page number (1-indexed)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"per_page"})}),(0,r.jsx)(n.td,{children:"20"}),(0,r.jsx)(n.td,{children:"100"}),(0,r.jsx)(n.td,{children:"Items per page"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"sort"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"downloads"})}),(0,r.jsx)(n.td,{children:"-"}),(0,r.jsx)(n.td,{children:"Sort field"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"order"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"desc"})}),(0,r.jsx)(n.td,{children:"-"}),(0,r.jsx)(n.td,{children:"Sort order (asc/desc)"})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Stable ordering:"})," To ensure deterministic results across pages, sorting includes a secondary key:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Primary: requested field (e.g., ",(0,r.jsx)(n.code,{children:"downloads"}),")"]}),"\n",(0,r.jsxs)(n.li,{children:["Secondary: ",(0,r.jsx)(n.code,{children:"published_at"})," (desc)"]}),"\n",(0,r.jsxs)(n.li,{children:["Tertiary: ",(0,r.jsx)(n.code,{children:"id"})," (for absolute stability)"]}),"\n"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-sql",children:"ORDER BY downloads DESC, published_at DESC, id DESC\nLIMIT 20 OFFSET 0\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Response pagination metadata:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "data": [...],\n "meta": {\n "page": 1,\n "per_page": 20,\n "total": 142,\n "total_pages": 8\n }\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"input-constraints",children:"Input Constraints"}),"\n",(0,r.jsx)(n.p,{children:"Size limits to prevent oversized uploads:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Field"}),(0,r.jsx)(n.th,{children:"Max Size"}),(0,r.jsx)(n.th,{children:"Notes"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"config.yaml"})}),(0,r.jsx)(n.td,{children:"64 KB"}),(0,r.jsx)(n.td,{children:"Tool definition"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"README.md"})}),(0,r.jsx)(n.td,{children:"256 KB"}),(0,r.jsx)(n.td,{children:"Documentation"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Request body"}),(0,r.jsx)(n.td,{children:"512 KB"}),(0,r.jsx)(n.td,{children:"Total POST payload"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Tool name"}),(0,r.jsx)(n.td,{children:"64 chars"}),(0,r.jsx)(n.td,{children:"Alphanumeric + hyphen"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Description"}),(0,r.jsx)(n.td,{children:"500 chars"}),(0,r.jsx)(n.td,{children:"Short summary"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Tag"}),(0,r.jsx)(n.td,{children:"32 chars"}),(0,r.jsx)(n.td,{children:"Individual tag"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Tags array"}),(0,r.jsx)(n.td,{children:"10 items"}),(0,r.jsx)(n.td,{children:"Maximum tags per tool"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Validation errors:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "error": {\n "code": "PAYLOAD_TOO_LARGE",\n "message": "config.yaml exceeds 64KB limit",\n "details": {\n "field": "config",\n "size": 72000,\n "limit": 65536\n }\n }\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"sort-fields-and-indexes",children:"Sort Fields and Indexes"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Allowed sort fields:"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Endpoint"}),(0,r.jsxs)(n.th,{children:["Allowed ",(0,r.jsx)(n.code,{children:"sort"})," values"]})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GET /tools"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"downloads"}),", ",(0,r.jsx)(n.code,{children:"published_at"}),", ",(0,r.jsx)(n.code,{children:"name"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GET /tools/search"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"relevance"}),", ",(0,r.jsx)(n.code,{children:"downloads"}),", ",(0,r.jsx)(n.code,{children:"published_at"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GET /categories"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"name"}),", ",(0,r.jsx)(n.code,{children:"tool_count"})]})]})]})]}),"\n",(0,r.jsx)(n.p,{children:"Invalid sort values return 400:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{"error": {"code": "INVALID_SORT", "message": "Unknown sort field \'foo\'. Allowed: downloads, published_at, name"}}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"tags-endpoint",children:"Tags Endpoint"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/tags"})})," - List all tags with usage counts."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Parameter"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"category"})}),(0,r.jsx)(n.td,{children:"-"}),(0,r.jsx)(n.td,{children:"Filter tags to those used in a specific category"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"limit"})}),(0,r.jsx)(n.td,{children:"100"}),(0,r.jsx)(n.td,{children:"Maximum tags to return (max 500)"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Response:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "data": [\n {"name": "cli", "count": 45},\n {"name": "ai", "count": 32},\n {"name": "text", "count": 28}\n ],\n "meta": {"total": 87}\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"advanced-search",children:"Advanced Search"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/tools/search"})})," supports advanced filtering beyond basic text search."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Parameter"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"q"})}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"required"}),(0,r.jsx)(n.td,{children:"Search query (uses FTS5 full-text search)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"category"})}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"-"}),(0,r.jsx)(n.td,{children:"Filter by single category"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"categories"})}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"-"}),(0,r.jsx)(n.td,{children:"Filter by multiple categories (comma-separated, OR logic)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"tags"})}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"-"}),(0,r.jsx)(n.td,{children:"Filter by tags (comma-separated, AND logic)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"owner"})}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"-"}),(0,r.jsx)(n.td,{children:"Filter by publisher/owner"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"min_downloads"})}),(0,r.jsx)(n.td,{children:"int"}),(0,r.jsx)(n.td,{children:"-"}),(0,r.jsx)(n.td,{children:"Minimum download count"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"max_downloads"})}),(0,r.jsx)(n.td,{children:"int"}),(0,r.jsx)(n.td,{children:"-"}),(0,r.jsx)(n.td,{children:"Maximum download count"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"published_after"})}),(0,r.jsx)(n.td,{children:"date"}),(0,r.jsx)(n.td,{children:"-"}),(0,r.jsx)(n.td,{children:"Published after date (ISO 8601: YYYY-MM-DD)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"published_before"})}),(0,r.jsx)(n.td,{children:"date"}),(0,r.jsx)(n.td,{children:"-"}),(0,r.jsx)(n.td,{children:"Published before date (ISO 8601: YYYY-MM-DD)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"deprecated"})}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"false"}),(0,r.jsx)(n.td,{children:"Include deprecated tools"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"include_facets"})}),(0,r.jsx)(n.td,{children:"bool"}),(0,r.jsx)(n.td,{children:"false"}),(0,r.jsx)(n.td,{children:"Include faceted counts in response"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"sort"})}),(0,r.jsx)(n.td,{children:"string"}),(0,r.jsx)(n.td,{children:"relevance"}),(0,r.jsx)(n.td,{children:"Sort by: relevance, downloads, published_at, name"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"page"})}),(0,r.jsx)(n.td,{children:"int"}),(0,r.jsx)(n.td,{children:"1"}),(0,r.jsx)(n.td,{children:"Page number"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"per_page"})}),(0,r.jsx)(n.td,{children:"int"}),(0,r.jsx)(n.td,{children:"20"}),(0,r.jsx)(n.td,{children:"Results per page (max 100)"})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Tag filtering (AND logic):"})," When multiple tags are specified, only tools with ALL tags are returned:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'GET /api/v1/tools/search?q=summarize&tags=cli,ai\n# Returns tools that have BOTH "cli" AND "ai" tags\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Category filtering (OR logic):"})," When multiple categories are specified, tools in ANY category are returned:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'GET /api/v1/tools/search?q=summarize&categories=text-processing,productivity\n# Returns tools in "text-processing" OR "productivity" category\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Faceted response:"})," When ",(0,r.jsx)(n.code,{children:"include_facets=true"}),", the response includes counts for filtering:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "data": [...],\n "meta": {"page": 1, "per_page": 20, "total": 42, "total_pages": 3},\n "facets": {\n "categories": [\n {"name": "text-processing", "count": 25},\n {"name": "productivity", "count": 17}\n ],\n "tags": [\n {"name": "ai", "count": 30},\n {"name": "cli", "count": 22},\n {"name": "text", "count": 18}\n ],\n "owners": [\n {"name": "official", "count": 15},\n {"name": "rob", "count": 10}\n ]\n }\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Database indexes:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-sql",children:"-- Frequent query patterns\nCREATE INDEX idx_tools_owner_name ON tools(owner, name);\nCREATE INDEX idx_tools_owner ON tools(owner); -- For owner filtering\nCREATE INDEX idx_tools_category ON tools(category);\nCREATE INDEX idx_tools_published_at ON tools(published_at DESC);\nCREATE INDEX idx_tools_downloads ON tools(downloads DESC);\nCREATE INDEX idx_tools_owner_name_version ON tools(owner, name, version);\n\n-- For pagination stability\nCREATE INDEX idx_tools_sort_stable ON tools(downloads DESC, published_at DESC, id DESC);\n\n-- Publisher lookups\nCREATE INDEX idx_publishers_slug ON publishers(slug);\nCREATE INDEX idx_publishers_email ON publishers(email);\n\n-- Token lookups\nCREATE INDEX idx_api_tokens_hash ON api_tokens(token_hash);\nCREATE INDEX idx_api_tokens_publisher ON api_tokens(publisher_id);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"api-version-compatibility",children:"API Version Compatibility"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Forward compatibility:"})," Clients should ignore unknown fields in API responses:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# Good: ignore unknown fields\ntool = response['data']\nname = tool.get('name')\n# Don't fail if 'new_field' exists but client doesn't know about it\n\n# Bad: strict parsing that fails on unknown fields\ntool = ToolSchema.parse(response['data']) # May fail on new fields\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Backward compatibility:"})," The API will:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Never remove fields in a version (only deprecate)"}),"\n",(0,r.jsx)(n.li,{children:"Never change field types"}),"\n",(0,r.jsx)(n.li,{children:"Add new optional fields without version bump"}),"\n",(0,r.jsxs)(n.li,{children:["Use new version (",(0,r.jsx)(n.code,{children:"/api/v2"}),") for breaking changes"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Deprecation process:"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["Add ",(0,r.jsx)(n.code,{children:"X-Deprecated-Field: old_field"})," header"]}),"\n",(0,r.jsx)(n.li,{children:"Document in changelog"}),"\n",(0,r.jsx)(n.li,{children:"Remove after 6 months minimum"}),"\n",(0,r.jsx)(n.li,{children:"Major version bump if widely used"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Client version header:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"X-CmdForge-Client: cli/1.2.0\n"})}),"\n",(0,r.jsx)(n.p,{children:"Helps server track client versions for deprecation decisions."}),"\n",(0,r.jsx)(n.h2,{id:"source-of-truth",children:"Source of Truth"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Gitea registry repo is the source of truth."}),"\n",(0,r.jsx)(n.li,{children:"API syncs repo content into SQLite for fast queries, stats, and FTS5 search."}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"index.json"})," remains useful for offline CLI search and as a fallback."]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"If the cache is stale, the API can fall back to repo reads; a warning header may be emitted."}),"\n",(0,r.jsx)(n.h2,{id:"namespacing-and-paths",children:"Namespacing and Paths"}),"\n",(0,r.jsx)(n.p,{children:"Support owner/name from day one:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Registry path: ",(0,r.jsx)(n.code,{children:"tools/{owner}/{name}/config.yaml"})]}),"\n",(0,r.jsxs)(n.li,{children:["API URL: ",(0,r.jsx)(n.code,{children:"/tools/{owner}/{name}"})]}),"\n",(0,r.jsxs)(n.li,{children:["Install: ",(0,r.jsx)(n.code,{children:"cmdforge registry install rob/summarize"})]}),"\n",(0,r.jsxs)(n.li,{children:["Shorthand: ",(0,r.jsx)(n.code,{children:"cmdforge registry install summarize"})," resolves to the official namespace."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["PR branches: ",(0,r.jsx)(n.code,{children:"submit/{owner}/{name}/{version}"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"namespace-identity",children:"Namespace Identity"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"owner"})," is an ",(0,r.jsx)(n.strong,{children:"immutable slug"}),", not the display name:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-sql",children:'-- In publishers table\nslug TEXT UNIQUE NOT NULL, -- immutable: "rob", "alice-dev"\ndisplay_name TEXT NOT NULL, -- mutable: "Rob", "Alice Developer"\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Slug rules:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Lowercase alphanumeric + hyphens only: ",(0,r.jsx)(n.code,{children:"^[a-z0-9][a-z0-9-]*[a-z0-9]$"})]}),"\n",(0,r.jsx)(n.li,{children:"2-39 characters"}),"\n",(0,r.jsx)(n.li,{children:"Cannot start/end with hyphen"}),"\n",(0,r.jsx)(n.li,{children:"Set once at registration, cannot be changed"}),"\n",(0,r.jsxs)(n.li,{children:["Reserved slugs: ",(0,r.jsx)(n.code,{children:"official"}),", ",(0,r.jsx)(n.code,{children:"admin"}),", ",(0,r.jsx)(n.code,{children:"system"}),", ",(0,r.jsx)(n.code,{children:"api"}),", ",(0,r.jsx)(n.code,{children:"registry"})]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Rename policy:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"display_name"})," can be changed anytime via dashboard"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"slug"})," (owner) is permanent to preserve URLs and tool references"]}),"\n",(0,r.jsxs)(n.li,{children:["If a publisher absolutely must change slug (legal reasons, etc.):","\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Create new account with new slug"}),"\n",(0,r.jsx)(n.li,{children:"Republish tools under new namespace"}),"\n",(0,r.jsxs)(n.li,{children:["Mark old tools as deprecated with ",(0,r.jsx)(n.code,{children:"replacement"})," pointing to new namespace"]}),"\n",(0,r.jsx)(n.li,{children:"Old namespace remains reserved (cannot be reused by others)"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Why immutable:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"rob/summarize@1.0.0"})," must always resolve to the same tool"]}),"\n",(0,r.jsx)(n.li,{children:"Prevents namespace hijacking after rename"}),"\n",(0,r.jsx)(n.li,{children:"Simplifies caching and CDN strategies"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"tool-format-registry--local",children:"Tool Format (Registry == Local)"}),"\n",(0,r.jsx)(n.p,{children:"Registry tool folders mirror local tools:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"tools/\n rob/\n summarize/\n config.yaml\n README.md\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Tool files match the existing CmdForge format. Registry-specific metadata is kept under ",(0,r.jsx)(n.code,{children:"registry:"}),". Deprecation is tool-defined and top-level:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:'name: summarize\nversion: "1.2.0"\ndeprecated: true\ndeprecated_message: "Security issue. Use v1.2.1"\nreplacement: "rob/summarize@1.2.1"\nregistry:\n published_at: "2025-01-15T10:30:00Z"\n downloads: 142\n'})}),"\n",(0,r.jsx)(n.h3,{id:"attribution-and-source-fields",children:"Attribution and Source Fields"}),"\n",(0,r.jsx)(n.p,{children:"Tools can include optional source attribution for provenance and licensing:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:'name: summarize\nversion: "1.2.0"\ndescription: "Summarize text using AI"\n\n# Attribution fields (optional)\nsource:\n type: original # original, adapted, or imported\n license: MIT # SPDX license identifier\n url: https://example.com/tool-repo\n author: "Original Author"\n\n # For adapted/imported tools\n original_tool: other/original-summarize@1.0.0\n changes: "Added French language support"\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Source types:"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"original"})}),(0,r.jsx)(n.td,{children:"Created from scratch by the publisher"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"adapted"})}),(0,r.jsx)(n.td,{children:"Based on another tool with modifications"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"imported"})}),(0,r.jsx)(n.td,{children:"Direct import of external tool (e.g., from npm/pip)"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"License field:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Uses SPDX identifiers: ",(0,r.jsx)(n.code,{children:"MIT"}),", ",(0,r.jsx)(n.code,{children:"Apache-2.0"}),", ",(0,r.jsx)(n.code,{children:"GPL-3.0"}),", etc."]}),"\n",(0,r.jsx)(n.li,{children:"Required for registry publication"}),"\n",(0,r.jsx)(n.li,{children:"Validated against SPDX license list"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"collections",children:"Collections"}),"\n",(0,r.jsx)(n.p,{children:"Collections are curated groups of tools that can be installed together with a single command."}),"\n",(0,r.jsx)(n.h3,{id:"collection-structure",children:"Collection Structure"}),"\n",(0,r.jsxs)(n.p,{children:["Collections are defined in ",(0,r.jsx)(n.code,{children:"collections/{name}.yaml"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:'name: text-processing-essentials\ndisplay_name: "Text Processing Essentials"\ndescription: "Essential tools for text processing and manipulation"\nicon: "\ud83d\udcdd"\n\ntools:\n - official/summarize\n - official/translate\n - official/fix-grammar\n - official/simplify\n - official/tone-shift\n\n# Optional\ncurator: official\ntags: ["text", "nlp", "writing"]\n'})}),"\n",(0,r.jsx)(n.h3,{id:"collections-api",children:"Collections API"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"List all collections:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'GET /api/v1/collections\n\nResponse:\n{\n "data": [\n {\n "name": "text-processing-essentials",\n "display_name": "Text Processing Essentials",\n "description": "Essential tools for text processing...",\n "icon": "\ud83d\udcdd",\n "tool_count": 5,\n "curator": "official"\n }\n ],\n "meta": {"page": 1, "per_page": 20, "total": 8}\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Get collection details:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'GET /api/v1/collections/{name}\n\nResponse:\n{\n "data": {\n "name": "text-processing-essentials",\n "display_name": "Text Processing Essentials",\n "description": "Essential tools for text processing...",\n "icon": "\ud83d\udcdd",\n "curator": "official",\n "tools": [\n {"owner": "official", "name": "summarize", "version": "1.2.0", ...},\n {"owner": "official", "name": "translate", "version": "2.1.0", ...}\n ]\n }\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"cli-commands",children:"CLI Commands"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# List available collections\ncmdforge collections list\n\n# List in JSON format\ncmdforge collections list --json\n\n# View collection details\ncmdforge collections info text-processing-essentials\n\n# View in JSON format\ncmdforge collections info text-processing-essentials --json\n\n# Install all tools in a collection\ncmdforge collections install text-processing-essentials\n\n# Install with pinned versions from collection\ncmdforge collections install text-processing-essentials --pinned\n"})}),"\n",(0,r.jsx)(n.h3,{id:"admin-collections-api",children:"Admin Collections API"}),"\n",(0,r.jsxs)(n.p,{children:["Collections are managed via the admin dashboard at ",(0,r.jsx)(n.code,{children:"/dashboard/admin/collections"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/admin/collections # List all collections (admin)\nPOST /api/v1/admin/collections # Create collection (admin)\nPUT /api/v1/admin/collections/:name # Update collection (admin)\nDELETE /api/v1/admin/collections/:name # Delete collection (admin)\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Schema compatibility note:"})," The current CmdForge config parser may reject unknown top-level keys like ",(0,r.jsx)(n.code,{children:"deprecated"}),", ",(0,r.jsx)(n.code,{children:"replacement"}),", and ",(0,r.jsx)(n.code,{children:"registry"}),". Before implementing registry features:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Update the YAML parser to ignore unknown keys (permissive mode)"}),"\n",(0,r.jsx)(n.li,{children:"Or explicitly define these fields in the Tool dataclass with defaults"}),"\n",(0,r.jsx)(n.li,{children:"Validate registry-specific fields only when publishing, not when running locally"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"This ensures local tools continue to work even if they don't have registry fields."}),"\n",(0,r.jsx)(n.h2,{id:"versioning-and-immutability",children:"Versioning and Immutability"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Unique key: ",(0,r.jsx)(n.code,{children:"owner/name + version"}),"."]}),"\n",(0,r.jsx)(n.li,{children:"Published versions are immutable."}),"\n",(0,r.jsxs)(n.li,{children:["Deprecation uses ",(0,r.jsx)(n.code,{children:"deprecated"}),", ",(0,r.jsx)(n.code,{children:"deprecated_message"}),", and ",(0,r.jsx)(n.code,{children:"replacement"}),"."]}),"\n",(0,r.jsx)(n.li,{children:"CLI warns on install if a version is deprecated."}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"yank-policy",children:"Yank Policy"}),"\n",(0,r.jsx)(n.p,{children:"Yanking allows removing a version from resolution without deleting it (for auditability):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:'# In tool config\nyanked: true\nyanked_reason: "Critical security vulnerability CVE-2025-1234"\nyanked_at: "2025-01-20T15:00:00Z"\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Yanked version behavior:"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Operation"}),(0,r.jsx)(n.th,{children:"Behavior"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"install foo@1.0.0"})," (exact)"]}),(0,r.jsx)(n.td,{children:"Warns but allows install"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"install foo@^1.0.0"})," (constraint)"]}),(0,r.jsx)(n.td,{children:"Excludes yanked, resolves to next valid"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"search"})," / ",(0,r.jsx)(n.code,{children:"browse"})]}),(0,r.jsxs)(n.td,{children:["Hidden by default, shown with ",(0,r.jsx)(n.code,{children:"--include-yanked"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Direct URL access"}),(0,r.jsxs)(n.td,{children:["Returns tool with ",(0,r.jsx)(n.code,{children:"yanked: true"})," in response"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Already installed"}),(0,r.jsx)(n.td,{children:"Continues to work, no forced removal"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Database schema addition:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-sql",children:"-- Add to tools table\nyanked BOOLEAN DEFAULT FALSE,\nyanked_reason TEXT,\nyanked_at TIMESTAMP\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Yank vs Delete:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Yank"}),": Version remains in DB, excluded from resolution, auditable"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Delete"}),": Reserved for DMCA/legal, requires admin action, leaves tombstone record"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"version-format",children:"Version Format"}),"\n",(0,r.jsx)(n.p,{children:"Tools use semantic versioning (semver):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]\n\nExamples:\n 1.0.0 # stable release\n 1.2.3 # stable release\n 2.0.0-alpha.1 # prerelease\n 2.0.0-beta.2 # prerelease\n 2.0.0-rc.1 # release candidate\n"})}),"\n",(0,r.jsx)(n.h3,{id:"version-constraints",children:"Version Constraints"}),"\n",(0,r.jsx)(n.p,{children:"Manifest files support these constraint formats:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Constraint"}),(0,r.jsx)(n.th,{children:"Meaning"}),(0,r.jsx)(n.th,{children:"Example Match"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"1.2.3"})}),(0,r.jsx)(n.td,{children:"Exact version"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"1.2.3"})," only"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:">=1.2.0"})}),(0,r.jsx)(n.td,{children:"Minimum version"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"1.2.0"}),", ",(0,r.jsx)(n.code,{children:"1.3.0"}),", ",(0,r.jsx)(n.code,{children:"2.0.0"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"<2.0.0"})}),(0,r.jsx)(n.td,{children:"Below version"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"1.9.9"}),", ",(0,r.jsx)(n.code,{children:"1.0.0"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:">=1.0.0,<2.0.0"})}),(0,r.jsx)(n.td,{children:"Range"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"1.0.0"})," to ",(0,r.jsx)(n.code,{children:"1.9.9"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"^1.2.3"})}),(0,r.jsx)(n.td,{children:"Compatible (same major)"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"1.2.3"})," to ",(0,r.jsx)(n.code,{children:"1.9.9"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"~1.2.3"})}),(0,r.jsx)(n.td,{children:"Approximately (same minor)"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"1.2.3"})," to ",(0,r.jsx)(n.code,{children:"1.2.9"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"*"})}),(0,r.jsx)(n.td,{children:"Any version"}),(0,r.jsx)(n.td,{children:"latest stable"})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"version-resolution-rules",children:"Version Resolution Rules"}),"\n",(0,r.jsx)(n.p,{children:"When resolving a version constraint:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Filter"}),": Get all versions matching the constraint"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Exclude prereleases"}),": Unless constraint explicitly includes them (e.g., ",(0,r.jsx)(n.code,{children:">=2.0.0-alpha.1"}),")"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Sort"}),": By semver precedence (descending)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Select"}),": Highest matching version"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Tie-breakers:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Stable versions preferred over prereleases"}),"\n",(0,r.jsx)(n.li,{children:"Later publish date wins if versions are equal (shouldn't happen with immutability)"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Unsatisfiable constraints:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'// API Response: 404\n{\n "error": {\n "code": "VERSION_NOT_FOUND",\n "message": "No version of \'rob/summarize\' satisfies constraint \'>=5.0.0\'",\n "details": {\n "tool": "rob/summarize",\n "constraint": ">=5.0.0",\n "available_versions": ["1.0.0", "1.1.0", "1.2.0"],\n "latest_stable": "1.2.0"\n }\n }\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"prerelease-handling",children:"Prerelease Handling"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Prereleases are ",(0,r.jsx)(n.strong,{children:"not"})," returned for ",(0,r.jsx)(n.code,{children:"*"})," or range constraints by default"]}),"\n",(0,r.jsxs)(n.li,{children:["To install prerelease: ",(0,r.jsx)(n.code,{children:"cmdforge registry install rob/summarize@2.0.0-beta.1"})]}),"\n",(0,r.jsxs)(n.li,{children:["To allow prereleases in manifest: ",(0,r.jsx)(n.code,{children:'version: ">=2.0.0-0"'})," (the ",(0,r.jsx)(n.code,{children:"-0"})," suffix includes prereleases)"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"download-endpoint-version-selection",children:"Download Endpoint Version Selection"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"/api/v1/tools/{owner}/{name}/download"})," endpoint accepts version parameters:"]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Parameter"}),(0,r.jsx)(n.th,{children:"Behavior"}),(0,r.jsx)(n.th,{children:"Example"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"(none)"}),(0,r.jsx)(n.td,{children:"Returns latest stable version"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"/download"})," \u2192 ",(0,r.jsx)(n.code,{children:"1.2.0"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"version=1.2.0"})}),(0,r.jsx)(n.td,{children:"Exact version (must exist)"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"/download?version=1.2.0"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"version=^1.0.0"})}),(0,r.jsx)(n.td,{children:"Server resolves constraint"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"/download?version=^1.0.0"})," \u2192 ",(0,r.jsx)(n.code,{children:"1.2.0"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"version=latest"})}),(0,r.jsx)(n.td,{children:"Alias for latest stable"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"/download?version=latest"})})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Server-side resolution:"})," The API server resolves version constraints, not the client. This ensures consistent resolution and allows the server to apply policies (e.g., exclude yanked versions)."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'GET /api/v1/tools/rob/summarize/download?version=^1.0.0&install=true\n\nResponse (200):\n{\n "data": {\n "owner": "rob",\n "name": "summarize",\n "resolved_version": "1.2.0",\n "config": "... YAML content ..."\n },\n "meta": {\n "constraint": "^1.0.0",\n "available_versions": ["1.0.0", "1.1.0", "1.2.0"]\n }\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Invalid/unsatisfiable constraint:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'GET /api/v1/tools/rob/summarize/download?version=^5.0.0\n\nResponse (404):\n{\n "error": {\n "code": "CONSTRAINT_UNSATISFIABLE",\n "message": "No version matches constraint \'^5.0.0\'",\n "details": {\n "constraint": "^5.0.0",\n "latest_stable": "1.2.0",\n "available_versions": ["1.0.0", "1.1.0", "1.2.0"]\n }\n }\n}\n'})}),"\n",(0,r.jsx)(n.h2,{id:"tool-resolution-order",children:"Tool Resolution Order"}),"\n",(0,r.jsx)(n.p,{children:"When a tool is invoked, the CLI searches in this order:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Local project"}),": ",(0,r.jsx)(n.code,{children:"./.cmdforge///config.yaml"})," (or ",(0,r.jsx)(n.code,{children:"./.cmdforge//"})," for unnamespaced)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Global user"}),": ",(0,r.jsx)(n.code,{children:"~/.cmdforge///config.yaml"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Registry"}),": Fetch from API, install to global, then run"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Error"}),": ",(0,r.jsx)(n.code,{children:"Tool '' not found"})]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Step 3 only occurs if ",(0,r.jsx)(n.code,{children:"auto_fetch_from_registry: true"})," in config (default: true)."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Path convention:"})," Use ",(0,r.jsx)(n.code,{children:".cmdforge/"})," (with leading dot) for both local and global to maintain consistency."]}),"\n",(0,r.jsx)(n.p,{children:"Resolution also respects namespacing:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"summarize"})," \u2192 searches for any tool named ",(0,r.jsx)(n.code,{children:"summarize"}),", prefers ",(0,r.jsx)(n.code,{children:"official/summarize"})," if exists"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"rob/summarize"})," \u2192 searches for exactly ",(0,r.jsx)(n.code,{children:"rob/summarize"})]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"official-namespace",children:"Official Namespace"}),"\n",(0,r.jsxs)(n.p,{children:["The slug ",(0,r.jsx)(n.code,{children:"official"})," is reserved for curated, high-quality tools maintained by the registry administrators."]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Shorthand ",(0,r.jsx)(n.code,{children:"summarize"})," resolves to ",(0,r.jsx)(n.code,{children:"official/summarize"})," if it exists"]}),"\n",(0,r.jsxs)(n.li,{children:["If no ",(0,r.jsx)(n.code,{children:"official/summarize"}),", falls back to most-downloaded tool named ",(0,r.jsx)(n.code,{children:"summarize"})]}),"\n",(0,r.jsxs)(n.li,{children:["To avoid ambiguity, always use full ",(0,r.jsx)(n.code,{children:"owner/name"})," in manifests"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Reserved slugs that cannot be registered: ",(0,r.jsx)(n.code,{children:"official"}),", ",(0,r.jsx)(n.code,{children:"admin"}),", ",(0,r.jsx)(n.code,{children:"system"}),", ",(0,r.jsx)(n.code,{children:"api"}),", ",(0,r.jsx)(n.code,{children:"registry"}),", ",(0,r.jsx)(n.code,{children:"cmdforge"})]}),"\n",(0,r.jsx)(n.h2,{id:"auto-fetch-behavior",children:"Auto-Fetch Behavior"}),"\n",(0,r.jsxs)(n.p,{children:["When enabled (",(0,r.jsx)(n.code,{children:"auto_fetch_from_registry: true"}),"), missing tools are automatically fetched:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"$ summarize < file.txt\n# Tool 'summarize' not found locally.\n# Fetching from registry...\n# Installed: official/summarize@1.2.0\n# Running...\n"})}),"\n",(0,r.jsx)(n.p,{children:"Behavior details:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Fetches latest stable version unless pinned in ",(0,r.jsx)(n.code,{children:"cmdforge.yaml"})]}),"\n",(0,r.jsxs)(n.li,{children:["Installs to ",(0,r.jsx)(n.code,{children:"~/.cmdforge///"})]}),"\n",(0,r.jsxs)(n.li,{children:["Generates wrapper script in ",(0,r.jsx)(n.code,{children:"~/.local/bin/"})]}),"\n",(0,r.jsx)(n.li,{children:"Subsequent runs use local copy (no re-fetch)"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"To disable (require explicit install):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"# ~/.cmdforge/config.yaml\nauto_fetch_from_registry: false\n"})}),"\n",(0,r.jsx)(n.h3,{id:"wrapper-script-collisions",children:"Wrapper Script Collisions"}),"\n",(0,r.jsx)(n.p,{children:"When two tools from different owners have the same name:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Scenario"}),(0,r.jsx)(n.th,{children:"Behavior"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["Install ",(0,r.jsx)(n.code,{children:"official/summarize"})]}),(0,r.jsxs)(n.td,{children:["Creates wrapper ",(0,r.jsx)(n.code,{children:"~/.local/bin/summarize"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["Install ",(0,r.jsx)(n.code,{children:"rob/summarize"})," (collision)"]}),(0,r.jsxs)(n.td,{children:["Creates wrapper ",(0,r.jsx)(n.code,{children:"~/.local/bin/rob-summarize"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["Uninstall ",(0,r.jsx)(n.code,{children:"official/summarize"})]}),(0,r.jsxs)(n.td,{children:["Removes ",(0,r.jsx)(n.code,{children:"summarize"})," wrapper, promotes ",(0,r.jsx)(n.code,{children:"rob-summarize"})," \u2192 ",(0,r.jsx)(n.code,{children:"summarize"})," if desired"]})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["The first-installed tool with a given name gets the short wrapper. Subsequent tools use ",(0,r.jsx)(n.code,{children:"owner-name"})," format."]}),"\n",(0,r.jsx)(n.p,{children:"To invoke a specific owner's tool:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# Short form (whichever was installed first)\nsummarize < file.txt\n\n# Explicit owner form (always works)\nrob-summarize < file.txt\n\n# Or via cmdforge run\ncmdforge run rob/summarize < file.txt\n"})}),"\n",(0,r.jsx)(n.h2,{id:"project-manifest-cmdforgeyaml",children:"Project Manifest (cmdforge.yaml)"}),"\n",(0,r.jsx)(n.p,{children:"Defines tool dependencies with optional runtime overrides:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'name: my-ai-project\nversion: "1.0.0"\ndependencies:\n - name: rob/summarize\n version: ">=1.0.0"\noverrides:\n rob/summarize:\n provider: ollama\n'})}),"\n",(0,r.jsx)(n.p,{children:"Overrides are applied at runtime and do not mutate installed tool configs."}),"\n",(0,r.jsx)(n.h2,{id:"cli-config-and-tokens",children:"CLI Config and Tokens"}),"\n",(0,r.jsxs)(n.p,{children:["Global config lives in ",(0,r.jsx)(n.code,{children:"~/.cmdforge/config.yaml"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:'registry:\n url: https://cmdforge.brrd.tech/api/v1 # Must match canonical base path\n token: "reg_xxxxxxxxxxxx"\nclient_id: "anon_abc123def456"\nauto_fetch_from_registry: true\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"client_id"})," is generated locally and used for anonymous install dedupe."]}),"\n",(0,r.jsx)(n.h2,{id:"publishing-and-auth",children:"Publishing and Auth"}),"\n",(0,r.jsx)(n.p,{children:"Publishing uses registry accounts, not Gitea accounts:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Public endpoints require no auth."}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"POST /tools"})," requires a registry token."]}),"\n",(0,r.jsx)(n.li,{children:"The API server uses a private Gitea service account to open PRs."}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"publish-idempotency-and-edge-cases",children:"Publish Idempotency and Edge Cases"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Idempotency key:"})," ",(0,r.jsx)(n.code,{children:"owner/name@version"})]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Scenario"}),(0,r.jsx)(n.th,{children:"API Response"}),(0,r.jsx)(n.th,{children:"HTTP Code"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"New version, no PR exists"}),(0,r.jsx)(n.td,{children:"Create PR, return URL"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"201 Created"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"PR already exists (pending)"}),(0,r.jsx)(n.td,{children:"Return existing PR URL"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"200 OK"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Version already published"}),(0,r.jsx)(n.td,{children:"Error: version exists"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"409 Conflict"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"PR was closed without merge"}),(0,r.jsx)(n.td,{children:"Allow new PR"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"201 Created"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"PR was merged, then tool deleted"}),(0,r.jsx)(n.td,{children:"Error: version exists (tombstone)"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"409 Conflict"})})]})]})]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Version immutability enforcement:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'// Attempt to publish existing version\n// Response: 409 Conflict\n{\n "error": {\n "code": "VERSION_EXISTS",\n "message": "Version 1.2.0 of \'rob/summarize\' already exists and cannot be overwritten",\n "details": {\n "published_at": "2025-01-15T10:30:00Z",\n "action": "Bump version number to publish changes"\n }\n }\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Closed PR handling:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Track PR state in database: ",(0,r.jsx)(n.code,{children:"pending"}),", ",(0,r.jsx)(n.code,{children:"merged"}),", ",(0,r.jsx)(n.code,{children:"closed"})]}),"\n",(0,r.jsx)(n.li,{children:"If PR was closed (rejected/abandoned), allow new submission for same version"}),"\n",(0,r.jsx)(n.li,{children:"If PR was merged, version is immutable forever"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Update flow (new version, not overwrite):"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Developer modifies tool locally"}),"\n",(0,r.jsxs)(n.li,{children:["Bumps version in ",(0,r.jsx)(n.code,{children:"config.yaml"})," (e.g., ",(0,r.jsx)(n.code,{children:"1.2.0"})," \u2192 ",(0,r.jsx)(n.code,{children:"1.3.0"}),")"]}),"\n",(0,r.jsxs)(n.li,{children:["Runs ",(0,r.jsx)(n.code,{children:"cmdforge registry publish"})]}),"\n",(0,r.jsxs)(n.li,{children:["New PR created for ",(0,r.jsx)(n.code,{children:"1.3.0"})]}),"\n",(0,r.jsxs)(n.li,{children:["Old version ",(0,r.jsx)(n.code,{children:"1.2.0"})," remains available"]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"publisher-registration",children:"Publisher Registration"}),"\n",(0,r.jsx)(n.p,{children:"Publishers register on the registry website, not Gitea:"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Registration flow:"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["User visits ",(0,r.jsx)(n.code,{children:"https://gitea.brrd.tech/registry/register"})," (or future ",(0,r.jsx)(n.code,{children:"cmdforge.brrd.tech"}),")"]}),"\n",(0,r.jsx)(n.li,{children:"Creates account with email + password + slug"}),"\n",(0,r.jsxs)(n.li,{children:["Receives verification email (optional in v1, but track ",(0,r.jsx)(n.code,{children:"verified"})," status)"]}),"\n",(0,r.jsxs)(n.li,{children:["Logs into dashboard at ",(0,r.jsx)(n.code,{children:"/dashboard"})]}),"\n",(0,r.jsx)(n.li,{children:"Generates API token from dashboard"}),"\n",(0,r.jsx)(n.li,{children:"Uses token in CLI for publishing"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"authentication-security",children:"Authentication Security"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Password hashing:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Algorithm: Argon2id (memory-hard, recommended by OWASP)"}),"\n",(0,r.jsxs)(n.li,{children:["Parameters: ",(0,r.jsx)(n.code,{children:"memory=65536, iterations=3, parallelism=4"})]}),"\n",(0,r.jsxs)(n.li,{children:["Library: ",(0,r.jsx)(n.code,{children:"argon2-cffi"})," for Python"]}),"\n"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"from argon2 import PasswordHasher\nph = PasswordHasher(memory_cost=65536, time_cost=3, parallelism=4)\nhash = ph.hash(password)\nph.verify(hash, password) # raises on mismatch\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"API token format:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"reg_\n\nExample: reg_7kX9mPqR2sT4vW6xY8zA1bC3dE5fG7hJ\n"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Prefix ",(0,r.jsx)(n.code,{children:"reg_"})," for easy identification in logs/configs"]}),"\n",(0,r.jsx)(n.li,{children:"32 bytes of cryptographically random data"}),"\n",(0,r.jsx)(n.li,{children:"Base62 encoded (alphanumeric, no special chars)"}),"\n",(0,r.jsx)(n.li,{children:"Total length: ~47 characters"}),"\n",(0,r.jsx)(n.li,{children:"Stored as SHA-256 hash in database (never plain text)"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Token lifecycle:"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Action"}),(0,r.jsx)(n.th,{children:"Behavior"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Generate"}),(0,r.jsx)(n.td,{children:"Create new token, return once, store hash"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"List"}),(0,r.jsx)(n.td,{children:"Show token name, created date, last used (not the token itself)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Revoke"}),(0,r.jsxs)(n.td,{children:["Set ",(0,r.jsx)(n.code,{children:"revoked_at"})," timestamp, reject future uses"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Rotate"}),(0,r.jsx)(n.td,{children:"Generate new token, optionally revoke old"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Rate limits:"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Endpoint"}),(0,r.jsx)(n.th,{children:"Limit"}),(0,r.jsx)(n.th,{children:"Window"}),(0,r.jsx)(n.th,{children:"Scope"}),(0,r.jsx)(n.th,{children:"Retry-After"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"POST /register"})}),(0,r.jsx)(n.td,{children:"5"}),(0,r.jsx)(n.td,{children:"1 hour"}),(0,r.jsx)(n.td,{children:"IP"}),(0,r.jsx)(n.td,{children:"3600"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"POST /login"})}),(0,r.jsx)(n.td,{children:"10"}),(0,r.jsx)(n.td,{children:"15 min"}),(0,r.jsx)(n.td,{children:"IP"}),(0,r.jsx)(n.td,{children:"900"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"POST /login"})," (failed)"]}),(0,r.jsx)(n.td,{children:"5"}),(0,r.jsx)(n.td,{children:"15 min"}),(0,r.jsx)(n.td,{children:"IP + email"}),(0,r.jsx)(n.td,{children:"900"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"POST /tokens"})}),(0,r.jsx)(n.td,{children:"10"}),(0,r.jsx)(n.td,{children:"1 hour"}),(0,r.jsx)(n.td,{children:"Token"}),(0,r.jsx)(n.td,{children:"3600"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"POST /tools"})}),(0,r.jsx)(n.td,{children:"20"}),(0,r.jsx)(n.td,{children:"1 hour"}),(0,r.jsx)(n.td,{children:"Token"}),(0,r.jsx)(n.td,{children:"3600"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GET /tools/*"})}),(0,r.jsx)(n.td,{children:"100"}),(0,r.jsx)(n.td,{children:"1 min"}),(0,r.jsx)(n.td,{children:"IP"}),(0,r.jsx)(n.td,{children:"60"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GET /download"})}),(0,r.jsx)(n.td,{children:"60"}),(0,r.jsx)(n.td,{children:"1 min"}),(0,r.jsx)(n.td,{children:"IP"}),(0,r.jsx)(n.td,{children:"60"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Rate limit response (429):"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "error": {\n "code": "RATE_LIMITED",\n "message": "Too many requests. Try again in 60 seconds.",\n "details": {\n "limit": 100,\n "window": "1 minute",\n "retry_after": 60\n }\n }\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Headers on rate-limited response:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"HTTP/1.1 429 Too Many Requests\nRetry-After: 60\nX-RateLimit-Limit: 100\nX-RateLimit-Remaining: 0\nX-RateLimit-Reset: 1705766400\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Scope priority:"})," For authenticated requests, both IP and token limits apply. The more restrictive limit wins."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Account lockout:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"After 5 failed login attempts: 15-minute lockout for that email"}),"\n",(0,r.jsx)(n.li,{children:"After 10 failed attempts: 1-hour lockout"}),"\n",(0,r.jsx)(n.li,{children:"Lockout clears on successful password reset"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Password reset flow (deferred to v1.1):"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"User requests reset via email"}),"\n",(0,r.jsx)(n.li,{children:"Server generates time-limited token (1 hour expiry)"}),"\n",(0,r.jsx)(n.li,{children:"Email contains reset link with token"}),"\n",(0,r.jsx)(n.li,{children:"User sets new password"}),"\n",(0,r.jsx)(n.li,{children:"All existing sessions/tokens optionally invalidated"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Email verification flow (deferred to v1.1):"})}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"On registration, send verification email"}),"\n",(0,r.jsx)(n.li,{children:"User clicks link with verification token"}),"\n",(0,r.jsxs)(n.li,{children:["Set ",(0,r.jsx)(n.code,{children:"verified = true"})," in database"]}),"\n",(0,r.jsx)(n.li,{children:"Unverified accounts can browse but not publish"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"token-scopes-and-authorization",children:"Token Scopes and Authorization"}),"\n",(0,r.jsx)(n.p,{children:"Tokens have scopes that limit their capabilities:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Scope"}),(0,r.jsx)(n.th,{children:"Permissions"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"read"})}),(0,r.jsx)(n.td,{children:"View own published tools, download stats"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"publish"})}),(0,r.jsx)(n.td,{children:"Submit new tools, update own tool metadata"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"admin"})}),(0,r.jsx)(n.td,{children:"Yank tools, manage categories (registry admins only)"})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Default scope:"})," New tokens get ",(0,r.jsx)(n.code,{children:"read,publish"})," by default."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Ownership enforcement:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"@app.route('/api/v1/tools', methods=['POST'])\n@require_token(scopes=['publish'])\ndef publish_tool():\n token = get_current_token()\n tool_data = request.json\n\n # Enforce owner == token holder's slug\n if tool_data['owner'] != token.publisher.slug:\n return {\n \"error\": {\n \"code\": \"FORBIDDEN\",\n \"message\": f\"Cannot publish to namespace '{tool_data['owner']}'. \"\n f\"Your namespace is '{token.publisher.slug}'.\"\n }\n }, 403\n\n # Proceed with publish...\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsxs)(n.strong,{children:[(0,r.jsx)(n.code,{children:"GET /api/v1/me/tools"})," authorization:"]})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Requires valid token with ",(0,r.jsx)(n.code,{children:"read"})," scope"]}),"\n",(0,r.jsxs)(n.li,{children:["Returns only tools where ",(0,r.jsx)(n.code,{children:"owner == token.publisher.slug"})]}),"\n",(0,r.jsx)(n.li,{children:"Includes pending PRs and all versions (including yanked)"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"web-session-security",children:"Web Session Security"}),"\n",(0,r.jsx)(n.p,{children:"Dashboard login uses session cookies (not tokens) for browser auth:"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Cookie settings:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"SESSION_COOKIE_NAME = 'cmdforge_session'\nSESSION_COOKIE_HTTPONLY = True # Prevent JS access\nSESSION_COOKIE_SECURE = True # HTTPS only in production\nSESSION_COOKIE_SAMESITE = 'Lax' # CSRF protection\nSESSION_COOKIE_MAX_AGE = 86400 * 7 # 7 days\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"CSRF protection:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["All POST/PUT/DELETE forms include ",(0,r.jsx)(n.code,{children:"csrf_token"})," hidden field"]}),"\n",(0,r.jsx)(n.li,{children:"Token validated server-side before processing"}),"\n",(0,r.jsx)(n.li,{children:"403 Forbidden if token missing or invalid"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Session lifecycle:"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Event"}),(0,r.jsx)(n.th,{children:"Action"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Login"}),(0,r.jsx)(n.td,{children:"Create session, set cookie"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Logout"}),(0,r.jsx)(n.td,{children:"Delete session, clear cookie"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Idle 24h"}),(0,r.jsx)(n.td,{children:"Session expires, re-login required"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Password change"}),(0,r.jsx)(n.td,{children:"Invalidate all sessions"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Token revocation"}),(0,r.jsx)(n.td,{children:"Existing sessions continue (token != session)"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Secure session storage:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# Store sessions in DB, not filesystem\nfrom flask_session import Session\napp.config['SESSION_TYPE'] = 'sqlalchemy'\napp.config['SESSION_SQLALCHEMY_TABLE'] = 'sessions'\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Database schema:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-sql",children:'-- Publishers\nCREATE TABLE publishers (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n email TEXT UNIQUE NOT NULL,\n password_hash TEXT NOT NULL,\n slug TEXT UNIQUE NOT NULL, -- immutable namespace: "rob", "alice-dev"\n display_name TEXT NOT NULL, -- mutable: "Rob", "Alice Developer"\n bio TEXT,\n website TEXT,\n verified BOOLEAN DEFAULT FALSE,\n locked_until TIMESTAMP, -- account lockout\n failed_login_attempts INTEGER DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\n-- API tokens (one publisher can have multiple)\nCREATE TABLE api_tokens (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n publisher_id INTEGER NOT NULL REFERENCES publishers(id),\n token_hash TEXT NOT NULL,\n name TEXT NOT NULL, -- "CLI token", "CI token"\n last_used_at TIMESTAMP,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n revoked_at TIMESTAMP -- NULL if active\n);\n\n-- Tools (links to publisher)\nCREATE TABLE tools (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n owner TEXT NOT NULL, -- namespace slug (immutable, from publisher.slug)\n name TEXT NOT NULL,\n version TEXT NOT NULL,\n description TEXT,\n category TEXT,\n tags TEXT, -- JSON array\n config_yaml TEXT NOT NULL, -- Full tool config\n readme TEXT,\n publisher_id INTEGER NOT NULL REFERENCES publishers(id),\n deprecated BOOLEAN DEFAULT FALSE,\n deprecated_message TEXT,\n replacement TEXT,\n downloads INTEGER DEFAULT 0,\n published_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n UNIQUE(owner, name, version)\n);\n\n-- Download stats (for deduplication)\nCREATE TABLE download_stats (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n tool_id INTEGER NOT NULL REFERENCES tools(id),\n client_id TEXT NOT NULL,\n downloaded_at DATE NOT NULL,\n UNIQUE(tool_id, client_id, downloaded_at)\n);\n\n-- Search index (FTS5)\nCREATE VIRTUAL TABLE tools_fts USING fts5(\n name, description, tags, readme,\n content=\'tools\',\n content_rowid=\'id\'\n);\n\n-- FTS5 sync triggers (required for external content tables)\nCREATE TRIGGER tools_ai AFTER INSERT ON tools BEGIN\n INSERT INTO tools_fts(rowid, name, description, tags, readme)\n VALUES (new.id, new.name, new.description, new.tags, new.readme);\nEND;\n\nCREATE TRIGGER tools_ad AFTER DELETE ON tools BEGIN\n INSERT INTO tools_fts(tools_fts, rowid, name, description, tags, readme)\n VALUES (\'delete\', old.id, old.name, old.description, old.tags, old.readme);\nEND;\n\nCREATE TRIGGER tools_au AFTER UPDATE ON tools BEGIN\n INSERT INTO tools_fts(tools_fts, rowid, name, description, tags, readme)\n VALUES (\'delete\', old.id, old.name, old.description, old.tags, old.readme);\n INSERT INTO tools_fts(rowid, name, description, tags, readme)\n VALUES (new.id, new.name, new.description, new.tags, new.readme);\nEND;\n\n-- Pending PRs (track publish state)\nCREATE TABLE pending_prs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n publisher_id INTEGER NOT NULL REFERENCES publishers(id),\n owner TEXT NOT NULL,\n name TEXT NOT NULL,\n version TEXT NOT NULL,\n pr_number INTEGER NOT NULL,\n pr_url TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT \'pending\', -- pending, merged, closed\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n UNIQUE(owner, name, version)\n);\n\n-- Webhook sync log (idempotency)\nCREATE TABLE webhook_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n delivery_id TEXT UNIQUE NOT NULL, -- Gitea delivery ID\n event_type TEXT NOT NULL,\n processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Note on tags indexing:"})," The ",(0,r.jsx)(n.code,{children:"tags"})," column stores JSON arrays as text. For v1, FTS5 will search within the JSON string. If tag filtering becomes a bottleneck, normalize to a ",(0,r.jsx)(n.code,{children:"tool_tags"})," junction table:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-sql",children:"-- Future: normalized tags (if needed)\nCREATE TABLE tags (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT UNIQUE NOT NULL\n);\n\nCREATE TABLE tool_tags (\n tool_id INTEGER REFERENCES tools(id),\n tag_id INTEGER REFERENCES tags(id),\n PRIMARY KEY (tool_id, tag_id)\n);\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Connecting to your account:"})}),"\n",(0,r.jsx)(n.p,{children:"The recommended way to authenticate is using the app pairing flow, which eliminates manual token copying:"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"CLI connection flow:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"$ cmdforge config connect rob\n\nConnecting to CmdForge as @rob...\nDevice: my-laptop\n\nWaiting for approval from the web interface...\nGo to https://cmdforge.brrd.tech/dashboard/connections\nand click 'Connect New App', then 'I've Run the Command'\n\nPress Ctrl+C to cancel\n\nWaiting...\n\nConnected successfully!\nYour device 'my-laptop' is now linked to @rob\n\nYou can now publish tools with: cmdforge registry publish\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"TUI connection flow:"})}),"\n",(0,r.jsxs)(n.p,{children:["The TUI (",(0,r.jsx)(n.code,{children:"cmdforge ui"}),") includes a Connect button for connecting without using the command line:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:'Click "Connect" button in the main menu'}),"\n",(0,r.jsx)(n.li,{children:"Enter your CmdForge username (create account at cmdforge.brrd.tech if needed)"}),"\n",(0,r.jsx)(n.li,{children:"A countdown timer shows the pairing expiration (5 minutes)"}),"\n",(0,r.jsx)(n.li,{children:"Go to cmdforge.brrd.tech/dashboard/connections in your browser"}),"\n",(0,r.jsx)(n.li,{children:'Click "Connect New App" and approve the pending connection'}),"\n",(0,r.jsx)(n.li,{children:"TUI automatically detects approval and saves the token"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"CLI first-time publish flow:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"$ cmdforge registry publish\n\nNo registry account configured.\n\nOptions:\n1. Connect to account (recommended): cmdforge config connect \n2. Manual token entry\n\nChoose option [1]: 1\nEnter username: rob\n\nConnecting to CmdForge as @rob...\n[Follows connection flow above]\n\nValidating tool...\n\u2713 config.yaml is valid\n\u2713 README.md exists (2.3 KB)\n\u2713 Version 1.0.0 not yet published\n\nPublishing rob/my-tool@1.0.0...\n\u2713 PR created: https://gitea.brrd.tech/rob/CmdForge-Registry/pulls/42\n\nYour tool is pending review. You'll receive an email when it's approved.\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"TUI publish flow:"})}),"\n",(0,r.jsx)(n.p,{children:'When already connected, the TUI main menu shows "Publish" instead of "Connect":'}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Select a tool in the list"}),"\n",(0,r.jsx)(n.li,{children:'Click "Publish" button'}),"\n",(0,r.jsx)(n.li,{children:"If no version in config, TUI prompts for version number"}),"\n",(0,r.jsx)(n.li,{children:"Confirm and publish"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Private sync after save:"})}),"\n",(0,r.jsx)(n.p,{children:"When connected to an account, saving a tool in the TUI offers to sync it privately to the registry:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Save a tool (new or edited)"}),"\n",(0,r.jsx)(n.li,{children:'TUI asks: "Sync to registry privately?"'}),"\n",(0,r.jsx)(n.li,{children:"If yes, enter/confirm version number"}),"\n",(0,r.jsxs)(n.li,{children:["Tool is published with ",(0,r.jsx)(n.code,{children:"visibility: private"})," (only you can see it)"]}),"\n",(0,r.jsx)(n.li,{children:"Useful for backup or accessing your tools from multiple machines"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"cli-commands-reference",children:"CLI Commands Reference"}),"\n",(0,r.jsx)(n.p,{children:"Full mapping of CLI commands to API calls:"}),"\n",(0,r.jsx)(n.h3,{id:"registry-commands",children:"Registry Commands"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# Search for tools (basic)\n$ cmdforge registry search [--category=] [--limit=20]\n \u2192 GET /api/v1/tools/search?q=&category=&limit=20\n\n# Search with advanced filtering\n$ cmdforge registry search [options]\n Options:\n -c, --category CAT Filter by category\n -t, --tag TAG Filter by tag (repeatable, AND logic)\n -o, --owner OWNER Filter by publisher/owner\n --min-downloads N Minimum downloads\n --popular Shortcut for --min-downloads 100\n --new Shortcut for --max-downloads 10\n --since DATE Published after (YYYY-MM-DD)\n --before DATE Published before (YYYY-MM-DD)\n -s, --sort FIELD Sort by: relevance, downloads, published_at, name\n -l, --limit N Max results (default: 20)\n --json Output as JSON\n --show-facets Show category/tag counts\n --deprecated Include deprecated tools\n\n# List available tags\n$ cmdforge registry tags [-c CATEGORY] [-l LIMIT] [--json]\n \u2192 GET /api/v1/tags?category=&limit=\n\n# Browse tools (TUI)\n$ cmdforge registry browse [--category=]\n \u2192 GET /api/v1/tools?category=&page=1\n \u2192 GET /api/v1/categories\n\n# View tool details\n$ cmdforge registry info \n \u2192 GET /api/v1/tools//\n\n# Install a tool\n$ cmdforge registry install [--version=]\n \u2192 GET /api/v1/tools///download?version=&install=true\n \u2192 Writes to ~/.cmdforge///config.yaml\n \u2192 Generates ~/.local/bin/ wrapper (or - if collision)\n\n# Uninstall a tool\n$ cmdforge registry uninstall \n \u2192 Removes ~/.cmdforge///\n \u2192 Removes wrapper script\n\n# Publish a tool\n$ cmdforge registry publish [path] [--dry-run]\n \u2192 POST /api/v1/tools (with registry token)\n \u2192 Returns PR URL\n\n# List my published tools\n$ cmdforge registry my-tools\n \u2192 GET /api/v1/me/tools (with registry token)\n\n# Update index cache\n$ cmdforge registry update\n \u2192 GET /api/v1/index.json\n \u2192 Writes to ~/.cmdforge/registry/index.json\n"})}),"\n",(0,r.jsx)(n.h3,{id:"project-commands",children:"Project Commands"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'# Install project dependencies from cmdforge.yaml\n$ cmdforge install\n \u2192 Reads ./cmdforge.yaml\n \u2192 For each dependency:\n GET /api/v1/tools///download?version=&install=true\n \u2192 Installs to ~/.cmdforge///\n\n# Add a dependency to cmdforge.yaml\n$ cmdforge add [--version=]\n \u2192 Adds to ./cmdforge.yaml dependencies\n \u2192 Runs install for that tool\n\n# Show project dependencies status\n$ cmdforge deps\n \u2192 Reads ./cmdforge.yaml\n \u2192 Shows installed status for each dependency\n \u2192 Note: "cmdforge list" is reserved for listing installed tools\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Command naming note:"})," ",(0,r.jsx)(n.code,{children:"cmdforge list"})," already exists to list locally installed tools. Use ",(0,r.jsx)(n.code,{children:"cmdforge deps"})," to show project manifest dependencies."]}),"\n",(0,r.jsx)(n.h3,{id:"config-commands",children:"Config Commands"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# Show current configuration\n$ cmdforge config show\n \u2192 Displays registry URL, token status, client ID, auto-fetch setting\n\n# Connect to your CmdForge account (recommended)\n$ cmdforge config connect \n \u2192 Initiates app pairing flow\n \u2192 Polls /api/v1/pairing/check/?hostname=\n \u2192 On approval, saves token to ~/.cmdforge/config.yaml\n\n# Set registry token manually (alternative to connect)\n$ cmdforge config set-token \n \u2192 Saves token to ~/.cmdforge/config.yaml\n\n# Set configuration values\n$ cmdforge config set \n \u2192 Available keys: auto_fetch, default_provider, registry_url\n"})}),"\n",(0,r.jsx)(n.h3,{id:"flags-available-on-most-commands",children:"Flags available on most commands"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Flag"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"--offline"})}),(0,r.jsx)(n.td,{children:"Use cached index only, don't fetch"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"--refresh"})}),(0,r.jsx)(n.td,{children:"Force refresh of cached data"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"--json"})}),(0,r.jsx)(n.td,{children:"Output in JSON format"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"--verbose"})}),(0,r.jsx)(n.td,{children:"Show detailed output"})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"publish-state-tracking",children:"Publish State Tracking"}),"\n",(0,r.jsx)(n.p,{children:"The GUI tracks the publish state of local tools to show whether they've been published, are pending review, or have been modified since publishing."}),"\n",(0,r.jsx)(n.h3,{id:"local-state-storage",children:"Local State Storage"}),"\n",(0,r.jsxs)(n.p,{children:["When a tool is published, two fields are saved to the local ",(0,r.jsx)(n.code,{children:"config.yaml"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"name: my-tool\ndescription: My awesome tool\n# ... tool config ...\nregistry_hash: sha256:abc123... # Hash of published config\nregistry_status: pending # Moderation status: pending, approved, rejected\n"})}),"\n",(0,r.jsx)(n.h3,{id:"visual-indicators",children:"Visual Indicators"}),"\n",(0,r.jsx)(n.p,{children:"The Tools page shows different indicators based on publish state:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"State"}),(0,r.jsx)(n.th,{children:"Indicator"}),(0,r.jsx)(n.th,{children:"Meaning"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Published"}),(0,r.jsx)(n.td,{children:"\u2713 (green)"}),(0,r.jsx)(n.td,{children:"Approved in registry, local matches published"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Pending"}),(0,r.jsx)(n.td,{children:"\u25d0 (yellow)"}),(0,r.jsx)(n.td,{children:"Submitted, awaiting moderator review"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Modified"}),(0,r.jsx)(n.td,{children:"\u25cf (orange)"}),(0,r.jsx)(n.td,{children:"Published but local config has changes"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Local"}),(0,r.jsx)(n.td,{children:"(none)"}),(0,r.jsx)(n.td,{children:"Never published to registry"})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"automatic-status-sync",children:"Automatic Status Sync"}),"\n",(0,r.jsx)(n.p,{children:"When the Tools page loads, a background sync automatically checks the registry for status updates on all published tools. This means:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"When a moderator approves your tool, the indicator updates automatically on next visit"}),"\n",(0,r.jsx)(n.li,{children:"No manual refresh needed - just navigate to the Tools page"}),"\n",(0,r.jsx)(n.li,{children:"Status messages appear when tool statuses change"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"manual-sync",children:"Manual Sync"}),"\n",(0,r.jsx)(n.p,{children:'A "Sync Status" button is available to force an immediate status check for the selected tool. This is useful if you want to check status without leaving the page.'}),"\n",(0,r.jsx)(n.h3,{id:"hash-computation",children:"Hash Computation"}),"\n",(0,r.jsx)(n.p,{children:"The publish state hash is computed from the tool's core content, excluding:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"registry_hash"})," - The stored hash itself"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"registry_status"})," - The moderation status"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"version"})," - Publication version (added during publish)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"tags"})," - Publication tags (added during publish)"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:'This ensures that only actual tool content changes (steps, arguments, prompts) trigger the "modified" indicator.'}),"\n",(0,r.jsx)(n.h3,{id:"api-endpoint",children:"API Endpoint"}),"\n",(0,r.jsx)(n.p,{children:"The status sync uses:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'GET /api/v1/me/tools//status\n\nResponse:\n{\n "data": {\n "name": "my-tool",\n "version": "1.0.0",\n "status": "approved",\n "config_hash": "sha256:abc123...",\n "published_at": "2025-01-15T10:30:00Z"\n }\n}\n'})}),"\n",(0,r.jsx)(n.h2,{id:"webhooks-and-security",children:"Webhooks and Security"}),"\n",(0,r.jsx)(n.h3,{id:"hmac-verification",children:"HMAC Verification"}),"\n",(0,r.jsx)(n.p,{children:"All Gitea webhooks are verified using HMAC-SHA256:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"import hmac\nimport hashlib\n\ndef verify_webhook(request, secret):\n signature = request.headers.get('X-Gitea-Signature')\n if not signature:\n return False\n\n expected = hmac.new(\n secret.encode(),\n request.body,\n hashlib.sha256\n ).hexdigest()\n\n return hmac.compare_digest(signature, expected)\n"})}),"\n",(0,r.jsx)(n.h3,{id:"replay-protection",children:"Replay Protection"}),"\n",(0,r.jsx)(n.p,{children:"While sync is idempotent, implement basic replay protection:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:'def process_webhook(request):\n delivery_id = request.headers.get(\'X-Gitea-Delivery\')\n\n # Check if already processed\n if db.webhook_log.exists(delivery_id=delivery_id):\n return {"status": "already_processed"}, 200\n\n # Verify signature\n if not verify_webhook(request, WEBHOOK_SECRET):\n return {"error": "invalid_signature"}, 401\n\n # Process with lock to prevent concurrent processing\n with db.lock(f"webhook:{delivery_id}"):\n # Double-check after acquiring lock\n if db.webhook_log.exists(delivery_id=delivery_id):\n return {"status": "already_processed"}, 200\n\n # Process the webhook\n result = sync_from_repo()\n\n # Log successful processing\n db.webhook_log.insert(\n delivery_id=delivery_id,\n event_type=request.json.get(\'action\'),\n processed_at=datetime.utcnow()\n )\n\n return {"status": "processed"}, 200\n'})}),"\n",(0,r.jsx)(n.h3,{id:"sync-job-locking",children:"Sync Job Locking"}),"\n",(0,r.jsx)(n.p,{children:"Prevent concurrent sync operations:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:'# Using file lock or database advisory lock\nSYNC_LOCK_TIMEOUT = 300 # 5 minutes max\n\ndef sync_from_repo():\n try:\n with acquire_lock("registry_sync", timeout=SYNC_LOCK_TIMEOUT):\n # Pull latest from Gitea\n repo.fetch()\n repo.reset(\'origin/main\', hard=True)\n\n # Parse and update database\n for tool_path in glob(\'tools/*/*/config.yaml\'):\n update_tool_in_db(tool_path)\n\n # Rebuild FTS index if needed\n rebuild_fts_index()\n\n except LockTimeout:\n logger.warning("Sync already in progress, skipping")\n return {"status": "skipped", "reason": "sync_in_progress"}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"atomic-sync-strategy",children:"Atomic Sync Strategy"}),"\n",(0,r.jsx)(n.p,{children:"To avoid partially updated DB during webhook sync, use transactional table swap:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:'def sync_from_repo_atomic():\n with acquire_lock("registry_sync", timeout=SYNC_LOCK_TIMEOUT):\n # 1. Pull latest from Gitea\n repo.fetch()\n repo.reset(\'origin/main\', hard=True)\n\n # 2. Parse all tools into memory\n new_tools = []\n for tool_path in glob(\'tools/*/*/config.yaml\'):\n tool_data = parse_tool(tool_path)\n if tool_data:\n new_tools.append(tool_data)\n\n # 3. Atomic swap using transaction\n with db.transaction():\n # Create temp table\n db.execute("CREATE TABLE tools_new AS SELECT * FROM tools WHERE 0")\n\n # Bulk insert into temp table\n for tool in new_tools:\n db.execute("INSERT INTO tools_new ...", tool)\n\n # Swap tables atomically\n db.execute("ALTER TABLE tools RENAME TO tools_old")\n db.execute("ALTER TABLE tools_new RENAME TO tools")\n db.execute("DROP TABLE tools_old")\n\n # Rebuild FTS index\n db.execute("INSERT INTO tools_fts(tools_fts) VALUES(\'rebuild\')")\n\n # Update sync timestamp\n db.execute("UPDATE sync_status SET last_sync = ?", [datetime.utcnow()])\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Why atomic:"})," Per-row updates with FTS triggers can yield inconsistent reads under load. Readers may see partial state mid-sync. Table swap ensures all-or-nothing visibility."]}),"\n",(0,r.jsx)(n.h3,{id:"error-handling",children:"Error Handling"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Error Scenario"}),(0,r.jsx)(n.th,{children:"Behavior"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Repo fetch fails"}),(0,r.jsx)(n.td,{children:"Log error, retry in 5 min, alert if 3 failures"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"YAML parse error"}),(0,r.jsx)(n.td,{children:"Skip tool, log error, continue with others"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Database write fails"}),(0,r.jsx)(n.td,{children:"Rollback transaction, retry once, then alert"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Lock timeout"}),(0,r.jsx)(n.td,{children:"Skip this sync, next webhook will retry"})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"automated-ci-validation",children:"Automated CI Validation"}),"\n",(0,r.jsx)(n.p,{children:"PRs are validated automatically using CmdForge (dogfooding):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"PR Submitted\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Gitea CI runs validation tools: \u2502\n\u2502 \u2022 schema-validator \u2502\n\u2502 \u2022 security-scanner \u2502\n\u2502 \u2022 duplicate-detector \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 \u2502\n All pass Any fail\n \u2502 \u2502\n \u25bc \u25bc\n Auto-merge or Add comment,\n flag for review request changes\n"})}),"\n",(0,r.jsx)(n.p,{children:"Validation checks:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Schema validation"}),": config.yaml matches expected format"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Security scan"}),": No dangerous shell commands, no secrets in prompts"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Duplicate detection"}),": AI-powered similarity check against existing tools"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"README check"}),": README.md exists and is non-empty"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["CI workflow (",(0,r.jsx)(n.code,{children:".gitea/workflows/validate.yaml"}),"):"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"name: Validate Tool Submission\non: [pull_request]\njobs:\n validate:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v3\n - name: Validate schema\n run: python scripts/validate_tool.py ${{ github.event.pull_request.head.sha }}\n - name: Security scan\n run: cmdforge run security-scanner < changed_files.txt\n - name: Check duplicates\n run: cmdforge run duplicate-detector < changed_files.txt\n"})}),"\n",(0,r.jsx)(n.h2,{id:"registry-repository-structure",children:"Registry Repository Structure"}),"\n",(0,r.jsx)(n.p,{children:"Full structure of the CmdForge-Registry repo:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"CmdForge-Registry/\n\u251c\u2500\u2500 README.md # Registry overview\n\u251c\u2500\u2500 CONTRIBUTING.md # How to submit tools\n\u251c\u2500\u2500 LICENSE\n\u2502\n\u251c\u2500\u2500 tools/ # All published tools\n\u2502 \u251c\u2500\u2500 rob/\n\u2502 \u2502 \u251c\u2500\u2500 summarize/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 config.yaml\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 README.md\n\u2502 \u2502 \u2514\u2500\u2500 translate/\n\u2502 \u2502 \u251c\u2500\u2500 config.yaml\n\u2502 \u2502 \u2514\u2500\u2500 README.md\n\u2502 \u2514\u2500\u2500 alice/\n\u2502 \u2514\u2500\u2500 code-review/\n\u2502 \u251c\u2500\u2500 config.yaml\n\u2502 \u2514\u2500\u2500 README.md\n\u2502\n\u251c\u2500\u2500 categories/\n\u2502 \u2514\u2500\u2500 categories.yaml # Category definitions\n\u2502\n\u251c\u2500\u2500 collections/ # Curated tool collections\n\u2502 \u251c\u2500\u2500 text-processing-essentials.yaml\n\u2502 \u251c\u2500\u2500 developer-toolkit.yaml\n\u2502 \u2514\u2500\u2500 data-pipeline-basics.yaml\n\u2502\n\u251c\u2500\u2500 index.json # Auto-generated search index\n\u2502\n\u251c\u2500\u2500 .gitea/\n\u2502 \u2514\u2500\u2500 workflows/\n\u2502 \u251c\u2500\u2500 validate.yaml # PR validation\n\u2502 \u251c\u2500\u2500 build-index.yaml # Rebuild index on merge\n\u2502 \u2514\u2500\u2500 notify-api.yaml # Webhook to API server\n\u2502\n\u2514\u2500\u2500 scripts/\n \u251c\u2500\u2500 validate_tool.py # Schema validation\n \u251c\u2500\u2500 build_index.py # Generate index.json\n \u251c\u2500\u2500 check_duplicates.py # Similarity detection\n \u2514\u2500\u2500 security_scan.py # Security checks\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"categories.yaml"})," format:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"categories:\n - name: text-processing\n description: Tools for manipulating and analyzing text\n icon: \ud83d\udcdd\n - name: code\n description: Tools for code review, generation, and analysis\n icon: \ud83d\udcbb\n - name: data\n description: Tools for data transformation and analysis\n icon: \ud83d\udcca\n - name: media\n description: Tools for image, audio, and video processing\n icon: \ud83c\udfa8\n - name: productivity\n description: General productivity and automation tools\n icon: \u26a1\n"})}),"\n",(0,r.jsx)(n.h2,{id:"download-stats",children:"Download Stats"}),"\n",(0,r.jsx)(n.h3,{id:"counting-methodology",children:"Counting Methodology"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Count installs only, not views or searches"}),"\n",(0,r.jsxs)(n.li,{children:["Increment ",(0,r.jsx)(n.strong,{children:"after"})," successful download (response sent)"]}),"\n",(0,r.jsxs)(n.li,{children:["Dedupe by ",(0,r.jsx)(n.code,{children:"client_id + tool_id + date"})]}),"\n"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:'def download_tool(owner, name, version, install=False, client_id=None):\n tool = get_tool(owner, name, version)\n if not tool:\n return {"error": "not_found"}, 404\n\n config_yaml = tool.config_yaml\n\n # Only count if this is an install (not just viewing)\n if install:\n record_download(tool.id, client_id)\n\n return {"config": config_yaml}, 200\n\ndef record_download(tool_id, client_id):\n today = date.today()\n\n # Use client_id if provided, otherwise generate anonymous fallback\n effective_client_id = client_id or f"anon_{hash(request.remote_addr)}"\n\n # Dedupe: only count once per client per tool per day\n try:\n db.download_stats.insert(\n tool_id=tool_id,\n client_id=effective_client_id,\n downloaded_at=today\n )\n # Increment counter (can be async/batch updated)\n db.execute("UPDATE tools SET downloads = downloads + 1 WHERE id = ?", [tool_id])\n except IntegrityError:\n pass # Already counted today, ignore\n'})}),"\n",(0,r.jsx)(n.h3,{id:"client-id-generation",children:"Client ID Generation"}),"\n",(0,r.jsx)(n.p,{children:"CLI generates a persistent anonymous ID on first run:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"# In CLI, on first run\nimport uuid\nimport os\n\nCONFIG_PATH = os.path.expanduser(\"~/.cmdforge/config.yaml\")\n\ndef get_or_create_client_id():\n config = load_config()\n if 'client_id' not in config:\n config['client_id'] = f\"anon_{uuid.uuid4().hex[:16]}\"\n save_config(config)\n return config['client_id']\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Fallback when client_id missing:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["If header ",(0,r.jsx)(n.code,{children:"X-Client-ID"})," not sent, use IP hash as fallback"]}),"\n",(0,r.jsx)(n.li,{children:"This still provides some dedupe for anonymous users"}),"\n",(0,r.jsx)(n.li,{children:"Logged users' downloads are attributed to their account instead"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"privacy-considerations",children:"Privacy Considerations"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"No IP addresses stored in database"}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"client_id"})," is client-controlled and can be regenerated"]}),"\n",(0,r.jsx)(n.li,{children:"Stats are aggregated (total count), not individual tracking"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"async-stats-strategy",children:"Async Stats Strategy"}),"\n",(0,r.jsx)(n.p,{children:"To avoid DB contention on the hot download path:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:'from queue import Queue\nfrom threading import Thread\n\n# In-memory queue for stats\nstats_queue = Queue()\n\ndef record_download_async(tool_id, client_id):\n """Non-blocking: enqueue for background processing"""\n stats_queue.put({\n \'tool_id\': tool_id,\n \'client_id\': client_id,\n \'date\': date.today()\n })\n\ndef stats_worker():\n """Background thread: batch process stats every 5 seconds"""\n batch = []\n while True:\n try:\n item = stats_queue.get(timeout=5)\n batch.append(item)\n except Empty:\n if batch:\n flush_batch(batch)\n batch = []\n\ndef flush_batch(batch):\n """Bulk insert with conflict ignore"""\n with db.transaction():\n for item in batch:\n try:\n db.execute("""\n INSERT INTO download_stats (tool_id, client_id, downloaded_at)\n VALUES (?, ?, ?)\n ON CONFLICT DO NOTHING\n """, [item[\'tool_id\'], item[\'client_id\'], item[\'date\']])\n except Exception as e:\n logger.warning(f"Stats insert failed: {e}")\n # Don\'t fail downloads for stats errors\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Failure behavior:"}),' If stats DB write fails, log the error but don\'t fail the download. Stats are "best effort" - the download must succeed.']}),"\n",(0,r.jsx)(n.h2,{id:"search",children:"Search"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Primary search: SQLite FTS5 inside the API."}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"index.json"})," provides offline CLI search and backup."]}),"\n",(0,r.jsxs)(n.li,{children:["If FTS5 is stale, return results with ",(0,r.jsx)(n.code,{children:"X-Search-Index-Stale: true"}),"."]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"api-caching-strategy",children:"API Caching Strategy"}),"\n",(0,r.jsx)(n.h3,{id:"cache-headers",children:"Cache Headers"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Endpoint"}),(0,r.jsx)(n.th,{children:"Cache-Control"}),(0,r.jsx)(n.th,{children:"ETag"}),(0,r.jsx)(n.th,{children:"Notes"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GET /index.json"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"max-age=300, stale-while-revalidate=60"})}),(0,r.jsx)(n.td,{children:"Yes"}),(0,r.jsx)(n.td,{children:"5 min cache, background refresh"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GET /tools/{owner}/{name}"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"max-age=60"})}),(0,r.jsx)(n.td,{children:"Yes"}),(0,r.jsx)(n.td,{children:"1 min cache"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GET /tools/{owner}/{name}/download"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"max-age=3600, immutable"})}),(0,r.jsx)(n.td,{children:"Yes"}),(0,r.jsx)(n.td,{children:"Immutable versions, 1 hour"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GET /tools/search"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"no-cache"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:"Always fresh"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GET /categories"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"max-age=3600"})}),(0,r.jsx)(n.td,{children:"Yes"}),(0,r.jsx)(n.td,{children:"Categories change rarely"})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"etag-implementation",children:"ETag Implementation"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:'import hashlib\nfrom datetime import datetime\n\ndef get_tool_etag(tool):\n """Generate ETag from tool identity (immutable versions don\'t change)"""\n # Since versions are immutable, owner/name@version is stable\n # Use published_at for extra safety (not updated_at, which doesn\'t exist)\n content = f"{tool.owner}/{tool.name}@{tool.version}:{tool.published_at.isoformat()}"\n return hashlib.md5(content.encode()).hexdigest()\n\ndef get_index_etag():\n """Generate ETag from last sync timestamp"""\n last_sync = db.get_last_sync_time()\n return hashlib.md5(last_sync.isoformat().encode()).hexdigest()\n\n@app.route(\'/api/v1/tools///download\')\ndef download_tool(owner, name):\n version = request.args.get(\'version\', \'latest\')\n tool = resolve_and_get_tool(owner, name, version)\n etag = get_tool_etag(tool)\n\n # Check If-None-Match header\n if request.headers.get(\'If-None-Match\') == etag:\n return \'\', 304 # Not Modified\n\n response = jsonify({\n "data": {\n "owner": tool.owner,\n "name": tool.name,\n "resolved_version": tool.version,\n "config": tool.config_yaml\n }\n })\n response.headers[\'ETag\'] = etag\n response.headers[\'Cache-Control\'] = \'max-age=3600, immutable\'\n return response\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Note:"})," Since tool versions are immutable, the ETag based on ",(0,r.jsx)(n.code,{children:"owner/name@version"})," is permanently stable. The ",(0,r.jsx)(n.code,{children:"published_at"})," timestamp is included for defense-in-depth but won't change."]}),"\n",(0,r.jsx)(n.h3,{id:"db-vs-repo-read-strategy",children:"DB vs Repo Read Strategy"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Scenario"}),(0,r.jsx)(n.th,{children:"Read From"}),(0,r.jsx)(n.th,{children:"Reason"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Normal operation"}),(0,r.jsx)(n.td,{children:"SQLite DB"}),(0,r.jsx)(n.td,{children:"Fast, indexed, FTS"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"DB empty/corrupted"}),(0,r.jsx)(n.td,{children:"Gitea repo"}),(0,r.jsx)(n.td,{children:"Fallback/recovery"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Webhook sync in progress"}),(0,r.jsx)(n.td,{children:"DB (stale OK)"}),(0,r.jsx)(n.td,{children:"Avoid blocking reads"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Search query"}),(0,r.jsx)(n.td,{children:"SQLite FTS5"}),(0,r.jsx)(n.td,{children:"Full-text search"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Download specific version"}),(0,r.jsx)(n.td,{children:"DB, fallback to repo"}),(0,r.jsx)(n.td,{children:"DB is cache, repo is truth"})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"staleness-detection",children:"Staleness Detection"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"STALE_THRESHOLD = timedelta(minutes=10)\n\ndef is_db_stale():\n last_sync = db.get_last_sync_time()\n return datetime.utcnow() - last_sync > STALE_THRESHOLD\n\n@app.route('/tools/search')\ndef search_tools(q):\n results = db.search_fts(q)\n\n response = jsonify({\"results\": results})\n if is_db_stale():\n response.headers['X-Search-Index-Stale'] = 'true'\n response.headers['X-Last-Sync'] = db.get_last_sync_time().isoformat()\n\n return response\n"})}),"\n",(0,r.jsx)(n.h2,{id:"error-model",children:"Error Model"}),"\n",(0,r.jsx)(n.h3,{id:"response-envelopes",children:"Response Envelopes"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Success response:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "data": { ... },\n "meta": {\n "page": 1,\n "per_page": 20,\n "total": 42,\n "total_pages": 3\n }\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Error response:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "error": {\n "code": "TOOL_NOT_FOUND",\n "message": "Tool \'foo/bar\' does not exist",\n "details": {\n "owner": "foo",\n "name": "bar",\n "suggestion": "Did you mean \'rob/bar\'?"\n },\n "docs_url": "https://cmdforge.brrd.tech/docs/errors#TOOL_NOT_FOUND"\n }\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"error-codes",children:"Error Codes"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Code"}),(0,r.jsx)(n.th,{children:"HTTP"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"TOOL_NOT_FOUND"})}),(0,r.jsx)(n.td,{children:"404"}),(0,r.jsx)(n.td,{children:"Tool does not exist"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"VERSION_NOT_FOUND"})}),(0,r.jsx)(n.td,{children:"404"}),(0,r.jsx)(n.td,{children:"Requested version doesn't exist"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"VERSION_EXISTS"})}),(0,r.jsx)(n.td,{children:"409"}),(0,r.jsx)(n.td,{children:"Cannot overwrite published version"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"INVALID_VERSION"})}),(0,r.jsx)(n.td,{children:"400"}),(0,r.jsx)(n.td,{children:"Version string is not valid semver"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"INVALID_CONSTRAINT"})}),(0,r.jsx)(n.td,{children:"400"}),(0,r.jsx)(n.td,{children:"Version constraint syntax error"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"CONSTRAINT_UNSATISFIABLE"})}),(0,r.jsx)(n.td,{children:"404"}),(0,r.jsx)(n.td,{children:"No version matches constraint"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"VALIDATION_ERROR"})}),(0,r.jsx)(n.td,{children:"400"}),(0,r.jsx)(n.td,{children:"Tool config validation failed"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"UNAUTHORIZED"})}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:"Missing or invalid auth token"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"FORBIDDEN"})}),(0,r.jsx)(n.td,{children:"403"}),(0,r.jsx)(n.td,{children:"Token valid but lacks permission"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RATE_LIMITED"})}),(0,r.jsx)(n.td,{children:"429"}),(0,r.jsx)(n.td,{children:"Too many requests"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SLUG_TAKEN"})}),(0,r.jsx)(n.td,{children:"409"}),(0,r.jsx)(n.td,{children:"Namespace slug already registered"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ACCOUNT_LOCKED"})}),(0,r.jsx)(n.td,{children:"403"}),(0,r.jsx)(n.td,{children:"Too many failed login attempts"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SERVER_ERROR"})}),(0,r.jsx)(n.td,{children:"500"}),(0,r.jsx)(n.td,{children:"Internal error (logged for debugging)"})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"error-scenarios-and-fallbacks",children:"Error Scenarios and Fallbacks"}),"\n",(0,r.jsx)(n.h3,{id:"cli-error-handling",children:"CLI Error Handling"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Scenario"}),(0,r.jsx)(n.th,{children:"CLI Behavior"}),(0,r.jsx)(n.th,{children:"User Message"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Registry offline"}),(0,r.jsx)(n.td,{children:"Use cached tools if available"}),(0,r.jsx)(n.td,{children:'"Registry unavailable. Using cached version."'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Tool not found"}),(0,r.jsx)(n.td,{children:"Check cache, then fail"}),(0,r.jsx)(n.td,{children:"\"Tool 'foo/bar' not found in registry or cache.\""})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Version constraint unsatisfiable"}),(0,r.jsx)(n.td,{children:"Show available versions"}),(0,r.jsx)(n.td,{children:"\"No version matches '>=5.0.0'. Available: 1.0.0, 1.1.0, 1.2.0\""})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Auth token expired"}),(0,r.jsx)(n.td,{children:"Prompt for new token"}),(0,r.jsx)(n.td,{children:'"Token expired. Please re-authenticate."'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Rate limited"}),(0,r.jsx)(n.td,{children:"Wait and retry (backoff)"}),(0,r.jsx)(n.td,{children:'"Rate limited. Retrying in 30 seconds..."'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Network timeout"}),(0,r.jsx)(n.td,{children:"Retry with backoff, then fail"}),(0,r.jsx)(n.td,{children:'"Connection timed out. Check your network."'})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"validation-failure-details",children:"Validation Failure Details"}),"\n",(0,r.jsxs)(n.p,{children:["When ",(0,r.jsx)(n.code,{children:"VALIDATION_ERROR"})," occurs, provide specific field errors:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "error": {\n "code": "VALIDATION_ERROR",\n "message": "Tool configuration is invalid",\n "details": {\n "errors": [\n {\n "path": "steps[0].provider",\n "message": "Provider \'gpt5\' is not recognized",\n "allowed": ["claude", "openai", "ollama", "mock"]\n },\n {\n "path": "version",\n "message": "Version \'1.0\' is not valid semver (use \'1.0.0\')"\n }\n ]\n },\n "docs_url": "https://cmdforge.brrd.tech/docs/tool-format"\n }\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"dependency-resolution-failures",children:"Dependency Resolution Failures"}),"\n",(0,r.jsxs)(n.p,{children:["When ",(0,r.jsx)(n.code,{children:"cmdforge install"})," fails on a manifest:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'$ cmdforge install\n\nError: Could not resolve all dependencies\n\n rob/summarize@^2.0.0\n \u2717 No matching version (latest: 1.2.0)\n\n alice/translate@>=1.0.0\n \u2713 Found 1.3.0\n\nSuggestions:\n - Update rob/summarize constraint to "^1.0.0"\n - Contact the tool author for a v2 release\n'})}),"\n",(0,r.jsx)(n.h3,{id:"graceful-degradation",children:"Graceful Degradation"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Component Down"}),(0,r.jsx)(n.th,{children:"Fallback Behavior"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"API server"}),(0,r.jsxs)(n.td,{children:["CLI uses ",(0,r.jsx)(n.code,{children:"~/.cmdforge/registry/index.json"})," for search"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Gitea repo"}),(0,r.jsx)(n.td,{children:"API serves from DB cache (may be stale)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"FTS5 index"}),(0,r.jsx)(n.td,{children:"Fall back to LIKE queries (slower but works)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Network"}),(0,r.jsx)(n.td,{children:"Use locally installed tools, skip registry features"})]})]})]}),"\n",(0,r.jsx)(n.h2,{id:"ux-requirements-clitui",children:"UX Requirements (CLI/TUI)"}),"\n",(0,r.jsx)(n.h3,{id:"publishing-ux",children:"Publishing UX"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"cmdforge registry publish --dry-run"})," validates locally and shows what would be submitted:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'$ cmdforge registry publish --dry-run\n\nValidating tool...\n\u2713 config.yaml is valid\n\u2713 README.md exists (2.3 KB)\n\u2713 Version 1.1.0 not yet published\n\nWould submit:\n Owner: rob\n Name: summarize\n Version: 1.1.0\n Category: text-processing\n Tags: summarization, ai, text\n\nConfig preview:\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nname: summarize\nversion: "1.1.0"\ndescription: Summarize text using AI\n...\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nRun without --dry-run to submit for review.\n'})}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Version bump reminder:"})," CLI warns if version hasn't changed from published:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"\u26a0 Version 1.0.0 is already published. Bump version in config.yaml to publish changes.\n"})}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:"First-time publishing flow prompts for token and saves it to config."}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"progress-indicators",children:"Progress Indicators"}),"\n",(0,r.jsx)(n.p,{children:"Long-running operations show progress:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"$ cmdforge install\n\nInstalling project dependencies...\n [1/3] rob/summarize@^1.0.0\n Resolving version... 1.2.0\n Downloading... done\n Installing... done \u2713\n [2/3] alice/translate@>=2.0.0\n Resolving version... 2.1.0\n Downloading... done\n Installing... done \u2713\n [3/3] official/code-review@*\n Resolving version... 1.0.0\n Downloading... done\n Installing... done \u2713\n\n\u2713 Installed 3 tools\n"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"$ cmdforge registry publish\n\nSubmitting rob/summarize@1.1.0...\n Validating... done \u2713\n Uploading... done \u2713\n Creating PR... done \u2713\n\n\u2713 PR created: https://gitea.brrd.tech/rob/CmdForge-Registry/pulls/42\n\nYour tool is pending review. You'll receive an email when it's approved.\n"})}),"\n",(0,r.jsx)(n.h3,{id:"tui-browse",children:"TUI Browse"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"cmdforge registry browse"})," opens a full-screen terminal UI:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"\u250c\u2500 CmdForge Registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Search: [________________] [All Categories \u25bc] [Sort: Popular \u25bc] \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u2502\n\u2502 \u25b6 rob/summarize v1.2.0 \u2b07 142 \u2502\n\u2502 Summarize text using AI \u2502\n\u2502 [text-processing] [ai] [summarization] \u2502\n\u2502 \u2502\n\u2502 alice/translate v2.1.0 \u2b07 98 \u2502\n\u2502 Translate text between languages \u2502\n\u2502 [text-processing] [translation] \u2502\n\u2502 \u2502\n\u2502 official/code-review v1.0.0 \u2b07 87 \u2502\n\u2502 AI-powered code review \u2502\n\u2502 [code] [review] [ai] \u2502\n\u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u2191\u2193 Navigate Enter: Details i: Install /: Search q: Quit \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Keyboard shortcuts:"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Key"}),(0,r.jsx)(n.th,{children:"Action"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"\u2191/\u2193"})," or ",(0,r.jsx)(n.code,{children:"j/k"})]}),(0,r.jsx)(n.td,{children:"Navigate list"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Enter"})}),(0,r.jsx)(n.td,{children:"View tool details"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"i"})}),(0,r.jsx)(n.td,{children:"Install selected tool"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"/"})}),(0,r.jsx)(n.td,{children:"Focus search box"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"c"})}),(0,r.jsx)(n.td,{children:"Change category filter"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"s"})}),(0,r.jsx)(n.td,{children:"Change sort order"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"?"})}),(0,r.jsx)(n.td,{children:"Show help"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"q"})}),(0,r.jsx)(n.td,{children:"Quit"})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Virtual scrolling:"})," For large tool lists (>100), use virtual scrolling to maintain performance."]}),"\n",(0,r.jsx)(n.h3,{id:"project-initialization",children:"Project Initialization"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"$ cmdforge init\n\nCreating cmdforge.yaml...\n\nProject name [my-project]: my-ai-project\nVersion [1.0.0]:\n\nWould you like to add any tools? (search with 's', skip with Enter)\n> s\nSearch: summ\n 1. rob/summarize v1.2.0 - Summarize text using AI\n 2. alice/summary v1.0.0 - Generate summaries\n\nAdd tool (number, or Enter to finish): 1\nAdded rob/summarize@^1.2.0\n\nAdd tool (number, or Enter to finish):\n\n\u2713 Created cmdforge.yaml\n\nname: my-ai-project\nversion: \"1.0.0\"\ndependencies:\n - name: rob/summarize\n version: \"^1.2.0\"\n\nRun 'cmdforge install' to install dependencies.\n"})}),"\n",(0,r.jsx)(n.h3,{id:"accessibility",children:"Accessibility"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"CLI:"})," All output works with screen readers, no color-only information"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"TUI:"})," Full keyboard navigation, high-contrast mode support"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Web UI:"})," WCAG 2.1 AA compliance target","\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Semantic HTML"}),"\n",(0,r.jsx)(n.li,{children:"ARIA labels for interactive elements"}),"\n",(0,r.jsx)(n.li,{children:"Focus management in modals"}),"\n",(0,r.jsx)(n.li,{children:"Skip links for navigation"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"offline-cache",children:"Offline Cache"}),"\n",(0,r.jsx)(n.p,{children:"Cache registry index locally:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"~/.cmdforge/registry/index.json\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Refresh when older than 24 hours; support ",(0,r.jsx)(n.code,{children:"--offline"})," and ",(0,r.jsx)(n.code,{children:"--refresh"})," flags."]}),"\n",(0,r.jsx)(n.h3,{id:"index-integrity",children:"Index Integrity"}),"\n",(0,r.jsxs)(n.p,{children:["The cached ",(0,r.jsx)(n.code,{children:"index.json"})," includes integrity metadata:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "version": "1.0",\n "generated_at": "2025-01-20T12:00:00Z",\n "checksum": "sha256:abc123...",\n "tool_count": 142,\n "tools": [...]\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"API response headers:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:'ETag: "abc123def456"\nX-Index-Checksum: sha256:abc123...\nX-Index-Generated: 2025-01-20T12:00:00Z\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"CLI verification:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"def verify_cached_index():\n \"\"\"Verify cached index integrity on load\"\"\"\n cached = load_cached_index()\n if not cached:\n return None\n\n # Verify checksum\n content = json.dumps(cached['tools'], sort_keys=True)\n computed = hashlib.sha256(content.encode()).hexdigest()\n\n if computed != cached.get('checksum', '').replace('sha256:', ''):\n logger.warning(\"Cached index checksum mismatch, will refresh\")\n return None\n\n return cached\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Corruption handling:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"If checksum fails, discard cache and fetch fresh"}),"\n",(0,r.jsx)(n.li,{children:"If partial write detected (missing fields), discard and refresh"}),"\n",(0,r.jsx)(n.li,{children:'CLI shows warning: "Cached index corrupted, fetching fresh copy..."'}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"web-ui-vision",children:"Web UI Vision"}),"\n",(0,r.jsx)(n.p,{children:"The registry includes a full website, not just an API:"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Site structure:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"cmdforge.brrd.tech (or gitea.brrd.tech/registry)\n\u251c\u2500\u2500 / # Landing page\n\u251c\u2500\u2500 /tools # Browse all tools\n\u251c\u2500\u2500 /tools/{owner}/{name} # Tool detail page\n\u251c\u2500\u2500 /categories # Browse by category\n\u251c\u2500\u2500 /categories/{name} # Tools in category\n\u251c\u2500\u2500 /collections # Browse curated collections\n\u251c\u2500\u2500 /collections/{name} # Collection detail page\n\u251c\u2500\u2500 /search?q=... # Search results\n\u251c\u2500\u2500 /docs # Documentation\n\u2502 \u251c\u2500\u2500 /docs/getting-started\n\u2502 \u251c\u2500\u2500 /docs/creating-tools\n\u2502 \u251c\u2500\u2500 /docs/publishing\n\u2502 \u2514\u2500\u2500 /docs/best-practices\n\u251c\u2500\u2500 /tutorials # Step-by-step guides\n\u2502 \u251c\u2500\u2500 /tutorials/first-tool\n\u2502 \u251c\u2500\u2500 /tutorials/chaining-steps\n\u2502 \u2514\u2500\u2500 /tutorials/code-steps\n\u251c\u2500\u2500 /examples # Example projects\n\u251c\u2500\u2500 /blog # Updates, announcements (optional)\n\u251c\u2500\u2500 /register # Publisher registration\n\u251c\u2500\u2500 /login # Publisher login\n\u251c\u2500\u2500 /dashboard # Publisher dashboard\n\u2502 \u251c\u2500\u2500 /dashboard/tools # My published tools\n\u2502 \u251c\u2500\u2500 /dashboard/connections # Connected apps\n\u2502 \u2514\u2500\u2500 /dashboard/settings # Account settings\n\u2514\u2500\u2500 /api/v1/... # API endpoints\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Landing page content:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'Hero: "Share and discover AI-powered CLI tools"'}),"\n",(0,r.jsx)(n.li,{children:"Quick install example"}),"\n",(0,r.jsx)(n.li,{children:"Featured/popular tools"}),"\n",(0,r.jsx)(n.li,{children:"Category highlights"}),"\n",(0,r.jsx)(n.li,{children:'"Get Started" CTA'}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Tool detail page:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Name, description, version, author"}),"\n",(0,r.jsx)(n.li,{children:"README rendered as markdown (sanitized)"}),"\n",(0,r.jsx)(n.li,{children:"Install command (copy-to-clipboard)"}),"\n",(0,r.jsx)(n.li,{children:"Version history"}),"\n",(0,r.jsx)(n.li,{children:"Download stats"}),"\n",(0,r.jsx)(n.li,{children:"Category/tags"}),"\n",(0,r.jsx)(n.li,{children:'"Report" button for abuse'}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"readme-security",children:"README Security"}),"\n",(0,r.jsx)(n.p,{children:"When rendering README markdown, apply XSS sanitization:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-python",children:"import bleach\nfrom markdown import markdown\n\nALLOWED_TAGS = [\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n 'p', 'br', 'hr',\n 'ul', 'ol', 'li',\n 'strong', 'em', 'code', 'pre',\n 'blockquote',\n 'a', 'img',\n 'table', 'thead', 'tbody', 'tr', 'th', 'td'\n]\n\nALLOWED_ATTRS = {\n 'a': ['href', 'title'],\n 'img': ['src', 'alt', 'title'],\n 'code': ['class'], # for syntax highlighting\n}\n\ndef render_readme_safe(readme_raw: str) -> str:\n \"\"\"Convert markdown to sanitized HTML\"\"\"\n # Convert markdown to HTML\n html = markdown(readme_raw, extensions=['fenced_code', 'tables'])\n\n # Sanitize to prevent XSS\n safe_html = bleach.clean(\n html,\n tags=ALLOWED_TAGS,\n attributes=ALLOWED_ATTRS,\n strip=True\n )\n\n # Linkify URLs\n safe_html = bleach.linkify(safe_html)\n\n return safe_html\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Storage strategy:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Store raw README in ",(0,r.jsx)(n.code,{children:"tools.readme"})]}),"\n",(0,r.jsx)(n.li,{children:"Render and sanitize on request (or cache rendered HTML)"}),"\n",(0,r.jsx)(n.li,{children:"Never trust client-submitted HTML directly"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Tech stack options:"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Option"}),(0,r.jsx)(n.th,{children:"Pros"}),(0,r.jsx)(n.th,{children:"Cons"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Flask + Jinja + Tailwind"}),(0,r.jsx)(n.td,{children:"Simple, Python-only, fast to build"}),(0,r.jsx)(n.td,{children:"Less interactive"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"FastAPI + Vue/React SPA"}),(0,r.jsx)(n.td,{children:"Modern, interactive"}),(0,r.jsx)(n.td,{children:"More complex, separate build"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Astro/Next.js"}),(0,r.jsx)(n.td,{children:"Great SEO, static-first"}),(0,r.jsx)(n.td,{children:"Different stack (Node.js)"})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Recommendation:"})," Flask + Jinja + Tailwind for v1"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Keeps everything in Python"}),"\n",(0,r.jsx)(n.li,{children:"Server-rendered is fine for a registry"}),"\n",(0,r.jsx)(n.li,{children:"Good SEO out of the box"}),"\n",(0,r.jsx)(n.li,{children:"Can add interactivity with Alpine.js or htmx if needed"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Monetization considerations:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"AdSense-compatible (server-rendered pages)"}),"\n",(0,r.jsx)(n.li,{children:"Analytics tracking for traffic insights"}),"\n",(0,r.jsx)(n.li,{children:"Future: sponsored tools, featured placements"}),"\n",(0,r.jsx)(n.li,{children:"Future: premium publisher tiers (more tools, priority review)"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"registry-curation-system",children:"Registry Curation System"}),"\n",(0,r.jsx)(n.p,{children:"The registry includes a moderation system for content curation, abuse prevention, and quality control."}),"\n",(0,r.jsx)(n.h3,{id:"roles-and-permissions",children:"Roles and Permissions"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Role"}),(0,r.jsx)(n.th,{children:"Permissions"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"user"})}),(0,r.jsx)(n.td,{children:"Publish tools, manage own tools, view public content"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"moderator"})}),(0,r.jsx)(n.td,{children:"All user permissions + approve/reject tools, resolve reports, view all publishers"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"admin"})}),(0,r.jsx)(n.td,{children:"All moderator permissions + ban/unban publishers, change roles, delete tools, view audit log"})]})]})]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Database columns:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-sql",children:"-- In publishers table\nrole TEXT DEFAULT 'user', -- 'user', 'moderator', 'admin'\nbanned INTEGER DEFAULT 0,\nbanned_at TIMESTAMP,\nbanned_by TEXT,\nban_reason TEXT,\n\n-- In tools table\nvisibility TEXT DEFAULT 'public', -- 'public', 'private', 'unlisted'\nmoderation_status TEXT DEFAULT 'pending', -- 'pending', 'approved', 'rejected', 'removed'\nmoderation_note TEXT,\nmoderated_by TEXT,\nmoderated_at TIMESTAMP,\n"})}),"\n",(0,r.jsx)(n.h3,{id:"tool-visibility",children:"Tool Visibility"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Visibility"}),(0,r.jsx)(n.th,{children:"In Search/List"}),(0,r.jsx)(n.th,{children:"Direct Link"}),(0,r.jsx)(n.th,{children:"Who Can See"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"public"})}),(0,r.jsx)(n.td,{children:"Yes (if approved)"}),(0,r.jsx)(n.td,{children:"Yes (if approved)"}),(0,r.jsx)(n.td,{children:"Everyone"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"private"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:"Owner only"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"unlisted"})}),(0,r.jsx)(n.td,{children:"No"}),(0,r.jsx)(n.td,{children:"Yes (if approved)"}),(0,r.jsx)(n.td,{children:"Anyone with link"})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"moderation-workflow",children:"Moderation Workflow"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"Tool Published\n \u2502\n \u251c\u2500\u2500 visibility = 'private' or 'unlisted'\n \u2502 \u2514\u2500\u2500 Auto-approved (moderation_status = 'approved')\n \u2502\n \u2514\u2500\u2500 visibility = 'public'\n \u2514\u2500\u2500 moderation_status = 'pending'\n \u2502\n \u251c\u2500\u2500 Moderator approves \u2192 'approved' \u2192 Visible in search\n \u251c\u2500\u2500 Moderator rejects \u2192 'rejected' \u2192 Not visible\n \u2514\u2500\u2500 Moderator removes \u2192 'removed' \u2192 Removed from view\n"})}),"\n",(0,r.jsx)(n.h3,{id:"admin-api-endpoints",children:"Admin API Endpoints"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Tool Moderation (moderator+):"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/admin/tools/pending # List pending tools\nGET /api/v1/admin/tools/ # Get full tool details for review\nPOST /api/v1/admin/tools//approve # Approve a tool\nPOST /api/v1/admin/tools//reject # Reject with reason (required)\nPOST /api/v1/admin/tools//remove # Soft-delete approved tool\nDELETE /api/v1/admin/tools/ # Hard delete (admin only)\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsxs)(n.strong,{children:["Tool Detail Response (",(0,r.jsx)(n.code,{children:"GET /api/v1/admin/tools/"}),"):"]})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "id": 123,\n "owner": "alice",\n "name": "my-tool",\n "version": "1.0.0",\n "description": "Tool description",\n "category": "Text Processing",\n "tags": "ai,text",\n "published_at": "2025-01-15T10:30:00Z",\n "publisher_name": "Alice Smith",\n "visibility": "public",\n "moderation_status": "pending",\n "scrutiny_status": "pending_review",\n "scrutiny_report": {\n "findings": [\n {"check": "shell_commands", "result": "warning", "message": "...", "suggestion": "..."}\n ]\n },\n "config": {\n "name": "my-tool",\n "description": "...",\n "arguments": [...],\n "steps": [...]\n },\n "readme": "# My Tool\\n\\nDocumentation here..."\n }\n}\n'})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Publisher Management (moderator+ to view, admin to modify):"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/admin/publishers # List all publishers\nGET /api/v1/admin/publishers/ # Publisher details\nPOST /api/v1/admin/publishers//ban # Ban with reason (admin)\nPOST /api/v1/admin/publishers//unban # Unban (admin)\nPOST /api/v1/admin/publishers//role # Change role (admin)\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Reports (moderator+):"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/admin/reports # List reports\nPOST /api/v1/admin/reports//resolve # Resolve with action\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Audit Log (admin only):"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"GET /api/v1/admin/audit-log # View moderation history\n ?target_type=tool|publisher\n ?target_id=\n ?actor_id=\n ?since=\n"})}),"\n",(0,r.jsx)(n.h3,{id:"ban-behavior",children:"Ban Behavior"}),"\n",(0,r.jsx)(n.p,{children:"When a publisher is banned:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"banned"})," set to 1, ",(0,r.jsx)(n.code,{children:"banned_at"}),", ",(0,r.jsx)(n.code,{children:"banned_by"}),", ",(0,r.jsx)(n.code,{children:"ban_reason"})," recorded"]}),"\n",(0,r.jsx)(n.li,{children:"All active API tokens revoked"}),"\n",(0,r.jsxs)(n.li,{children:["All their tools set to ",(0,r.jsx)(n.code,{children:"moderation_status = 'removed'"})]}),"\n",(0,r.jsx)(n.li,{children:"Action logged to audit trail"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Banned publishers see error on any authenticated API call:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "error": {\n "code": "ACCOUNT_BANNED",\n "message": "Your account has been banned: "\n }\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"report-resolution-actions",children:"Report Resolution Actions"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Action"}),(0,r.jsx)(n.th,{children:"Effect"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"dismiss"})}),(0,r.jsx)(n.td,{children:"Close report, no action taken"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"warn"})}),(0,r.jsx)(n.td,{children:"Close report, no automated action (manual warning)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"remove_tool"})}),(0,r.jsx)(n.td,{children:"Remove the reported tool"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ban_publisher"})}),(0,r.jsx)(n.td,{children:"Ban the tool's publisher"})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"audit-log",children:"Audit Log"}),"\n",(0,r.jsx)(n.p,{children:"All moderation actions are logged:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-sql",children:"CREATE TABLE audit_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n action TEXT NOT NULL, -- 'approve_tool', 'reject_tool', 'ban_publisher', etc.\n target_type TEXT NOT NULL, -- 'tool', 'publisher', 'report'\n target_id TEXT NOT NULL,\n actor_id TEXT NOT NULL, -- Who performed the action\n details TEXT, -- JSON with additional context\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"web-ui-admin-dashboard",children:"Web UI Admin Dashboard"}),"\n",(0,r.jsx)(n.p,{children:'Moderators and admins see an "Admin Panel" link in their dashboard sidebar leading to:'}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"/dashboard/admin"})," - Overview with pending counts"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"/dashboard/admin/pending"})," - Pending tools queue"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"/dashboard/admin/publishers"})," - Publisher management"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"/dashboard/admin/reports"})," - Report queue"]}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"pending-tools-review-page",children:"Pending Tools Review Page"}),"\n",(0,r.jsxs)(n.p,{children:["The pending tools page (",(0,r.jsx)(n.code,{children:"/dashboard/admin/pending"}),") provides a comprehensive interface for reviewing submitted tools:"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Table View:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Tool name (clickable to open detail modal)"}),"\n",(0,r.jsx)(n.li,{children:"Publisher name and category"}),"\n",(0,r.jsx)(n.li,{children:"Scrutiny status with expandable warnings"}),"\n",(0,r.jsx)(n.li,{children:"Submission date"}),"\n",(0,r.jsx)(n.li,{children:"Approve/Reject action buttons"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Pagination:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Page number links with ellipsis for large ranges"}),"\n",(0,r.jsxs)(n.li,{children:["First (",(0,r.jsx)(n.code,{children:"\xab"}),") and Last (",(0,r.jsx)(n.code,{children:"\xbb"}),") page buttons"]}),"\n",(0,r.jsx)(n.li,{children:"Previous/Next navigation"}),"\n",(0,r.jsx)(n.li,{children:'"Page X of Y (total)" indicator'}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Tool Detail Modal:"})}),"\n",(0,r.jsx)(n.p,{children:"Clicking a tool name opens a draggable modal showing:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Scrutiny Warnings"})," - Yellow warning boxes at the top showing any security or quality concerns from automated analysis, including:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:'Check name (e.g., "shell_commands", "network_access")'}),"\n",(0,r.jsx)(n.li,{children:"Warning message"}),"\n",(0,r.jsx)(n.li,{children:"Suggestion for resolution"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Description"})," - Tool's description text"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Arguments"})," - List of tool arguments with flags, variables, and descriptions"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Steps"})," - Full display of all tool steps:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Prompt steps"}),": Shows provider, output variable, and full prompt content"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Code steps"}),": Shows output variable and code with syntax highlighting (dark theme)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Tool steps"}),": Shows tool name and arguments"]}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"README"})," - Full README content if provided"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Modal Features:"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Draggable by header bar"}),"\n",(0,r.jsx)(n.li,{children:"Scrollable content area (header and buttons stay fixed)"}),"\n",(0,r.jsx)(n.li,{children:"Approve/Reject buttons in modal footer"}),"\n",(0,r.jsx)(n.li,{children:"Dark overlay prevents interaction with page behind"}),"\n",(0,r.jsx)(n.li,{children:"Background scroll locked while modal is open"}),"\n",(0,r.jsx)(n.li,{children:"Closes with X button or after action"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"creating-the-first-admin",children:"Creating the First Admin"}),"\n",(0,r.jsx)(n.p,{children:"After deployment, create the first admin via direct SQL:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-sql",children:"UPDATE publishers SET role = 'admin' WHERE slug = 'rob';\n"})}),"\n",(0,r.jsx)(n.p,{children:"Subsequent admins can be promoted via the web UI or API."}),"\n",(0,r.jsx)(n.h2,{id:"implementation-phases",children:"Implementation Phases"}),"\n",(0,r.jsx)(n.h3,{id:"phase-1-foundation",children:"Phase 1: Foundation"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Define ",(0,r.jsx)(n.code,{children:"cmdforge.yaml"})," manifest format"]}),"\n",(0,r.jsx)(n.li,{children:"Implement tool resolution order (local \u2192 global \u2192 registry)"}),"\n",(0,r.jsx)(n.li,{children:"Create CmdForge-Registry repo on Gitea (bootstrap)"}),"\n",(0,r.jsx)(n.li,{children:"Add 3-5 example tools to seed the registry"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"phase-2-core-backend",children:"Phase 2: Core Backend"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Set up Flask/FastAPI project structure"}),"\n",(0,r.jsx)(n.li,{children:"Implement SQLite database schema"}),"\n",(0,r.jsx)(n.li,{children:"Build core API endpoints (list, search, get, download)"}),"\n",(0,r.jsx)(n.li,{children:"Implement webhook receiver for Gitea sync"}),"\n",(0,r.jsx)(n.li,{children:"Set up HMAC verification"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"phase-3-cli-commands",children:"Phase 3: CLI Commands"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"cmdforge registry search"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"cmdforge registry install"})}),"\n",(0,r.jsx)(n.li,{children:(0,r.jsx)(n.code,{children:"cmdforge registry info"})}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"cmdforge registry browse"})," (TUI)"]}),"\n",(0,r.jsx)(n.li,{children:"Local index caching"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"phase-4-publishing",children:"Phase 4: Publishing"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Publisher registration (web UI)"}),"\n",(0,r.jsx)(n.li,{children:"Token management"}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"cmdforge registry publish"})," command"]}),"\n",(0,r.jsx)(n.li,{children:"PR creation via Gitea API"}),"\n",(0,r.jsx)(n.li,{children:"CI validation workflows"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"phase-5-project-dependencies",children:"Phase 5: Project Dependencies"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"cmdforge install"})," (from manifest)"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"cmdforge add"})," command"]}),"\n",(0,r.jsx)(n.li,{children:"Runtime override application"}),"\n",(0,r.jsx)(n.li,{children:"Dependency resolution"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"phase-6-smart-features",children:"Phase 6: Smart Features"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"SQLite FTS5 search index"}),"\n",(0,r.jsx)(n.li,{children:"AI-powered auto-categorization"}),"\n",(0,r.jsx)(n.li,{children:"Duplicate/similarity detection"}),"\n",(0,r.jsx)(n.li,{children:"Security scanning"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"phase-7-full-web-ui",children:"Phase 7: Full Web UI"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Landing page"}),"\n",(0,r.jsx)(n.li,{children:"Tool browsing/search pages"}),"\n",(0,r.jsx)(n.li,{children:"Tool detail pages with README rendering"}),"\n",(0,r.jsx)(n.li,{children:"Publisher dashboard"}),"\n",(0,r.jsx)(n.li,{children:"Documentation/tutorials section"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"phase-8-polish--scale",children:"Phase 8: Polish & Scale"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Rate limiting"}),"\n",(0,r.jsx)(n.li,{children:"Abuse reporting"}),"\n",(0,r.jsx)(n.li,{children:"Analytics integration"}),"\n",(0,r.jsx)(n.li,{children:"Performance optimization"}),"\n",(0,r.jsx)(n.li,{children:"Monitoring/alerting"}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,l.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(a,{...e})}):a(e)}},8453(e,n,s){s.d(n,{R:()=>d,x:()=>t});var i=s(6540);const r={},l=i.createContext(r);function d(e){const n=i.useContext(l);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function t(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:d(e.components),i.createElement(l.Provider,{value:n},e.children)}}}]);