@toothfairyai/tfcode 1.0.0-beta.9 → 1.0.1
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/LICENSE +21 -0
- package/bin/tfcode.js +15 -693
- package/package.json +1 -30
- package/postinstall.mjs +232 -0
- package/toothfairyai-tfcode-1.0.1.tgz +0 -0
- package/README.md +0 -146
- package/bin/tfcode +0 -17
- package/scripts/postinstall.cjs +0 -171
- package/scripts/quickstart.cjs +0 -66
package/bin/tfcode.js
CHANGED
|
@@ -1,699 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
const { spawn } = require("child_process")
|
|
3
|
+
const path = require("path")
|
|
4
|
+
const fs = require("fs")
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
7
|
-
import { join, dirname } from 'path';
|
|
8
|
-
import { homedir } from 'os';
|
|
9
|
-
import * as readline from 'readline';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
6
|
+
const binDir = path.join(__dirname, "bin")
|
|
7
|
+
const binary = process.platform === "win32" ? "tfcode.exe" : "tfcode"
|
|
8
|
+
const binaryPath = path.join(binDir, binary)
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const TOOLS_FILE = join(TFCODE_DIR, 'tools.json');
|
|
17
|
-
const CREDENTIALS_FILE = join(TFCODE_DIR, 'credentials.json');
|
|
18
|
-
const CONFIG_FILE = join(TFCODE_DIR, 'config.json');
|
|
19
|
-
|
|
20
|
-
const COLORS = {
|
|
21
|
-
reset: '\x1b[0m',
|
|
22
|
-
bold: '\x1b[1m',
|
|
23
|
-
green: '\x1b[32m',
|
|
24
|
-
red: '\x1b[31m',
|
|
25
|
-
cyan: '\x1b[36m',
|
|
26
|
-
dim: '\x1b[90m',
|
|
27
|
-
yellow: '\x1b[33m',
|
|
28
|
-
magenta: '\x1b[35m'
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
function log(msg) {
|
|
32
|
-
console.log(msg);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function success(msg) {
|
|
36
|
-
console.log(`${COLORS.green}✓${COLORS.reset} ${msg}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function error(msg) {
|
|
40
|
-
console.error(`${COLORS.red}✗${COLORS.reset} ${msg}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function info(msg) {
|
|
44
|
-
console.log(`${COLORS.cyan}ℹ${COLORS.reset} ${msg}`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function question(rl, prompt, hidden = false) {
|
|
48
|
-
return new Promise((resolve) => {
|
|
49
|
-
if (hidden && process.stdin.isTTY) {
|
|
50
|
-
// Use simpler approach for hidden input
|
|
51
|
-
process.stdout.write(prompt);
|
|
52
|
-
|
|
53
|
-
let input = '';
|
|
54
|
-
const stdin = process.stdin;
|
|
55
|
-
|
|
56
|
-
stdin.setRawMode(true);
|
|
57
|
-
stdin.setEncoding('utf8');
|
|
58
|
-
stdin.resume();
|
|
59
|
-
|
|
60
|
-
const onKeypress = (str) => {
|
|
61
|
-
if (str === '\n' || str === '\r' || str === '\u0004') {
|
|
62
|
-
stdin.setRawMode(false);
|
|
63
|
-
stdin.pause();
|
|
64
|
-
stdin.removeListener('data', onKeypress);
|
|
65
|
-
process.stdout.write('\n');
|
|
66
|
-
resolve(input);
|
|
67
|
-
} else if (str === '\u0003') {
|
|
68
|
-
process.stdout.write('\n');
|
|
69
|
-
process.exit();
|
|
70
|
-
} else if (str === '\u007F' || str === '\b') {
|
|
71
|
-
input = input.slice(0, -1);
|
|
72
|
-
} else if (str.length === 1 && str.charCodeAt(0) >= 32) {
|
|
73
|
-
input += str;
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
stdin.on('data', onKeypress);
|
|
78
|
-
} else {
|
|
79
|
-
rl.question(prompt, (answer) => {
|
|
80
|
-
resolve(answer.trim());
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function select(prompt, options) {
|
|
87
|
-
return new Promise((resolve) => {
|
|
88
|
-
log('');
|
|
89
|
-
log(prompt);
|
|
90
|
-
log('');
|
|
91
|
-
options.forEach((opt, i) => {
|
|
92
|
-
log(` ${COLORS.cyan}${i + 1}.${COLORS.reset} ${opt}`);
|
|
93
|
-
});
|
|
94
|
-
log('');
|
|
95
|
-
|
|
96
|
-
// Create a fresh readline for select
|
|
97
|
-
const rlSelect = readline.createInterface({
|
|
98
|
-
input: process.stdin,
|
|
99
|
-
output: process.stdout
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
rlSelect.question('Select (1-' + options.length + '): ', (answer) => {
|
|
103
|
-
rlSelect.close();
|
|
104
|
-
const idx = parseInt(answer.trim()) - 1;
|
|
105
|
-
resolve(idx >= 0 && idx < options.length ? idx : 0);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function ensureConfigDir() {
|
|
111
|
-
if (!existsSync(TFCODE_DIR)) {
|
|
112
|
-
mkdirSync(TFCODE_DIR, { recursive: true });
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function loadConfig() {
|
|
117
|
-
// Priority: env vars > config file
|
|
118
|
-
const envConfig = {
|
|
119
|
-
workspace_id: process.env.TF_WORKSPACE_ID,
|
|
120
|
-
api_key: process.env.TF_API_KEY,
|
|
121
|
-
region: process.env.TF_REGION
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
if (envConfig.workspace_id && envConfig.api_key) {
|
|
125
|
-
return envConfig;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (existsSync(CONFIG_FILE)) {
|
|
129
|
-
try {
|
|
130
|
-
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
|
|
131
|
-
} catch {}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function saveConfig(config) {
|
|
138
|
-
ensureConfigDir();
|
|
139
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
10
|
+
if (!fs.existsSync(binaryPath)) {
|
|
11
|
+
console.error("tfcode binary not found. The postinstall script should have downloaded it.")
|
|
12
|
+
console.error("Try running: npm rebuild @toothfairyai/tfcode")
|
|
13
|
+
process.exit(1)
|
|
140
14
|
}
|
|
141
15
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const pythonCode = `
|
|
148
|
-
import json
|
|
149
|
-
import sys
|
|
150
|
-
import os
|
|
151
|
-
|
|
152
|
-
try:
|
|
153
|
-
os.environ["TF_WORKSPACE_ID"] = "${wsId}"
|
|
154
|
-
os.environ["TF_API_KEY"] = "${apiKey}"
|
|
155
|
-
os.environ["TF_REGION"] = "${region}"
|
|
156
|
-
|
|
157
|
-
from tf_sync.config import load_config, validate_credentials, Region
|
|
158
|
-
from tf_sync.tools import sync_tools
|
|
159
|
-
from tf_sync.config import get_region_urls
|
|
160
|
-
|
|
161
|
-
method = "${method}"
|
|
162
|
-
|
|
163
|
-
if method == "validate":
|
|
164
|
-
config = load_config()
|
|
165
|
-
result = validate_credentials(config)
|
|
166
|
-
urls = get_region_urls(config.region)
|
|
167
|
-
print(json.dumps({
|
|
168
|
-
"success": result.success,
|
|
169
|
-
"workspace_id": result.workspace_id,
|
|
170
|
-
"workspace_name": result.workspace_name,
|
|
171
|
-
"error": result.error,
|
|
172
|
-
"base_url": urls["base_url"]
|
|
173
|
-
}))
|
|
174
|
-
|
|
175
|
-
elif method == "sync":
|
|
176
|
-
config = load_config()
|
|
177
|
-
result = sync_tools(config)
|
|
178
|
-
|
|
179
|
-
tools_data = []
|
|
180
|
-
for tool in result.tools:
|
|
181
|
-
tools_data.append({
|
|
182
|
-
"id": tool.id,
|
|
183
|
-
"name": tool.name,
|
|
184
|
-
"description": tool.description,
|
|
185
|
-
"tool_type": tool.tool_type.value,
|
|
186
|
-
"request_type": tool.request_type.value if tool.request_type else None,
|
|
187
|
-
"url": tool.url,
|
|
188
|
-
"auth_via": tool.auth_via
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
print(json.dumps({
|
|
192
|
-
"success": result.success,
|
|
193
|
-
"tools": tools_data,
|
|
194
|
-
"by_type": result.by_type,
|
|
195
|
-
"error": result.error
|
|
196
|
-
}))
|
|
197
|
-
|
|
198
|
-
except Exception as e:
|
|
199
|
-
print(json.dumps({"success": False, "error": str(e)}))
|
|
200
|
-
sys.exit(0)
|
|
201
|
-
`;
|
|
202
|
-
|
|
203
|
-
return new Promise((resolve, reject) => {
|
|
204
|
-
const pythonPath = process.env.TFCODE_PYTHON_PATH || 'python3';
|
|
205
|
-
const proc = spawn(pythonPath, ['-c', pythonCode], {
|
|
206
|
-
env: { ...process.env }
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
let stdout = '';
|
|
210
|
-
let stderr = '';
|
|
211
|
-
|
|
212
|
-
proc.stdout.on('data', (data) => {
|
|
213
|
-
stdout += data.toString();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
proc.stderr.on('data', (data) => {
|
|
217
|
-
stderr += data.toString();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
proc.on('close', (code) => {
|
|
221
|
-
if (code !== 0 && !stdout) {
|
|
222
|
-
reject(new Error(`Python sync failed: ${stderr}`));
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
const result = JSON.parse(stdout.trim());
|
|
228
|
-
resolve(result);
|
|
229
|
-
} catch (e) {
|
|
230
|
-
reject(new Error(`Failed to parse Python output: ${stdout}\nstderr: ${stderr}`));
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
proc.on('error', (err) => {
|
|
235
|
-
reject(err);
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function loadCachedTools() {
|
|
241
|
-
if (!existsSync(TOOLS_FILE)) {
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
try {
|
|
245
|
-
return JSON.parse(readFileSync(TOOLS_FILE, 'utf-8'));
|
|
246
|
-
} catch {
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function saveToolsCache(tools) {
|
|
252
|
-
ensureConfigDir();
|
|
253
|
-
writeFileSync(TOOLS_FILE, JSON.stringify(tools, null, 2));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async function interactiveSetup() {
|
|
257
|
-
log('');
|
|
258
|
-
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
259
|
-
log(`${COLORS.bold}${COLORS.magenta} tfcode Setup${COLORS.reset}`);
|
|
260
|
-
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
261
|
-
log('');
|
|
262
|
-
log('This will guide you through setting up your ToothFairyAI credentials.');
|
|
263
|
-
log('');
|
|
264
|
-
log(`${COLORS.dim}You can find your credentials at:${COLORS.reset}`);
|
|
265
|
-
log(`${COLORS.dim} https://app.toothfairyai.com → Settings → API Keys${COLORS.reset}`);
|
|
266
|
-
log('');
|
|
267
|
-
|
|
268
|
-
// Step 1: Workspace ID
|
|
269
|
-
log(`${COLORS.bold}Step 1: Workspace ID${COLORS.reset}`);
|
|
270
|
-
log(`${COLORS.dim}This is your workspace UUID (e.g., 12345678-1234-1234-1234-123456789012)${COLORS.reset}`);
|
|
271
|
-
log('');
|
|
272
|
-
|
|
273
|
-
const workspaceId = await new Promise((resolve) => {
|
|
274
|
-
const rl = readline.createInterface({
|
|
275
|
-
input: process.stdin,
|
|
276
|
-
output: process.stdout
|
|
277
|
-
});
|
|
278
|
-
rl.question('Enter your Workspace ID: ', (answer) => {
|
|
279
|
-
rl.close();
|
|
280
|
-
resolve(answer.trim());
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
if (!workspaceId) {
|
|
285
|
-
error('Workspace ID is required');
|
|
286
|
-
process.exit(1);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
log('');
|
|
290
|
-
|
|
291
|
-
// Step 2: API Key
|
|
292
|
-
log(`${COLORS.bold}Step 2: API Key${COLORS.reset}`);
|
|
293
|
-
log(`${COLORS.dim}Paste or type your API key${COLORS.reset}`);
|
|
294
|
-
log('');
|
|
295
|
-
|
|
296
|
-
const apiKey = await new Promise((resolve) => {
|
|
297
|
-
const rl = readline.createInterface({
|
|
298
|
-
input: process.stdin,
|
|
299
|
-
output: process.stdout
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// Simple approach - just show the input (API keys are long anyway)
|
|
303
|
-
// This allows paste and works reliably
|
|
304
|
-
rl.question('Enter your API Key: ', (answer) => {
|
|
305
|
-
rl.close();
|
|
306
|
-
resolve(answer.trim());
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
if (!apiKey) {
|
|
311
|
-
error('API Key is required');
|
|
312
|
-
process.exit(1);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
log('');
|
|
316
|
-
|
|
317
|
-
// Step 3: Region
|
|
318
|
-
log(`${COLORS.bold}Step 3: Region${COLORS.reset}`);
|
|
319
|
-
const regions = ['dev (Development)', 'au (Australia)', 'eu (Europe)', 'us (United States)'];
|
|
320
|
-
const regionIdx = await select('Select your region:', regions);
|
|
321
|
-
const regions_map = ['dev', 'au', 'eu', 'us'];
|
|
322
|
-
const region = regions_map[regionIdx];
|
|
323
|
-
|
|
324
|
-
log('');
|
|
325
|
-
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
326
|
-
log('');
|
|
327
|
-
|
|
328
|
-
// Summary
|
|
329
|
-
log(`${COLORS.bold}Summary:${COLORS.reset}`);
|
|
330
|
-
log(` Workspace ID: ${workspaceId}`);
|
|
331
|
-
log(` API Key: ***${apiKey.slice(-4)}`);
|
|
332
|
-
log(` Region: ${region}`);
|
|
333
|
-
log('');
|
|
334
|
-
|
|
335
|
-
const confirm = await new Promise((resolve) => {
|
|
336
|
-
const rl = readline.createInterface({
|
|
337
|
-
input: process.stdin,
|
|
338
|
-
output: process.stdout
|
|
339
|
-
});
|
|
340
|
-
rl.question('Save these credentials? (Y/n): ', (answer) => {
|
|
341
|
-
rl.close();
|
|
342
|
-
resolve(answer.trim());
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
if (confirm.toLowerCase() !== 'n' && confirm.toLowerCase() !== 'no') {
|
|
347
|
-
const config = { workspace_id: workspaceId, api_key: apiKey, region };
|
|
348
|
-
saveConfig(config);
|
|
349
|
-
success('Credentials saved to ~/.tfcode/config.json');
|
|
350
|
-
log('');
|
|
351
|
-
|
|
352
|
-
// Validate
|
|
353
|
-
const testNow = await new Promise((resolve) => {
|
|
354
|
-
const rl = readline.createInterface({
|
|
355
|
-
input: process.stdin,
|
|
356
|
-
output: process.stdout
|
|
357
|
-
});
|
|
358
|
-
rl.question('Validate credentials now? (Y/n): ', (answer) => {
|
|
359
|
-
rl.close();
|
|
360
|
-
resolve(answer.trim());
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
if (testNow.toLowerCase() !== 'n' && testNow.toLowerCase() !== 'no') {
|
|
365
|
-
log('');
|
|
366
|
-
info('Validating credentials...');
|
|
367
|
-
log('');
|
|
368
|
-
|
|
369
|
-
try {
|
|
370
|
-
const result = await runPythonSync('validate', config);
|
|
371
|
-
|
|
372
|
-
if (result.success) {
|
|
373
|
-
success('Credentials valid!');
|
|
374
|
-
log(` API URL: ${result.base_url}`);
|
|
375
|
-
log(` Workspace ID: ${result.workspace_id}`);
|
|
376
|
-
log('');
|
|
377
|
-
|
|
378
|
-
const syncNow = await new Promise((resolve) => {
|
|
379
|
-
const rl = readline.createInterface({
|
|
380
|
-
input: process.stdin,
|
|
381
|
-
output: process.stdout
|
|
382
|
-
});
|
|
383
|
-
rl.question('Sync tools now? (Y/n): ', (answer) => {
|
|
384
|
-
rl.close();
|
|
385
|
-
resolve(answer.trim());
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
if (syncNow.toLowerCase() !== 'n' && syncNow.toLowerCase() !== 'no') {
|
|
390
|
-
log('');
|
|
391
|
-
info('Syncing tools...');
|
|
392
|
-
log('');
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
const syncResult = await runPythonSync('sync', config);
|
|
396
|
-
|
|
397
|
-
if (syncResult.success) {
|
|
398
|
-
saveToolsCache(syncResult);
|
|
399
|
-
success(`Synced ${syncResult.tools.length} tools`);
|
|
400
|
-
|
|
401
|
-
if (syncResult.by_type && Object.keys(syncResult.by_type).length > 0) {
|
|
402
|
-
log('');
|
|
403
|
-
log('By type:');
|
|
404
|
-
for (const [type, count] of Object.entries(syncResult.by_type)) {
|
|
405
|
-
log(` ${type}: ${count}`);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
} else {
|
|
409
|
-
error(`Sync failed: ${syncResult.error}`);
|
|
410
|
-
}
|
|
411
|
-
} catch (e) {
|
|
412
|
-
error(`Sync failed: ${e.message}`);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
} else {
|
|
416
|
-
error(`Validation failed: ${result.error}`);
|
|
417
|
-
log('');
|
|
418
|
-
log(`${COLORS.dim}Check your credentials and try again with: tfcode setup${COLORS.reset}`);
|
|
419
|
-
}
|
|
420
|
-
} catch (e) {
|
|
421
|
-
error(`Validation failed: ${e.message}`);
|
|
422
|
-
log('');
|
|
423
|
-
log(`${COLORS.dim}Make sure Python 3.10+ and toothfairyai SDK are installed:${COLORS.reset}`);
|
|
424
|
-
log(`${COLORS.dim} pip install toothfairyai pydantic httpx rich${COLORS.reset}`);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
log('');
|
|
429
|
-
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
430
|
-
log(`${COLORS.bold}${COLORS.green} Setup Complete!${COLORS.reset}`);
|
|
431
|
-
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
432
|
-
log('');
|
|
433
|
-
log('You can now use tfcode:');
|
|
434
|
-
log('');
|
|
435
|
-
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} ${COLORS.dim}Check credentials${COLORS.reset}`);
|
|
436
|
-
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} ${COLORS.dim}Sync tools${COLORS.reset}`);
|
|
437
|
-
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} ${COLORS.dim}List your tools${COLORS.reset}`);
|
|
438
|
-
log('');
|
|
439
|
-
|
|
440
|
-
} else {
|
|
441
|
-
log('Setup cancelled.');
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const cli = yargs(hideBin(process.argv))
|
|
446
|
-
.scriptName('tfcode')
|
|
447
|
-
.wrap(100)
|
|
448
|
-
.help()
|
|
449
|
-
.alias('help', 'h')
|
|
450
|
-
.command({
|
|
451
|
-
command: '*',
|
|
452
|
-
describe: 'start tfcode TUI',
|
|
453
|
-
handler: () => {
|
|
454
|
-
// Try to run the full TUI
|
|
455
|
-
// Check if we're in the source repo
|
|
456
|
-
const possiblePaths = [
|
|
457
|
-
join(__dirname, '..', 'src', 'index.ts'),
|
|
458
|
-
join(__dirname, '..', '..', '..', 'src', 'index.ts'),
|
|
459
|
-
];
|
|
460
|
-
|
|
461
|
-
let srcPath = null;
|
|
462
|
-
for (const p of possiblePaths) {
|
|
463
|
-
if (existsSync(p)) {
|
|
464
|
-
srcPath = p;
|
|
465
|
-
break;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (srcPath) {
|
|
470
|
-
// Run via bun or tsx
|
|
471
|
-
const runner = process.env.TFCODE_RUNNER || (existsSync('/usr/local/bin/bun') ? 'bun' : 'npx');
|
|
472
|
-
const args = runner === 'bun' ? ['run', srcPath] : ['tsx', srcPath];
|
|
473
|
-
|
|
474
|
-
const child = spawn(runner, args, {
|
|
475
|
-
stdio: 'inherit',
|
|
476
|
-
cwd: __dirname,
|
|
477
|
-
env: { ...process.env }
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
child.on('exit', (code) => {
|
|
481
|
-
process.exit(code || 0);
|
|
482
|
-
});
|
|
483
|
-
} else {
|
|
484
|
-
// Not in source repo - show message
|
|
485
|
-
log('');
|
|
486
|
-
log(`${COLORS.bold}${COLORS.cyan}tfcode${COLORS.reset} - ToothFairyAI's AI coding assistant`);
|
|
487
|
-
log('');
|
|
488
|
-
log('The full TUI requires the compiled binary.');
|
|
489
|
-
log('');
|
|
490
|
-
log('Available commands:');
|
|
491
|
-
log(` ${COLORS.cyan}tfcode setup${COLORS.reset} Interactive credential setup`);
|
|
492
|
-
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Test your credentials`);
|
|
493
|
-
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools from workspace`);
|
|
494
|
-
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List synced tools`);
|
|
495
|
-
log('');
|
|
496
|
-
log(`${COLORS.dim}For full TUI, build from source:${COLORS.reset}`);
|
|
497
|
-
log(`${COLORS.dim} git clone https://github.com/ToothFairyAI/tfcode${COLORS.reset}`);
|
|
498
|
-
log(`${COLORS.dim} cd tfcode/packages/tfcode && bun install && bun run src/index.ts${COLORS.reset}`);
|
|
499
|
-
log('');
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
})
|
|
503
|
-
.command({
|
|
504
|
-
command: 'setup',
|
|
505
|
-
describe: 'interactive credential setup',
|
|
506
|
-
handler: async () => {
|
|
507
|
-
await interactiveSetup();
|
|
508
|
-
}
|
|
509
|
-
})
|
|
510
|
-
.command({
|
|
511
|
-
command: 'quickstart',
|
|
512
|
-
describe: 'show quick start guide',
|
|
513
|
-
handler: () => {
|
|
514
|
-
log('');
|
|
515
|
-
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
516
|
-
log(`${COLORS.bold} tfcode - Quick Start Guide${COLORS.reset}`);
|
|
517
|
-
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
518
|
-
log('');
|
|
519
|
-
log('Welcome to tfcode! Follow these steps to get started:');
|
|
520
|
-
log('');
|
|
521
|
-
log(`${COLORS.cyan}OPTION A: Interactive Setup (Recommended)${COLORS.reset}`);
|
|
522
|
-
log(`${COLORS.dim} tfcode setup${COLORS.reset}`);
|
|
523
|
-
log('');
|
|
524
|
-
log(`${COLORS.cyan}OPTION B: Manual Setup${COLORS.reset}`);
|
|
525
|
-
log(`${COLORS.dim} export TF_WORKSPACE_ID="your-workspace-id"${COLORS.reset}`);
|
|
526
|
-
log(`${COLORS.dim} export TF_API_KEY="your-api-key"${COLORS.reset}`);
|
|
527
|
-
log(`${COLORS.dim} export TF_REGION="au"${COLORS.reset}`);
|
|
528
|
-
log('');
|
|
529
|
-
log(`${COLORS.cyan}Then:${COLORS.reset}`);
|
|
530
|
-
log(`${COLORS.dim} tfcode validate${COLORS.reset}`);
|
|
531
|
-
log(`${COLORS.dim} tfcode sync${COLORS.reset}`);
|
|
532
|
-
log('');
|
|
533
|
-
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
534
|
-
log('');
|
|
535
|
-
log(' Commands:');
|
|
536
|
-
log('');
|
|
537
|
-
log(' tfcode setup Interactive credential setup');
|
|
538
|
-
log(' tfcode validate Test your credentials');
|
|
539
|
-
log(' tfcode sync Sync tools from workspace');
|
|
540
|
-
log(' tfcode tools list Show all your tools');
|
|
541
|
-
log('');
|
|
542
|
-
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
543
|
-
log('');
|
|
544
|
-
log(' Need help? https://toothfairyai.com/developers/tfcode');
|
|
545
|
-
log('');
|
|
546
|
-
}
|
|
547
|
-
})
|
|
548
|
-
.command({
|
|
549
|
-
command: 'validate',
|
|
550
|
-
describe: 'validate ToothFairyAI credentials',
|
|
551
|
-
handler: async () => {
|
|
552
|
-
const config = loadConfig();
|
|
553
|
-
|
|
554
|
-
if (!config) {
|
|
555
|
-
error('No credentials found.');
|
|
556
|
-
log('');
|
|
557
|
-
log('Run interactive setup:');
|
|
558
|
-
log(` ${COLORS.cyan}tfcode setup${COLORS.reset}`);
|
|
559
|
-
log('');
|
|
560
|
-
log('Or set environment variables:');
|
|
561
|
-
log(` ${COLORS.dim}export TF_WORKSPACE_ID="your-workspace-id"${COLORS.reset}`);
|
|
562
|
-
log(` ${COLORS.dim}export TF_API_KEY="your-api-key"${COLORS.reset}`);
|
|
563
|
-
process.exit(1);
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
info('Validating ToothFairyAI credentials...');
|
|
568
|
-
log('');
|
|
569
|
-
|
|
570
|
-
try {
|
|
571
|
-
const result = await runPythonSync('validate', config);
|
|
572
|
-
|
|
573
|
-
if (result.success) {
|
|
574
|
-
success('Credentials valid');
|
|
575
|
-
if (result.base_url) {
|
|
576
|
-
log(`${COLORS.dim} API URL: ${result.base_url}${COLORS.reset}`);
|
|
577
|
-
}
|
|
578
|
-
if (result.workspace_id) {
|
|
579
|
-
log(`${COLORS.dim} Workspace ID: ${result.workspace_id}${COLORS.reset}`);
|
|
580
|
-
}
|
|
581
|
-
} else {
|
|
582
|
-
error(`Validation failed: ${result.error || 'Unknown error'}`);
|
|
583
|
-
process.exit(1);
|
|
584
|
-
}
|
|
585
|
-
} catch (e) {
|
|
586
|
-
error(`Failed to validate: ${e.message}`);
|
|
587
|
-
log('');
|
|
588
|
-
log(`${COLORS.dim}Make sure Python 3.10+ and toothfairyai SDK are installed:${COLORS.reset}`);
|
|
589
|
-
log(`${COLORS.dim} pip install toothfairyai pydantic httpx rich${COLORS.reset}`);
|
|
590
|
-
process.exit(1);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
})
|
|
594
|
-
.command({
|
|
595
|
-
command: 'sync',
|
|
596
|
-
describe: 'sync tools from ToothFairyAI workspace',
|
|
597
|
-
handler: async () => {
|
|
598
|
-
const config = loadConfig();
|
|
599
|
-
|
|
600
|
-
if (!config) {
|
|
601
|
-
error('No credentials found. Run: tfcode setup');
|
|
602
|
-
process.exit(1);
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
info('Syncing tools from ToothFairyAI workspace...');
|
|
607
|
-
log('');
|
|
608
|
-
|
|
609
|
-
try {
|
|
610
|
-
const result = await runPythonSync('sync', config);
|
|
611
|
-
|
|
612
|
-
if (result.success) {
|
|
613
|
-
saveToolsCache(result);
|
|
614
|
-
success(`Synced ${result.tools.length} tools`);
|
|
615
|
-
log('');
|
|
616
|
-
|
|
617
|
-
if (result.by_type && Object.keys(result.by_type).length > 0) {
|
|
618
|
-
log('By type:');
|
|
619
|
-
for (const [type, count] of Object.entries(result.by_type)) {
|
|
620
|
-
log(` ${type}: ${count}`);
|
|
621
|
-
}
|
|
622
|
-
log('');
|
|
623
|
-
}
|
|
624
|
-
} else {
|
|
625
|
-
error(`Sync failed: ${result.error || 'Unknown error'}`);
|
|
626
|
-
process.exit(1);
|
|
627
|
-
}
|
|
628
|
-
} catch (e) {
|
|
629
|
-
error(`Failed to sync: ${e.message}`);
|
|
630
|
-
process.exit(1);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
})
|
|
634
|
-
.command({
|
|
635
|
-
command: 'tools',
|
|
636
|
-
describe: 'manage tools',
|
|
637
|
-
builder: (yargs) => {
|
|
638
|
-
return yargs
|
|
639
|
-
.command({
|
|
640
|
-
command: 'list',
|
|
641
|
-
describe: 'list synced tools',
|
|
642
|
-
builder: (yargs) => {
|
|
643
|
-
return yargs.option('type', {
|
|
644
|
-
type: 'string',
|
|
645
|
-
describe: 'filter by type (api_function)'
|
|
646
|
-
});
|
|
647
|
-
},
|
|
648
|
-
handler: (args) => {
|
|
649
|
-
const cached = loadCachedTools();
|
|
650
|
-
|
|
651
|
-
if (!cached || !cached.success) {
|
|
652
|
-
error('No tools synced. Run \'tfcode sync\' first.');
|
|
653
|
-
process.exit(1);
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
let tools = cached.tools;
|
|
658
|
-
|
|
659
|
-
if (args.type) {
|
|
660
|
-
tools = tools.filter(t => t.tool_type === args.type);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
if (tools.length === 0) {
|
|
664
|
-
log('No tools found.');
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
log('');
|
|
669
|
-
log(`${tools.length} tool(s):`);
|
|
670
|
-
log('');
|
|
671
|
-
|
|
672
|
-
for (const tool of tools) {
|
|
673
|
-
log(` ${COLORS.cyan}${tool.name}${COLORS.reset}`);
|
|
674
|
-
log(` Type: ${tool.tool_type}`);
|
|
675
|
-
if (tool.description) {
|
|
676
|
-
log(` ${COLORS.dim}${tool.description.slice(0, 60)}${tool.description.length > 60 ? '...' : ''}${COLORS.reset}`);
|
|
677
|
-
}
|
|
678
|
-
log(` Auth: ${tool.auth_via}`);
|
|
679
|
-
log('');
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
})
|
|
683
|
-
.demandCommand();
|
|
684
|
-
},
|
|
685
|
-
handler: () => {}
|
|
686
|
-
})
|
|
687
|
-
.demandCommand()
|
|
688
|
-
.strict()
|
|
689
|
-
.fail((msg, err) => {
|
|
690
|
-
if (msg) {
|
|
691
|
-
error(msg);
|
|
692
|
-
}
|
|
693
|
-
if (err) {
|
|
694
|
-
error(err.message);
|
|
695
|
-
}
|
|
696
|
-
process.exit(1);
|
|
697
|
-
});
|
|
16
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
17
|
+
stdio: "inherit",
|
|
18
|
+
env: process.env
|
|
19
|
+
})
|
|
698
20
|
|
|
699
|
-
|
|
21
|
+
child.on("exit", (code) => process.exit(code || 0))
|