@toothfairyai/tfcode 1.0.0-beta.1 → 1.0.0-beta.2

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.
Files changed (2) hide show
  1. package/bin/tfcode.js +307 -32
  2. package/package.json +1 -1
package/bin/tfcode.js CHANGED
@@ -6,10 +6,12 @@ import { spawn } from 'child_process';
6
6
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { homedir } from 'os';
9
+ import * as readline from 'readline';
9
10
 
10
11
  const TFCODE_DIR = join(homedir(), '.tfcode');
11
12
  const TOOLS_FILE = join(TFCODE_DIR, 'tools.json');
12
13
  const CREDENTIALS_FILE = join(TFCODE_DIR, 'credentials.json');
14
+ const CONFIG_FILE = join(TFCODE_DIR, 'config.json');
13
15
 
14
16
  const COLORS = {
15
17
  reset: '\x1b[0m',
@@ -18,7 +20,8 @@ const COLORS = {
18
20
  red: '\x1b[31m',
19
21
  cyan: '\x1b[36m',
20
22
  dim: '\x1b[90m',
21
- yellow: '\x1b[33m'
23
+ yellow: '\x1b[33m',
24
+ magenta: '\x1b[35m'
22
25
  };
23
26
 
24
27
  function log(msg) {
@@ -37,13 +40,123 @@ function info(msg) {
37
40
  console.log(`${COLORS.cyan}ℹ${COLORS.reset} ${msg}`);
38
41
  }
39
42
 
40
- function runPythonSync(method) {
43
+ function question(rl, prompt, hidden = false) {
44
+ return new Promise((resolve) => {
45
+ if (hidden) {
46
+ const stdin = process.stdin;
47
+ const wasRaw = stdin.isRaw;
48
+
49
+ process.stdout.write(prompt);
50
+
51
+ if (stdin.isTTY) {
52
+ stdin.setRawMode(true);
53
+ }
54
+
55
+ let input = '';
56
+
57
+ stdin.resume();
58
+ stdin.on('data', function charListener(char) {
59
+ const c = char.toString('utf8');
60
+
61
+ switch (c) {
62
+ case '\n':
63
+ case '\r':
64
+ case '\u0004':
65
+ if (stdin.isTTY) {
66
+ stdin.setRawMode(wasRaw || false);
67
+ }
68
+ stdin.pause();
69
+ stdin.removeListener('data', charListener);
70
+ process.stdout.write('\n');
71
+ resolve(input);
72
+ break;
73
+ case '\u0003':
74
+ process.exit();
75
+ break;
76
+ case '\u007F':
77
+ input = input.slice(0, -1);
78
+ break;
79
+ default:
80
+ input += c;
81
+ break;
82
+ }
83
+ });
84
+ } else {
85
+ rl.question(prompt, (answer) => {
86
+ resolve(answer.trim());
87
+ });
88
+ }
89
+ });
90
+ }
91
+
92
+ function select(rl, prompt, options) {
93
+ return new Promise((resolve) => {
94
+ log('');
95
+ log(prompt);
96
+ log('');
97
+ options.forEach((opt, i) => {
98
+ log(` ${COLORS.cyan}${i + 1}.${COLORS.reset} ${opt}`);
99
+ });
100
+ log('');
101
+
102
+ rl.question('Select (1-' + options.length + '): ', (answer) => {
103
+ const idx = parseInt(answer.trim()) - 1;
104
+ if (idx >= 0 && idx < options.length) {
105
+ resolve(idx);
106
+ } else {
107
+ resolve(0);
108
+ }
109
+ });
110
+ });
111
+ }
112
+
113
+ function ensureConfigDir() {
114
+ if (!existsSync(TFCODE_DIR)) {
115
+ mkdirSync(TFCODE_DIR, { recursive: true });
116
+ }
117
+ }
118
+
119
+ function loadConfig() {
120
+ // Priority: env vars > config file
121
+ const envConfig = {
122
+ workspace_id: process.env.TF_WORKSPACE_ID,
123
+ api_key: process.env.TF_API_KEY,
124
+ region: process.env.TF_REGION
125
+ };
126
+
127
+ if (envConfig.workspace_id && envConfig.api_key) {
128
+ return envConfig;
129
+ }
130
+
131
+ if (existsSync(CONFIG_FILE)) {
132
+ try {
133
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
134
+ } catch {}
135
+ }
136
+
137
+ return null;
138
+ }
139
+
140
+ function saveConfig(config) {
141
+ ensureConfigDir();
142
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
143
+ }
144
+
145
+ function runPythonSync(method, config = null) {
146
+ const wsId = config?.workspace_id || process.env.TF_WORKSPACE_ID || '';
147
+ const apiKey = config?.api_key || process.env.TF_API_KEY || '';
148
+ const region = config?.region || process.env.TF_REGION || 'au';
149
+
41
150
  const pythonCode = `
42
151
  import json
43
152
  import sys
44
153
  import os
45
154
 
46
155
  try:
156
+ os.environ["TF_WORKSPACE_ID"] = "${wsId}"
157
+ os.environ["TF_API_KEY"] = "${apiKey}"
158
+ os.environ["TF_REGION"] = "${region}"
159
+
47
160
  from tf_sync.config import load_config, validate_credentials, Region
48
161
  from tf_sync.tools import sync_tools
49
162
  from tf_sync.config import get_region_urls
@@ -93,10 +206,7 @@ sys.exit(0)
93
206
  return new Promise((resolve, reject) => {
94
207
  const pythonPath = process.env.TFCODE_PYTHON_PATH || 'python3';
95
208
  const proc = spawn(pythonPath, ['-c', pythonCode], {
96
- env: {
97
- ...process.env,
98
- PYTHONPATH: process.env.TFCODE_PYTHONPATH || ''
99
- }
209
+ env: { ...process.env }
100
210
  });
101
211
 
102
212
  let stdout = '';
@@ -130,12 +240,6 @@ sys.exit(0)
130
240
  });
131
241
  }
132
242
 
133
- function ensureConfigDir() {
134
- if (!existsSync(TFCODE_DIR)) {
135
- mkdirSync(TFCODE_DIR, { recursive: true });
136
- }
137
- }
138
-
139
243
  function loadCachedTools() {
140
244
  if (!existsSync(TOOLS_FILE)) {
141
245
  return null;
@@ -152,11 +256,166 @@ function saveToolsCache(tools) {
152
256
  writeFileSync(TOOLS_FILE, JSON.stringify(tools, null, 2));
153
257
  }
154
258
 
259
+ async function interactiveSetup() {
260
+ const rl = readline.createInterface({
261
+ input: process.stdin,
262
+ output: process.stdout
263
+ });
264
+
265
+ log('');
266
+ log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
267
+ log(`${COLORS.bold}${COLORS.magenta} tfcode Setup${COLORS.reset}`);
268
+ log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
269
+ log('');
270
+ log('This will guide you through setting up your ToothFairyAI credentials.');
271
+ log('');
272
+ log(`${COLORS.dim}You can find your credentials at:${COLORS.reset}`);
273
+ log(`${COLORS.dim} https://app.toothfairyai.com → Settings → API Keys${COLORS.reset}`);
274
+ log('');
275
+
276
+ // Workspace ID
277
+ log(`${COLORS.bold}Step 1: Workspace ID${COLORS.reset}`);
278
+ log(`${COLORS.dim}This is your workspace UUID (e.g., 12345678-1234-1234-1234-123456789012)${COLORS.reset}`);
279
+ log('');
280
+ const workspaceId = await question(rl, 'Enter your Workspace ID: ');
281
+
282
+ if (!workspaceId) {
283
+ error('Workspace ID is required');
284
+ rl.close();
285
+ process.exit(1);
286
+ }
287
+
288
+ log('');
289
+
290
+ // API Key
291
+ log(`${COLORS.bold}Step 2: API Key${COLORS.reset}`);
292
+ log(`${COLORS.dim}Your API key will be hidden as you type${COLORS.reset}`);
293
+ log('');
294
+ const apiKey = await question(rl, 'Enter your API Key: ', true);
295
+
296
+ if (!apiKey) {
297
+ error('API Key is required');
298
+ rl.close();
299
+ process.exit(1);
300
+ }
301
+
302
+ log('');
303
+
304
+ // Region
305
+ log(`${COLORS.bold}Step 3: Region${COLORS.reset}`);
306
+ const regions = ['dev (Development)', 'au (Australia)', 'eu (Europe)', 'us (United States)'];
307
+ const regionIdx = await select(rl, 'Select your region:', regions);
308
+ const regions_map = ['dev', 'au', 'eu', 'us'];
309
+ const region = regions_map[regionIdx];
310
+
311
+ log('');
312
+ log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
313
+ log('');
314
+
315
+ // Summary
316
+ log(`${COLORS.bold}Summary:${COLORS.reset}`);
317
+ log(` Workspace ID: ${workspaceId}`);
318
+ log(` API Key: ***${apiKey.slice(-4)}`);
319
+ log(` Region: ${region}`);
320
+ log('');
321
+
322
+ const confirm = await question(rl, 'Save these credentials? (Y/n): ');
323
+
324
+ if (confirm.toLowerCase() !== 'n' && confirm.toLowerCase() !== 'no') {
325
+ const config = { workspace_id: workspaceId, api_key: apiKey, region };
326
+ saveConfig(config);
327
+ success('Credentials saved to ~/.tfcode/config.json');
328
+ log('');
329
+
330
+ // Validate
331
+ const testNow = await question(rl, 'Validate credentials now? (Y/n): ');
332
+
333
+ if (testNow.toLowerCase() !== 'n' && testNow.toLowerCase() !== 'no') {
334
+ log('');
335
+ info('Validating credentials...');
336
+ log('');
337
+
338
+ try {
339
+ const result = await runPythonSync('validate', config);
340
+
341
+ if (result.success) {
342
+ success('Credentials valid!');
343
+ log(` API URL: ${result.base_url}`);
344
+ log(` Workspace ID: ${result.workspace_id}`);
345
+ log('');
346
+
347
+ const syncNow = await question(rl, 'Sync tools now? (Y/n): ');
348
+
349
+ if (syncNow.toLowerCase() !== 'n' && syncNow.toLowerCase() !== 'no') {
350
+ log('');
351
+ info('Syncing tools...');
352
+ log('');
353
+
354
+ try {
355
+ const syncResult = await runPythonSync('sync', config);
356
+
357
+ if (syncResult.success) {
358
+ saveToolsCache(syncResult);
359
+ success(`Synced ${syncResult.tools.length} tools`);
360
+
361
+ if (syncResult.by_type && Object.keys(syncResult.by_type).length > 0) {
362
+ log('');
363
+ log('By type:');
364
+ for (const [type, count] of Object.entries(syncResult.by_type)) {
365
+ log(` ${type}: ${count}`);
366
+ }
367
+ }
368
+ } else {
369
+ error(`Sync failed: ${syncResult.error}`);
370
+ }
371
+ } catch (e) {
372
+ error(`Sync failed: ${e.message}`);
373
+ }
374
+ }
375
+ } else {
376
+ error(`Validation failed: ${result.error}`);
377
+ log('');
378
+ log(`${COLORS.dim}Check your credentials and try again with: tfcode setup${COLORS.reset}`);
379
+ }
380
+ } catch (e) {
381
+ error(`Validation failed: ${e.message}`);
382
+ log('');
383
+ log(`${COLORS.dim}Make sure Python 3.10+ and toothfairyai SDK are installed:${COLORS.reset}`);
384
+ log(`${COLORS.dim} pip install toothfairyai pydantic httpx rich${COLORS.reset}`);
385
+ }
386
+ }
387
+
388
+ log('');
389
+ log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
390
+ log(`${COLORS.bold}${COLORS.green} Setup Complete!${COLORS.reset}`);
391
+ log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
392
+ log('');
393
+ log('You can now use tfcode:');
394
+ log('');
395
+ log(` ${COLORS.cyan}tfcode validate${COLORS.reset} ${COLORS.dim}Check credentials${COLORS.reset}`);
396
+ log(` ${COLORS.cyan}tfcode sync${COLORS.reset} ${COLORS.dim}Sync tools${COLORS.reset}`);
397
+ log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} ${COLORS.dim}List your tools${COLORS.reset}`);
398
+ log('');
399
+
400
+ } else {
401
+ log('Setup cancelled.');
402
+ }
403
+
404
+ rl.close();
405
+ }
406
+
155
407
  const cli = yargs(hideBin(process.argv))
156
408
  .scriptName('tfcode')
157
409
  .wrap(100)
158
410
  .help()
159
411
  .alias('help', 'h')
412
+ .command({
413
+ command: 'setup',
414
+ describe: 'interactive credential setup',
415
+ handler: async () => {
416
+ await interactiveSetup();
417
+ }
418
+ })
160
419
  .command({
161
420
  command: 'quickstart',
162
421
  describe: 'show quick start guide',
@@ -168,28 +427,26 @@ const cli = yargs(hideBin(process.argv))
168
427
  log('');
169
428
  log('Welcome to tfcode! Follow these steps to get started:');
170
429
  log('');
171
- log(`${COLORS.cyan}STEP 1: Set Your Credentials${COLORS.reset}`);
430
+ log(`${COLORS.cyan}OPTION A: Interactive Setup (Recommended)${COLORS.reset}`);
431
+ log(`${COLORS.dim} tfcode setup${COLORS.reset}`);
432
+ log('');
433
+ log(`${COLORS.cyan}OPTION B: Manual Setup${COLORS.reset}`);
172
434
  log(`${COLORS.dim} export TF_WORKSPACE_ID="your-workspace-id"${COLORS.reset}`);
173
435
  log(`${COLORS.dim} export TF_API_KEY="your-api-key"${COLORS.reset}`);
174
436
  log(`${COLORS.dim} export TF_REGION="au"${COLORS.reset}`);
175
- log(`${COLORS.dim} Regions: dev, au, eu, us${COLORS.reset}`);
176
437
  log('');
177
- log(`${COLORS.cyan}STEP 2: Validate Connection${COLORS.reset}`);
438
+ log(`${COLORS.cyan}Then:${COLORS.reset}`);
178
439
  log(`${COLORS.dim} tfcode validate${COLORS.reset}`);
179
- log('');
180
- log(`${COLORS.cyan}STEP 3: Sync Your Tools${COLORS.reset}`);
181
440
  log(`${COLORS.dim} tfcode sync${COLORS.reset}`);
182
441
  log('');
183
- log(`${COLORS.cyan}STEP 4: Start Coding!${COLORS.reset}`);
184
- log(`${COLORS.dim} tfcode${COLORS.reset}`);
185
- log('');
186
442
  log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
187
443
  log('');
188
- log(' Useful Commands:');
444
+ log(' Commands:');
189
445
  log('');
190
- log(' tfcode validate Test your credentials');
191
- log(' tfcode sync Sync tools from workspace');
192
- log(' tfcode tools list Show all your tools');
446
+ log(' tfcode setup Interactive credential setup');
447
+ log(' tfcode validate Test your credentials');
448
+ log(' tfcode sync Sync tools from workspace');
449
+ log(' tfcode tools list Show all your tools');
193
450
  log('');
194
451
  log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
195
452
  log('');
@@ -201,11 +458,26 @@ const cli = yargs(hideBin(process.argv))
201
458
  command: 'validate',
202
459
  describe: 'validate ToothFairyAI credentials',
203
460
  handler: async () => {
461
+ const config = loadConfig();
462
+
463
+ if (!config) {
464
+ error('No credentials found.');
465
+ log('');
466
+ log('Run interactive setup:');
467
+ log(` ${COLORS.cyan}tfcode setup${COLORS.reset}`);
468
+ log('');
469
+ log('Or set environment variables:');
470
+ log(` ${COLORS.dim}export TF_WORKSPACE_ID="your-workspace-id"${COLORS.reset}`);
471
+ log(` ${COLORS.dim}export TF_API_KEY="your-api-key"${COLORS.reset}`);
472
+ process.exit(1);
473
+ return;
474
+ }
475
+
204
476
  info('Validating ToothFairyAI credentials...');
205
477
  log('');
206
478
 
207
479
  try {
208
- const result = await runPythonSync('validate');
480
+ const result = await runPythonSync('validate', config);
209
481
 
210
482
  if (result.success) {
211
483
  success('Credentials valid');
@@ -217,17 +489,12 @@ const cli = yargs(hideBin(process.argv))
217
489
  }
218
490
  } else {
219
491
  error(`Validation failed: ${result.error || 'Unknown error'}`);
220
- log('');
221
- log(`${COLORS.dim}Check your credentials:${COLORS.reset}`);
222
- log(`${COLORS.dim} TF_WORKSPACE_ID: ${process.env.TF_WORKSPACE_ID || 'not set'}${COLORS.reset}`);
223
- log(`${COLORS.dim} TF_API_KEY: ${process.env.TF_API_KEY ? '***' + process.env.TF_API_KEY.slice(-4) : 'not set'}${COLORS.reset}`);
224
- log(`${COLORS.dim} TF_REGION: ${process.env.TF_REGION || 'au (default)'}${COLORS.reset}`);
225
492
  process.exit(1);
226
493
  }
227
494
  } catch (e) {
228
495
  error(`Failed to validate: ${e.message}`);
229
496
  log('');
230
- log(`${COLORS.dim}Make sure Python 3.10+ and the ToothFairyAI SDK are installed:${COLORS.reset}`);
497
+ log(`${COLORS.dim}Make sure Python 3.10+ and toothfairyai SDK are installed:${COLORS.reset}`);
231
498
  log(`${COLORS.dim} pip install toothfairyai pydantic httpx rich${COLORS.reset}`);
232
499
  process.exit(1);
233
500
  }
@@ -237,11 +504,19 @@ const cli = yargs(hideBin(process.argv))
237
504
  command: 'sync',
238
505
  describe: 'sync tools from ToothFairyAI workspace',
239
506
  handler: async () => {
507
+ const config = loadConfig();
508
+
509
+ if (!config) {
510
+ error('No credentials found. Run: tfcode setup');
511
+ process.exit(1);
512
+ return;
513
+ }
514
+
240
515
  info('Syncing tools from ToothFairyAI workspace...');
241
516
  log('');
242
517
 
243
518
  try {
244
- const result = await runPythonSync('sync');
519
+ const result = await runPythonSync('sync', config);
245
520
 
246
521
  if (result.success) {
247
522
  saveToolsCache(result);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toothfairyai/tfcode",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "ToothFairyAI's official AI coding agent",
5
5
  "keywords": ["toothfairyai", "ai", "coding", "cli"],
6
6
  "author": "ToothFairyAI",