mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-29 21:33:54 +00:00
feat: sync
This commit is contained in:
parent
39bd38040c
commit
4596310485
73
README.md
73
README.md
@ -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
115
docs/sync-implementation.md
Normal 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
129
docs/testing-sync.md
Normal 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
|
||||
71
packages/tf-sync/README.md
Normal file
71
packages/tf-sync/README.md
Normal 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
|
||||
@ -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",
|
||||
|
||||
Binary file not shown.
BIN
packages/tf-sync/src/tf_sync/__pycache__/agents.cpython-313.pyc
Normal file
BIN
packages/tf-sync/src/tf_sync/__pycache__/agents.cpython-313.pyc
Normal file
Binary file not shown.
BIN
packages/tf-sync/src/tf_sync/__pycache__/config.cpython-313.pyc
Normal file
BIN
packages/tf-sync/src/tf_sync/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
packages/tf-sync/src/tf_sync/__pycache__/mcp.cpython-313.pyc
Normal file
BIN
packages/tf-sync/src/tf_sync/__pycache__/mcp.cpython-313.pyc
Normal file
Binary file not shown.
BIN
packages/tf-sync/src/tf_sync/__pycache__/tools.cpython-313.pyc
Normal file
BIN
packages/tf-sync/src/tf_sync/__pycache__/tools.cpython-313.pyc
Normal file
Binary file not shown.
@ -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",
|
||||
|
||||
@ -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.",
|
||||
)
|
||||
@ -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])
|
||||
@ -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",
|
||||
|
||||
171
packages/tfcode/scripts/postinstall.cjs
Normal file
171
packages/tfcode/scripts/postinstall.cjs
Normal 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);
|
||||
});
|
||||
544
packages/tfcode/src/cli/cmd/tools.ts
Normal file
544
packages/tfcode/src/cli/cmd/tools.ts
Normal 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 }
|
||||
@ -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
15
scripts/setup-tf-dev.sh
Normal 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
136
scripts/test-sync.py
Normal 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
60
scripts/test-sync.sh
Normal 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 "========================================"
|
||||
Loading…
x
Reference in New Issue
Block a user