feat: initialize tfcode project structure

- Add README.md as living documentation
- Add tfcode.json schema and config template
- Add FORK_MANAGEMENT.md with mirror-based fork strategy
- Add scripts/rebrand.sh for reapplying branding after upstream merges
- Add packages/tf-sync Python module using official ToothFairyAI SDK
- Add packages/tf-mcp-bridge TypeScript module (stub)
- Multi-region support (AU, EU, US)
- Tool sync: MCP servers, Agent Skills, Database Scripts, API Functions
This commit is contained in:
Gab 2026-03-24 13:02:06 +11:00
parent 7bb69038ec
commit abdfa7330e
14 changed files with 1627 additions and 0 deletions

218
FORK_MANAGEMENT.md Normal file
View File

@ -0,0 +1,218 @@
# tfcode Fork Management Strategy
> How we manage the soft fork of opencode for tfcode development
---
## Repository Architecture
### Three-Repo Model
| Repo | URL | Purpose | Access |
|------|-----|---------|--------|
| **Upstream** | `github.com/anomalyco/opencode` | Official source, releases | Read-only |
| **Mirror** | `gitea.toothfairyai.com/GitHub/opencode` | Auto-mirror of upstream | Read-only |
| **Development** | `gitea.toothfairyai.com/ToothFairyAI/tf_code` | Our fork, tfcode product | Read-write |
### Visual Flow
```
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ UPSTREAM MIRROR DEV REPO │
│ (Official) (Reference) (Our Fork) │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐│
│ │ │ │ │ │ ││
│ │ opencode │───auto────▶│ opencode │──manual──▶│ tf_code ││
│ │ (github) │ mirror │ (gitea) │ PR sync │ (gitea) ││
│ │ │ │ │ │ ││
│ └───────────┘ └───────────┘ └───────────┘│
│ │ │ │ │
│ │ │ │ │
│ Releases Releases + tfcode │
│ (v1.x.x) Commit history product │
│ + TF integrations│
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Sync Workflow
### Per-Release Sync Process
When a new opencode release is published:
**1. Mirror Updates** (Automatic)
- Gitea automatically mirrors the new release
- Tags appear in `GitHub/opencode` repo
**2. Create Sync PR** (Manual)
```bash
# In tf_code repo
git remote add mirror https://gitea.toothfairyai.com/GitHub/opencode.git
git fetch mirror --tags
# Create sync branch from latest release
git checkout -b sync/v1.3.0
git merge mirror/dev --no-commit
# Review conflicts, resolve manually
# Reapply branding changes if needed
# Push and create PR
git push origin sync/v1.3.0
```
**3. Evaluate Merge**
- Review all changes from upstream
- Check for breaking changes
- Verify branding still applies
- Test ToothFairyAI integrations
**4. Merge to Main**
- If approved, merge PR to `main`
- If rejected, note incompatibilities for later work
---
## Rebrand Tracking
### What Needs Rebranding
| Category | Original | Rebranded | Files Affected |
|----------|----------|-----------|----------------|
| **Command** | `opencode` | `tfcode` | CLI, scripts |
| **Package** | `opencode-ai` | `tfcode` | package.json, pyproject.toml |
| **Config** | `opencode.json` | `tfcode.json` | config loaders |
| **Env vars** | `OPENCODE_*` | `TFCODE_*` | documentation, code |
| **URLs** | `opencode.ai` | `toothfairyai.com` | docs, links |
| **Directories** | `.opencode/` | `.tfcode/` | config paths |
| **Branding** | opencode logos | tfcode logos | assets/ |
### Rebrand Script
After each upstream merge, run rebrand script:
```bash
./scripts/rebrand.sh
```
---
## Gitea Configuration
### Mirror Setup
In Gitea admin panel for `GitHub/opencode`:
```
Mirror Settings:
- URL: https://github.com/anomalyco/opencode.git
- Sync Interval: 30 minutes
- Sync: Enable
- LFS: Enable if used
```
---
## Conflict Resolution Guidelines
### Common Conflict Areas
| Area | Conflict Likelihood | Resolution Strategy |
|------|---------------------|---------------------|
| Package.json | High | Keep tfcode name, merge deps |
| Config schemas | Medium | Merge both schemas |
| CLI commands | High | Keep tfcode branding |
| Core engine | Low | Usually clean merge |
| UI/Themes | Medium | Keep tfcode branding |
### Conflict Resolution Process
1. **Identify conflict type**:
- Branding conflict (expected, reapply our changes)
- Feature conflict (evaluate which to keep)
- Breaking change (may require code updates)
2. **Resolution priority**:
- Keep upstream core changes (features, fixes)
- Reapply our branding on top
- Preserve TF integrations
3. **Test after merge**:
- Run test suite
- Verify tfcode branding intact
- Test TF tool sync functionality
---
## Release Schedule
### Upstream Sync Cadence
| Trigger | Action |
|---------|--------|
| **Minor release** (v1.x.0) | Full sync PR, thorough review |
| **Patch release** (v1.x.y) | Quick sync PR, focus on bug fixes |
| **Major release** (v2.0.0) | Careful evaluation, may defer |
### Our Release Process
1. Sync from upstream (if available)
2. Test tfcode functionality
3. Update version in package files
4. Tag release in tf_code repo
5. Build and publish packages
---
## Do's and Don'ts
### ✅ Do
- Keep upstream code changes when merging
- Reapply branding after merges
- Test thoroughly after each sync
- Document any breaking changes from upstream
- Maintain separate release notes for tfcode
### ❌ Don't
- Don't modify mirror repo (read-only)
- Don't skip conflict resolution
- Don't auto-merge without review
- Don't lose TF integration changes during merge
- Don't contribute back to opencode directly (unless agreed)
---
## Quick Reference Commands
```bash
# Add mirror remote (one-time setup)
git remote add mirror https://gitea.toothfairyai.com/GitHub/opencode.git
# Fetch latest from mirror
git fetch mirror --tags
# Check what's new since last sync
git log HEAD..mirror/dev --oneline
# Create sync PR branch
git checkout -b sync/v1.3.0
git merge mirror/dev
# After resolving conflicts
./scripts/rebrand.sh # Reapply branding
npm test # Test
git push origin sync/v1.3.0
# Merge PR after approval
git checkout main
git merge --no-ff sync/v1.3.0
git push origin main
git tag v1.3.0-tf.1
```

452
README.md Normal file
View File

@ -0,0 +1,452 @@
# tfcode
> ToothFairyAI's official coding agent
**Status**: 🟡 Planning Phase | **Last Updated**: 2026-03-24
---
## Product Overview
**tfcode** is ToothFairyAI's official AI coding agent - a terminal-based coding assistant that integrates seamlessly with your TF workspace tools, MCP servers, agent skills, and database connections.
---
## Product Identity
| Aspect | Details |
|--------|---------|
| **Name** | tfcode |
| **Positioning** | ToothFairyAI's official coding agent |
| **Target Users** | Existing TF customers |
| **Config Compatibility** | Supports opencode.json for migration |
---
## Key Features
### Core Capabilities
- Terminal-based AI coding assistant
- File read/write/edit operations
- Bash command execution
- Code search (glob, grep)
- Multi-agent support (Tab switching)
### ToothFairyAI Integration
- **MCP Servers** from TF workspace (`isMCPServer`)
- **Agent Skills** from TF workspace (`isAgentSkill`)
- **Database Scripts** from TF workspace (`isDatabaseScript`)
- **API Functions** from TF workspace (`requestType`)
---
## Fork Management
This is a **soft fork** of the opencode project, managed via a mirror-based strategy.
| Repo | Purpose |
|------|---------|
| `github.com/anomalyco/opencode` | Upstream (official source) |
| `gitea.toothfairyai.com/GitHub/opencode` | Mirror (auto-synced) |
| `gitea.toothfairyai.com/ToothFairyAI/tf_code` | Development (this repo) |
**Sync Strategy**: Per-release manual PR-based sync from mirror to dev repo.
See [FORK_MANAGEMENT.md](./FORK_MANAGEMENT.md) for full details.
---
## Architecture
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ tfcode │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ CORE ENGINE │ │
│ │ │ │
│ │ • TUI (Terminal UI) │ │
│ │ • Agent orchestration │ │
│ │ • Tool execution │ │
│ │ • File operations │ │
│ │ • Code search │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ TOOTHFAIRYAI INTEGRATION │ │
│ │ │ │
│ │ TF WORKSPACE TFCODE │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ Tools: │ │ Synced Tools: │ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ │
│ │ │ │ MCP Servers │ │ SYNC │ │ github-mcp │ │ │ │
│ │ │ │ (isMCPServer)│ │ ──────▶ │ │ auth: proxy │ │ │ │
│ │ │ └──────────────┘ │ │ └──────────────┘ │ │ │
│ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ │
│ │ │ │ Agent Skills │ │ SYNC │ │ code-reviewer│ │ │ │
│ │ │ │(isAgentSkill)│ │ ──────▶ │ │ auth: proxy │ │ │ │
│ │ │ └──────────────┘ │ │ └──────────────┘ │ │ │
│ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ │
│ │ │ │ DB Scripts │ │ SYNC │ │ postgres-main│ │ │ │
│ │ │ │(isDatabase) │ │ ──────▶ │ │ auth: proxy │ │ │ │
│ │ │ └──────────────┘ │ │ └──────────────┘ │ │ │
│ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ │
│ │ │ │ API Functions│ │ SYNC │ │ weather_api │ │ │ │
│ │ │ │(requestType)│ │ ──────▶ │ │ auth: user │ │ │ │
│ │ │ └──────────────┘ │ │ └──────────────┘ │ │ │
│ │ └──────────────────┘ └──────────────────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ CREDENTIALS TOOL CALLS │ │
│ │ (Stay in TF) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ MCP/Skill/DB Tools API Function Tools │ │ │
│ │ │ │ │ │ │ │
│ │ │ ▼ ▼ │ │ │
│ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │
│ │ │ │ TF Proxy │ │ Direct HTTP │ │ │ │
│ │ │ │ • Inject creds │ │ • User api_key │ │ │ │
│ │ │ │ • Route to tool │ │ • Config-based │ │ │ │
│ │ │ └─────────────────┘ └─────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Tool Types
### From ToothFairyAI Workspace
| Type | Flag/Field | Credential Handling |
|------|------------|---------------------|
| MCP Servers | `isMCPServer: true` | TF Proxy (secure) |
| Agent Skills | `isAgentSkill: true` | TF Proxy (secure) |
| Database Scripts | `isDatabaseScript: true` | TF Proxy (secure) |
| API Functions | `requestType` enum | User provides in config |
### requestType Values
```typescript
enum FunctionRequestType {
get = "get",
post = "post",
put = "put",
delete = "delete",
patch = "patch",
custom = "custom",
graphql_query = "graphql_query",
graphql_mutation = "graphql_mutation"
}
```
---
## Configuration
### tfcode.json (Primary)
```json
{
"$schema": "https://toothfairyai.com/schemas/tfcode.json",
"toothfairy": {
"workspace_id": "{env:TF_WORKSPACE_ID}",
"api_key": "{env:TF_API_KEY}",
"region": "au"
},
"tools": {
"github-mcp": {
"type": "mcp_server",
"isMCPServer": true,
"auth_via": "tf_proxy"
},
"code-reviewer": {
"type": "agent_skill",
"isAgentSkill": true,
"auth_via": "tf_proxy"
},
"postgres-main": {
"type": "database_script",
"isDatabaseScript": true,
"auth_via": "tf_proxy"
},
"weather_api": {
"type": "api_function",
"requestType": "get",
"url": "https://api.weatherapi.com/v1/current.json",
"api_key": "{env:WEATHER_API_KEY}"
}
}
}
```
### opencode.json (Migration Support)
For users migrating from opencode, tfcode supports the legacy schema:
```json
{
"$schema": "https://opencode.ai/config.json",
"toothfairy": {
"workspace_id": "{env:TF_WORKSPACE_ID}",
"api_key": "{env:TF_API_KEY}",
"region": "au"
},
"mcp": {
"github-mcp": {
"type": "remote",
"url": "https://mcp.github.com",
"auth_via": "tf_proxy"
}
}
}
```
tfcode automatically detects and converts opencode.json to tfcode.json on first run.
---
## Installation
```bash
# Via curl (recommended)
curl -fsSL https://toothfairyai.com/install/tfcode | bash
# Via npm
npm install -g tfcode
# Via pip
pip install tfcode-cli
# Via brew
brew install toothfairyai/tap/tfcode
```
---
## Quick Start
```bash
# Set environment variables
export TF_API_KEY="tf_live_xxx"
export TF_WORKSPACE_ID="your-workspace-uuid"
# Validate credentials
tfcode validate
# Sync tools from TF workspace
tfcode sync
# Start tfcode
tfcode
# Switch agents with Tab
# Use TF tools with @tool-name:operation
> @github-mcp:repo_list owner="myorg"
> @postgres-main:query sql="SELECT * FROM users"
```
---
## CLI Commands
```bash
# Credential Management
tfcode validate # Test TF credentials
tfcode connect # Interactive credential setup
# Tool Sync
tfcode sync # Sync all tools from TF
tfcode sync --force # Force re-sync
tfcode tools list # List synced tools
tfcode tools list --type mcp # List MCP servers
tfcode tools list --type skill # List agent skills
tfcode tools list --type database # List DB scripts
tfcode tools list --type function # List API functions
# API Function Credentials
tfcode tools credentials <name> --set # Set API key
tfcode tools credentials <name> --show # Show stored key
# Debug
tfcode tools debug <name> # Debug tool connection
tfcode tools test <name> # Test tool call
```
---
## Implementation Phases
### Phase 1: Rebrand & Foundation ⏳ IN PROGRESS
**Tasks**:
- [x] Fork repository structure
- [ ] Rebrand opencode → tfcode
- [ ] Replace all string references
- [ ] Replace URLs (opencode.ai → toothfairyai.com)
- [ ] Replace logos and branding assets
- [ ] Update package name to `tfcode`
- [x] Create new config schema (`tfcode.json`)
- [ ] Add opencode.json → tfcode.json migration
- [x] Define tool type schema
- [x] `isMCPServer` detection
- [x] `isAgentSkill` detection
- [x] `isDatabaseScript` detection
- [x] `requestType` enum handling
- [x] Implement credential validation
- [x] Create TF SDK integration layer
- [x] Multi-region support (AU, EU, US)
### Phase 2: Tool Sync ⏳ IN PROGRESS
**Tasks**:
- [x] Implement TF workspace tool listing via SDK
- [x] Build tool type classifier
- [x] `isMCPServer` detection
- [x] `isAgentSkill` detection
- [x] `isDatabaseScript` detection
- [x] `requestType` enum handling
- [x] Create tool metadata sync using SDK
- [ ] Handle API functions with user credentials
- [ ] `isAgentSkill` detection
- [ ] `isDatabaseScript` detection
- [ ] `requestType` enum handling
- [ ] Create tool metadata sync
- [ ] Handle API functions with user credentials
### Phase 3: TF Proxy Integration
**Tasks**:
- [ ] Build TF Proxy client
- [ ] Implement tool call routing
- [ ] MCP/Skill/DB → TF Proxy
- [ ] API Functions → Direct HTTP
- [ ] Add tool namespace (`@tool-name:operation`)
- [ ] Implement permission checks
### Phase 4: CLI & UX
**Tasks**:
- [ ] Implement tfcode CLI commands
- [ ] Build interactive credential setup
- [ ] Add tool status reporting
- [ ] Create user-friendly error messages
### Phase 5: Documentation & Release
**Tasks**:
- [ ] User documentation
- [ ] Migration guide (opencode → tfcode)
- [ ] Installation scripts
- [ ] Package publishing (npm, pip, brew)
- [ ] First release
---
## Rebrand Checklist
### Code References
- [ ] `opencode``tfcode` in all source files
- [ ] `OPENCODE_``TFCODE_` in environment variables
- [ ] `opencode.json` support (migration) + `tfcode.json` (primary)
- [ ] Package name: `tfcode` (npm, pip)
### URLs & Branding
- [ ] `opencode.ai``toothfairyai.com`
- [ ] `github.com/anomalyco/opencode` → new repo
- [ ] Logo replacement
- [ ] Theme colors (toothfairyai palette)
- [ ] Command: `opencode``tfcode`
### Config
- [ ] Schema URL: `toothfairyai.com/schemas/tfcode.json`
- [ ] Config directory: `~/.config/tfcode/`
- [ ] Data directory: `~/.local/share/tfcode/`
- [ ] Support `opencode.json` for migration
---
## Implementation Notes
### 2026-03-24: Planning & Phase 1 Start
**Product Decision**:
- **Name**: tfcode
- **Positioning**: ToothFairyAI's official coding agent
- **Target**: Existing TF customers
- **Config**: tfcode.json primary, opencode.json supported for migration
**Fork Management**:
- **Upstream**: `github.com/anomalyco/opencode`
- **Mirror**: `gitea.toothfairyai.com/GitHub/opencode` (auto-synced)
- **Development**: `gitea.toothfairyai.com/ToothFairyAI/tf_code` (this repo)
- **Sync Strategy**: Per-release manual PR-based sync
- **Rebrand Script**: `scripts/rebrand.sh` reapplies branding after merges
**Scope**:
- Full rebrand of codebase
- Tool integration: MCP, Skills, Database, API Functions
- Credentials: TF Proxy for secure types, user-provided for API functions
**Architecture**:
- Core engine (TUI, agents, file ops)
- TF integration layer (tool sync, proxy routing)
**Implementation Progress**:
- [x] README created as living document
- [x] tfcode.json schema defined
- [x] Project structure created (packages/tf-sync, packages/tf-mcp-bridge)
- [x] Python SDK integration (using toothfairyai SDK for multi-region support)
- [x] Tool sync module using SDK's agent_functions.list()
- [x] Multi-region URL configuration (AU, EU, US)
- [x] Fork management strategy documented (FORK_MANAGEMENT.md)
- [x] Rebrand script created (scripts/rebrand.sh)
- [ ] TypeScript/Node.js bridge module
- [ ] MCP proxy client
- [ ] CLI commands
**Key Technical Decisions**:
- Use official ToothFairyAI Python SDK for all TF API interactions
- Multi-region support via Region enum and REGION_URLS config
- Tool types classified by: `isMCPServer`, `isAgentSkill`, `isDatabaseScript`, `requestType`
- Sync functions are synchronous (SDK handles async internally)
---
## Resources
### ToothFairyAI
- [Developer Portal](https://toothfairyai.com/developers)
- [API Documentation](https://apidocs.toothfairyai.com)
- [Functions/Tools](https://docs.toothfairyai.com/docs/Settings/functions)
### Downloads
- [tfcode CLI](https://toothfairyai.com/install/tfcode)
- [npm package](https://www.npmjs.com/package/tfcode)
- [PyPI package](https://pypi.org/project/tfcode-cli/)
---
## License
MIT License
---
*Living document - Updated: 2026-03-24*

32
opencode.json Normal file
View File

@ -0,0 +1,32 @@
{
"$schema": "https://opencode.ai/config.json",
"autoupdate": true,
"model": "fireworks-ai/accounts/fireworks/models/glm-5",
"mcp": {
"toothfairy": {
"type": "remote",
"url": "https://mcp.toothfairyai.com/sse",
"enabled": true
},
"toothfairy_dev": {
"type": "remote",
"url": "https://mcp.toothfairylab.link/sse",
"enabled": false
}
},
"agent": {
"tf": {
"mode": "primary",
"model": "fireworks-ai/accounts/fireworks/models/glm-5",
"temperature": 1,
"description": "ToothFairy agent with high creativity",
"prompt": "You are a talented code co-author that never performs rollbacks of files especially in the git stack whether they are staged or stashed. You must help the human coder in making specific changes that can be inspected leveraging git changes in VS code"
}
},
"formatter": {
"black": {
"command": ["black", "$FILE"],
"extensions": [".py", ".pyi"]
}
}
}

View File

@ -0,0 +1,27 @@
{
"name": "tf-mcp-bridge",
"version": "0.1.0",
"description": "MCP proxy bridge for tfcode",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --dts",
"dev": "tsup src/index.ts --dts --watch",
"typecheck": "tsc --noEmit",
"lint": "eslint src/",
"test": "vitest"
},
"dependencies": {
"zod": "^3.22.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsup": "^8.0.0",
"typescript": "^5.3.0",
"vitest": "^1.0.0",
"eslint": "^8.0.0"
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,10 @@
/**
* tf-mcp-bridge: MCP proxy bridge for tfcode
*
* Handles communication with ToothFairyAI MCP proxy for secure tool calls.
* Credentials stay in TF - this module routes calls through the proxy.
*/
export * from './proxy';
export * from './router';
export * from './types';

View File

@ -0,0 +1,35 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "tf-sync"
version = "0.1.0"
description = "ToothFairyAI workspace sync layer for tfcode"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"toothfairyai>=1.0.0",
"pydantic>=2.0.0",
"httpx>=0.25.0",
"rich>=13.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"mypy>=1.0.0",
"ruff>=0.1.0",
]
[tool.hatch.build.targets.wheel]
packages = ["src/tf_sync"]
[tool.ruff]
line-length = 100
target-version = "py310"
[tool.mypy]
python_version = "3.10"
strict = true

View File

@ -0,0 +1,21 @@
"""
tf-sync: ToothFairyAI workspace sync layer for tfcode
"""
from tf_sync.agents import sync_agents
from tf_sync.mcp import sync_mcp_servers
from tf_sync.tools import sync_tools, ToolType
from tf_sync.config import TFConfig, load_config, validate_credentials, get_region_urls
__all__ = [
"sync_agents",
"sync_mcp_servers",
"sync_tools",
"ToolType",
"TFConfig",
"load_config",
"validate_credentials",
"get_region_urls",
]
__version__ = "0.1.0"

View File

@ -0,0 +1,39 @@
"""
Agent sync module for tfcode.
NOTE: This module is reserved for future implementation.
Currently, tfcode only syncs tools (MCP, Skills, Database, Functions).
Agent sync will be added in a later phase.
"""
from typing import Any
from pydantic import BaseModel
from tf_sync.config import TFConfig
class AgentSyncResult(BaseModel):
"""Result of agent sync operation."""
success: bool
agents: list[dict[str, Any]] = []
error: str | None = None
def sync_agents(config: TFConfig) -> AgentSyncResult:
"""
Sync agents from ToothFairyAI workspace.
NOTE: Currently not implemented. Reserved for future use.
Args:
config: TFConfig instance
Returns:
AgentSyncResult (currently always returns not implemented)
"""
return AgentSyncResult(
success=False,
error="Agent sync not yet implemented. Use tools sync for now.",
)

View File

@ -0,0 +1,211 @@
"""
Configuration management for tfcode ToothFairyAI integration.
Uses the official ToothFairyAI Python SDK for multi-region support.
"""
import os
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field, SecretStr
from toothfairyai import ToothFairyClient
from toothfairyai.errors import ToothFairyError
class Region(str, Enum):
AU = "au"
EU = "eu"
US = "us"
class ToolType(str, Enum):
MCP_SERVER = "mcp_server"
AGENT_SKILL = "agent_skill"
DATABASE_SCRIPT = "database_script"
API_FUNCTION = "api_function"
class FunctionRequestType(str, Enum):
GET = "get"
POST = "post"
PUT = "put"
DELETE = "delete"
PATCH = "patch"
CUSTOM = "custom"
GRAPHQL_QUERY = "graphql_query"
GRAPHQL_MUTATION = "graphql_mutation"
# Region-specific URL configurations
REGION_URLS = {
Region.AU: {
"base_url": "https://api.au.toothfairyai.com",
"ai_url": "https://ai.au.toothfairyai.com",
"ai_stream_url": "https://ais.au.toothfairyai.com",
"mcp_url": "https://mcp.au.toothfairyai.com/sse",
"mcp_proxy_url": "https://mcp-proxy.au.toothfairyai.com",
},
Region.EU: {
"base_url": "https://api.eu.toothfairyai.com",
"ai_url": "https://ai.eu.toothfairyai.com",
"ai_stream_url": "https://ais.eu.toothfairyai.com",
"mcp_url": "https://mcp.eu.toothfairyai.com/sse",
"mcp_proxy_url": "https://mcp-proxy.eu.toothfairyai.com",
},
Region.US: {
"base_url": "https://api.us.toothfairyai.com",
"ai_url": "https://ai.us.toothfairyai.com",
"ai_stream_url": "https://ais.us.toothfairyai.com",
"mcp_url": "https://mcp.us.toothfairyai.com/sse",
"mcp_proxy_url": "https://mcp-proxy.us.toothfairyai.com",
},
}
def get_region_urls(region: Region) -> dict[str, str]:
"""Get URLs for a specific region."""
return REGION_URLS.get(region, REGION_URLS[Region.AU])
class TFConfig(BaseModel):
"""ToothFairyAI workspace configuration."""
workspace_id: str
api_key: SecretStr
region: Region = Region.AU
enabled: bool = True
sync_interval: int = Field(default=3600, ge=60)
mcp_proxy_timeout: int = Field(default=30000, ge=1000)
_client: Optional[ToothFairyClient] = None
def get_client(self) -> ToothFairyClient:
"""
Get or create a ToothFairyClient instance configured for this region.
Returns:
ToothFairyClient configured with region-specific URLs
"""
if self._client is None:
urls = get_region_urls(self.region)
self._client = ToothFairyClient(
api_key=self.api_key.get_secret_value(),
workspace_id=self.workspace_id,
base_url=urls["base_url"],
ai_url=urls["ai_url"],
ai_stream_url=urls["ai_stream_url"],
)
return self._client
@property
def mcp_sse_url(self) -> str:
"""Get the MCP SSE endpoint URL for this region."""
return get_region_urls(self.region)["mcp_url"]
@property
def mcp_proxy_url(self) -> str:
"""Get the MCP proxy URL for this region."""
return get_region_urls(self.region)["mcp_proxy_url"]
class CredentialValidationResult(BaseModel):
"""Result of credential validation."""
success: bool
workspace_id: Optional[str] = None
workspace_name: Optional[str] = None
error: Optional[str] = None
def load_config(
workspace_id: Optional[str] = None,
api_key: Optional[str] = None,
region: Optional[Region] = None,
) -> TFConfig:
"""
Load ToothFairyAI configuration from environment or parameters.
Args:
workspace_id: Workspace UUID (defaults to TF_WORKSPACE_ID env var)
api_key: API key (defaults to TF_API_KEY env var)
region: Region (defaults to TF_REGION env var or 'au')
Returns:
TFConfig instance
Raises:
ValueError: If required configuration is missing
"""
ws_id = workspace_id or os.environ.get("TF_WORKSPACE_ID")
key = api_key or os.environ.get("TF_API_KEY")
# Parse region from env or use provided/default
region_str = os.environ.get("TF_REGION", "au")
reg = region or Region(region_str)
if not ws_id:
raise ValueError("TF_WORKSPACE_ID not set. Set environment variable or pass workspace_id.")
if not key:
raise ValueError("TF_API_KEY not set. Set environment variable or pass api_key.")
return TFConfig(
workspace_id=ws_id,
api_key=SecretStr(key),
region=reg,
)
def validate_credentials(config: TFConfig) -> CredentialValidationResult:
"""
Validate ToothFairyAI credentials using the SDK.
Args:
config: TFConfig instance
Returns:
CredentialValidationResult indicating success or failure
"""
try:
client = config.get_client()
# Test connection by listing chats (lightweight operation)
if client.test_connection():
return CredentialValidationResult(
success=True,
workspace_id=config.workspace_id,
workspace_name="Connected",
)
else:
return CredentialValidationResult(
success=False,
error="Connection test failed. Check credentials and region.",
)
except ToothFairyError as e:
error_msg = str(e)
if "401" in error_msg or "Unauthorized" in error_msg:
return CredentialValidationResult(
success=False,
error="Invalid API key. Check TF_API_KEY environment variable.",
)
elif "403" in error_msg or "Forbidden" in error_msg:
return CredentialValidationResult(
success=False,
error="API access not allowed. Business or Enterprise subscription required.",
)
elif "404" in error_msg or "Not Found" in error_msg:
return CredentialValidationResult(
success=False,
error="Workspace not found. Check TF_WORKSPACE_ID environment variable.",
)
else:
return CredentialValidationResult(
success=False,
error=f"API error: {error_msg}",
)
except Exception as e:
return CredentialValidationResult(
success=False,
error=f"Unexpected error: {str(e)}",
)

View File

@ -0,0 +1,54 @@
"""
MCP server sync module for tfcode.
Uses the official ToothFairyAI Python SDK.
"""
from pydantic import BaseModel
from tf_sync.config import TFConfig
from tf_sync.tools import SyncedTool, sync_tools, ToolType
class MCPServerSyncResult(BaseModel):
"""Result of MCP server sync operation."""
success: bool
servers: list[SyncedTool] = []
error: str | None = None
def sync_mcp_servers(config: TFConfig) -> MCPServerSyncResult:
"""
Sync MCP servers from ToothFairyAI workspace.
MCP servers are tools with isMCPServer=true.
Credentials stay in TF and are accessed via tf_proxy.
Args:
config: TFConfig instance
Returns:
MCPServerSyncResult with synced MCP servers
"""
result = sync_tools_by_type(config, [ToolType.MCP_SERVER])
if not result.success:
return MCPServerSyncResult(
success=False,
error=result.error,
)
# Get MCP servers from tools with isMCPServer
mcp_servers = [
t for t in result.tools
if t.is_mcp_server
]
return MCPServerSyncResult(
success=True,
servers=mcp_servers,
)
# Import from tools module
from tf_sync.tools import sync_tools_by_type

View File

@ -0,0 +1,185 @@
"""
Tool sync module for tfcode.
Syncs MCP servers, Agent Skills, Database Scripts, and API Functions from ToothFairyAI workspace.
Uses the official ToothFairyAI Python SDK.
"""
from typing import Any, Optional
from pydantic import BaseModel
from toothfairyai.types import AgentFunction
from tf_sync.config import TFConfig, ToolType, FunctionRequestType
class SyncedTool(BaseModel):
"""A tool synced from ToothFairyAI workspace."""
id: str
name: str
description: Optional[str] = None
tool_type: ToolType
is_mcp_server: bool = False
is_agent_skill: bool = False
is_database_script: bool = False
request_type: Optional[FunctionRequestType] = None
url: Optional[str] = None
tools: list[str] = []
auth_via: str = "tf_proxy"
class ToolSyncResult(BaseModel):
"""Result of tool sync operation."""
success: bool
tools: list[SyncedTool] = []
by_type: dict[str, int] = {}
error: Optional[str] = None
def classify_tool(tool: AgentFunction) -> ToolType:
"""
Classify a tool based on its flags and fields.
Args:
tool: AgentFunction from TF SDK
Returns:
ToolType enum value
"""
if tool.is_mcp_server:
return ToolType.MCP_SERVER
if tool.is_agent_skill:
return ToolType.AGENT_SKILL
if tool.is_database_script:
return ToolType.DATABASE_SCRIPT
if tool.request_type:
return ToolType.API_FUNCTION
return ToolType.API_FUNCTION
def parse_tool(tool: AgentFunction) -> SyncedTool:
"""
Parse AgentFunction from SDK into SyncedTool.
Args:
tool: AgentFunction from TF SDK
Returns:
SyncedTool instance
"""
tool_type = classify_tool(tool)
request_type_enum = None
if tool.request_type:
try:
request_type_enum = FunctionRequestType(tool.request_type)
except ValueError:
pass
auth_via = "user_provided" if tool_type == ToolType.API_FUNCTION else "tf_proxy"
return SyncedTool(
id=tool.id,
name=tool.name,
description=tool.description,
tool_type=tool_type,
is_mcp_server=tool.is_mcp_server or False,
is_agent_skill=tool.is_agent_skill or False,
is_database_script=tool.is_database_script or False,
request_type=request_type_enum,
url=tool.url,
tools=[],
auth_via=auth_via,
)
def sync_tools(config: TFConfig) -> ToolSyncResult:
"""
Sync all tools from ToothFairyAI workspace using SDK.
Args:
config: TFConfig instance
Returns:
ToolSyncResult with synced tools
"""
try:
client = config.get_client()
result = client.agent_functions.list()
tools = [parse_tool(f) for f in result.items]
by_type = {}
for tool in tools:
type_name = tool.tool_type.value
by_type[type_name] = by_type.get(type_name, 0) + 1
return ToolSyncResult(
success=True,
tools=tools,
by_type=by_type,
)
except Exception as e:
return ToolSyncResult(
success=False,
error=f"Sync failed: {str(e)}",
)
def sync_tools_by_type(
config: TFConfig,
tool_types: Optional[list[ToolType]] = None,
) -> ToolSyncResult:
"""
Sync tools of specific types from ToothFairyAI workspace.
Args:
config: TFConfig instance
tool_types: List of ToolType to sync (None = all)
Returns:
ToolSyncResult with filtered tools
"""
result = sync_tools(config)
if not result.success or not tool_types:
return result
filtered = [t for t in result.tools if t.tool_type in tool_types]
by_type = {}
for tool in filtered:
type_name = tool.tool_type.value
by_type[type_name] = by_type.get(type_name, 0) + 1
return ToolSyncResult(
success=True,
tools=filtered,
by_type=by_type,
)
def sync_mcp_servers_only(config: TFConfig) -> ToolSyncResult:
"""Sync only MCP servers (isMCPServer=true)."""
return sync_tools_by_type(config, [ToolType.MCP_SERVER])
def sync_agent_skills_only(config: TFConfig) -> ToolSyncResult:
"""Sync only Agent Skills (isAgentSkill=true)."""
return sync_tools_by_type(config, [ToolType.AGENT_SKILL])
def sync_database_scripts_only(config: TFConfig) -> ToolSyncResult:
"""Sync only Database Scripts (isDatabaseScript=true)."""
return sync_tools_by_type(config, [ToolType.DATABASE_SCRIPT])
def sync_api_functions_only(config: TFConfig) -> ToolSyncResult:
"""Sync only API Functions (has requestType)."""
return sync_tools_by_type(config, [ToolType.API_FUNCTION])

119
schemas/tfcode.schema.json Normal file
View File

@ -0,0 +1,119 @@
{
"$schema": "https://toothfairyai.com/schemas/tfcode.json",
"$defs": {
"ToolType": {
"type": "string",
"enum": ["mcp_server", "agent_skill", "database_script", "api_function"]
},
"FunctionRequestType": {
"type": "string",
"enum": ["get", "post", "put", "delete", "patch", "custom", "graphql_query", "graphql_mutation"]
},
"AuthVia": {
"type": "string",
"enum": ["tf_proxy", "user_provided"]
},
"Tool": {
"type": "object",
"properties": {
"type": { "$ref": "#/$defs/ToolType" },
"isMCPServer": { "type": "boolean" },
"isAgentSkill": { "type": "boolean" },
"isDatabaseScript": { "type": "boolean" },
"requestType": { "$ref": "#/$defs/FunctionRequestType" },
"url": { "type": "string" },
"auth_via": { "$ref": "#/$defs/AuthVia" },
"api_key": { "type": "string" },
"tools": {
"type": "array",
"items": { "type": "string" }
},
"description": { "type": "string" }
}
}
},
"type": "object",
"properties": {
"$schema": { "type": "string" },
"toothfairy": {
"type": "object",
"properties": {
"enabled": { "type": "boolean", "default": true },
"workspace_id": { "type": "string" },
"api_key": { "type": "string" },
"region": {
"type": "string",
"enum": ["au", "eu", "us"],
"default": "au"
},
"sync": {
"type": "object",
"properties": {
"on_startup": { "type": "boolean", "default": true },
"interval": { "type": "integer", "default": 3600 },
"tools": {
"type": "object",
"properties": {
"types": {
"type": "array",
"items": { "type": "string", "enum": ["mcp", "skill", "database", "function"] }
}
}
}
}
},
"mcp_proxy": {
"type": "object",
"properties": {
"url": { "type": "string" },
"timeout": { "type": "integer", "default": 30000 }
}
}
},
"required": ["workspace_id", "api_key"]
},
"tools": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/Tool" }
},
"agent": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"mode": { "type": "string", "enum": ["primary", "subagent"] },
"model": { "type": "string" },
"temperature": { "type": "number" },
"description": { "type": "string" },
"prompt": { "type": "string" },
"tools": {
"type": "array",
"items": { "type": "string" }
},
"mcp": {
"type": "array",
"items": { "type": "string" }
}
}
}
},
"model": { "type": "string" },
"autoupdate": { "type": "boolean" },
"formatter": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"command": {
"type": "array",
"items": { "type": "string" }
},
"extensions": {
"type": "array",
"items": { "type": "string" }
}
}
}
}
}
}

196
scripts/rebrand.sh Executable file
View File

@ -0,0 +1,196 @@
#!/bin/bash
# tfcode Rebrand Script
# Reapplies all branding changes after upstream merge
# Run this after merging from opencode mirror
set -e
echo "🔧 Applying tfcode branding..."
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Track changes
CHANGES=0
# Function to count changes
count_changes() {
CHANGES=$((CHANGES + 1))
}
# Function to sed with both extensions (GNU and BSD)
safe_sed() {
local pattern="$1"
local file="$2"
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS BSD sed
sed -i '' "$pattern" "$file" 2>/dev/null || true
else
# GNU sed
sed -i "$pattern" "$file" 2>/dev/null || true
fi
count_changes
}
echo ""
echo "📁 Processing TypeScript/JavaScript files..."
# TypeScript and JavaScript files
find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" -o -name "*.mjs" -o -name "*.cjs" \) \
-not -path "*/node_modules/*" \
-not -path "*/.git/*" \
-not -path "*/dist/*" \
-not -path "*/build/*" | while read file; do
# Command name: opencode -> tfcode
safe_sed 's/opencode/tfcode/g' "$file"
# Package name: opencode-ai -> tfcode
safe_sed 's/opencode-ai/tfcode/g' "$file"
# Env vars: OPENCODE_ -> TFCODE_
safe_sed 's/OPENCODE_/TFCODE_/g' "$file"
# URLs: opencode.ai -> toothfairyai.com
safe_sed 's/opencode\.ai/toothfairyai.com/g' "$file"
# Config directory: .opencode -> .tfcode
safe_sed 's/\.opencode/.tfcode/g' "$file"
done
echo ""
echo "📁 Processing JSON files..."
# JSON files
find . -type f -name "*.json" \
-not -path "*/node_modules/*" \
-not -path "*/.git/*" \
-not -path "*/dist/*" \
-not -path "*/build/*" \
-not -name "package-lock.json" | while read file; do
safe_sed 's/opencode/tfcode/g' "$file"
safe_sed 's/opencode-ai/tfcode/g' "$file"
safe_sed 's/OPENCODE_/TFCODE_/g' "$file"
safe_sed 's/opencode\.ai/toothfairyai.com/g' "$file"
safe_sed 's/\.opencode/.tfcode/g' "$file"
done
echo ""
echo "📁 Processing Markdown files..."
# Markdown files
find . -type f -name "*.md" \
-not -path "*/node_modules/*" \
-not -path "*/.git/*" | while read file; do
safe_sed 's/opencode/tfcode/g' "$file"
safe_sed 's/opencode-ai/tfcode/g' "$file"
safe_sed 's/OPENCODE_/TFCODE_/g' "$file"
safe_sed 's/opencode\.ai/toothfairyai.com/g' "$file"
safe_sed 's/\.opencode/.tfcode/g' "$file"
done
echo ""
echo "📁 Processing Shell scripts..."
# Shell scripts
find . -type f \( -name "*.sh" -o -name "*.bash" \) \
-not -path "*/.git/*" | while read file; do
safe_sed 's/opencode/tfcode/g' "$file"
safe_sed 's/opencode-ai/tfcode/g' "$file"
safe_sed 's/OPENCODE_/TFCODE_/g' "$file"
safe_sed 's/opencode\.ai/toothfairyai.com/g' "$file"
done
echo ""
echo "📁 Processing Python files..."
# Python files
find . -type f -name "*.py" \
-not -path "*/.git/*" \
-not -path "*/__pycache__/*" \
-not -path "*/.venv/*" \
-not -path "*/venv/*" | while read file; do
safe_sed 's/opencode/tfcode/g' "$file"
safe_sed 's/opencode-ai/tfcode/g' "$file"
safe_sed 's/OPENCODE_/TFCODE_/g' "$file"
safe_sed 's/opencode\.ai/toothfairyai.com/g' "$file"
done
echo ""
echo "📁 Processing YAML files..."
# YAML files
find . -type f \( -name "*.yml" -o -name "*.yaml" \) \
-not -path "*/node_modules/*" \
-not -path "*/.git/*" | while read file; do
safe_sed 's/opencode/tfcode/g' "$file"
safe_sed 's/opencode-ai/tfcode/g' "$file"
safe_sed 's/OPENCODE_/TFCODE_/g' "$file"
safe_sed 's/opencode\.ai/toothfairyai.com/g' "$file"
done
echo ""
echo "📁 Processing config files..."
# Config files (no extension or specific names)
find . -type f \( \
-name "Makefile" -o \
-name "Dockerfile*" -o \
-name ".env*" -o \
-name "LICENSE" -o \
-name "AUTHORS" -o \
-name "CONTRIBUTORS" \
\) -not -path "*/.git/*" | while read file; do
safe_sed 's/opencode/tfcode/g' "$file"
safe_sed 's/opencode-ai/tfcode/g' "$file"
safe_sed 's/OPENCODE_/TFCODE_/g' "$file"
safe_sed 's/opencode\.ai/toothfairyai.com/g' "$file"
done
echo ""
echo "🔄 Renaming config files..."
# Rename opencode.json to tfcode.json if it exists
if [ -f "opencode.json" ]; then
if [ ! -f "tfcode.json" ]; then
mv opencode.json tfcode.json
echo " ✓ Renamed opencode.json -> tfcode.json"
else
echo " ⚠ tfcode.json already exists, keeping both files"
fi
fi
# Rename .opencode directory to .tfcode if it exists
if [ -d ".opencode" ]; then
if [ ! -d ".tfcode" ]; then
mv .opencode .tfcode
echo " ✓ Renamed .opencode/ -> .tfcode/"
else
echo " ⚠ .tfcode/ already exists, merging directories"
cp -r .opencode/* .tfcode/ 2>/dev/null || true
fi
fi
echo ""
echo "${GREEN}✅ Rebranding complete!${NC}"
echo ""
echo "Summary:"
echo " - Processed files and applied branding changes"
echo " - Renamed config files where applicable"
echo ""
echo "${YELLOW}Next steps:${NC}"
echo " 1. Review changes: git diff"
echo " 2. Run tests: npm test (or equivalent)"
echo " 3. Commit changes: git add . && git commit -m 'chore: reapply tfcode branding'"
echo ""

28
tfcode.json Normal file
View File

@ -0,0 +1,28 @@
{
"$schema": "./schemas/tfcode.schema.json",
"toothfairy": {
"workspace_id": "{env:TF_WORKSPACE_ID}",
"api_key": "{env:TF_API_KEY}",
"region": "au",
"sync": {
"on_startup": true,
"interval": 3600,
"tools": {
"types": ["mcp", "skill", "database", "function"]
}
},
"mcp_proxy": {
"url": "https://mcp-proxy.{region}.toothfairyai.com",
"timeout": 30000
}
},
"tools": {},
"agent": {
"build": {
"mode": "primary",
"description": "Default coding agent",
"mcp": []
}
},
"autoupdate": true
}