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" import * as prompts from "@clack/prompts" 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" | "coder_agent" | "prompt" 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 interpolation_string?: string goals?: string temperature?: number max_tokens?: number llm_base_model?: string llm_provider?: string } interface SyncedPrompt { id: string label: string interpolation_string: string prompt_type?: string available_to_agents?: string[] description?: string } interface ToolSyncResult { success: boolean tools: SyncedTool[] prompts: SyncedPrompt[] 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 { // Check embedded python path first (for npm distribution) const embedded = [ path.join(__dirname, "..", "..", "..", "..", "python"), // packages/tfcode/python path.join(__dirname, "..", "..", "..", "python"), // dist/python ] for (const p of embedded) { if (existsSync(p)) return p } // Fallback to development paths const dev = [ path.join(__dirname, "..", "..", "..", "..", "tf-sync", "src", "tf_sync"), path.join(process.cwd(), "packages", "tf-sync", "src", "tf_sync"), ] for (const p of dev) { if (existsSync(p)) return p } return "tf_sync" } async function runPythonSync(method: string, args: Record = {}): Promise { const credentials = await loadCredentials() const pythonPath = getPythonSyncPath() const pythonCode = ` import json import sys import os try: # Add embedded tf_sync module path sys.path.insert(0, "${pythonPath.replace(/\\/g, "/")}") 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, "interpolation_string": tool.interpolation_string, "goals": tool.goals, "temperature": tool.temperature, "max_tokens": tool.max_tokens, "llm_base_model": tool.llm_base_model, "llm_provider": tool.llm_provider }) prompts_data = [] for prompt in result.prompts: prompts_data.append({ "id": prompt.id, "label": prompt.label, "interpolation_string": prompt.interpolation_string, "prompt_type": prompt.prompt_type, "available_to_agents": prompt.available_to_agents, "description": prompt.description }) print(json.dumps({ "success": result.success, "tools": tools_data, "prompts": prompts_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, "interpolation_string": tool.interpolation_string, "goals": tool.goals, "temperature": tool.temperature, "max_tokens": tool.max_tokens, "llm_base_model": tool.llm_base_model, "llm_provider": tool.llm_provider }) 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(), TF_WORKSPACE_ID: credentials?.workspace_id || "", TF_API_KEY: credentials?.api_key || "", TF_REGION: credentials?.region || "au", }, }) 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) } function getCredentialsFilePath(): string { return path.join(getConfigPath(), "credentials.json") } async function loadCredentials(): Promise<{ workspace_id: string; api_key: string; region: string } | null> { const credFile = getCredentialsFilePath() if (!(await Filesystem.exists(credFile))) { return null } try { const content = await Bun.file(credFile).text() return JSON.parse(content) } catch { return null } } 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", coder_agent: "Coder Agent", prompt: "Prompt", }[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 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(), async handler() {}, }) 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(), async handler() {}, }) export const SetupCommand = cmd({ command: "setup", describe: "configure ToothFairyAI credentials", builder: (yargs) => yargs .option("api-key", { type: "string", describe: "API key", }) .option("workspace-id", { type: "string", describe: "Workspace ID", }) .option("region", { type: "string", describe: "Region (dev, au, eu, us)", default: "au", }), handler: async (args) => { const configPath = getConfigPath() const credFile = getCredentialsFilePath() // Load existing credentials let existing: { api_key?: string; workspace_id?: string; region?: string } = {} if (await Filesystem.exists(credFile)) { try { existing = await Bun.file(credFile).json() } catch {} } // Get credentials from args or prompt let apiKey = args["api-key"] let workspaceId = args["workspace-id"] let region = args.region || existing.region || "au" if (!apiKey || !workspaceId) { info("") info("ToothFairyAI Credentials Setup") info("━".repeat(40)) info("") if (!workspaceId) { process.stdout.write(`Workspace ID [${existing.workspace_id || ""}]: `) const input = await new Promise((resolve) => { process.stdin.once("data", (data) => resolve(data.toString().trim())) }) workspaceId = input || existing.workspace_id } if (!apiKey) { process.stdout.write(`API Key [${existing.api_key ? "****" + existing.api_key.slice(-4) : ""}]: `) const input = await new Promise((resolve) => { process.stdin.once("data", (data) => resolve(data.toString().trim())) }) apiKey = input || existing.api_key } process.stdout.write(`Region [${region}]: `) const regionInput = await new Promise((resolve) => { process.stdin.once("data", (data) => resolve(data.toString().trim())) }) if (regionInput) region = regionInput } if (!apiKey || !workspaceId) { printError("API key and workspace ID are required") process.exitCode = 1 return } // Save credentials await mkdir(configPath, { recursive: true }) await Bun.write( credFile, JSON.stringify( { api_key: apiKey, workspace_id: workspaceId, region, }, null, 2, ), ) success(`✓ Credentials saved to ${credFile}`) info(` Workspace: ${workspaceId}`) info(` Region: ${region}`) info("") info("Run 'tfcode validate' to test your credentials") }, }) export { ValidateCommand, SyncCommand, ToolsCommand }