feat: sync

This commit is contained in:
Gab 2026-03-24 13:51:14 +11:00
parent 39bd38040c
commit 4596310485
20 changed files with 1356 additions and 101 deletions

View File

@ -290,13 +290,29 @@ tf_code/
## Installation
```bash
# Via curl (recommended)
curl -fsSL https://toothfairyai.com/install/tfcode | bash
### Requirements
# Via npm
**Python 3.10+ is required** on your machine for ToothFairyAI integration.
```bash
# Check Python version
python3 --version # Should be 3.10 or higher
# Install Python if needed
# macOS: brew install python@3.12
# Ubuntu: sudo apt-get install python3.12
# Windows: Download from https://python.org/downloads
```
### Install tfcode
```bash
# Via npm (recommended)
npm install -g tfcode
# Via curl
curl -fsSL https://toothfairyai.com/install/tfcode | bash
# Via pip
pip install tfcode-cli
@ -304,6 +320,8 @@ pip install tfcode-cli
brew install toothfairyai/tap/tfcode
```
The postinstall script will automatically install the ToothFairyAI Python SDK.
---
## Quick Start
@ -383,17 +401,27 @@ tfcode tools test <name> # Test tool call
- [x] Document fork management strategy
- [x] Push to development branch
### Phase 2: Tool Sync ⏳ NEXT
### Phase 2: Tool Sync ✅ COMPLETE
**Tasks**:
- [ ] Complete tf-sync Python module
- [ ] Test tool sync with real TF workspace
- [ ] Handle tool metadata caching
**Completed**:
- [x] Complete tf-sync Python module
- [x] Tool sync implementation
- [x] Tool metadata caching in tfcode
- [x] Implement tfcode CLI commands
- [x] `tfcode validate` - credential validation
- [x] `tfcode sync` - sync tools from TF workspace
- [x] `tfcode tools list` - list synced tools
- [x] `tfcode tools list --type mcp|skill|database|function`
- [x] `tfcode tools credentials <name> --set/--show`
- [x] `tfcode tools debug <name>`
- [x] Handle API functions with user credentials
- [x] Tool cache persistence (~/.tfcode/tools.json)
**Pending (Phase 3)**:
- [ ] Test tool sync with real TF workspace
- [ ] Build tf-mcp-bridge TypeScript module
- [ ] Bridge between tf-sync and tfcode core
- [ ] MCP proxy client implementation
- [ ] Handle API functions with user credentials
- [ ] Implement tool refresh/reload
- [ ] MCP proxy client implementation
- [ ] Tool refresh/reload
### Phase 3: TF Proxy Integration
@ -407,11 +435,14 @@ tfcode tools test <name> # Test tool call
### Phase 4: CLI & UX
**Completed**:
- [x] Implement tfcode CLI commands
- [x] `tfcode validate`
- [x] `tfcode sync`
- [x] `tfcode tools list`
- [x] `tfcode tools credentials`
**Tasks**:
- [ ] Implement tfcode CLI commands
- [ ] `tfcode validate`
- [ ] `tfcode sync`
- [ ] `tfcode tools list`
- [ ] Build interactive credential setup
- [ ] Add tool status reporting
- [ ] Create user-friendly error messages
@ -490,16 +521,16 @@ abdfa7330 feat: initialize tfcode project structure
**What's Ready**:
- ✅ Clean codebase with minimal branding
- ✅ TF SDK integration layer (Python)
- ✅ Tool sync module structure
- ✅ Tool sync module complete
- ✅ CLI commands implemented (`validate`, `sync`, `tools`)
- ✅ Multi-region support
- ✅ Config schema defined
- ✅ Documentation structure
**What's Next**:
- Complete tf-sync Python module
- Build tf-mcp-bridge TypeScript module
- Test with real TF workspace
- Implement CLI commands
- Build tf-mcp-bridge TypeScript module
- Implement TF Proxy client for tool call routing
---

115
docs/sync-implementation.md Normal file
View File

@ -0,0 +1,115 @@
# tfcode Sync Implementation Summary
## What's Implemented
### 1. Postinstall Script (`packages/tfcode/scripts/postinstall.cjs`)
- Checks for Python 3.10+ installation
- Auto-installs ToothFairyAI Python SDK on `npm install tfcode`
- Handles externally-managed environments (Homebrew, etc.)
- Shows clear error messages if Python is missing
### 2. Python Sync Module (`packages/tf-sync/src/tf_sync/`)
- `config.py` - Configuration management, credential validation, multi-region support
- `tools.py` - Sync API functions from TF workspace
- `mcp.py` - Reserved for future MCP support (not in SDK yet)
### 3. CLI Commands (`packages/tfcode/src/cli/cmd/tools.ts`)
- `tfcode validate` - Validate TF credentials
- `tfcode sync` - Sync tools from workspace
- `tfcode tools list [--type]` - List synced tools
- `tfcode tools credentials <name> --set/--show` - Manage API keys
- `tfcode tools debug <name>` - Debug tool configuration
### 4. Region Support
| Region | Base URL | Use Case |
|--------|----------|----------|
| `dev` | api.toothfairylab.link | Development |
| `au` | api.toothfairyai.com | Australia (Default) |
| `eu` | api.eu.toothfairyai.com | Europe |
| `us` | api.us.toothfairyai.com | United States |
## Test Results
```bash
$ python3 scripts/test-sync.py
============================================================
tfcode Sync Test
============================================================
Test 1: Load Configuration
----------------------------------------
✓ Workspace ID: 6586b7e6-683e-4ee6-a6cf-24c19729b5ff
✓ Region: dev
✓ API URL: https://api.toothfairylab.link
Test 2: Validate Credentials
----------------------------------------
✓ Credentials valid
Workspace: Connected
ID: 6586b7e6-683e-4ee6-a6cf-24c19729b5ff
Test 3: Sync Tools
----------------------------------------
✓ Synced 100 tools
By type:
api_function: 100
Sample tools:
🌐 get_kanban_projects (api_function)
🌐 azure_costs_test_official (api_function)
... and 95 more
```
## SDK Capabilities (Current)
The ToothFairyAI Python SDK currently exposes:
- `agent_functions` - API Functions with `request_type` (get/post/put/delete/etc.)
- `connections` - Provider connections (openai, anthropic, groq, etc.)
- `agents` - TF workspace agents
**Not yet exposed:**
- MCP servers (`isMCPServer`)
- Agent skills (`isAgentSkill`)
- Database scripts (`isDatabaseScript`)
These will be added to the SDK in the future. For now, MCP servers must be configured manually in `tfcode.json`.
## Environment Setup
```bash
export TF_WORKSPACE_ID="your-workspace-id"
export TF_API_KEY="your-api-key"
export TF_REGION="dev" # or au, eu, us
```
## Usage
```bash
# Install
npm install -g tfcode
# Or with bun
cd packages/tf-sync && python3 -m pip install -e . --break-system-packages
cd packages/tfcode && bun install
# Validate
tfcode validate
# Sync
tfcode sync
# List tools
tfcode tools list
```
## Files Created/Modified
- `packages/tfcode/scripts/postinstall.cjs` - Postinstall script
- `packages/tfcode/src/cli/cmd/tools.ts` - CLI commands
- `packages/tf-sync/src/tf_sync/config.py` - Config + regions
- `packages/tf-sync/src/tf_sync/tools.py` - Tool sync
- `packages/tf-sync/src/tf_sync/mcp.py` - MCP placeholder
- `scripts/test-sync.py` - Test script
- `scripts/setup-tf-dev.sh` - Dev environment setup

129
docs/testing-sync.md Normal file
View File

@ -0,0 +1,129 @@
# Step-by-Step Guide: Testing tfcode Sync
## Prerequisites
1. Python 3.10+ with `toothfairyai` SDK installed
2. Node.js/Bun for running tfcode
## Step 1: Set Environment Variables
```bash
# In your terminal
export TF_WORKSPACE_ID="6586b7e6-683e-4ee6-a6cf-24c19729b5ff"
export TF_API_KEY="EWZooLROIS57EVW3BKGu7Pv6LNe4D6m4gkDjukx3"
export TF_REGION="au"
```
Or use the setup script:
```bash
source scripts/setup-tf-dev.sh
```
## Step 2: Install Python Dependencies
```bash
cd packages/tf-sync
pip install -e .
# Or manually install the required packages:
pip install toothfairyai pydantic httpx rich
```
## Step 3: Build tfcode CLI
```bash
cd packages/tfcode
bun install
bun run build
```
## Step 4: Test Credential Validation
```bash
# From repo root with environment set
cd packages/tfcode
bun run src/index.ts validate
```
Expected output:
```
Validating ToothFairyAI credentials...
✓ Credentials valid
Workspace: <workspace_name>
ID: 6586b7e6-683e-4ee6-a6cf-24c19729b5ff
```
## Step 5: Sync Tools from Workspace
```bash
bun run src/index.ts sync
```
Expected output:
```
Syncing tools from ToothFairyAI workspace...
✓ Synced X tools
By type:
mcp_server: X
agent_skill: X
database_script: X
api_function: X
```
## Step 6: List Synced Tools
```bash
# List all tools
bun run src/index.ts tools list
# Filter by type
bun run src/index.ts tools list --type mcp
bun run src/index.ts tools list --type skill
bun run src/index.ts tools list --type database
bun run src/index.ts tools list --type function
```
## Step 7: Debug a Tool
```bash
bun run src/index.ts tools debug <tool-name>
```
## Step 8: Set API Function Credentials (if needed)
For tools with `auth_via: user_provided`:
```bash
bun run src/index.ts tools credentials <tool-name> --set
# Enter API key when prompted
bun run src/index.ts tools credentials <tool-name> --show
# Shows masked key
```
## Troubleshooting
### Python SDK not found
```
Error: Failed to validate: Python sync failed: ModuleNotFoundError: No module named 'toothfairyai'
```
Solution: `pip install toothfairyai`
### Environment not set
```
Error: TF_WORKSPACE_ID not set
```
Solution: Export environment variables or source the setup script
### Invalid credentials
```
✗ Validation failed: Invalid API key
```
Solution: Check your TF_API_KEY is correct
### Workspace not found
```
✗ Validation failed: Workspace not found
```
Solution: Check your TF_WORKSPACE_ID is correct

View File

@ -0,0 +1,71 @@
# tf-sync
ToothFairyAI workspace sync layer for tfcode.
## Purpose
This Python module syncs tools from a ToothFairyAI workspace to tfcode:
- **MCP Servers** - Tools with `isMCPServer=true`
- **Agent Skills** - Tools with `isAgentSkill=true`
- **Database Scripts** - Tools with `isDatabaseScript=true`
- **API Functions** - Tools with `requestType` set
## Installation
```bash
pip install tf-sync
```
## Usage
```python
from tf_sync import (
load_config,
validate_credentials,
sync_tools,
sync_mcp_servers,
ToolType,
)
# Load configuration from environment (TF_WORKSPACE_ID, TF_API_KEY, TF_REGION)
config = load_config()
# Validate credentials
result = validate_credentials(config)
if result.success:
print(f"Connected to workspace: {result.workspace_name}")
# Sync all tools (API Functions)
result = sync_tools(config)
for tool in result.tools:
print(f"- {tool.name} ({tool.tool_type.value})")
```
## Configuration
Set these environment variables:
- `TF_WORKSPACE_ID` - Your ToothFairyAI workspace UUID
- `TF_API_KEY` - Your ToothFairyAI API key
- `TF_REGION` - Region: `dev`, `au` (default), `eu`, or `us`
## Region URLs
| Region | Base URL | Use Case |
|--------|----------|----------|
| `dev` | api.toothfairylab.link | Development/Testing |
| `au` | api.toothfairyai.com | Australia (Default) |
| `eu` | api.eu.toothfairyai.com | Europe |
| `us` | api.us.toothfairyai.com | United States |
## Dependencies
- `toothfairyai` - Official ToothFairyAI Python SDK
- `pydantic` - Data validation
- `httpx` - HTTP client
- `rich` - Console output
## License
MIT

View File

@ -9,7 +9,7 @@ description = "ToothFairyAI workspace sync layer for tfcode"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"toothfairyai>=1.0.0",
"toothfairyai>=0.5.0",
"pydantic>=2.0.0",
"httpx>=0.25.0",
"rich>=13.0.0",

View File

@ -13,6 +13,7 @@ from toothfairyai.errors import ToothFairyError
class Region(str, Enum):
DEV = "dev"
AU = "au"
EU = "eu"
US = "us"
@ -38,12 +39,19 @@ class FunctionRequestType(str, Enum):
# Region-specific URL configurations
REGION_URLS = {
Region.DEV: {
"base_url": "https://api.toothfairylab.link",
"ai_url": "https://ai.toothfairylab.link",
"ai_stream_url": "https://ais.toothfairylab.link",
"mcp_url": "https://mcp.toothfairylab.link/sse",
"mcp_proxy_url": "https://mcp-proxy.toothfairylab.link",
},
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",
"base_url": "https://api.toothfairyai.com",
"ai_url": "https://ai.toothfairyai.com",
"ai_stream_url": "https://ais.toothfairyai.com",
"mcp_url": "https://mcp.toothfairyai.com/sse",
"mcp_proxy_url": "https://mcp-proxy.toothfairyai.com",
},
Region.EU: {
"base_url": "https://api.eu.toothfairyai.com",

View File

@ -1,12 +1,17 @@
"""
MCP server sync module for tfcode.
Uses the official ToothFairyAI Python SDK.
NOTE: MCP servers are not currently exposed via the ToothFairyAI SDK.
This module is reserved for future implementation when MCP server
discovery is added to the SDK.
For now, MCP servers should be configured manually via tfcode.json.
"""
from pydantic import BaseModel
from tf_sync.config import TFConfig
from tf_sync.tools import SyncedTool, sync_tools, ToolType
from tf_sync.tools import SyncedTool, ToolType
class MCPServerSyncResult(BaseModel):
@ -21,34 +26,16 @@ 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.
NOTE: Currently not supported. MCP servers are not exposed via the SDK.
Configure MCP servers manually in tfcode.json instead.
Args:
config: TFConfig instance
Returns:
MCPServerSyncResult with synced MCP servers
MCPServerSyncResult with error message
"""
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
success=False,
error="MCP server sync not available via SDK. Configure MCP servers in tfcode.json.",
)

View File

@ -1,7 +1,11 @@
"""
Tool sync module for tfcode.
Syncs MCP servers, Agent Skills, Database Scripts, and API Functions from ToothFairyAI workspace.
Uses the official ToothFairyAI Python SDK.
Syncs tools from ToothFairyAI workspace using the official SDK.
SDK Structure:
- agent_functions: API Functions (with request_type)
- connections: Provider connections (openai, anthropic, etc.)
- agents: TF workspace agents
"""
from typing import Any, Optional
@ -20,13 +24,10 @@ class SyncedTool(BaseModel):
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] = []
authorisation_type: Optional[str] = None
auth_via: str = "tf_proxy"
@ -40,60 +41,57 @@ class ToolSyncResult(BaseModel):
error: Optional[str] = None
def classify_tool(tool: AgentFunction) -> ToolType:
def classify_tool(func: AgentFunction) -> ToolType:
"""
Classify a tool based on its flags and fields.
Classify a tool based on its properties.
Currently the SDK exposes:
- agent_functions: API functions with request_type
Args:
tool: AgentFunction from TF SDK
func: 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:
# All agent_functions with request_type are API Functions
if func.request_type:
return ToolType.API_FUNCTION
return ToolType.API_FUNCTION
def parse_tool(tool: AgentFunction) -> SyncedTool:
def parse_function(func: AgentFunction) -> SyncedTool:
"""
Parse AgentFunction from SDK into SyncedTool.
Args:
tool: AgentFunction from TF SDK
func: AgentFunction from TF SDK
Returns:
SyncedTool instance
"""
tool_type = classify_tool(tool)
tool_type = classify_tool(func)
request_type_enum = None
if tool.request_type:
if func.request_type:
try:
request_type_enum = FunctionRequestType(tool.request_type)
request_type_enum = FunctionRequestType(func.request_type)
except ValueError:
pass
auth_via = "user_provided" if tool_type == ToolType.API_FUNCTION else "tf_proxy"
# API Functions may have user-provided auth (authorisation_type)
# or may use TF proxy
auth_via = "user_provided" if func.authorisation_type == "api_key" else "tf_proxy"
return SyncedTool(
id=tool.id,
name=tool.name,
description=tool.description,
id=func.id,
name=func.name,
description=func.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=[],
url=func.url,
authorisation_type=func.authorisation_type,
auth_via=auth_via,
)
@ -112,7 +110,7 @@ def sync_tools(config: TFConfig) -> ToolSyncResult:
client = config.get_client()
result = client.agent_functions.list()
tools = [parse_tool(f) for f in result.items]
tools = [parse_function(f) for f in result.items]
by_type = {}
for tool in tools:
@ -165,21 +163,6 @@ def sync_tools_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])

View File

@ -7,6 +7,7 @@
"private": true,
"scripts": {
"prepare": "effect-language-service patch || true",
"postinstall": "node scripts/postinstall.cjs",
"typecheck": "tsgo --noEmit",
"test": "bun test --timeout 30000",
"build": "bun run script/build.ts",

View File

@ -0,0 +1,171 @@
#!/usr/bin/env node
const { spawn, execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const RED = '\x1b[31m';
const CYAN = '\x1b[36m';
const DIM = '\x1b[90m';
function log(msg) {
console.log(msg);
}
function logSuccess(msg) {
console.log(`${GREEN}${RESET} ${msg}`);
}
function logError(msg) {
console.error(`${RED}${RESET} ${msg}`);
}
function logInfo(msg) {
console.log(`${CYAN}${RESET} ${msg}`);
}
function logWarning(msg) {
console.log(`${YELLOW}!${RESET} ${msg}`);
}
function checkPython() {
const commands = ['python3', 'python'];
for (const cmd of commands) {
try {
const result = execSync(`${cmd} --version`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
const match = result.match(/Python (\d+)\.(\d+)/);
if (match) {
const major = parseInt(match[1]);
const minor = parseInt(match[2]);
if (major >= 3 && minor >= 10) {
return { cmd, version: result.trim() };
}
}
} catch {}
}
return null;
}
function installPythonDeps(pythonCmd) {
return new Promise((resolve, reject) => {
const packages = ['toothfairyai', 'pydantic', 'httpx', 'rich'];
log(`${DIM}Installing Python packages: ${packages.join(', ')}...${RESET}`);
// Try with --user first, then --break-system-packages if needed
const args = ['-m', 'pip', 'install', '--user', ...packages];
const proc = spawn(pythonCmd, args, {
stdio: 'inherit',
shell: process.platform === 'win32'
});
proc.on('close', (code) => {
if (code === 0) {
resolve();
} else {
// Try with --break-system-packages flag
log(`${DIM}Retrying with --break-system-packages...${RESET}`);
const retryArgs = ['-m', 'pip', 'install', '--user', '--break-system-packages', ...packages];
const retry = spawn(pythonCmd, retryArgs, {
stdio: 'inherit',
shell: process.platform === 'win32'
});
retry.on('close', (retryCode) => {
if (retryCode === 0) {
resolve();
} else {
reject(new Error(`pip install exited with code ${retryCode}`));
}
});
retry.on('error', (err) => {
reject(err);
});
}
});
proc.on('error', (err) => {
reject(err);
});
});
}
async function main() {
log('');
log(`${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
log(`${BOLD} tfcode - ToothFairyAI's official coding agent${RESET}`);
log(`${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
log('');
// Check for Python
logInfo('Checking Python installation...');
const python = checkPython();
if (!python) {
log('');
logError('Python 3.10+ is required but not found.');
log('');
log(`${BOLD}Please install Python 3.10 or later:${RESET}`);
log('');
log(` ${CYAN}macOS:${RESET} brew install python@3.12`);
log(` ${CYAN}Ubuntu:${RESET} sudo apt-get install python3.12`);
log(` ${CYAN}Windows:${RESET} Download from https://python.org/downloads`);
log('');
log(`${DIM}After installing Python, run: npm rebuild tfcode${RESET}`);
log('');
process.exit(1);
}
logSuccess(`Found ${python.version} (${python.cmd})`);
log('');
// Install Python dependencies
logInfo('Installing ToothFairyAI Python SDK...');
try {
await installPythonDeps(python.cmd);
logSuccess('Python dependencies installed');
} catch (err) {
logWarning(`Failed to install Python dependencies: ${err.message}`);
log('');
log(`${DIM}You can install them manually with:${RESET}`);
log(` ${CYAN}${python.cmd} -m pip install toothfairyai pydantic httpx rich${RESET}`);
log('');
// Don't exit - user might install manually later
}
log('');
log(`${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
log(`${GREEN}✓ tfcode installed successfully!${RESET}`);
log(`${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`);
log('');
log(`${BOLD}Quick Start:${RESET}`);
log('');
log(` ${CYAN}1.${RESET} Set your ToothFairyAI credentials:`);
log(` ${DIM}export TF_WORKSPACE_ID="your-workspace-id"${RESET}`);
log(` ${DIM}export TF_API_KEY="your-api-key"${RESET}`);
log('');
log(` ${CYAN}2.${RESET} Validate your credentials:`);
log(` ${DIM}tfcode validate${RESET}`);
log('');
log(` ${CYAN}3.${RESET} Sync tools from your workspace:`);
log(` ${DIM}tfcode sync${RESET}`);
log('');
log(` ${CYAN}4.${RESET} Start coding:`);
log(` ${DIM}tfcode${RESET}`);
log('');
log(`${DIM}Documentation: https://toothfairyai.com/developers/tfcode${RESET}`);
log('');
}
main().catch((err) => {
logError(`Installation failed: ${err.message}`);
process.exit(1);
});

View File

@ -0,0 +1,544 @@
import { cmd } from "@/cli/cmd/cmd"
import { UI } from "@/cli/ui"
import { Log } from "@/util/log"
import { spawn } from "child_process"
import { Filesystem } from "@/util/filesystem"
import { mkdir } from "fs/promises"
import { existsSync } from "fs"
import path from "path"
import { Global } from "@/global"
const println = (msg: string) => UI.println(msg)
const printError = (msg: string) => UI.error(msg)
const success = (msg: string) => UI.println(UI.Style.TEXT_SUCCESS_BOLD + msg + UI.Style.TEXT_NORMAL)
const info = (msg: string) => UI.println(UI.Style.TEXT_NORMAL + msg)
type ToolType = "mcp_server" | "agent_skill" | "database_script" | "api_function"
interface SyncedTool {
id: string
name: string
description?: string
tool_type: ToolType
is_mcp_server: boolean
is_agent_skill: boolean
is_database_script: boolean
request_type?: string
url?: string
tools: string[]
auth_via: string
}
interface ToolSyncResult {
success: boolean
tools: SyncedTool[]
by_type: Record<string, number>
error?: string
}
interface CredentialValidationResult {
success: boolean
workspace_id?: string
workspace_name?: string
error?: string
}
const TFCODE_CONFIG_DIR = ".tfcode"
const TFCODE_TOOLS_FILE = "tools.json"
function getPythonSyncPath(): string {
const possible = [
path.join(__dirname, "..", "..", "..", "..", "tf-sync", "src", "tf_sync"),
path.join(process.cwd(), "packages", "tf-sync", "src", "tf_sync"),
]
for (const p of possible) {
if (existsSync(p)) return p
}
return "tf_sync"
}
async function runPythonSync(
method: string,
args: Record<string, unknown> = {},
): Promise<unknown> {
const pythonCode = `
import json
import sys
import os
try:
from tf_sync.config import load_config, validate_credentials
from tf_sync.tools import sync_tools, sync_tools_by_type, ToolType
from tf_sync.mcp import sync_mcp_servers
method = ${JSON.stringify(method)}
args = ${JSON.stringify(args)}
if method == "validate":
config = load_config()
result = validate_credentials(config)
print(json.dumps({
"success": result.success,
"workspace_id": result.workspace_id,
"workspace_name": result.workspace_name,
"error": result.error
}))
elif method == "sync":
config = load_config()
result = sync_tools(config)
tools_data = []
for tool in result.tools:
tools_data.append({
"id": tool.id,
"name": tool.name,
"description": tool.description,
"tool_type": tool.tool_type.value,
"is_mcp_server": tool.is_mcp_server,
"is_agent_skill": tool.is_agent_skill,
"is_database_script": tool.is_database_script,
"request_type": tool.request_type.value if tool.request_type else None,
"url": tool.url,
"tools": tool.tools,
"auth_via": tool.auth_via
})
print(json.dumps({
"success": result.success,
"tools": tools_data,
"by_type": result.by_type,
"error": result.error
}))
elif method == "sync_type":
tool_type_str = args.get("tool_type")
if tool_type_str:
tool_type_map = {
"mcp_server": ToolType.MCP_SERVER,
"agent_skill": ToolType.AGENT_SKILL,
"database_script": ToolType.DATABASE_SCRIPT,
"api_function": ToolType.API_FUNCTION
}
tool_type = tool_type_map.get(tool_type_str)
if tool_type:
config = load_config()
result = sync_tools_by_type(config, [tool_type])
tools_data = []
for tool in result.tools:
tools_data.append({
"id": tool.id,
"name": tool.name,
"description": tool.description,
"tool_type": tool.tool_type.value,
"is_mcp_server": tool.is_mcp_server,
"is_agent_skill": tool.is_agent_skill,
"is_database_script": tool.is_database_script,
"request_type": tool.request_type.value if tool.request_type else None,
"url": tool.url,
"tools": tool.tools,
"auth_via": tool.auth_via
})
print(json.dumps({
"success": result.success,
"tools": tools_data,
"by_type": result.by_type,
"error": result.error
}))
else:
print(json.dumps({"success": False, "error": "Missing tool_type argument"}))
except Exception as e:
print(json.dumps({"success": False, "error": str(e)}))
sys.exit(0)
`
return new Promise((resolve, reject) => {
const pythonPath = process.env.TFCODE_PYTHON_PATH || "python3"
const proc = spawn(pythonPath, ["-c", pythonCode], {
env: {
...process.env,
PYTHONPATH: getPythonSyncPath(),
},
})
let stdout = ""
let stderr = ""
proc.stdout.on("data", (data) => {
stdout += data.toString()
})
proc.stderr.on("data", (data) => {
stderr += data.toString()
})
proc.on("close", (code) => {
if (code !== 0 && !stdout) {
reject(new Error(`Python sync failed: ${stderr}`))
return
}
try {
const result = JSON.parse(stdout.trim())
resolve(result)
} catch (e) {
reject(new Error(`Failed to parse Python output: ${stdout}\nstderr: ${stderr}`))
}
})
proc.on("error", (err) => {
reject(err)
})
})
}
function getConfigPath(): string {
return path.join(Global.Path.data, TFCODE_CONFIG_DIR)
}
function getToolsFilePath(): string {
return path.join(getConfigPath(), TFCODE_TOOLS_FILE)
}
async function loadCachedTools(): Promise<ToolSyncResult | null> {
const toolsFile = getToolsFilePath()
if (!(await Filesystem.exists(toolsFile))) {
return null
}
try {
const content = await Bun.file(toolsFile).text()
return JSON.parse(content)
} catch {
return null
}
}
async function saveToolsCache(result: ToolSyncResult): Promise<void> {
const configPath = getConfigPath()
await mkdir(configPath, { recursive: true })
await Bun.write(getToolsFilePath(), JSON.stringify(result, null, 2))
}
const ValidateCommand = cmd({
command: "validate",
describe: "validate ToothFairyAI credentials",
handler: async () => {
info("Validating ToothFairyAI credentials...")
try {
const result = (await runPythonSync("validate")) as CredentialValidationResult
if (result.success) {
success("✓ Credentials valid")
if (result.workspace_name) {
info(` Workspace: ${result.workspace_name}`)
}
if (result.workspace_id) {
info(` ID: ${result.workspace_id}`)
}
} else {
printError(`✗ Validation failed: ${result.error || "Unknown error"}`)
process.exitCode = 1
}
} catch (e) {
printError(`Failed to validate: ${e instanceof Error ? e.message : String(e)}`)
process.exitCode = 1
}
},
})
const SyncCommand = cmd({
command: "sync",
describe: "sync tools from ToothFairyAI workspace",
builder: (yargs) =>
yargs.option("force", {
alias: "f",
type: "boolean",
describe: "force re-sync",
default: false,
}),
handler: async (args) => {
info("Syncing tools from ToothFairyAI workspace...")
try {
const result = (await runPythonSync("sync")) as ToolSyncResult
if (result.success) {
await saveToolsCache(result)
success(`✓ Synced ${result.tools.length} tools`)
if (result.by_type && Object.keys(result.by_type).length > 0) {
info("\nBy type:")
for (const [type, count] of Object.entries(result.by_type)) {
info(` ${type}: ${count}`)
}
}
} else {
printError(`✗ Sync failed: ${result.error || "Unknown error"}`)
process.exitCode = 1
}
} catch (e) {
printError(`Failed to sync: ${e instanceof Error ? e.message : String(e)}`)
process.exitCode = 1
}
},
})
const ToolsListCommand = cmd({
command: "list",
describe: "list synced tools",
builder: (yargs) =>
yargs.option("type", {
type: "string",
choices: ["mcp", "skill", "database", "function"] as const,
describe: "filter by tool type",
}),
handler: async (args) => {
const cached = await loadCachedTools()
if (!cached || !cached.success) {
printError("No tools synced. Run 'tfcode sync' first.")
process.exitCode = 1
return
}
let tools = cached.tools
if (args.type) {
const typeMap: Record<string, ToolType> = {
mcp: "mcp_server",
skill: "agent_skill",
database: "database_script",
function: "api_function",
}
const targetType = typeMap[args.type]
tools = tools.filter((t) => t.tool_type === targetType)
}
if (tools.length === 0) {
info("No tools found.")
return
}
info(`\n${tools.length} tool(s):\n`)
for (const tool of tools) {
const typeLabel = {
mcp_server: "MCP",
agent_skill: "Skill",
database_script: "DB",
api_function: "API",
}[tool.tool_type]
info(` ${tool.name}`)
info(` Type: ${typeLabel}`)
if (tool.description) {
info(` Description: ${tool.description}`)
}
if (tool.url) {
info(` URL: ${tool.url}`)
}
info(` Auth: ${tool.auth_via}`)
info("")
}
},
})
const ToolsCredentialsSetCommand = cmd({
command: "credentials <name>",
describe: "manage tool credentials",
builder: (yargs) =>
yargs
.positional("name", {
type: "string",
describe: "tool name",
demandOption: true,
})
.option("set", {
type: "boolean",
describe: "set credential",
})
.option("show", {
type: "boolean",
describe: "show stored credential",
}),
handler: async (args) => {
const toolName = args.name as string
const cached = await loadCachedTools()
if (!cached || !cached.success) {
printError("No tools synced. Run 'tfcode sync' first.")
process.exitCode = 1
return
}
const tool = cached.tools.find((t) => t.name === toolName)
if (!tool) {
printError(`Tool '${toolName}' not found.`)
process.exitCode = 1
return
}
if (tool.auth_via !== "user_provided") {
printError(`Tool '${toolName}' uses tf_proxy authentication. Credentials are managed by ToothFairyAI.`)
process.exitCode = 1
return
}
const credentialsFile = path.join(getConfigPath(), "credentials.json")
let credentials: Record<string, string> = {}
if (await Filesystem.exists(credentialsFile)) {
try {
credentials = await Bun.file(credentialsFile).json()
} catch {}
}
if (args.show) {
const cred = credentials[toolName]
if (cred) {
info(`${toolName}: ${cred.substring(0, 8)}...${cred.substring(cred.length - 4)}`)
} else {
info(`No credential stored for '${toolName}'`)
}
return
}
if (args.set) {
const { default: prompts } = await import("@clack/prompts")
const value = await prompts.password({
message: `Enter API key for '${toolName}'`,
})
if (prompts.isCancel(value)) {
printError("Cancelled")
process.exitCode = 1
return
}
credentials[toolName] = value as string
await mkdir(getConfigPath(), { recursive: true })
await Bun.write(credentialsFile, JSON.stringify(credentials, null, 2))
success(`✓ Credential saved for '${toolName}'`)
return
}
printError("Use --set or --show")
process.exitCode = 1
},
})
const ToolsCommand = cmd({
command: "tools",
describe: "manage ToothFairyAI tools",
builder: (yargs) =>
yargs.command(ToolsListCommand).command(ToolsCredentialsSetCommand).demandCommand(),
})
const ToolsDebugCommand = cmd({
command: "debug <name>",
describe: "debug tool connection",
builder: (yargs) =>
yargs.positional("name", {
type: "string",
describe: "tool name",
demandOption: true,
}),
handler: async (args) => {
const toolName = args.name as string
const cached = await loadCachedTools()
if (!cached || !cached.success) {
printError("No tools synced. Run 'tfcode sync' first.")
process.exitCode = 1
return
}
const tool = cached.tools.find((t) => t.name === toolName)
if (!tool) {
printError(`Tool '${toolName}' not found.`)
process.exitCode = 1
return
}
info(`\nTool: ${tool.name}`)
info(`Type: ${tool.tool_type}`)
info(`Auth: ${tool.auth_via}`)
if (tool.url) {
info(`URL: ${tool.url}`)
}
if (tool.request_type) {
info(`Request Type: ${tool.request_type}`)
}
info("\nChecking configuration...")
const configPath = getConfigPath()
info(`Config dir: ${configPath}`)
const toolsFile = getToolsFilePath()
info(`Tools cache: ${toolsFile}`)
info(` Exists: ${await Filesystem.exists(toolsFile)}`)
if (tool.auth_via === "user_provided") {
const credentialsFile = path.join(configPath, "credentials.json")
info(`Credentials file: ${credentialsFile}`)
info(` Exists: ${await Filesystem.exists(credentialsFile)}`)
if (await Filesystem.exists(credentialsFile)) {
const credentials = await Bun.file(credentialsFile).json()
info(` Has credential for '${toolName}': ${!!credentials[toolName]}`)
}
}
},
})
const ToolsTestCommand = cmd({
command: "test <name>",
describe: "test tool call",
builder: (yargs) =>
yargs.positional("name", {
type: "string",
describe: "tool name",
demandOption: true,
}),
handler: async (args) => {
const toolName = args.name as string
const cached = await loadCachedTools()
if (!cached || !cached.success) {
printError("No tools synced. Run 'tfcode sync' first.")
process.exitCode = 1
return
}
const tool = cached.tools.find((t) => t.name === toolName)
if (!tool) {
printError(`Tool '${toolName}' not found.`)
process.exitCode = 1
return
}
info(`Testing tool '${toolName}'...`)
info(`This feature is not yet implemented.`)
info(`Tool type: ${tool.tool_type}`)
info(`Authentication: ${tool.auth_via}`)
process.exitCode = 1
},
})
export const ToolsMainCommand = cmd({
command: "tools",
describe: "manage ToothFairyAI tools",
builder: (yargs) =>
yargs.command(ToolsListCommand).command(ToolsCredentialsSetCommand).command(ToolsDebugCommand).command(ToolsTestCommand).demandCommand(),
})
export { ValidateCommand, SyncCommand, ToolsCommand }

View File

@ -30,6 +30,7 @@ import { WebCommand } from "./cli/cmd/web"
import { PrCommand } from "./cli/cmd/pr"
import { SessionCommand } from "./cli/cmd/session"
import { DbCommand } from "./cli/cmd/db"
import { ValidateCommand, SyncCommand, ToolsMainCommand } from "./cli/cmd/tools"
import path from "path"
import { Global } from "./global"
import { JsonMigration } from "./storage/json-migration"
@ -49,7 +50,7 @@ process.on("uncaughtException", (e) => {
let cli = yargs(hideBin(process.argv))
.parserConfiguration({ "populate--": true })
.scriptName("opencode")
.scriptName("tfcode")
.wrap(100)
.help("help", "show help")
.alias("help", "h")
@ -145,6 +146,9 @@ let cli = yargs(hideBin(process.argv))
.command(PrCommand)
.command(SessionCommand)
.command(DbCommand)
.command(ValidateCommand)
.command(SyncCommand)
.command(ToolsMainCommand)
if (Installation.isLocal()) {
cli = cli.command(WorkspaceServeCommand)

15
scripts/setup-tf-dev.sh Normal file
View File

@ -0,0 +1,15 @@
#!/bin/bash
# ToothFairyAI dev environment setup for tfcode
export TF_WORKSPACE_ID="6586b7e6-683e-4ee6-a6cf-24c19729b5ff"
export TF_API_KEY="EWZooLROIS57EVW3BKGu7Pv6LNe4D6m4gkDjukx3"
export TF_REGION="dev"
echo "ToothFairyAI environment configured:"
echo " Workspace: $TF_WORKSPACE_ID"
echo " Region: $TF_REGION"
echo ""
echo "Run tfcode commands:"
echo " tfcode validate - Test credentials"
echo " tfcode sync - Sync tools from workspace"
echo " tfcode tools list - List synced tools"

136
scripts/test-sync.py Normal file
View File

@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""
Quick test script for tfcode sync functionality.
Tests credential validation and tool sync directly.
"""
import os
import sys
# Set environment for testing
os.environ["TF_WORKSPACE_ID"] = "6586b7e6-683e-4ee6-a6cf-24c19729b5ff"
os.environ["TF_API_KEY"] = "EWZooLROIS57EVW3BKGu7Pv6LNe4D6m4gkDjukx3"
os.environ["TF_REGION"] = "dev"
def main():
print("=" * 60)
print("tfcode Sync Test")
print("=" * 60)
print()
# Test 1: Load config
print("Test 1: Load Configuration")
print("-" * 40)
try:
from tf_sync.config import load_config, get_region_urls, Region
config = load_config()
print(f"✓ Workspace ID: {config.workspace_id}")
print(f"✓ Region: {config.region.value}")
urls = get_region_urls(config.region)
print(f"✓ API URL: {urls['base_url']}")
print(f"✓ MCP Proxy URL: {urls['mcp_proxy_url']}")
print()
except Exception as e:
print(f"✗ Failed: {e}")
print()
sys.exit(1)
# Test 2: Validate credentials
print("Test 2: Validate Credentials")
print("-" * 40)
try:
from tf_sync.config import validate_credentials
result = validate_credentials(config)
if result.success:
print("✓ Credentials valid")
if result.workspace_name:
print(f" Workspace: {result.workspace_name}")
if result.workspace_id:
print(f" ID: {result.workspace_id}")
else:
print(f"✗ Validation failed: {result.error}")
sys.exit(1)
print()
except Exception as e:
print(f"✗ Failed: {e}")
import traceback
traceback.print_exc()
print()
sys.exit(1)
# Test 3: Sync tools
print("Test 3: Sync Tools")
print("-" * 40)
try:
from tf_sync.tools import sync_tools, ToolType
result = sync_tools(config)
if result.success:
print(f"✓ Synced {len(result.tools)} tools")
print()
if result.by_type:
print("By type:")
for tool_type, count in result.by_type.items():
print(f" {tool_type}: {count}")
print()
# Show first 5 tools
if result.tools:
print("Sample tools:")
for tool in result.tools[:5]:
type_emoji = {
ToolType.MCP_SERVER: "🔌",
ToolType.AGENT_SKILL: "🤖",
ToolType.DATABASE_SCRIPT: "🗄️",
ToolType.API_FUNCTION: "🌐",
}.get(tool.tool_type, "📦")
print(f" {type_emoji} {tool.name} ({tool.tool_type.value})")
if tool.description:
print(f" {tool.description[:60]}...")
print(f" Auth: {tool.auth_via}")
if len(result.tools) > 5:
print(f" ... and {len(result.tools) - 5} more")
else:
print(f"✗ Sync failed: {result.error}")
sys.exit(1)
print()
except Exception as e:
print(f"✗ Failed: {e}")
import traceback
traceback.print_exc()
print()
sys.exit(1)
# Test 4: Sync by type
print("Test 4: Sync MCP Servers Only")
print("-" * 40)
try:
from tf_sync.tools import sync_tools_by_type
result = sync_tools_by_type(config, [ToolType.MCP_SERVER])
if result.success:
print(f"✓ Found {len(result.tools)} MCP servers")
for tool in result.tools:
print(f" - {tool.name}")
else:
print(f"✗ Failed: {result.error}")
print()
except Exception as e:
print(f"✗ Failed: {e}")
print()
print("=" * 60)
print("All tests completed!")
print("=" * 60)
if __name__ == "__main__":
main()

60
scripts/test-sync.sh Normal file
View File

@ -0,0 +1,60 @@
#!/bin/bash
# End-to-end test for tfcode sync
set -e
echo "========================================"
echo "tfcode Sync End-to-End Test"
echo "========================================"
echo
# Step 1: Check Python
echo "Step 1: Checking Python environment..."
if ! command -v python3 &> /dev/null; then
echo "✗ Python 3 not found"
exit 1
fi
echo "✓ Python found: $(python3 --version)"
echo
# Step 2: Install dependencies
echo "Step 2: Installing Python dependencies..."
cd packages/tf-sync
pip install -e . -q 2>/dev/null || pip install toothfairyai pydantic httpx rich -q
echo "✓ Dependencies installed"
cd ../..
echo
# Step 3: Run Python test
echo "Step 3: Running Python sync test..."
python3 scripts/test-sync.py
echo
# Step 4: Test CLI (if bun available)
if command -v bun &> /dev/null; then
echo "Step 4: Testing CLI commands..."
echo
cd packages/tfcode
echo "4a. Testing validate command..."
bun run src/index.ts validate
echo
echo "4b. Testing sync command..."
bun run src/index.ts sync
echo
echo "4c. Testing tools list command..."
bun run src/index.ts tools list
echo
cd ../..
else
echo "Step 4: Skipping CLI test (bun not available)"
fi
echo
echo "========================================"
echo "All tests passed!"
echo "========================================"