mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-09 18:29:39 +00:00
feat: sync
This commit is contained in:
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)
|
||||
|
||||
Reference in New Issue
Block a user