#!/usr/bin/env node import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { spawn } from 'child_process'; import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; const TFCODE_DIR = join(homedir(), '.tfcode'); const TOOLS_FILE = join(TFCODE_DIR, 'tools.json'); const CREDENTIALS_FILE = join(TFCODE_DIR, 'credentials.json'); const COLORS = { reset: '\x1b[0m', bold: '\x1b[1m', green: '\x1b[32m', red: '\x1b[31m', cyan: '\x1b[36m', dim: '\x1b[90m', yellow: '\x1b[33m' }; function log(msg) { console.log(msg); } function success(msg) { console.log(`${COLORS.green}✓${COLORS.reset} ${msg}`); } function error(msg) { console.error(`${COLORS.red}✗${COLORS.reset} ${msg}`); } function info(msg) { console.log(`${COLORS.cyan}ℹ${COLORS.reset} ${msg}`); } function runPythonSync(method) { const pythonCode = ` import json import sys import os try: from tf_sync.config import load_config, validate_credentials, Region from tf_sync.tools import sync_tools from tf_sync.config import get_region_urls method = "${method}" if method == "validate": config = load_config() result = validate_credentials(config) urls = get_region_urls(config.region) print(json.dumps({ "success": result.success, "workspace_id": result.workspace_id, "workspace_name": result.workspace_name, "error": result.error, "base_url": urls["base_url"] })) 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, "request_type": tool.request_type.value if tool.request_type else None, "url": tool.url, "auth_via": tool.auth_via }) print(json.dumps({ "success": result.success, "tools": tools_data, "by_type": result.by_type, "error": result.error })) 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: process.env.TFCODE_PYTHONPATH || '' } }); 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 ensureConfigDir() { if (!existsSync(TFCODE_DIR)) { mkdirSync(TFCODE_DIR, { recursive: true }); } } function loadCachedTools() { if (!existsSync(TOOLS_FILE)) { return null; } try { return JSON.parse(readFileSync(TOOLS_FILE, 'utf-8')); } catch { return null; } } function saveToolsCache(tools) { ensureConfigDir(); writeFileSync(TOOLS_FILE, JSON.stringify(tools, null, 2)); } const cli = yargs(hideBin(process.argv)) .scriptName('tfcode') .wrap(100) .help() .alias('help', 'h') .command({ command: 'quickstart', describe: 'show quick start guide', handler: () => { log(''); log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`); log(`${COLORS.bold} tfcode - Quick Start Guide${COLORS.reset}`); log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`); log(''); log('Welcome to tfcode! Follow these steps to get started:'); log(''); log(`${COLORS.cyan}STEP 1: Set Your Credentials${COLORS.reset}`); log(`${COLORS.dim} export TF_WORKSPACE_ID="your-workspace-id"${COLORS.reset}`); log(`${COLORS.dim} export TF_API_KEY="your-api-key"${COLORS.reset}`); log(`${COLORS.dim} export TF_REGION="au"${COLORS.reset}`); log(`${COLORS.dim} Regions: dev, au, eu, us${COLORS.reset}`); log(''); log(`${COLORS.cyan}STEP 2: Validate Connection${COLORS.reset}`); log(`${COLORS.dim} tfcode validate${COLORS.reset}`); log(''); log(`${COLORS.cyan}STEP 3: Sync Your Tools${COLORS.reset}`); log(`${COLORS.dim} tfcode sync${COLORS.reset}`); log(''); log(`${COLORS.cyan}STEP 4: Start Coding!${COLORS.reset}`); log(`${COLORS.dim} tfcode${COLORS.reset}`); log(''); log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`); log(''); log(' Useful Commands:'); log(''); log(' tfcode validate Test your credentials'); log(' tfcode sync Sync tools from workspace'); log(' tfcode tools list Show all your tools'); log(''); log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`); log(''); log(' Need help? https://toothfairyai.com/developers/tfcode'); log(''); } }) .command({ command: 'validate', describe: 'validate ToothFairyAI credentials', handler: async () => { info('Validating ToothFairyAI credentials...'); log(''); try { const result = await runPythonSync('validate'); if (result.success) { success('Credentials valid'); if (result.base_url) { log(`${COLORS.dim} API URL: ${result.base_url}${COLORS.reset}`); } if (result.workspace_id) { log(`${COLORS.dim} Workspace ID: ${result.workspace_id}${COLORS.reset}`); } } else { error(`Validation failed: ${result.error || 'Unknown error'}`); log(''); log(`${COLORS.dim}Check your credentials:${COLORS.reset}`); log(`${COLORS.dim} TF_WORKSPACE_ID: ${process.env.TF_WORKSPACE_ID || 'not set'}${COLORS.reset}`); log(`${COLORS.dim} TF_API_KEY: ${process.env.TF_API_KEY ? '***' + process.env.TF_API_KEY.slice(-4) : 'not set'}${COLORS.reset}`); log(`${COLORS.dim} TF_REGION: ${process.env.TF_REGION || 'au (default)'}${COLORS.reset}`); process.exit(1); } } catch (e) { error(`Failed to validate: ${e.message}`); log(''); log(`${COLORS.dim}Make sure Python 3.10+ and the ToothFairyAI SDK are installed:${COLORS.reset}`); log(`${COLORS.dim} pip install toothfairyai pydantic httpx rich${COLORS.reset}`); process.exit(1); } } }) .command({ command: 'sync', describe: 'sync tools from ToothFairyAI workspace', handler: async () => { info('Syncing tools from ToothFairyAI workspace...'); log(''); try { const result = await runPythonSync('sync'); if (result.success) { saveToolsCache(result); success(`Synced ${result.tools.length} tools`); log(''); if (result.by_type && Object.keys(result.by_type).length > 0) { log('By type:'); for (const [type, count] of Object.entries(result.by_type)) { log(` ${type}: ${count}`); } log(''); } } else { error(`Sync failed: ${result.error || 'Unknown error'}`); process.exit(1); } } catch (e) { error(`Failed to sync: ${e.message}`); process.exit(1); } } }) .command({ command: 'tools', describe: 'manage tools', builder: (yargs) => { return yargs .command({ command: 'list', describe: 'list synced tools', builder: (yargs) => { return yargs.option('type', { type: 'string', describe: 'filter by type (api_function)' }); }, handler: (args) => { const cached = loadCachedTools(); if (!cached || !cached.success) { error('No tools synced. Run \'tfcode sync\' first.'); process.exit(1); return; } let tools = cached.tools; if (args.type) { tools = tools.filter(t => t.tool_type === args.type); } if (tools.length === 0) { log('No tools found.'); return; } log(''); log(`${tools.length} tool(s):`); log(''); for (const tool of tools) { log(` ${COLORS.cyan}${tool.name}${COLORS.reset}`); log(` Type: ${tool.tool_type}`); if (tool.description) { log(` ${COLORS.dim}${tool.description.slice(0, 60)}${tool.description.length > 60 ? '...' : ''}${COLORS.reset}`); } log(` Auth: ${tool.auth_via}`); log(''); } } }) .demandCommand(); }, handler: () => {} }) .demandCommand() .strict() .fail((msg, err) => { if (msg) { error(msg); } if (err) { error(err.message); } process.exit(1); }); cli.parse();