@toothfairyai/tfcode 1.0.38 → 1.0.40
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 +146 -0
- package/bin/tfcode +0 -0
- package/bin/tfcode.js +515 -13
- package/package.json +156 -25
- package/python/tf_sync/__init__.py +0 -0
- package/python/tf_sync/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/tf_sync/__pycache__/agents.cpython-313.pyc +0 -0
- package/python/tf_sync/__pycache__/config.cpython-313.pyc +0 -0
- package/python/tf_sync/__pycache__/mcp.cpython-313.pyc +0 -0
- package/python/tf_sync/__pycache__/tools.cpython-313.pyc +0 -0
- package/python/tf_sync/agents.py +0 -0
- package/python/tf_sync/config.py +0 -0
- package/python/tf_sync/mcp.py +0 -0
- package/python/tf_sync/tools.py +0 -0
- package/LICENSE +0 -21
- package/postinstall.mjs +0 -310
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# tfcode
|
|
2
|
+
|
|
3
|
+
**ToothFairyAI's official AI coding agent** - A terminal-based coding assistant with seamless integration to your ToothFairyAI workspace.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🤖 **AI-Powered Coding** - Intelligent code generation, refactoring, and debugging
|
|
8
|
+
- 🔌 **Tool Integration** - Sync and use tools from your ToothFairyAI workspace
|
|
9
|
+
- 💻 **Terminal-Based** - Fast, lightweight, runs in your terminal
|
|
10
|
+
- 🔐 **Secure** - Credentials stay in ToothFairyAI, route via TF Proxy
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- **Python 3.10+** - Required for ToothFairyAI integration
|
|
15
|
+
- **Node.js 18+** - Required to run tfcode
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g @toothfairyai/tfcode@beta
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The installer will:
|
|
24
|
+
- Check Python is installed
|
|
25
|
+
- Install the ToothFairyAI Python SDK automatically
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### Option A: Interactive Setup (Recommended)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Run interactive setup
|
|
33
|
+
tfcode setup
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This will guide you through entering your credentials step by step:
|
|
37
|
+
1. Enter your Workspace ID
|
|
38
|
+
2. Enter your API Key (hidden as you type)
|
|
39
|
+
3. Select your region
|
|
40
|
+
4. Validate and sync automatically
|
|
41
|
+
|
|
42
|
+
### Option B: Manual Setup
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 1. Set your ToothFairyAI credentials
|
|
46
|
+
export TF_WORKSPACE_ID="your-workspace-id"
|
|
47
|
+
export TF_API_KEY="your-api-key"
|
|
48
|
+
export TF_REGION="au"
|
|
49
|
+
|
|
50
|
+
# 2. Validate your credentials
|
|
51
|
+
tfcode validate
|
|
52
|
+
|
|
53
|
+
# 3. Sync tools from your workspace
|
|
54
|
+
tfcode sync
|
|
55
|
+
|
|
56
|
+
# 4. List your tools
|
|
57
|
+
tfcode tools list
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
| Command | Description |
|
|
63
|
+
|---------|-------------|
|
|
64
|
+
| `tfcode setup` | **Interactive credential setup** |
|
|
65
|
+
| `tfcode quickstart` | Show quick start guide |
|
|
66
|
+
| `tfcode validate` | Test your credentials |
|
|
67
|
+
| `tfcode sync` | Sync tools from workspace |
|
|
68
|
+
| `tfcode tools list` | List synced tools |
|
|
69
|
+
| `tfcode tools list --type api_function` | Filter by type |
|
|
70
|
+
|
|
71
|
+
## Regions
|
|
72
|
+
|
|
73
|
+
| Region | URL | Use Case |
|
|
74
|
+
|--------|-----|----------|
|
|
75
|
+
| `dev` | api.toothfairylab.link | Development/Testing |
|
|
76
|
+
| `au` | api.toothfairyai.com | Australia (Default) |
|
|
77
|
+
| `eu` | api.eu.toothfairyai.com | Europe |
|
|
78
|
+
| `us` | api.us.toothfairyai.com | United States |
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
Credentials are stored in `~/.tfcode/config.json`:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"workspace_id": "your-workspace-id",
|
|
87
|
+
"api_key": "your-api-key",
|
|
88
|
+
"region": "au"
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
You can also set credentials via environment variables (takes priority over config file):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
export TF_WORKSPACE_ID="your-workspace-id"
|
|
96
|
+
export TF_API_KEY="your-api-key"
|
|
97
|
+
export TF_REGION="au"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Getting Your Credentials
|
|
101
|
+
|
|
102
|
+
1. Log in to [ToothFairyAI](https://app.toothfairyai.com)
|
|
103
|
+
2. Go to **Settings** → **API Keys**
|
|
104
|
+
3. Copy your **Workspace ID** and **API Key**
|
|
105
|
+
|
|
106
|
+
## Troubleshooting
|
|
107
|
+
|
|
108
|
+
### "Python 3.10+ is required but not found"
|
|
109
|
+
|
|
110
|
+
Install Python:
|
|
111
|
+
- **macOS**: `brew install python@3.12`
|
|
112
|
+
- **Windows**: Download from https://python.org/downloads
|
|
113
|
+
- **Linux**: `sudo apt install python3.12`
|
|
114
|
+
|
|
115
|
+
### "Failed to validate: Invalid API key"
|
|
116
|
+
|
|
117
|
+
- Check your API key is correct
|
|
118
|
+
- Generate a new key in ToothFairyAI Settings → API Keys
|
|
119
|
+
- Run `tfcode setup` to re-enter credentials
|
|
120
|
+
|
|
121
|
+
### "Failed to validate: Connection test failed"
|
|
122
|
+
|
|
123
|
+
Try a different region:
|
|
124
|
+
- Run `tfcode setup` and select a different region
|
|
125
|
+
- Or set `TF_REGION="eu"` or `TF_REGION="us"`
|
|
126
|
+
|
|
127
|
+
### "No credentials found"
|
|
128
|
+
|
|
129
|
+
Run interactive setup:
|
|
130
|
+
```bash
|
|
131
|
+
tfcode setup
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Documentation
|
|
135
|
+
|
|
136
|
+
- **Full Guide**: https://toothfairyai.com/developers/tfcode
|
|
137
|
+
- **API Docs**: https://apidocs.toothfairyai.com
|
|
138
|
+
- **Issues**: https://github.com/ToothFairyAI/tfcode/issues
|
|
139
|
+
|
|
140
|
+
## Credits
|
|
141
|
+
|
|
142
|
+
tfcode is based on [opencode](https://github.com/anomalyco/opencode) by [anomalyco](https://github.com/anomalyco).
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
package/bin/tfcode
CHANGED
|
Binary file
|
package/bin/tfcode.js
CHANGED
|
@@ -1,19 +1,521 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const { spawn } = require('child_process')
|
|
3
|
-
const path = require('path')
|
|
4
|
-
const fs = require('fs')
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { spawn } from "child_process"
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs"
|
|
5
|
+
import { join, dirname } from "path"
|
|
6
|
+
import { homedir } from "os"
|
|
7
|
+
import * as readline from "readline"
|
|
8
|
+
import { fileURLToPath } from "url"
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = dirname(__filename)
|
|
12
|
+
|
|
13
|
+
const TFCODE_DIR = join(homedir(), ".tfcode")
|
|
14
|
+
const TOOLS_FILE = join(TFCODE_DIR, "tools.json")
|
|
15
|
+
const CREDENTIALS_FILE = join(TFCODE_DIR, "credentials.json")
|
|
16
|
+
const CONFIG_FILE = join(TFCODE_DIR, "config.json")
|
|
17
|
+
|
|
18
|
+
const COLORS = {
|
|
19
|
+
reset: "\x1b[0m",
|
|
20
|
+
bold: "\x1b[1m",
|
|
21
|
+
green: "\x1b[32m",
|
|
22
|
+
red: "\x1b[31m",
|
|
23
|
+
cyan: "\x1b[36m",
|
|
24
|
+
dim: "\x1b[90m",
|
|
25
|
+
yellow: "\x1b[33m",
|
|
26
|
+
magenta: "\x1b[35m",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function log(msg) {
|
|
30
|
+
console.log(msg)
|
|
31
|
+
}
|
|
32
|
+
function success(msg) {
|
|
33
|
+
console.log(`${COLORS.green}✓${COLORS.reset} ${msg}`)
|
|
34
|
+
}
|
|
35
|
+
function error(msg) {
|
|
36
|
+
console.error(`${COLORS.red}✗${COLORS.reset} ${msg}`)
|
|
37
|
+
}
|
|
38
|
+
function info(msg) {
|
|
39
|
+
console.log(`${COLORS.cyan}ℹ${COLORS.reset} ${msg}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureConfigDir() {
|
|
43
|
+
if (!existsSync(TFCODE_DIR)) mkdirSync(TFCODE_DIR, { recursive: true })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadConfig() {
|
|
47
|
+
const envConfig = {
|
|
48
|
+
workspace_id: process.env.TF_WORKSPACE_ID,
|
|
49
|
+
api_key: process.env.TF_API_KEY,
|
|
50
|
+
region: process.env.TF_REGION,
|
|
51
|
+
}
|
|
52
|
+
if (envConfig.workspace_id && envConfig.api_key) return envConfig
|
|
53
|
+
if (existsSync(CONFIG_FILE)) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function saveConfig(config) {
|
|
62
|
+
ensureConfigDir()
|
|
63
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runPythonSync(method, config = null) {
|
|
67
|
+
const wsId = config?.workspace_id || process.env.TF_WORKSPACE_ID || ""
|
|
68
|
+
const apiKey = config?.api_key || process.env.TF_API_KEY || ""
|
|
69
|
+
const region = config?.region || process.env.TF_REGION || "au"
|
|
70
|
+
|
|
71
|
+
// Add embedded python path to sys.path
|
|
72
|
+
const embeddedPythonPath = join(__dirname, "..", "python")
|
|
73
|
+
|
|
74
|
+
const pythonCode = `
|
|
75
|
+
import json, sys, os
|
|
76
|
+
try:
|
|
77
|
+
# Add embedded tf_sync module path
|
|
78
|
+
sys.path.insert(0, "${embeddedPythonPath}")
|
|
79
|
+
os.environ["TF_WORKSPACE_ID"] = "${wsId}"
|
|
80
|
+
os.environ["TF_API_KEY"] = "${apiKey}"
|
|
81
|
+
os.environ["TF_REGION"] = "${region}"
|
|
82
|
+
from tf_sync.config import load_config, validate_credentials, Region
|
|
83
|
+
from tf_sync.tools import sync_tools
|
|
84
|
+
from tf_sync.config import get_region_urls
|
|
85
|
+
|
|
86
|
+
method = "${method}"
|
|
87
|
+
if method == "validate":
|
|
88
|
+
config = load_config()
|
|
89
|
+
result = validate_credentials(config)
|
|
90
|
+
urls = get_region_urls(config.region)
|
|
91
|
+
print(json.dumps({"success": result.success, "workspace_id": result.workspace_id, "workspace_name": result.workspace_name, "error": result.error, "base_url": urls["base_url"]}))
|
|
92
|
+
elif method == "sync":
|
|
93
|
+
config = load_config()
|
|
94
|
+
result = sync_tools(config)
|
|
95
|
+
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]
|
|
96
|
+
print(json.dumps({"success": result.success, "tools": tools_data, "by_type": result.by_type, "error": result.error}))
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print(json.dumps({"success": False, "error": str(e)}))
|
|
99
|
+
`
|
|
100
|
+
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const proc = spawn(process.env.TFCODE_PYTHON_PATH || "python3", ["-c", pythonCode], { env: { ...process.env } })
|
|
103
|
+
let stdout = "",
|
|
104
|
+
stderr = ""
|
|
105
|
+
proc.stdout.on("data", (d) => (stdout += d))
|
|
106
|
+
proc.stderr.on("data", (d) => (stderr += d))
|
|
107
|
+
proc.on("close", (code) => {
|
|
108
|
+
if (code !== 0 && !stdout) reject(new Error(`Python failed: ${stderr}`))
|
|
109
|
+
else
|
|
110
|
+
try {
|
|
111
|
+
resolve(JSON.parse(stdout.trim()))
|
|
112
|
+
} catch (e) {
|
|
113
|
+
reject(new Error(`Parse error: ${stdout}`))
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
proc.on("error", reject)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function loadCachedTools() {
|
|
121
|
+
if (!existsSync(TOOLS_FILE)) return null
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(readFileSync(TOOLS_FILE, "utf-8"))
|
|
124
|
+
} catch {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function saveToolsCache(tools) {
|
|
130
|
+
ensureConfigDir()
|
|
131
|
+
writeFileSync(TOOLS_FILE, JSON.stringify(tools, null, 2))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function question(prompt) {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
137
|
+
rl.question(prompt, (answer) => {
|
|
138
|
+
rl.close()
|
|
139
|
+
resolve(answer.trim())
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function select(prompt, options) {
|
|
145
|
+
log("")
|
|
146
|
+
log(prompt)
|
|
147
|
+
log("")
|
|
148
|
+
options.forEach((opt, i) => log(` ${COLORS.cyan}${i + 1}.${COLORS.reset} ${opt}`))
|
|
149
|
+
log("")
|
|
150
|
+
const answer = await question("Select (1-" + options.length + "): ")
|
|
151
|
+
const idx = parseInt(answer) - 1
|
|
152
|
+
return idx >= 0 && idx < options.length ? idx : 0
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function interactiveSetup() {
|
|
156
|
+
log("")
|
|
157
|
+
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
|
158
|
+
log(`${COLORS.bold}${COLORS.magenta} tfcode Setup${COLORS.reset}`)
|
|
159
|
+
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
|
160
|
+
log("")
|
|
161
|
+
log("This will guide you through setting up your ToothFairyAI credentials.")
|
|
162
|
+
log("")
|
|
163
|
+
log(`${COLORS.dim}You can find your credentials at:${COLORS.reset}`)
|
|
164
|
+
log(`${COLORS.dim} https://app.toothfairyai.com → Settings → API Keys${COLORS.reset}`)
|
|
165
|
+
log("")
|
|
166
|
+
|
|
167
|
+
log(`${COLORS.bold}Step 1: Workspace ID${COLORS.reset}`)
|
|
168
|
+
log(`${COLORS.dim}This is your workspace UUID${COLORS.reset}`)
|
|
169
|
+
log("")
|
|
170
|
+
const workspaceId = await question("Enter your Workspace ID: ")
|
|
171
|
+
if (!workspaceId) {
|
|
172
|
+
error("Workspace ID is required")
|
|
173
|
+
process.exit(1)
|
|
174
|
+
}
|
|
175
|
+
log("")
|
|
176
|
+
|
|
177
|
+
log(`${COLORS.bold}Step 2: API Key${COLORS.reset}`)
|
|
178
|
+
log(`${COLORS.dim}Paste or type your API key${COLORS.reset}`)
|
|
179
|
+
log("")
|
|
180
|
+
const apiKey = await question("Enter your API Key: ")
|
|
181
|
+
if (!apiKey) {
|
|
182
|
+
error("API Key is required")
|
|
183
|
+
process.exit(1)
|
|
184
|
+
}
|
|
185
|
+
log("")
|
|
186
|
+
|
|
187
|
+
log(`${COLORS.bold}Step 3: Region${COLORS.reset}`)
|
|
188
|
+
const regions = ["dev (Development)", "au (Australia)", "eu (Europe)", "us (United States)"]
|
|
189
|
+
const regionIdx = await select("Select your region:", regions)
|
|
190
|
+
const region = ["dev", "au", "eu", "us"][regionIdx]
|
|
191
|
+
|
|
192
|
+
log("")
|
|
193
|
+
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
|
194
|
+
log("")
|
|
195
|
+
log(`${COLORS.bold}Summary:${COLORS.reset}`)
|
|
196
|
+
log(` Workspace ID: ${workspaceId}`)
|
|
197
|
+
log(` API Key: ***${apiKey.slice(-4)}`)
|
|
198
|
+
log(` Region: ${region}`)
|
|
199
|
+
log("")
|
|
200
|
+
|
|
201
|
+
const confirm = await question("Save these credentials? (Y/n): ")
|
|
202
|
+
if (confirm.toLowerCase() === "n" || confirm.toLowerCase() === "no") {
|
|
203
|
+
log("Setup cancelled.")
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const config = { workspace_id: workspaceId, api_key: apiKey, region }
|
|
208
|
+
saveConfig(config)
|
|
209
|
+
success("Credentials saved to ~/.tfcode/config.json")
|
|
210
|
+
log("")
|
|
211
|
+
|
|
212
|
+
const testNow = await question("Validate credentials now? (Y/n): ")
|
|
213
|
+
if (testNow.toLowerCase() === "n" || testNow.toLowerCase() === "no") return
|
|
214
|
+
|
|
215
|
+
log("")
|
|
216
|
+
info("Validating credentials...")
|
|
217
|
+
log("")
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const result = await runPythonSync("validate", config)
|
|
221
|
+
if (result.success) {
|
|
222
|
+
success("Credentials valid!")
|
|
223
|
+
log(` API URL: ${result.base_url}`)
|
|
224
|
+
log(` Workspace ID: ${result.workspace_id}`)
|
|
225
|
+
log("")
|
|
226
|
+
|
|
227
|
+
const syncNow = await question("Sync tools now? (Y/n): ")
|
|
228
|
+
if (syncNow.toLowerCase() === "n" || syncNow.toLowerCase() === "no") return
|
|
229
|
+
|
|
230
|
+
log("")
|
|
231
|
+
info("Syncing tools...")
|
|
232
|
+
log("")
|
|
233
|
+
|
|
234
|
+
const syncResult = await runPythonSync("sync", config)
|
|
235
|
+
if (syncResult.success) {
|
|
236
|
+
saveToolsCache(syncResult)
|
|
237
|
+
success(`Synced ${syncResult.tools.length} tools`)
|
|
238
|
+
if (syncResult.by_type && Object.keys(syncResult.by_type).length > 0) {
|
|
239
|
+
log("")
|
|
240
|
+
log("By type:")
|
|
241
|
+
for (const [type, count] of Object.entries(syncResult.by_type)) log(` ${type}: ${count}`)
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
error(`Sync failed: ${syncResult.error}`)
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
error(`Validation failed: ${result.error}`)
|
|
248
|
+
}
|
|
249
|
+
} catch (e) {
|
|
250
|
+
error(`Failed: ${e.message}`)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
log("")
|
|
254
|
+
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
|
255
|
+
log(`${COLORS.bold}${COLORS.green} Setup Complete!${COLORS.reset}`)
|
|
256
|
+
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
|
257
|
+
log("")
|
|
258
|
+
log("Commands:")
|
|
259
|
+
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Check credentials`)
|
|
260
|
+
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools`)
|
|
261
|
+
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List tools`)
|
|
262
|
+
log("")
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function showHelp() {
|
|
266
|
+
log("")
|
|
267
|
+
log(`${COLORS.bold}tfcode${COLORS.reset} - ToothFairyAI's AI coding agent`)
|
|
268
|
+
log("")
|
|
269
|
+
log("Commands:")
|
|
270
|
+
log(` ${COLORS.cyan}tfcode setup${COLORS.reset} Interactive credential setup`)
|
|
271
|
+
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Test credentials`)
|
|
272
|
+
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools from workspace`)
|
|
273
|
+
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List synced tools`)
|
|
274
|
+
log(` ${COLORS.cyan}tfcode test-agent <id>${COLORS.reset} Test agent prompt injection`)
|
|
275
|
+
log(` ${COLORS.cyan}tfcode debug${COLORS.reset} Show debug info`)
|
|
276
|
+
log(` ${COLORS.cyan}tfcode --help${COLORS.reset} Show this help`)
|
|
277
|
+
log(` ${COLORS.cyan}tfcode --version${COLORS.reset} Show version`)
|
|
278
|
+
log("")
|
|
279
|
+
log(`${COLORS.dim}For full TUI, run from source:${COLORS.reset}`)
|
|
280
|
+
log(`${COLORS.dim} bun run packages/tfcode/src/index.ts${COLORS.reset}`)
|
|
281
|
+
log("")
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function showDebugInfo() {
|
|
285
|
+
log("")
|
|
286
|
+
log(`${COLORS.bold}Debug Information${COLORS.reset}`)
|
|
287
|
+
log("")
|
|
288
|
+
log(` ${COLORS.bold}TFCODE_DIR:${COLORS.reset} ${TFCODE_DIR}`)
|
|
289
|
+
log(` ${COLORS.bold}TOOLS_FILE:${COLORS.reset} ${TOOLS_FILE}`)
|
|
290
|
+
log(` ${COLORS.bold}CONFIG_FILE:${COLORS.reset} ${CONFIG_FILE}`)
|
|
291
|
+
log(` ${COLORS.bold}CREDENTIALS_FILE:${COLORS.reset} ${CREDENTIALS_FILE}`)
|
|
292
|
+
log("")
|
|
293
|
+
log(` ${COLORS.bold}tools.json exists:${COLORS.reset} ${existsSync(TOOLS_FILE)}`)
|
|
294
|
+
if (existsSync(TOOLS_FILE)) {
|
|
295
|
+
const tools = loadCachedTools()
|
|
296
|
+
log(` ${COLORS.bold}tools.json valid:${COLORS.reset} ${tools?.success ?? false}`)
|
|
297
|
+
log(` ${COLORS.bold}tools count:${COLORS.reset} ${tools?.tools?.length ?? 0}`)
|
|
298
|
+
const coderAgents = tools?.tools?.filter((t) => t.tool_type === "coder_agent") ?? []
|
|
299
|
+
log(` ${COLORS.bold}coder_agent count:${COLORS.reset} ${coderAgents.length}`)
|
|
300
|
+
if (coderAgents.length > 0) {
|
|
301
|
+
log("")
|
|
302
|
+
log(` ${COLORS.bold}Coder Agents:${COLORS.reset}`)
|
|
303
|
+
coderAgents.forEach((a) => {
|
|
304
|
+
log(` - ${a.name} (id: ${a.id})`)
|
|
305
|
+
log(` interpolation_string: ${a.interpolation_string ? "YES" : "NO"}`)
|
|
306
|
+
log(` goals: ${a.goals ? "YES" : "NO"}`)
|
|
307
|
+
log(` llm_provider: ${a.llm_provider ?? "(null)"}`)
|
|
308
|
+
log(` llm_base_model: ${a.llm_base_model ?? "(null)"}`)
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
log("")
|
|
12
313
|
}
|
|
13
314
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
315
|
+
function testAgentPrompt(agentId) {
|
|
316
|
+
const tools = loadCachedTools()
|
|
317
|
+
if (!tools?.success) {
|
|
318
|
+
error("No tools. Run: tfcode sync")
|
|
319
|
+
process.exit(1)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const agent = tools.tools.find((t) => t.id === agentId || t.name === agentId)
|
|
323
|
+
if (!agent) {
|
|
324
|
+
error(`Agent not found: ${agentId}`)
|
|
325
|
+
log("")
|
|
326
|
+
log("Available coder agents:")
|
|
327
|
+
tools.tools
|
|
328
|
+
.filter((t) => t.tool_type === "coder_agent")
|
|
329
|
+
.forEach((t) => {
|
|
330
|
+
log(` ${COLORS.cyan}${t.id}${COLORS.reset} - ${t.name}`)
|
|
331
|
+
})
|
|
332
|
+
process.exit(1)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
log("")
|
|
336
|
+
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
|
337
|
+
log(`${COLORS.bold}${COLORS.magenta} Agent Data from tools.json${COLORS.reset}`)
|
|
338
|
+
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
|
339
|
+
log("")
|
|
340
|
+
log(` ${COLORS.bold}id:${COLORS.reset} ${agent.id}`)
|
|
341
|
+
log(` ${COLORS.bold}name:${COLORS.reset} ${agent.name}`)
|
|
342
|
+
log(` ${COLORS.bold}description:${COLORS.reset} ${agent.description || "(none)"}`)
|
|
343
|
+
log(` ${COLORS.bold}tool_type:${COLORS.reset} ${agent.tool_type}`)
|
|
344
|
+
log(` ${COLORS.bold}auth_via:${COLORS.reset} ${agent.auth_via}`)
|
|
345
|
+
log("")
|
|
346
|
+
log(` ${COLORS.bold}interpolation_string:${COLORS.reset}`)
|
|
347
|
+
if (agent.interpolation_string) {
|
|
348
|
+
log(` ${agent.interpolation_string.substring(0, 200)}${agent.interpolation_string.length > 200 ? "..." : ""}`)
|
|
349
|
+
} else {
|
|
350
|
+
log(` ${COLORS.dim}(none)${COLORS.reset}`)
|
|
351
|
+
}
|
|
352
|
+
log("")
|
|
353
|
+
log(` ${COLORS.bold}goals:${COLORS.reset}`)
|
|
354
|
+
if (agent.goals) {
|
|
355
|
+
log(` ${agent.goals.substring(0, 200)}${agent.goals.length > 200 ? "..." : ""}`)
|
|
356
|
+
} else {
|
|
357
|
+
log(` ${COLORS.dim}(none)${COLORS.reset}`)
|
|
358
|
+
}
|
|
359
|
+
log("")
|
|
360
|
+
log(` ${COLORS.bold}temperature:${COLORS.reset} ${agent.temperature ?? "(none)"}`)
|
|
361
|
+
log(` ${COLORS.bold}max_tokens:${COLORS.reset} ${agent.max_tokens ?? "(none)"}`)
|
|
362
|
+
log(` ${COLORS.bold}llm_base_model:${COLORS.reset} ${agent.llm_base_model ?? "(none)"}`)
|
|
363
|
+
log(` ${COLORS.bold}llm_provider:${COLORS.reset} ${agent.llm_provider ?? "(none)"}`)
|
|
364
|
+
log("")
|
|
365
|
+
|
|
366
|
+
// Build highlighted instructions
|
|
367
|
+
const isTFProvider = !agent.llm_provider || agent.llm_provider === "toothfairyai" || agent.llm_provider === "tf"
|
|
368
|
+
|
|
369
|
+
const hasPrompt = agent.interpolation_string && agent.interpolation_string.trim().length > 0
|
|
370
|
+
const hasGoals = agent.goals && agent.goals.trim().length > 0
|
|
371
|
+
|
|
372
|
+
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
|
373
|
+
log(`${COLORS.bold}${COLORS.magenta} Model Mapping${COLORS.reset}`)
|
|
374
|
+
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
|
375
|
+
log("")
|
|
376
|
+
log(` ${COLORS.bold}isTFProvider:${COLORS.reset} ${isTFProvider}`)
|
|
377
|
+
log(
|
|
378
|
+
` ${COLORS.bold}mapped model:${COLORS.reset} ${isTFProvider && agent.llm_base_model ? `toothfairyai/${agent.llm_base_model}` : "(no mapping)"}`,
|
|
379
|
+
)
|
|
380
|
+
log("")
|
|
381
|
+
|
|
382
|
+
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
|
383
|
+
log(`${COLORS.bold}${COLORS.magenta} Highlighted Instructions Preview${COLORS.reset}`)
|
|
384
|
+
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
|
|
385
|
+
log("")
|
|
386
|
+
|
|
387
|
+
if (!hasPrompt && !hasGoals) {
|
|
388
|
+
log(
|
|
389
|
+
` ${COLORS.dim}(No interpolation_string or goals - no highlighted instructions will be generated)${COLORS.reset}`,
|
|
390
|
+
)
|
|
391
|
+
} else {
|
|
392
|
+
log("")
|
|
393
|
+
log("═══════════════════════════════════════════════════════════════════════════════")
|
|
394
|
+
log("⚠️ ULTRA IMPORTANT - AGENT CONFIGURATION ⚠️")
|
|
395
|
+
log("═══════════════════════════════════════════════════════════════════════════════")
|
|
396
|
+
log("")
|
|
397
|
+
log(`You are acting as the agent: "${agent.name}"`)
|
|
398
|
+
if (agent.description) {
|
|
399
|
+
log(`Description: ${agent.description}`)
|
|
400
|
+
}
|
|
401
|
+
log("")
|
|
402
|
+
log("The following instructions and goals are MANDATORY and MUST be followed")
|
|
403
|
+
log("with the HIGHEST PRIORITY. These override any conflicting default behaviors.")
|
|
404
|
+
log("═══════════════════════════════════════════════════════════════════════════════")
|
|
18
405
|
|
|
19
|
-
|
|
406
|
+
if (hasPrompt) {
|
|
407
|
+
log("")
|
|
408
|
+
log("┌─────────────────────────────────────────────────────────────────────────────┐")
|
|
409
|
+
log(`│ 🎯 AGENT "${agent.name}" INSTRUCTIONS (CRITICAL - MUST FOLLOW) │`)
|
|
410
|
+
log("└─────────────────────────────────────────────────────────────────────────────┘")
|
|
411
|
+
log("")
|
|
412
|
+
log(agent.interpolation_string)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (hasGoals) {
|
|
416
|
+
log("")
|
|
417
|
+
log("┌─────────────────────────────────────────────────────────────────────────────┐")
|
|
418
|
+
log(`│ 🎯 AGENT "${agent.name}" GOALS (CRITICAL - MUST ACHIEVE) │`)
|
|
419
|
+
log("└─────────────────────────────────────────────────────────────────────────────┘")
|
|
420
|
+
log("")
|
|
421
|
+
log(agent.goals)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
log("")
|
|
425
|
+
log("═══════════════════════════════════════════════════════════════════════════════")
|
|
426
|
+
log(`⚠️ END OF ULTRA IMPORTANT AGENT "${agent.name}" CONFIGURATION ⚠️`)
|
|
427
|
+
log("═══════════════════════════════════════════════════════════════════════════════")
|
|
428
|
+
}
|
|
429
|
+
log("")
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const args = process.argv.slice(2)
|
|
433
|
+
const command = args[0]
|
|
434
|
+
|
|
435
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
436
|
+
showHelp()
|
|
437
|
+
} else if (args.includes("--version") || args.includes("-v")) {
|
|
438
|
+
log("tfcode v1.0.0-beta.9")
|
|
439
|
+
} else if (command === "setup") {
|
|
440
|
+
interactiveSetup()
|
|
441
|
+
} else if (command === "validate") {
|
|
442
|
+
;(async () => {
|
|
443
|
+
const config = loadConfig()
|
|
444
|
+
if (!config) {
|
|
445
|
+
error("No credentials. Run: tfcode setup")
|
|
446
|
+
process.exit(1)
|
|
447
|
+
}
|
|
448
|
+
info("Validating...")
|
|
449
|
+
try {
|
|
450
|
+
const result = await runPythonSync("validate", config)
|
|
451
|
+
if (result.success) {
|
|
452
|
+
success("Credentials valid")
|
|
453
|
+
log(` API URL: ${result.base_url}`)
|
|
454
|
+
} else {
|
|
455
|
+
error(`Failed: ${result.error}`)
|
|
456
|
+
process.exit(1)
|
|
457
|
+
}
|
|
458
|
+
} catch (e) {
|
|
459
|
+
error(`Failed: ${e.message}`)
|
|
460
|
+
process.exit(1)
|
|
461
|
+
}
|
|
462
|
+
})()
|
|
463
|
+
} else if (command === "sync") {
|
|
464
|
+
;(async () => {
|
|
465
|
+
const config = loadConfig()
|
|
466
|
+
if (!config) {
|
|
467
|
+
error("No credentials. Run: tfcode setup")
|
|
468
|
+
process.exit(1)
|
|
469
|
+
}
|
|
470
|
+
info("Syncing tools...")
|
|
471
|
+
try {
|
|
472
|
+
const result = await runPythonSync("sync", config)
|
|
473
|
+
if (result.success) {
|
|
474
|
+
saveToolsCache(result)
|
|
475
|
+
success(`Synced ${result.tools.length} tools`)
|
|
476
|
+
if (result.by_type) {
|
|
477
|
+
log("")
|
|
478
|
+
log("By type:")
|
|
479
|
+
for (const [t, c] of Object.entries(result.by_type)) log(` ${t}: ${c}`)
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
error(`Failed: ${result.error}`)
|
|
483
|
+
process.exit(1)
|
|
484
|
+
}
|
|
485
|
+
} catch (e) {
|
|
486
|
+
error(`Failed: ${e.message}`)
|
|
487
|
+
process.exit(1)
|
|
488
|
+
}
|
|
489
|
+
})()
|
|
490
|
+
} else if (command === "tools" && args[1] === "list") {
|
|
491
|
+
const cached = loadCachedTools()
|
|
492
|
+
if (!cached?.success) {
|
|
493
|
+
error("No tools. Run: tfcode sync")
|
|
494
|
+
process.exit(1)
|
|
495
|
+
}
|
|
496
|
+
let tools = cached.tools
|
|
497
|
+
if (args[2] === "--type" && args[3]) tools = tools.filter((t) => t.tool_type === args[3])
|
|
498
|
+
log(`\n${tools.length} tool(s):\n`)
|
|
499
|
+
for (const t of tools) {
|
|
500
|
+
log(` ${COLORS.cyan}${t.name}${COLORS.reset}`)
|
|
501
|
+
log(` Type: ${t.tool_type}`)
|
|
502
|
+
if (t.description) log(` ${COLORS.dim}${t.description.slice(0, 60)}${COLORS.reset}`)
|
|
503
|
+
log(` Auth: ${t.auth_via}\n`)
|
|
504
|
+
}
|
|
505
|
+
} else if (command === "test-agent") {
|
|
506
|
+
const agentId = args[1]
|
|
507
|
+
if (!agentId) {
|
|
508
|
+
error("Usage: tfcode test-agent <agent-id-or-name>")
|
|
509
|
+
process.exit(1)
|
|
510
|
+
}
|
|
511
|
+
testAgentPrompt(agentId)
|
|
512
|
+
} else if (command === "debug") {
|
|
513
|
+
showDebugInfo()
|
|
514
|
+
} else if (!command) {
|
|
515
|
+
// Show help instead of trying TUI (TUI requires full build)
|
|
516
|
+
showHelp()
|
|
517
|
+
} else {
|
|
518
|
+
error(`Unknown command: ${command}`)
|
|
519
|
+
showHelp()
|
|
520
|
+
process.exit(1)
|
|
521
|
+
}
|
package/package.json
CHANGED
|
@@ -1,33 +1,164 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"version": "1.0.40",
|
|
2
4
|
"name": "@toothfairyai/tfcode",
|
|
3
|
-
"
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"files": [
|
|
8
|
+
"bin",
|
|
9
|
+
"app",
|
|
10
|
+
"python",
|
|
11
|
+
"postinstall.mjs",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"prepare": "effect-language-service patch || true",
|
|
16
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
17
|
+
"typecheck": "tsgo --noEmit",
|
|
18
|
+
"test": "bun test --timeout 30000",
|
|
19
|
+
"build": "bun run script/build-tfcode.ts",
|
|
20
|
+
"build:single": "bun run script/build-tfcode.ts --single",
|
|
21
|
+
"publish:upload": "bun run script/publish-tfcode.ts --upload",
|
|
22
|
+
"publish:npm": "bun run script/publish-tfcode.ts --npm",
|
|
23
|
+
"publish:brew": "bun run script/publish-tfcode.ts --brew",
|
|
24
|
+
"publish:all": "bun run script/publish-tfcode.ts --all",
|
|
25
|
+
"dev": "bun run --conditions=browser ./src/index.ts",
|
|
26
|
+
"db": "bun drizzle-kit"
|
|
27
|
+
},
|
|
4
28
|
"bin": {
|
|
5
|
-
"tfcode": "./bin/tfcode
|
|
29
|
+
"tfcode": "./bin/tfcode"
|
|
6
30
|
},
|
|
7
|
-
"
|
|
8
|
-
"
|
|
31
|
+
"exports": {
|
|
32
|
+
"./*": "./src/*.ts"
|
|
9
33
|
},
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"@
|
|
19
|
-
"@
|
|
20
|
-
"@
|
|
21
|
-
"@
|
|
22
|
-
"@
|
|
23
|
-
"@
|
|
34
|
+
"imports": {
|
|
35
|
+
"#db": {
|
|
36
|
+
"bun": "./src/storage/db.bun.ts",
|
|
37
|
+
"node": "./src/storage/db.node.ts",
|
|
38
|
+
"default": "./src/storage/db.bun.ts"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@babel/core": "7.28.4",
|
|
43
|
+
"@effect/language-service": "0.79.0",
|
|
44
|
+
"@octokit/webhooks-types": "7.6.1",
|
|
45
|
+
"@opencode-ai/script": "workspace:*",
|
|
46
|
+
"@parcel/watcher-darwin-arm64": "2.5.1",
|
|
47
|
+
"@parcel/watcher-darwin-x64": "2.5.1",
|
|
48
|
+
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
|
49
|
+
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
|
50
|
+
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
|
51
|
+
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
|
52
|
+
"@parcel/watcher-win32-arm64": "2.5.1",
|
|
53
|
+
"@parcel/watcher-win32-x64": "2.5.1",
|
|
54
|
+
"@standard-schema/spec": "1.0.0",
|
|
55
|
+
"@tsconfig/bun": "catalog:",
|
|
56
|
+
"@types/babel__core": "7.20.5",
|
|
57
|
+
"@types/bun": "catalog:",
|
|
58
|
+
"@types/cross-spawn": "6.0.6",
|
|
59
|
+
"@types/mime-types": "3.0.1",
|
|
60
|
+
"@types/semver": "^7.5.8",
|
|
61
|
+
"@types/turndown": "5.0.5",
|
|
62
|
+
"@types/which": "3.0.4",
|
|
63
|
+
"@types/yargs": "17.0.33",
|
|
64
|
+
"@typescript/native-preview": "catalog:",
|
|
65
|
+
"drizzle-kit": "catalog:",
|
|
66
|
+
"drizzle-orm": "catalog:",
|
|
67
|
+
"typescript": "catalog:",
|
|
68
|
+
"vscode-languageserver-types": "3.17.5",
|
|
69
|
+
"why-is-node-running": "3.2.2",
|
|
70
|
+
"zod-to-json-schema": "3.24.5"
|
|
24
71
|
},
|
|
25
|
-
"
|
|
26
|
-
"
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@actions/core": "1.11.1",
|
|
74
|
+
"@actions/github": "6.0.1",
|
|
75
|
+
"@agentclientprotocol/sdk": "0.14.1",
|
|
76
|
+
"@ai-sdk/amazon-bedrock": "3.0.82",
|
|
77
|
+
"@ai-sdk/anthropic": "2.0.65",
|
|
78
|
+
"@ai-sdk/azure": "2.0.91",
|
|
79
|
+
"@ai-sdk/cerebras": "1.0.36",
|
|
80
|
+
"@ai-sdk/cohere": "2.0.22",
|
|
81
|
+
"@ai-sdk/deepinfra": "1.0.36",
|
|
82
|
+
"@ai-sdk/gateway": "2.0.30",
|
|
83
|
+
"@ai-sdk/google": "2.0.54",
|
|
84
|
+
"@ai-sdk/google-vertex": "3.0.106",
|
|
85
|
+
"@ai-sdk/groq": "2.0.34",
|
|
86
|
+
"@ai-sdk/mistral": "2.0.27",
|
|
87
|
+
"@ai-sdk/openai": "2.0.89",
|
|
88
|
+
"@ai-sdk/openai-compatible": "1.0.32",
|
|
89
|
+
"@ai-sdk/perplexity": "2.0.23",
|
|
90
|
+
"@ai-sdk/provider": "2.0.1",
|
|
91
|
+
"@ai-sdk/provider-utils": "3.0.21",
|
|
92
|
+
"@ai-sdk/togetherai": "1.0.34",
|
|
93
|
+
"@ai-sdk/vercel": "1.0.33",
|
|
94
|
+
"@ai-sdk/xai": "2.0.51",
|
|
95
|
+
"@aws-sdk/credential-providers": "3.993.0",
|
|
96
|
+
"@clack/prompts": "1.0.0-alpha.1",
|
|
97
|
+
"@effect/platform-node": "catalog:",
|
|
98
|
+
"@hono/standard-validator": "0.1.5",
|
|
99
|
+
"@hono/zod-validator": "catalog:",
|
|
100
|
+
"@modelcontextprotocol/sdk": "1.25.2",
|
|
101
|
+
"@octokit/graphql": "9.0.2",
|
|
102
|
+
"@octokit/rest": "catalog:",
|
|
103
|
+
"@openauthjs/openauth": "catalog:",
|
|
104
|
+
"@opencode-ai/plugin": "workspace:*",
|
|
105
|
+
"@opencode-ai/script": "workspace:*",
|
|
106
|
+
"@opencode-ai/sdk": "workspace:*",
|
|
107
|
+
"@opencode-ai/util": "workspace:*",
|
|
108
|
+
"@openrouter/ai-sdk-provider": "1.5.4",
|
|
109
|
+
"@opentui/core": "0.1.90",
|
|
110
|
+
"@opentui/solid": "0.1.90",
|
|
111
|
+
"@parcel/watcher": "2.5.1",
|
|
112
|
+
"@pierre/diffs": "catalog:",
|
|
113
|
+
"@solid-primitives/event-bus": "1.1.2",
|
|
114
|
+
"@solid-primitives/scheduled": "1.5.2",
|
|
115
|
+
"@standard-schema/spec": "1.0.0",
|
|
116
|
+
"@zip.js/zip.js": "2.7.62",
|
|
117
|
+
"ai": "catalog:",
|
|
118
|
+
"ai-gateway-provider": "2.3.1",
|
|
119
|
+
"bonjour-service": "1.3.0",
|
|
120
|
+
"bun-pty": "0.4.8",
|
|
121
|
+
"chokidar": "4.0.3",
|
|
122
|
+
"clipboardy": "4.0.0",
|
|
123
|
+
"cross-spawn": "^7.0.6",
|
|
124
|
+
"decimal.js": "10.5.0",
|
|
125
|
+
"diff": "catalog:",
|
|
126
|
+
"drizzle-orm": "catalog:",
|
|
127
|
+
"effect": "catalog:",
|
|
128
|
+
"fuzzysort": "3.1.0",
|
|
129
|
+
"gitlab-ai-provider": "5.2.2",
|
|
130
|
+
"glob": "13.0.5",
|
|
131
|
+
"google-auth-library": "10.5.0",
|
|
132
|
+
"gray-matter": "4.0.3",
|
|
133
|
+
"hono": "catalog:",
|
|
134
|
+
"hono-openapi": "catalog:",
|
|
135
|
+
"ignore": "7.0.5",
|
|
136
|
+
"jsonc-parser": "3.3.1",
|
|
137
|
+
"mime-types": "3.0.2",
|
|
138
|
+
"minimatch": "10.0.3",
|
|
139
|
+
"open": "10.1.2",
|
|
140
|
+
"opencode-gitlab-auth": "2.0.0",
|
|
141
|
+
"opentui-spinner": "0.0.6",
|
|
142
|
+
"partial-json": "0.1.7",
|
|
143
|
+
"react": "19.2.4",
|
|
144
|
+
"react-dom": "19.2.4",
|
|
145
|
+
"remeda": "catalog:",
|
|
146
|
+
"semver": "^7.6.3",
|
|
147
|
+
"solid-js": "catalog:",
|
|
148
|
+
"strip-ansi": "7.1.2",
|
|
149
|
+
"tree-sitter-bash": "0.25.0",
|
|
150
|
+
"tree-sitter-powershell": "0.25.10",
|
|
151
|
+
"turndown": "7.2.0",
|
|
152
|
+
"ulid": "catalog:",
|
|
153
|
+
"vscode-jsonrpc": "8.2.1",
|
|
154
|
+
"web-tree-sitter": "0.25.10",
|
|
155
|
+
"which": "6.0.1",
|
|
156
|
+
"xdg-basedir": "5.1.0",
|
|
157
|
+
"yargs": "18.0.0",
|
|
158
|
+
"zod": "catalog:",
|
|
159
|
+
"zod-to-json-schema": "3.24.5"
|
|
27
160
|
},
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
"type": "git",
|
|
31
|
-
"url": "https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git"
|
|
161
|
+
"overrides": {
|
|
162
|
+
"drizzle-orm": "catalog:"
|
|
32
163
|
}
|
|
33
|
-
}
|
|
164
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/python/tf_sync/agents.py
CHANGED
|
File without changes
|
package/python/tf_sync/config.py
CHANGED
|
File without changes
|
package/python/tf_sync/mcp.py
CHANGED
|
File without changes
|
package/python/tf_sync/tools.py
CHANGED
|
File without changes
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 opencode
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/postinstall.mjs
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from "fs"
|
|
4
|
-
import path from "path"
|
|
5
|
-
import os from "os"
|
|
6
|
-
import { spawnSync } from "child_process"
|
|
7
|
-
import { fileURLToPath } from "url"
|
|
8
|
-
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
-
const __dirname = path.dirname(__filename)
|
|
11
|
-
|
|
12
|
-
const GITEA_HOST = process.env.TFCODE_GITEA_HOST || "gitea.toothfairyai.com"
|
|
13
|
-
const GITEA_REPO = process.env.TFCODE_GITEA_REPO || "ToothFairyAI/tf_code"
|
|
14
|
-
|
|
15
|
-
function detectPlatform() {
|
|
16
|
-
let platform
|
|
17
|
-
switch (os.platform()) {
|
|
18
|
-
case "darwin":
|
|
19
|
-
platform = "darwin"
|
|
20
|
-
break
|
|
21
|
-
case "linux":
|
|
22
|
-
platform = "linux"
|
|
23
|
-
break
|
|
24
|
-
case "win32":
|
|
25
|
-
platform = "windows"
|
|
26
|
-
break
|
|
27
|
-
default:
|
|
28
|
-
platform = os.platform()
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let arch
|
|
32
|
-
switch (os.arch()) {
|
|
33
|
-
case "x64":
|
|
34
|
-
arch = "x64"
|
|
35
|
-
break
|
|
36
|
-
case "arm64":
|
|
37
|
-
arch = "arm64"
|
|
38
|
-
break
|
|
39
|
-
default:
|
|
40
|
-
arch = os.arch()
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Check for AVX2 on x64
|
|
44
|
-
let needsBaseline = false
|
|
45
|
-
if (arch === "x64" && (platform === "linux" || platform === "darwin")) {
|
|
46
|
-
try {
|
|
47
|
-
if (platform === "linux") {
|
|
48
|
-
const cpuinfo = fs.readFileSync("/proc/cpuinfo", "utf8")
|
|
49
|
-
needsBaseline = !cpuinfo.toLowerCase().includes("avx2")
|
|
50
|
-
} else if (platform === "darwin") {
|
|
51
|
-
const result = spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], { encoding: "utf8" })
|
|
52
|
-
needsBaseline = result.stdout.trim() !== "1"
|
|
53
|
-
}
|
|
54
|
-
} catch {}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Check for musl on Linux
|
|
58
|
-
let abi = ""
|
|
59
|
-
if (platform === "linux") {
|
|
60
|
-
try {
|
|
61
|
-
if (fs.existsSync("/etc/alpine-release")) {
|
|
62
|
-
abi = "musl"
|
|
63
|
-
} else {
|
|
64
|
-
const result = spawnSync("ldd", ["--version"], { encoding: "utf8" })
|
|
65
|
-
if ((result.stdout + result.stderr).toLowerCase().includes("musl")) {
|
|
66
|
-
abi = "musl"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
} catch {}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return { platform, arch, needsBaseline, abi }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function getVersion() {
|
|
76
|
-
try {
|
|
77
|
-
const res = await fetch(`https://${GITEA_HOST}/api/v1/repos/${GITEA_REPO}/releases/latest`)
|
|
78
|
-
const data = await res.json()
|
|
79
|
-
return data.tag_name?.replace(/^v/, "") || "1.0.0"
|
|
80
|
-
} catch {
|
|
81
|
-
return "1.0.0"
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function downloadBinary() {
|
|
86
|
-
const { platform, arch, needsBaseline, abi } = detectPlatform()
|
|
87
|
-
const version = await getVersion()
|
|
88
|
-
|
|
89
|
-
// Check if binary already exists (included in npm package)
|
|
90
|
-
const binaryName = platform === "windows" ? "tfcode.exe" : "tfcode"
|
|
91
|
-
const binDir = path.join(__dirname, "bin")
|
|
92
|
-
const existingBinary = path.join(binDir, binaryName)
|
|
93
|
-
|
|
94
|
-
if (fs.existsSync(existingBinary)) {
|
|
95
|
-
console.log(`✓ Binary already exists at ${existingBinary}`)
|
|
96
|
-
// Still need to copy app dir from platform package if missing
|
|
97
|
-
const targetAppDir = path.join(binDir, "app")
|
|
98
|
-
if (!fs.existsSync(path.join(targetAppDir, "dist", "index.html"))) {
|
|
99
|
-
let target = `tfcode-${platform}-${arch}`
|
|
100
|
-
if (needsBaseline) target += "-baseline"
|
|
101
|
-
if (abi) target += `-${abi}`
|
|
102
|
-
const pkgName = `@toothfairyai/${target}`
|
|
103
|
-
try {
|
|
104
|
-
const pkgUrl = import.meta.resolve(`${pkgName}/package.json`)
|
|
105
|
-
const pkgDir = path.dirname(fileURLToPath(pkgUrl))
|
|
106
|
-
const srcAppDir = path.join(pkgDir, "bin", "app")
|
|
107
|
-
if (fs.existsSync(path.join(srcAppDir, "dist", "index.html"))) {
|
|
108
|
-
fs.cpSync(srcAppDir, targetAppDir, { recursive: true })
|
|
109
|
-
console.log("✓ Web app copied from platform package")
|
|
110
|
-
}
|
|
111
|
-
} catch (e) {
|
|
112
|
-
console.log(`! Could not copy web app: ${e.message}`)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Build filename for download
|
|
119
|
-
let target = `tfcode-${platform}-${arch}`
|
|
120
|
-
if (needsBaseline) target += "-baseline"
|
|
121
|
-
if (abi) target += `-${abi}`
|
|
122
|
-
|
|
123
|
-
const ext = platform === "linux" ? ".tar.gz" : ".zip"
|
|
124
|
-
const filename = `${target}${ext}`
|
|
125
|
-
const url = `https://${GITEA_HOST}/${GITEA_REPO}/releases/download/v${version}/${filename}`
|
|
126
|
-
|
|
127
|
-
console.log(`Downloading tfcode v${version} for ${target}...`)
|
|
128
|
-
|
|
129
|
-
// Download
|
|
130
|
-
if (!fs.existsSync(binDir)) fs.mkdirSync(binDir, { recursive: true })
|
|
131
|
-
|
|
132
|
-
const tmpDir = path.join(os.tmpdir(), `tfcode-install-${process.pid}`)
|
|
133
|
-
fs.mkdirSync(tmpDir, { recursive: true })
|
|
134
|
-
const archivePath = path.join(tmpDir, filename)
|
|
135
|
-
|
|
136
|
-
// Use curl to download
|
|
137
|
-
const curlResult = spawnSync("curl", ["-fsSL", "-o", archivePath, url], { stdio: "inherit" })
|
|
138
|
-
if (curlResult.status !== 0) {
|
|
139
|
-
console.error(`Failed to download from ${url}`)
|
|
140
|
-
console.error("Trying npm package fallback...")
|
|
141
|
-
|
|
142
|
-
// Fallback to npm optionalDependencies
|
|
143
|
-
try {
|
|
144
|
-
const pkgName = `@toothfairyai/${target}`
|
|
145
|
-
|
|
146
|
-
// ESM-compatible module resolution
|
|
147
|
-
let pkgDir
|
|
148
|
-
try {
|
|
149
|
-
const pkgUrl = import.meta.resolve(`${pkgName}/package.json`)
|
|
150
|
-
const pkgPath = fileURLToPath(pkgUrl)
|
|
151
|
-
pkgDir = path.dirname(pkgPath)
|
|
152
|
-
} catch (e) {
|
|
153
|
-
console.error(`Could not resolve ${pkgName}:`, e.message)
|
|
154
|
-
throw e
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const binaryName = platform === "windows" ? "tfcode.exe" : "tfcode"
|
|
158
|
-
const binaryPath = path.join(pkgDir, "bin", binaryName)
|
|
159
|
-
|
|
160
|
-
console.log(`Looking for binary at: ${binaryPath}`)
|
|
161
|
-
|
|
162
|
-
if (fs.existsSync(binaryPath)) {
|
|
163
|
-
console.log(`Found binary at npm package location`)
|
|
164
|
-
setupBinary(binaryPath, platform)
|
|
165
|
-
return
|
|
166
|
-
} else {
|
|
167
|
-
console.error(`Binary not found at ${binaryPath}`)
|
|
168
|
-
}
|
|
169
|
-
} catch (e) {
|
|
170
|
-
console.error("npm package fallback failed:", e.message)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
console.error("")
|
|
174
|
-
console.error("Installation failed. The binary could not be downloaded or found.")
|
|
175
|
-
console.error("")
|
|
176
|
-
console.error("Possible solutions:")
|
|
177
|
-
console.error(" 1. If this is a private installation, set TFCODE_GITEA_HOST to an accessible host")
|
|
178
|
-
console.error(" 2. Manually download the binary and place it in the bin/ directory")
|
|
179
|
-
console.error(" 3. Contact ToothFairyAI support for assistance")
|
|
180
|
-
console.error("")
|
|
181
|
-
process.exit(1)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Extract
|
|
185
|
-
console.log("Extracting...")
|
|
186
|
-
if (platform === "linux") {
|
|
187
|
-
spawnSync("tar", ["-xzf", archivePath, "-C", tmpDir], { stdio: "inherit" })
|
|
188
|
-
} else {
|
|
189
|
-
spawnSync("unzip", ["-q", archivePath, "-d", tmpDir], { stdio: "inherit" })
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Move binary
|
|
193
|
-
const extractedBinary = path.join(tmpDir, "tfcode")
|
|
194
|
-
const targetBinary = path.join(binDir, binaryName)
|
|
195
|
-
|
|
196
|
-
if (fs.existsSync(extractedBinary)) {
|
|
197
|
-
if (fs.existsSync(targetBinary)) fs.unlinkSync(targetBinary)
|
|
198
|
-
fs.copyFileSync(extractedBinary, targetBinary)
|
|
199
|
-
fs.chmodSync(targetBinary, 0o755)
|
|
200
|
-
console.log(`Installed tfcode to ${targetBinary}`)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Move app directory
|
|
204
|
-
const extractedAppDir = path.join(tmpDir, "app")
|
|
205
|
-
const targetAppDir = path.join(binDir, "app")
|
|
206
|
-
if (fs.existsSync(path.join(extractedAppDir, "dist", "index.html"))) {
|
|
207
|
-
if (fs.existsSync(targetAppDir)) fs.rmSync(targetAppDir, { recursive: true, force: true })
|
|
208
|
-
fs.cpSync(extractedAppDir, targetAppDir, { recursive: true })
|
|
209
|
-
console.log("Installed web app")
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Cleanup
|
|
213
|
-
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function setupBinary(sourcePath, platform) {
|
|
217
|
-
const binDir = path.join(__dirname, "bin")
|
|
218
|
-
const binaryName = platform === "windows" ? "tfcode.exe" : "tfcode"
|
|
219
|
-
const targetBinary = path.join(binDir, binaryName)
|
|
220
|
-
|
|
221
|
-
if (!fs.existsSync(binDir)) fs.mkdirSync(binDir, { recursive: true })
|
|
222
|
-
if (fs.existsSync(targetBinary)) fs.unlinkSync(targetBinary)
|
|
223
|
-
|
|
224
|
-
// Try hardlink, fall back to copy
|
|
225
|
-
try {
|
|
226
|
-
fs.linkSync(sourcePath, targetBinary)
|
|
227
|
-
} catch {
|
|
228
|
-
fs.copyFileSync(sourcePath, targetBinary)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
fs.chmodSync(targetBinary, 0o755)
|
|
232
|
-
console.log(`tfcode installed to ${targetBinary}`)
|
|
233
|
-
|
|
234
|
-
// Also copy app dir from platform package
|
|
235
|
-
const sourceDir = path.dirname(sourcePath)
|
|
236
|
-
const srcAppDir = path.join(sourceDir, "app")
|
|
237
|
-
const targetAppDir = path.join(binDir, "app")
|
|
238
|
-
if (fs.existsSync(path.join(srcAppDir, "dist", "index.html"))) {
|
|
239
|
-
if (fs.existsSync(targetAppDir)) fs.rmSync(targetAppDir, { recursive: true, force: true })
|
|
240
|
-
fs.cpSync(srcAppDir, targetAppDir, { recursive: true })
|
|
241
|
-
console.log("Web app copied from platform package")
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function main() {
|
|
246
|
-
console.log("")
|
|
247
|
-
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
248
|
-
console.log(" tfcode - ToothFairyAI's official coding agent")
|
|
249
|
-
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
250
|
-
console.log("")
|
|
251
|
-
|
|
252
|
-
// Download and setup binary
|
|
253
|
-
await downloadBinary()
|
|
254
|
-
|
|
255
|
-
// Check for Python (needed for TF integration)
|
|
256
|
-
try {
|
|
257
|
-
const result = spawnSync("python3", ["--version"], { encoding: "utf8" })
|
|
258
|
-
if (result.status === 0) {
|
|
259
|
-
console.log(`✓ Found ${result.stdout.trim()}`)
|
|
260
|
-
|
|
261
|
-
// Install Python SDK
|
|
262
|
-
console.log("Installing ToothFairyAI Python SDK...")
|
|
263
|
-
const pipResult = spawnSync(
|
|
264
|
-
"python3",
|
|
265
|
-
["-m", "pip", "install", "--user", "--break-system-packages", "toothfairyai", "pydantic", "httpx", "rich"],
|
|
266
|
-
{
|
|
267
|
-
stdio: "inherit",
|
|
268
|
-
},
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
if (pipResult.status === 0) {
|
|
272
|
-
console.log("✓ Python SDK installed")
|
|
273
|
-
} else {
|
|
274
|
-
console.log("! Python SDK install failed, run manually:")
|
|
275
|
-
console.log(" pip install toothfairyai pydantic httpx rich")
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
} catch {
|
|
279
|
-
console.log("! Python 3.10+ not found. Install with:")
|
|
280
|
-
console.log(" macOS: brew install python@3.12")
|
|
281
|
-
console.log(" Ubuntu: sudo apt install python3.12")
|
|
282
|
-
console.log(" Windows: Download from python.org/downloads")
|
|
283
|
-
console.log("")
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
console.log("")
|
|
287
|
-
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
288
|
-
console.log("✓ tfcode installed successfully!")
|
|
289
|
-
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
290
|
-
console.log("")
|
|
291
|
-
console.log("Quick Start:")
|
|
292
|
-
console.log("")
|
|
293
|
-
console.log(" 1. Set credentials:")
|
|
294
|
-
console.log(' export TF_WORKSPACE_ID="your-workspace-id"')
|
|
295
|
-
console.log(' export TF_API_KEY="your-api-key"')
|
|
296
|
-
console.log("")
|
|
297
|
-
console.log(" 2. Validate:")
|
|
298
|
-
console.log(" tfcode validate")
|
|
299
|
-
console.log("")
|
|
300
|
-
console.log(" 3. Sync tools:")
|
|
301
|
-
console.log(" tfcode sync")
|
|
302
|
-
console.log("")
|
|
303
|
-
console.log(" 4. Start coding:")
|
|
304
|
-
console.log(" tfcode")
|
|
305
|
-
console.log("")
|
|
306
|
-
console.log("Documentation: https://toothfairyai.com/developers/tfcode")
|
|
307
|
-
console.log("")
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
main().catch(console.error)
|