From ff77a81141c13aadc00ad7126db14322c9b1724d Mon Sep 17 00:00:00 2001 From: Gab Date: Tue, 24 Mar 2026 17:42:26 +1100 Subject: [PATCH] feat: agents and skills --- .../tf_sync/__pycache__/tools.cpython-313.pyc | Bin 6847 -> 7170 bytes packages/tf-sync/src/tf_sync/tools.py | 19 ++- packages/tfcode/src/cli/cmd/tui/app.tsx | 14 +- .../cli/cmd/tui/component/dialog-provider.tsx | 27 +++- .../tfcode/src/cli/cmd/tui/component/tips.tsx | 2 +- .../src/cli/cmd/tui/ui/dialog-changelog.tsx | 74 ++++++++++ packages/tfcode/src/provider/models.ts | 139 +++++++++++++++++- packages/tfcode/src/provider/provider.ts | 23 +++ packages/tfcode/src/provider/schema.ts | 1 + packages/tfcode/test-creds.ts | 19 +++ 10 files changed, 305 insertions(+), 13 deletions(-) create mode 100644 packages/tfcode/src/cli/cmd/tui/ui/dialog-changelog.tsx create mode 100644 packages/tfcode/test-creds.ts 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 33690be8469b2414754aaa2581797d24216cb354..30103b5898db400631da4fa78c2ad245b859ea8f 100644 GIT binary patch delta 1060 zcmaiy%TE(Q9LHy<`{>eA`c@23ERbEmMl2-Q2#N&(Z7SL#Xib_bTWqX_%x>f3WQ;Mw z!I+FBCVJrD0dE=-9|vQCHxEWM2Hf~BXjW;2c=9((OpGyc()oOUkLk?LZ+@@3ulLlf zJDn1OQEpu>I0H{=?h9=kxHg#U4*~NigF&n@hEh0H)y;A+Qnm#BD74UsGeF-8LHf<_ zrJI72Dc_8gZC78@2C=<2ePu?~V$$wV|IlDEolPAbNhD&Sf|kuqsU?Z1v=R@TmP{+0Axx=yPS*($A!1bju_N)(FJiQ_8{(ZX zwL!o&RB1q>>z4k`-?E^85IP=jdoOf-b_G{m;T2c-oftNG+G&qU4p?km_$Dpc=Obe{ zW0eG==mg9CHDVJ8<*J|nT*;Pk(N3w+hE8~>Tk>Nc&`U=p9tW6Di;_R_-|dEBD}JB7 z88x8{|NATf<=~S-s1YUS@~GF=i1Jtw=p*TY!w~iSR^1dk9jcZiR)G`;VYeqf20Orz zPUebbb&!y0LV_&Hsz##pc6CB8@wu z{cFL{!{OEDo)!27d%k%k2X7*Y<2NfoR9m->u-*O1QES>je`;>nalwBmik72N9NCr+ z;5b@(* z0!Lp%X2a4Y?^D|ucmk9rBS1wUYz=8+2r$$#j4~W&kO7KF2AF*sVDLoM%0=D4x*_HZ zMO`K8Bxz^qafS&x8*JmUU_M)V9_+v#W^O%27g!1=1N6ssnahE*M#DR3ul0=VQpBp2 z^*LeYTK{D0NF759Lo2`#_g|czEEm;2lBerC`VR+i(>v3KKI=uhtFz0z^&vfB*mh delta 712 zcmYLGOKTHR6uviUCX;!johH^av1YUinb;OJRGNsTi8iG)w8gYq3HA{?X@ZiLI}@zc zg}8Jj9)zMUx(rkh3AhkM=%OoKi&^>uWVBg`5AfWI_QLtT?|k>%d+y`G(7QqZo~lX& z$6#QmQVhQGKl27Crf!VAWrQbWmRu(A%WJ^L?f?vlDreyYG3drF%!{W+F4h<`tM;us zx?MK){G|!qTwhzYOe-bz>q{#*wNR>6Z`hU9s+H2y)8qR8RKH!Z%djgJe1@9^u^SU% z%$Lv-D9>P6ClT%w=Ll^35>$rkUA^Fv?hX`W^!EoIPWh(~7+wEC2HR59OJ`l^cEWEd z-j??yMVOMiKx~HGp5opR4gE_}V>`MQetQAsSxBYgn8^7l#fGQyiu_#1IUT zWw^cu_0Un_3YS;meJJjkNBN@hGt@^rxnv@Tvj_%d8Ayg>o<&SPKq(AS{&S3nuNMS7?S=OXE>MvrZ(4neBNTAZAiPKC`LhalBcEly6BYUsuJyx_$8sq|>w QAxK-3El$odyp4?h0ZzoIW&i*H 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