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
index 33690be84..30103b589 100644
Binary files a/packages/tf-sync/src/tf_sync/__pycache__/tools.cpython-313.pyc and b/packages/tf-sync/src/tf_sync/__pycache__/tools.cpython-313.pyc differ
diff --git a/packages/tf-sync/src/tf_sync/tools.py b/packages/tf-sync/src/tf_sync/tools.py
index 053a5083b..53f96bdc5 100644
--- a/packages/tf-sync/src/tf_sync/tools.py
+++ b/packages/tf-sync/src/tf_sync/tools.py
@@ -50,8 +50,9 @@ def classify_tool(func: AgentFunction) -> ToolType:
"""
Classify a tool based on its properties.
- Currently the SDK exposes:
- - agent_functions: API functions with request_type
+ Types:
+ - AGENT_SKILL: is_agent_skill=True
+ - API_FUNCTION: has request_type
Args:
func: AgentFunction from TF SDK
@@ -59,6 +60,10 @@ def classify_tool(func: AgentFunction) -> ToolType:
Returns:
ToolType enum value
"""
+ # Agent skills have is_agent_skill=True
+ if getattr(func, 'is_agent_skill', None) is True:
+ return ToolType.AGENT_SKILL
+
# All agent_functions with request_type are API Functions
if func.request_type:
return ToolType.API_FUNCTION
@@ -89,6 +94,10 @@ def parse_function(func: AgentFunction) -> SyncedTool:
# or may use TF proxy
auth_via = "user_provided" if func.authorisation_type == "api_key" else "tf_proxy"
+ # Agent skills use skill script
+ if tool_type == ToolType.AGENT_SKILL:
+ auth_via = "tf_skill"
+
return SyncedTool(
id=func.id,
name=func.name,
@@ -98,6 +107,7 @@ def parse_function(func: AgentFunction) -> SyncedTool:
url=func.url,
authorisation_type=func.authorisation_type,
auth_via=auth_via,
+ is_agent_skill=tool_type == ToolType.AGENT_SKILL,
)
@@ -126,7 +136,8 @@ def sync_tools(config: TFConfig) -> ToolSyncResult:
Sync all tools from ToothFairyAI workspace using SDK.
Includes:
- - Agent Functions (API Functions)
+ - Agent Functions (API Functions with request_type)
+ - Agent Skills (functions with is_agent_skill=True)
- Coder Agents (agents with mode='coder')
Args:
@@ -138,7 +149,7 @@ def sync_tools(config: TFConfig) -> ToolSyncResult:
try:
client = config.get_client()
- # Sync agent functions
+ # Sync agent functions (includes API Functions and Agent Skills)
func_result = client.agent_functions.list()
tools = [parse_function(f) for f in func_result.items]
diff --git a/packages/tfcode/src/cli/cmd/tui/app.tsx b/packages/tfcode/src/cli/cmd/tui/app.tsx
index dc052c4d2..5422adf0d 100644
--- a/packages/tfcode/src/cli/cmd/tui/app.tsx
+++ b/packages/tfcode/src/cli/cmd/tui/app.tsx
@@ -17,6 +17,7 @@ import { DialogMcp } from "@tui/component/dialog-mcp"
import { DialogStatus } from "@tui/component/dialog-status"
import { DialogThemeList } from "@tui/component/dialog-theme-list"
import { DialogHelp } from "./ui/dialog-help"
+import { DialogChangelog } from "./ui/dialog-changelog"
import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command"
import { DialogAgent } from "@tui/component/dialog-agent"
import { DialogSessionList } from "@tui/component/dialog-session-list"
@@ -576,11 +577,22 @@ function App() {
},
category: "System",
},
+ {
+ title: "Changelog",
+ value: "changelog.show",
+ slash: {
+ name: "changelog",
+ },
+ onSelect: () => {
+ dialog.replace(() => )
+ },
+ category: "System",
+ },
{
title: "Open docs",
value: "docs.open",
onSelect: () => {
- open("https://opencode.ai/docs").catch(() => {})
+ open("https://toothfairyai.com/developers/tfcode").catch(() => {})
dialog.clear()
},
category: "System",
diff --git a/packages/tfcode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/tfcode/src/cli/cmd/tui/component/dialog-provider.tsx
index 635ed71f5..4d97f8180 100644
--- a/packages/tfcode/src/cli/cmd/tui/component/dialog-provider.tsx
+++ b/packages/tfcode/src/cli/cmd/tui/component/dialog-provider.tsx
@@ -15,12 +15,13 @@ import { Clipboard } from "@tui/util/clipboard"
import { useToast } from "../ui/toast"
const PROVIDER_PRIORITY: Record = {
- opencode: 0,
- "opencode-go": 1,
- openai: 2,
- "github-copilot": 3,
- anthropic: 4,
- google: 5,
+ toothfairyai: 0,
+ opencode: 1,
+ "opencode-go": 2,
+ openai: 3,
+ "github-copilot": 4,
+ anthropic: 5,
+ google: 6,
}
export function createDialogProviderOptions() {
@@ -36,6 +37,7 @@ export function createDialogProviderOptions() {
title: provider.name,
value: provider.id,
description: {
+ toothfairyai: "(Recommended - ToothFairyAI)",
opencode: "(Recommended)",
anthropic: "(API key)",
openai: "(ChatGPT Plus/Pro or API key)",
@@ -237,6 +239,19 @@ function ApiMethod(props: ApiMethodProps) {
placeholder="API key"
description={
{
+ toothfairyai: (
+
+
+ ToothFairyAI gives you access to AI coding models through your workspace credentials.
+
+
+ Set credentials via tfcode setup or environment variables
+
+
+ TF_WORKSPACE_ID, TF_API_KEY, TF_REGION (dev/au/eu/us)
+
+
+ ),
opencode: (
diff --git a/packages/tfcode/src/cli/cmd/tui/component/tips.tsx b/packages/tfcode/src/cli/cmd/tui/component/tips.tsx
index 70b1c075e..282bc59a1 100644
--- a/packages/tfcode/src/cli/cmd/tui/component/tips.tsx
+++ b/packages/tfcode/src/cli/cmd/tui/component/tips.tsx
@@ -67,7 +67,7 @@ const TIPS = [
"Press {highlight}Ctrl+X X{/highlight} or {highlight}/export{/highlight} to save the conversation as Markdown",
"Press {highlight}Ctrl+X Y{/highlight} to copy the assistant's last message to clipboard",
"Press {highlight}Ctrl+P{/highlight} to see all available actions and commands",
- "Run {highlight}/connect{/highlight} to add API keys for 75+ supported LLM providers",
+ "Run {highlight}/connect{/highlight} to configure your ToothFairyAI provider",
"The leader key is {highlight}Ctrl+X{/highlight}; combine with other keys for quick actions",
"Press {highlight}F2{/highlight} to quickly switch between recently used models",
"Press {highlight}Ctrl+X B{/highlight} to show/hide the sidebar panel",
diff --git a/packages/tfcode/src/cli/cmd/tui/ui/dialog-changelog.tsx b/packages/tfcode/src/cli/cmd/tui/ui/dialog-changelog.tsx
new file mode 100644
index 000000000..f7a6dcb25
--- /dev/null
+++ b/packages/tfcode/src/cli/cmd/tui/ui/dialog-changelog.tsx
@@ -0,0 +1,74 @@
+import { TextAttributes } from "@opentui/core"
+import { useTheme } from "@tui/context/theme"
+import { useDialog } from "./dialog"
+import { useKeyboard } from "@opentui/solid"
+import { For } from "solid-js"
+
+interface ChangelogEntry {
+ version: string
+ date: string
+ changes: string[]
+}
+
+const CHANGELOG: ChangelogEntry[] = [
+ {
+ version: "1.0.0-beta",
+ date: "2026-03-24",
+ changes: [
+ "Initial tfcode release based on opencode",
+ "Custom TF CODE logo with teal branding",
+ "ToothFairyAI workspace integration",
+ "Sync tools from ToothFairyAI workspace",
+ "TF coder agents appear in /agents",
+ "Commands: tfcode sync, tfcode validate, tfcode tools list",
+ "Support for dev, au, eu, us regions",
+ ],
+ },
+]
+
+export function DialogChangelog() {
+ const dialog = useDialog()
+ const { theme } = useTheme()
+
+ useKeyboard((evt) => {
+ if (evt.name === "return" || evt.name === "escape") {
+ dialog.clear()
+ }
+ })
+
+ return (
+
+
+
+ tfcode Changelog
+
+ dialog.clear()}>
+ esc/enter
+
+
+
+
+ {(release) => (
+
+
+ v{release.version} ({release.date})
+
+
+ {(change) => (
+
+ {" "}• {change}
+
+ )}
+
+
+ )}
+
+
+
+ dialog.clear()}>
+ ok
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/packages/tfcode/src/provider/models.ts b/packages/tfcode/src/provider/models.ts
index bae331784..8d20ba37a 100644
--- a/packages/tfcode/src/provider/models.ts
+++ b/packages/tfcode/src/provider/models.ts
@@ -15,6 +15,13 @@ export namespace ModelsDev {
const log = Log.create({ service: "models.dev" })
const filepath = path.join(Global.Path.cache, "models.json")
+ const REGION_URLS: Record = {
+ dev: "https://ai.toothfairylab.link",
+ au: "https://ai.toothfairyai.com",
+ eu: "https://ai.eu.toothfairyai.com",
+ us: "https://ai.us.toothfairyai.com",
+ }
+
export const Model = z.object({
id: z.string(),
name: z.string(),
@@ -81,6 +88,21 @@ export namespace ModelsDev {
export type Provider = z.infer
+ interface TFModel {
+ name: string
+ provider: string
+ modelType: string
+ reasoner: boolean
+ supportsVision: boolean
+ toolCalling: boolean
+ maxTokens: number
+ deprecated: boolean
+ pricing?: {
+ inputPer1mTokens: number
+ outputPer1mTokens: number
+ }
+ }
+
function url() {
return Flag.OPENCODE_MODELS_URL || "https://models.dev"
}
@@ -100,7 +122,122 @@ export namespace ModelsDev {
export async function get() {
const result = await Data()
- return result as Record
+ const providers = result as Record
+
+ // Try to fetch ToothFairyAI models dynamically
+ // First check env vars, then stored credentials
+ let tfApiKey = process.env.TF_API_KEY
+ let tfRegion = process.env.TF_REGION || "au"
+
+ // Try to load from stored credentials
+ if (!tfApiKey) {
+ try {
+ const credPath = path.join(Global.Path.data, ".tfcode", "credentials.json")
+ const credData = await Bun.file(credPath).json() as { api_key?: string; region?: string }
+ if (credData.api_key) {
+ tfApiKey = credData.api_key
+ tfRegion = credData.region || "au"
+ }
+ } catch {}
+ }
+
+ const tfBaseUrl = REGION_URLS[tfRegion] || REGION_URLS.au
+
+ if (tfApiKey) {
+ try {
+ const tfResponse = await fetch(`${tfBaseUrl}/models_list`, {
+ headers: {
+ "x-api-key": tfApiKey,
+ },
+ signal: AbortSignal.timeout(10000),
+ })
+
+ if (tfResponse.ok) {
+ const tfData = await tfResponse.json() as { templates: Record }
+ const tfModels: Record = {}
+
+ for (const [key, model] of Object.entries(tfData.templates || {})) {
+ if (model.deprecated) continue
+
+ const modelId = key.startsWith("z/") ? key.slice(2) : key
+
+ tfModels[modelId] = {
+ id: modelId,
+ name: model.name,
+ family: model.provider,
+ release_date: new Date().toISOString().split("T")[0],
+ attachment: model.supportsVision || false,
+ reasoning: model.reasoner || false,
+ temperature: true,
+ tool_call: model.toolCalling || false,
+ options: {},
+ cost: {
+ input: model.pricing?.inputPer1mTokens || 0,
+ output: model.pricing?.outputPer1mTokens || 0,
+ },
+ limit: {
+ context: model.maxTokens ? model.maxTokens * 4 : 128000,
+ output: model.maxTokens || 16000,
+ },
+ modalities: {
+ input: model.supportsVision ? ["text", "image"] : ["text"],
+ output: ["text"],
+ },
+ }
+ }
+
+ providers["toothfairyai"] = {
+ id: "toothfairyai",
+ name: "ToothFairyAI",
+ env: ["TF_API_KEY", "TF_WORKSPACE_ID"],
+ models: tfModels,
+ }
+ }
+ } catch (e) {
+ log.error("Failed to fetch ToothFairyAI models", { error: e })
+ }
+ }
+
+ // Fallback to static models if dynamic fetch failed
+ if (!providers["toothfairyai"] || Object.keys(providers["toothfairyai"].models).length === 0) {
+ providers["toothfairyai"] = {
+ id: "toothfairyai",
+ name: "ToothFairyAI",
+ env: ["TF_API_KEY", "TF_WORKSPACE_ID"],
+ models: {
+ "sorcerer": {
+ id: "sorcerer",
+ name: "TF Sorcerer",
+ family: "groq",
+ release_date: "2025-01-01",
+ attachment: true,
+ reasoning: false,
+ temperature: true,
+ tool_call: true,
+ options: {},
+ cost: { input: 0.62, output: 1.75 },
+ limit: { context: 128000, output: 16000 },
+ modalities: { input: ["text", "image"], output: ["text"] },
+ },
+ "mystica": {
+ id: "mystica",
+ name: "TF Mystica",
+ family: "fireworks",
+ release_date: "2025-01-01",
+ attachment: true,
+ reasoning: false,
+ temperature: true,
+ tool_call: true,
+ options: {},
+ cost: { input: 2.5, output: 7.5 },
+ limit: { context: 128000, output: 16000 },
+ modalities: { input: ["text", "image"], output: ["text"] },
+ },
+ },
+ }
+ }
+
+ return providers
}
export async function refresh() {
diff --git a/packages/tfcode/src/provider/provider.ts b/packages/tfcode/src/provider/provider.ts
index 6ab45d028..4f1cefd69 100644
--- a/packages/tfcode/src/provider/provider.ts
+++ b/packages/tfcode/src/provider/provider.ts
@@ -182,6 +182,29 @@ export namespace Provider {
options: hasKey ? {} : { apiKey: "public" },
}
},
+ async toothfairyai(input) {
+ const hasCredentials = await (async () => {
+ const env = Env.all()
+ if (env.TF_API_KEY) return true
+ if (await Auth.get(input.id)) return true
+ const config = await Config.get()
+ if (config.provider?.["toothfairyai"]?.options?.apiKey) return true
+
+ // Check stored credentials file
+ try {
+ const credPath = path.join(Global.Path.data, ".tfcode", "credentials.json")
+ const credData = await Bun.file(credPath).json() as { api_key?: string }
+ if (credData.api_key) return true
+ } catch {}
+
+ return false
+ })()
+
+ return {
+ autoload: hasCredentials,
+ options: hasCredentials ? {} : { apiKey: "setup-required" },
+ }
+ },
openai: async () => {
return {
autoload: false,
diff --git a/packages/tfcode/src/provider/schema.ts b/packages/tfcode/src/provider/schema.ts
index 71c8a1029..2a4967fab 100644
--- a/packages/tfcode/src/provider/schema.ts
+++ b/packages/tfcode/src/provider/schema.ts
@@ -12,6 +12,7 @@ export const ProviderID = providerIdSchema.pipe(
make: (id: string) => schema.makeUnsafe(id),
zod: z.string().pipe(z.custom()),
// Well-known providers
+ toothfairyai: schema.makeUnsafe("toothfairyai"),
opencode: schema.makeUnsafe("opencode"),
anthropic: schema.makeUnsafe("anthropic"),
openai: schema.makeUnsafe("openai"),
diff --git a/packages/tfcode/test-creds.ts b/packages/tfcode/test-creds.ts
new file mode 100644
index 000000000..b40e86c6c
--- /dev/null
+++ b/packages/tfcode/test-creds.ts
@@ -0,0 +1,19 @@
+import { Global } from "./src/global"
+import path from "path"
+import { ModelsDev } from "./src/provider/models"
+
+console.log("Global.Path.data:", Global.Path.data)
+const credPath = path.join(Global.Path.data, ".tfcode", "credentials.json")
+console.log("Credentials path:", credPath)
+
+try {
+ const credData = await Bun.file(credPath).json()
+ console.log("Credentials loaded:", { api_key: (credData as any).api_key?.slice(0,10)+"...", region: (credData as any).region })
+} catch (e) {
+ console.log("Error loading credentials:", e)
+}
+
+const providers = await ModelsDev.get()
+console.log("ToothFairyAI provider found:", !!providers["toothfairyai"])
+console.log("Models count:", Object.keys(providers["toothfairyai"]?.models || {}).length)
+console.log("Sample models:", Object.keys(providers["toothfairyai"]?.models || {}).slice(0, 5))
\ No newline at end of file