@toothfairyai/tfcode 1.0.24 → 1.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/tfcode CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toothfairyai/tfcode",
3
- "version": "1.0.24",
3
+ "version": "1.0.25",
4
4
  "bin": {
5
5
  "tfcode": "./bin/tfcode.js"
6
6
  },
@@ -9,18 +9,18 @@
9
9
  },
10
10
  "license": "MIT",
11
11
  "optionalDependencies": {
12
- "@toothfairyai/tfcode-linux-arm64": "1.0.24",
13
- "@toothfairyai/tfcode-windows-x64": "1.0.24",
14
- "@toothfairyai/tfcode-linux-x64-baseline-musl": "1.0.24",
15
- "@toothfairyai/tfcode-darwin-x64-baseline": "1.0.24",
16
- "@toothfairyai/tfcode-linux-x64-musl": "1.0.24",
17
- "@toothfairyai/tfcode-windows-x64-baseline": "1.0.24",
18
- "@toothfairyai/tfcode-linux-arm64-musl": "1.0.24",
19
- "@toothfairyai/tfcode-windows-arm64": "1.0.24",
20
- "@toothfairyai/tfcode-linux-x64": "1.0.24",
21
- "@toothfairyai/tfcode-darwin-x64": "1.0.24",
22
- "@toothfairyai/tfcode-linux-x64-baseline": "1.0.24",
23
- "@toothfairyai/tfcode-darwin-arm64": "1.0.24"
12
+ "@toothfairyai/tfcode-linux-arm64": "1.0.25",
13
+ "@toothfairyai/tfcode-windows-x64": "1.0.25",
14
+ "@toothfairyai/tfcode-linux-x64-baseline-musl": "1.0.25",
15
+ "@toothfairyai/tfcode-darwin-x64-baseline": "1.0.25",
16
+ "@toothfairyai/tfcode-linux-x64-musl": "1.0.25",
17
+ "@toothfairyai/tfcode-windows-x64-baseline": "1.0.25",
18
+ "@toothfairyai/tfcode-linux-arm64-musl": "1.0.25",
19
+ "@toothfairyai/tfcode-windows-arm64": "1.0.25",
20
+ "@toothfairyai/tfcode-linux-x64": "1.0.25",
21
+ "@toothfairyai/tfcode-darwin-x64": "1.0.25",
22
+ "@toothfairyai/tfcode-linux-x64-baseline": "1.0.25",
23
+ "@toothfairyai/tfcode-darwin-arm64": "1.0.25"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=18"
@@ -0,0 +1,21 @@
1
+ """
2
+ tf-sync: ToothFairyAI workspace sync layer for tfcode
3
+ """
4
+
5
+ from tf_sync.agents import sync_agents
6
+ from tf_sync.mcp import sync_mcp_servers
7
+ from tf_sync.tools import sync_tools, ToolType
8
+ from tf_sync.config import TFConfig, load_config, validate_credentials, get_region_urls
9
+
10
+ __all__ = [
11
+ "sync_agents",
12
+ "sync_mcp_servers",
13
+ "sync_tools",
14
+ "ToolType",
15
+ "TFConfig",
16
+ "load_config",
17
+ "validate_credentials",
18
+ "get_region_urls",
19
+ ]
20
+
21
+ __version__ = "0.1.0"
@@ -0,0 +1,39 @@
1
+ """
2
+ Agent sync module for tfcode.
3
+
4
+ NOTE: This module is reserved for future implementation.
5
+ Currently, tfcode only syncs tools (MCP, Skills, Database, Functions).
6
+ Agent sync will be added in a later phase.
7
+ """
8
+
9
+ from typing import Any
10
+
11
+ from pydantic import BaseModel
12
+
13
+ from tf_sync.config import TFConfig
14
+
15
+
16
+ class AgentSyncResult(BaseModel):
17
+ """Result of agent sync operation."""
18
+
19
+ success: bool
20
+ agents: list[dict[str, Any]] = []
21
+ error: str | None = None
22
+
23
+
24
+ def sync_agents(config: TFConfig) -> AgentSyncResult:
25
+ """
26
+ Sync agents from ToothFairyAI workspace.
27
+
28
+ NOTE: Currently not implemented. Reserved for future use.
29
+
30
+ Args:
31
+ config: TFConfig instance
32
+
33
+ Returns:
34
+ AgentSyncResult (currently always returns not implemented)
35
+ """
36
+ return AgentSyncResult(
37
+ success=False,
38
+ error="Agent sync not yet implemented. Use tools sync for now.",
39
+ )
@@ -0,0 +1,221 @@
1
+ """
2
+ Configuration management for tfcode ToothFairyAI integration.
3
+ Uses the official ToothFairyAI Python SDK for multi-region support.
4
+ """
5
+
6
+ import os
7
+ from enum import Enum
8
+ from typing import Optional
9
+
10
+ from pydantic import BaseModel, Field, SecretStr
11
+ from toothfairyai import ToothFairyClient
12
+ from toothfairyai.errors import ToothFairyError
13
+
14
+
15
+ class Region(str, Enum):
16
+ DEV = "dev"
17
+ AU = "au"
18
+ EU = "eu"
19
+ US = "us"
20
+
21
+
22
+ class ToolType(str, Enum):
23
+ MCP_SERVER = "mcp_server"
24
+ AGENT_SKILL = "agent_skill"
25
+ CODER_AGENT = "coder_agent"
26
+ DATABASE_SCRIPT = "database_script"
27
+ API_FUNCTION = "api_function"
28
+ PROMPT = "prompt"
29
+
30
+
31
+ class FunctionRequestType(str, Enum):
32
+ GET = "get"
33
+ POST = "post"
34
+ PUT = "put"
35
+ DELETE = "delete"
36
+ PATCH = "patch"
37
+ CUSTOM = "custom"
38
+ GRAPHQL_QUERY = "graphql_query"
39
+ GRAPHQL_MUTATION = "graphql_mutation"
40
+
41
+
42
+ # Region-specific URL configurations
43
+ REGION_URLS = {
44
+ Region.DEV: {
45
+ "base_url": "https://api.toothfairylab.link",
46
+ "ai_url": "https://ai.toothfairylab.link",
47
+ "ai_stream_url": "https://ais.toothfairylab.link",
48
+ "mcp_url": "https://mcp.toothfairylab.link/sse",
49
+ "mcp_proxy_url": "https://mcp-proxy.toothfairylab.link",
50
+ },
51
+ Region.AU: {
52
+ "base_url": "https://api.toothfairyai.com",
53
+ "ai_url": "https://ai.toothfairyai.com",
54
+ "ai_stream_url": "https://ais.toothfairyai.com",
55
+ "mcp_url": "https://mcp.toothfairyai.com/sse",
56
+ "mcp_proxy_url": "https://mcp-proxy.toothfairyai.com",
57
+ },
58
+ Region.EU: {
59
+ "base_url": "https://api.eu.toothfairyai.com",
60
+ "ai_url": "https://ai.eu.toothfairyai.com",
61
+ "ai_stream_url": "https://ais.eu.toothfairyai.com",
62
+ "mcp_url": "https://mcp.eu.toothfairyai.com/sse",
63
+ "mcp_proxy_url": "https://mcp-proxy.eu.toothfairyai.com",
64
+ },
65
+ Region.US: {
66
+ "base_url": "https://api.us.toothfairyai.com",
67
+ "ai_url": "https://ai.us.toothfairyai.com",
68
+ "ai_stream_url": "https://ais.us.toothfairyai.com",
69
+ "mcp_url": "https://mcp.us.toothfairyai.com/sse",
70
+ "mcp_proxy_url": "https://mcp-proxy.us.toothfairyai.com",
71
+ },
72
+ }
73
+
74
+
75
+ def get_region_urls(region: Region) -> dict[str, str]:
76
+ """Get URLs for a specific region."""
77
+ return REGION_URLS.get(region, REGION_URLS[Region.AU])
78
+
79
+
80
+ class TFConfig(BaseModel):
81
+ """ToothFairyAI workspace configuration."""
82
+
83
+ workspace_id: str
84
+ api_key: SecretStr
85
+ region: Region = Region.AU
86
+ enabled: bool = True
87
+
88
+ sync_interval: int = Field(default=3600, ge=60)
89
+ mcp_proxy_timeout: int = Field(default=30000, ge=1000)
90
+
91
+ _client: Optional[ToothFairyClient] = None
92
+
93
+ def get_client(self) -> ToothFairyClient:
94
+ """
95
+ Get or create a ToothFairyClient instance configured for this region.
96
+
97
+ Returns:
98
+ ToothFairyClient configured with region-specific URLs
99
+ """
100
+ if self._client is None:
101
+ urls = get_region_urls(self.region)
102
+ self._client = ToothFairyClient(
103
+ api_key=self.api_key.get_secret_value(),
104
+ workspace_id=self.workspace_id,
105
+ base_url=urls["base_url"],
106
+ ai_url=urls["ai_url"],
107
+ ai_stream_url=urls["ai_stream_url"],
108
+ )
109
+ return self._client
110
+
111
+ @property
112
+ def mcp_sse_url(self) -> str:
113
+ """Get the MCP SSE endpoint URL for this region."""
114
+ return get_region_urls(self.region)["mcp_url"]
115
+
116
+ @property
117
+ def mcp_proxy_url(self) -> str:
118
+ """Get the MCP proxy URL for this region."""
119
+ return get_region_urls(self.region)["mcp_proxy_url"]
120
+
121
+
122
+ class CredentialValidationResult(BaseModel):
123
+ """Result of credential validation."""
124
+ success: bool
125
+ workspace_id: Optional[str] = None
126
+ workspace_name: Optional[str] = None
127
+ error: Optional[str] = None
128
+
129
+
130
+ def load_config(
131
+ workspace_id: Optional[str] = None,
132
+ api_key: Optional[str] = None,
133
+ region: Optional[Region] = None,
134
+ ) -> TFConfig:
135
+ """
136
+ Load ToothFairyAI configuration from environment or parameters.
137
+
138
+ Args:
139
+ workspace_id: Workspace UUID (defaults to TF_WORKSPACE_ID env var)
140
+ api_key: API key (defaults to TF_API_KEY env var)
141
+ region: Region (defaults to TF_REGION env var or 'au')
142
+
143
+ Returns:
144
+ TFConfig instance
145
+
146
+ Raises:
147
+ ValueError: If required configuration is missing
148
+ """
149
+ ws_id = workspace_id or os.environ.get("TF_WORKSPACE_ID")
150
+ key = api_key or os.environ.get("TF_API_KEY")
151
+
152
+ # Parse region from env or use provided/default
153
+ region_str = os.environ.get("TF_REGION", "au")
154
+ reg = region or Region(region_str)
155
+
156
+ if not ws_id:
157
+ raise ValueError("TF_WORKSPACE_ID not set. Set environment variable or pass workspace_id.")
158
+ if not key:
159
+ raise ValueError("TF_API_KEY not set. Set environment variable or pass api_key.")
160
+
161
+ return TFConfig(
162
+ workspace_id=ws_id,
163
+ api_key=SecretStr(key),
164
+ region=reg,
165
+ )
166
+
167
+
168
+ def validate_credentials(config: TFConfig) -> CredentialValidationResult:
169
+ """
170
+ Validate ToothFairyAI credentials using the SDK.
171
+
172
+ Args:
173
+ config: TFConfig instance
174
+
175
+ Returns:
176
+ CredentialValidationResult indicating success or failure
177
+ """
178
+ try:
179
+ client = config.get_client()
180
+
181
+ # Test connection by listing chats (lightweight operation)
182
+ if client.test_connection():
183
+ return CredentialValidationResult(
184
+ success=True,
185
+ workspace_id=config.workspace_id,
186
+ workspace_name="Connected",
187
+ )
188
+ else:
189
+ return CredentialValidationResult(
190
+ success=False,
191
+ error="Connection test failed. Check credentials and region.",
192
+ )
193
+
194
+ except ToothFairyError as e:
195
+ error_msg = str(e)
196
+
197
+ if "401" in error_msg or "Unauthorized" in error_msg:
198
+ return CredentialValidationResult(
199
+ success=False,
200
+ error="Invalid API key. Check TF_API_KEY environment variable.",
201
+ )
202
+ elif "403" in error_msg or "Forbidden" in error_msg:
203
+ return CredentialValidationResult(
204
+ success=False,
205
+ error="API access not allowed. Business or Enterprise subscription required.",
206
+ )
207
+ elif "404" in error_msg or "Not Found" in error_msg:
208
+ return CredentialValidationResult(
209
+ success=False,
210
+ error="Workspace not found. Check TF_WORKSPACE_ID environment variable.",
211
+ )
212
+ else:
213
+ return CredentialValidationResult(
214
+ success=False,
215
+ error=f"API error: {error_msg}",
216
+ )
217
+ except Exception as e:
218
+ return CredentialValidationResult(
219
+ success=False,
220
+ error=f"Unexpected error: {str(e)}",
221
+ )
@@ -0,0 +1,41 @@
1
+ """
2
+ MCP server sync module for tfcode.
3
+
4
+ NOTE: MCP servers are not currently exposed via the ToothFairyAI SDK.
5
+ This module is reserved for future implementation when MCP server
6
+ discovery is added to the SDK.
7
+
8
+ For now, MCP servers should be configured manually via tfcode.json.
9
+ """
10
+
11
+ from pydantic import BaseModel
12
+
13
+ from tf_sync.config import TFConfig
14
+ from tf_sync.tools import SyncedTool, ToolType
15
+
16
+
17
+ class MCPServerSyncResult(BaseModel):
18
+ """Result of MCP server sync operation."""
19
+
20
+ success: bool
21
+ servers: list[SyncedTool] = []
22
+ error: str | None = None
23
+
24
+
25
+ def sync_mcp_servers(config: TFConfig) -> MCPServerSyncResult:
26
+ """
27
+ Sync MCP servers from ToothFairyAI workspace.
28
+
29
+ NOTE: Currently not supported. MCP servers are not exposed via the SDK.
30
+ Configure MCP servers manually in tfcode.json instead.
31
+
32
+ Args:
33
+ config: TFConfig instance
34
+
35
+ Returns:
36
+ MCPServerSyncResult with error message
37
+ """
38
+ return MCPServerSyncResult(
39
+ success=False,
40
+ error="MCP server sync not available via SDK. Configure MCP servers in tfcode.json.",
41
+ )
@@ -0,0 +1,282 @@
1
+ """
2
+ Tool sync module for tfcode.
3
+ Syncs tools from ToothFairyAI workspace using the official SDK.
4
+
5
+ SDK Structure:
6
+ - agent_functions: API Functions (with request_type)
7
+ - connections: Provider connections (openai, anthropic, etc.)
8
+ - agents: TF workspace agents
9
+ - prompts: Prompt templates (with available_to_agents mapping)
10
+ """
11
+
12
+ from typing import Any, Optional, List
13
+
14
+ from pydantic import BaseModel
15
+ from toothfairyai.types import AgentFunction
16
+
17
+ from tf_sync.config import TFConfig, ToolType, FunctionRequestType
18
+
19
+
20
+ class SyncedTool(BaseModel):
21
+ """A tool synced from ToothFairyAI workspace."""
22
+
23
+ id: str
24
+ name: str
25
+ description: Optional[str] = None
26
+ tool_type: ToolType
27
+
28
+ is_mcp_server: bool = False
29
+ is_agent_skill: bool = False
30
+ is_database_script: bool = False
31
+
32
+ request_type: Optional[FunctionRequestType] = None
33
+ url: Optional[str] = None
34
+ tools: list[str] = []
35
+
36
+ authorisation_type: Optional[str] = None
37
+
38
+ auth_via: str = "tf_proxy"
39
+
40
+ # Coder agent specific fields for prompting/model configuration
41
+ interpolation_string: Optional[str] = None
42
+ goals: Optional[str] = None
43
+ temperature: Optional[float] = None
44
+ max_tokens: Optional[int] = None
45
+ llm_base_model: Optional[str] = None
46
+ llm_provider: Optional[str] = None
47
+
48
+
49
+ class SyncedPrompt(BaseModel):
50
+ """A prompt template synced from ToothFairyAI workspace."""
51
+
52
+ id: str
53
+ label: str
54
+ interpolation_string: str
55
+ prompt_type: Optional[str] = None
56
+ available_to_agents: Optional[List[str]] = None
57
+ description: Optional[str] = None
58
+
59
+
60
+ class ToolSyncResult(BaseModel):
61
+ """Result of tool sync operation."""
62
+
63
+ success: bool
64
+ tools: list[SyncedTool] = []
65
+ prompts: list[SyncedPrompt] = []
66
+ by_type: dict[str, int] = {}
67
+ error: Optional[str] = None
68
+
69
+
70
+ def classify_tool(func: AgentFunction) -> ToolType:
71
+ """
72
+ Classify a tool based on its properties.
73
+
74
+ Types:
75
+ - AGENT_SKILL: is_agent_skill=True
76
+ - API_FUNCTION: has request_type
77
+
78
+ Args:
79
+ func: AgentFunction from TF SDK
80
+
81
+ Returns:
82
+ ToolType enum value
83
+ """
84
+ # Agent skills have is_agent_skill=True
85
+ if getattr(func, 'is_agent_skill', None) is True:
86
+ return ToolType.AGENT_SKILL
87
+
88
+ # All agent_functions with request_type are API Functions
89
+ if func.request_type:
90
+ return ToolType.API_FUNCTION
91
+
92
+ return ToolType.API_FUNCTION
93
+
94
+
95
+ def parse_function(func: AgentFunction) -> SyncedTool:
96
+ """
97
+ Parse AgentFunction from SDK into SyncedTool.
98
+
99
+ Args:
100
+ func: AgentFunction from TF SDK
101
+
102
+ Returns:
103
+ SyncedTool instance
104
+ """
105
+ tool_type = classify_tool(func)
106
+
107
+ request_type_enum = None
108
+ if func.request_type:
109
+ try:
110
+ request_type_enum = FunctionRequestType(func.request_type)
111
+ except ValueError:
112
+ pass
113
+
114
+ # API Functions may have user-provided auth (authorisation_type)
115
+ # or may use TF proxy
116
+ auth_via = "user_provided" if func.authorisation_type == "api_key" else "tf_proxy"
117
+
118
+ # Agent skills use skill script
119
+ if tool_type == ToolType.AGENT_SKILL:
120
+ auth_via = "tf_skill"
121
+
122
+ return SyncedTool(
123
+ id=func.id,
124
+ name=func.name,
125
+ description=func.description,
126
+ tool_type=tool_type,
127
+ request_type=request_type_enum,
128
+ url=func.url,
129
+ authorisation_type=func.authorisation_type,
130
+ auth_via=auth_via,
131
+ is_agent_skill=tool_type == ToolType.AGENT_SKILL,
132
+ )
133
+
134
+
135
+ def parse_agent(agent) -> SyncedTool:
136
+ """
137
+ Parse Agent from SDK into SyncedTool.
138
+
139
+ Coder agents (mode='coder') are CODER_AGENT type, not skills.
140
+
141
+ Args:
142
+ agent: Agent from TF SDK
143
+
144
+ Returns:
145
+ SyncedTool instance with full agent configuration
146
+ """
147
+ return SyncedTool(
148
+ id=agent.id,
149
+ name=agent.label or f"agent_{agent.id[:8]}",
150
+ description=agent.description,
151
+ tool_type=ToolType.CODER_AGENT,
152
+ is_agent_skill=False,
153
+ auth_via="tf_agent",
154
+ # Agent prompting configuration
155
+ interpolation_string=getattr(agent, 'interpolation_string', None),
156
+ goals=getattr(agent, 'goals', None),
157
+ # Agent model configuration
158
+ temperature=getattr(agent, 'temperature', None),
159
+ max_tokens=getattr(agent, 'max_tokens', None),
160
+ llm_base_model=getattr(agent, 'llm_base_model', None),
161
+ llm_provider=getattr(agent, 'llm_provider', None),
162
+ )
163
+
164
+
165
+ def parse_prompt(prompt) -> SyncedPrompt:
166
+ """
167
+ Parse Prompt from SDK into SyncedPrompt.
168
+
169
+ Args:
170
+ prompt: Prompt from TF SDK
171
+
172
+ Returns:
173
+ SyncedPrompt instance
174
+ """
175
+ return SyncedPrompt(
176
+ id=prompt.id,
177
+ label=prompt.label,
178
+ interpolation_string=prompt.interpolation_string,
179
+ prompt_type=getattr(prompt, 'prompt_type', None),
180
+ available_to_agents=getattr(prompt, 'available_to_agents', None),
181
+ description=getattr(prompt, 'description', None),
182
+ )
183
+
184
+
185
+ def sync_tools(config: TFConfig) -> ToolSyncResult:
186
+ """
187
+ Sync all tools from ToothFairyAI workspace using SDK.
188
+
189
+ Includes:
190
+ - Agent Functions (API Functions with request_type)
191
+ - Agent Skills (functions with is_agent_skill=True)
192
+ - Coder Agents (agents with mode='coder')
193
+ - Prompts (prompt templates with available_to_agents mapping)
194
+
195
+ Args:
196
+ config: TFConfig instance
197
+
198
+ Returns:
199
+ ToolSyncResult with synced tools and prompts
200
+ """
201
+ try:
202
+ client = config.get_client()
203
+
204
+ # Sync agent functions (API auto-paginates up to 5000)
205
+ func_result = client.agent_functions.list()
206
+ tools = [parse_function(f) for f in func_result.items]
207
+
208
+ # Sync coder agents (API auto-paginates up to 5000)
209
+ try:
210
+ agents_result = client.agents.list()
211
+ for agent in agents_result.items:
212
+ if getattr(agent, 'mode', None) == 'coder':
213
+ tools.append(parse_agent(agent))
214
+ except Exception:
215
+ pass
216
+
217
+ # Sync prompts (API auto-paginates up to 5000)
218
+ prompts = []
219
+ try:
220
+ prompts_result = client.prompts.list()
221
+ prompts = [parse_prompt(p) for p in prompts_result.items]
222
+ except Exception:
223
+ pass
224
+
225
+ by_type = {}
226
+ for tool in tools:
227
+ type_name = tool.tool_type.value
228
+ by_type[type_name] = by_type.get(type_name, 0) + 1
229
+
230
+ if prompts:
231
+ by_type['prompt'] = len(prompts)
232
+
233
+ return ToolSyncResult(
234
+ success=True,
235
+ tools=tools,
236
+ prompts=prompts,
237
+ by_type=by_type,
238
+ )
239
+
240
+ except Exception as e:
241
+ return ToolSyncResult(
242
+ success=False,
243
+ error=f"Sync failed: {str(e)}",
244
+ )
245
+
246
+
247
+ def sync_tools_by_type(
248
+ config: TFConfig,
249
+ tool_types: Optional[list[ToolType]] = None,
250
+ ) -> ToolSyncResult:
251
+ """
252
+ Sync tools of specific types from ToothFairyAI workspace.
253
+
254
+ Args:
255
+ config: TFConfig instance
256
+ tool_types: List of ToolType to sync (None = all)
257
+
258
+ Returns:
259
+ ToolSyncResult with filtered tools
260
+ """
261
+ result = sync_tools(config)
262
+
263
+ if not result.success or not tool_types:
264
+ return result
265
+
266
+ filtered = [t for t in result.tools if t.tool_type in tool_types]
267
+
268
+ by_type = {}
269
+ for tool in filtered:
270
+ type_name = tool.tool_type.value
271
+ by_type[type_name] = by_type.get(type_name, 0) + 1
272
+
273
+ return ToolSyncResult(
274
+ success=True,
275
+ tools=filtered,
276
+ by_type=by_type,
277
+ )
278
+
279
+
280
+ def sync_api_functions_only(config: TFConfig) -> ToolSyncResult:
281
+ """Sync only API Functions (has requestType)."""
282
+ return sync_tools_by_type(config, [ToolType.API_FUNCTION])