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

@@ -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)