@toothfairyai/tfcode 1.2.8 → 1.3.0
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/README.md +191 -0
- package/bin/tfcode +0 -0
- package/package.json +13 -13
- package/python/tf_sync/__init__.py +21 -0
- package/python/tf_sync/agents.py +39 -0
- package/python/tf_sync/config.py +222 -0
- package/python/tf_sync/mcp.py +41 -0
- package/python/tf_sync/tools.py +282 -0
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# TF Code
|
|
2
|
+
|
|
3
|
+
**ToothFairyAI's AI coding agent** — a command-line interface that brings AI-powered coding assistance to your terminal with deep ToothFairyAI workspace integration.
|
|
4
|
+
|
|
5
|
+
## Agents
|
|
6
|
+
|
|
7
|
+
TF Code ships with three built-in agents:
|
|
8
|
+
|
|
9
|
+
| Agent | Purpose | Key Ability |
|
|
10
|
+
| --------------- | ----------------------------- | ----------------------------------------------------------- |
|
|
11
|
+
| **Build** | Execute and implement | Full read/write access, the default agent |
|
|
12
|
+
| **Plan** | Explore and plan | Read-only analysis, writes plans to `.tfcode/plans/` |
|
|
13
|
+
| **TF Engineer** | Manage ToothFairyAI workspace | Exclusive access to live MCP tools for workspace operations |
|
|
14
|
+
|
|
15
|
+
Switch between them at any time with **Tab** or the `--agent` flag.
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- **Node.js** 18+
|
|
20
|
+
- A modern terminal emulator (WezTerm, Alacritty, Kitty, Ghostty recommended)
|
|
21
|
+
- ToothFairyAI API credentials (from **Admin → TFCode & API Integration**)
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
npm install -g @toothfairyai/tfcode
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Verify:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
tfcode --version
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configure
|
|
36
|
+
|
|
37
|
+
Run the interactive setup to create a profile:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
tfcode setup
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This prompts for profile name, workspace ID, API key, and region. Alternatively, set environment variables:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
export TF_WORKSPACE_ID="your-workspace-id"
|
|
47
|
+
export TF_API_KEY="your-api-key"
|
|
48
|
+
export TF_REGION="au"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Validate:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
tfcode validate
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
### Interactive session
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
tfcode
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Single command
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
tfcode run "Explain this codebase"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Plan first, then build
|
|
72
|
+
|
|
73
|
+
Use **Tab** to switch to Plan mode, describe what you want, then switch back to Build to implement.
|
|
74
|
+
|
|
75
|
+
1. **Plan** — describe the feature, review the plan
|
|
76
|
+
2. **Build** — switch back and ask it to implement
|
|
77
|
+
|
|
78
|
+
### Workspace management
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
tfcode run --agent "TF Engineer" "Create a retriever agent called SupportBot"
|
|
82
|
+
tfcode run --agent "TF Engineer" "Search documents about refund policy"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Undo changes
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
/undo
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Profiles
|
|
92
|
+
|
|
93
|
+
Profiles store multiple sets of ToothFairyAI credentials so you can switch between workspaces instantly.
|
|
94
|
+
|
|
95
|
+
### Create
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
tfcode setup
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Non-interactive:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
tfcode setup --profile dev --workspace-id dev-ws-456 --api-key dev-key-xxx --region dev --name dev
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Switch
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
tfcode profile prod
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Use in commands
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
tfcode -p prod run "Deploy to production"
|
|
117
|
+
TF_PROFILE=prod tfcode run "Force prod workspace"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Regions
|
|
121
|
+
|
|
122
|
+
| Region | Base URL | Streaming URL |
|
|
123
|
+
| ------ | ------------------------ | ------------------------- |
|
|
124
|
+
| `dev` | `ai.toothfairylab.link` | `ais.toothfairylab.link` |
|
|
125
|
+
| `au` | `ai.toothfairyai.com` | `ais.toothfairyai.com` |
|
|
126
|
+
| `eu` | `ai.eu.toothfairyai.com` | `ais.eu.toothfairyai.com` |
|
|
127
|
+
| `us` | `ai.us.toothfairyai.com` | `ais.us.toothfairyai.com` |
|
|
128
|
+
|
|
129
|
+
## Environment Variables
|
|
130
|
+
|
|
131
|
+
| Variable | Description |
|
|
132
|
+
| ------------------------- | ----------------------------------------------- |
|
|
133
|
+
| `TF_WORKSPACE_ID` | ToothFairyAI workspace ID |
|
|
134
|
+
| `TF_API_KEY` | ToothFairyAI API key |
|
|
135
|
+
| `TF_REGION` | ToothFairyAI region: dev, au, eu, us |
|
|
136
|
+
| `TF_PROFILE` | Profile name to use (overrides default profile) |
|
|
137
|
+
| `TFCODE_SERVER_PASSWORD` | HTTP basic auth password for server mode |
|
|
138
|
+
| `TFCODE_SERVER_USERNAME` | HTTP basic auth username (default: tfcode) |
|
|
139
|
+
| `OPENCODE_CONFIG` | Path to custom config file |
|
|
140
|
+
| `OPENCODE_CONFIG_CONTENT` | Inline config content (highest priority) |
|
|
141
|
+
|
|
142
|
+
## CLI Commands
|
|
143
|
+
|
|
144
|
+
### Top-level
|
|
145
|
+
|
|
146
|
+
| Command | Description |
|
|
147
|
+
| --------------------- | ----------------------------------- |
|
|
148
|
+
| `tfcode` | Start interactive session |
|
|
149
|
+
| `tfcode run <prompt>` | Execute a single task and exit |
|
|
150
|
+
| `tfcode serve` | Start headless HTTP server |
|
|
151
|
+
| `tfcode web` | Start server and open web interface |
|
|
152
|
+
| `tfcode attach <url>` | Attach TUI to a running server |
|
|
153
|
+
|
|
154
|
+
### Setup & credentials
|
|
155
|
+
|
|
156
|
+
| Command | Description |
|
|
157
|
+
| -------------------------- | ------------------------------------------------------ |
|
|
158
|
+
| `tfcode setup` | Interactive credential setup (creates a profile) |
|
|
159
|
+
| `tfcode validate` | Validate credentials (`--profile <name>` for specific) |
|
|
160
|
+
| `tfcode profile` | Show current profile and list all |
|
|
161
|
+
| `tfcode profile <name>` | Switch to a different profile |
|
|
162
|
+
| `tfcode sync` | Sync workspace tools metadata |
|
|
163
|
+
| `tfcode providers list` | List available providers |
|
|
164
|
+
| `tfcode models [provider]` | List available models (optional provider filter) |
|
|
165
|
+
| `tfcode models --verbose` | List models with metadata |
|
|
166
|
+
| `tfcode models --refresh` | Refresh the models cache |
|
|
167
|
+
|
|
168
|
+
### MCP
|
|
169
|
+
|
|
170
|
+
| Command | Description |
|
|
171
|
+
| ------------------- | ---------------------------------- |
|
|
172
|
+
| `tfcode mcp list` | List MCP servers and tools |
|
|
173
|
+
| `tfcode mcp add` | Add a local MCP server |
|
|
174
|
+
| `tfcode mcp auth` | Authenticate with OAuth MCP server |
|
|
175
|
+
| `tfcode mcp logout` | Logout from MCP server |
|
|
176
|
+
|
|
177
|
+
### Chat commands (interactive)
|
|
178
|
+
|
|
179
|
+
| Command | Description |
|
|
180
|
+
| ------------------------- | --------------------------------------- |
|
|
181
|
+
| `/compact [instructions]` | Compact context with preservation hints |
|
|
182
|
+
| `/help` | Show available commands |
|
|
183
|
+
| `/undo` | Undo last changes |
|
|
184
|
+
|
|
185
|
+
## Documentation
|
|
186
|
+
|
|
187
|
+
Full documentation at [https://docs.toothfairyai.com/tfcode/intro](https://docs.toothfairyai.com/tfcode/intro)
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT
|
package/bin/tfcode
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toothfairyai/tfcode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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-
|
|
13
|
-
"@toothfairyai/tfcode-
|
|
14
|
-
"@toothfairyai/tfcode-
|
|
15
|
-
"@toothfairyai/tfcode-
|
|
16
|
-
"@toothfairyai/tfcode-linux-
|
|
17
|
-
"@toothfairyai/tfcode-
|
|
18
|
-
"@toothfairyai/tfcode-linux-
|
|
19
|
-
"@toothfairyai/tfcode-
|
|
20
|
-
"@toothfairyai/tfcode-linux-x64
|
|
21
|
-
"@toothfairyai/tfcode-
|
|
22
|
-
"@toothfairyai/tfcode-
|
|
23
|
-
"@toothfairyai/tfcode-
|
|
12
|
+
"@toothfairyai/tfcode-linux-arm64": "1.3.0",
|
|
13
|
+
"@toothfairyai/tfcode-windows-x64": "1.3.0",
|
|
14
|
+
"@toothfairyai/tfcode-linux-x64-baseline-musl": "1.3.0",
|
|
15
|
+
"@toothfairyai/tfcode-darwin-x64-baseline": "1.3.0",
|
|
16
|
+
"@toothfairyai/tfcode-linux-x64-musl": "1.3.0",
|
|
17
|
+
"@toothfairyai/tfcode-windows-x64-baseline": "1.3.0",
|
|
18
|
+
"@toothfairyai/tfcode-linux-arm64-musl": "1.3.0",
|
|
19
|
+
"@toothfairyai/tfcode-windows-arm64": "1.3.0",
|
|
20
|
+
"@toothfairyai/tfcode-linux-x64": "1.3.0",
|
|
21
|
+
"@toothfairyai/tfcode-darwin-x64": "1.3.0",
|
|
22
|
+
"@toothfairyai/tfcode-linux-x64-baseline": "1.3.0",
|
|
23
|
+
"@toothfairyai/tfcode-darwin-arm64": "1.3.0"
|
|
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,222 @@
|
|
|
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
|
+
user_agent="tfcode",
|
|
109
|
+
)
|
|
110
|
+
return self._client
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def mcp_sse_url(self) -> str:
|
|
114
|
+
"""Get the MCP SSE endpoint URL for this region."""
|
|
115
|
+
return get_region_urls(self.region)["mcp_url"]
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def mcp_proxy_url(self) -> str:
|
|
119
|
+
"""Get the MCP proxy URL for this region."""
|
|
120
|
+
return get_region_urls(self.region)["mcp_proxy_url"]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CredentialValidationResult(BaseModel):
|
|
124
|
+
"""Result of credential validation."""
|
|
125
|
+
success: bool
|
|
126
|
+
workspace_id: Optional[str] = None
|
|
127
|
+
workspace_name: Optional[str] = None
|
|
128
|
+
error: Optional[str] = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def load_config(
|
|
132
|
+
workspace_id: Optional[str] = None,
|
|
133
|
+
api_key: Optional[str] = None,
|
|
134
|
+
region: Optional[Region] = None,
|
|
135
|
+
) -> TFConfig:
|
|
136
|
+
"""
|
|
137
|
+
Load ToothFairyAI configuration from environment or parameters.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
workspace_id: Workspace UUID (defaults to TF_WORKSPACE_ID env var)
|
|
141
|
+
api_key: API key (defaults to TF_API_KEY env var)
|
|
142
|
+
region: Region (defaults to TF_REGION env var or 'au')
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
TFConfig instance
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ValueError: If required configuration is missing
|
|
149
|
+
"""
|
|
150
|
+
ws_id = workspace_id or os.environ.get("TF_WORKSPACE_ID")
|
|
151
|
+
key = api_key or os.environ.get("TF_API_KEY")
|
|
152
|
+
|
|
153
|
+
# Parse region from env or use provided/default
|
|
154
|
+
region_str = os.environ.get("TF_REGION", "au")
|
|
155
|
+
reg = region or Region(region_str)
|
|
156
|
+
|
|
157
|
+
if not ws_id:
|
|
158
|
+
raise ValueError("TF_WORKSPACE_ID not set. Set environment variable or pass workspace_id.")
|
|
159
|
+
if not key:
|
|
160
|
+
raise ValueError("TF_API_KEY not set. Set environment variable or pass api_key.")
|
|
161
|
+
|
|
162
|
+
return TFConfig(
|
|
163
|
+
workspace_id=ws_id,
|
|
164
|
+
api_key=SecretStr(key),
|
|
165
|
+
region=reg,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def validate_credentials(config: TFConfig) -> CredentialValidationResult:
|
|
170
|
+
"""
|
|
171
|
+
Validate ToothFairyAI credentials using the SDK.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
config: TFConfig instance
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
CredentialValidationResult indicating success or failure
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
client = config.get_client()
|
|
181
|
+
|
|
182
|
+
# Test connection by listing chats (lightweight operation)
|
|
183
|
+
if client.test_connection():
|
|
184
|
+
return CredentialValidationResult(
|
|
185
|
+
success=True,
|
|
186
|
+
workspace_id=config.workspace_id,
|
|
187
|
+
workspace_name="Connected",
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
return CredentialValidationResult(
|
|
191
|
+
success=False,
|
|
192
|
+
error="Connection test failed. Check credentials and region.",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
except ToothFairyError as e:
|
|
196
|
+
error_msg = str(e)
|
|
197
|
+
|
|
198
|
+
if "401" in error_msg or "Unauthorized" in error_msg:
|
|
199
|
+
return CredentialValidationResult(
|
|
200
|
+
success=False,
|
|
201
|
+
error="Invalid API key. Check TF_API_KEY environment variable.",
|
|
202
|
+
)
|
|
203
|
+
elif "403" in error_msg or "Forbidden" in error_msg:
|
|
204
|
+
return CredentialValidationResult(
|
|
205
|
+
success=False,
|
|
206
|
+
error="API access not allowed. Business or Enterprise subscription required.",
|
|
207
|
+
)
|
|
208
|
+
elif "404" in error_msg or "Not Found" in error_msg:
|
|
209
|
+
return CredentialValidationResult(
|
|
210
|
+
success=False,
|
|
211
|
+
error="Workspace not found. Check TF_WORKSPACE_ID environment variable.",
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
return CredentialValidationResult(
|
|
215
|
+
success=False,
|
|
216
|
+
error=f"API error: {error_msg}",
|
|
217
|
+
)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
return CredentialValidationResult(
|
|
220
|
+
success=False,
|
|
221
|
+
error=f"Unexpected error: {str(e)}",
|
|
222
|
+
)
|
|
@@ -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])
|