@toothfairyai/tfcode 1.0.0-beta.8 → 1.0.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/LICENSE +21 -0
- package/bin/tfcode.js +168 -569
- package/package.json +19 -19
- package/postinstall.mjs +232 -0
- package/README.md +0 -146
- package/scripts/postinstall.cjs +0 -171
- package/scripts/quickstart.cjs +0 -66
package/bin/tfcode.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import yargs from 'yargs';
|
|
4
|
-
import { hideBin } from 'yargs/helpers';
|
|
5
3
|
import { spawn } from 'child_process';
|
|
6
4
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
7
|
-
import { join } from 'path';
|
|
5
|
+
import { join, dirname } from 'path';
|
|
8
6
|
import { homedir } from 'os';
|
|
9
7
|
import * as readline from 'readline';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
10
12
|
|
|
11
13
|
const TFCODE_DIR = join(homedir(), '.tfcode');
|
|
12
14
|
const TOOLS_FILE = join(TFCODE_DIR, 'tools.json');
|
|
@@ -24,109 +26,25 @@ const COLORS = {
|
|
|
24
26
|
magenta: '\x1b[35m'
|
|
25
27
|
};
|
|
26
28
|
|
|
27
|
-
function log(msg) {
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function success(msg) {
|
|
32
|
-
console.log(`${COLORS.green}✓${COLORS.reset} ${msg}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function error(msg) {
|
|
36
|
-
console.error(`${COLORS.red}✗${COLORS.reset} ${msg}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function info(msg) {
|
|
40
|
-
console.log(`${COLORS.cyan}ℹ${COLORS.reset} ${msg}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function question(rl, prompt, hidden = false) {
|
|
44
|
-
return new Promise((resolve) => {
|
|
45
|
-
if (hidden && process.stdin.isTTY) {
|
|
46
|
-
// Use simpler approach for hidden input
|
|
47
|
-
process.stdout.write(prompt);
|
|
48
|
-
|
|
49
|
-
let input = '';
|
|
50
|
-
const stdin = process.stdin;
|
|
51
|
-
|
|
52
|
-
stdin.setRawMode(true);
|
|
53
|
-
stdin.setEncoding('utf8');
|
|
54
|
-
stdin.resume();
|
|
55
|
-
|
|
56
|
-
const onKeypress = (str) => {
|
|
57
|
-
if (str === '\n' || str === '\r' || str === '\u0004') {
|
|
58
|
-
stdin.setRawMode(false);
|
|
59
|
-
stdin.pause();
|
|
60
|
-
stdin.removeListener('data', onKeypress);
|
|
61
|
-
process.stdout.write('\n');
|
|
62
|
-
resolve(input);
|
|
63
|
-
} else if (str === '\u0003') {
|
|
64
|
-
process.stdout.write('\n');
|
|
65
|
-
process.exit();
|
|
66
|
-
} else if (str === '\u007F' || str === '\b') {
|
|
67
|
-
input = input.slice(0, -1);
|
|
68
|
-
} else if (str.length === 1 && str.charCodeAt(0) >= 32) {
|
|
69
|
-
input += str;
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
stdin.on('data', onKeypress);
|
|
74
|
-
} else {
|
|
75
|
-
rl.question(prompt, (answer) => {
|
|
76
|
-
resolve(answer.trim());
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function select(prompt, options) {
|
|
83
|
-
return new Promise((resolve) => {
|
|
84
|
-
log('');
|
|
85
|
-
log(prompt);
|
|
86
|
-
log('');
|
|
87
|
-
options.forEach((opt, i) => {
|
|
88
|
-
log(` ${COLORS.cyan}${i + 1}.${COLORS.reset} ${opt}`);
|
|
89
|
-
});
|
|
90
|
-
log('');
|
|
91
|
-
|
|
92
|
-
// Create a fresh readline for select
|
|
93
|
-
const rlSelect = readline.createInterface({
|
|
94
|
-
input: process.stdin,
|
|
95
|
-
output: process.stdout
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
rlSelect.question('Select (1-' + options.length + '): ', (answer) => {
|
|
99
|
-
rlSelect.close();
|
|
100
|
-
const idx = parseInt(answer.trim()) - 1;
|
|
101
|
-
resolve(idx >= 0 && idx < options.length ? idx : 0);
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
}
|
|
29
|
+
function log(msg) { console.log(msg); }
|
|
30
|
+
function success(msg) { console.log(`${COLORS.green}✓${COLORS.reset} ${msg}`); }
|
|
31
|
+
function error(msg) { console.error(`${COLORS.red}✗${COLORS.reset} ${msg}`); }
|
|
32
|
+
function info(msg) { console.log(`${COLORS.cyan}ℹ${COLORS.reset} ${msg}`); }
|
|
105
33
|
|
|
106
34
|
function ensureConfigDir() {
|
|
107
|
-
if (!existsSync(TFCODE_DIR)) {
|
|
108
|
-
mkdirSync(TFCODE_DIR, { recursive: true });
|
|
109
|
-
}
|
|
35
|
+
if (!existsSync(TFCODE_DIR)) mkdirSync(TFCODE_DIR, { recursive: true });
|
|
110
36
|
}
|
|
111
37
|
|
|
112
38
|
function loadConfig() {
|
|
113
|
-
// Priority: env vars > config file
|
|
114
39
|
const envConfig = {
|
|
115
40
|
workspace_id: process.env.TF_WORKSPACE_ID,
|
|
116
41
|
api_key: process.env.TF_API_KEY,
|
|
117
42
|
region: process.env.TF_REGION
|
|
118
43
|
};
|
|
119
|
-
|
|
120
|
-
if (envConfig.workspace_id && envConfig.api_key) {
|
|
121
|
-
return envConfig;
|
|
122
|
-
}
|
|
123
|
-
|
|
44
|
+
if (envConfig.workspace_id && envConfig.api_key) return envConfig;
|
|
124
45
|
if (existsSync(CONFIG_FILE)) {
|
|
125
|
-
try {
|
|
126
|
-
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
|
|
127
|
-
} catch {}
|
|
46
|
+
try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')); } catch {}
|
|
128
47
|
}
|
|
129
|
-
|
|
130
48
|
return null;
|
|
131
49
|
}
|
|
132
50
|
|
|
@@ -141,107 +59,46 @@ function runPythonSync(method, config = null) {
|
|
|
141
59
|
const region = config?.region || process.env.TF_REGION || 'au';
|
|
142
60
|
|
|
143
61
|
const pythonCode = `
|
|
144
|
-
import json
|
|
145
|
-
import sys
|
|
146
|
-
import os
|
|
147
|
-
|
|
62
|
+
import json, sys, os
|
|
148
63
|
try:
|
|
149
64
|
os.environ["TF_WORKSPACE_ID"] = "${wsId}"
|
|
150
65
|
os.environ["TF_API_KEY"] = "${apiKey}"
|
|
151
66
|
os.environ["TF_REGION"] = "${region}"
|
|
152
|
-
|
|
153
67
|
from tf_sync.config import load_config, validate_credentials, Region
|
|
154
68
|
from tf_sync.tools import sync_tools
|
|
155
69
|
from tf_sync.config import get_region_urls
|
|
156
70
|
|
|
157
71
|
method = "${method}"
|
|
158
|
-
|
|
159
72
|
if method == "validate":
|
|
160
73
|
config = load_config()
|
|
161
74
|
result = validate_credentials(config)
|
|
162
75
|
urls = get_region_urls(config.region)
|
|
163
|
-
print(json.dumps({
|
|
164
|
-
"success": result.success,
|
|
165
|
-
"workspace_id": result.workspace_id,
|
|
166
|
-
"workspace_name": result.workspace_name,
|
|
167
|
-
"error": result.error,
|
|
168
|
-
"base_url": urls["base_url"]
|
|
169
|
-
}))
|
|
170
|
-
|
|
76
|
+
print(json.dumps({"success": result.success, "workspace_id": result.workspace_id, "workspace_name": result.workspace_name, "error": result.error, "base_url": urls["base_url"]}))
|
|
171
77
|
elif method == "sync":
|
|
172
78
|
config = load_config()
|
|
173
79
|
result = sync_tools(config)
|
|
174
|
-
|
|
175
|
-
tools_data
|
|
176
|
-
for tool in result.tools:
|
|
177
|
-
tools_data.append({
|
|
178
|
-
"id": tool.id,
|
|
179
|
-
"name": tool.name,
|
|
180
|
-
"description": tool.description,
|
|
181
|
-
"tool_type": tool.tool_type.value,
|
|
182
|
-
"request_type": tool.request_type.value if tool.request_type else None,
|
|
183
|
-
"url": tool.url,
|
|
184
|
-
"auth_via": tool.auth_via
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
print(json.dumps({
|
|
188
|
-
"success": result.success,
|
|
189
|
-
"tools": tools_data,
|
|
190
|
-
"by_type": result.by_type,
|
|
191
|
-
"error": result.error
|
|
192
|
-
}))
|
|
193
|
-
|
|
80
|
+
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} for t in result.tools]
|
|
81
|
+
print(json.dumps({"success": result.success, "tools": tools_data, "by_type": result.by_type, "error": result.error}))
|
|
194
82
|
except Exception as e:
|
|
195
83
|
print(json.dumps({"success": False, "error": str(e)}))
|
|
196
|
-
sys.exit(0)
|
|
197
84
|
`;
|
|
198
85
|
|
|
199
86
|
return new Promise((resolve, reject) => {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
let stdout = '';
|
|
206
|
-
let stderr = '';
|
|
207
|
-
|
|
208
|
-
proc.stdout.on('data', (data) => {
|
|
209
|
-
stdout += data.toString();
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
proc.stderr.on('data', (data) => {
|
|
213
|
-
stderr += data.toString();
|
|
214
|
-
});
|
|
215
|
-
|
|
87
|
+
const proc = spawn(process.env.TFCODE_PYTHON_PATH || 'python3', ['-c', pythonCode], { env: { ...process.env } });
|
|
88
|
+
let stdout = '', stderr = '';
|
|
89
|
+
proc.stdout.on('data', (d) => stdout += d);
|
|
90
|
+
proc.stderr.on('data', (d) => stderr += d);
|
|
216
91
|
proc.on('close', (code) => {
|
|
217
|
-
if (code !== 0 && !stdout) {
|
|
218
|
-
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
const result = JSON.parse(stdout.trim());
|
|
224
|
-
resolve(result);
|
|
225
|
-
} catch (e) {
|
|
226
|
-
reject(new Error(`Failed to parse Python output: ${stdout}\nstderr: ${stderr}`));
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
proc.on('error', (err) => {
|
|
231
|
-
reject(err);
|
|
92
|
+
if (code !== 0 && !stdout) reject(new Error(`Python failed: ${stderr}`));
|
|
93
|
+
else try { resolve(JSON.parse(stdout.trim())); } catch (e) { reject(new Error(`Parse error: ${stdout}`)); }
|
|
232
94
|
});
|
|
95
|
+
proc.on('error', reject);
|
|
233
96
|
});
|
|
234
97
|
}
|
|
235
98
|
|
|
236
99
|
function loadCachedTools() {
|
|
237
|
-
if (!existsSync(TOOLS_FILE))
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
try {
|
|
241
|
-
return JSON.parse(readFileSync(TOOLS_FILE, 'utf-8'));
|
|
242
|
-
} catch {
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
100
|
+
if (!existsSync(TOOLS_FILE)) return null;
|
|
101
|
+
try { return JSON.parse(readFileSync(TOOLS_FILE, 'utf-8')); } catch { return null; }
|
|
245
102
|
}
|
|
246
103
|
|
|
247
104
|
function saveToolsCache(tools) {
|
|
@@ -249,6 +106,24 @@ function saveToolsCache(tools) {
|
|
|
249
106
|
writeFileSync(TOOLS_FILE, JSON.stringify(tools, null, 2));
|
|
250
107
|
}
|
|
251
108
|
|
|
109
|
+
async function question(prompt) {
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
112
|
+
rl.question(prompt, (answer) => { rl.close(); resolve(answer.trim()); });
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function select(prompt, options) {
|
|
117
|
+
log('');
|
|
118
|
+
log(prompt);
|
|
119
|
+
log('');
|
|
120
|
+
options.forEach((opt, i) => log(` ${COLORS.cyan}${i + 1}.${COLORS.reset} ${opt}`));
|
|
121
|
+
log('');
|
|
122
|
+
const answer = await question('Select (1-' + options.length + '): ');
|
|
123
|
+
const idx = parseInt(answer) - 1;
|
|
124
|
+
return idx >= 0 && idx < options.length ? idx : 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
252
127
|
async function interactiveSetup() {
|
|
253
128
|
log('');
|
|
254
129
|
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
@@ -261,439 +136,163 @@ async function interactiveSetup() {
|
|
|
261
136
|
log(`${COLORS.dim} https://app.toothfairyai.com → Settings → API Keys${COLORS.reset}`);
|
|
262
137
|
log('');
|
|
263
138
|
|
|
264
|
-
// Step 1: Workspace ID
|
|
265
139
|
log(`${COLORS.bold}Step 1: Workspace ID${COLORS.reset}`);
|
|
266
|
-
log(`${COLORS.dim}This is your workspace UUID
|
|
140
|
+
log(`${COLORS.dim}This is your workspace UUID${COLORS.reset}`);
|
|
267
141
|
log('');
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const rl = readline.createInterface({
|
|
271
|
-
input: process.stdin,
|
|
272
|
-
output: process.stdout
|
|
273
|
-
});
|
|
274
|
-
rl.question('Enter your Workspace ID: ', (answer) => {
|
|
275
|
-
rl.close();
|
|
276
|
-
resolve(answer.trim());
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
if (!workspaceId) {
|
|
281
|
-
error('Workspace ID is required');
|
|
282
|
-
process.exit(1);
|
|
283
|
-
}
|
|
284
|
-
|
|
142
|
+
const workspaceId = await question('Enter your Workspace ID: ');
|
|
143
|
+
if (!workspaceId) { error('Workspace ID is required'); process.exit(1); }
|
|
285
144
|
log('');
|
|
286
145
|
|
|
287
|
-
// Step 2: API Key
|
|
288
146
|
log(`${COLORS.bold}Step 2: API Key${COLORS.reset}`);
|
|
289
147
|
log(`${COLORS.dim}Paste or type your API key${COLORS.reset}`);
|
|
290
148
|
log('');
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const rl = readline.createInterface({
|
|
294
|
-
input: process.stdin,
|
|
295
|
-
output: process.stdout
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
// Simple approach - just show the input (API keys are long anyway)
|
|
299
|
-
// This allows paste and works reliably
|
|
300
|
-
rl.question('Enter your API Key: ', (answer) => {
|
|
301
|
-
rl.close();
|
|
302
|
-
resolve(answer.trim());
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
if (!apiKey) {
|
|
307
|
-
error('API Key is required');
|
|
308
|
-
process.exit(1);
|
|
309
|
-
}
|
|
310
|
-
|
|
149
|
+
const apiKey = await question('Enter your API Key: ');
|
|
150
|
+
if (!apiKey) { error('API Key is required'); process.exit(1); }
|
|
311
151
|
log('');
|
|
312
152
|
|
|
313
|
-
// Step 3: Region
|
|
314
153
|
log(`${COLORS.bold}Step 3: Region${COLORS.reset}`);
|
|
315
154
|
const regions = ['dev (Development)', 'au (Australia)', 'eu (Europe)', 'us (United States)'];
|
|
316
155
|
const regionIdx = await select('Select your region:', regions);
|
|
317
|
-
const
|
|
318
|
-
const region = regions_map[regionIdx];
|
|
156
|
+
const region = ['dev', 'au', 'eu', 'us'][regionIdx];
|
|
319
157
|
|
|
320
158
|
log('');
|
|
321
159
|
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
322
160
|
log('');
|
|
323
|
-
|
|
324
|
-
// Summary
|
|
325
161
|
log(`${COLORS.bold}Summary:${COLORS.reset}`);
|
|
326
162
|
log(` Workspace ID: ${workspaceId}`);
|
|
327
163
|
log(` API Key: ***${apiKey.slice(-4)}`);
|
|
328
164
|
log(` Region: ${region}`);
|
|
329
165
|
log('');
|
|
330
166
|
|
|
331
|
-
const confirm = await
|
|
332
|
-
|
|
333
|
-
input: process.stdin,
|
|
334
|
-
output: process.stdout
|
|
335
|
-
});
|
|
336
|
-
rl.question('Save these credentials? (Y/n): ', (answer) => {
|
|
337
|
-
rl.close();
|
|
338
|
-
resolve(answer.trim());
|
|
339
|
-
});
|
|
340
|
-
});
|
|
167
|
+
const confirm = await question('Save these credentials? (Y/n): ');
|
|
168
|
+
if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') { log('Setup cancelled.'); return; }
|
|
341
169
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (testNow.toLowerCase() !== 'n' && testNow.toLowerCase() !== 'no') {
|
|
361
|
-
log('');
|
|
362
|
-
info('Validating credentials...');
|
|
170
|
+
const config = { workspace_id: workspaceId, api_key: apiKey, region };
|
|
171
|
+
saveConfig(config);
|
|
172
|
+
success('Credentials saved to ~/.tfcode/config.json');
|
|
173
|
+
log('');
|
|
174
|
+
|
|
175
|
+
const testNow = await question('Validate credentials now? (Y/n): ');
|
|
176
|
+
if (testNow.toLowerCase() === 'n' || testNow.toLowerCase() === 'no') return;
|
|
177
|
+
|
|
178
|
+
log('');
|
|
179
|
+
info('Validating credentials...');
|
|
180
|
+
log('');
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const result = await runPythonSync('validate', config);
|
|
184
|
+
if (result.success) {
|
|
185
|
+
success('Credentials valid!');
|
|
186
|
+
log(` API URL: ${result.base_url}`);
|
|
187
|
+
log(` Workspace ID: ${result.workspace_id}`);
|
|
363
188
|
log('');
|
|
364
189
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (result.success) {
|
|
369
|
-
success('Credentials valid!');
|
|
370
|
-
log(` API URL: ${result.base_url}`);
|
|
371
|
-
log(` Workspace ID: ${result.workspace_id}`);
|
|
372
|
-
log('');
|
|
373
|
-
|
|
374
|
-
const syncNow = await new Promise((resolve) => {
|
|
375
|
-
const rl = readline.createInterface({
|
|
376
|
-
input: process.stdin,
|
|
377
|
-
output: process.stdout
|
|
378
|
-
});
|
|
379
|
-
rl.question('Sync tools now? (Y/n): ', (answer) => {
|
|
380
|
-
rl.close();
|
|
381
|
-
resolve(answer.trim());
|
|
382
|
-
});
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
if (syncNow.toLowerCase() !== 'n' && syncNow.toLowerCase() !== 'no') {
|
|
386
|
-
log('');
|
|
387
|
-
info('Syncing tools...');
|
|
388
|
-
log('');
|
|
389
|
-
|
|
390
|
-
try {
|
|
391
|
-
const syncResult = await runPythonSync('sync', config);
|
|
392
|
-
|
|
393
|
-
if (syncResult.success) {
|
|
394
|
-
saveToolsCache(syncResult);
|
|
395
|
-
success(`Synced ${syncResult.tools.length} tools`);
|
|
396
|
-
|
|
397
|
-
if (syncResult.by_type && Object.keys(syncResult.by_type).length > 0) {
|
|
398
|
-
log('');
|
|
399
|
-
log('By type:');
|
|
400
|
-
for (const [type, count] of Object.entries(syncResult.by_type)) {
|
|
401
|
-
log(` ${type}: ${count}`);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
} else {
|
|
405
|
-
error(`Sync failed: ${syncResult.error}`);
|
|
406
|
-
}
|
|
407
|
-
} catch (e) {
|
|
408
|
-
error(`Sync failed: ${e.message}`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
} else {
|
|
412
|
-
error(`Validation failed: ${result.error}`);
|
|
413
|
-
log('');
|
|
414
|
-
log(`${COLORS.dim}Check your credentials and try again with: tfcode setup${COLORS.reset}`);
|
|
415
|
-
}
|
|
416
|
-
} catch (e) {
|
|
417
|
-
error(`Validation failed: ${e.message}`);
|
|
418
|
-
log('');
|
|
419
|
-
log(`${COLORS.dim}Make sure Python 3.10+ and toothfairyai SDK are installed:${COLORS.reset}`);
|
|
420
|
-
log(`${COLORS.dim} pip install toothfairyai pydantic httpx rich${COLORS.reset}`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
log('');
|
|
425
|
-
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
426
|
-
log(`${COLORS.bold}${COLORS.green} Setup Complete!${COLORS.reset}`);
|
|
427
|
-
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
428
|
-
log('');
|
|
429
|
-
log('You can now use tfcode:');
|
|
430
|
-
log('');
|
|
431
|
-
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} ${COLORS.dim}Check credentials${COLORS.reset}`);
|
|
432
|
-
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} ${COLORS.dim}Sync tools${COLORS.reset}`);
|
|
433
|
-
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} ${COLORS.dim}List your tools${COLORS.reset}`);
|
|
434
|
-
log('');
|
|
435
|
-
|
|
436
|
-
} else {
|
|
437
|
-
log('Setup cancelled.');
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const cli = yargs(hideBin(process.argv))
|
|
442
|
-
.scriptName('tfcode')
|
|
443
|
-
.wrap(100)
|
|
444
|
-
.help()
|
|
445
|
-
.alias('help', 'h')
|
|
446
|
-
.command({
|
|
447
|
-
command: '*',
|
|
448
|
-
describe: 'start tfcode TUI',
|
|
449
|
-
handler: async () => {
|
|
450
|
-
// Try to run the full TUI
|
|
451
|
-
const { spawn } = await import('child_process');
|
|
452
|
-
const { existsSync } = await import('fs');
|
|
453
|
-
const { join } = await import('path');
|
|
454
|
-
|
|
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
|
-
}
|
|
190
|
+
const syncNow = await question('Sync tools now? (Y/n): ');
|
|
191
|
+
if (syncNow.toLowerCase() === 'n' || syncNow.toLowerCase() === 'no') return;
|
|
468
192
|
|
|
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
193
|
log('');
|
|
544
|
-
|
|
194
|
+
info('Syncing tools...');
|
|
545
195
|
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
196
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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`);
|
|
197
|
+
const syncResult = await runPythonSync('sync', config);
|
|
198
|
+
if (syncResult.success) {
|
|
199
|
+
saveToolsCache(syncResult);
|
|
200
|
+
success(`Synced ${syncResult.tools.length} tools`);
|
|
201
|
+
if (syncResult.by_type && Object.keys(syncResult.by_type).length > 0) {
|
|
615
202
|
log('');
|
|
616
|
-
|
|
617
|
-
|
|
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);
|
|
203
|
+
log('By type:');
|
|
204
|
+
for (const [type, count] of Object.entries(syncResult.by_type)) log(` ${type}: ${count}`);
|
|
627
205
|
}
|
|
628
|
-
}
|
|
629
|
-
error(`
|
|
630
|
-
process.exit(1);
|
|
206
|
+
} else {
|
|
207
|
+
error(`Sync failed: ${syncResult.error}`);
|
|
631
208
|
}
|
|
209
|
+
} else {
|
|
210
|
+
error(`Validation failed: ${result.error}`);
|
|
632
211
|
}
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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('');
|
|
212
|
+
} catch (e) {
|
|
213
|
+
error(`Failed: ${e.message}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
log('');
|
|
217
|
+
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
218
|
+
log(`${COLORS.bold}${COLORS.green} Setup Complete!${COLORS.reset}`);
|
|
219
|
+
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
220
|
+
log('');
|
|
221
|
+
log('Commands:');
|
|
222
|
+
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Check credentials`);
|
|
223
|
+
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools`);
|
|
224
|
+
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List tools`);
|
|
225
|
+
log('');
|
|
226
|
+
}
|
|
671
227
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
});
|
|
228
|
+
function showHelp() {
|
|
229
|
+
log('');
|
|
230
|
+
log(`${COLORS.bold}tfcode${COLORS.reset} - ToothFairyAI's AI coding agent`);
|
|
231
|
+
log('');
|
|
232
|
+
log('Commands:');
|
|
233
|
+
log(` ${COLORS.cyan}tfcode setup${COLORS.reset} Interactive credential setup`);
|
|
234
|
+
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Test credentials`);
|
|
235
|
+
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools from workspace`);
|
|
236
|
+
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List synced tools`);
|
|
237
|
+
log(` ${COLORS.cyan}tfcode --help${COLORS.reset} Show this help`);
|
|
238
|
+
log(` ${COLORS.cyan}tfcode --version${COLORS.reset} Show version`);
|
|
239
|
+
log('');
|
|
240
|
+
log(`${COLORS.dim}For full TUI, run from source:${COLORS.reset}`);
|
|
241
|
+
log(`${COLORS.dim} bun run packages/tfcode/src/index.ts${COLORS.reset}`);
|
|
242
|
+
log('');
|
|
243
|
+
}
|
|
698
244
|
|
|
699
|
-
|
|
245
|
+
const args = process.argv.slice(2);
|
|
246
|
+
const command = args[0];
|
|
247
|
+
|
|
248
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
249
|
+
showHelp();
|
|
250
|
+
} else if (args.includes('--version') || args.includes('-v')) {
|
|
251
|
+
log('tfcode v1.0.0-beta.9');
|
|
252
|
+
} else if (command === 'setup') {
|
|
253
|
+
interactiveSetup();
|
|
254
|
+
} else if (command === 'validate') {
|
|
255
|
+
(async () => {
|
|
256
|
+
const config = loadConfig();
|
|
257
|
+
if (!config) { error('No credentials. Run: tfcode setup'); process.exit(1); }
|
|
258
|
+
info('Validating...');
|
|
259
|
+
try {
|
|
260
|
+
const result = await runPythonSync('validate', config);
|
|
261
|
+
if (result.success) { success('Credentials valid'); log(` API URL: ${result.base_url}`); }
|
|
262
|
+
else { error(`Failed: ${result.error}`); process.exit(1); }
|
|
263
|
+
} catch (e) { error(`Failed: ${e.message}`); process.exit(1); }
|
|
264
|
+
})();
|
|
265
|
+
} else if (command === 'sync') {
|
|
266
|
+
(async () => {
|
|
267
|
+
const config = loadConfig();
|
|
268
|
+
if (!config) { error('No credentials. Run: tfcode setup'); process.exit(1); }
|
|
269
|
+
info('Syncing tools...');
|
|
270
|
+
try {
|
|
271
|
+
const result = await runPythonSync('sync', config);
|
|
272
|
+
if (result.success) {
|
|
273
|
+
saveToolsCache(result);
|
|
274
|
+
success(`Synced ${result.tools.length} tools`);
|
|
275
|
+
if (result.by_type) { log(''); log('By type:'); for (const [t, c] of Object.entries(result.by_type)) log(` ${t}: ${c}`); }
|
|
276
|
+
} else { error(`Failed: ${result.error}`); process.exit(1); }
|
|
277
|
+
} catch (e) { error(`Failed: ${e.message}`); process.exit(1); }
|
|
278
|
+
})();
|
|
279
|
+
} else if (command === 'tools' && args[1] === 'list') {
|
|
280
|
+
const cached = loadCachedTools();
|
|
281
|
+
if (!cached?.success) { error('No tools. Run: tfcode sync'); process.exit(1); }
|
|
282
|
+
let tools = cached.tools;
|
|
283
|
+
if (args[3] === '--type' && args[4]) tools = tools.filter(t => t.tool_type === args[4]);
|
|
284
|
+
log(`\n${tools.length} tool(s):\n`);
|
|
285
|
+
for (const t of tools) {
|
|
286
|
+
log(` ${COLORS.cyan}${t.name}${COLORS.reset}`);
|
|
287
|
+
log(` Type: ${t.tool_type}`);
|
|
288
|
+
if (t.description) log(` ${COLORS.dim}${t.description.slice(0, 60)}${COLORS.reset}`);
|
|
289
|
+
log(` Auth: ${t.auth_via}\n`);
|
|
290
|
+
}
|
|
291
|
+
} else if (!command) {
|
|
292
|
+
// Show help instead of trying TUI (TUI requires full build)
|
|
293
|
+
showHelp();
|
|
294
|
+
} else {
|
|
295
|
+
error(`Unknown command: ${command}`);
|
|
296
|
+
showHelp();
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|