2026-03-27 08:42:09 +11:00

517 lines
21 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
import { spawn } from "child_process"
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs"
import { join, dirname } from "path"
import { homedir } from "os"
import * as readline from "readline"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const TFCODE_DIR = join(homedir(), ".tfcode")
const TOOLS_FILE = join(TFCODE_DIR, "tools.json")
const CREDENTIALS_FILE = join(TFCODE_DIR, "credentials.json")
const CONFIG_FILE = join(TFCODE_DIR, "config.json")
const COLORS = {
reset: "\x1b[0m",
bold: "\x1b[1m",
green: "\x1b[32m",
red: "\x1b[31m",
cyan: "\x1b[36m",
dim: "\x1b[90m",
yellow: "\x1b[33m",
magenta: "\x1b[35m",
}
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 ensureConfigDir() {
if (!existsSync(TFCODE_DIR)) mkdirSync(TFCODE_DIR, { recursive: true })
}
function loadConfig() {
const envConfig = {
workspace_id: process.env.TF_WORKSPACE_ID,
api_key: process.env.TF_API_KEY,
region: process.env.TF_REGION,
}
if (envConfig.workspace_id && envConfig.api_key) return envConfig
if (existsSync(CONFIG_FILE)) {
try {
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))
} catch {}
}
return null
}
function saveConfig(config) {
ensureConfigDir()
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
}
function runPythonSync(method, config = null) {
const wsId = config?.workspace_id || process.env.TF_WORKSPACE_ID || ""
const apiKey = config?.api_key || process.env.TF_API_KEY || ""
const region = config?.region || process.env.TF_REGION || "au"
const pythonCode = `
import json, sys, os
try:
os.environ["TF_WORKSPACE_ID"] = "${wsId}"
os.environ["TF_API_KEY"] = "${apiKey}"
os.environ["TF_REGION"] = "${region}"
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 = [{"id": t.id, "name": t.name, "description": t.description, "tool_type": t.tool_type.value, "request_type": t.request_type.value if t.request_type else None, "url": t.url, "auth_via": t.auth_via, "interpolation_string": t.interpolation_string, "goals": t.goals, "temperature": t.temperature, "max_tokens": t.max_tokens, "llm_base_model": t.llm_base_model, "llm_provider": t.llm_provider} for t in result.tools]
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)}))
`
return new Promise((resolve, reject) => {
const proc = spawn(process.env.TFCODE_PYTHON_PATH || "python3", ["-c", pythonCode], { env: { ...process.env } })
let stdout = "",
stderr = ""
proc.stdout.on("data", (d) => (stdout += d))
proc.stderr.on("data", (d) => (stderr += d))
proc.on("close", (code) => {
if (code !== 0 && !stdout) reject(new Error(`Python failed: ${stderr}`))
else
try {
resolve(JSON.parse(stdout.trim()))
} catch (e) {
reject(new Error(`Parse error: ${stdout}`))
}
})
proc.on("error", reject)
})
}
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))
}
async function question(prompt) {
return new Promise((resolve) => {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
rl.question(prompt, (answer) => {
rl.close()
resolve(answer.trim())
})
})
}
async function select(prompt, options) {
log("")
log(prompt)
log("")
options.forEach((opt, i) => log(` ${COLORS.cyan}${i + 1}.${COLORS.reset} ${opt}`))
log("")
const answer = await question("Select (1-" + options.length + "): ")
const idx = parseInt(answer) - 1
return idx >= 0 && idx < options.length ? idx : 0
}
async function interactiveSetup() {
log("")
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta} tfcode Setup${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log("")
log("This will guide you through setting up your ToothFairyAI credentials.")
log("")
log(`${COLORS.dim}You can find your credentials at:${COLORS.reset}`)
log(`${COLORS.dim} https://app.toothfairyai.com → Settings → API Keys${COLORS.reset}`)
log("")
log(`${COLORS.bold}Step 1: Workspace ID${COLORS.reset}`)
log(`${COLORS.dim}This is your workspace UUID${COLORS.reset}`)
log("")
const workspaceId = await question("Enter your Workspace ID: ")
if (!workspaceId) {
error("Workspace ID is required")
process.exit(1)
}
log("")
log(`${COLORS.bold}Step 2: API Key${COLORS.reset}`)
log(`${COLORS.dim}Paste or type your API key${COLORS.reset}`)
log("")
const apiKey = await question("Enter your API Key: ")
if (!apiKey) {
error("API Key is required")
process.exit(1)
}
log("")
log(`${COLORS.bold}Step 3: Region${COLORS.reset}`)
const regions = ["dev (Development)", "au (Australia)", "eu (Europe)", "us (United States)"]
const regionIdx = await select("Select your region:", regions)
const region = ["dev", "au", "eu", "us"][regionIdx]
log("")
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log("")
log(`${COLORS.bold}Summary:${COLORS.reset}`)
log(` Workspace ID: ${workspaceId}`)
log(` API Key: ***${apiKey.slice(-4)}`)
log(` Region: ${region}`)
log("")
const confirm = await question("Save these credentials? (Y/n): ")
if (confirm.toLowerCase() === "n" || confirm.toLowerCase() === "no") {
log("Setup cancelled.")
return
}
const config = { workspace_id: workspaceId, api_key: apiKey, region }
saveConfig(config)
success("Credentials saved to ~/.tfcode/config.json")
log("")
const testNow = await question("Validate credentials now? (Y/n): ")
if (testNow.toLowerCase() === "n" || testNow.toLowerCase() === "no") return
log("")
info("Validating credentials...")
log("")
try {
const result = await runPythonSync("validate", config)
if (result.success) {
success("Credentials valid!")
log(` API URL: ${result.base_url}`)
log(` Workspace ID: ${result.workspace_id}`)
log("")
const syncNow = await question("Sync tools now? (Y/n): ")
if (syncNow.toLowerCase() === "n" || syncNow.toLowerCase() === "no") return
log("")
info("Syncing tools...")
log("")
const syncResult = await runPythonSync("sync", config)
if (syncResult.success) {
saveToolsCache(syncResult)
success(`Synced ${syncResult.tools.length} tools`)
if (syncResult.by_type && Object.keys(syncResult.by_type).length > 0) {
log("")
log("By type:")
for (const [type, count] of Object.entries(syncResult.by_type)) log(` ${type}: ${count}`)
}
} else {
error(`Sync failed: ${syncResult.error}`)
}
} else {
error(`Validation failed: ${result.error}`)
}
} catch (e) {
error(`Failed: ${e.message}`)
}
log("")
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.green} Setup Complete!${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log("")
log("Commands:")
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Check credentials`)
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools`)
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List tools`)
log("")
}
function showHelp() {
log("")
log(`${COLORS.bold}tfcode${COLORS.reset} - ToothFairyAI's AI coding agent`)
log("")
log("Commands:")
log(` ${COLORS.cyan}tfcode setup${COLORS.reset} Interactive credential setup`)
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Test credentials`)
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools from workspace`)
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List synced tools`)
log(` ${COLORS.cyan}tfcode test-agent <id>${COLORS.reset} Test agent prompt injection`)
log(` ${COLORS.cyan}tfcode debug${COLORS.reset} Show debug info`)
log(` ${COLORS.cyan}tfcode --help${COLORS.reset} Show this help`)
log(` ${COLORS.cyan}tfcode --version${COLORS.reset} Show version`)
log("")
log(`${COLORS.dim}For full TUI, run from source:${COLORS.reset}`)
log(`${COLORS.dim} bun run packages/tfcode/src/index.ts${COLORS.reset}`)
log("")
}
function showDebugInfo() {
log("")
log(`${COLORS.bold}Debug Information${COLORS.reset}`)
log("")
log(` ${COLORS.bold}TFCODE_DIR:${COLORS.reset} ${TFCODE_DIR}`)
log(` ${COLORS.bold}TOOLS_FILE:${COLORS.reset} ${TOOLS_FILE}`)
log(` ${COLORS.bold}CONFIG_FILE:${COLORS.reset} ${CONFIG_FILE}`)
log(` ${COLORS.bold}CREDENTIALS_FILE:${COLORS.reset} ${CREDENTIALS_FILE}`)
log("")
log(` ${COLORS.bold}tools.json exists:${COLORS.reset} ${existsSync(TOOLS_FILE)}`)
if (existsSync(TOOLS_FILE)) {
const tools = loadCachedTools()
log(` ${COLORS.bold}tools.json valid:${COLORS.reset} ${tools?.success ?? false}`)
log(` ${COLORS.bold}tools count:${COLORS.reset} ${tools?.tools?.length ?? 0}`)
const coderAgents = tools?.tools?.filter((t) => t.tool_type === "coder_agent") ?? []
log(` ${COLORS.bold}coder_agent count:${COLORS.reset} ${coderAgents.length}`)
if (coderAgents.length > 0) {
log("")
log(` ${COLORS.bold}Coder Agents:${COLORS.reset}`)
coderAgents.forEach((a) => {
log(` - ${a.name} (id: ${a.id})`)
log(` interpolation_string: ${a.interpolation_string ? "YES" : "NO"}`)
log(` goals: ${a.goals ? "YES" : "NO"}`)
log(` llm_provider: ${a.llm_provider ?? "(null)"}`)
log(` llm_base_model: ${a.llm_base_model ?? "(null)"}`)
})
}
}
log("")
}
function testAgentPrompt(agentId) {
const tools = loadCachedTools()
if (!tools?.success) {
error("No tools. Run: tfcode sync")
process.exit(1)
}
const agent = tools.tools.find((t) => t.id === agentId || t.name === agentId)
if (!agent) {
error(`Agent not found: ${agentId}`)
log("")
log("Available coder agents:")
tools.tools
.filter((t) => t.tool_type === "coder_agent")
.forEach((t) => {
log(` ${COLORS.cyan}${t.id}${COLORS.reset} - ${t.name}`)
})
process.exit(1)
}
log("")
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta} Agent Data from tools.json${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log("")
log(` ${COLORS.bold}id:${COLORS.reset} ${agent.id}`)
log(` ${COLORS.bold}name:${COLORS.reset} ${agent.name}`)
log(` ${COLORS.bold}description:${COLORS.reset} ${agent.description || "(none)"}`)
log(` ${COLORS.bold}tool_type:${COLORS.reset} ${agent.tool_type}`)
log(` ${COLORS.bold}auth_via:${COLORS.reset} ${agent.auth_via}`)
log("")
log(` ${COLORS.bold}interpolation_string:${COLORS.reset}`)
if (agent.interpolation_string) {
log(` ${agent.interpolation_string.substring(0, 200)}${agent.interpolation_string.length > 200 ? "..." : ""}`)
} else {
log(` ${COLORS.dim}(none)${COLORS.reset}`)
}
log("")
log(` ${COLORS.bold}goals:${COLORS.reset}`)
if (agent.goals) {
log(` ${agent.goals.substring(0, 200)}${agent.goals.length > 200 ? "..." : ""}`)
} else {
log(` ${COLORS.dim}(none)${COLORS.reset}`)
}
log("")
log(` ${COLORS.bold}temperature:${COLORS.reset} ${agent.temperature ?? "(none)"}`)
log(` ${COLORS.bold}max_tokens:${COLORS.reset} ${agent.max_tokens ?? "(none)"}`)
log(` ${COLORS.bold}llm_base_model:${COLORS.reset} ${agent.llm_base_model ?? "(none)"}`)
log(` ${COLORS.bold}llm_provider:${COLORS.reset} ${agent.llm_provider ?? "(none)"}`)
log("")
// Build highlighted instructions
const isTFProvider = !agent.llm_provider || agent.llm_provider === "toothfairyai" || agent.llm_provider === "tf"
const hasPrompt = agent.interpolation_string && agent.interpolation_string.trim().length > 0
const hasGoals = agent.goals && agent.goals.trim().length > 0
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta} Model Mapping${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log("")
log(` ${COLORS.bold}isTFProvider:${COLORS.reset} ${isTFProvider}`)
log(
` ${COLORS.bold}mapped model:${COLORS.reset} ${isTFProvider && agent.llm_base_model ? `toothfairyai/${agent.llm_base_model}` : "(no mapping)"}`,
)
log("")
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta} Highlighted Instructions Preview${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log("")
if (!hasPrompt && !hasGoals) {
log(
` ${COLORS.dim}(No interpolation_string or goals - no highlighted instructions will be generated)${COLORS.reset}`,
)
} else {
log("")
log("═══════════════════════════════════════════════════════════════════════════════")
log("⚠️ ULTRA IMPORTANT - AGENT CONFIGURATION ⚠️")
log("═══════════════════════════════════════════════════════════════════════════════")
log("")
log(`You are acting as the agent: "${agent.name}"`)
if (agent.description) {
log(`Description: ${agent.description}`)
}
log("")
log("The following instructions and goals are MANDATORY and MUST be followed")
log("with the HIGHEST PRIORITY. These override any conflicting default behaviors.")
log("═══════════════════════════════════════════════════════════════════════════════")
if (hasPrompt) {
log("")
log("┌─────────────────────────────────────────────────────────────────────────────┐")
log(`│ 🎯 AGENT "${agent.name}" INSTRUCTIONS (CRITICAL - MUST FOLLOW) │`)
log("└─────────────────────────────────────────────────────────────────────────────┘")
log("")
log(agent.interpolation_string)
}
if (hasGoals) {
log("")
log("┌─────────────────────────────────────────────────────────────────────────────┐")
log(`│ 🎯 AGENT "${agent.name}" GOALS (CRITICAL - MUST ACHIEVE) │`)
log("└─────────────────────────────────────────────────────────────────────────────┘")
log("")
log(agent.goals)
}
log("")
log("═══════════════════════════════════════════════════════════════════════════════")
log(`⚠️ END OF ULTRA IMPORTANT AGENT "${agent.name}" CONFIGURATION ⚠️`)
log("═══════════════════════════════════════════════════════════════════════════════")
}
log("")
}
const args = process.argv.slice(2)
const command = args[0]
if (args.includes("--help") || args.includes("-h")) {
showHelp()
} else if (args.includes("--version") || args.includes("-v")) {
log("tfcode v1.0.0-beta.9")
} else if (command === "setup") {
interactiveSetup()
} else if (command === "validate") {
;(async () => {
const config = loadConfig()
if (!config) {
error("No credentials. Run: tfcode setup")
process.exit(1)
}
info("Validating...")
try {
const result = await runPythonSync("validate", config)
if (result.success) {
success("Credentials valid")
log(` API URL: ${result.base_url}`)
} else {
error(`Failed: ${result.error}`)
process.exit(1)
}
} catch (e) {
error(`Failed: ${e.message}`)
process.exit(1)
}
})()
} else if (command === "sync") {
;(async () => {
const config = loadConfig()
if (!config) {
error("No credentials. Run: tfcode setup")
process.exit(1)
}
info("Syncing tools...")
try {
const result = await runPythonSync("sync", config)
if (result.success) {
saveToolsCache(result)
success(`Synced ${result.tools.length} tools`)
if (result.by_type) {
log("")
log("By type:")
for (const [t, c] of Object.entries(result.by_type)) log(` ${t}: ${c}`)
}
} else {
error(`Failed: ${result.error}`)
process.exit(1)
}
} catch (e) {
error(`Failed: ${e.message}`)
process.exit(1)
}
})()
} else if (command === "tools" && args[1] === "list") {
const cached = loadCachedTools()
if (!cached?.success) {
error("No tools. Run: tfcode sync")
process.exit(1)
}
let tools = cached.tools
if (args[2] === "--type" && args[3]) tools = tools.filter((t) => t.tool_type === args[3])
log(`\n${tools.length} tool(s):\n`)
for (const t of tools) {
log(` ${COLORS.cyan}${t.name}${COLORS.reset}`)
log(` Type: ${t.tool_type}`)
if (t.description) log(` ${COLORS.dim}${t.description.slice(0, 60)}${COLORS.reset}`)
log(` Auth: ${t.auth_via}\n`)
}
} else if (command === "test-agent") {
const agentId = args[1]
if (!agentId) {
error("Usage: tfcode test-agent <agent-id-or-name>")
process.exit(1)
}
testAgentPrompt(agentId)
} else if (command === "debug") {
showDebugInfo()
} else if (!command) {
// Show help instead of trying TUI (TUI requires full build)
showHelp()
} else {
error(`Unknown command: ${command}`)
showHelp()
process.exit(1)
}