@toothfairyai/tfcode 1.1.3 → 1.2.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 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
- const binary = process.platform === 'win32' ? 'tfcode.exe' : 'tfcode'
7
- const binaryPath = path.join(__dirname, binary)
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
- if (!fs.existsSync(binaryPath)) {
10
- console.error('tfcode binary not found. Run: npm install @toothfairyai/tfcode')
11
- process.exit(1)
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
- const child = spawn(binaryPath, process.argv.slice(2), {
15
- stdio: 'inherit',
16
- env: process.env
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
- child.on('exit', (code) => process.exit(code || 0))
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,167 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "version": "1.2.0",
2
4
  "name": "@toothfairyai/tfcode",
3
- "version": "1.1.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.js"
29
+ "tfcode": "./bin/tfcode"
6
30
  },
7
- "scripts": {
8
- "postinstall": "node ./postinstall.mjs"
31
+ "exports": {
32
+ "./*": "./src/*.ts"
9
33
  },
10
- "license": "MIT",
11
- "optionalDependencies": {
12
- "@toothfairyai/tfcode-linux-arm64": "1.1.3",
13
- "@toothfairyai/tfcode-windows-x64": "1.1.3",
14
- "@toothfairyai/tfcode-linux-x64-baseline-musl": "1.1.3",
15
- "@toothfairyai/tfcode-darwin-x64-baseline": "1.1.3",
16
- "@toothfairyai/tfcode-linux-x64-musl": "1.1.3",
17
- "@toothfairyai/tfcode-windows-x64-baseline": "1.1.3",
18
- "@toothfairyai/tfcode-linux-arm64-musl": "1.1.3",
19
- "@toothfairyai/tfcode-windows-arm64": "1.1.3",
20
- "@toothfairyai/tfcode-linux-x64": "1.1.3",
21
- "@toothfairyai/tfcode-darwin-x64": "1.1.3",
22
- "@toothfairyai/tfcode-linux-x64-baseline": "1.1.3",
23
- "@toothfairyai/tfcode-darwin-arm64": "1.1.3"
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": "1.3.11",
58
+ "@types/cross-spawn": "6.0.6",
59
+ "@types/mime-types": "3.0.1",
60
+ "@types/node": "22.13.9",
61
+ "@types/semver": "^7.5.8",
62
+ "@types/turndown": "5.0.5",
63
+ "@types/which": "3.0.4",
64
+ "@types/ws": "8.18.1",
65
+ "@types/yargs": "17.0.33",
66
+ "@typescript/native-preview": "catalog:",
67
+ "drizzle-kit": "catalog:",
68
+ "drizzle-orm": "catalog:",
69
+ "typescript": "catalog:",
70
+ "vscode-languageserver-types": "3.17.5",
71
+ "why-is-node-running": "3.2.2",
72
+ "zod-to-json-schema": "3.24.5"
24
73
  },
25
- "engines": {
26
- "node": ">=18"
74
+ "dependencies": {
75
+ "@actions/core": "1.11.1",
76
+ "@actions/github": "6.0.1",
77
+ "@agentclientprotocol/sdk": "0.14.1",
78
+ "@ai-sdk/amazon-bedrock": "3.0.82",
79
+ "@ai-sdk/anthropic": "2.0.65",
80
+ "@ai-sdk/azure": "2.0.91",
81
+ "@ai-sdk/cerebras": "1.0.36",
82
+ "@ai-sdk/cohere": "2.0.22",
83
+ "@ai-sdk/deepinfra": "1.0.36",
84
+ "@ai-sdk/gateway": "2.0.30",
85
+ "@ai-sdk/google": "2.0.54",
86
+ "@ai-sdk/google-vertex": "3.0.106",
87
+ "@ai-sdk/groq": "2.0.34",
88
+ "@ai-sdk/mistral": "2.0.27",
89
+ "@ai-sdk/openai": "2.0.89",
90
+ "@ai-sdk/openai-compatible": "1.0.32",
91
+ "@ai-sdk/perplexity": "2.0.23",
92
+ "@ai-sdk/provider": "2.0.1",
93
+ "@ai-sdk/provider-utils": "3.0.21",
94
+ "@ai-sdk/togetherai": "1.0.34",
95
+ "@ai-sdk/vercel": "1.0.33",
96
+ "@ai-sdk/xai": "2.0.51",
97
+ "@aws-sdk/credential-providers": "3.993.0",
98
+ "@clack/prompts": "1.0.0-alpha.1",
99
+ "@effect/platform-node": "catalog:",
100
+ "@hono/standard-validator": "0.1.5",
101
+ "@hono/zod-validator": "catalog:",
102
+ "@modelcontextprotocol/sdk": "1.29.0",
103
+ "@octokit/graphql": "9.0.2",
104
+ "@octokit/rest": "catalog:",
105
+ "@openauthjs/openauth": "catalog:",
106
+ "@opencode-ai/plugin": "workspace:*",
107
+ "@opencode-ai/script": "workspace:*",
108
+ "@opencode-ai/sdk": "workspace:*",
109
+ "@opencode-ai/util": "workspace:*",
110
+ "@openrouter/ai-sdk-provider": "1.5.4",
111
+ "@opentui/core": "0.1.90",
112
+ "@opentui/solid": "0.1.90",
113
+ "@parcel/watcher": "2.5.1",
114
+ "@pierre/diffs": "catalog:",
115
+ "@solid-primitives/event-bus": "1.1.2",
116
+ "@solid-primitives/scheduled": "1.5.2",
117
+ "@standard-schema/spec": "1.0.0",
118
+ "@zip.js/zip.js": "2.7.62",
119
+ "ai": "catalog:",
120
+ "ai-gateway-provider": "2.3.1",
121
+ "bonjour-service": "1.3.0",
122
+ "bun-pty": "0.4.8",
123
+ "chokidar": "4.0.3",
124
+ "clipboardy": "4.0.0",
125
+ "cross-spawn": "^7.0.6",
126
+ "decimal.js": "10.5.0",
127
+ "diff": "catalog:",
128
+ "drizzle-orm": "catalog:",
129
+ "effect": "catalog:",
130
+ "fuzzysort": "3.1.0",
131
+ "gitlab-ai-provider": "5.2.2",
132
+ "glob": "13.0.5",
133
+ "google-auth-library": "10.5.0",
134
+ "gray-matter": "4.0.3",
135
+ "hono": "catalog:",
136
+ "hono-openapi": "catalog:",
137
+ "ignore": "7.0.5",
138
+ "jsonc-parser": "3.3.1",
139
+ "mime-types": "3.0.2",
140
+ "minimatch": "10.0.3",
141
+ "open": "10.1.2",
142
+ "opencode-gitlab-auth": "2.0.0",
143
+ "opentui-spinner": "0.0.6",
144
+ "partial-json": "0.1.7",
145
+ "react": "19.2.4",
146
+ "react-dom": "19.2.4",
147
+ "remeda": "catalog:",
148
+ "semver": "^7.6.3",
149
+ "solid-js": "catalog:",
150
+ "strip-ansi": "7.1.2",
151
+ "tree-sitter-bash": "0.25.0",
152
+ "tree-sitter-powershell": "0.25.10",
153
+ "turndown": "7.2.0",
154
+ "ulid": "catalog:",
155
+ "vscode-jsonrpc": "8.2.1",
156
+ "web-tree-sitter": "0.25.10",
157
+ "which": "6.0.1",
158
+ "ws": "8.21.0",
159
+ "xdg-basedir": "5.1.0",
160
+ "yargs": "18.0.0",
161
+ "zod": "catalog:",
162
+ "zod-to-json-schema": "3.24.5"
27
163
  },
28
- "homepage": "https://toothfairyai.com/developers/tfcode",
29
- "repository": {
30
- "type": "git",
31
- "url": "https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git"
164
+ "overrides": {
165
+ "drizzle-orm": "catalog:"
32
166
  }
33
- }
167
+ }
File without changes
File without changes
File without changes
File without changes
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)