diff --git a/README.md b/README.md index 8d32a939d..3f5851cd8 100644 --- a/README.md +++ b/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 # 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 --set/--show` + - [x] `tfcode tools debug ` +- [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 # 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 --- diff --git a/docs/sync-implementation.md b/docs/sync-implementation.md new file mode 100644 index 000000000..8d18cc300 --- /dev/null +++ b/docs/sync-implementation.md @@ -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 --set/--show` - Manage API keys +- `tfcode tools debug ` - 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 \ No newline at end of file diff --git a/docs/testing-sync.md b/docs/testing-sync.md new file mode 100644 index 000000000..609959e80 --- /dev/null +++ b/docs/testing-sync.md @@ -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: + 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 +``` + +## Step 8: Set API Function Credentials (if needed) + +For tools with `auth_via: user_provided`: + +```bash +bun run src/index.ts tools credentials --set +# Enter API key when prompted + +bun run src/index.ts tools credentials --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 \ No newline at end of file diff --git a/packages/tf-sync/README.md b/packages/tf-sync/README.md new file mode 100644 index 000000000..49b719219 --- /dev/null +++ b/packages/tf-sync/README.md @@ -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 \ No newline at end of file diff --git a/packages/tf-sync/pyproject.toml b/packages/tf-sync/pyproject.toml index f4cbe356c..2eec81b1d 100644 --- a/packages/tf-sync/pyproject.toml +++ b/packages/tf-sync/pyproject.toml @@ -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", diff --git a/packages/tf-sync/src/tf_sync/__pycache__/__init__.cpython-313.pyc b/packages/tf-sync/src/tf_sync/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 000000000..420116a7b Binary files /dev/null and b/packages/tf-sync/src/tf_sync/__pycache__/__init__.cpython-313.pyc differ diff --git a/packages/tf-sync/src/tf_sync/__pycache__/agents.cpython-313.pyc b/packages/tf-sync/src/tf_sync/__pycache__/agents.cpython-313.pyc new file mode 100644 index 000000000..5e39ba86a Binary files /dev/null and b/packages/tf-sync/src/tf_sync/__pycache__/agents.cpython-313.pyc differ diff --git a/packages/tf-sync/src/tf_sync/__pycache__/config.cpython-313.pyc b/packages/tf-sync/src/tf_sync/__pycache__/config.cpython-313.pyc new file mode 100644 index 000000000..e1a47a6da Binary files /dev/null and b/packages/tf-sync/src/tf_sync/__pycache__/config.cpython-313.pyc differ diff --git a/packages/tf-sync/src/tf_sync/__pycache__/mcp.cpython-313.pyc b/packages/tf-sync/src/tf_sync/__pycache__/mcp.cpython-313.pyc new file mode 100644 index 000000000..3f5410e27 Binary files /dev/null and b/packages/tf-sync/src/tf_sync/__pycache__/mcp.cpython-313.pyc differ diff --git a/packages/tf-sync/src/tf_sync/__pycache__/tools.cpython-313.pyc b/packages/tf-sync/src/tf_sync/__pycache__/tools.cpython-313.pyc new file mode 100644 index 000000000..3f0405583 Binary files /dev/null and b/packages/tf-sync/src/tf_sync/__pycache__/tools.cpython-313.pyc differ diff --git a/packages/tf-sync/src/tf_sync/config.py b/packages/tf-sync/src/tf_sync/config.py index d6abac1d4..9cb7bd3ef 100644 --- a/packages/tf-sync/src/tf_sync/config.py +++ b/packages/tf-sync/src/tf_sync/config.py @@ -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", diff --git a/packages/tf-sync/src/tf_sync/mcp.py b/packages/tf-sync/src/tf_sync/mcp.py index 355f46b0e..564d02d37 100644 --- a/packages/tf-sync/src/tf_sync/mcp.py +++ b/packages/tf-sync/src/tf_sync/mcp.py @@ -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 \ No newline at end of file + success=False, + error="MCP server sync not available via SDK. Configure MCP servers in tfcode.json.", + ) \ No newline at end of file diff --git a/packages/tf-sync/src/tf_sync/tools.py b/packages/tf-sync/src/tf_sync/tools.py index 2c07cf6f3..9bc92678d 100644 --- a/packages/tf-sync/src/tf_sync/tools.py +++ b/packages/tf-sync/src/tf_sync/tools.py @@ -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]) \ No newline at end of file diff --git a/packages/tfcode/package.json b/packages/tfcode/package.json index 3370f28e8..daf18bdc4 100644 --- a/packages/tfcode/package.json +++ b/packages/tfcode/package.json @@ -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", diff --git a/packages/tfcode/scripts/postinstall.cjs b/packages/tfcode/scripts/postinstall.cjs new file mode 100644 index 000000000..b2c49bff3 --- /dev/null +++ b/packages/tfcode/scripts/postinstall.cjs @@ -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); +}); \ No newline at end of file diff --git a/packages/tfcode/src/cli/cmd/tools.ts b/packages/tfcode/src/cli/cmd/tools.ts new file mode 100644 index 000000000..612a78864 --- /dev/null +++ b/packages/tfcode/src/cli/cmd/tools.ts @@ -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 + 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 = {}, +): Promise { + 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 { + 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 { + 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 = { + 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 ", + 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 = {} + + 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 ", + 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 ", + 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 } \ No newline at end of file diff --git a/packages/tfcode/src/index.ts b/packages/tfcode/src/index.ts index b3d1db7eb..10a09f827 100644 --- a/packages/tfcode/src/index.ts +++ b/packages/tfcode/src/index.ts @@ -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) diff --git a/scripts/setup-tf-dev.sh b/scripts/setup-tf-dev.sh new file mode 100644 index 000000000..45a1bac18 --- /dev/null +++ b/scripts/setup-tf-dev.sh @@ -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" \ No newline at end of file diff --git a/scripts/test-sync.py b/scripts/test-sync.py new file mode 100644 index 000000000..075e4c27b --- /dev/null +++ b/scripts/test-sync.py @@ -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() \ No newline at end of file diff --git a/scripts/test-sync.sh b/scripts/test-sync.sh new file mode 100644 index 000000000..788e4157d --- /dev/null +++ b/scripts/test-sync.sh @@ -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 "========================================" \ No newline at end of file