1897 lines
58 KiB
Markdown
1897 lines
58 KiB
Markdown
# SmartTools Registry Design
|
|
|
|
## Purpose
|
|
Build a centralized registry for SmartTools to enable discovery, publishing, dependency management, and future curation at scale.
|
|
|
|
## Terminology
|
|
|
|
| Term | Definition |
|
|
|------|------------|
|
|
| **Tool definition** | The full YAML file in the registry (`config.yaml`) containing name, steps, arguments, etc. |
|
|
| **Tool config** | The configuration within a tool definition (arguments, steps, provider settings) |
|
|
| **smarttools.yaml** | Project manifest file declaring tool dependencies and overrides |
|
|
| **config.yaml** | The tool definition file, both in registry and when installed locally |
|
|
| **Owner** | Immutable namespace slug identifying the publisher (e.g., `rob`, `alice`) |
|
|
| **Publisher** | A registered user who can publish tools to the registry |
|
|
| **Wrapper script** | Auto-generated bash script in `~/.local/bin/` that invokes a tool |
|
|
|
|
**Canonical naming:** Use `SmartTools-Registry` (capitalized, hyphenated) for the repository name.
|
|
|
|
## Diagram References
|
|
- System overview: `discussions/diagrams/smarttools-registry_rob_1.puml`
|
|
- Data flows: `discussions/diagrams/smarttools-registry_rob_5.puml`
|
|
|
|
## System Overview
|
|
Users interact via the CLI and a future Web UI. Both call a Registry API hosted at `https://gitea.brrd.tech/api/v1` (future alias: `registry.smarttools.dev/api/v1`). The API syncs from a Gitea-backed registry repo and maintains a SQLite cache/search index.
|
|
|
|
**Canonical API base path:** `https://gitea.brrd.tech/api/v1`
|
|
|
|
All API endpoints are versioned under `/api/v1`. When breaking changes are needed, a new version (`/api/v2`) will be introduced with deprecation notices.
|
|
|
|
Core API endpoints:
|
|
- `GET /api/v1/tools`
|
|
- `GET /api/v1/tools/search?q=...`
|
|
- `GET /api/v1/tools/{owner}/{name}`
|
|
- `GET /api/v1/tools/{owner}/{name}/versions`
|
|
- `GET /api/v1/tools/{owner}/{name}/download?version=...`
|
|
- `POST /api/v1/tools` (publish)
|
|
- `GET /api/v1/categories`
|
|
- `GET /api/v1/stats/popular`
|
|
- `POST /api/v1/webhook/gitea`
|
|
|
|
### Pagination
|
|
|
|
All list endpoints support pagination:
|
|
|
|
| Parameter | Default | Max | Description |
|
|
|-----------|---------|-----|-------------|
|
|
| `page` | 1 | - | Page number (1-indexed) |
|
|
| `per_page` | 20 | 100 | Items per page |
|
|
| `sort` | `downloads` | - | Sort field |
|
|
| `order` | `desc` | - | Sort order (asc/desc) |
|
|
|
|
**Stable ordering:** To ensure deterministic results across pages, sorting includes a secondary key:
|
|
- Primary: requested field (e.g., `downloads`)
|
|
- Secondary: `published_at` (desc)
|
|
- Tertiary: `id` (for absolute stability)
|
|
|
|
```sql
|
|
ORDER BY downloads DESC, published_at DESC, id DESC
|
|
LIMIT 20 OFFSET 0
|
|
```
|
|
|
|
**Response pagination metadata:**
|
|
```json
|
|
{
|
|
"data": [...],
|
|
"meta": {
|
|
"page": 1,
|
|
"per_page": 20,
|
|
"total": 142,
|
|
"total_pages": 8
|
|
}
|
|
}
|
|
```
|
|
|
|
### Input Constraints
|
|
|
|
Size limits to prevent oversized uploads:
|
|
|
|
| Field | Max Size | Notes |
|
|
|-------|----------|-------|
|
|
| `config.yaml` | 64 KB | Tool definition |
|
|
| `README.md` | 256 KB | Documentation |
|
|
| Request body | 512 KB | Total POST payload |
|
|
| Tool name | 64 chars | Alphanumeric + hyphen |
|
|
| Description | 500 chars | Short summary |
|
|
| Tag | 32 chars | Individual tag |
|
|
| Tags array | 10 items | Maximum tags per tool |
|
|
|
|
**Validation errors:**
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "PAYLOAD_TOO_LARGE",
|
|
"message": "config.yaml exceeds 64KB limit",
|
|
"details": {
|
|
"field": "config",
|
|
"size": 72000,
|
|
"limit": 65536
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Sort Fields and Indexes
|
|
|
|
**Allowed sort fields:**
|
|
|
|
| Endpoint | Allowed `sort` values |
|
|
|----------|----------------------|
|
|
| `GET /tools` | `downloads`, `published_at`, `name` |
|
|
| `GET /tools/search` | `relevance`, `downloads`, `published_at` |
|
|
| `GET /categories` | `name`, `tool_count` |
|
|
|
|
Invalid sort values return 400:
|
|
```json
|
|
{"error": {"code": "INVALID_SORT", "message": "Unknown sort field 'foo'. Allowed: downloads, published_at, name"}}
|
|
```
|
|
|
|
**Database indexes:**
|
|
```sql
|
|
-- Frequent query patterns
|
|
CREATE INDEX idx_tools_owner_name ON tools(owner, name);
|
|
CREATE INDEX idx_tools_category ON tools(category);
|
|
CREATE INDEX idx_tools_published_at ON tools(published_at DESC);
|
|
CREATE INDEX idx_tools_downloads ON tools(downloads DESC);
|
|
CREATE INDEX idx_tools_owner_name_version ON tools(owner, name, version);
|
|
|
|
-- For pagination stability
|
|
CREATE INDEX idx_tools_sort_stable ON tools(downloads DESC, published_at DESC, id DESC);
|
|
|
|
-- Publisher lookups
|
|
CREATE INDEX idx_publishers_slug ON publishers(slug);
|
|
CREATE INDEX idx_publishers_email ON publishers(email);
|
|
|
|
-- Token lookups
|
|
CREATE INDEX idx_api_tokens_hash ON api_tokens(token_hash);
|
|
CREATE INDEX idx_api_tokens_publisher ON api_tokens(publisher_id);
|
|
```
|
|
|
|
### API Version Compatibility
|
|
|
|
**Forward compatibility:** Clients should ignore unknown fields in API responses:
|
|
|
|
```python
|
|
# Good: ignore unknown fields
|
|
tool = response['data']
|
|
name = tool.get('name')
|
|
# Don't fail if 'new_field' exists but client doesn't know about it
|
|
|
|
# Bad: strict parsing that fails on unknown fields
|
|
tool = ToolSchema.parse(response['data']) # May fail on new fields
|
|
```
|
|
|
|
**Backward compatibility:** The API will:
|
|
- Never remove fields in a version (only deprecate)
|
|
- Never change field types
|
|
- Add new optional fields without version bump
|
|
- Use new version (`/api/v2`) for breaking changes
|
|
|
|
**Deprecation process:**
|
|
1. Add `X-Deprecated-Field: old_field` header
|
|
2. Document in changelog
|
|
3. Remove after 6 months minimum
|
|
4. Major version bump if widely used
|
|
|
|
**Client version header:**
|
|
```
|
|
X-SmartTools-Client: cli/1.2.0
|
|
```
|
|
Helps server track client versions for deprecation decisions.
|
|
|
|
## Source of Truth
|
|
- Gitea registry repo is the source of truth.
|
|
- API syncs repo content into SQLite for fast queries, stats, and FTS5 search.
|
|
- `index.json` remains useful for offline CLI search and as a fallback.
|
|
|
|
If the cache is stale, the API can fall back to repo reads; a warning header may be emitted.
|
|
|
|
## Namespacing and Paths
|
|
Support owner/name from day one:
|
|
- Registry path: `tools/{owner}/{name}/config.yaml`
|
|
- API URL: `/tools/{owner}/{name}`
|
|
- Install: `smarttools registry install rob/summarize`
|
|
- Shorthand: `smarttools registry install summarize` resolves to the official namespace.
|
|
|
|
PR branches: `submit/{owner}/{name}/{version}`.
|
|
|
|
### Namespace Identity
|
|
|
|
The `owner` is an **immutable slug**, not the display name:
|
|
|
|
```sql
|
|
-- In publishers table
|
|
slug TEXT UNIQUE NOT NULL, -- immutable: "rob", "alice-dev"
|
|
display_name TEXT NOT NULL, -- mutable: "Rob", "Alice Developer"
|
|
```
|
|
|
|
**Slug rules:**
|
|
- Lowercase alphanumeric + hyphens only: `^[a-z0-9][a-z0-9-]*[a-z0-9]$`
|
|
- 2-39 characters
|
|
- Cannot start/end with hyphen
|
|
- Set once at registration, cannot be changed
|
|
- Reserved slugs: `official`, `admin`, `system`, `api`, `registry`
|
|
|
|
**Rename policy:**
|
|
- `display_name` can be changed anytime via dashboard
|
|
- `slug` (owner) is permanent to preserve URLs and tool references
|
|
- If a publisher absolutely must change slug (legal reasons, etc.):
|
|
1. Create new account with new slug
|
|
2. Republish tools under new namespace
|
|
3. Mark old tools as deprecated with `replacement` pointing to new namespace
|
|
4. Old namespace remains reserved (cannot be reused by others)
|
|
|
|
**Why immutable:**
|
|
- `rob/summarize@1.0.0` must always resolve to the same tool
|
|
- Prevents namespace hijacking after rename
|
|
- Simplifies caching and CDN strategies
|
|
|
|
## Tool Format (Registry == Local)
|
|
Registry tool folders mirror local tools:
|
|
```
|
|
tools/
|
|
rob/
|
|
summarize/
|
|
config.yaml
|
|
README.md
|
|
```
|
|
|
|
Tool files match the existing SmartTools format. Registry-specific metadata is kept under `registry:`. Deprecation is tool-defined and top-level:
|
|
```yaml
|
|
name: summarize
|
|
version: "1.2.0"
|
|
deprecated: true
|
|
deprecated_message: "Security issue. Use v1.2.1"
|
|
replacement: "rob/summarize@1.2.1"
|
|
registry:
|
|
published_at: "2025-01-15T10:30:00Z"
|
|
downloads: 142
|
|
```
|
|
|
|
**Schema compatibility note:** The current SmartTools config parser may reject unknown top-level keys like `deprecated`, `replacement`, and `registry`. Before implementing registry features:
|
|
1. Update the YAML parser to ignore unknown keys (permissive mode)
|
|
2. Or explicitly define these fields in the Tool dataclass with defaults
|
|
3. Validate registry-specific fields only when publishing, not when running locally
|
|
|
|
This ensures local tools continue to work even if they don't have registry fields.
|
|
|
|
## Versioning and Immutability
|
|
- Unique key: `owner/name + version`.
|
|
- Published versions are immutable.
|
|
- Deprecation uses `deprecated`, `deprecated_message`, and `replacement`.
|
|
- CLI warns on install if a version is deprecated.
|
|
|
|
### Yank Policy
|
|
|
|
Yanking allows removing a version from resolution without deleting it (for auditability):
|
|
|
|
```yaml
|
|
# In tool config
|
|
yanked: true
|
|
yanked_reason: "Critical security vulnerability CVE-2025-1234"
|
|
yanked_at: "2025-01-20T15:00:00Z"
|
|
```
|
|
|
|
**Yanked version behavior:**
|
|
|
|
| Operation | Behavior |
|
|
|-----------|----------|
|
|
| `install foo@1.0.0` (exact) | Warns but allows install |
|
|
| `install foo@^1.0.0` (constraint) | Excludes yanked, resolves to next valid |
|
|
| `search` / `browse` | Hidden by default, shown with `--include-yanked` |
|
|
| Direct URL access | Returns tool with `yanked: true` in response |
|
|
| Already installed | Continues to work, no forced removal |
|
|
|
|
**Database schema addition:**
|
|
```sql
|
|
-- Add to tools table
|
|
yanked BOOLEAN DEFAULT FALSE,
|
|
yanked_reason TEXT,
|
|
yanked_at TIMESTAMP
|
|
```
|
|
|
|
**Yank vs Delete:**
|
|
- **Yank**: Version remains in DB, excluded from resolution, auditable
|
|
- **Delete**: Reserved for DMCA/legal, requires admin action, leaves tombstone record
|
|
|
|
### Version Format
|
|
|
|
Tools use semantic versioning (semver):
|
|
```
|
|
MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
|
|
|
|
Examples:
|
|
1.0.0 # stable release
|
|
1.2.3 # stable release
|
|
2.0.0-alpha.1 # prerelease
|
|
2.0.0-beta.2 # prerelease
|
|
2.0.0-rc.1 # release candidate
|
|
```
|
|
|
|
### Version Constraints
|
|
|
|
Manifest files support these constraint formats:
|
|
|
|
| Constraint | Meaning | Example Match |
|
|
|------------|---------|---------------|
|
|
| `1.2.3` | Exact version | `1.2.3` only |
|
|
| `>=1.2.0` | Minimum version | `1.2.0`, `1.3.0`, `2.0.0` |
|
|
| `<2.0.0` | Below version | `1.9.9`, `1.0.0` |
|
|
| `>=1.0.0,<2.0.0` | Range | `1.0.0` to `1.9.9` |
|
|
| `^1.2.3` | Compatible (same major) | `1.2.3` to `1.9.9` |
|
|
| `~1.2.3` | Approximately (same minor) | `1.2.3` to `1.2.9` |
|
|
| `*` | Any version | latest stable |
|
|
|
|
### Version Resolution Rules
|
|
|
|
When resolving a version constraint:
|
|
|
|
1. **Filter**: Get all versions matching the constraint
|
|
2. **Exclude prereleases**: Unless constraint explicitly includes them (e.g., `>=2.0.0-alpha.1`)
|
|
3. **Sort**: By semver precedence (descending)
|
|
4. **Select**: Highest matching version
|
|
|
|
**Tie-breakers:**
|
|
- Stable versions preferred over prereleases
|
|
- Later publish date wins if versions are equal (shouldn't happen with immutability)
|
|
|
|
**Unsatisfiable constraints:**
|
|
```json
|
|
// API Response: 404
|
|
{
|
|
"error": {
|
|
"code": "VERSION_NOT_FOUND",
|
|
"message": "No version of 'rob/summarize' satisfies constraint '>=5.0.0'",
|
|
"details": {
|
|
"tool": "rob/summarize",
|
|
"constraint": ">=5.0.0",
|
|
"available_versions": ["1.0.0", "1.1.0", "1.2.0"],
|
|
"latest_stable": "1.2.0"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Prerelease Handling
|
|
|
|
- Prereleases are **not** returned for `*` or range constraints by default
|
|
- To install prerelease: `smarttools registry install rob/summarize@2.0.0-beta.1`
|
|
- To allow prereleases in manifest: `version: ">=2.0.0-0"` (the `-0` suffix includes prereleases)
|
|
|
|
### Download Endpoint Version Selection
|
|
|
|
The `/api/v1/tools/{owner}/{name}/download` endpoint accepts version parameters:
|
|
|
|
| Parameter | Behavior | Example |
|
|
|-----------|----------|---------|
|
|
| (none) | Returns latest stable version | `/download` → `1.2.0` |
|
|
| `version=1.2.0` | Exact version (must exist) | `/download?version=1.2.0` |
|
|
| `version=^1.0.0` | Server resolves constraint | `/download?version=^1.0.0` → `1.2.0` |
|
|
| `version=latest` | Alias for latest stable | `/download?version=latest` |
|
|
|
|
**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).
|
|
|
|
```
|
|
GET /api/v1/tools/rob/summarize/download?version=^1.0.0&install=true
|
|
|
|
Response (200):
|
|
{
|
|
"data": {
|
|
"owner": "rob",
|
|
"name": "summarize",
|
|
"resolved_version": "1.2.0",
|
|
"config": "... YAML content ..."
|
|
},
|
|
"meta": {
|
|
"constraint": "^1.0.0",
|
|
"available_versions": ["1.0.0", "1.1.0", "1.2.0"]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Invalid/unsatisfiable constraint:**
|
|
```
|
|
GET /api/v1/tools/rob/summarize/download?version=^5.0.0
|
|
|
|
Response (404):
|
|
{
|
|
"error": {
|
|
"code": "CONSTRAINT_UNSATISFIABLE",
|
|
"message": "No version matches constraint '^5.0.0'",
|
|
"details": {
|
|
"constraint": "^5.0.0",
|
|
"latest_stable": "1.2.0",
|
|
"available_versions": ["1.0.0", "1.1.0", "1.2.0"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Tool Resolution Order
|
|
When a tool is invoked, the CLI searches in this order:
|
|
|
|
1. **Local project**: `./.smarttools/<owner>/<name>/config.yaml` (or `./.smarttools/<name>/` for unnamespaced)
|
|
2. **Global user**: `~/.smarttools/<owner>/<name>/config.yaml`
|
|
3. **Registry**: Fetch from API, install to global, then run
|
|
4. **Error**: `Tool '<toolname>' not found`
|
|
|
|
Step 3 only occurs if `auto_fetch_from_registry: true` in config (default: true).
|
|
|
|
**Path convention:** Use `.smarttools/` (with leading dot) for both local and global to maintain consistency.
|
|
|
|
Resolution also respects namespacing:
|
|
- `summarize` → searches for any tool named `summarize`, prefers `official/summarize` if exists
|
|
- `rob/summarize` → searches for exactly `rob/summarize`
|
|
|
|
### Official Namespace
|
|
|
|
The slug `official` is reserved for curated, high-quality tools maintained by the registry administrators.
|
|
|
|
- Shorthand `summarize` resolves to `official/summarize` if it exists
|
|
- If no `official/summarize`, falls back to most-downloaded tool named `summarize`
|
|
- To avoid ambiguity, always use full `owner/name` in manifests
|
|
|
|
Reserved slugs that cannot be registered: `official`, `admin`, `system`, `api`, `registry`, `smarttools`
|
|
|
|
## Auto-Fetch Behavior
|
|
When enabled (`auto_fetch_from_registry: true`), missing tools are automatically fetched:
|
|
|
|
```bash
|
|
$ summarize < file.txt
|
|
# Tool 'summarize' not found locally.
|
|
# Fetching from registry...
|
|
# Installed: official/summarize@1.2.0
|
|
# Running...
|
|
```
|
|
|
|
Behavior details:
|
|
- Fetches latest stable version unless pinned in `smarttools.yaml`
|
|
- Installs to `~/.smarttools/<owner>/<name>/`
|
|
- Generates wrapper script in `~/.local/bin/`
|
|
- Subsequent runs use local copy (no re-fetch)
|
|
|
|
To disable (require explicit install):
|
|
```yaml
|
|
# ~/.smarttools/config.yaml
|
|
auto_fetch_from_registry: false
|
|
```
|
|
|
|
### Wrapper Script Collisions
|
|
|
|
When two tools from different owners have the same name:
|
|
|
|
| Scenario | Behavior |
|
|
|----------|----------|
|
|
| Install `official/summarize` | Creates wrapper `~/.local/bin/summarize` |
|
|
| Install `rob/summarize` (collision) | Creates wrapper `~/.local/bin/rob-summarize` |
|
|
| Uninstall `official/summarize` | Removes `summarize` wrapper, promotes `rob-summarize` → `summarize` if desired |
|
|
|
|
The first-installed tool with a given name gets the short wrapper. Subsequent tools use `owner-name` format.
|
|
|
|
To invoke a specific owner's tool:
|
|
```bash
|
|
# Short form (whichever was installed first)
|
|
summarize < file.txt
|
|
|
|
# Explicit owner form (always works)
|
|
rob-summarize < file.txt
|
|
|
|
# Or via smarttools run
|
|
smarttools run rob/summarize < file.txt
|
|
```
|
|
|
|
## Project Manifest (smarttools.yaml)
|
|
Defines tool dependencies with optional runtime overrides:
|
|
```
|
|
name: my-ai-project
|
|
version: "1.0.0"
|
|
dependencies:
|
|
- name: rob/summarize
|
|
version: ">=1.0.0"
|
|
overrides:
|
|
rob/summarize:
|
|
provider: ollama
|
|
```
|
|
|
|
Overrides are applied at runtime and do not mutate installed tool configs.
|
|
|
|
## CLI Config and Tokens
|
|
Global config lives in `~/.smarttools/config.yaml`:
|
|
```yaml
|
|
registry:
|
|
url: https://gitea.brrd.tech/api/v1 # Must match canonical base path
|
|
token: "reg_xxxxxxxxxxxx"
|
|
client_id: "anon_abc123def456"
|
|
auto_fetch_from_registry: true
|
|
```
|
|
|
|
`client_id` is generated locally and used for anonymous install dedupe.
|
|
|
|
## Publishing and Auth
|
|
Publishing uses registry accounts, not Gitea accounts:
|
|
- Public endpoints require no auth.
|
|
- `POST /tools` requires a registry token.
|
|
- The API server uses a private Gitea service account to open PRs.
|
|
|
|
### Publish Idempotency and Edge Cases
|
|
|
|
**Idempotency key:** `owner/name@version`
|
|
|
|
| Scenario | API Response | HTTP Code |
|
|
|----------|--------------|-----------|
|
|
| New version, no PR exists | Create PR, return URL | `201 Created` |
|
|
| PR already exists (pending) | Return existing PR URL | `200 OK` |
|
|
| Version already published | Error: version exists | `409 Conflict` |
|
|
| PR was closed without merge | Allow new PR | `201 Created` |
|
|
| PR was merged, then tool deleted | Error: version exists (tombstone) | `409 Conflict` |
|
|
|
|
**Version immutability enforcement:**
|
|
```json
|
|
// Attempt to publish existing version
|
|
// Response: 409 Conflict
|
|
{
|
|
"error": {
|
|
"code": "VERSION_EXISTS",
|
|
"message": "Version 1.2.0 of 'rob/summarize' already exists and cannot be overwritten",
|
|
"details": {
|
|
"published_at": "2025-01-15T10:30:00Z",
|
|
"action": "Bump version number to publish changes"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Closed PR handling:**
|
|
- Track PR state in database: `pending`, `merged`, `closed`
|
|
- If PR was closed (rejected/abandoned), allow new submission for same version
|
|
- If PR was merged, version is immutable forever
|
|
|
|
**Update flow (new version, not overwrite):**
|
|
1. Developer modifies tool locally
|
|
2. Bumps version in `config.yaml` (e.g., `1.2.0` → `1.3.0`)
|
|
3. Runs `smarttools registry publish`
|
|
4. New PR created for `1.3.0`
|
|
5. Old version `1.2.0` remains available
|
|
|
|
## Publisher Registration
|
|
Publishers register on the registry website, not Gitea:
|
|
|
|
**Registration flow:**
|
|
1. User visits `https://gitea.brrd.tech/registry/register` (or future `registry.smarttools.dev`)
|
|
2. Creates account with email + password + slug
|
|
3. Receives verification email (optional in v1, but track `verified` status)
|
|
4. Logs into dashboard at `/dashboard`
|
|
5. Generates API token from dashboard
|
|
6. Uses token in CLI for publishing
|
|
|
|
### Authentication Security
|
|
|
|
**Password hashing:**
|
|
- Algorithm: Argon2id (memory-hard, recommended by OWASP)
|
|
- Parameters: `memory=65536, iterations=3, parallelism=4`
|
|
- Library: `argon2-cffi` for Python
|
|
|
|
```python
|
|
from argon2 import PasswordHasher
|
|
ph = PasswordHasher(memory_cost=65536, time_cost=3, parallelism=4)
|
|
hash = ph.hash(password)
|
|
ph.verify(hash, password) # raises on mismatch
|
|
```
|
|
|
|
**API token format:**
|
|
```
|
|
reg_<random-32-bytes-base62>
|
|
|
|
Example: reg_7kX9mPqR2sT4vW6xY8zA1bC3dE5fG7hJ
|
|
```
|
|
- Prefix `reg_` for easy identification in logs/configs
|
|
- 32 bytes of cryptographically random data
|
|
- Base62 encoded (alphanumeric, no special chars)
|
|
- Total length: ~47 characters
|
|
- Stored as SHA-256 hash in database (never plain text)
|
|
|
|
**Token lifecycle:**
|
|
| Action | Behavior |
|
|
|--------|----------|
|
|
| Generate | Create new token, return once, store hash |
|
|
| List | Show token name, created date, last used (not the token itself) |
|
|
| Revoke | Set `revoked_at` timestamp, reject future uses |
|
|
| Rotate | Generate new token, optionally revoke old |
|
|
|
|
**Rate limits:**
|
|
|
|
| Endpoint | Limit | Window | Scope | Retry-After |
|
|
|----------|-------|--------|-------|-------------|
|
|
| `POST /register` | 5 | 1 hour | IP | 3600 |
|
|
| `POST /login` | 10 | 15 min | IP | 900 |
|
|
| `POST /login` (failed) | 5 | 15 min | IP + email | 900 |
|
|
| `POST /tokens` | 10 | 1 hour | Token | 3600 |
|
|
| `POST /tools` | 20 | 1 hour | Token | 3600 |
|
|
| `GET /tools/*` | 100 | 1 min | IP | 60 |
|
|
| `GET /download` | 60 | 1 min | IP | 60 |
|
|
|
|
**Rate limit response (429):**
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "RATE_LIMITED",
|
|
"message": "Too many requests. Try again in 60 seconds.",
|
|
"details": {
|
|
"limit": 100,
|
|
"window": "1 minute",
|
|
"retry_after": 60
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Headers on rate-limited response:**
|
|
```
|
|
HTTP/1.1 429 Too Many Requests
|
|
Retry-After: 60
|
|
X-RateLimit-Limit: 100
|
|
X-RateLimit-Remaining: 0
|
|
X-RateLimit-Reset: 1705766400
|
|
```
|
|
|
|
**Scope priority:** For authenticated requests, both IP and token limits apply. The more restrictive limit wins.
|
|
|
|
**Account lockout:**
|
|
- After 5 failed login attempts: 15-minute lockout for that email
|
|
- After 10 failed attempts: 1-hour lockout
|
|
- Lockout clears on successful password reset
|
|
|
|
**Password reset flow (deferred to v1.1):**
|
|
1. User requests reset via email
|
|
2. Server generates time-limited token (1 hour expiry)
|
|
3. Email contains reset link with token
|
|
4. User sets new password
|
|
5. All existing sessions/tokens optionally invalidated
|
|
|
|
**Email verification flow (deferred to v1.1):**
|
|
1. On registration, send verification email
|
|
2. User clicks link with verification token
|
|
3. Set `verified = true` in database
|
|
4. Unverified accounts can browse but not publish
|
|
|
|
### Token Scopes and Authorization
|
|
|
|
Tokens have scopes that limit their capabilities:
|
|
|
|
| Scope | Permissions |
|
|
|-------|-------------|
|
|
| `read` | View own published tools, download stats |
|
|
| `publish` | Submit new tools, update own tool metadata |
|
|
| `admin` | Yank tools, manage categories (registry admins only) |
|
|
|
|
**Default scope:** New tokens get `read,publish` by default.
|
|
|
|
**Ownership enforcement:**
|
|
|
|
```python
|
|
@app.route('/api/v1/tools', methods=['POST'])
|
|
@require_token(scopes=['publish'])
|
|
def publish_tool():
|
|
token = get_current_token()
|
|
tool_data = request.json
|
|
|
|
# Enforce owner == token holder's slug
|
|
if tool_data['owner'] != token.publisher.slug:
|
|
return {
|
|
"error": {
|
|
"code": "FORBIDDEN",
|
|
"message": f"Cannot publish to namespace '{tool_data['owner']}'. "
|
|
f"Your namespace is '{token.publisher.slug}'."
|
|
}
|
|
}, 403
|
|
|
|
# Proceed with publish...
|
|
```
|
|
|
|
**`GET /api/v1/me/tools` authorization:**
|
|
- Requires valid token with `read` scope
|
|
- Returns only tools where `owner == token.publisher.slug`
|
|
- Includes pending PRs and all versions (including yanked)
|
|
|
|
### Web Session Security
|
|
|
|
Dashboard login uses session cookies (not tokens) for browser auth:
|
|
|
|
**Cookie settings:**
|
|
```python
|
|
SESSION_COOKIE_NAME = 'smarttools_session'
|
|
SESSION_COOKIE_HTTPONLY = True # Prevent JS access
|
|
SESSION_COOKIE_SECURE = True # HTTPS only in production
|
|
SESSION_COOKIE_SAMESITE = 'Lax' # CSRF protection
|
|
SESSION_COOKIE_MAX_AGE = 86400 * 7 # 7 days
|
|
```
|
|
|
|
**CSRF protection:**
|
|
- All POST/PUT/DELETE forms include `csrf_token` hidden field
|
|
- Token validated server-side before processing
|
|
- 403 Forbidden if token missing or invalid
|
|
|
|
**Session lifecycle:**
|
|
| Event | Action |
|
|
|-------|--------|
|
|
| Login | Create session, set cookie |
|
|
| Logout | Delete session, clear cookie |
|
|
| Idle 24h | Session expires, re-login required |
|
|
| Password change | Invalidate all sessions |
|
|
| Token revocation | Existing sessions continue (token != session) |
|
|
|
|
**Secure session storage:**
|
|
```python
|
|
# Store sessions in DB, not filesystem
|
|
from flask_session import Session
|
|
app.config['SESSION_TYPE'] = 'sqlalchemy'
|
|
app.config['SESSION_SQLALCHEMY_TABLE'] = 'sessions'
|
|
```
|
|
|
|
**Database schema:**
|
|
```sql
|
|
-- Publishers
|
|
CREATE TABLE publishers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
email TEXT UNIQUE NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
slug TEXT UNIQUE NOT NULL, -- immutable namespace: "rob", "alice-dev"
|
|
display_name TEXT NOT NULL, -- mutable: "Rob", "Alice Developer"
|
|
bio TEXT,
|
|
website TEXT,
|
|
verified BOOLEAN DEFAULT FALSE,
|
|
locked_until TIMESTAMP, -- account lockout
|
|
failed_login_attempts INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- API tokens (one publisher can have multiple)
|
|
CREATE TABLE api_tokens (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
publisher_id INTEGER NOT NULL REFERENCES publishers(id),
|
|
token_hash TEXT NOT NULL,
|
|
name TEXT NOT NULL, -- "CLI token", "CI token"
|
|
last_used_at TIMESTAMP,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
revoked_at TIMESTAMP -- NULL if active
|
|
);
|
|
|
|
-- Tools (links to publisher)
|
|
CREATE TABLE tools (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
owner TEXT NOT NULL, -- namespace slug (immutable, from publisher.slug)
|
|
name TEXT NOT NULL,
|
|
version TEXT NOT NULL,
|
|
description TEXT,
|
|
category TEXT,
|
|
tags TEXT, -- JSON array
|
|
config_yaml TEXT NOT NULL, -- Full tool config
|
|
readme TEXT,
|
|
publisher_id INTEGER NOT NULL REFERENCES publishers(id),
|
|
deprecated BOOLEAN DEFAULT FALSE,
|
|
deprecated_message TEXT,
|
|
replacement TEXT,
|
|
downloads INTEGER DEFAULT 0,
|
|
published_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(owner, name, version)
|
|
);
|
|
|
|
-- Download stats (for deduplication)
|
|
CREATE TABLE download_stats (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
tool_id INTEGER NOT NULL REFERENCES tools(id),
|
|
client_id TEXT NOT NULL,
|
|
downloaded_at DATE NOT NULL,
|
|
UNIQUE(tool_id, client_id, downloaded_at)
|
|
);
|
|
|
|
-- Search index (FTS5)
|
|
CREATE VIRTUAL TABLE tools_fts USING fts5(
|
|
name, description, tags, readme,
|
|
content='tools',
|
|
content_rowid='id'
|
|
);
|
|
|
|
-- FTS5 sync triggers (required for external content tables)
|
|
CREATE TRIGGER tools_ai AFTER INSERT ON tools BEGIN
|
|
INSERT INTO tools_fts(rowid, name, description, tags, readme)
|
|
VALUES (new.id, new.name, new.description, new.tags, new.readme);
|
|
END;
|
|
|
|
CREATE TRIGGER tools_ad AFTER DELETE ON tools BEGIN
|
|
INSERT INTO tools_fts(tools_fts, rowid, name, description, tags, readme)
|
|
VALUES ('delete', old.id, old.name, old.description, old.tags, old.readme);
|
|
END;
|
|
|
|
CREATE TRIGGER tools_au AFTER UPDATE ON tools BEGIN
|
|
INSERT INTO tools_fts(tools_fts, rowid, name, description, tags, readme)
|
|
VALUES ('delete', old.id, old.name, old.description, old.tags, old.readme);
|
|
INSERT INTO tools_fts(rowid, name, description, tags, readme)
|
|
VALUES (new.id, new.name, new.description, new.tags, new.readme);
|
|
END;
|
|
|
|
-- Pending PRs (track publish state)
|
|
CREATE TABLE pending_prs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
publisher_id INTEGER NOT NULL REFERENCES publishers(id),
|
|
owner TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
version TEXT NOT NULL,
|
|
pr_number INTEGER NOT NULL,
|
|
pr_url TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending', -- pending, merged, closed
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(owner, name, version)
|
|
);
|
|
|
|
-- Webhook sync log (idempotency)
|
|
CREATE TABLE webhook_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
delivery_id TEXT UNIQUE NOT NULL, -- Gitea delivery ID
|
|
event_type TEXT NOT NULL,
|
|
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
```
|
|
|
|
**Note on tags indexing:** The `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 `tool_tags` junction table:
|
|
|
|
```sql
|
|
-- Future: normalized tags (if needed)
|
|
CREATE TABLE tags (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT UNIQUE NOT NULL
|
|
);
|
|
|
|
CREATE TABLE tool_tags (
|
|
tool_id INTEGER REFERENCES tools(id),
|
|
tag_id INTEGER REFERENCES tags(id),
|
|
PRIMARY KEY (tool_id, tag_id)
|
|
);
|
|
```
|
|
|
|
**CLI first-time publish flow:**
|
|
```bash
|
|
$ smarttools registry publish
|
|
|
|
No registry account configured.
|
|
|
|
1. Register at: https://gitea.brrd.tech/registry/register
|
|
2. Generate a token from your dashboard
|
|
3. Enter your token below
|
|
|
|
Registry token: ********
|
|
Token saved to ~/.smarttools/config.yaml
|
|
|
|
Validating tool...
|
|
✓ config.yaml is valid
|
|
✓ README.md exists (2.3 KB)
|
|
✓ Version 1.0.0 not yet published
|
|
|
|
Publishing rob/my-tool@1.0.0...
|
|
✓ PR created: https://gitea.brrd.tech/rob/SmartTools-Registry/pulls/42
|
|
|
|
Your tool is pending review. You'll receive an email when it's approved.
|
|
```
|
|
|
|
## CLI Commands Reference
|
|
Full mapping of CLI commands to API calls:
|
|
|
|
### Registry Commands
|
|
|
|
```bash
|
|
# Search for tools
|
|
$ smarttools registry search <query> [--category=<cat>] [--limit=20]
|
|
→ GET /api/v1/tools/search?q=<query>&category=<cat>&limit=20
|
|
|
|
# Browse tools (TUI)
|
|
$ smarttools registry browse [--category=<cat>]
|
|
→ GET /api/v1/tools?category=<cat>&page=1
|
|
→ GET /api/v1/categories
|
|
|
|
# View tool details
|
|
$ smarttools registry info <owner/name>
|
|
→ GET /api/v1/tools/<owner>/<name>
|
|
|
|
# Install a tool
|
|
$ smarttools registry install <owner/name> [--version=<ver>]
|
|
→ GET /api/v1/tools/<owner>/<name>/download?version=<ver>&install=true
|
|
→ Writes to ~/.smarttools/<owner>/<name>/config.yaml
|
|
→ Generates ~/.local/bin/<name> wrapper (or <owner>-<name> if collision)
|
|
|
|
# Uninstall a tool
|
|
$ smarttools registry uninstall <owner/name>
|
|
→ Removes ~/.smarttools/<owner>/<name>/
|
|
→ Removes wrapper script
|
|
|
|
# Publish a tool
|
|
$ smarttools registry publish [path] [--dry-run]
|
|
→ POST /api/v1/tools (with registry token)
|
|
→ Returns PR URL
|
|
|
|
# List my published tools
|
|
$ smarttools registry my-tools
|
|
→ GET /api/v1/me/tools (with registry token)
|
|
|
|
# Update index cache
|
|
$ smarttools registry update
|
|
→ GET /api/v1/index.json
|
|
→ Writes to ~/.smarttools/registry/index.json
|
|
```
|
|
|
|
### Project Commands
|
|
|
|
```bash
|
|
# Install project dependencies from smarttools.yaml
|
|
$ smarttools install
|
|
→ Reads ./smarttools.yaml
|
|
→ For each dependency:
|
|
GET /api/v1/tools/<owner>/<name>/download?version=<constraint>&install=true
|
|
→ Installs to ~/.smarttools/<owner>/<name>/
|
|
|
|
# Add a dependency to smarttools.yaml
|
|
$ smarttools add <owner/name> [--version=<constraint>]
|
|
→ Adds to ./smarttools.yaml dependencies
|
|
→ Runs install for that tool
|
|
|
|
# Show project dependencies status
|
|
$ smarttools deps
|
|
→ Reads ./smarttools.yaml
|
|
→ Shows installed status for each dependency
|
|
→ Note: "smarttools list" is reserved for listing installed tools
|
|
```
|
|
|
|
**Command naming note:** `smarttools list` already exists to list locally installed tools. Use `smarttools deps` to show project manifest dependencies.
|
|
|
|
### Flags available on most commands
|
|
|
|
| Flag | Description |
|
|
|------|-------------|
|
|
| `--offline` | Use cached index only, don't fetch |
|
|
| `--refresh` | Force refresh of cached data |
|
|
| `--json` | Output in JSON format |
|
|
| `--verbose` | Show detailed output |
|
|
|
|
## Webhooks and Security
|
|
|
|
### HMAC Verification
|
|
|
|
All Gitea webhooks are verified using HMAC-SHA256:
|
|
|
|
```python
|
|
import hmac
|
|
import hashlib
|
|
|
|
def verify_webhook(request, secret):
|
|
signature = request.headers.get('X-Gitea-Signature')
|
|
if not signature:
|
|
return False
|
|
|
|
expected = hmac.new(
|
|
secret.encode(),
|
|
request.body,
|
|
hashlib.sha256
|
|
).hexdigest()
|
|
|
|
return hmac.compare_digest(signature, expected)
|
|
```
|
|
|
|
### Replay Protection
|
|
|
|
While sync is idempotent, implement basic replay protection:
|
|
|
|
```python
|
|
def process_webhook(request):
|
|
delivery_id = request.headers.get('X-Gitea-Delivery')
|
|
|
|
# Check if already processed
|
|
if db.webhook_log.exists(delivery_id=delivery_id):
|
|
return {"status": "already_processed"}, 200
|
|
|
|
# Verify signature
|
|
if not verify_webhook(request, WEBHOOK_SECRET):
|
|
return {"error": "invalid_signature"}, 401
|
|
|
|
# Process with lock to prevent concurrent processing
|
|
with db.lock(f"webhook:{delivery_id}"):
|
|
# Double-check after acquiring lock
|
|
if db.webhook_log.exists(delivery_id=delivery_id):
|
|
return {"status": "already_processed"}, 200
|
|
|
|
# Process the webhook
|
|
result = sync_from_repo()
|
|
|
|
# Log successful processing
|
|
db.webhook_log.insert(
|
|
delivery_id=delivery_id,
|
|
event_type=request.json.get('action'),
|
|
processed_at=datetime.utcnow()
|
|
)
|
|
|
|
return {"status": "processed"}, 200
|
|
```
|
|
|
|
### Sync Job Locking
|
|
|
|
Prevent concurrent sync operations:
|
|
|
|
```python
|
|
# Using file lock or database advisory lock
|
|
SYNC_LOCK_TIMEOUT = 300 # 5 minutes max
|
|
|
|
def sync_from_repo():
|
|
try:
|
|
with acquire_lock("registry_sync", timeout=SYNC_LOCK_TIMEOUT):
|
|
# Pull latest from Gitea
|
|
repo.fetch()
|
|
repo.reset('origin/main', hard=True)
|
|
|
|
# Parse and update database
|
|
for tool_path in glob('tools/*/*/config.yaml'):
|
|
update_tool_in_db(tool_path)
|
|
|
|
# Rebuild FTS index if needed
|
|
rebuild_fts_index()
|
|
|
|
except LockTimeout:
|
|
logger.warning("Sync already in progress, skipping")
|
|
return {"status": "skipped", "reason": "sync_in_progress"}
|
|
```
|
|
|
|
### Atomic Sync Strategy
|
|
|
|
To avoid partially updated DB during webhook sync, use transactional table swap:
|
|
|
|
```python
|
|
def sync_from_repo_atomic():
|
|
with acquire_lock("registry_sync", timeout=SYNC_LOCK_TIMEOUT):
|
|
# 1. Pull latest from Gitea
|
|
repo.fetch()
|
|
repo.reset('origin/main', hard=True)
|
|
|
|
# 2. Parse all tools into memory
|
|
new_tools = []
|
|
for tool_path in glob('tools/*/*/config.yaml'):
|
|
tool_data = parse_tool(tool_path)
|
|
if tool_data:
|
|
new_tools.append(tool_data)
|
|
|
|
# 3. Atomic swap using transaction
|
|
with db.transaction():
|
|
# Create temp table
|
|
db.execute("CREATE TABLE tools_new AS SELECT * FROM tools WHERE 0")
|
|
|
|
# Bulk insert into temp table
|
|
for tool in new_tools:
|
|
db.execute("INSERT INTO tools_new ...", tool)
|
|
|
|
# Swap tables atomically
|
|
db.execute("ALTER TABLE tools RENAME TO tools_old")
|
|
db.execute("ALTER TABLE tools_new RENAME TO tools")
|
|
db.execute("DROP TABLE tools_old")
|
|
|
|
# Rebuild FTS index
|
|
db.execute("INSERT INTO tools_fts(tools_fts) VALUES('rebuild')")
|
|
|
|
# Update sync timestamp
|
|
db.execute("UPDATE sync_status SET last_sync = ?", [datetime.utcnow()])
|
|
```
|
|
|
|
**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.
|
|
|
|
### Error Handling
|
|
|
|
| Error Scenario | Behavior |
|
|
|----------------|----------|
|
|
| Repo fetch fails | Log error, retry in 5 min, alert if 3 failures |
|
|
| YAML parse error | Skip tool, log error, continue with others |
|
|
| Database write fails | Rollback transaction, retry once, then alert |
|
|
| Lock timeout | Skip this sync, next webhook will retry |
|
|
|
|
## Automated CI Validation
|
|
PRs are validated automatically using SmartTools (dogfooding):
|
|
|
|
```
|
|
PR Submitted
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────┐
|
|
│ Gitea CI runs validation tools: │
|
|
│ • schema-validator │
|
|
│ • security-scanner │
|
|
│ • duplicate-detector │
|
|
└───────────────┬─────────────────────┘
|
|
│
|
|
┌───────┴───────┐
|
|
│ │
|
|
All pass Any fail
|
|
│ │
|
|
▼ ▼
|
|
Auto-merge or Add comment,
|
|
flag for review request changes
|
|
```
|
|
|
|
Validation checks:
|
|
1. **Schema validation**: config.yaml matches expected format
|
|
2. **Security scan**: No dangerous shell commands, no secrets in prompts
|
|
3. **Duplicate detection**: AI-powered similarity check against existing tools
|
|
4. **README check**: README.md exists and is non-empty
|
|
|
|
CI workflow (`.gitea/workflows/validate.yaml`):
|
|
```yaml
|
|
name: Validate Tool Submission
|
|
on: [pull_request]
|
|
jobs:
|
|
validate:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
- name: Validate schema
|
|
run: python scripts/validate_tool.py ${{ github.event.pull_request.head.sha }}
|
|
- name: Security scan
|
|
run: smarttools run security-scanner < changed_files.txt
|
|
- name: Check duplicates
|
|
run: smarttools run duplicate-detector < changed_files.txt
|
|
```
|
|
|
|
## Registry Repository Structure
|
|
Full structure of the SmartTools-Registry repo:
|
|
|
|
```
|
|
SmartTools-Registry/
|
|
├── README.md # Registry overview
|
|
├── CONTRIBUTING.md # How to submit tools
|
|
├── LICENSE
|
|
│
|
|
├── tools/ # All published tools
|
|
│ ├── rob/
|
|
│ │ ├── summarize/
|
|
│ │ │ ├── config.yaml
|
|
│ │ │ └── README.md
|
|
│ │ └── translate/
|
|
│ │ ├── config.yaml
|
|
│ │ └── README.md
|
|
│ └── alice/
|
|
│ └── code-review/
|
|
│ ├── config.yaml
|
|
│ └── README.md
|
|
│
|
|
├── categories/
|
|
│ └── categories.yaml # Category definitions
|
|
│
|
|
├── index.json # Auto-generated search index
|
|
│
|
|
├── .gitea/
|
|
│ └── workflows/
|
|
│ ├── validate.yaml # PR validation
|
|
│ ├── build-index.yaml # Rebuild index on merge
|
|
│ └── notify-api.yaml # Webhook to API server
|
|
│
|
|
└── scripts/
|
|
├── validate_tool.py # Schema validation
|
|
├── build_index.py # Generate index.json
|
|
├── check_duplicates.py # Similarity detection
|
|
└── security_scan.py # Security checks
|
|
```
|
|
|
|
`categories.yaml` format:
|
|
```yaml
|
|
categories:
|
|
- name: text-processing
|
|
description: Tools for manipulating and analyzing text
|
|
icon: 📝
|
|
- name: code
|
|
description: Tools for code review, generation, and analysis
|
|
icon: 💻
|
|
- name: data
|
|
description: Tools for data transformation and analysis
|
|
icon: 📊
|
|
- name: media
|
|
description: Tools for image, audio, and video processing
|
|
icon: 🎨
|
|
- name: productivity
|
|
description: General productivity and automation tools
|
|
icon: ⚡
|
|
```
|
|
|
|
## Download Stats
|
|
|
|
### Counting Methodology
|
|
|
|
- Count installs only, not views or searches
|
|
- Increment **after** successful download (response sent)
|
|
- Dedupe by `client_id + tool_id + date`
|
|
|
|
```python
|
|
def download_tool(owner, name, version, install=False, client_id=None):
|
|
tool = get_tool(owner, name, version)
|
|
if not tool:
|
|
return {"error": "not_found"}, 404
|
|
|
|
config_yaml = tool.config_yaml
|
|
|
|
# Only count if this is an install (not just viewing)
|
|
if install:
|
|
record_download(tool.id, client_id)
|
|
|
|
return {"config": config_yaml}, 200
|
|
|
|
def record_download(tool_id, client_id):
|
|
today = date.today()
|
|
|
|
# Use client_id if provided, otherwise generate anonymous fallback
|
|
effective_client_id = client_id or f"anon_{hash(request.remote_addr)}"
|
|
|
|
# Dedupe: only count once per client per tool per day
|
|
try:
|
|
db.download_stats.insert(
|
|
tool_id=tool_id,
|
|
client_id=effective_client_id,
|
|
downloaded_at=today
|
|
)
|
|
# Increment counter (can be async/batch updated)
|
|
db.execute("UPDATE tools SET downloads = downloads + 1 WHERE id = ?", [tool_id])
|
|
except IntegrityError:
|
|
pass # Already counted today, ignore
|
|
```
|
|
|
|
### Client ID Generation
|
|
|
|
CLI generates a persistent anonymous ID on first run:
|
|
|
|
```python
|
|
# In CLI, on first run
|
|
import uuid
|
|
import os
|
|
|
|
CONFIG_PATH = os.path.expanduser("~/.smarttools/config.yaml")
|
|
|
|
def get_or_create_client_id():
|
|
config = load_config()
|
|
if 'client_id' not in config:
|
|
config['client_id'] = f"anon_{uuid.uuid4().hex[:16]}"
|
|
save_config(config)
|
|
return config['client_id']
|
|
```
|
|
|
|
**Fallback when client_id missing:**
|
|
- If header `X-Client-ID` not sent, use IP hash as fallback
|
|
- This still provides some dedupe for anonymous users
|
|
- Logged users' downloads are attributed to their account instead
|
|
|
|
### Privacy Considerations
|
|
|
|
- No IP addresses stored in database
|
|
- `client_id` is client-controlled and can be regenerated
|
|
- Stats are aggregated (total count), not individual tracking
|
|
|
|
### Async Stats Strategy
|
|
|
|
To avoid DB contention on the hot download path:
|
|
|
|
```python
|
|
from queue import Queue
|
|
from threading import Thread
|
|
|
|
# In-memory queue for stats
|
|
stats_queue = Queue()
|
|
|
|
def record_download_async(tool_id, client_id):
|
|
"""Non-blocking: enqueue for background processing"""
|
|
stats_queue.put({
|
|
'tool_id': tool_id,
|
|
'client_id': client_id,
|
|
'date': date.today()
|
|
})
|
|
|
|
def stats_worker():
|
|
"""Background thread: batch process stats every 5 seconds"""
|
|
batch = []
|
|
while True:
|
|
try:
|
|
item = stats_queue.get(timeout=5)
|
|
batch.append(item)
|
|
except Empty:
|
|
if batch:
|
|
flush_batch(batch)
|
|
batch = []
|
|
|
|
def flush_batch(batch):
|
|
"""Bulk insert with conflict ignore"""
|
|
with db.transaction():
|
|
for item in batch:
|
|
try:
|
|
db.execute("""
|
|
INSERT INTO download_stats (tool_id, client_id, downloaded_at)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT DO NOTHING
|
|
""", [item['tool_id'], item['client_id'], item['date']])
|
|
except Exception as e:
|
|
logger.warning(f"Stats insert failed: {e}")
|
|
# Don't fail downloads for stats errors
|
|
```
|
|
|
|
**Failure behavior:** If stats DB write fails, log the error but don't fail the download. Stats are "best effort" - the download must succeed.
|
|
|
|
## Search
|
|
- Primary search: SQLite FTS5 inside the API.
|
|
- `index.json` provides offline CLI search and backup.
|
|
- If FTS5 is stale, return results with `X-Search-Index-Stale: true`.
|
|
|
|
## API Caching Strategy
|
|
|
|
### Cache Headers
|
|
|
|
| Endpoint | Cache-Control | ETag | Notes |
|
|
|----------|---------------|------|-------|
|
|
| `GET /index.json` | `max-age=300, stale-while-revalidate=60` | Yes | 5 min cache, background refresh |
|
|
| `GET /tools/{owner}/{name}` | `max-age=60` | Yes | 1 min cache |
|
|
| `GET /tools/{owner}/{name}/download` | `max-age=3600, immutable` | Yes | Immutable versions, 1 hour |
|
|
| `GET /tools/search` | `no-cache` | No | Always fresh |
|
|
| `GET /categories` | `max-age=3600` | Yes | Categories change rarely |
|
|
|
|
### ETag Implementation
|
|
|
|
```python
|
|
import hashlib
|
|
from datetime import datetime
|
|
|
|
def get_tool_etag(tool):
|
|
"""Generate ETag from tool identity (immutable versions don't change)"""
|
|
# Since versions are immutable, owner/name@version is stable
|
|
# Use published_at for extra safety (not updated_at, which doesn't exist)
|
|
content = f"{tool.owner}/{tool.name}@{tool.version}:{tool.published_at.isoformat()}"
|
|
return hashlib.md5(content.encode()).hexdigest()
|
|
|
|
def get_index_etag():
|
|
"""Generate ETag from last sync timestamp"""
|
|
last_sync = db.get_last_sync_time()
|
|
return hashlib.md5(last_sync.isoformat().encode()).hexdigest()
|
|
|
|
@app.route('/api/v1/tools/<owner>/<name>/download')
|
|
def download_tool(owner, name):
|
|
version = request.args.get('version', 'latest')
|
|
tool = resolve_and_get_tool(owner, name, version)
|
|
etag = get_tool_etag(tool)
|
|
|
|
# Check If-None-Match header
|
|
if request.headers.get('If-None-Match') == etag:
|
|
return '', 304 # Not Modified
|
|
|
|
response = jsonify({
|
|
"data": {
|
|
"owner": tool.owner,
|
|
"name": tool.name,
|
|
"resolved_version": tool.version,
|
|
"config": tool.config_yaml
|
|
}
|
|
})
|
|
response.headers['ETag'] = etag
|
|
response.headers['Cache-Control'] = 'max-age=3600, immutable'
|
|
return response
|
|
```
|
|
|
|
**Note:** Since tool versions are immutable, the ETag based on `owner/name@version` is permanently stable. The `published_at` timestamp is included for defense-in-depth but won't change.
|
|
|
|
### DB vs Repo Read Strategy
|
|
|
|
| Scenario | Read From | Reason |
|
|
|----------|-----------|--------|
|
|
| Normal operation | SQLite DB | Fast, indexed, FTS |
|
|
| DB empty/corrupted | Gitea repo | Fallback/recovery |
|
|
| Webhook sync in progress | DB (stale OK) | Avoid blocking reads |
|
|
| Search query | SQLite FTS5 | Full-text search |
|
|
| Download specific version | DB, fallback to repo | DB is cache, repo is truth |
|
|
|
|
### Staleness Detection
|
|
|
|
```python
|
|
STALE_THRESHOLD = timedelta(minutes=10)
|
|
|
|
def is_db_stale():
|
|
last_sync = db.get_last_sync_time()
|
|
return datetime.utcnow() - last_sync > STALE_THRESHOLD
|
|
|
|
@app.route('/tools/search')
|
|
def search_tools(q):
|
|
results = db.search_fts(q)
|
|
|
|
response = jsonify({"results": results})
|
|
if is_db_stale():
|
|
response.headers['X-Search-Index-Stale'] = 'true'
|
|
response.headers['X-Last-Sync'] = db.get_last_sync_time().isoformat()
|
|
|
|
return response
|
|
```
|
|
|
|
## Error Model
|
|
|
|
### Response Envelopes
|
|
|
|
**Success response:**
|
|
```json
|
|
{
|
|
"data": { ... },
|
|
"meta": {
|
|
"page": 1,
|
|
"per_page": 20,
|
|
"total": 42,
|
|
"total_pages": 3
|
|
}
|
|
}
|
|
```
|
|
|
|
**Error response:**
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "TOOL_NOT_FOUND",
|
|
"message": "Tool 'foo/bar' does not exist",
|
|
"details": {
|
|
"owner": "foo",
|
|
"name": "bar",
|
|
"suggestion": "Did you mean 'rob/bar'?"
|
|
},
|
|
"docs_url": "https://registry.smarttools.dev/docs/errors#TOOL_NOT_FOUND"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Error Codes
|
|
|
|
| Code | HTTP | Description |
|
|
|------|------|-------------|
|
|
| `TOOL_NOT_FOUND` | 404 | Tool does not exist |
|
|
| `VERSION_NOT_FOUND` | 404 | Requested version doesn't exist |
|
|
| `VERSION_EXISTS` | 409 | Cannot overwrite published version |
|
|
| `INVALID_VERSION` | 400 | Version string is not valid semver |
|
|
| `INVALID_CONSTRAINT` | 400 | Version constraint syntax error |
|
|
| `CONSTRAINT_UNSATISFIABLE` | 404 | No version matches constraint |
|
|
| `VALIDATION_ERROR` | 400 | Tool config validation failed |
|
|
| `UNAUTHORIZED` | 401 | Missing or invalid auth token |
|
|
| `FORBIDDEN` | 403 | Token valid but lacks permission |
|
|
| `RATE_LIMITED` | 429 | Too many requests |
|
|
| `SLUG_TAKEN` | 409 | Namespace slug already registered |
|
|
| `ACCOUNT_LOCKED` | 403 | Too many failed login attempts |
|
|
| `SERVER_ERROR` | 500 | Internal error (logged for debugging) |
|
|
|
|
## Error Scenarios and Fallbacks
|
|
|
|
### CLI Error Handling
|
|
|
|
| Scenario | CLI Behavior | User Message |
|
|
|----------|--------------|--------------|
|
|
| Registry offline | Use cached tools if available | "Registry unavailable. Using cached version." |
|
|
| Tool not found | Check cache, then fail | "Tool 'foo/bar' not found in registry or cache." |
|
|
| Version constraint unsatisfiable | Show available versions | "No version matches '>=5.0.0'. Available: 1.0.0, 1.1.0, 1.2.0" |
|
|
| Auth token expired | Prompt for new token | "Token expired. Please re-authenticate." |
|
|
| Rate limited | Wait and retry (backoff) | "Rate limited. Retrying in 30 seconds..." |
|
|
| Network timeout | Retry with backoff, then fail | "Connection timed out. Check your network." |
|
|
|
|
### Validation Failure Details
|
|
|
|
When `VALIDATION_ERROR` occurs, provide specific field errors:
|
|
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "VALIDATION_ERROR",
|
|
"message": "Tool configuration is invalid",
|
|
"details": {
|
|
"errors": [
|
|
{
|
|
"path": "steps[0].provider",
|
|
"message": "Provider 'gpt5' is not recognized",
|
|
"allowed": ["claude", "openai", "ollama", "mock"]
|
|
},
|
|
{
|
|
"path": "version",
|
|
"message": "Version '1.0' is not valid semver (use '1.0.0')"
|
|
}
|
|
]
|
|
},
|
|
"docs_url": "https://registry.smarttools.dev/docs/tool-format"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Dependency Resolution Failures
|
|
|
|
When `smarttools install` fails on a manifest:
|
|
|
|
```bash
|
|
$ smarttools install
|
|
|
|
Error: Could not resolve all dependencies
|
|
|
|
rob/summarize@^2.0.0
|
|
✗ No matching version (latest: 1.2.0)
|
|
|
|
alice/translate@>=1.0.0
|
|
✓ Found 1.3.0
|
|
|
|
Suggestions:
|
|
- Update rob/summarize constraint to "^1.0.0"
|
|
- Contact the tool author for a v2 release
|
|
```
|
|
|
|
### Graceful Degradation
|
|
|
|
| Component Down | Fallback Behavior |
|
|
|----------------|-------------------|
|
|
| API server | CLI uses `~/.smarttools/registry/index.json` for search |
|
|
| Gitea repo | API serves from DB cache (may be stale) |
|
|
| FTS5 index | Fall back to LIKE queries (slower but works) |
|
|
| Network | Use locally installed tools, skip registry features |
|
|
|
|
## UX Requirements (CLI/TUI)
|
|
|
|
### Publishing UX
|
|
|
|
- `smarttools registry publish --dry-run` validates locally and shows what would be submitted:
|
|
```bash
|
|
$ smarttools registry publish --dry-run
|
|
|
|
Validating tool...
|
|
✓ config.yaml is valid
|
|
✓ README.md exists (2.3 KB)
|
|
✓ Version 1.1.0 not yet published
|
|
|
|
Would submit:
|
|
Owner: rob
|
|
Name: summarize
|
|
Version: 1.1.0
|
|
Category: text-processing
|
|
Tags: summarization, ai, text
|
|
|
|
Config preview:
|
|
─────────────────────────────
|
|
name: summarize
|
|
version: "1.1.0"
|
|
description: Summarize text using AI
|
|
...
|
|
─────────────────────────────
|
|
|
|
Run without --dry-run to submit for review.
|
|
```
|
|
|
|
- **Version bump reminder:** CLI warns if version hasn't changed from published:
|
|
```
|
|
⚠ Version 1.0.0 is already published. Bump version in config.yaml to publish changes.
|
|
```
|
|
|
|
- First-time publishing flow prompts for token and saves it to config.
|
|
|
|
### Progress Indicators
|
|
|
|
Long-running operations show progress:
|
|
|
|
```bash
|
|
$ smarttools install
|
|
|
|
Installing project dependencies...
|
|
[1/3] rob/summarize@^1.0.0
|
|
Resolving version... 1.2.0
|
|
Downloading... done
|
|
Installing... done ✓
|
|
[2/3] alice/translate@>=2.0.0
|
|
Resolving version... 2.1.0
|
|
Downloading... done
|
|
Installing... done ✓
|
|
[3/3] official/code-review@*
|
|
Resolving version... 1.0.0
|
|
Downloading... done
|
|
Installing... done ✓
|
|
|
|
✓ Installed 3 tools
|
|
```
|
|
|
|
```bash
|
|
$ smarttools registry publish
|
|
|
|
Submitting rob/summarize@1.1.0...
|
|
Validating... done ✓
|
|
Uploading... done ✓
|
|
Creating PR... done ✓
|
|
|
|
✓ PR created: https://gitea.brrd.tech/rob/SmartTools-Registry/pulls/42
|
|
|
|
Your tool is pending review. You'll receive an email when it's approved.
|
|
```
|
|
|
|
### TUI Browse
|
|
|
|
`smarttools registry browse` opens a full-screen terminal UI:
|
|
|
|
```
|
|
┌─ SmartTools Registry ───────────────────────────────────────┐
|
|
│ Search: [________________] [All Categories ▼] [Sort: Popular ▼] │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ▶ rob/summarize v1.2.0 ⬇ 142 │
|
|
│ Summarize text using AI │
|
|
│ [text-processing] [ai] [summarization] │
|
|
│ │
|
|
│ alice/translate v2.1.0 ⬇ 98 │
|
|
│ Translate text between languages │
|
|
│ [text-processing] [translation] │
|
|
│ │
|
|
│ official/code-review v1.0.0 ⬇ 87 │
|
|
│ AI-powered code review │
|
|
│ [code] [review] [ai] │
|
|
│ │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ ↑↓ Navigate Enter: Details i: Install /: Search q: Quit │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
**Keyboard shortcuts:**
|
|
| Key | Action |
|
|
|-----|--------|
|
|
| `↑/↓` or `j/k` | Navigate list |
|
|
| `Enter` | View tool details |
|
|
| `i` | Install selected tool |
|
|
| `/` | Focus search box |
|
|
| `c` | Change category filter |
|
|
| `s` | Change sort order |
|
|
| `?` | Show help |
|
|
| `q` | Quit |
|
|
|
|
**Virtual scrolling:** For large tool lists (>100), use virtual scrolling to maintain performance.
|
|
|
|
### Project Initialization
|
|
|
|
```bash
|
|
$ smarttools init
|
|
|
|
Creating smarttools.yaml...
|
|
|
|
Project name [my-project]: my-ai-project
|
|
Version [1.0.0]:
|
|
|
|
Would you like to add any tools? (search with 's', skip with Enter)
|
|
> s
|
|
Search: summ
|
|
1. rob/summarize v1.2.0 - Summarize text using AI
|
|
2. alice/summary v1.0.0 - Generate summaries
|
|
|
|
Add tool (number, or Enter to finish): 1
|
|
Added rob/summarize@^1.2.0
|
|
|
|
Add tool (number, or Enter to finish):
|
|
|
|
✓ Created smarttools.yaml
|
|
|
|
name: my-ai-project
|
|
version: "1.0.0"
|
|
dependencies:
|
|
- name: rob/summarize
|
|
version: "^1.2.0"
|
|
|
|
Run 'smarttools install' to install dependencies.
|
|
```
|
|
|
|
### Accessibility
|
|
|
|
- **CLI:** All output works with screen readers, no color-only information
|
|
- **TUI:** Full keyboard navigation, high-contrast mode support
|
|
- **Web UI:** WCAG 2.1 AA compliance target
|
|
- Semantic HTML
|
|
- ARIA labels for interactive elements
|
|
- Focus management in modals
|
|
- Skip links for navigation
|
|
|
|
## Offline Cache
|
|
Cache registry index locally:
|
|
```
|
|
~/.smarttools/registry/index.json
|
|
```
|
|
Refresh when older than 24 hours; support `--offline` and `--refresh` flags.
|
|
|
|
### Index Integrity
|
|
|
|
The cached `index.json` includes integrity metadata:
|
|
|
|
```json
|
|
{
|
|
"version": "1.0",
|
|
"generated_at": "2025-01-20T12:00:00Z",
|
|
"checksum": "sha256:abc123...",
|
|
"tool_count": 142,
|
|
"tools": [...]
|
|
}
|
|
```
|
|
|
|
**API response headers:**
|
|
```
|
|
ETag: "abc123def456"
|
|
X-Index-Checksum: sha256:abc123...
|
|
X-Index-Generated: 2025-01-20T12:00:00Z
|
|
```
|
|
|
|
**CLI verification:**
|
|
```python
|
|
def verify_cached_index():
|
|
"""Verify cached index integrity on load"""
|
|
cached = load_cached_index()
|
|
if not cached:
|
|
return None
|
|
|
|
# Verify checksum
|
|
content = json.dumps(cached['tools'], sort_keys=True)
|
|
computed = hashlib.sha256(content.encode()).hexdigest()
|
|
|
|
if computed != cached.get('checksum', '').replace('sha256:', ''):
|
|
logger.warning("Cached index checksum mismatch, will refresh")
|
|
return None
|
|
|
|
return cached
|
|
```
|
|
|
|
**Corruption handling:**
|
|
- If checksum fails, discard cache and fetch fresh
|
|
- If partial write detected (missing fields), discard and refresh
|
|
- CLI shows warning: "Cached index corrupted, fetching fresh copy..."
|
|
|
|
## Web UI Vision
|
|
The registry includes a full website, not just an API:
|
|
|
|
**Site structure:**
|
|
```
|
|
registry.smarttools.dev (or gitea.brrd.tech/registry)
|
|
├── / # Landing page
|
|
├── /tools # Browse all tools
|
|
├── /tools/{owner}/{name} # Tool detail page
|
|
├── /categories # Browse by category
|
|
├── /categories/{name} # Tools in category
|
|
├── /search?q=... # Search results
|
|
├── /docs # Documentation
|
|
│ ├── /docs/getting-started
|
|
│ ├── /docs/creating-tools
|
|
│ ├── /docs/publishing
|
|
│ └── /docs/best-practices
|
|
├── /tutorials # Step-by-step guides
|
|
│ ├── /tutorials/first-tool
|
|
│ ├── /tutorials/chaining-steps
|
|
│ └── /tutorials/code-steps
|
|
├── /examples # Example projects
|
|
├── /blog # Updates, announcements (optional)
|
|
├── /register # Publisher registration
|
|
├── /login # Publisher login
|
|
├── /dashboard # Publisher dashboard
|
|
│ ├── /dashboard/tools # My published tools
|
|
│ ├── /dashboard/tokens # API tokens
|
|
│ └── /dashboard/settings # Account settings
|
|
└── /api/v1/... # API endpoints
|
|
```
|
|
|
|
**Landing page content:**
|
|
- Hero: "Share and discover AI-powered CLI tools"
|
|
- Quick install example
|
|
- Featured/popular tools
|
|
- Category highlights
|
|
- "Get Started" CTA
|
|
|
|
**Tool detail page:**
|
|
- Name, description, version, author
|
|
- README rendered as markdown (sanitized)
|
|
- Install command (copy-to-clipboard)
|
|
- Version history
|
|
- Download stats
|
|
- Category/tags
|
|
- "Report" button for abuse
|
|
|
|
### README Security
|
|
|
|
When rendering README markdown, apply XSS sanitization:
|
|
|
|
```python
|
|
import bleach
|
|
from markdown import markdown
|
|
|
|
ALLOWED_TAGS = [
|
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
'p', 'br', 'hr',
|
|
'ul', 'ol', 'li',
|
|
'strong', 'em', 'code', 'pre',
|
|
'blockquote',
|
|
'a', 'img',
|
|
'table', 'thead', 'tbody', 'tr', 'th', 'td'
|
|
]
|
|
|
|
ALLOWED_ATTRS = {
|
|
'a': ['href', 'title'],
|
|
'img': ['src', 'alt', 'title'],
|
|
'code': ['class'], # for syntax highlighting
|
|
}
|
|
|
|
def render_readme_safe(readme_raw: str) -> str:
|
|
"""Convert markdown to sanitized HTML"""
|
|
# Convert markdown to HTML
|
|
html = markdown(readme_raw, extensions=['fenced_code', 'tables'])
|
|
|
|
# Sanitize to prevent XSS
|
|
safe_html = bleach.clean(
|
|
html,
|
|
tags=ALLOWED_TAGS,
|
|
attributes=ALLOWED_ATTRS,
|
|
strip=True
|
|
)
|
|
|
|
# Linkify URLs
|
|
safe_html = bleach.linkify(safe_html)
|
|
|
|
return safe_html
|
|
```
|
|
|
|
**Storage strategy:**
|
|
- Store raw README in `tools.readme`
|
|
- Render and sanitize on request (or cache rendered HTML)
|
|
- Never trust client-submitted HTML directly
|
|
|
|
**Tech stack options:**
|
|
| Option | Pros | Cons |
|
|
|--------|------|------|
|
|
| Flask + Jinja + Tailwind | Simple, Python-only, fast to build | Less interactive |
|
|
| FastAPI + Vue/React SPA | Modern, interactive | More complex, separate build |
|
|
| Astro/Next.js | Great SEO, static-first | Different stack (Node.js) |
|
|
|
|
**Recommendation:** Flask + Jinja + Tailwind for v1
|
|
- Keeps everything in Python
|
|
- Server-rendered is fine for a registry
|
|
- Good SEO out of the box
|
|
- Can add interactivity with Alpine.js or htmx if needed
|
|
|
|
**Monetization considerations:**
|
|
- AdSense-compatible (server-rendered pages)
|
|
- Analytics tracking for traffic insights
|
|
- Future: sponsored tools, featured placements
|
|
- Future: premium publisher tiers (more tools, priority review)
|
|
|
|
## Implementation Phases
|
|
|
|
### Phase 1: Foundation
|
|
- Define `smarttools.yaml` manifest format
|
|
- Implement tool resolution order (local → global → registry)
|
|
- Create SmartTools-Registry repo on Gitea (bootstrap)
|
|
- Add 3-5 example tools to seed the registry
|
|
|
|
### Phase 2: Core Backend
|
|
- Set up Flask/FastAPI project structure
|
|
- Implement SQLite database schema
|
|
- Build core API endpoints (list, search, get, download)
|
|
- Implement webhook receiver for Gitea sync
|
|
- Set up HMAC verification
|
|
|
|
### Phase 3: CLI Commands
|
|
- `smarttools registry search`
|
|
- `smarttools registry install`
|
|
- `smarttools registry info`
|
|
- `smarttools registry browse` (TUI)
|
|
- Local index caching
|
|
|
|
### Phase 4: Publishing
|
|
- Publisher registration (web UI)
|
|
- Token management
|
|
- `smarttools registry publish` command
|
|
- PR creation via Gitea API
|
|
- CI validation workflows
|
|
|
|
### Phase 5: Project Dependencies
|
|
- `smarttools install` (from manifest)
|
|
- `smarttools add` command
|
|
- Runtime override application
|
|
- Dependency resolution
|
|
|
|
### Phase 6: Smart Features
|
|
- SQLite FTS5 search index
|
|
- AI-powered auto-categorization
|
|
- Duplicate/similarity detection
|
|
- Security scanning
|
|
|
|
### Phase 7: Full Web UI
|
|
- Landing page
|
|
- Tool browsing/search pages
|
|
- Tool detail pages with README rendering
|
|
- Publisher dashboard
|
|
- Documentation/tutorials section
|
|
|
|
### Phase 8: Polish & Scale
|
|
- Rate limiting
|
|
- Abuse reporting
|
|
- Analytics integration
|
|
- Performance optimization
|
|
- Monitoring/alerting
|