@toothfairyai/cli 1.2.0 → 1.5.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/bin/toothfairy.js CHANGED
@@ -7,6 +7,7 @@ const { table } = require('table');
7
7
  require('dotenv').config();
8
8
  const path = require('path');
9
9
 
10
+ const fs = require('fs');
10
11
  const ToothFairyAPI = require('../src/api');
11
12
  const {
12
13
  loadConfig,
@@ -17,6 +18,128 @@ const {
17
18
  validateDocumentConfiguration,
18
19
  } = require('../src/config');
19
20
 
21
+ /**
22
+ * Determines the best way to handle a local file based on its type and size.
23
+ * Text files under a certain size are read as text content.
24
+ * Binary files or large text files should be uploaded.
25
+ *
26
+ * @param {string} filePath - Path to the local file
27
+ * @returns {Object} - { strategy: 'text'|'upload', content?: string, mimeType: string, size: number }
28
+ */
29
+ function analyzeLocalFile(filePath) {
30
+ const absolutePath = path.resolve(filePath);
31
+
32
+ if (!fs.existsSync(absolutePath)) {
33
+ throw new Error(`File not found: ${filePath}`);
34
+ }
35
+
36
+ const stats = fs.statSync(absolutePath);
37
+ const ext = path.extname(filePath).toLowerCase().slice(1);
38
+ const sizeInMB = stats.size / (1024 * 1024);
39
+
40
+ // Text-based file extensions that can be read as content
41
+ const textExtensions = [
42
+ 'txt', 'md', 'markdown', 'json', 'jsonl', 'yaml', 'yml',
43
+ 'js', 'ts', 'jsx', 'tsx', 'py', 'java', 'c', 'cpp', 'h', 'hpp',
44
+ 'cs', 'rb', 'go', 'rs', 'php', 'swift', 'kt', 'scala',
45
+ 'html', 'htm', 'css', 'scss', 'sass', 'less',
46
+ 'xml', 'svg', 'sql', 'sh', 'bash', 'zsh', 'ps1',
47
+ 'env', 'ini', 'cfg', 'conf', 'toml', 'properties',
48
+ 'csv', 'log', 'gitignore', 'dockerignore', 'editorconfig'
49
+ ];
50
+
51
+ // Binary file extensions that must be uploaded
52
+ const binaryExtensions = [
53
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
54
+ 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'ico', 'tiff',
55
+ 'mp3', 'wav', 'aac', 'ogg', 'flac', 'm4a',
56
+ 'mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv',
57
+ 'zip', 'tar', 'gz', 'rar', '7z',
58
+ 'exe', 'dll', 'so', 'dylib',
59
+ 'woff', 'woff2', 'ttf', 'otf', 'eot'
60
+ ];
61
+
62
+ const isTextFile = textExtensions.includes(ext) || ext === '';
63
+ const isBinaryFile = binaryExtensions.includes(ext);
64
+
65
+ // Max size for reading as text (500KB)
66
+ const maxTextSize = 0.5;
67
+
68
+ // Determine strategy
69
+ let strategy;
70
+ let content = null;
71
+
72
+ if (isBinaryFile) {
73
+ strategy = 'upload';
74
+ } else if (isTextFile && sizeInMB <= maxTextSize) {
75
+ strategy = 'text';
76
+ content = fs.readFileSync(absolutePath, 'utf-8');
77
+ } else if (sizeInMB > 15) {
78
+ throw new Error(`File size (${sizeInMB.toFixed(2)}MB) exceeds 15MB limit`);
79
+ } else {
80
+ // Large text file or unknown extension - upload it
81
+ strategy = 'upload';
82
+ }
83
+
84
+ return {
85
+ strategy,
86
+ content,
87
+ absolutePath,
88
+ fileName: path.basename(filePath),
89
+ extension: ext,
90
+ size: stats.size,
91
+ sizeInMB
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Processes local files and prepares them for sending to an agent.
97
+ * Returns message text with file contents embedded and/or attachments for upload.
98
+ *
99
+ * @param {string[]} localFiles - Array of local file paths
100
+ * @param {string} originalMessage - The original message from the user
101
+ * @param {Object} existingAttachments - Existing attachment object
102
+ * @returns {Object} - { message: string, attachments: Object }
103
+ */
104
+ function processLocalFiles(localFiles, originalMessage, existingAttachments = {}) {
105
+ const textContents = [];
106
+ const attachments = { ...existingAttachments };
107
+
108
+ for (const filePath of localFiles) {
109
+ const analysis = analyzeLocalFile(filePath);
110
+
111
+ if (analysis.strategy === 'text') {
112
+ // Add file content as text
113
+ textContents.push({
114
+ fileName: analysis.fileName,
115
+ content: analysis.content
116
+ });
117
+ } else {
118
+ // Add to attachments for upload
119
+ if (!attachments.files) {
120
+ attachments.files = [];
121
+ }
122
+ attachments.files.push(analysis.absolutePath);
123
+ }
124
+ }
125
+
126
+ // Build enhanced message with text file contents
127
+ let enhancedMessage = originalMessage;
128
+
129
+ if (textContents.length > 0) {
130
+ const fileSection = textContents.map(file =>
131
+ `\n\n--- File: ${file.fileName} ---\n${file.content}\n--- End of ${file.fileName} ---`
132
+ ).join('');
133
+
134
+ enhancedMessage = `${originalMessage}${fileSection}`;
135
+ }
136
+
137
+ return {
138
+ message: enhancedMessage,
139
+ attachments
140
+ };
141
+ }
142
+
20
143
  const program = new Command();
21
144
 
22
145
  program
@@ -48,6 +171,7 @@ program
48
171
  .option('--api-key <key>', 'API key')
49
172
  .option('--workspace-id <id>', 'Workspace ID')
50
173
  .option('--user-id <id>', 'User ID')
174
+ .option('--default-agent-id <id>', 'Default agent ID for send/chat commands')
51
175
  .option('--config-path <path>', 'Path to save config file')
52
176
  .action(async (options) => {
53
177
  try {
@@ -57,7 +181,7 @@ program
57
181
  existingConfig = loadConfig(options.configPath);
58
182
  } catch (error) {
59
183
  // No existing config, use empty defaults
60
- existingConfig = new ToothFairyConfig('', '', '', '', '', '');
184
+ existingConfig = new ToothFairyConfig('', '', '', '', '', '', '');
61
185
  }
62
186
 
63
187
  // Merge provided options with existing config
@@ -71,7 +195,8 @@ program
71
195
  'https://ais.toothfairyai.com',
72
196
  options.apiKey || existingConfig.apiKey || '',
73
197
  options.workspaceId || existingConfig.workspaceId || '',
74
- options.userId || existingConfig.userId || ''
198
+ options.userId || existingConfig.userId || '',
199
+ options.defaultAgentId || existingConfig.defaultAgentId || ''
75
200
  );
76
201
 
77
202
  // Validate that at least API key and workspace ID are set
@@ -109,7 +234,7 @@ program
109
234
  .command('send')
110
235
  .description('Send a message to a ToothFairyAI agent')
111
236
  .argument('<message>', 'message to send')
112
- .requiredOption('--agent-id <id>', 'Agent ID to send message to')
237
+ .option('--agent-id <id>', 'Agent ID to send message to (uses default if configured)')
113
238
  .option('--chat-id <id>', 'Chat ID to use existing conversation (optional)')
114
239
  .option('--phone-number <number>', 'Phone number for SMS channel')
115
240
  .option('--customer-id <id>', 'Customer ID')
@@ -119,6 +244,7 @@ program
119
244
  .option('--audio <path>', 'Path to audio file (wav, mp3, aac, ogg, flac)')
120
245
  .option('--video <path>', 'Path to video file (mp4, avi, mov, wmv, flv, webm)')
121
246
  .option('--file <path...>', 'Path to document files (max 5)')
247
+ .option('--local-file <path...>', 'Path to local files to include (text files embedded in message, binary files uploaded)')
122
248
  .option('-o, --output <format>', 'Output format (json|text)', 'text')
123
249
  .option('-v, --verbose', 'Show detailed response information')
124
250
  .action(async (message, options, command) => {
@@ -127,6 +253,15 @@ program
127
253
  const config = loadConfig(globalOptions.config);
128
254
  validateConfiguration(config);
129
255
 
256
+ // Use provided agent-id or fall back to default from config
257
+ const agentId = options.agentId || config.defaultAgentId;
258
+ if (!agentId) {
259
+ console.error(chalk.red('Error: --agent-id is required'));
260
+ console.error(chalk.yellow('Either provide --agent-id or set a default with:'));
261
+ console.error(chalk.dim(' tf configure --default-agent-id YOUR_AGENT_ID'));
262
+ process.exit(1);
263
+ }
264
+
130
265
  const api = new ToothFairyAPI(
131
266
  config.baseUrl,
132
267
  config.aiUrl,
@@ -148,7 +283,7 @@ program
148
283
  }
149
284
 
150
285
  // Process file attachments if provided
151
- const attachments = {};
286
+ let attachments = {};
152
287
  if (options.image) {
153
288
  attachments.images = [options.image];
154
289
  }
@@ -162,11 +297,28 @@ program
162
297
  attachments.files = Array.isArray(options.file) ? options.file : [options.file];
163
298
  }
164
299
 
300
+ // Process local files - text files are embedded in message, binary files are uploaded
301
+ let finalMessage = message;
302
+ if (options.localFile) {
303
+ const localFiles = Array.isArray(options.localFile) ? options.localFile : [options.localFile];
304
+ const processed = processLocalFiles(localFiles, message, attachments);
305
+ finalMessage = processed.message;
306
+ attachments = processed.attachments;
307
+
308
+ if (globalOptions.verbose || options.verbose) {
309
+ console.log(chalk.dim(`Processing ${localFiles.length} local file(s)...`));
310
+ for (const filePath of localFiles) {
311
+ const analysis = analyzeLocalFile(filePath);
312
+ console.log(chalk.dim(` - ${analysis.fileName}: ${analysis.strategy} (${analysis.sizeInMB.toFixed(2)}MB)`));
313
+ }
314
+ }
315
+ }
316
+
165
317
  const spinner = ora('Sending message to agent...').start();
166
318
 
167
319
  const response = await api.sendMessageToAgent(
168
- message,
169
- options.agentId,
320
+ finalMessage,
321
+ agentId,
170
322
  options.phoneNumber,
171
323
  options.customerId,
172
324
  options.providerId,
@@ -238,7 +390,7 @@ program
238
390
  'Send a message to a ToothFairyAI agent with real-time streaming response'
239
391
  )
240
392
  .argument('<message>', 'message to send')
241
- .requiredOption('--agent-id <id>', 'Agent ID to send message to')
393
+ .option('--agent-id <id>', 'Agent ID to send message to (uses default if configured)')
242
394
  .option('--chat-id <id>', 'Chat ID to use existing conversation (optional)')
243
395
  .option('--phone-number <number>', 'Phone number for SMS channel')
244
396
  .option('--customer-id <id>', 'Customer ID')
@@ -248,6 +400,7 @@ program
248
400
  .option('--audio <path>', 'Path to audio file (WAV, max 1)')
249
401
  .option('--video <path>', 'Path to video file (MP4, max 1)')
250
402
  .option('--file <path>', 'Path to file (max 5, can be used multiple times)', (value, previous) => previous.concat([value]), [])
403
+ .option('--local-file <path...>', 'Path to local files to include (text files embedded in message, binary files uploaded)')
251
404
  .option('-o, --output <format>', 'Output format (json|text)', 'text')
252
405
  .option('-v, --verbose', 'Show detailed streaming information')
253
406
  .option('--show-progress', 'Show agent processing status updates')
@@ -277,6 +430,15 @@ program
277
430
  const config = loadConfig(globalOptions.config);
278
431
  validateConfiguration(config);
279
432
 
433
+ // Use provided agent-id or fall back to default from config
434
+ const agentId = options.agentId || config.defaultAgentId;
435
+ if (!agentId) {
436
+ console.error(chalk.red('Error: --agent-id is required'));
437
+ console.error(chalk.yellow('Either provide --agent-id or set a default with:'));
438
+ console.error(chalk.dim(' tf configure --default-agent-id YOUR_AGENT_ID'));
439
+ process.exit(1);
440
+ }
441
+
280
442
  const api = new ToothFairyAPI(
281
443
  config.baseUrl,
282
444
  config.aiUrl,
@@ -298,7 +460,7 @@ program
298
460
  }
299
461
 
300
462
  // Process file attachments if provided
301
- const attachments = {};
463
+ let attachments = {};
302
464
  if (options.image) {
303
465
  attachments.images = [options.image];
304
466
  }
@@ -312,18 +474,34 @@ program
312
474
  attachments.files = Array.isArray(options.file) ? options.file : [options.file];
313
475
  }
314
476
 
477
+ // Process local files - text files are embedded in message, binary files are uploaded
478
+ let finalMessage = message;
479
+ if (options.localFile) {
480
+ const localFiles = Array.isArray(options.localFile) ? options.localFile : [options.localFile];
481
+ const processed = processLocalFiles(localFiles, message, attachments);
482
+ finalMessage = processed.message;
483
+ attachments = processed.attachments;
484
+
485
+ if (globalOptions.verbose || options.verbose) {
486
+ console.log(chalk.dim(`Processing ${localFiles.length} local file(s)...`));
487
+ for (const filePath of localFiles) {
488
+ const analysis = analyzeLocalFile(filePath);
489
+ console.log(chalk.dim(` - ${analysis.fileName}: ${analysis.strategy} (${analysis.sizeInMB.toFixed(2)}MB)`));
490
+ }
491
+ }
492
+ }
493
+
315
494
  console.log(
316
- chalk.cyan(`Streaming message to agent ${options.agentId}...`)
495
+ chalk.cyan(`Streaming message to agent ${agentId}...`)
317
496
  );
318
- console.log(chalk.dim(`Message: ${message}`));
497
+ console.log(chalk.dim(`Message: ${options.localFile ? '(with local files) ' : ''}${message}`));
319
498
  console.log();
320
499
 
321
500
  // Initialize variables for streaming
322
- let currentText = '';
323
501
  let finalResponse = null;
324
502
  let processingStatus = null;
325
- let lastUpdateTime = 0;
326
503
  let hasStartedResponse = false;
504
+ let hasCompletedResponse = false;
327
505
 
328
506
  const mapStateWithLabel = (state) => {
329
507
  switch (state) {
@@ -377,8 +555,8 @@ program
377
555
  const allEvents = [];
378
556
 
379
557
  await api.sendMessageToAgentStream(
380
- message,
381
- options.agentId,
558
+ finalMessage,
559
+ agentId,
382
560
  options.phoneNumber,
383
561
  options.customerId,
384
562
  options.providerId,
@@ -396,7 +574,8 @@ program
396
574
  },
397
575
  attachments,
398
576
  options.showProgress,
399
- options.chatId
577
+ options.chatId,
578
+ true // rawStream - enable chunk-by-chunk streaming
400
579
  );
401
580
 
402
581
  console.log(JSON.stringify(allEvents, null, 2));
@@ -404,42 +583,10 @@ program
404
583
  // Text mode: show live streaming
405
584
  let currentSpinner = null;
406
585
 
407
- // Function to update display with status filtering
408
- const updateDisplay = (text, agentStatus, type = 'response') => {
409
- if (currentSpinner) {
410
- currentSpinner.stop();
411
- currentSpinner = null;
412
- }
413
-
414
- // Only show text when agent is actually replying
415
- if (text && type === 'response' && agentStatus === 'replying') {
416
- const trimmedText = text.trim();
417
- const now = Date.now();
418
-
419
- // Only update if the text is actually different and enough time has passed
420
- // This prevents rapid duplicate updates
421
- if (
422
- trimmedText !== currentText &&
423
- trimmedText?.length > currentText?.length &&
424
- now - lastUpdateTime > 50
425
- ) {
426
- if (!hasStartedResponse) {
427
- // First time showing response - print header on new line
428
- console.log(chalk.green('🧚 Responding'));
429
- hasStartedResponse = true;
430
- }
431
- // Stream new text content on new line
432
- console.log(trimmedText);
433
- currentText = trimmedText;
434
- lastUpdateTime = now;
435
- }
436
- }
437
- };
438
-
439
586
  try {
440
587
  await api.sendMessageToAgentStream(
441
- message,
442
- options.agentId,
588
+ finalMessage,
589
+ agentId,
443
590
  options.phoneNumber,
444
591
  options.customerId,
445
592
  options.providerId,
@@ -480,19 +627,71 @@ program
480
627
  return;
481
628
  }
482
629
 
483
- // Handle message events
630
+ // Handle streaming data events (tokens from raw_stream mode)
631
+ if (eventType === 'data' && eventData.text) {
632
+ // Stop spinner when text starts streaming
633
+ if (currentSpinner) {
634
+ currentSpinner.stop();
635
+ currentSpinner = null;
636
+ }
637
+
638
+ if (!hasStartedResponse) {
639
+ console.log(chalk.green('🧚 Responding'));
640
+ hasStartedResponse = true;
641
+ }
642
+
643
+ // Stream chunk directly to output
644
+ process.stdout.write(eventData.text);
645
+ }
646
+
647
+ // Handle progress events (from raw_stream mode)
648
+ if (eventType === 'progress') {
649
+ const status = eventData.processing_status || eventData.metadata_parsed?.agent_processing_status;
650
+ if (status && status !== processingStatus) {
651
+ processingStatus = status;
652
+ if (options.showProgress) {
653
+ const statusMsg = mapStateWithLabel(processingStatus);
654
+ if (currentSpinner) {
655
+ currentSpinner.stop();
656
+ }
657
+ currentSpinner = ora(chalk.yellow(statusMsg)).start();
658
+ }
659
+ }
660
+ }
661
+
662
+ // Handle chat_created events
663
+ if (eventType === 'chat_created' && eventData.chatId) {
664
+ if (options.verbose) {
665
+ console.log(chalk.dim(`\n📝 Chat created: ${eventData.chatId}`));
666
+ }
667
+ }
668
+
669
+ // Handle complete events
670
+ if (eventType === 'complete') {
671
+ finalResponse = eventData;
672
+ if (currentSpinner) {
673
+ currentSpinner.stop();
674
+ currentSpinner = null;
675
+ }
676
+ }
677
+
678
+ // Handle message events (legacy format / additional metadata)
484
679
  if (eventData.type === 'message') {
485
680
  let metadata = {};
486
681
  let agentStatus = null;
487
682
 
488
683
  // Parse metadata if available
489
684
  if (eventData.metadata) {
490
- try {
491
- metadata = JSON.parse(eventData.metadata);
492
- agentStatus = metadata.agent_processing_status;
493
- } catch (e) {
494
- // Metadata parsing failed, continue without it
685
+ if (typeof eventData.metadata === 'object') {
686
+ metadata = eventData.metadata;
687
+ } else {
688
+ try {
689
+ metadata = JSON.parse(eventData.metadata);
690
+ } catch (e) {
691
+ // Metadata parsing failed, continue without it
692
+ }
495
693
  }
694
+ agentStatus = metadata.agent_processing_status;
496
695
  }
497
696
 
498
697
  // Handle status changes
@@ -507,22 +706,15 @@ program
507
706
  }
508
707
  }
509
708
 
510
- // Handle progressive text streaming
511
- if (eventData.text && agentStatus === 'replying') {
512
- updateDisplay(eventData.text, agentStatus, 'response');
513
- }
514
-
515
709
  // Handle fulfilled status (final response)
516
- if (eventData.status === 'fulfilled') {
710
+ if (eventData.status === 'fulfilled' && !hasCompletedResponse) {
517
711
  finalResponse = eventData;
518
- if (currentText) {
519
- // Clean final display with magic wand emoji
520
- if (currentSpinner) {
521
- currentSpinner.stop();
522
- currentSpinner = null;
523
- }
524
- console.log(chalk.blue('🪄 Response complete'));
712
+ hasCompletedResponse = true;
713
+ if (currentSpinner) {
714
+ currentSpinner.stop();
715
+ currentSpinner = null;
525
716
  }
717
+ console.log(chalk.blue('\n🪄 Response complete'));
526
718
  }
527
719
 
528
720
  // Handle additional metadata events (images, files, callback metadata)
@@ -530,14 +722,12 @@ program
530
722
  eventData.images !== undefined ||
531
723
  eventData.files !== undefined
532
724
  ) {
533
- // These are attachment events - could show notification if needed
534
725
  if (options.verbose) {
535
726
  console.log(chalk.dim('\n📎 Attachments processed'));
536
727
  }
537
728
  }
538
729
 
539
730
  if (eventData.callbackMetadata) {
540
- // Function execution metadata
541
731
  if (options.verbose) {
542
732
  console.log(
543
733
  chalk.dim('\n⚙️ Function execution metadata received')
@@ -558,7 +748,8 @@ program
558
748
  },
559
749
  attachments,
560
750
  options.showProgress,
561
- options.chatId
751
+ options.chatId,
752
+ true // rawStream - enable chunk-by-chunk streaming
562
753
  );
563
754
 
564
755
  // Clean up spinner after streaming completes
@@ -610,7 +801,7 @@ program
610
801
  console.log('─'.repeat(50));
611
802
  }
612
803
 
613
- if (!currentText) {
804
+ if (!hasStartedResponse) {
614
805
  console.log(chalk.yellow('No text response received from agent.'));
615
806
  }
616
807
  } catch (streamError) {
@@ -626,6 +817,378 @@ program
626
817
  }
627
818
  });
628
819
 
820
+ // Interactive Chat command
821
+ program
822
+ .command('chat')
823
+ .description('Start an interactive chat session with a ToothFairyAI agent (use @ to reference local files)')
824
+ .option('--agent-id <id>', 'Agent ID to chat with (uses default if configured)')
825
+ .option('--chat-id <id>', 'Resume an existing chat conversation')
826
+ .option('-v, --verbose', 'Show detailed information')
827
+ .option('--show-progress', 'Show agent processing status updates')
828
+ .action(async (options, command) => {
829
+ const readline = require('readline');
830
+ const glob = require('glob');
831
+
832
+ try {
833
+ const globalOptions = command.parent.opts();
834
+ const config = loadConfig(globalOptions.config);
835
+ validateConfiguration(config);
836
+
837
+ // Use provided agent-id or fall back to default from config
838
+ const agentId = options.agentId || config.defaultAgentId;
839
+ if (!agentId) {
840
+ console.error(chalk.red('Error: --agent-id is required'));
841
+ console.error(chalk.yellow('Either provide --agent-id or set a default with:'));
842
+ console.error(chalk.dim(' tf configure --default-agent-id YOUR_AGENT_ID'));
843
+ process.exit(1);
844
+ }
845
+
846
+ const api = new ToothFairyAPI(
847
+ config.baseUrl,
848
+ config.aiUrl,
849
+ config.aiStreamUrl,
850
+ config.apiKey,
851
+ config.workspaceId,
852
+ globalOptions.verbose || options.verbose
853
+ );
854
+
855
+ // Track current chat ID for conversation continuity
856
+ let currentChatId = options.chatId || null;
857
+ const workingDir = process.cwd();
858
+
859
+ // Helper to get files matching a pattern
860
+ const getMatchingFiles = (pattern) => {
861
+ try {
862
+ // Remove @ prefix if present
863
+ const cleanPattern = pattern.startsWith('@') ? pattern.slice(1) : pattern;
864
+
865
+ // If it's a directory, list its contents
866
+ if (fs.existsSync(cleanPattern) && fs.statSync(cleanPattern).isDirectory()) {
867
+ return fs.readdirSync(cleanPattern).map(f => path.join(cleanPattern, f));
868
+ }
869
+
870
+ // Try glob pattern
871
+ const matches = glob.sync(cleanPattern, {
872
+ cwd: workingDir,
873
+ nodir: false,
874
+ dot: false
875
+ });
876
+ return matches;
877
+ } catch (e) {
878
+ return [];
879
+ }
880
+ };
881
+
882
+ // Helper to list files for autocomplete
883
+ const listFilesForCompletion = (partial) => {
884
+ const cleanPartial = partial.startsWith('@') ? partial.slice(1) : partial;
885
+ const dir = path.dirname(cleanPartial) || '.';
886
+ const prefix = path.basename(cleanPartial);
887
+
888
+ try {
889
+ let searchDir = dir === '.' ? workingDir : path.resolve(workingDir, dir);
890
+ if (!fs.existsSync(searchDir)) {
891
+ searchDir = workingDir;
892
+ }
893
+
894
+ const files = fs.readdirSync(searchDir);
895
+ const matches = files
896
+ .filter(f => f.startsWith(prefix) || prefix === '')
897
+ .map(f => {
898
+ const fullPath = path.join(dir === '.' ? '' : dir, f);
899
+ const isDir = fs.statSync(path.join(searchDir, f)).isDirectory();
900
+ return isDir ? fullPath + '/' : fullPath;
901
+ });
902
+ return matches;
903
+ } catch (e) {
904
+ return [];
905
+ }
906
+ };
907
+
908
+ // Parse @ references from message
909
+ const parseFileReferences = (message) => {
910
+ const fileRefs = [];
911
+ // Match @path/to/file or @"path with spaces/file" or @./relative/path
912
+ const regex = /@(?:"([^"]+)"|([^\s@]+))/g;
913
+ let match;
914
+
915
+ while ((match = regex.exec(message)) !== null) {
916
+ const filePath = match[1] || match[2];
917
+ fileRefs.push(filePath);
918
+ }
919
+
920
+ return fileRefs;
921
+ };
922
+
923
+ // Remove @ references from message for cleaner prompt
924
+ const cleanMessage = (message) => {
925
+ return message.replace(/@(?:"[^"]+"|[^\s@]+)/g, '').trim();
926
+ };
927
+
928
+ // Print welcome banner
929
+ console.log(chalk.cyan.bold('\n🧚 ToothFairy Interactive Chat'));
930
+ console.log(chalk.dim('─'.repeat(50)));
931
+ console.log(chalk.white(`Agent: ${agentId}`));
932
+ console.log(chalk.white(`Working directory: ${workingDir}`));
933
+ if (currentChatId) {
934
+ console.log(chalk.white(`Resuming chat: ${currentChatId}`));
935
+ }
936
+ console.log(chalk.dim('─'.repeat(50)));
937
+ console.log(chalk.yellow('\nTips:'));
938
+ console.log(chalk.dim(' • Use @filename to include a file in your message'));
939
+ console.log(chalk.dim(' • Use @path/to/file or @./relative/path for paths'));
940
+ console.log(chalk.dim(' • Use @"path with spaces/file.txt" for paths with spaces'));
941
+ console.log(chalk.dim(' • Use @*.js or @src/**/*.ts for glob patterns'));
942
+ console.log(chalk.dim(' • Type /files to list files in current directory'));
943
+ console.log(chalk.dim(' • Type /cd <path> to change directory'));
944
+ console.log(chalk.dim(' • Type /chat to see current chat ID'));
945
+ console.log(chalk.dim(' • Type /clear to start a new chat'));
946
+ console.log(chalk.dim(' • Type /exit or Ctrl+C to quit'));
947
+ console.log(chalk.dim('─'.repeat(50)));
948
+ console.log();
949
+
950
+ // Create readline interface with custom completer
951
+ const rl = readline.createInterface({
952
+ input: process.stdin,
953
+ output: process.stdout,
954
+ completer: (line) => {
955
+ // Check if we're completing a file reference
956
+ const atMatch = line.match(/@([^\s]*)$/);
957
+ if (atMatch) {
958
+ const partial = atMatch[1];
959
+ const completions = listFilesForCompletion(partial);
960
+ const hits = completions.map(c => '@' + c);
961
+ return [hits.length ? hits : [], line];
962
+ }
963
+ return [[], line];
964
+ },
965
+ prompt: chalk.green('You: ')
966
+ });
967
+
968
+ rl.prompt();
969
+
970
+ rl.on('line', async (input) => {
971
+ const trimmedInput = input.trim();
972
+
973
+ if (!trimmedInput) {
974
+ rl.prompt();
975
+ return;
976
+ }
977
+
978
+ // Handle special commands
979
+ if (trimmedInput.startsWith('/')) {
980
+ const [cmd, ...args] = trimmedInput.slice(1).split(' ');
981
+
982
+ switch (cmd.toLowerCase()) {
983
+ case 'exit':
984
+ case 'quit':
985
+ case 'q':
986
+ console.log(chalk.cyan('\nGoodbye! 👋'));
987
+ rl.close();
988
+ process.exit(0);
989
+ break;
990
+
991
+ case 'files':
992
+ case 'ls': {
993
+ const targetDir = args[0] || '.';
994
+ try {
995
+ const files = fs.readdirSync(path.resolve(workingDir, targetDir));
996
+ console.log(chalk.cyan(`\nFiles in ${targetDir}:`));
997
+ files.forEach(f => {
998
+ const fullPath = path.join(workingDir, targetDir, f);
999
+ const isDir = fs.statSync(fullPath).isDirectory();
1000
+ console.log(chalk.dim(` ${isDir ? '📁' : '📄'} ${f}${isDir ? '/' : ''}`));
1001
+ });
1002
+ console.log();
1003
+ } catch (e) {
1004
+ console.log(chalk.red(`Cannot list directory: ${e.message}`));
1005
+ }
1006
+ break;
1007
+ }
1008
+
1009
+ case 'cd': {
1010
+ if (args[0]) {
1011
+ const newDir = path.resolve(workingDir, args[0]);
1012
+ if (fs.existsSync(newDir) && fs.statSync(newDir).isDirectory()) {
1013
+ process.chdir(newDir);
1014
+ console.log(chalk.cyan(`\nChanged directory to: ${newDir}\n`));
1015
+ } else {
1016
+ console.log(chalk.red(`Directory not found: ${args[0]}`));
1017
+ }
1018
+ } else {
1019
+ console.log(chalk.yellow('Usage: /cd <path>'));
1020
+ }
1021
+ break;
1022
+ }
1023
+
1024
+ case 'chat':
1025
+ console.log(chalk.cyan(`\nCurrent chat ID: ${currentChatId || '(new chat)'}\n`));
1026
+ break;
1027
+
1028
+ case 'clear':
1029
+ case 'new':
1030
+ currentChatId = null;
1031
+ console.log(chalk.cyan('\nStarting new chat session.\n'));
1032
+ break;
1033
+
1034
+ case 'help':
1035
+ console.log(chalk.yellow('\nCommands:'));
1036
+ console.log(chalk.dim(' /files [path] - List files in directory'));
1037
+ console.log(chalk.dim(' /cd <path> - Change directory'));
1038
+ console.log(chalk.dim(' /chat - Show current chat ID'));
1039
+ console.log(chalk.dim(' /clear - Start new chat'));
1040
+ console.log(chalk.dim(' /exit - Quit'));
1041
+ console.log();
1042
+ break;
1043
+
1044
+ default:
1045
+ console.log(chalk.yellow(`Unknown command: /${cmd}. Type /help for available commands.`));
1046
+ }
1047
+
1048
+ rl.prompt();
1049
+ return;
1050
+ }
1051
+
1052
+ // Parse file references from the message
1053
+ const fileRefs = parseFileReferences(trimmedInput);
1054
+ let messageText = trimmedInput;
1055
+ let attachments = {};
1056
+
1057
+ // Process file references
1058
+ if (fileRefs.length > 0) {
1059
+ const allFiles = [];
1060
+
1061
+ for (const ref of fileRefs) {
1062
+ const matches = getMatchingFiles(ref);
1063
+ if (matches.length === 0) {
1064
+ // Check if it's a direct file path
1065
+ const directPath = path.resolve(workingDir, ref);
1066
+ if (fs.existsSync(directPath)) {
1067
+ allFiles.push(directPath);
1068
+ } else {
1069
+ console.log(chalk.yellow(`Warning: No files found matching @${ref}`));
1070
+ }
1071
+ } else {
1072
+ allFiles.push(...matches.map(m => path.resolve(workingDir, m)));
1073
+ }
1074
+ }
1075
+
1076
+ if (allFiles.length > 0) {
1077
+ // Filter out directories
1078
+ const validFiles = allFiles.filter(f => {
1079
+ try {
1080
+ return fs.existsSync(f) && fs.statSync(f).isFile();
1081
+ } catch {
1082
+ return false;
1083
+ }
1084
+ });
1085
+
1086
+ if (validFiles.length > 0) {
1087
+ console.log(chalk.dim(`\nIncluding ${validFiles.length} file(s):`));
1088
+ validFiles.forEach(f => console.log(chalk.dim(` 📄 ${path.relative(workingDir, f)}`)));
1089
+ console.log();
1090
+
1091
+ // Process the files
1092
+ const cleanedMessage = cleanMessage(messageText);
1093
+ const processed = processLocalFiles(validFiles, cleanedMessage || 'Please review these files:', attachments);
1094
+ messageText = processed.message;
1095
+ attachments = processed.attachments;
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ // Send message to agent with streaming
1101
+ console.log(chalk.blue('\n🧚 Agent: '));
1102
+
1103
+ let processingStatus = null;
1104
+ let currentSpinner = null;
1105
+
1106
+ try {
1107
+ await api.sendMessageToAgentStream(
1108
+ messageText,
1109
+ agentId,
1110
+ null, // phoneNumber
1111
+ null, // customerId
1112
+ null, // providerId
1113
+ {}, // customerInfo
1114
+ (eventType, eventData) => {
1115
+ // Handle chat creation event
1116
+ if (eventData.type === 'chat_created' || eventData.event === 'chat_created') {
1117
+ if (eventData.chatId) {
1118
+ currentChatId = eventData.chatId;
1119
+ }
1120
+ }
1121
+
1122
+ // Update chat ID from response
1123
+ if (eventData.chatId && !currentChatId) {
1124
+ currentChatId = eventData.chatId;
1125
+ }
1126
+
1127
+ // Handle status events
1128
+ if (eventData.status === 'connected' && options.showProgress) {
1129
+ console.log(chalk.dim('Connected...'));
1130
+ }
1131
+
1132
+ // Handle progress events
1133
+ if (eventType === 'progress' && options.showProgress) {
1134
+ const status = eventData.processing_status || eventData.status;
1135
+ if (status && status !== processingStatus) {
1136
+ processingStatus = status;
1137
+ if (currentSpinner) currentSpinner.stop();
1138
+ currentSpinner = ora(chalk.dim(status)).start();
1139
+ }
1140
+ }
1141
+
1142
+ // Handle streaming text (raw_stream tokens come as 'data' events with chunk in text)
1143
+ if (eventType === 'data' && eventData.text) {
1144
+ if (currentSpinner) {
1145
+ currentSpinner.stop();
1146
+ currentSpinner = null;
1147
+ }
1148
+ // Print chunk directly (raw_stream sends individual chunks, not cumulative)
1149
+ process.stdout.write(eventData.text);
1150
+ }
1151
+
1152
+ // Handle completion
1153
+ if (eventData.status === 'fulfilled' || eventType === 'complete') {
1154
+ if (currentSpinner) {
1155
+ currentSpinner.stop();
1156
+ currentSpinner = null;
1157
+ }
1158
+ }
1159
+
1160
+ // Handle errors
1161
+ if (eventType === 'error') {
1162
+ if (currentSpinner) currentSpinner.stop();
1163
+ console.log(chalk.red(`\nError: ${eventData.message || 'Unknown error'}`));
1164
+ }
1165
+ },
1166
+ attachments,
1167
+ options.showProgress,
1168
+ currentChatId,
1169
+ true // rawStream - enable chunk-by-chunk streaming
1170
+ );
1171
+
1172
+ console.log('\n');
1173
+ } catch (error) {
1174
+ if (currentSpinner) currentSpinner.stop();
1175
+ console.log(chalk.red(`\nError: ${error.message}\n`));
1176
+ }
1177
+
1178
+ rl.prompt();
1179
+ });
1180
+
1181
+ rl.on('close', () => {
1182
+ console.log(chalk.cyan('\nGoodbye! 👋\n'));
1183
+ process.exit(0);
1184
+ });
1185
+
1186
+ } catch (error) {
1187
+ console.error(chalk.red(`Error starting chat: ${error.message}`));
1188
+ process.exit(1);
1189
+ }
1190
+ });
1191
+
629
1192
  // Search command
630
1193
  program
631
1194
  .command('search')
@@ -963,6 +1526,7 @@ program
963
1526
  ],
964
1527
  ['Workspace ID', config.workspaceId],
965
1528
  ['User ID', config.userId ? config.userId : 'Not set'],
1529
+ ['Default Agent ID', config.defaultAgentId ? config.defaultAgentId : 'Not set'],
966
1530
  ];
967
1531
 
968
1532
  console.log(
@@ -3387,5 +3951,4017 @@ program
3387
3951
  }
3388
3952
  });
3389
3953
 
3954
+ // Agent commands
3955
+ program
3956
+ .command('create-agent')
3957
+ .description('Create a new agent')
3958
+ .option('--label <label>', 'Agent label')
3959
+ .option('--description <description>', 'Agent description')
3960
+ .option('--emoji <emoji>', 'Agent emoji')
3961
+ .option('--mode <mode>', 'Agent mode (retriever|coder|chatter|planner|computer|voice|accuracy)')
3962
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
3963
+ .option('-v, --verbose', 'Show detailed information')
3964
+ .action(async (options, command) => {
3965
+ try {
3966
+ const globalOptions = command.parent.opts();
3967
+ const config = loadConfig(globalOptions.config);
3968
+ validateConfiguration(config);
3969
+
3970
+ const api = new ToothFairyAPI(
3971
+ config.baseUrl,
3972
+ config.aiUrl,
3973
+ config.aiStreamUrl,
3974
+ config.apiKey,
3975
+ config.workspaceId,
3976
+ globalOptions.verbose || options.verbose
3977
+ );
3978
+
3979
+ const spinner = ora('Creating agent...').start();
3980
+ const result = await api._makeRequest('POST', 'agent/create', {
3981
+ label: options.label,
3982
+ description: options.description,
3983
+ emoji: options.emoji,
3984
+ mode: options.mode,
3985
+ });
3986
+ spinner.stop();
3987
+
3988
+ if (options.output === 'json') {
3989
+ console.log(JSON.stringify(result, null, 2));
3990
+ } else {
3991
+ console.log(chalk.green.bold('✅ Agent created successfully!'));
3992
+ console.log(chalk.dim(`ID: ${result.id || 'N/A'}`));
3993
+ }
3994
+ } catch (error) {
3995
+ console.error(chalk.red(`Error creating agent: ${error.message}`));
3996
+ process.exit(1);
3997
+ }
3998
+ });
3999
+
4000
+ program
4001
+ .command('list-agents')
4002
+ .description('List all agents')
4003
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4004
+ .option('-v, --verbose', 'Show detailed information')
4005
+ .action(async (options, command) => {
4006
+ try {
4007
+ const globalOptions = command.parent.opts();
4008
+ const config = loadConfig(globalOptions.config);
4009
+ validateConfiguration(config);
4010
+
4011
+ const api = new ToothFairyAPI(
4012
+ config.baseUrl,
4013
+ config.aiUrl,
4014
+ config.aiStreamUrl,
4015
+ config.apiKey,
4016
+ config.workspaceId,
4017
+ globalOptions.verbose || options.verbose
4018
+ );
4019
+
4020
+ const spinner = ora('Fetching agents...').start();
4021
+ const result = await api._makeRequest('GET', 'agent/list');
4022
+ spinner.stop();
4023
+
4024
+ if (options.output === 'json') {
4025
+ console.log(JSON.stringify(result, null, 2));
4026
+ } else {
4027
+ const agents = Array.isArray(result) ? result : result.items || [];
4028
+ console.log(chalk.green.bold(`Found ${agents.length} agent(s)`));
4029
+ agents.forEach(agent => {
4030
+ console.log(chalk.cyan(` • ${agent.label || 'Unnamed'} (${agent.id})`));
4031
+ });
4032
+ }
4033
+ } catch (error) {
4034
+ console.error(chalk.red(`Error listing agents: ${error.message}`));
4035
+ process.exit(1);
4036
+ }
4037
+ });
4038
+
4039
+ program
4040
+ .command('get-agent')
4041
+ .description('Get details of a specific agent')
4042
+ .argument('<id>', 'Agent ID')
4043
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4044
+ .option('-v, --verbose', 'Show detailed information')
4045
+ .action(async (agentId, options, command) => {
4046
+ try {
4047
+ const globalOptions = command.parent.opts();
4048
+ const config = loadConfig(globalOptions.config);
4049
+ validateConfiguration(config);
4050
+
4051
+ const api = new ToothFairyAPI(
4052
+ config.baseUrl,
4053
+ config.aiUrl,
4054
+ config.aiStreamUrl,
4055
+ config.apiKey,
4056
+ config.workspaceId,
4057
+ globalOptions.verbose || options.verbose
4058
+ );
4059
+
4060
+ const spinner = ora('Fetching agent...').start();
4061
+ const result = await api._makeRequest('GET', `agent/get/${agentId}`);
4062
+ spinner.stop();
4063
+
4064
+ if (options.output === 'json') {
4065
+ console.log(JSON.stringify(result, null, 2));
4066
+ } else {
4067
+ console.log(chalk.green.bold('Agent Details'));
4068
+ console.log(chalk.dim(`ID: ${result.id}`));
4069
+ console.log(chalk.dim(`Label: ${result.label}`));
4070
+ console.log(chalk.dim(`Description: ${result.description || 'N/A'}`));
4071
+ console.log(chalk.dim(`Mode: ${result.mode || 'N/A'}`));
4072
+ }
4073
+ } catch (error) {
4074
+ console.error(chalk.red(`Error getting agent: ${error.message}`));
4075
+ process.exit(1);
4076
+ }
4077
+ });
4078
+
4079
+ program
4080
+ .command('update-agent')
4081
+ .description('Update an existing agent')
4082
+ .argument('<id>', 'Agent ID')
4083
+ .option('--label <label>', 'Agent label')
4084
+ .option('--description <description>', 'Agent description')
4085
+ .option('--emoji <emoji>', 'Agent emoji')
4086
+ .option('--mode <mode>', 'Agent mode')
4087
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4088
+ .option('-v, --verbose', 'Show detailed information')
4089
+ .action(async (agentId, options, command) => {
4090
+ try {
4091
+ const globalOptions = command.parent.opts();
4092
+ const config = loadConfig(globalOptions.config);
4093
+ validateConfiguration(config);
4094
+
4095
+ const api = new ToothFairyAPI(
4096
+ config.baseUrl,
4097
+ config.aiUrl,
4098
+ config.aiStreamUrl,
4099
+ config.apiKey,
4100
+ config.workspaceId,
4101
+ globalOptions.verbose || options.verbose
4102
+ );
4103
+
4104
+ const spinner = ora('Updating agent...').start();
4105
+ const result = await api._makeRequest('POST', `agent/update/${agentId}`, {
4106
+ label: options.label,
4107
+ description: options.description,
4108
+ emoji: options.emoji,
4109
+ mode: options.mode,
4110
+ });
4111
+ spinner.stop();
4112
+
4113
+ if (options.output === 'json') {
4114
+ console.log(JSON.stringify(result, null, 2));
4115
+ } else {
4116
+ console.log(chalk.green.bold('✅ Agent updated successfully!'));
4117
+ }
4118
+ } catch (error) {
4119
+ console.error(chalk.red(`Error updating agent: ${error.message}`));
4120
+ process.exit(1);
4121
+ }
4122
+ });
4123
+
4124
+ program
4125
+ .command('delete-agent')
4126
+ .description('Delete an agent')
4127
+ .argument('<id>', 'Agent ID')
4128
+ .option('--confirm', 'Skip confirmation prompt')
4129
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4130
+ .option('-v, --verbose', 'Show detailed information')
4131
+ .action(async (agentId, options, command) => {
4132
+ try {
4133
+ const globalOptions = command.parent.opts();
4134
+ const config = loadConfig(globalOptions.config);
4135
+ validateConfiguration(config);
4136
+
4137
+ if (!options.confirm) {
4138
+ const readline = require('readline');
4139
+ const rl = readline.createInterface({
4140
+ input: process.stdin,
4141
+ output: process.stdout,
4142
+ });
4143
+
4144
+ const answer = await new Promise((resolve) => {
4145
+ rl.question(
4146
+ chalk.yellow(`⚠️ Are you sure you want to delete agent ${agentId}? (y/N): `),
4147
+ resolve
4148
+ );
4149
+ });
4150
+ rl.close();
4151
+
4152
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
4153
+ console.log(chalk.gray('Deletion cancelled.'));
4154
+ process.exit(0);
4155
+ }
4156
+ }
4157
+
4158
+ const api = new ToothFairyAPI(
4159
+ config.baseUrl,
4160
+ config.aiUrl,
4161
+ config.aiStreamUrl,
4162
+ config.apiKey,
4163
+ config.workspaceId,
4164
+ globalOptions.verbose || options.verbose
4165
+ );
4166
+
4167
+ const spinner = ora('Deleting agent...').start();
4168
+ const result = await api._makeRequest('DELETE', `agent/delete/${agentId}`);
4169
+ spinner.stop();
4170
+
4171
+ if (options.output === 'json') {
4172
+ console.log(JSON.stringify(result, null, 2));
4173
+ } else {
4174
+ console.log(chalk.green.bold('✅ Agent deleted successfully!'));
4175
+ }
4176
+ } catch (error) {
4177
+ console.error(chalk.red(`Error deleting agent: ${error.message}`));
4178
+ process.exit(1);
4179
+ }
4180
+ });
4181
+
4182
+ // Function commands
4183
+ program
4184
+ .command('create-function')
4185
+ .description('Create a new agent function')
4186
+ .option('--name <name>', 'Function name')
4187
+ .option('--description <description>', 'Function description')
4188
+ .option('--code <code>', 'Function code')
4189
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4190
+ .option('-v, --verbose', 'Show detailed information')
4191
+ .action(async (options, command) => {
4192
+ try {
4193
+ const globalOptions = command.parent.opts();
4194
+ const config = loadConfig(globalOptions.config);
4195
+ validateConfiguration(config);
4196
+
4197
+ const api = new ToothFairyAPI(
4198
+ config.baseUrl,
4199
+ config.aiUrl,
4200
+ config.aiStreamUrl,
4201
+ config.apiKey,
4202
+ config.workspaceId,
4203
+ globalOptions.verbose || options.verbose
4204
+ );
4205
+
4206
+ const spinner = ora('Creating function...').start();
4207
+ const result = await api._makeRequest('POST', 'function/create', {
4208
+ name: options.name,
4209
+ description: options.description,
4210
+ code: options.code,
4211
+ });
4212
+ spinner.stop();
4213
+
4214
+ if (options.output === 'json') {
4215
+ console.log(JSON.stringify(result, null, 2));
4216
+ } else {
4217
+ console.log(chalk.green.bold('✅ Function created successfully!'));
4218
+ console.log(chalk.dim(`ID: ${result.id || 'N/A'}`));
4219
+ }
4220
+ } catch (error) {
4221
+ console.error(chalk.red(`Error creating function: ${error.message}`));
4222
+ process.exit(1);
4223
+ }
4224
+ });
4225
+
4226
+ program
4227
+ .command('list-functions')
4228
+ .description('List all functions')
4229
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4230
+ .option('-v, --verbose', 'Show detailed information')
4231
+ .action(async (options, command) => {
4232
+ try {
4233
+ const globalOptions = command.parent.opts();
4234
+ const config = loadConfig(globalOptions.config);
4235
+ validateConfiguration(config);
4236
+
4237
+ const api = new ToothFairyAPI(
4238
+ config.baseUrl,
4239
+ config.aiUrl,
4240
+ config.aiStreamUrl,
4241
+ config.apiKey,
4242
+ config.workspaceId,
4243
+ globalOptions.verbose || options.verbose
4244
+ );
4245
+
4246
+ const spinner = ora('Fetching functions...').start();
4247
+ const result = await api._makeRequest('GET', 'function/list');
4248
+ spinner.stop();
4249
+
4250
+ if (options.output === 'json') {
4251
+ console.log(JSON.stringify(result, null, 2));
4252
+ } else {
4253
+ const functions = Array.isArray(result) ? result : result.items || [];
4254
+ console.log(chalk.green.bold(`Found ${functions.length} function(s)`));
4255
+ functions.forEach(fn => {
4256
+ console.log(chalk.cyan(` • ${fn.name || 'Unnamed'} (${fn.id})`));
4257
+ });
4258
+ }
4259
+ } catch (error) {
4260
+ console.error(chalk.red(`Error listing functions: ${error.message}`));
4261
+ process.exit(1);
4262
+ }
4263
+ });
4264
+
4265
+ program
4266
+ .command('get-function')
4267
+ .description('Get details of a specific function')
4268
+ .argument('<id>', 'Function ID')
4269
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4270
+ .option('-v, --verbose', 'Show detailed information')
4271
+ .action(async (functionId, options, command) => {
4272
+ try {
4273
+ const globalOptions = command.parent.opts();
4274
+ const config = loadConfig(globalOptions.config);
4275
+ validateConfiguration(config);
4276
+
4277
+ const api = new ToothFairyAPI(
4278
+ config.baseUrl,
4279
+ config.aiUrl,
4280
+ config.aiStreamUrl,
4281
+ config.apiKey,
4282
+ config.workspaceId,
4283
+ globalOptions.verbose || options.verbose
4284
+ );
4285
+
4286
+ const spinner = ora('Fetching function...').start();
4287
+ const result = await api._makeRequest('GET', `function/get/${functionId}`);
4288
+ spinner.stop();
4289
+
4290
+ if (options.output === 'json') {
4291
+ console.log(JSON.stringify(result, null, 2));
4292
+ } else {
4293
+ console.log(chalk.green.bold('Function Details'));
4294
+ console.log(chalk.dim(`ID: ${result.id}`));
4295
+ console.log(chalk.dim(`Name: ${result.name}`));
4296
+ console.log(chalk.dim(`Description: ${result.description || 'N/A'}`));
4297
+ }
4298
+ } catch (error) {
4299
+ console.error(chalk.red(`Error getting function: ${error.message}`));
4300
+ process.exit(1);
4301
+ }
4302
+ });
4303
+
4304
+ program
4305
+ .command('update-function')
4306
+ .description('Update an existing function')
4307
+ .argument('<id>', 'Function ID')
4308
+ .option('--name <name>', 'Function name')
4309
+ .option('--description <description>', 'Function description')
4310
+ .option('--code <code>', 'Function code')
4311
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4312
+ .option('-v, --verbose', 'Show detailed information')
4313
+ .action(async (functionId, options, command) => {
4314
+ try {
4315
+ const globalOptions = command.parent.opts();
4316
+ const config = loadConfig(globalOptions.config);
4317
+ validateConfiguration(config);
4318
+
4319
+ const api = new ToothFairyAPI(
4320
+ config.baseUrl,
4321
+ config.aiUrl,
4322
+ config.aiStreamUrl,
4323
+ config.apiKey,
4324
+ config.workspaceId,
4325
+ globalOptions.verbose || options.verbose
4326
+ );
4327
+
4328
+ const spinner = ora('Updating function...').start();
4329
+ const result = await api._makeRequest('POST', `function/update/${functionId}`, {
4330
+ name: options.name,
4331
+ description: options.description,
4332
+ code: options.code,
4333
+ });
4334
+ spinner.stop();
4335
+
4336
+ if (options.output === 'json') {
4337
+ console.log(JSON.stringify(result, null, 2));
4338
+ } else {
4339
+ console.log(chalk.green.bold('✅ Function updated successfully!'));
4340
+ }
4341
+ } catch (error) {
4342
+ console.error(chalk.red(`Error updating function: ${error.message}`));
4343
+ process.exit(1);
4344
+ }
4345
+ });
4346
+
4347
+ program
4348
+ .command('delete-function')
4349
+ .description('Delete a function')
4350
+ .argument('<id>', 'Function ID')
4351
+ .option('--confirm', 'Skip confirmation prompt')
4352
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4353
+ .option('-v, --verbose', 'Show detailed information')
4354
+ .action(async (functionId, options, command) => {
4355
+ try {
4356
+ const globalOptions = command.parent.opts();
4357
+ const config = loadConfig(globalOptions.config);
4358
+ validateConfiguration(config);
4359
+
4360
+ if (!options.confirm) {
4361
+ const readline = require('readline');
4362
+ const rl = readline.createInterface({
4363
+ input: process.stdin,
4364
+ output: process.stdout,
4365
+ });
4366
+
4367
+ const answer = await new Promise((resolve) => {
4368
+ rl.question(
4369
+ chalk.yellow(`⚠️ Are you sure you want to delete function ${functionId}? (y/N): `),
4370
+ resolve
4371
+ );
4372
+ });
4373
+ rl.close();
4374
+
4375
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
4376
+ console.log(chalk.gray('Deletion cancelled.'));
4377
+ process.exit(0);
4378
+ }
4379
+ }
4380
+
4381
+ const api = new ToothFairyAPI(
4382
+ config.baseUrl,
4383
+ config.aiUrl,
4384
+ config.aiStreamUrl,
4385
+ config.apiKey,
4386
+ config.workspaceId,
4387
+ globalOptions.verbose || options.verbose
4388
+ );
4389
+
4390
+ const spinner = ora('Deleting function...').start();
4391
+ const result = await api._makeRequest('DELETE', `function/delete/${functionId}`);
4392
+ spinner.stop();
4393
+
4394
+ if (options.output === 'json') {
4395
+ console.log(JSON.stringify(result, null, 2));
4396
+ } else {
4397
+ console.log(chalk.green.bold('✅ Function deleted successfully!'));
4398
+ }
4399
+ } catch (error) {
4400
+ console.error(chalk.red(`Error deleting function: ${error.message}`));
4401
+ process.exit(1);
4402
+ }
4403
+ });
4404
+
4405
+ // Channel commands
4406
+ program
4407
+ .command('create-channel')
4408
+ .description('Create a new channel')
4409
+ .option('--name <name>', 'Channel name')
4410
+ .option('--type <type>', 'Channel type')
4411
+ .option('--config <config>', 'Channel configuration (JSON)')
4412
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4413
+ .option('-v, --verbose', 'Show detailed information')
4414
+ .action(async (options, command) => {
4415
+ try {
4416
+ const globalOptions = command.parent.opts();
4417
+ const config = loadConfig(globalOptions.config);
4418
+ validateConfiguration(config);
4419
+
4420
+ const api = new ToothFairyAPI(
4421
+ config.baseUrl,
4422
+ config.aiUrl,
4423
+ config.aiStreamUrl,
4424
+ config.apiKey,
4425
+ config.workspaceId,
4426
+ globalOptions.verbose || options.verbose
4427
+ );
4428
+
4429
+ let channelConfig = {};
4430
+ if (options.config) {
4431
+ try {
4432
+ channelConfig = JSON.parse(options.config);
4433
+ } catch (e) {
4434
+ console.error(chalk.red('Invalid JSON in config'));
4435
+ process.exit(1);
4436
+ }
4437
+ }
4438
+
4439
+ const spinner = ora('Creating channel...').start();
4440
+ const result = await api._makeRequest('POST', 'channel/create', {
4441
+ name: options.name,
4442
+ type: options.type,
4443
+ config: channelConfig,
4444
+ });
4445
+ spinner.stop();
4446
+
4447
+ if (options.output === 'json') {
4448
+ console.log(JSON.stringify(result, null, 2));
4449
+ } else {
4450
+ console.log(chalk.green.bold('✅ Channel created successfully!'));
4451
+ console.log(chalk.dim(`ID: ${result.id || 'N/A'}`));
4452
+ }
4453
+ } catch (error) {
4454
+ console.error(chalk.red(`Error creating channel: ${error.message}`));
4455
+ process.exit(1);
4456
+ }
4457
+ });
4458
+
4459
+ program
4460
+ .command('list-channels')
4461
+ .description('List all channels')
4462
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4463
+ .option('-v, --verbose', 'Show detailed information')
4464
+ .action(async (options, command) => {
4465
+ try {
4466
+ const globalOptions = command.parent.opts();
4467
+ const config = loadConfig(globalOptions.config);
4468
+ validateConfiguration(config);
4469
+
4470
+ const api = new ToothFairyAPI(
4471
+ config.baseUrl,
4472
+ config.aiUrl,
4473
+ config.aiStreamUrl,
4474
+ config.apiKey,
4475
+ config.workspaceId,
4476
+ globalOptions.verbose || options.verbose
4477
+ );
4478
+
4479
+ const spinner = ora('Fetching channels...').start();
4480
+ const result = await api._makeRequest('GET', 'channel/list');
4481
+ spinner.stop();
4482
+
4483
+ if (options.output === 'json') {
4484
+ console.log(JSON.stringify(result, null, 2));
4485
+ } else {
4486
+ const channels = Array.isArray(result) ? result : result.items || [];
4487
+ console.log(chalk.green.bold(`Found ${channels.length} channel(s)`));
4488
+ channels.forEach(ch => {
4489
+ console.log(chalk.cyan(` • ${ch.name || 'Unnamed'} (${ch.id})`));
4490
+ });
4491
+ }
4492
+ } catch (error) {
4493
+ console.error(chalk.red(`Error listing channels: ${error.message}`));
4494
+ process.exit(1);
4495
+ }
4496
+ });
4497
+
4498
+ program
4499
+ .command('get-channel')
4500
+ .description('Get details of a specific channel')
4501
+ .argument('<id>', 'Channel ID')
4502
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4503
+ .option('-v, --verbose', 'Show detailed information')
4504
+ .action(async (channelId, options, command) => {
4505
+ try {
4506
+ const globalOptions = command.parent.opts();
4507
+ const config = loadConfig(globalOptions.config);
4508
+ validateConfiguration(config);
4509
+
4510
+ const api = new ToothFairyAPI(
4511
+ config.baseUrl,
4512
+ config.aiUrl,
4513
+ config.aiStreamUrl,
4514
+ config.apiKey,
4515
+ config.workspaceId,
4516
+ globalOptions.verbose || options.verbose
4517
+ );
4518
+
4519
+ const spinner = ora('Fetching channel...').start();
4520
+ const result = await api._makeRequest('GET', `channel/get/${channelId}`);
4521
+ spinner.stop();
4522
+
4523
+ if (options.output === 'json') {
4524
+ console.log(JSON.stringify(result, null, 2));
4525
+ } else {
4526
+ console.log(chalk.green.bold('Channel Details'));
4527
+ console.log(chalk.dim(`ID: ${result.id}`));
4528
+ console.log(chalk.dim(`Name: ${result.name}`));
4529
+ console.log(chalk.dim(`Type: ${result.type || 'N/A'}`));
4530
+ }
4531
+ } catch (error) {
4532
+ console.error(chalk.red(`Error getting channel: ${error.message}`));
4533
+ process.exit(1);
4534
+ }
4535
+ });
4536
+
4537
+ program
4538
+ .command('update-channel')
4539
+ .description('Update an existing channel')
4540
+ .argument('<id>', 'Channel ID')
4541
+ .option('--name <name>', 'Channel name')
4542
+ .option('--type <type>', 'Channel type')
4543
+ .option('--config <config>', 'Channel configuration (JSON)')
4544
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4545
+ .option('-v, --verbose', 'Show detailed information')
4546
+ .action(async (channelId, options, command) => {
4547
+ try {
4548
+ const globalOptions = command.parent.opts();
4549
+ const config = loadConfig(globalOptions.config);
4550
+ validateConfiguration(config);
4551
+
4552
+ const api = new ToothFairyAPI(
4553
+ config.baseUrl,
4554
+ config.aiUrl,
4555
+ config.aiStreamUrl,
4556
+ config.apiKey,
4557
+ config.workspaceId,
4558
+ globalOptions.verbose || options.verbose
4559
+ );
4560
+
4561
+ let channelConfig = {};
4562
+ if (options.config) {
4563
+ try {
4564
+ channelConfig = JSON.parse(options.config);
4565
+ } catch (e) {
4566
+ console.error(chalk.red('Invalid JSON in config'));
4567
+ process.exit(1);
4568
+ }
4569
+ }
4570
+
4571
+ const spinner = ora('Updating channel...').start();
4572
+ const result = await api._makeRequest('POST', 'channel/update', {
4573
+ id: channelId,
4574
+ name: options.name,
4575
+ type: options.type,
4576
+ config: channelConfig,
4577
+ });
4578
+ spinner.stop();
4579
+
4580
+ if (options.output === 'json') {
4581
+ console.log(JSON.stringify(result, null, 2));
4582
+ } else {
4583
+ console.log(chalk.green.bold('✅ Channel updated successfully!'));
4584
+ }
4585
+ } catch (error) {
4586
+ console.error(chalk.red(`Error updating channel: ${error.message}`));
4587
+ process.exit(1);
4588
+ }
4589
+ });
4590
+
4591
+ program
4592
+ .command('delete-channel')
4593
+ .description('Delete a channel')
4594
+ .argument('<id>', 'Channel ID')
4595
+ .option('--confirm', 'Skip confirmation prompt')
4596
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4597
+ .option('-v, --verbose', 'Show detailed information')
4598
+ .action(async (channelId, options, command) => {
4599
+ try {
4600
+ const globalOptions = command.parent.opts();
4601
+ const config = loadConfig(globalOptions.config);
4602
+ validateConfiguration(config);
4603
+
4604
+ if (!options.confirm) {
4605
+ const readline = require('readline');
4606
+ const rl = readline.createInterface({
4607
+ input: process.stdin,
4608
+ output: process.stdout,
4609
+ });
4610
+
4611
+ const answer = await new Promise((resolve) => {
4612
+ rl.question(
4613
+ chalk.yellow(`⚠️ Are you sure you want to delete channel ${channelId}? (y/N): `),
4614
+ resolve
4615
+ );
4616
+ });
4617
+ rl.close();
4618
+
4619
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
4620
+ console.log(chalk.gray('Deletion cancelled.'));
4621
+ process.exit(0);
4622
+ }
4623
+ }
4624
+
4625
+ const api = new ToothFairyAPI(
4626
+ config.baseUrl,
4627
+ config.aiUrl,
4628
+ config.aiStreamUrl,
4629
+ config.apiKey,
4630
+ config.workspaceId,
4631
+ globalOptions.verbose || options.verbose
4632
+ );
4633
+
4634
+ const spinner = ora('Deleting channel...').start();
4635
+ const result = await api._makeRequest('DELETE', `channel/delete/${channelId}`);
4636
+ spinner.stop();
4637
+
4638
+ if (options.output === 'json') {
4639
+ console.log(JSON.stringify(result, null, 2));
4640
+ } else {
4641
+ console.log(chalk.green.bold('✅ Channel deleted successfully!'));
4642
+ }
4643
+ } catch (error) {
4644
+ console.error(chalk.red(`Error deleting channel: ${error.message}`));
4645
+ process.exit(1);
4646
+ }
4647
+ });
4648
+
4649
+ // Billing command
4650
+ program
4651
+ .command('billing')
4652
+ .description('Get monthly billing costs')
4653
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4654
+ .option('-v, --verbose', 'Show detailed information')
4655
+ .action(async (options, command) => {
4656
+ try {
4657
+ const globalOptions = command.parent.opts();
4658
+ const config = loadConfig(globalOptions.config);
4659
+ validateConfiguration(config);
4660
+
4661
+ const api = new ToothFairyAPI(
4662
+ config.baseUrl,
4663
+ config.aiUrl,
4664
+ config.aiStreamUrl,
4665
+ config.apiKey,
4666
+ config.workspaceId,
4667
+ globalOptions.verbose || options.verbose
4668
+ );
4669
+
4670
+ const spinner = ora('Fetching billing information...').start();
4671
+ const result = await api._makeRequest('GET', 'billing/monthCosts');
4672
+ spinner.stop();
4673
+
4674
+ if (options.output === 'json') {
4675
+ console.log(JSON.stringify(result, null, 2));
4676
+ } else {
4677
+ console.log(chalk.green.bold('💰 Monthly Billing'));
4678
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
4679
+ }
4680
+ } catch (error) {
4681
+ console.error(chalk.red(`Error fetching billing: ${error.message}`));
4682
+ process.exit(1);
4683
+ }
4684
+ });
4685
+
4686
+ // Embedding command
4687
+ program
4688
+ .command('get-embedding')
4689
+ .description('Get embedding for text')
4690
+ .argument('<text>', 'Text to get embedding for')
4691
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4692
+ .option('-v, --verbose', 'Show detailed information')
4693
+ .action(async (text, options, command) => {
4694
+ try {
4695
+ const globalOptions = command.parent.opts();
4696
+ const config = loadConfig(globalOptions.config);
4697
+ validateConfiguration(config);
4698
+
4699
+ const api = new ToothFairyAPI(
4700
+ config.baseUrl,
4701
+ config.aiUrl,
4702
+ config.aiStreamUrl,
4703
+ config.apiKey,
4704
+ config.workspaceId,
4705
+ globalOptions.verbose || options.verbose
4706
+ );
4707
+
4708
+ const spinner = ora('Generating embedding...').start();
4709
+ const result = await api._makeRequest('GET', 'embedding/get', { text });
4710
+ spinner.stop();
4711
+
4712
+ if (options.output === 'json') {
4713
+ console.log(JSON.stringify(result, null, 2));
4714
+ } else {
4715
+ console.log(chalk.green.bold('🔢 Embedding Generated'));
4716
+ console.log(chalk.dim(`Dimensions: ${result.embedding?.length || 'N/A'}`));
4717
+ }
4718
+ } catch (error) {
4719
+ console.error(chalk.red(`Error getting embedding: ${error.message}`));
4720
+ process.exit(1);
4721
+ }
4722
+ });
4723
+
4724
+ // Dictionary commands
4725
+ program
4726
+ .command('get-dictionary')
4727
+ .description('Get dictionary entry')
4728
+ .argument('<id>', 'Dictionary ID')
4729
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4730
+ .option('-v, --verbose', 'Show detailed information')
4731
+ .action(async (dictId, options, command) => {
4732
+ try {
4733
+ const globalOptions = command.parent.opts();
4734
+ const config = loadConfig(globalOptions.config);
4735
+ validateConfiguration(config);
4736
+
4737
+ const api = new ToothFairyAPI(
4738
+ config.baseUrl,
4739
+ config.aiUrl,
4740
+ config.aiStreamUrl,
4741
+ config.apiKey,
4742
+ config.workspaceId,
4743
+ globalOptions.verbose || options.verbose
4744
+ );
4745
+
4746
+ const spinner = ora('Fetching dictionary...').start();
4747
+ const result = await api._makeRequest('GET', `dictionary/get/${dictId}`);
4748
+ spinner.stop();
4749
+
4750
+ if (options.output === 'json') {
4751
+ console.log(JSON.stringify(result, null, 2));
4752
+ } else {
4753
+ console.log(chalk.green.bold('Dictionary Entry'));
4754
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
4755
+ }
4756
+ } catch (error) {
4757
+ console.error(chalk.red(`Error getting dictionary: ${error.message}`));
4758
+ process.exit(1);
4759
+ }
4760
+ });
4761
+
4762
+ program
4763
+ .command('list-dictionaries')
4764
+ .description('List all dictionaries')
4765
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4766
+ .option('-v, --verbose', 'Show detailed information')
4767
+ .action(async (options, command) => {
4768
+ try {
4769
+ const globalOptions = command.parent.opts();
4770
+ const config = loadConfig(globalOptions.config);
4771
+ validateConfiguration(config);
4772
+
4773
+ const api = new ToothFairyAPI(
4774
+ config.baseUrl,
4775
+ config.aiUrl,
4776
+ config.aiStreamUrl,
4777
+ config.apiKey,
4778
+ config.workspaceId,
4779
+ globalOptions.verbose || options.verbose
4780
+ );
4781
+
4782
+ const spinner = ora('Fetching dictionaries...').start();
4783
+ const result = await api._makeRequest('GET', 'dictionary/list');
4784
+ spinner.stop();
4785
+
4786
+ if (options.output === 'json') {
4787
+ console.log(JSON.stringify(result, null, 2));
4788
+ } else {
4789
+ const dicts = Array.isArray(result) ? result : result.items || [];
4790
+ console.log(chalk.green.bold(`Found ${dicts.length} dictionary/ies`));
4791
+ dicts.forEach(d => {
4792
+ console.log(chalk.cyan(` • ${d.id}`));
4793
+ });
4794
+ }
4795
+ } catch (error) {
4796
+ console.error(chalk.red(`Error listing dictionaries: ${error.message}`));
4797
+ process.exit(1);
4798
+ }
4799
+ });
4800
+
4801
+ // Connection commands
4802
+ program
4803
+ .command('list-connections')
4804
+ .description('List all connections')
4805
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4806
+ .option('-v, --verbose', 'Show detailed information')
4807
+ .action(async (options, command) => {
4808
+ try {
4809
+ const globalOptions = command.parent.opts();
4810
+ const config = loadConfig(globalOptions.config);
4811
+ validateConfiguration(config);
4812
+
4813
+ const api = new ToothFairyAPI(
4814
+ config.baseUrl,
4815
+ config.aiUrl,
4816
+ config.aiStreamUrl,
4817
+ config.apiKey,
4818
+ config.workspaceId,
4819
+ globalOptions.verbose || options.verbose
4820
+ );
4821
+
4822
+ const spinner = ora('Fetching connections...').start();
4823
+ const result = await api._makeRequest('GET', 'connection/list');
4824
+ spinner.stop();
4825
+
4826
+ if (options.output === 'json') {
4827
+ console.log(JSON.stringify(result, null, 2));
4828
+ } else {
4829
+ const connections = Array.isArray(result) ? result : result.items || [];
4830
+ console.log(chalk.green.bold(`Found ${connections.length} connection(s)`));
4831
+ connections.forEach(c => {
4832
+ console.log(chalk.cyan(` • ${c.id}`));
4833
+ });
4834
+ }
4835
+ } catch (error) {
4836
+ console.error(chalk.red(`Error listing connections: ${error.message}`));
4837
+ process.exit(1);
4838
+ }
4839
+ });
4840
+
4841
+ program
4842
+ .command('get-connection')
4843
+ .description('Get details of a specific connection')
4844
+ .argument('<id>', 'Connection ID')
4845
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4846
+ .option('-v, --verbose', 'Show detailed information')
4847
+ .action(async (connectionId, options, command) => {
4848
+ try {
4849
+ const globalOptions = command.parent.opts();
4850
+ const config = loadConfig(globalOptions.config);
4851
+ validateConfiguration(config);
4852
+
4853
+ const api = new ToothFairyAPI(
4854
+ config.baseUrl,
4855
+ config.aiUrl,
4856
+ config.aiStreamUrl,
4857
+ config.apiKey,
4858
+ config.workspaceId,
4859
+ globalOptions.verbose || options.verbose
4860
+ );
4861
+
4862
+ const spinner = ora('Fetching connection...').start();
4863
+ const result = await api._makeRequest('GET', `connection/get/${connectionId}`);
4864
+ spinner.stop();
4865
+
4866
+ if (options.output === 'json') {
4867
+ console.log(JSON.stringify(result, null, 2));
4868
+ } else {
4869
+ console.log(chalk.green.bold('Connection Details'));
4870
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
4871
+ }
4872
+ } catch (error) {
4873
+ console.error(chalk.red(`Error getting connection: ${error.message}`));
4874
+ process.exit(1);
4875
+ }
4876
+ });
4877
+
4878
+ program
4879
+ .command('delete-connection')
4880
+ .description('Delete a connection')
4881
+ .argument('<id>', 'Connection ID')
4882
+ .option('--confirm', 'Skip confirmation prompt')
4883
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4884
+ .option('-v, --verbose', 'Show detailed information')
4885
+ .action(async (connectionId, options, command) => {
4886
+ try {
4887
+ const globalOptions = command.parent.opts();
4888
+ const config = loadConfig(globalOptions.config);
4889
+ validateConfiguration(config);
4890
+
4891
+ if (!options.confirm) {
4892
+ const readline = require('readline');
4893
+ const rl = readline.createInterface({
4894
+ input: process.stdin,
4895
+ output: process.stdout,
4896
+ });
4897
+
4898
+ const answer = await new Promise((resolve) => {
4899
+ rl.question(
4900
+ chalk.yellow(`⚠️ Are you sure you want to delete connection ${connectionId}? (y/N): `),
4901
+ resolve
4902
+ );
4903
+ });
4904
+ rl.close();
4905
+
4906
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
4907
+ console.log(chalk.gray('Deletion cancelled.'));
4908
+ process.exit(0);
4909
+ }
4910
+ }
4911
+
4912
+ const api = new ToothFairyAPI(
4913
+ config.baseUrl,
4914
+ config.aiUrl,
4915
+ config.aiStreamUrl,
4916
+ config.apiKey,
4917
+ config.workspaceId,
4918
+ globalOptions.verbose || options.verbose
4919
+ );
4920
+
4921
+ const spinner = ora('Deleting connection...').start();
4922
+ const result = await api._makeRequest('DELETE', `connection/delete/${connectionId}`);
4923
+ spinner.stop();
4924
+
4925
+ if (options.output === 'json') {
4926
+ console.log(JSON.stringify(result, null, 2));
4927
+ } else {
4928
+ console.log(chalk.green.bold('✅ Connection deleted successfully!'));
4929
+ }
4930
+ } catch (error) {
4931
+ console.error(chalk.red(`Error deleting connection: ${error.message}`));
4932
+ process.exit(1);
4933
+ }
4934
+ });
4935
+
4936
+ // Member commands
4937
+ program
4938
+ .command('list-members')
4939
+ .description('List all members')
4940
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4941
+ .option('-v, --verbose', 'Show detailed information')
4942
+ .action(async (options, command) => {
4943
+ try {
4944
+ const globalOptions = command.parent.opts();
4945
+ const config = loadConfig(globalOptions.config);
4946
+ validateConfiguration(config);
4947
+
4948
+ const api = new ToothFairyAPI(
4949
+ config.baseUrl,
4950
+ config.aiUrl,
4951
+ config.aiStreamUrl,
4952
+ config.apiKey,
4953
+ config.workspaceId,
4954
+ globalOptions.verbose || options.verbose
4955
+ );
4956
+
4957
+ const spinner = ora('Fetching members...').start();
4958
+ const result = await api._makeRequest('GET', 'member/list');
4959
+ spinner.stop();
4960
+
4961
+ if (options.output === 'json') {
4962
+ console.log(JSON.stringify(result, null, 2));
4963
+ } else {
4964
+ const members = Array.isArray(result) ? result : result.items || [];
4965
+ console.log(chalk.green.bold(`Found ${members.length} member(s)`));
4966
+ members.forEach(m => {
4967
+ console.log(chalk.cyan(` • ${m.name || m.email || m.id}`));
4968
+ });
4969
+ }
4970
+ } catch (error) {
4971
+ console.error(chalk.red(`Error listing members: ${error.message}`));
4972
+ process.exit(1);
4973
+ }
4974
+ });
4975
+
4976
+ program
4977
+ .command('get-member')
4978
+ .description('Get details of a specific member')
4979
+ .argument('<id>', 'Member ID')
4980
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
4981
+ .option('-v, --verbose', 'Show detailed information')
4982
+ .action(async (memberId, options, command) => {
4983
+ try {
4984
+ const globalOptions = command.parent.opts();
4985
+ const config = loadConfig(globalOptions.config);
4986
+ validateConfiguration(config);
4987
+
4988
+ const api = new ToothFairyAPI(
4989
+ config.baseUrl,
4990
+ config.aiUrl,
4991
+ config.aiStreamUrl,
4992
+ config.apiKey,
4993
+ config.workspaceId,
4994
+ globalOptions.verbose || options.verbose
4995
+ );
4996
+
4997
+ const spinner = ora('Fetching member...').start();
4998
+ const result = await api._makeRequest('GET', `member/get/${memberId}`);
4999
+ spinner.stop();
5000
+
5001
+ if (options.output === 'json') {
5002
+ console.log(JSON.stringify(result, null, 2));
5003
+ } else {
5004
+ console.log(chalk.green.bold('Member Details'));
5005
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
5006
+ }
5007
+ } catch (error) {
5008
+ console.error(chalk.red(`Error getting member: ${error.message}`));
5009
+ process.exit(1);
5010
+ }
5011
+ });
5012
+
5013
+ program
5014
+ .command('update-member')
5015
+ .description('Update a member')
5016
+ .argument('<id>', 'Member ID')
5017
+ .option('--name <name>', 'Member name')
5018
+ .option('--email <email>', 'Member email')
5019
+ .option('--role <role>', 'Member role')
5020
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5021
+ .option('-v, --verbose', 'Show detailed information')
5022
+ .action(async (memberId, options, command) => {
5023
+ try {
5024
+ const globalOptions = command.parent.opts();
5025
+ const config = loadConfig(globalOptions.config);
5026
+ validateConfiguration(config);
5027
+
5028
+ const api = new ToothFairyAPI(
5029
+ config.baseUrl,
5030
+ config.aiUrl,
5031
+ config.aiStreamUrl,
5032
+ config.apiKey,
5033
+ config.workspaceId,
5034
+ globalOptions.verbose || options.verbose
5035
+ );
5036
+
5037
+ const spinner = ora('Updating member...').start();
5038
+ const result = await api._makeRequest('POST', 'member/update', {
5039
+ id: memberId,
5040
+ name: options.name,
5041
+ email: options.email,
5042
+ role: options.role,
5043
+ });
5044
+ spinner.stop();
5045
+
5046
+ if (options.output === 'json') {
5047
+ console.log(JSON.stringify(result, null, 2));
5048
+ } else {
5049
+ console.log(chalk.green.bold('✅ Member updated successfully!'));
5050
+ }
5051
+ } catch (error) {
5052
+ console.error(chalk.red(`Error updating member: ${error.message}`));
5053
+ process.exit(1);
5054
+ }
5055
+ });
5056
+
5057
+ program
5058
+ .command('delete-member')
5059
+ .description('Delete a member')
5060
+ .argument('<id>', 'Member ID')
5061
+ .option('--confirm', 'Skip confirmation prompt')
5062
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5063
+ .option('-v, --verbose', 'Show detailed information')
5064
+ .action(async (memberId, options, command) => {
5065
+ try {
5066
+ const globalOptions = command.parent.opts();
5067
+ const config = loadConfig(globalOptions.config);
5068
+ validateConfiguration(config);
5069
+
5070
+ if (!options.confirm) {
5071
+ const readline = require('readline');
5072
+ const rl = readline.createInterface({
5073
+ input: process.stdin,
5074
+ output: process.stdout,
5075
+ });
5076
+
5077
+ const answer = await new Promise((resolve) => {
5078
+ rl.question(
5079
+ chalk.yellow(`⚠️ Are you sure you want to delete member ${memberId}? (y/N): `),
5080
+ resolve
5081
+ );
5082
+ });
5083
+ rl.close();
5084
+
5085
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
5086
+ console.log(chalk.gray('Deletion cancelled.'));
5087
+ process.exit(0);
5088
+ }
5089
+ }
5090
+
5091
+ const api = new ToothFairyAPI(
5092
+ config.baseUrl,
5093
+ config.aiUrl,
5094
+ config.aiStreamUrl,
5095
+ config.apiKey,
5096
+ config.workspaceId,
5097
+ globalOptions.verbose || options.verbose
5098
+ );
5099
+
5100
+ const spinner = ora('Deleting member...').start();
5101
+ const result = await api._makeRequest('DELETE', `member/delete/${memberId}`);
5102
+ spinner.stop();
5103
+
5104
+ if (options.output === 'json') {
5105
+ console.log(JSON.stringify(result, null, 2));
5106
+ } else {
5107
+ console.log(chalk.green.bold('✅ Member deleted successfully!'));
5108
+ }
5109
+ } catch (error) {
5110
+ console.error(chalk.red(`Error deleting member: ${error.message}`));
5111
+ process.exit(1);
5112
+ }
5113
+ });
5114
+
5115
+ // Site commands
5116
+ program
5117
+ .command('list-sites')
5118
+ .description('List all sites')
5119
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5120
+ .option('-v, --verbose', 'Show detailed information')
5121
+ .action(async (options, command) => {
5122
+ try {
5123
+ const globalOptions = command.parent.opts();
5124
+ const config = loadConfig(globalOptions.config);
5125
+ validateConfiguration(config);
5126
+
5127
+ const api = new ToothFairyAPI(
5128
+ config.baseUrl,
5129
+ config.aiUrl,
5130
+ config.aiStreamUrl,
5131
+ config.apiKey,
5132
+ config.workspaceId,
5133
+ globalOptions.verbose || options.verbose
5134
+ );
5135
+
5136
+ const spinner = ora('Fetching sites...').start();
5137
+ const result = await api._makeRequest('GET', 'site/list');
5138
+ spinner.stop();
5139
+
5140
+ if (options.output === 'json') {
5141
+ console.log(JSON.stringify(result, null, 2));
5142
+ } else {
5143
+ const sites = Array.isArray(result) ? result : result.items || [];
5144
+ console.log(chalk.green.bold(`Found ${sites.length} site(s)`));
5145
+ sites.forEach(s => {
5146
+ console.log(chalk.cyan(` • ${s.name || s.id}`));
5147
+ });
5148
+ }
5149
+ } catch (error) {
5150
+ console.error(chalk.red(`Error listing sites: ${error.message}`));
5151
+ process.exit(1);
5152
+ }
5153
+ });
5154
+
5155
+ program
5156
+ .command('get-site')
5157
+ .description('Get details of a specific site')
5158
+ .argument('<id>', 'Site ID')
5159
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5160
+ .option('-v, --verbose', 'Show detailed information')
5161
+ .action(async (siteId, options, command) => {
5162
+ try {
5163
+ const globalOptions = command.parent.opts();
5164
+ const config = loadConfig(globalOptions.config);
5165
+ validateConfiguration(config);
5166
+
5167
+ const api = new ToothFairyAPI(
5168
+ config.baseUrl,
5169
+ config.aiUrl,
5170
+ config.aiStreamUrl,
5171
+ config.apiKey,
5172
+ config.workspaceId,
5173
+ globalOptions.verbose || options.verbose
5174
+ );
5175
+
5176
+ const spinner = ora('Fetching site...').start();
5177
+ const result = await api._makeRequest('GET', `site/get/${siteId}`);
5178
+ spinner.stop();
5179
+
5180
+ if (options.output === 'json') {
5181
+ console.log(JSON.stringify(result, null, 2));
5182
+ } else {
5183
+ console.log(chalk.green.bold('Site Details'));
5184
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
5185
+ }
5186
+ } catch (error) {
5187
+ console.error(chalk.red(`Error getting site: ${error.message}`));
5188
+ process.exit(1);
5189
+ }
5190
+ });
5191
+
5192
+ program
5193
+ .command('update-site')
5194
+ .description('Update a site')
5195
+ .argument('<id>', 'Site ID')
5196
+ .option('--name <name>', 'Site name')
5197
+ .option('--url <url>', 'Site URL')
5198
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5199
+ .option('-v, --verbose', 'Show detailed information')
5200
+ .action(async (siteId, options, command) => {
5201
+ try {
5202
+ const globalOptions = command.parent.opts();
5203
+ const config = loadConfig(globalOptions.config);
5204
+ validateConfiguration(config);
5205
+
5206
+ const api = new ToothFairyAPI(
5207
+ config.baseUrl,
5208
+ config.aiUrl,
5209
+ config.aiStreamUrl,
5210
+ config.apiKey,
5211
+ config.workspaceId,
5212
+ globalOptions.verbose || options.verbose
5213
+ );
5214
+
5215
+ const spinner = ora('Updating site...').start();
5216
+ const result = await api._makeRequest('POST', 'site/update', {
5217
+ id: siteId,
5218
+ name: options.name,
5219
+ url: options.url,
5220
+ });
5221
+ spinner.stop();
5222
+
5223
+ if (options.output === 'json') {
5224
+ console.log(JSON.stringify(result, null, 2));
5225
+ } else {
5226
+ console.log(chalk.green.bold('✅ Site updated successfully!'));
5227
+ }
5228
+ } catch (error) {
5229
+ console.error(chalk.red(`Error updating site: ${error.message}`));
5230
+ process.exit(1);
5231
+ }
5232
+ });
5233
+
5234
+ program
5235
+ .command('delete-site')
5236
+ .description('Delete a site')
5237
+ .argument('<id>', 'Site ID')
5238
+ .option('--confirm', 'Skip confirmation prompt')
5239
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5240
+ .option('-v, --verbose', 'Show detailed information')
5241
+ .action(async (siteId, options, command) => {
5242
+ try {
5243
+ const globalOptions = command.parent.opts();
5244
+ const config = loadConfig(globalOptions.config);
5245
+ validateConfiguration(config);
5246
+
5247
+ if (!options.confirm) {
5248
+ const readline = require('readline');
5249
+ const rl = readline.createInterface({
5250
+ input: process.stdin,
5251
+ output: process.stdout,
5252
+ });
5253
+
5254
+ const answer = await new Promise((resolve) => {
5255
+ rl.question(
5256
+ chalk.yellow(`⚠️ Are you sure you want to delete site ${siteId}? (y/N): `),
5257
+ resolve
5258
+ );
5259
+ });
5260
+ rl.close();
5261
+
5262
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
5263
+ console.log(chalk.gray('Deletion cancelled.'));
5264
+ process.exit(0);
5265
+ }
5266
+ }
5267
+
5268
+ const api = new ToothFairyAPI(
5269
+ config.baseUrl,
5270
+ config.aiUrl,
5271
+ config.aiStreamUrl,
5272
+ config.apiKey,
5273
+ config.workspaceId,
5274
+ globalOptions.verbose || options.verbose
5275
+ );
5276
+
5277
+ const spinner = ora('Deleting site...').start();
5278
+ const result = await api._makeRequest('DELETE', `site/delete/${siteId}`);
5279
+ spinner.stop();
5280
+
5281
+ if (options.output === 'json') {
5282
+ console.log(JSON.stringify(result, null, 2));
5283
+ } else {
5284
+ console.log(chalk.green.bold('✅ Site deleted successfully!'));
5285
+ }
5286
+ } catch (error) {
5287
+ console.error(chalk.red(`Error deleting site: ${error.message}`));
5288
+ process.exit(1);
5289
+ }
5290
+ });
5291
+
5292
+ // Stream commands
5293
+ program
5294
+ .command('list-streams')
5295
+ .description('List all streams')
5296
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5297
+ .option('-v, --verbose', 'Show detailed information')
5298
+ .action(async (options, command) => {
5299
+ try {
5300
+ const globalOptions = command.parent.opts();
5301
+ const config = loadConfig(globalOptions.config);
5302
+ validateConfiguration(config);
5303
+
5304
+ const api = new ToothFairyAPI(
5305
+ config.baseUrl,
5306
+ config.aiUrl,
5307
+ config.aiStreamUrl,
5308
+ config.apiKey,
5309
+ config.workspaceId,
5310
+ globalOptions.verbose || options.verbose
5311
+ );
5312
+
5313
+ const spinner = ora('Fetching streams...').start();
5314
+ const result = await api._makeRequest('GET', 'stream/list');
5315
+ spinner.stop();
5316
+
5317
+ if (options.output === 'json') {
5318
+ console.log(JSON.stringify(result, null, 2));
5319
+ } else {
5320
+ const streams = Array.isArray(result) ? result : result.items || [];
5321
+ console.log(chalk.green.bold(`Found ${streams.length} stream(s)`));
5322
+ streams.forEach(s => {
5323
+ console.log(chalk.cyan(` • ${s.id}`));
5324
+ });
5325
+ }
5326
+ } catch (error) {
5327
+ console.error(chalk.red(`Error listing streams: ${error.message}`));
5328
+ process.exit(1);
5329
+ }
5330
+ });
5331
+
5332
+ program
5333
+ .command('get-stream')
5334
+ .description('Get details of a specific stream')
5335
+ .argument('<id>', 'Stream ID')
5336
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5337
+ .option('-v, --verbose', 'Show detailed information')
5338
+ .action(async (streamId, options, command) => {
5339
+ try {
5340
+ const globalOptions = command.parent.opts();
5341
+ const config = loadConfig(globalOptions.config);
5342
+ validateConfiguration(config);
5343
+
5344
+ const api = new ToothFairyAPI(
5345
+ config.baseUrl,
5346
+ config.aiUrl,
5347
+ config.aiStreamUrl,
5348
+ config.apiKey,
5349
+ config.workspaceId,
5350
+ globalOptions.verbose || options.verbose
5351
+ );
5352
+
5353
+ const spinner = ora('Fetching stream...').start();
5354
+ const result = await api._makeRequest('GET', `stream/get/${streamId}`);
5355
+ spinner.stop();
5356
+
5357
+ if (options.output === 'json') {
5358
+ console.log(JSON.stringify(result, null, 2));
5359
+ } else {
5360
+ console.log(chalk.green.bold('Stream Details'));
5361
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
5362
+ }
5363
+ } catch (error) {
5364
+ console.error(chalk.red(`Error getting stream: ${error.message}`));
5365
+ process.exit(1);
5366
+ }
5367
+ });
5368
+
5369
+ // Request commands
5370
+ program
5371
+ .command('list-requests')
5372
+ .description('List all requests')
5373
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5374
+ .option('-v, --verbose', 'Show detailed information')
5375
+ .action(async (options, command) => {
5376
+ try {
5377
+ const globalOptions = command.parent.opts();
5378
+ const config = loadConfig(globalOptions.config);
5379
+ validateConfiguration(config);
5380
+
5381
+ const api = new ToothFairyAPI(
5382
+ config.baseUrl,
5383
+ config.aiUrl,
5384
+ config.aiStreamUrl,
5385
+ config.apiKey,
5386
+ config.workspaceId,
5387
+ globalOptions.verbose || options.verbose
5388
+ );
5389
+
5390
+ const spinner = ora('Fetching requests...').start();
5391
+ const result = await api._makeRequest('GET', 'request/list');
5392
+ spinner.stop();
5393
+
5394
+ if (options.output === 'json') {
5395
+ console.log(JSON.stringify(result, null, 2));
5396
+ } else {
5397
+ const requests = Array.isArray(result) ? result : result.items || [];
5398
+ console.log(chalk.green.bold(`Found ${requests.length} request(s)`));
5399
+ requests.forEach(r => {
5400
+ console.log(chalk.cyan(` • ${r.id}`));
5401
+ });
5402
+ }
5403
+ } catch (error) {
5404
+ console.error(chalk.red(`Error listing requests: ${error.message}`));
5405
+ process.exit(1);
5406
+ }
5407
+ });
5408
+
5409
+ program
5410
+ .command('get-request')
5411
+ .description('Get details of a specific request')
5412
+ .argument('<id>', 'Request ID')
5413
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5414
+ .option('-v, --verbose', 'Show detailed information')
5415
+ .action(async (requestId, options, command) => {
5416
+ try {
5417
+ const globalOptions = command.parent.opts();
5418
+ const config = loadConfig(globalOptions.config);
5419
+ validateConfiguration(config);
5420
+
5421
+ const api = new ToothFairyAPI(
5422
+ config.baseUrl,
5423
+ config.aiUrl,
5424
+ config.aiStreamUrl,
5425
+ config.apiKey,
5426
+ config.workspaceId,
5427
+ globalOptions.verbose || options.verbose
5428
+ );
5429
+
5430
+ const spinner = ora('Fetching request...').start();
5431
+ const result = await api._makeRequest('GET', `request/get/${requestId}`);
5432
+ spinner.stop();
5433
+
5434
+ if (options.output === 'json') {
5435
+ console.log(JSON.stringify(result, null, 2));
5436
+ } else {
5437
+ console.log(chalk.green.bold('Request Details'));
5438
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
5439
+ }
5440
+ } catch (error) {
5441
+ console.error(chalk.red(`Error getting request: ${error.message}`));
5442
+ process.exit(1);
5443
+ }
5444
+ });
5445
+
5446
+ // Secret commands
5447
+ program
5448
+ .command('create-secret')
5449
+ .description('Create a new secret')
5450
+ .option('--name <name>', 'Secret name')
5451
+ .option('--value <value>', 'Secret value')
5452
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5453
+ .option('-v, --verbose', 'Show detailed information')
5454
+ .action(async (options, command) => {
5455
+ try {
5456
+ const globalOptions = command.parent.opts();
5457
+ const config = loadConfig(globalOptions.config);
5458
+ validateConfiguration(config);
5459
+
5460
+ const api = new ToothFairyAPI(
5461
+ config.baseUrl,
5462
+ config.aiUrl,
5463
+ config.aiStreamUrl,
5464
+ config.apiKey,
5465
+ config.workspaceId,
5466
+ globalOptions.verbose || options.verbose
5467
+ );
5468
+
5469
+ const spinner = ora('Creating secret...').start();
5470
+ const result = await api._makeRequest('POST', 'secret/create', {
5471
+ name: options.name,
5472
+ value: options.value,
5473
+ });
5474
+ spinner.stop();
5475
+
5476
+ if (options.output === 'json') {
5477
+ console.log(JSON.stringify(result, null, 2));
5478
+ } else {
5479
+ console.log(chalk.green.bold('✅ Secret created successfully!'));
5480
+ console.log(chalk.dim(`ID: ${result.id || 'N/A'}`));
5481
+ }
5482
+ } catch (error) {
5483
+ console.error(chalk.red(`Error creating secret: ${error.message}`));
5484
+ process.exit(1);
5485
+ }
5486
+ });
5487
+
5488
+ program
5489
+ .command('delete-secret')
5490
+ .description('Delete a secret')
5491
+ .argument('<id>', 'Secret ID')
5492
+ .option('--confirm', 'Skip confirmation prompt')
5493
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5494
+ .option('-v, --verbose', 'Show detailed information')
5495
+ .action(async (secretId, options, command) => {
5496
+ try {
5497
+ const globalOptions = command.parent.opts();
5498
+ const config = loadConfig(globalOptions.config);
5499
+ validateConfiguration(config);
5500
+
5501
+ if (!options.confirm) {
5502
+ const readline = require('readline');
5503
+ const rl = readline.createInterface({
5504
+ input: process.stdin,
5505
+ output: process.stdout,
5506
+ });
5507
+
5508
+ const answer = await new Promise((resolve) => {
5509
+ rl.question(
5510
+ chalk.yellow(`⚠️ Are you sure you want to delete secret ${secretId}? (y/N): `),
5511
+ resolve
5512
+ );
5513
+ });
5514
+ rl.close();
5515
+
5516
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
5517
+ console.log(chalk.gray('Deletion cancelled.'));
5518
+ process.exit(0);
5519
+ }
5520
+ }
5521
+
5522
+ const api = new ToothFairyAPI(
5523
+ config.baseUrl,
5524
+ config.aiUrl,
5525
+ config.aiStreamUrl,
5526
+ config.apiKey,
5527
+ config.workspaceId,
5528
+ globalOptions.verbose || options.verbose
5529
+ );
5530
+
5531
+ const spinner = ora('Deleting secret...').start();
5532
+ const result = await api._makeRequest('DELETE', `secret/delete/${secretId}`);
5533
+ spinner.stop();
5534
+
5535
+ if (options.output === 'json') {
5536
+ console.log(JSON.stringify(result, null, 2));
5537
+ } else {
5538
+ console.log(chalk.green.bold('✅ Secret deleted successfully!'));
5539
+ }
5540
+ } catch (error) {
5541
+ console.error(chalk.red(`Error deleting secret: ${error.message}`));
5542
+ process.exit(1);
5543
+ }
5544
+ });
5545
+
5546
+ // Hook commands
5547
+ program
5548
+ .command('create-hook')
5549
+ .description('Create a new hook')
5550
+ .option('--name <name>', 'Hook name')
5551
+ .option('--url <url>', 'Hook URL')
5552
+ .option('--events <events>', 'Events to trigger on (comma-separated)')
5553
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5554
+ .option('-v, --verbose', 'Show detailed information')
5555
+ .action(async (options, command) => {
5556
+ try {
5557
+ const globalOptions = command.parent.opts();
5558
+ const config = loadConfig(globalOptions.config);
5559
+ validateConfiguration(config);
5560
+
5561
+ const api = new ToothFairyAPI(
5562
+ config.baseUrl,
5563
+ config.aiUrl,
5564
+ config.aiStreamUrl,
5565
+ config.apiKey,
5566
+ config.workspaceId,
5567
+ globalOptions.verbose || options.verbose
5568
+ );
5569
+
5570
+ const events = options.events ? options.events.split(',').map(e => e.trim()) : [];
5571
+
5572
+ const spinner = ora('Creating hook...').start();
5573
+ const result = await api._makeRequest('POST', 'hook/create', {
5574
+ name: options.name,
5575
+ url: options.url,
5576
+ events,
5577
+ });
5578
+ spinner.stop();
5579
+
5580
+ if (options.output === 'json') {
5581
+ console.log(JSON.stringify(result, null, 2));
5582
+ } else {
5583
+ console.log(chalk.green.bold('✅ Hook created successfully!'));
5584
+ console.log(chalk.dim(`ID: ${result.id || 'N/A'}`));
5585
+ }
5586
+ } catch (error) {
5587
+ console.error(chalk.red(`Error creating hook: ${error.message}`));
5588
+ process.exit(1);
5589
+ }
5590
+ });
5591
+
5592
+ program
5593
+ .command('list-hooks')
5594
+ .description('List all hooks')
5595
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5596
+ .option('-v, --verbose', 'Show detailed information')
5597
+ .action(async (options, command) => {
5598
+ try {
5599
+ const globalOptions = command.parent.opts();
5600
+ const config = loadConfig(globalOptions.config);
5601
+ validateConfiguration(config);
5602
+
5603
+ const api = new ToothFairyAPI(
5604
+ config.baseUrl,
5605
+ config.aiUrl,
5606
+ config.aiStreamUrl,
5607
+ config.apiKey,
5608
+ config.workspaceId,
5609
+ globalOptions.verbose || options.verbose
5610
+ );
5611
+
5612
+ const spinner = ora('Fetching hooks...').start();
5613
+ const result = await api._makeRequest('GET', 'hook/list');
5614
+ spinner.stop();
5615
+
5616
+ if (options.output === 'json') {
5617
+ console.log(JSON.stringify(result, null, 2));
5618
+ } else {
5619
+ const hooks = Array.isArray(result) ? result : result.items || [];
5620
+ console.log(chalk.green.bold(`Found ${hooks.length} hook(s)`));
5621
+ hooks.forEach(h => {
5622
+ console.log(chalk.cyan(` • ${h.name || 'Unnamed'} (${h.id})`));
5623
+ });
5624
+ }
5625
+ } catch (error) {
5626
+ console.error(chalk.red(`Error listing hooks: ${error.message}`));
5627
+ process.exit(1);
5628
+ }
5629
+ });
5630
+
5631
+ program
5632
+ .command('get-hook')
5633
+ .description('Get details of a specific hook')
5634
+ .argument('<id>', 'Hook ID')
5635
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5636
+ .option('-v, --verbose', 'Show detailed information')
5637
+ .action(async (hookId, options, command) => {
5638
+ try {
5639
+ const globalOptions = command.parent.opts();
5640
+ const config = loadConfig(globalOptions.config);
5641
+ validateConfiguration(config);
5642
+
5643
+ const api = new ToothFairyAPI(
5644
+ config.baseUrl,
5645
+ config.aiUrl,
5646
+ config.aiStreamUrl,
5647
+ config.apiKey,
5648
+ config.workspaceId,
5649
+ globalOptions.verbose || options.verbose
5650
+ );
5651
+
5652
+ const spinner = ora('Fetching hook...').start();
5653
+ const result = await api._makeRequest('GET', `hook/get/${hookId}`);
5654
+ spinner.stop();
5655
+
5656
+ if (options.output === 'json') {
5657
+ console.log(JSON.stringify(result, null, 2));
5658
+ } else {
5659
+ console.log(chalk.green.bold('Hook Details'));
5660
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
5661
+ }
5662
+ } catch (error) {
5663
+ console.error(chalk.red(`Error getting hook: ${error.message}`));
5664
+ process.exit(1);
5665
+ }
5666
+ });
5667
+
5668
+ program
5669
+ .command('update-hook')
5670
+ .description('Update an existing hook')
5671
+ .argument('<id>', 'Hook ID')
5672
+ .option('--name <name>', 'Hook name')
5673
+ .option('--url <url>', 'Hook URL')
5674
+ .option('--events <events>', 'Events to trigger on (comma-separated)')
5675
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5676
+ .option('-v, --verbose', 'Show detailed information')
5677
+ .action(async (hookId, options, command) => {
5678
+ try {
5679
+ const globalOptions = command.parent.opts();
5680
+ const config = loadConfig(globalOptions.config);
5681
+ validateConfiguration(config);
5682
+
5683
+ const api = new ToothFairyAPI(
5684
+ config.baseUrl,
5685
+ config.aiUrl,
5686
+ config.aiStreamUrl,
5687
+ config.apiKey,
5688
+ config.workspaceId,
5689
+ globalOptions.verbose || options.verbose
5690
+ );
5691
+
5692
+ const events = options.events ? options.events.split(',').map(e => e.trim()) : [];
5693
+
5694
+ const spinner = ora('Updating hook...').start();
5695
+ const result = await api._makeRequest('POST', `hook/update/${hookId}`, {
5696
+ name: options.name,
5697
+ url: options.url,
5698
+ events,
5699
+ });
5700
+ spinner.stop();
5701
+
5702
+ if (options.output === 'json') {
5703
+ console.log(JSON.stringify(result, null, 2));
5704
+ } else {
5705
+ console.log(chalk.green.bold('✅ Hook updated successfully!'));
5706
+ }
5707
+ } catch (error) {
5708
+ console.error(chalk.red(`Error updating hook: ${error.message}`));
5709
+ process.exit(1);
5710
+ }
5711
+ });
5712
+
5713
+ program
5714
+ .command('delete-hook')
5715
+ .description('Delete a hook')
5716
+ .argument('<id>', 'Hook ID')
5717
+ .option('--confirm', 'Skip confirmation prompt')
5718
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5719
+ .option('-v, --verbose', 'Show detailed information')
5720
+ .action(async (hookId, options, command) => {
5721
+ try {
5722
+ const globalOptions = command.parent.opts();
5723
+ const config = loadConfig(globalOptions.config);
5724
+ validateConfiguration(config);
5725
+
5726
+ if (!options.confirm) {
5727
+ const readline = require('readline');
5728
+ const rl = readline.createInterface({
5729
+ input: process.stdin,
5730
+ output: process.stdout,
5731
+ });
5732
+
5733
+ const answer = await new Promise((resolve) => {
5734
+ rl.question(
5735
+ chalk.yellow(`⚠️ Are you sure you want to delete hook ${hookId}? (y/N): `),
5736
+ resolve
5737
+ );
5738
+ });
5739
+ rl.close();
5740
+
5741
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
5742
+ console.log(chalk.gray('Deletion cancelled.'));
5743
+ process.exit(0);
5744
+ }
5745
+ }
5746
+
5747
+ const api = new ToothFairyAPI(
5748
+ config.baseUrl,
5749
+ config.aiUrl,
5750
+ config.aiStreamUrl,
5751
+ config.apiKey,
5752
+ config.workspaceId,
5753
+ globalOptions.verbose || options.verbose
5754
+ );
5755
+
5756
+ const spinner = ora('Deleting hook...').start();
5757
+ const result = await api._makeRequest('DELETE', `hook/delete/${hookId}`);
5758
+ spinner.stop();
5759
+
5760
+ if (options.output === 'json') {
5761
+ console.log(JSON.stringify(result, null, 2));
5762
+ } else {
5763
+ console.log(chalk.green.bold('✅ Hook deleted successfully!'));
5764
+ }
5765
+ } catch (error) {
5766
+ console.error(chalk.red(`Error deleting hook: ${error.message}`));
5767
+ process.exit(1);
5768
+ }
5769
+ });
5770
+
5771
+ // Benchmark commands
5772
+ program
5773
+ .command('create-benchmark')
5774
+ .description('Create a new benchmark')
5775
+ .option('--name <name>', 'Benchmark name')
5776
+ .option('--description <description>', 'Benchmark description')
5777
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5778
+ .option('-v, --verbose', 'Show detailed information')
5779
+ .action(async (options, command) => {
5780
+ try {
5781
+ const globalOptions = command.parent.opts();
5782
+ const config = loadConfig(globalOptions.config);
5783
+ validateConfiguration(config);
5784
+
5785
+ const api = new ToothFairyAPI(
5786
+ config.baseUrl,
5787
+ config.aiUrl,
5788
+ config.aiStreamUrl,
5789
+ config.apiKey,
5790
+ config.workspaceId,
5791
+ globalOptions.verbose || options.verbose
5792
+ );
5793
+
5794
+ const spinner = ora('Creating benchmark...').start();
5795
+ const result = await api._makeRequest('POST', 'benchmark/create', {
5796
+ name: options.name,
5797
+ description: options.description,
5798
+ });
5799
+ spinner.stop();
5800
+
5801
+ if (options.output === 'json') {
5802
+ console.log(JSON.stringify(result, null, 2));
5803
+ } else {
5804
+ console.log(chalk.green.bold('✅ Benchmark created successfully!'));
5805
+ console.log(chalk.dim(`ID: ${result.id || 'N/A'}`));
5806
+ }
5807
+ } catch (error) {
5808
+ console.error(chalk.red(`Error creating benchmark: ${error.message}`));
5809
+ process.exit(1);
5810
+ }
5811
+ });
5812
+
5813
+ program
5814
+ .command('list-benchmarks')
5815
+ .description('List all benchmarks')
5816
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5817
+ .option('-v, --verbose', 'Show detailed information')
5818
+ .action(async (options, command) => {
5819
+ try {
5820
+ const globalOptions = command.parent.opts();
5821
+ const config = loadConfig(globalOptions.config);
5822
+ validateConfiguration(config);
5823
+
5824
+ const api = new ToothFairyAPI(
5825
+ config.baseUrl,
5826
+ config.aiUrl,
5827
+ config.aiStreamUrl,
5828
+ config.apiKey,
5829
+ config.workspaceId,
5830
+ globalOptions.verbose || options.verbose
5831
+ );
5832
+
5833
+ const spinner = ora('Fetching benchmarks...').start();
5834
+ const result = await api._makeRequest('GET', 'benchmark/list');
5835
+ spinner.stop();
5836
+
5837
+ if (options.output === 'json') {
5838
+ console.log(JSON.stringify(result, null, 2));
5839
+ } else {
5840
+ const benchmarks = Array.isArray(result) ? result : result.items || [];
5841
+ console.log(chalk.green.bold(`Found ${benchmarks.length} benchmark(s)`));
5842
+ benchmarks.forEach(b => {
5843
+ console.log(chalk.cyan(` • ${b.name || 'Unnamed'} (${b.id})`));
5844
+ });
5845
+ }
5846
+ } catch (error) {
5847
+ console.error(chalk.red(`Error listing benchmarks: ${error.message}`));
5848
+ process.exit(1);
5849
+ }
5850
+ });
5851
+
5852
+ program
5853
+ .command('get-benchmark')
5854
+ .description('Get details of a specific benchmark')
5855
+ .argument('<id>', 'Benchmark ID')
5856
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5857
+ .option('-v, --verbose', 'Show detailed information')
5858
+ .action(async (benchmarkId, options, command) => {
5859
+ try {
5860
+ const globalOptions = command.parent.opts();
5861
+ const config = loadConfig(globalOptions.config);
5862
+ validateConfiguration(config);
5863
+
5864
+ const api = new ToothFairyAPI(
5865
+ config.baseUrl,
5866
+ config.aiUrl,
5867
+ config.aiStreamUrl,
5868
+ config.apiKey,
5869
+ config.workspaceId,
5870
+ globalOptions.verbose || options.verbose
5871
+ );
5872
+
5873
+ const spinner = ora('Fetching benchmark...').start();
5874
+ const result = await api._makeRequest('GET', `benchmark/get/${benchmarkId}`);
5875
+ spinner.stop();
5876
+
5877
+ if (options.output === 'json') {
5878
+ console.log(JSON.stringify(result, null, 2));
5879
+ } else {
5880
+ console.log(chalk.green.bold('Benchmark Details'));
5881
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
5882
+ }
5883
+ } catch (error) {
5884
+ console.error(chalk.red(`Error getting benchmark: ${error.message}`));
5885
+ process.exit(1);
5886
+ }
5887
+ });
5888
+
5889
+ program
5890
+ .command('update-benchmark')
5891
+ .description('Update an existing benchmark')
5892
+ .argument('<id>', 'Benchmark ID')
5893
+ .option('--name <name>', 'Benchmark name')
5894
+ .option('--description <description>', 'Benchmark description')
5895
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5896
+ .option('-v, --verbose', 'Show detailed information')
5897
+ .action(async (benchmarkId, options, command) => {
5898
+ try {
5899
+ const globalOptions = command.parent.opts();
5900
+ const config = loadConfig(globalOptions.config);
5901
+ validateConfiguration(config);
5902
+
5903
+ const api = new ToothFairyAPI(
5904
+ config.baseUrl,
5905
+ config.aiUrl,
5906
+ config.aiStreamUrl,
5907
+ config.apiKey,
5908
+ config.workspaceId,
5909
+ globalOptions.verbose || options.verbose
5910
+ );
5911
+
5912
+ const spinner = ora('Updating benchmark...').start();
5913
+ const result = await api._makeRequest('POST', `benchmark/update/${benchmarkId}`, {
5914
+ name: options.name,
5915
+ description: options.description,
5916
+ });
5917
+ spinner.stop();
5918
+
5919
+ if (options.output === 'json') {
5920
+ console.log(JSON.stringify(result, null, 2));
5921
+ } else {
5922
+ console.log(chalk.green.bold('✅ Benchmark updated successfully!'));
5923
+ }
5924
+ } catch (error) {
5925
+ console.error(chalk.red(`Error updating benchmark: ${error.message}`));
5926
+ process.exit(1);
5927
+ }
5928
+ });
5929
+
5930
+ program
5931
+ .command('delete-benchmark')
5932
+ .description('Delete a benchmark')
5933
+ .argument('<id>', 'Benchmark ID')
5934
+ .option('--confirm', 'Skip confirmation prompt')
5935
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5936
+ .option('-v, --verbose', 'Show detailed information')
5937
+ .action(async (benchmarkId, options, command) => {
5938
+ try {
5939
+ const globalOptions = command.parent.opts();
5940
+ const config = loadConfig(globalOptions.config);
5941
+ validateConfiguration(config);
5942
+
5943
+ if (!options.confirm) {
5944
+ const readline = require('readline');
5945
+ const rl = readline.createInterface({
5946
+ input: process.stdin,
5947
+ output: process.stdout,
5948
+ });
5949
+
5950
+ const answer = await new Promise((resolve) => {
5951
+ rl.question(
5952
+ chalk.yellow(`⚠️ Are you sure you want to delete benchmark ${benchmarkId}? (y/N): `),
5953
+ resolve
5954
+ );
5955
+ });
5956
+ rl.close();
5957
+
5958
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
5959
+ console.log(chalk.gray('Deletion cancelled.'));
5960
+ process.exit(0);
5961
+ }
5962
+ }
5963
+
5964
+ const api = new ToothFairyAPI(
5965
+ config.baseUrl,
5966
+ config.aiUrl,
5967
+ config.aiStreamUrl,
5968
+ config.apiKey,
5969
+ config.workspaceId,
5970
+ globalOptions.verbose || options.verbose
5971
+ );
5972
+
5973
+ const spinner = ora('Deleting benchmark...').start();
5974
+ const result = await api._makeRequest('DELETE', `benchmark/delete/${benchmarkId}`);
5975
+ spinner.stop();
5976
+
5977
+ if (options.output === 'json') {
5978
+ console.log(JSON.stringify(result, null, 2));
5979
+ } else {
5980
+ console.log(chalk.green.bold('✅ Benchmark deleted successfully!'));
5981
+ }
5982
+ } catch (error) {
5983
+ console.error(chalk.red(`Error deleting benchmark: ${error.message}`));
5984
+ process.exit(1);
5985
+ }
5986
+ });
5987
+
5988
+ // Scheduled Job commands
5989
+ program
5990
+ .command('create-scheduled-job')
5991
+ .description('Create a new scheduled job')
5992
+ .option('--name <name>', 'Job name')
5993
+ .option('--schedule <schedule>', 'Cron schedule expression')
5994
+ .option('--task <task>', 'Task to execute')
5995
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
5996
+ .option('-v, --verbose', 'Show detailed information')
5997
+ .action(async (options, command) => {
5998
+ try {
5999
+ const globalOptions = command.parent.opts();
6000
+ const config = loadConfig(globalOptions.config);
6001
+ validateConfiguration(config);
6002
+
6003
+ const api = new ToothFairyAPI(
6004
+ config.baseUrl,
6005
+ config.aiUrl,
6006
+ config.aiStreamUrl,
6007
+ config.apiKey,
6008
+ config.workspaceId,
6009
+ globalOptions.verbose || options.verbose
6010
+ );
6011
+
6012
+ const spinner = ora('Creating scheduled job...').start();
6013
+ const result = await api._makeRequest('POST', 'scheduled_job/create', {
6014
+ name: options.name,
6015
+ schedule: options.schedule,
6016
+ task: options.task,
6017
+ });
6018
+ spinner.stop();
6019
+
6020
+ if (options.output === 'json') {
6021
+ console.log(JSON.stringify(result, null, 2));
6022
+ } else {
6023
+ console.log(chalk.green.bold('✅ Scheduled job created successfully!'));
6024
+ console.log(chalk.dim(`ID: ${result.id || 'N/A'}`));
6025
+ }
6026
+ } catch (error) {
6027
+ console.error(chalk.red(`Error creating scheduled job: ${error.message}`));
6028
+ process.exit(1);
6029
+ }
6030
+ });
6031
+
6032
+ program
6033
+ .command('list-scheduled-jobs')
6034
+ .description('List all scheduled jobs')
6035
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6036
+ .option('-v, --verbose', 'Show detailed information')
6037
+ .action(async (options, command) => {
6038
+ try {
6039
+ const globalOptions = command.parent.opts();
6040
+ const config = loadConfig(globalOptions.config);
6041
+ validateConfiguration(config);
6042
+
6043
+ const api = new ToothFairyAPI(
6044
+ config.baseUrl,
6045
+ config.aiUrl,
6046
+ config.aiStreamUrl,
6047
+ config.apiKey,
6048
+ config.workspaceId,
6049
+ globalOptions.verbose || options.verbose
6050
+ );
6051
+
6052
+ const spinner = ora('Fetching scheduled jobs...').start();
6053
+ const result = await api._makeRequest('GET', 'scheduled_job/list');
6054
+ spinner.stop();
6055
+
6056
+ if (options.output === 'json') {
6057
+ console.log(JSON.stringify(result, null, 2));
6058
+ } else {
6059
+ const jobs = Array.isArray(result) ? result : result.items || [];
6060
+ console.log(chalk.green.bold(`Found ${jobs.length} scheduled job(s)`));
6061
+ jobs.forEach(j => {
6062
+ console.log(chalk.cyan(` • ${j.name || 'Unnamed'} (${j.id})`));
6063
+ });
6064
+ }
6065
+ } catch (error) {
6066
+ console.error(chalk.red(`Error listing scheduled jobs: ${error.message}`));
6067
+ process.exit(1);
6068
+ }
6069
+ });
6070
+
6071
+ program
6072
+ .command('get-scheduled-job')
6073
+ .description('Get details of a specific scheduled job')
6074
+ .argument('<id>', 'Scheduled Job ID')
6075
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6076
+ .option('-v, --verbose', 'Show detailed information')
6077
+ .action(async (jobId, options, command) => {
6078
+ try {
6079
+ const globalOptions = command.parent.opts();
6080
+ const config = loadConfig(globalOptions.config);
6081
+ validateConfiguration(config);
6082
+
6083
+ const api = new ToothFairyAPI(
6084
+ config.baseUrl,
6085
+ config.aiUrl,
6086
+ config.aiStreamUrl,
6087
+ config.apiKey,
6088
+ config.workspaceId,
6089
+ globalOptions.verbose || options.verbose
6090
+ );
6091
+
6092
+ const spinner = ora('Fetching scheduled job...').start();
6093
+ const result = await api._makeRequest('GET', `scheduled_job/get/${jobId}`);
6094
+ spinner.stop();
6095
+
6096
+ if (options.output === 'json') {
6097
+ console.log(JSON.stringify(result, null, 2));
6098
+ } else {
6099
+ console.log(chalk.green.bold('Scheduled Job Details'));
6100
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
6101
+ }
6102
+ } catch (error) {
6103
+ console.error(chalk.red(`Error getting scheduled job: ${error.message}`));
6104
+ process.exit(1);
6105
+ }
6106
+ });
6107
+
6108
+ program
6109
+ .command('update-scheduled-job')
6110
+ .description('Update an existing scheduled job')
6111
+ .argument('<id>', 'Scheduled Job ID')
6112
+ .option('--name <name>', 'Job name')
6113
+ .option('--schedule <schedule>', 'Cron schedule expression')
6114
+ .option('--task <task>', 'Task to execute')
6115
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6116
+ .option('-v, --verbose', 'Show detailed information')
6117
+ .action(async (jobId, options, command) => {
6118
+ try {
6119
+ const globalOptions = command.parent.opts();
6120
+ const config = loadConfig(globalOptions.config);
6121
+ validateConfiguration(config);
6122
+
6123
+ const api = new ToothFairyAPI(
6124
+ config.baseUrl,
6125
+ config.aiUrl,
6126
+ config.aiStreamUrl,
6127
+ config.apiKey,
6128
+ config.workspaceId,
6129
+ globalOptions.verbose || options.verbose
6130
+ );
6131
+
6132
+ const spinner = ora('Updating scheduled job...').start();
6133
+ const result = await api._makeRequest('POST', `scheduled_job/update/${jobId}`, {
6134
+ name: options.name,
6135
+ schedule: options.schedule,
6136
+ task: options.task,
6137
+ });
6138
+ spinner.stop();
6139
+
6140
+ if (options.output === 'json') {
6141
+ console.log(JSON.stringify(result, null, 2));
6142
+ } else {
6143
+ console.log(chalk.green.bold('✅ Scheduled job updated successfully!'));
6144
+ }
6145
+ } catch (error) {
6146
+ console.error(chalk.red(`Error updating scheduled job: ${error.message}`));
6147
+ process.exit(1);
6148
+ }
6149
+ });
6150
+
6151
+ program
6152
+ .command('delete-scheduled-job')
6153
+ .description('Delete a scheduled job')
6154
+ .argument('<id>', 'Scheduled Job ID')
6155
+ .option('--confirm', 'Skip confirmation prompt')
6156
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6157
+ .option('-v, --verbose', 'Show detailed information')
6158
+ .action(async (jobId, options, command) => {
6159
+ try {
6160
+ const globalOptions = command.parent.opts();
6161
+ const config = loadConfig(globalOptions.config);
6162
+ validateConfiguration(config);
6163
+
6164
+ if (!options.confirm) {
6165
+ const readline = require('readline');
6166
+ const rl = readline.createInterface({
6167
+ input: process.stdin,
6168
+ output: process.stdout,
6169
+ });
6170
+
6171
+ const answer = await new Promise((resolve) => {
6172
+ rl.question(
6173
+ chalk.yellow(`⚠️ Are you sure you want to delete scheduled job ${jobId}? (y/N): `),
6174
+ resolve
6175
+ );
6176
+ });
6177
+ rl.close();
6178
+
6179
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
6180
+ console.log(chalk.gray('Deletion cancelled.'));
6181
+ process.exit(0);
6182
+ }
6183
+ }
6184
+
6185
+ const api = new ToothFairyAPI(
6186
+ config.baseUrl,
6187
+ config.aiUrl,
6188
+ config.aiStreamUrl,
6189
+ config.apiKey,
6190
+ config.workspaceId,
6191
+ globalOptions.verbose || options.verbose
6192
+ );
6193
+
6194
+ const spinner = ora('Deleting scheduled job...').start();
6195
+ const result = await api._makeRequest('DELETE', `scheduled_job/delete/${jobId}`);
6196
+ spinner.stop();
6197
+
6198
+ if (options.output === 'json') {
6199
+ console.log(JSON.stringify(result, null, 2));
6200
+ } else {
6201
+ console.log(chalk.green.bold('✅ Scheduled job deleted successfully!'));
6202
+ }
6203
+ } catch (error) {
6204
+ console.error(chalk.red(`Error deleting scheduled job: ${error.message}`));
6205
+ process.exit(1);
6206
+ }
6207
+ });
6208
+
6209
+ // Authorisation commands
6210
+ program
6211
+ .command('create-authorisation')
6212
+ .description('Create a new authorisation')
6213
+ .option('--name <name>', 'Authorisation name')
6214
+ .option('--permissions <permissions>', 'Permissions (JSON)')
6215
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6216
+ .option('-v, --verbose', 'Show detailed information')
6217
+ .action(async (options, command) => {
6218
+ try {
6219
+ const globalOptions = command.parent.opts();
6220
+ const config = loadConfig(globalOptions.config);
6221
+ validateConfiguration(config);
6222
+
6223
+ const api = new ToothFairyAPI(
6224
+ config.baseUrl,
6225
+ config.aiUrl,
6226
+ config.aiStreamUrl,
6227
+ config.apiKey,
6228
+ config.workspaceId,
6229
+ globalOptions.verbose || options.verbose
6230
+ );
6231
+
6232
+ let permissions = {};
6233
+ if (options.permissions) {
6234
+ try {
6235
+ permissions = JSON.parse(options.permissions);
6236
+ } catch (e) {
6237
+ console.error(chalk.red('Invalid JSON in permissions'));
6238
+ process.exit(1);
6239
+ }
6240
+ }
6241
+
6242
+ const spinner = ora('Creating authorisation...').start();
6243
+ const result = await api._makeRequest('POST', 'authorisation/create', {
6244
+ name: options.name,
6245
+ permissions,
6246
+ });
6247
+ spinner.stop();
6248
+
6249
+ if (options.output === 'json') {
6250
+ console.log(JSON.stringify(result, null, 2));
6251
+ } else {
6252
+ console.log(chalk.green.bold('✅ Authorisation created successfully!'));
6253
+ console.log(chalk.dim(`ID: ${result.id || 'N/A'}`));
6254
+ }
6255
+ } catch (error) {
6256
+ console.error(chalk.red(`Error creating authorisation: ${error.message}`));
6257
+ process.exit(1);
6258
+ }
6259
+ });
6260
+
6261
+ program
6262
+ .command('list-authorisations')
6263
+ .description('List all authorisations')
6264
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6265
+ .option('-v, --verbose', 'Show detailed information')
6266
+ .action(async (options, command) => {
6267
+ try {
6268
+ const globalOptions = command.parent.opts();
6269
+ const config = loadConfig(globalOptions.config);
6270
+ validateConfiguration(config);
6271
+
6272
+ const api = new ToothFairyAPI(
6273
+ config.baseUrl,
6274
+ config.aiUrl,
6275
+ config.aiStreamUrl,
6276
+ config.apiKey,
6277
+ config.workspaceId,
6278
+ globalOptions.verbose || options.verbose
6279
+ );
6280
+
6281
+ const spinner = ora('Fetching authorisations...').start();
6282
+ const result = await api._makeRequest('GET', 'authorisation/list');
6283
+ spinner.stop();
6284
+
6285
+ if (options.output === 'json') {
6286
+ console.log(JSON.stringify(result, null, 2));
6287
+ } else {
6288
+ const auths = Array.isArray(result) ? result : result.items || [];
6289
+ console.log(chalk.green.bold(`Found ${auths.length} authorisation(s)`));
6290
+ auths.forEach(a => {
6291
+ console.log(chalk.cyan(` • ${a.name || 'Unnamed'} (${a.id})`));
6292
+ });
6293
+ }
6294
+ } catch (error) {
6295
+ console.error(chalk.red(`Error listing authorisations: ${error.message}`));
6296
+ process.exit(1);
6297
+ }
6298
+ });
6299
+
6300
+ program
6301
+ .command('get-authorisation')
6302
+ .description('Get details of a specific authorisation')
6303
+ .argument('<id>', 'Authorisation ID')
6304
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6305
+ .option('-v, --verbose', 'Show detailed information')
6306
+ .action(async (authId, options, command) => {
6307
+ try {
6308
+ const globalOptions = command.parent.opts();
6309
+ const config = loadConfig(globalOptions.config);
6310
+ validateConfiguration(config);
6311
+
6312
+ const api = new ToothFairyAPI(
6313
+ config.baseUrl,
6314
+ config.aiUrl,
6315
+ config.aiStreamUrl,
6316
+ config.apiKey,
6317
+ config.workspaceId,
6318
+ globalOptions.verbose || options.verbose
6319
+ );
6320
+
6321
+ const spinner = ora('Fetching authorisation...').start();
6322
+ const result = await api._makeRequest('GET', `authorisation/get/${authId}`);
6323
+ spinner.stop();
6324
+
6325
+ if (options.output === 'json') {
6326
+ console.log(JSON.stringify(result, null, 2));
6327
+ } else {
6328
+ console.log(chalk.green.bold('Authorisation Details'));
6329
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
6330
+ }
6331
+ } catch (error) {
6332
+ console.error(chalk.red(`Error getting authorisation: ${error.message}`));
6333
+ process.exit(1);
6334
+ }
6335
+ });
6336
+
6337
+ program
6338
+ .command('update-authorisation')
6339
+ .description('Update an existing authorisation')
6340
+ .argument('<id>', 'Authorisation ID')
6341
+ .option('--name <name>', 'Authorisation name')
6342
+ .option('--permissions <permissions>', 'Permissions (JSON)')
6343
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6344
+ .option('-v, --verbose', 'Show detailed information')
6345
+ .action(async (authId, options, command) => {
6346
+ try {
6347
+ const globalOptions = command.parent.opts();
6348
+ const config = loadConfig(globalOptions.config);
6349
+ validateConfiguration(config);
6350
+
6351
+ const api = new ToothFairyAPI(
6352
+ config.baseUrl,
6353
+ config.aiUrl,
6354
+ config.aiStreamUrl,
6355
+ config.apiKey,
6356
+ config.workspaceId,
6357
+ globalOptions.verbose || options.verbose
6358
+ );
6359
+
6360
+ let permissions = {};
6361
+ if (options.permissions) {
6362
+ try {
6363
+ permissions = JSON.parse(options.permissions);
6364
+ } catch (e) {
6365
+ console.error(chalk.red('Invalid JSON in permissions'));
6366
+ process.exit(1);
6367
+ }
6368
+ }
6369
+
6370
+ const spinner = ora('Updating authorisation...').start();
6371
+ const result = await api._makeRequest('POST', 'authorisation/update', {
6372
+ id: authId,
6373
+ name: options.name,
6374
+ permissions,
6375
+ });
6376
+ spinner.stop();
6377
+
6378
+ if (options.output === 'json') {
6379
+ console.log(JSON.stringify(result, null, 2));
6380
+ } else {
6381
+ console.log(chalk.green.bold('✅ Authorisation updated successfully!'));
6382
+ }
6383
+ } catch (error) {
6384
+ console.error(chalk.red(`Error updating authorisation: ${error.message}`));
6385
+ process.exit(1);
6386
+ }
6387
+ });
6388
+
6389
+ program
6390
+ .command('delete-authorisation')
6391
+ .description('Delete an authorisation')
6392
+ .argument('<id>', 'Authorisation ID')
6393
+ .option('--confirm', 'Skip confirmation prompt')
6394
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6395
+ .option('-v, --verbose', 'Show detailed information')
6396
+ .action(async (authId, options, command) => {
6397
+ try {
6398
+ const globalOptions = command.parent.opts();
6399
+ const config = loadConfig(globalOptions.config);
6400
+ validateConfiguration(config);
6401
+
6402
+ if (!options.confirm) {
6403
+ const readline = require('readline');
6404
+ const rl = readline.createInterface({
6405
+ input: process.stdin,
6406
+ output: process.stdout,
6407
+ });
6408
+
6409
+ const answer = await new Promise((resolve) => {
6410
+ rl.question(
6411
+ chalk.yellow(`⚠️ Are you sure you want to delete authorisation ${authId}? (y/N): `),
6412
+ resolve
6413
+ );
6414
+ });
6415
+ rl.close();
6416
+
6417
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
6418
+ console.log(chalk.gray('Deletion cancelled.'));
6419
+ process.exit(0);
6420
+ }
6421
+ }
6422
+
6423
+ const api = new ToothFairyAPI(
6424
+ config.baseUrl,
6425
+ config.aiUrl,
6426
+ config.aiStreamUrl,
6427
+ config.apiKey,
6428
+ config.workspaceId,
6429
+ globalOptions.verbose || options.verbose
6430
+ );
6431
+
6432
+ const spinner = ora('Deleting authorisation...').start();
6433
+ const result = await api._makeRequest('DELETE', `authorisation/delete/${authId}`);
6434
+ spinner.stop();
6435
+
6436
+ if (options.output === 'json') {
6437
+ console.log(JSON.stringify(result, null, 2));
6438
+ } else {
6439
+ console.log(chalk.green.bold('✅ Authorisation deleted successfully!'));
6440
+ }
6441
+ } catch (error) {
6442
+ console.error(chalk.red(`Error deleting authorisation: ${error.message}`));
6443
+ process.exit(1);
6444
+ }
6445
+ });
6446
+
6447
+ // Charting Settings commands
6448
+ program
6449
+ .command('get-charting-settings')
6450
+ .description('Get charting settings')
6451
+ .argument('<id>', 'Settings ID')
6452
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6453
+ .option('-v, --verbose', 'Show detailed information')
6454
+ .action(async (settingsId, options, command) => {
6455
+ try {
6456
+ const globalOptions = command.parent.opts();
6457
+ const config = loadConfig(globalOptions.config);
6458
+ validateConfiguration(config);
6459
+
6460
+ const api = new ToothFairyAPI(
6461
+ config.baseUrl,
6462
+ config.aiUrl,
6463
+ config.aiStreamUrl,
6464
+ config.apiKey,
6465
+ config.workspaceId,
6466
+ globalOptions.verbose || options.verbose
6467
+ );
6468
+
6469
+ const spinner = ora('Fetching charting settings...').start();
6470
+ const result = await api._makeRequest('GET', `charting_settings/get/${settingsId}`);
6471
+ spinner.stop();
6472
+
6473
+ if (options.output === 'json') {
6474
+ console.log(JSON.stringify(result, null, 2));
6475
+ } else {
6476
+ console.log(chalk.green.bold('Charting Settings'));
6477
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
6478
+ }
6479
+ } catch (error) {
6480
+ console.error(chalk.red(`Error getting charting settings: ${error.message}`));
6481
+ process.exit(1);
6482
+ }
6483
+ });
6484
+
6485
+ program
6486
+ .command('update-charting-settings')
6487
+ .description('Update charting settings')
6488
+ .argument('<id>', 'Settings ID')
6489
+ .option('--config <config>', 'Settings configuration (JSON)')
6490
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6491
+ .option('-v, --verbose', 'Show detailed information')
6492
+ .action(async (settingsId, options, command) => {
6493
+ try {
6494
+ const globalOptions = command.parent.opts();
6495
+ const config = loadConfig(globalOptions.config);
6496
+ validateConfiguration(config);
6497
+
6498
+ const api = new ToothFairyAPI(
6499
+ config.baseUrl,
6500
+ config.aiUrl,
6501
+ config.aiStreamUrl,
6502
+ config.apiKey,
6503
+ config.workspaceId,
6504
+ globalOptions.verbose || options.verbose
6505
+ );
6506
+
6507
+ let settingsConfig = {};
6508
+ if (options.config) {
6509
+ try {
6510
+ settingsConfig = JSON.parse(options.config);
6511
+ } catch (e) {
6512
+ console.error(chalk.red('Invalid JSON in config'));
6513
+ process.exit(1);
6514
+ }
6515
+ }
6516
+
6517
+ const spinner = ora('Updating charting settings...').start();
6518
+ const result = await api._makeRequest('POST', 'charting_settings/update', {
6519
+ id: settingsId,
6520
+ ...settingsConfig,
6521
+ });
6522
+ spinner.stop();
6523
+
6524
+ if (options.output === 'json') {
6525
+ console.log(JSON.stringify(result, null, 2));
6526
+ } else {
6527
+ console.log(chalk.green.bold('✅ Charting settings updated successfully!'));
6528
+ }
6529
+ } catch (error) {
6530
+ console.error(chalk.red(`Error updating charting settings: ${error.message}`));
6531
+ process.exit(1);
6532
+ }
6533
+ });
6534
+
6535
+ // Embeddings Settings commands
6536
+ program
6537
+ .command('get-embeddings-settings')
6538
+ .description('Get embeddings settings')
6539
+ .argument('<id>', 'Settings ID')
6540
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6541
+ .option('-v, --verbose', 'Show detailed information')
6542
+ .action(async (settingsId, options, command) => {
6543
+ try {
6544
+ const globalOptions = command.parent.opts();
6545
+ const config = loadConfig(globalOptions.config);
6546
+ validateConfiguration(config);
6547
+
6548
+ const api = new ToothFairyAPI(
6549
+ config.baseUrl,
6550
+ config.aiUrl,
6551
+ config.aiStreamUrl,
6552
+ config.apiKey,
6553
+ config.workspaceId,
6554
+ globalOptions.verbose || options.verbose
6555
+ );
6556
+
6557
+ const spinner = ora('Fetching embeddings settings...').start();
6558
+ const result = await api._makeRequest('GET', `embeddings_settings/get/${settingsId}`);
6559
+ spinner.stop();
6560
+
6561
+ if (options.output === 'json') {
6562
+ console.log(JSON.stringify(result, null, 2));
6563
+ } else {
6564
+ console.log(chalk.green.bold('Embeddings Settings'));
6565
+ console.log(chalk.dim(JSON.stringify(result, null, 2)));
6566
+ }
6567
+ } catch (error) {
6568
+ console.error(chalk.red(`Error getting embeddings settings: ${error.message}`));
6569
+ process.exit(1);
6570
+ }
6571
+ });
6572
+
6573
+ program
6574
+ .command('update-embeddings-settings')
6575
+ .description('Update embeddings settings')
6576
+ .argument('<id>', 'Settings ID')
6577
+ .option('--config <config>', 'Settings configuration (JSON)')
6578
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6579
+ .option('-v, --verbose', 'Show detailed information')
6580
+ .action(async (settingsId, options, command) => {
6581
+ try {
6582
+ const globalOptions = command.parent.opts();
6583
+ const config = loadConfig(globalOptions.config);
6584
+ validateConfiguration(config);
6585
+
6586
+ const api = new ToothFairyAPI(
6587
+ config.baseUrl,
6588
+ config.aiUrl,
6589
+ config.aiStreamUrl,
6590
+ config.apiKey,
6591
+ config.workspaceId,
6592
+ globalOptions.verbose || options.verbose
6593
+ );
6594
+
6595
+ let settingsConfig = {};
6596
+ if (options.config) {
6597
+ try {
6598
+ settingsConfig = JSON.parse(options.config);
6599
+ } catch (e) {
6600
+ console.error(chalk.red('Invalid JSON in config'));
6601
+ process.exit(1);
6602
+ }
6603
+ }
6604
+
6605
+ const spinner = ora('Updating embeddings settings...').start();
6606
+ const result = await api._makeRequest('POST', 'embeddings_settings/update', {
6607
+ id: settingsId,
6608
+ ...settingsConfig,
6609
+ });
6610
+ spinner.stop();
6611
+
6612
+ if (options.output === 'json') {
6613
+ console.log(JSON.stringify(result, null, 2));
6614
+ } else {
6615
+ console.log(chalk.green.bold('✅ Embeddings settings updated successfully!'));
6616
+ }
6617
+ } catch (error) {
6618
+ console.error(chalk.red(`Error updating embeddings settings: ${error.message}`));
6619
+ process.exit(1);
6620
+ }
6621
+ });
6622
+
6623
+ // Authorization Management Commands
6624
+ program
6625
+ .command('create-authorization')
6626
+ .description('Create a new authorization')
6627
+ .option('--name <name>', 'Authorization name')
6628
+ .option('--type <type>', 'Authorization type')
6629
+ .option('--config <config>', 'Authorization configuration (JSON)')
6630
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6631
+ .option('-v, --verbose', 'Show detailed information')
6632
+ .action(async (options, command) => {
6633
+ try {
6634
+ const globalOptions = command.parent.opts();
6635
+ const config = loadConfig(globalOptions.config);
6636
+ validateConfiguration(config);
6637
+
6638
+ const api = new ToothFairyAPI(
6639
+ config.baseUrl,
6640
+ config.aiUrl,
6641
+ config.aiStreamUrl,
6642
+ config.apiKey,
6643
+ config.workspaceId,
6644
+ globalOptions.verbose || options.verbose
6645
+ );
6646
+
6647
+ let authConfig = {};
6648
+ if (options.config) {
6649
+ try {
6650
+ authConfig = JSON.parse(options.config);
6651
+ } catch (e) {
6652
+ console.error(chalk.red('Invalid JSON in config'));
6653
+ process.exit(1);
6654
+ }
6655
+ }
6656
+
6657
+ const spinner = ora('Creating authorization...').start();
6658
+ const result = await api.createAuthorization({
6659
+ name: options.name,
6660
+ type: options.type,
6661
+ ...authConfig,
6662
+ });
6663
+ spinner.stop();
6664
+
6665
+ if (options.output === 'json') {
6666
+ console.log(JSON.stringify(result, null, 2));
6667
+ } else {
6668
+ console.log(chalk.green.bold('✅ Authorization created successfully!'));
6669
+ console.log(chalk.dim(`ID: ${result.id || 'N/A'}`));
6670
+ }
6671
+ } catch (error) {
6672
+ console.error(chalk.red(`Error creating authorization: ${error.message}`));
6673
+ process.exit(1);
6674
+ }
6675
+ });
6676
+
6677
+ program
6678
+ .command('list-authorizations')
6679
+ .description('List all authorizations')
6680
+ .option('--limit <number>', 'Maximum number to return', '50')
6681
+ .option('--offset <number>', 'Number to skip', '0')
6682
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6683
+ .option('-v, --verbose', 'Show detailed information')
6684
+ .action(async (options, command) => {
6685
+ try {
6686
+ const globalOptions = command.parent.opts();
6687
+ const config = loadConfig(globalOptions.config);
6688
+ validateConfiguration(config);
6689
+
6690
+ const api = new ToothFairyAPI(
6691
+ config.baseUrl,
6692
+ config.aiUrl,
6693
+ config.aiStreamUrl,
6694
+ config.apiKey,
6695
+ config.workspaceId,
6696
+ globalOptions.verbose || options.verbose
6697
+ );
6698
+
6699
+ const spinner = ora('Fetching authorizations...').start();
6700
+ const result = await api.listAuthorizations(
6701
+ parseInt(options.limit),
6702
+ parseInt(options.offset)
6703
+ );
6704
+ spinner.stop();
6705
+
6706
+ if (options.output === 'json') {
6707
+ console.log(JSON.stringify(result, null, 2));
6708
+ } else {
6709
+ const auths = Array.isArray(result) ? result : result.items || [];
6710
+ console.log(chalk.green.bold(`Found ${auths.length} authorization(s)`));
6711
+ auths.forEach(auth => {
6712
+ console.log(chalk.cyan(` • ${auth.name || 'Unnamed'} (${auth.id})`));
6713
+ });
6714
+ }
6715
+ } catch (error) {
6716
+ console.error(chalk.red(`Error listing authorizations: ${error.message}`));
6717
+ process.exit(1);
6718
+ }
6719
+ });
6720
+
6721
+ program
6722
+ .command('get-authorization')
6723
+ .description('Get details of a specific authorization')
6724
+ .argument('<id>', 'Authorization ID')
6725
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6726
+ .option('-v, --verbose', 'Show detailed information')
6727
+ .action(async (authId, options, command) => {
6728
+ try {
6729
+ const globalOptions = command.parent.opts();
6730
+ const config = loadConfig(globalOptions.config);
6731
+ validateConfiguration(config);
6732
+
6733
+ const api = new ToothFairyAPI(
6734
+ config.baseUrl,
6735
+ config.aiUrl,
6736
+ config.aiStreamUrl,
6737
+ config.apiKey,
6738
+ config.workspaceId,
6739
+ globalOptions.verbose || options.verbose
6740
+ );
6741
+
6742
+ const spinner = ora('Fetching authorization...').start();
6743
+ const result = await api.getAuthorization(authId);
6744
+ spinner.stop();
6745
+
6746
+ if (options.output === 'json') {
6747
+ console.log(JSON.stringify(result, null, 2));
6748
+ } else {
6749
+ console.log(chalk.green.bold('Authorization Details'));
6750
+ console.log(chalk.dim(`ID: ${result.id}`));
6751
+ console.log(chalk.dim(`Name: ${result.name || 'N/A'}`));
6752
+ console.log(chalk.dim(`Type: ${result.type || 'N/A'}`));
6753
+ }
6754
+ } catch (error) {
6755
+ console.error(chalk.red(`Error getting authorization: ${error.message}`));
6756
+ process.exit(1);
6757
+ }
6758
+ });
6759
+
6760
+ program
6761
+ .command('update-authorization')
6762
+ .description('Update an existing authorization')
6763
+ .option('--id <id>', 'Authorization ID')
6764
+ .option('--name <name>', 'Authorization name')
6765
+ .option('--type <type>', 'Authorization type')
6766
+ .option('--config <config>', 'Authorization configuration (JSON)')
6767
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6768
+ .option('-v, --verbose', 'Show detailed information')
6769
+ .action(async (options, command) => {
6770
+ try {
6771
+ const globalOptions = command.parent.opts();
6772
+ const config = loadConfig(globalOptions.config);
6773
+ validateConfiguration(config);
6774
+
6775
+ const api = new ToothFairyAPI(
6776
+ config.baseUrl,
6777
+ config.aiUrl,
6778
+ config.aiStreamUrl,
6779
+ config.apiKey,
6780
+ config.workspaceId,
6781
+ globalOptions.verbose || options.verbose
6782
+ );
6783
+
6784
+ let authConfig = {};
6785
+ if (options.config) {
6786
+ try {
6787
+ authConfig = JSON.parse(options.config);
6788
+ } catch (e) {
6789
+ console.error(chalk.red('Invalid JSON in config'));
6790
+ process.exit(1);
6791
+ }
6792
+ }
6793
+
6794
+ const spinner = ora('Updating authorization...').start();
6795
+ const result = await api.updateAuthorization({
6796
+ id: options.id,
6797
+ name: options.name,
6798
+ type: options.type,
6799
+ ...authConfig,
6800
+ });
6801
+ spinner.stop();
6802
+
6803
+ if (options.output === 'json') {
6804
+ console.log(JSON.stringify(result, null, 2));
6805
+ } else {
6806
+ console.log(chalk.green.bold('✅ Authorization updated successfully!'));
6807
+ }
6808
+ } catch (error) {
6809
+ console.error(chalk.red(`Error updating authorization: ${error.message}`));
6810
+ process.exit(1);
6811
+ }
6812
+ });
6813
+
6814
+ program
6815
+ .command('delete-authorization')
6816
+ .description('Delete an authorization')
6817
+ .argument('<id>', 'Authorization ID')
6818
+ .option('--confirm', 'Skip confirmation prompt')
6819
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6820
+ .option('-v, --verbose', 'Show detailed information')
6821
+ .action(async (authId, options, command) => {
6822
+ try {
6823
+ const globalOptions = command.parent.opts();
6824
+ const config = loadConfig(globalOptions.config);
6825
+ validateConfiguration(config);
6826
+
6827
+ const api = new ToothFairyAPI(
6828
+ config.baseUrl,
6829
+ config.aiUrl,
6830
+ config.aiStreamUrl,
6831
+ config.apiKey,
6832
+ config.workspaceId,
6833
+ globalOptions.verbose || options.verbose
6834
+ );
6835
+
6836
+ if (!options.confirm) {
6837
+ const readline = require('readline');
6838
+ const rl = readline.createInterface({
6839
+ input: process.stdin,
6840
+ output: process.stdout,
6841
+ });
6842
+
6843
+ const answer = await new Promise((resolve) => {
6844
+ rl.question(
6845
+ chalk.yellow(
6846
+ `⚠️ Are you sure you want to delete authorization ${authId}? (y/N): `
6847
+ ),
6848
+ resolve
6849
+ );
6850
+ });
6851
+
6852
+ rl.close();
6853
+
6854
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
6855
+ console.log(chalk.gray('Deletion cancelled.'));
6856
+ process.exit(0);
6857
+ }
6858
+ }
6859
+
6860
+ const spinner = ora('Deleting authorization...').start();
6861
+ const result = await api.deleteAuthorization(authId);
6862
+ spinner.stop();
6863
+
6864
+ if (options.output === 'json') {
6865
+ console.log(JSON.stringify(result, null, 2));
6866
+ } else {
6867
+ console.log(chalk.green.bold('✅ Authorization deleted successfully!'));
6868
+ }
6869
+ } catch (error) {
6870
+ console.error(chalk.red(`Error deleting authorization: ${error.message}`));
6871
+ process.exit(1);
6872
+ }
6873
+ });
6874
+
6875
+ // Benchmark Management Commands
6876
+ program
6877
+ .command('create-benchmark')
6878
+ .description('Create a new benchmark')
6879
+ .option('--name <name>', 'Benchmark name')
6880
+ .option('--description <description>', 'Benchmark description')
6881
+ .option('--questions <questions>', 'Questions JSON array')
6882
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6883
+ .option('-v, --verbose', 'Show detailed information')
6884
+ .action(async (options, command) => {
6885
+ try {
6886
+ const globalOptions = command.parent.opts();
6887
+ const config = loadConfig(globalOptions.config);
6888
+ validateConfiguration(config);
6889
+
6890
+ const api = new ToothFairyAPI(
6891
+ config.baseUrl,
6892
+ config.aiUrl,
6893
+ config.aiStreamUrl,
6894
+ config.apiKey,
6895
+ config.workspaceId,
6896
+ globalOptions.verbose || options.verbose
6897
+ );
6898
+
6899
+ let questions = [];
6900
+ if (options.questions) {
6901
+ try {
6902
+ questions = JSON.parse(options.questions);
6903
+ } catch (e) {
6904
+ console.error(chalk.red('Invalid JSON in questions'));
6905
+ process.exit(1);
6906
+ }
6907
+ }
6908
+
6909
+ const spinner = ora('Creating benchmark...').start();
6910
+ const result = await api.createBenchmark({
6911
+ name: options.name,
6912
+ description: options.description,
6913
+ questions: questions,
6914
+ });
6915
+ spinner.stop();
6916
+
6917
+ if (options.output === 'json') {
6918
+ console.log(JSON.stringify(result, null, 2));
6919
+ } else {
6920
+ console.log(chalk.green.bold('✅ Benchmark created successfully!'));
6921
+ console.log(chalk.dim(`ID: ${result.id || 'N/A'}`));
6922
+ }
6923
+ } catch (error) {
6924
+ console.error(chalk.red(`Error creating benchmark: ${error.message}`));
6925
+ process.exit(1);
6926
+ }
6927
+ });
6928
+
6929
+ program
6930
+ .command('list-benchmarks')
6931
+ .description('List all benchmarks')
6932
+ .option('--limit <number>', 'Maximum number to return', '50')
6933
+ .option('--offset <number>', 'Number to skip', '0')
6934
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6935
+ .option('-v, --verbose', 'Show detailed information')
6936
+ .action(async (options, command) => {
6937
+ try {
6938
+ const globalOptions = command.parent.opts();
6939
+ const config = loadConfig(globalOptions.config);
6940
+ validateConfiguration(config);
6941
+
6942
+ const api = new ToothFairyAPI(
6943
+ config.baseUrl,
6944
+ config.aiUrl,
6945
+ config.aiStreamUrl,
6946
+ config.apiKey,
6947
+ config.workspaceId,
6948
+ globalOptions.verbose || options.verbose
6949
+ );
6950
+
6951
+ const spinner = ora('Fetching benchmarks...').start();
6952
+ const result = await api.listBenchmarks(
6953
+ parseInt(options.limit),
6954
+ parseInt(options.offset)
6955
+ );
6956
+ spinner.stop();
6957
+
6958
+ if (options.output === 'json') {
6959
+ console.log(JSON.stringify(result, null, 2));
6960
+ } else {
6961
+ const benchmarks = Array.isArray(result) ? result : result.items || [];
6962
+ console.log(chalk.green.bold(`Found ${benchmarks.length} benchmark(s)`));
6963
+ benchmarks.forEach(bm => {
6964
+ console.log(chalk.cyan(` • ${bm.name || 'Unnamed'} (${bm.id})`));
6965
+ });
6966
+ }
6967
+ } catch (error) {
6968
+ console.error(chalk.red(`Error listing benchmarks: ${error.message}`));
6969
+ process.exit(1);
6970
+ }
6971
+ });
6972
+
6973
+ program
6974
+ .command('get-benchmark')
6975
+ .description('Get details of a specific benchmark')
6976
+ .argument('<id>', 'Benchmark ID')
6977
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
6978
+ .option('-v, --verbose', 'Show detailed information')
6979
+ .action(async (benchmarkId, options, command) => {
6980
+ try {
6981
+ const globalOptions = command.parent.opts();
6982
+ const config = loadConfig(globalOptions.config);
6983
+ validateConfiguration(config);
6984
+
6985
+ const api = new ToothFairyAPI(
6986
+ config.baseUrl,
6987
+ config.aiUrl,
6988
+ config.aiStreamUrl,
6989
+ config.apiKey,
6990
+ config.workspaceId,
6991
+ globalOptions.verbose || options.verbose
6992
+ );
6993
+
6994
+ const spinner = ora('Fetching benchmark...').start();
6995
+ const result = await api.getBenchmark(benchmarkId);
6996
+ spinner.stop();
6997
+
6998
+ if (options.output === 'json') {
6999
+ console.log(JSON.stringify(result, null, 2));
7000
+ } else {
7001
+ console.log(chalk.green.bold('Benchmark Details'));
7002
+ console.log(chalk.dim(`ID: ${result.id}`));
7003
+ console.log(chalk.dim(`Name: ${result.name || 'N/A'}`));
7004
+ console.log(chalk.dim(`Description: ${result.description || 'N/A'}`));
7005
+ }
7006
+ } catch (error) {
7007
+ console.error(chalk.red(`Error getting benchmark: ${error.message}`));
7008
+ process.exit(1);
7009
+ }
7010
+ });
7011
+
7012
+ program
7013
+ .command('update-benchmark')
7014
+ .description('Update an existing benchmark')
7015
+ .option('--id <id>', 'Benchmark ID')
7016
+ .option('--name <name>', 'Benchmark name')
7017
+ .option('--description <description>', 'Benchmark description')
7018
+ .option('--questions <questions>', 'Questions JSON array')
7019
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7020
+ .option('-v, --verbose', 'Show detailed information')
7021
+ .action(async (options, command) => {
7022
+ try {
7023
+ const globalOptions = command.parent.opts();
7024
+ const config = loadConfig(globalOptions.config);
7025
+ validateConfiguration(config);
7026
+
7027
+ const api = new ToothFairyAPI(
7028
+ config.baseUrl,
7029
+ config.aiUrl,
7030
+ config.aiStreamUrl,
7031
+ config.apiKey,
7032
+ config.workspaceId,
7033
+ globalOptions.verbose || options.verbose
7034
+ );
7035
+
7036
+ let questions = undefined;
7037
+ if (options.questions) {
7038
+ try {
7039
+ questions = JSON.parse(options.questions);
7040
+ } catch (e) {
7041
+ console.error(chalk.red('Invalid JSON in questions'));
7042
+ process.exit(1);
7043
+ }
7044
+ }
7045
+
7046
+ const spinner = ora('Updating benchmark...').start();
7047
+ const result = await api.updateBenchmark({
7048
+ id: options.id,
7049
+ name: options.name,
7050
+ description: options.description,
7051
+ questions: questions,
7052
+ });
7053
+ spinner.stop();
7054
+
7055
+ if (options.output === 'json') {
7056
+ console.log(JSON.stringify(result, null, 2));
7057
+ } else {
7058
+ console.log(chalk.green.bold('✅ Benchmark updated successfully!'));
7059
+ }
7060
+ } catch (error) {
7061
+ console.error(chalk.red(`Error updating benchmark: ${error.message}`));
7062
+ process.exit(1);
7063
+ }
7064
+ });
7065
+
7066
+ program
7067
+ .command('delete-benchmark')
7068
+ .description('Delete a benchmark')
7069
+ .argument('<id>', 'Benchmark ID')
7070
+ .option('--confirm', 'Skip confirmation prompt')
7071
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7072
+ .option('-v, --verbose', 'Show detailed information')
7073
+ .action(async (benchmarkId, options, command) => {
7074
+ try {
7075
+ const globalOptions = command.parent.opts();
7076
+ const config = loadConfig(globalOptions.config);
7077
+ validateConfiguration(config);
7078
+
7079
+ const api = new ToothFairyAPI(
7080
+ config.baseUrl,
7081
+ config.aiUrl,
7082
+ config.aiStreamUrl,
7083
+ config.apiKey,
7084
+ config.workspaceId,
7085
+ globalOptions.verbose || options.verbose
7086
+ );
7087
+
7088
+ if (!options.confirm) {
7089
+ const readline = require('readline');
7090
+ const rl = readline.createInterface({
7091
+ input: process.stdin,
7092
+ output: process.stdout,
7093
+ });
7094
+
7095
+ const answer = await new Promise((resolve) => {
7096
+ rl.question(
7097
+ chalk.yellow(
7098
+ `⚠️ Are you sure you want to delete benchmark ${benchmarkId}? (y/N): `
7099
+ ),
7100
+ resolve
7101
+ );
7102
+ });
7103
+
7104
+ rl.close();
7105
+
7106
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
7107
+ console.log(chalk.gray('Deletion cancelled.'));
7108
+ process.exit(0);
7109
+ }
7110
+ }
7111
+
7112
+ const spinner = ora('Deleting benchmark...').start();
7113
+ const result = await api.deleteBenchmark(benchmarkId);
7114
+ spinner.stop();
7115
+
7116
+ if (options.output === 'json') {
7117
+ console.log(JSON.stringify(result, null, 2));
7118
+ } else {
7119
+ console.log(chalk.green.bold('✅ Benchmark deleted successfully!'));
7120
+ }
7121
+ } catch (error) {
7122
+ console.error(chalk.red(`Error deleting benchmark: ${error.message}`));
7123
+ process.exit(1);
7124
+ }
7125
+ });
7126
+
7127
+ // Billing Commands
7128
+ program
7129
+ .command('billing-month-costs')
7130
+ .description('Get monthly usage and cost information')
7131
+ .argument('<month>', 'Month number (1-12)')
7132
+ .argument('<year>', 'Year (4-digit)')
7133
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7134
+ .option('-v, --verbose', 'Show detailed information')
7135
+ .action(async (month, year, options, command) => {
7136
+ try {
7137
+ const globalOptions = command.parent.opts();
7138
+ const config = loadConfig(globalOptions.config);
7139
+ validateConfiguration(config);
7140
+
7141
+ const api = new ToothFairyAPI(
7142
+ config.baseUrl,
7143
+ config.aiUrl,
7144
+ config.aiStreamUrl,
7145
+ config.apiKey,
7146
+ config.workspaceId,
7147
+ globalOptions.verbose || options.verbose
7148
+ );
7149
+
7150
+ const monthNum = parseInt(month);
7151
+ const yearNum = parseInt(year);
7152
+
7153
+ if (isNaN(monthNum) || monthNum < 1 || monthNum > 12) {
7154
+ console.error(chalk.red('Error: Month must be a number between 1 and 12'));
7155
+ process.exit(1);
7156
+ }
7157
+
7158
+ if (isNaN(yearNum) || yearNum < 2020 || yearNum > 2100) {
7159
+ console.error(chalk.red('Error: Year must be a valid 4-digit year'));
7160
+ process.exit(1);
7161
+ }
7162
+
7163
+ const spinner = ora('Fetching billing information...').start();
7164
+ const result = await api.getMonthCosts(monthNum, yearNum);
7165
+ spinner.stop();
7166
+
7167
+ if (options.output === 'json') {
7168
+ console.log(JSON.stringify(result, null, 2));
7169
+ } else {
7170
+ console.log(chalk.green.bold(`Billing Information for ${month}/${year}`));
7171
+ console.log();
7172
+
7173
+ if (result.apiUsage) {
7174
+ console.log(chalk.cyan('API Usage:'));
7175
+ if (result.apiUsage.totalUoI !== undefined) {
7176
+ console.log(chalk.dim(` Total Units of Interaction: ${result.apiUsage.totalUoI}`));
7177
+ }
7178
+ if (result.apiUsage.totalCostUSD !== undefined) {
7179
+ console.log(chalk.dim(` Total Cost: $${result.apiUsage.totalCostUSD}`));
7180
+ }
7181
+ }
7182
+
7183
+ if (result.trainingUsage) {
7184
+ console.log(chalk.cyan('Training Usage:'));
7185
+ console.log(chalk.dim(` See --verbose for details`));
7186
+ }
7187
+ }
7188
+ } catch (error) {
7189
+ console.error(chalk.red(`Error getting billing information: ${error.message}`));
7190
+ process.exit(1);
7191
+ }
7192
+ });
7193
+
7194
+ // Channel Management Commands
7195
+ program
7196
+ .command('create-channel')
7197
+ .description('Create a new communication channel')
7198
+ .option('--name <name>', 'Channel name')
7199
+ .option('--channel <channel>', 'Channel type (sms|whatsapp|email)')
7200
+ .option('--provider <provider>', 'Service provider')
7201
+ .option('--senderid <senderid>', 'Sender ID')
7202
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7203
+ .option('-v, --verbose', 'Show detailed information')
7204
+ .action(async (options, command) => {
7205
+ try {
7206
+ const globalOptions = command.parent.opts();
7207
+ const config = loadConfig(globalOptions.config);
7208
+ validateConfiguration(config);
7209
+
7210
+ const api = new ToothFairyAPI(
7211
+ config.baseUrl,
7212
+ config.aiUrl,
7213
+ config.aiStreamUrl,
7214
+ config.apiKey,
7215
+ config.workspaceId,
7216
+ globalOptions.verbose || options.verbose
7217
+ );
7218
+
7219
+ const spinner = ora('Creating channel...').start();
7220
+ const result = await api.createChannel({
7221
+ name: options.name,
7222
+ channel: options.channel,
7223
+ provider: options.provider,
7224
+ senderid: options.senderid,
7225
+ });
7226
+ spinner.stop();
7227
+
7228
+ if (options.output === 'json') {
7229
+ console.log(JSON.stringify(result, null, 2));
7230
+ } else {
7231
+ console.log(chalk.green.bold('✅ Channel created successfully!'));
7232
+ console.log(chalk.dim(`ID: ${result.id || 'N/A'}`));
7233
+ }
7234
+ } catch (error) {
7235
+ console.error(chalk.red(`Error creating channel: ${error.message}`));
7236
+ process.exit(1);
7237
+ }
7238
+ });
7239
+
7240
+ program
7241
+ .command('list-channels')
7242
+ .description('List all channels')
7243
+ .option('--limit <number>', 'Maximum number to return', '50')
7244
+ .option('--offset <number>', 'Number to skip', '0')
7245
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7246
+ .option('-v, --verbose', 'Show detailed information')
7247
+ .action(async (options, command) => {
7248
+ try {
7249
+ const globalOptions = command.parent.opts();
7250
+ const config = loadConfig(globalOptions.config);
7251
+ validateConfiguration(config);
7252
+
7253
+ const api = new ToothFairyAPI(
7254
+ config.baseUrl,
7255
+ config.aiUrl,
7256
+ config.aiStreamUrl,
7257
+ config.apiKey,
7258
+ config.workspaceId,
7259
+ globalOptions.verbose || options.verbose
7260
+ );
7261
+
7262
+ const spinner = ora('Fetching channels...').start();
7263
+ const result = await api.listChannels(
7264
+ parseInt(options.limit),
7265
+ parseInt(options.offset)
7266
+ );
7267
+ spinner.stop();
7268
+
7269
+ if (options.output === 'json') {
7270
+ console.log(JSON.stringify(result, null, 2));
7271
+ } else {
7272
+ const channels = Array.isArray(result) ? result : result.items || [];
7273
+ console.log(chalk.green.bold(`Found ${channels.length} channel(s)`));
7274
+ channels.forEach(ch => {
7275
+ console.log(chalk.cyan(` • ${ch.name || 'Unnamed'} (${ch.id})`));
7276
+ });
7277
+ }
7278
+ } catch (error) {
7279
+ console.error(chalk.red(`Error listing channels: ${error.message}`));
7280
+ process.exit(1);
7281
+ }
7282
+ });
7283
+
7284
+ program
7285
+ .command('get-channel')
7286
+ .description('Get details of a specific channel')
7287
+ .argument('<id>', 'Channel ID')
7288
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7289
+ .option('-v, --verbose', 'Show detailed information')
7290
+ .action(async (channelId, options, command) => {
7291
+ try {
7292
+ const globalOptions = command.parent.opts();
7293
+ const config = loadConfig(globalOptions.config);
7294
+ validateConfiguration(config);
7295
+
7296
+ const api = new ToothFairyAPI(
7297
+ config.baseUrl,
7298
+ config.aiUrl,
7299
+ config.aiStreamUrl,
7300
+ config.apiKey,
7301
+ config.workspaceId,
7302
+ globalOptions.verbose || options.verbose
7303
+ );
7304
+
7305
+ const spinner = ora('Fetching channel...').start();
7306
+ const result = await api.getChannel(channelId);
7307
+ spinner.stop();
7308
+
7309
+ if (options.output === 'json') {
7310
+ console.log(JSON.stringify(result, null, 2));
7311
+ } else {
7312
+ console.log(chalk.green.bold('Channel Details'));
7313
+ console.log(chalk.dim(`ID: ${result.id}`));
7314
+ console.log(chalk.dim(`Name: ${result.name || 'N/A'}`));
7315
+ console.log(chalk.dim(`Channel: ${result.channel || 'N/A'}`));
7316
+ console.log(chalk.dim(`Provider: ${result.provider || 'N/A'}`));
7317
+ }
7318
+ } catch (error) {
7319
+ console.error(chalk.red(`Error getting channel: ${error.message}`));
7320
+ process.exit(1);
7321
+ }
7322
+ });
7323
+
7324
+ program
7325
+ .command('update-channel')
7326
+ .description('Update an existing channel')
7327
+ .option('--id <id>', 'Channel ID')
7328
+ .option('--name <name>', 'Channel name')
7329
+ .option('--channel <channel>', 'Channel type')
7330
+ .option('--provider <provider>', 'Service provider')
7331
+ .option('--senderid <senderid>', 'Sender ID')
7332
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7333
+ .option('-v, --verbose', 'Show detailed information')
7334
+ .action(async (options, command) => {
7335
+ try {
7336
+ const globalOptions = command.parent.opts();
7337
+ const config = loadConfig(globalOptions.config);
7338
+ validateConfiguration(config);
7339
+
7340
+ const api = new ToothFairyAPI(
7341
+ config.baseUrl,
7342
+ config.aiUrl,
7343
+ config.aiStreamUrl,
7344
+ config.apiKey,
7345
+ config.workspaceId,
7346
+ globalOptions.verbose || options.verbose
7347
+ );
7348
+
7349
+ const spinner = ora('Updating channel...').start();
7350
+ const result = await api.updateChannel({
7351
+ id: options.id,
7352
+ name: options.name,
7353
+ channel: options.channel,
7354
+ provider: options.provider,
7355
+ senderid: options.senderid,
7356
+ });
7357
+ spinner.stop();
7358
+
7359
+ if (options.output === 'json') {
7360
+ console.log(JSON.stringify(result, null, 2));
7361
+ } else {
7362
+ console.log(chalk.green.bold('✅ Channel updated successfully!'));
7363
+ }
7364
+ } catch (error) {
7365
+ console.error(chalk.red(`Error updating channel: ${error.message}`));
7366
+ process.exit(1);
7367
+ }
7368
+ });
7369
+
7370
+ program
7371
+ .command('delete-channel')
7372
+ .description('Delete a channel')
7373
+ .argument('<id>', 'Channel ID')
7374
+ .option('--confirm', 'Skip confirmation prompt')
7375
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7376
+ .option('-v, --verbose', 'Show detailed information')
7377
+ .action(async (channelId, options, command) => {
7378
+ try {
7379
+ const globalOptions = command.parent.opts();
7380
+ const config = loadConfig(globalOptions.config);
7381
+ validateConfiguration(config);
7382
+
7383
+ const api = new ToothFairyAPI(
7384
+ config.baseUrl,
7385
+ config.aiUrl,
7386
+ config.aiStreamUrl,
7387
+ config.apiKey,
7388
+ config.workspaceId,
7389
+ globalOptions.verbose || options.verbose
7390
+ );
7391
+
7392
+ if (!options.confirm) {
7393
+ const readline = require('readline');
7394
+ const rl = readline.createInterface({
7395
+ input: process.stdin,
7396
+ output: process.stdout,
7397
+ });
7398
+
7399
+ const answer = await new Promise((resolve) => {
7400
+ rl.question(
7401
+ chalk.yellow(
7402
+ `⚠️ Are you sure you want to delete channel ${channelId}? (y/N): `
7403
+ ),
7404
+ resolve
7405
+ );
7406
+ });
7407
+
7408
+ rl.close();
7409
+
7410
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
7411
+ console.log(chalk.gray('Deletion cancelled.'));
7412
+ process.exit(0);
7413
+ }
7414
+ }
7415
+
7416
+ const spinner = ora('Deleting channel...').start();
7417
+ const result = await api.deleteChannel(channelId);
7418
+ spinner.stop();
7419
+
7420
+ if (options.output === 'json') {
7421
+ console.log(JSON.stringify(result, null, 2));
7422
+ } else {
7423
+ console.log(chalk.green.bold('✅ Channel deleted successfully!'));
7424
+ }
7425
+ } catch (error) {
7426
+ console.error(chalk.red(`Error deleting channel: ${error.message}`));
7427
+ process.exit(1);
7428
+ }
7429
+ });
7430
+
7431
+ // Connection Management Commands
7432
+ program
7433
+ .command('list-connections')
7434
+ .description('List all connections')
7435
+ .option('--limit <number>', 'Maximum number to return', '50')
7436
+ .option('--offset <number>', 'Number to skip', '0')
7437
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7438
+ .option('-v, --verbose', 'Show detailed information')
7439
+ .action(async (options, command) => {
7440
+ try {
7441
+ const globalOptions = command.parent.opts();
7442
+ const config = loadConfig(globalOptions.config);
7443
+ validateConfiguration(config);
7444
+
7445
+ const api = new ToothFairyAPI(
7446
+ config.baseUrl,
7447
+ config.aiUrl,
7448
+ config.aiStreamUrl,
7449
+ config.apiKey,
7450
+ config.workspaceId,
7451
+ globalOptions.verbose || options.verbose
7452
+ );
7453
+
7454
+ const spinner = ora('Fetching connections...').start();
7455
+ const result = await api.listConnections(
7456
+ parseInt(options.limit),
7457
+ parseInt(options.offset)
7458
+ );
7459
+ spinner.stop();
7460
+
7461
+ if (options.output === 'json') {
7462
+ console.log(JSON.stringify(result, null, 2));
7463
+ } else {
7464
+ const connections = Array.isArray(result) ? result : result.items || [];
7465
+ console.log(chalk.green.bold(`Found ${connections.length} connection(s)`));
7466
+ connections.forEach(conn => {
7467
+ console.log(chalk.cyan(` • ${conn.name || 'Unnamed'} (${conn.id})`));
7468
+ });
7469
+ }
7470
+ } catch (error) {
7471
+ console.error(chalk.red(`Error listing connections: ${error.message}`));
7472
+ process.exit(1);
7473
+ }
7474
+ });
7475
+
7476
+ program
7477
+ .command('get-connection')
7478
+ .description('Get details of a specific connection')
7479
+ .argument('<id>', 'Connection ID')
7480
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7481
+ .option('-v, --verbose', 'Show detailed information')
7482
+ .action(async (connectionId, options, command) => {
7483
+ try {
7484
+ const globalOptions = command.parent.opts();
7485
+ const config = loadConfig(globalOptions.config);
7486
+ validateConfiguration(config);
7487
+
7488
+ const api = new ToothFairyAPI(
7489
+ config.baseUrl,
7490
+ config.aiUrl,
7491
+ config.aiStreamUrl,
7492
+ config.apiKey,
7493
+ config.workspaceId,
7494
+ globalOptions.verbose || options.verbose
7495
+ );
7496
+
7497
+ const spinner = ora('Fetching connection...').start();
7498
+ const result = await api.getConnection(connectionId);
7499
+ spinner.stop();
7500
+
7501
+ if (options.output === 'json') {
7502
+ console.log(JSON.stringify(result, null, 2));
7503
+ } else {
7504
+ console.log(chalk.green.bold('Connection Details'));
7505
+ console.log(chalk.dim(`ID: ${result.id}`));
7506
+ console.log(chalk.dim(`Name: ${result.name || 'N/A'}`));
7507
+ console.log(chalk.dim(`Type: ${result.type || 'N/A'}`));
7508
+ console.log(chalk.dim(`Host: ${result.host || 'N/A'}`));
7509
+ }
7510
+ } catch (error) {
7511
+ console.error(chalk.red(`Error getting connection: ${error.message}`));
7512
+ process.exit(1);
7513
+ }
7514
+ });
7515
+
7516
+ program
7517
+ .command('delete-connection')
7518
+ .description('Delete a connection')
7519
+ .argument('<id>', 'Connection ID')
7520
+ .option('--confirm', 'Skip confirmation prompt')
7521
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7522
+ .option('-v, --verbose', 'Show detailed information')
7523
+ .action(async (connectionId, options, command) => {
7524
+ try {
7525
+ const globalOptions = command.parent.opts();
7526
+ const config = loadConfig(globalOptions.config);
7527
+ validateConfiguration(config);
7528
+
7529
+ const api = new ToothFairyAPI(
7530
+ config.baseUrl,
7531
+ config.aiUrl,
7532
+ config.aiStreamUrl,
7533
+ config.apiKey,
7534
+ config.workspaceId,
7535
+ globalOptions.verbose || options.verbose
7536
+ );
7537
+
7538
+ if (!options.confirm) {
7539
+ const readline = require('readline');
7540
+ const rl = readline.createInterface({
7541
+ input: process.stdin,
7542
+ output: process.stdout,
7543
+ });
7544
+
7545
+ const answer = await new Promise((resolve) => {
7546
+ rl.question(
7547
+ chalk.yellow(
7548
+ `⚠️ Are you sure you want to delete connection ${connectionId}? (y/N): `
7549
+ ),
7550
+ resolve
7551
+ );
7552
+ });
7553
+
7554
+ rl.close();
7555
+
7556
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
7557
+ console.log(chalk.gray('Deletion cancelled.'));
7558
+ process.exit(0);
7559
+ }
7560
+ }
7561
+
7562
+ const spinner = ora('Deleting connection...').start();
7563
+ const result = await api.deleteConnection(connectionId);
7564
+ spinner.stop();
7565
+
7566
+ if (options.output === 'json') {
7567
+ console.log(JSON.stringify(result, null, 2));
7568
+ } else {
7569
+ console.log(chalk.green.bold('✅ Connection deleted successfully!'));
7570
+ }
7571
+ } catch (error) {
7572
+ console.error(chalk.red(`Error deleting connection: ${error.message}`));
7573
+ process.exit(1);
7574
+ }
7575
+ });
7576
+
7577
+ // Dictionary Management Commands
7578
+ program
7579
+ .command('list-dictionaries')
7580
+ .description('List all dictionary entries')
7581
+ .option('--limit <number>', 'Maximum number to return', '50')
7582
+ .option('--offset <number>', 'Number to skip', '0')
7583
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7584
+ .option('-v, --verbose', 'Show detailed information')
7585
+ .action(async (options, command) => {
7586
+ try {
7587
+ const globalOptions = command.parent.opts();
7588
+ const config = loadConfig(globalOptions.config);
7589
+ validateConfiguration(config);
7590
+
7591
+ const api = new ToothFairyAPI(
7592
+ config.baseUrl,
7593
+ config.aiUrl,
7594
+ config.aiStreamUrl,
7595
+ config.apiKey,
7596
+ config.workspaceId,
7597
+ globalOptions.verbose || options.verbose
7598
+ );
7599
+
7600
+ const spinner = ora('Fetching dictionary entries...').start();
7601
+ const result = await api.listDictionaries(
7602
+ parseInt(options.limit),
7603
+ parseInt(options.offset)
7604
+ );
7605
+ spinner.stop();
7606
+
7607
+ if (options.output === 'json') {
7608
+ console.log(JSON.stringify(result, null, 2));
7609
+ } else {
7610
+ const dicts = Array.isArray(result) ? result : result.items || [];
7611
+ console.log(chalk.green.bold(`Found ${dicts.length} dictionary entr(y/ies)`));
7612
+ dicts.forEach(dict => {
7613
+ console.log(chalk.cyan(` • ${dict.sourceText || 'N/A'} → ${dict.targetText || 'N/A'} (${dict.id})`));
7614
+ });
7615
+ }
7616
+ } catch (error) {
7617
+ console.error(chalk.red(`Error listing dictionaries: ${error.message}`));
7618
+ process.exit(1);
7619
+ }
7620
+ });
7621
+
7622
+ program
7623
+ .command('get-dictionary')
7624
+ .description('Get details of a specific dictionary entry')
7625
+ .argument('<id>', 'Dictionary entry ID')
7626
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7627
+ .option('-v, --verbose', 'Show detailed information')
7628
+ .action(async (dictionaryId, options, command) => {
7629
+ try {
7630
+ const globalOptions = command.parent.opts();
7631
+ const config = loadConfig(globalOptions.config);
7632
+ validateConfiguration(config);
7633
+
7634
+ const api = new ToothFairyAPI(
7635
+ config.baseUrl,
7636
+ config.aiUrl,
7637
+ config.aiStreamUrl,
7638
+ config.apiKey,
7639
+ config.workspaceId,
7640
+ globalOptions.verbose || options.verbose
7641
+ );
7642
+
7643
+ const spinner = ora('Fetching dictionary entry...').start();
7644
+ const result = await api.getDictionary(dictionaryId);
7645
+ spinner.stop();
7646
+
7647
+ if (options.output === 'json') {
7648
+ console.log(JSON.stringify(result, null, 2));
7649
+ } else {
7650
+ console.log(chalk.green.bold('Dictionary Entry Details'));
7651
+ console.log(chalk.dim(`ID: ${result.id}`));
7652
+ console.log(chalk.dim(`Source: ${result.sourceText || 'N/A'}`));
7653
+ console.log(chalk.dim(`Target: ${result.targetText || 'N/A'}`));
7654
+ console.log(chalk.dim(`Source Language: ${result.sourceLanguage || 'N/A'}`));
7655
+ console.log(chalk.dim(`Target Language: ${result.targetLanguage || 'N/A'}`));
7656
+ }
7657
+ } catch (error) {
7658
+ console.error(chalk.red(`Error getting dictionary entry: ${error.message}`));
7659
+ process.exit(1);
7660
+ }
7661
+ });
7662
+
7663
+ // Embedding Management Commands
7664
+ program
7665
+ .command('get-embedding')
7666
+ .description('Get details of a specific embedding')
7667
+ .argument('<id>', 'Embedding ID')
7668
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7669
+ .option('-v, --verbose', 'Show detailed information')
7670
+ .action(async (embeddingId, options, command) => {
7671
+ try {
7672
+ const globalOptions = command.parent.opts();
7673
+ const config = loadConfig(globalOptions.config);
7674
+ validateConfiguration(config);
7675
+
7676
+ const api = new ToothFairyAPI(
7677
+ config.baseUrl,
7678
+ config.aiUrl,
7679
+ config.aiStreamUrl,
7680
+ config.apiKey,
7681
+ config.workspaceId,
7682
+ globalOptions.verbose || options.verbose
7683
+ );
7684
+
7685
+ const spinner = ora('Fetching embedding...').start();
7686
+ const result = await api.getEmbedding(embeddingId);
7687
+ spinner.stop();
7688
+
7689
+ if (options.output === 'json') {
7690
+ console.log(JSON.stringify(result, null, 2));
7691
+ } else {
7692
+ console.log(chalk.green.bold('Embedding Details'));
7693
+ console.log(chalk.dim(`ID: ${result.id}`));
7694
+ console.log(chalk.dim(`Chunk ID: ${result.chunk_id || 'N/A'}`));
7695
+ console.log(chalk.dim(`Title: ${result.title || 'N/A'}`));
7696
+ }
7697
+ } catch (error) {
7698
+ console.error(chalk.red(`Error getting embedding: ${error.message}`));
7699
+ process.exit(1);
7700
+ }
7701
+ });
7702
+
7703
+ // Settings Management Commands
7704
+ program
7705
+ .command('get-charting-settings')
7706
+ .description('Get charting settings for the workspace')
7707
+ .argument('<id>', 'Settings ID')
7708
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7709
+ .option('-v, --verbose', 'Show detailed information')
7710
+ .action(async (settingsId, options, command) => {
7711
+ try {
7712
+ const globalOptions = command.parent.opts();
7713
+ const config = loadConfig(globalOptions.config);
7714
+ validateConfiguration(config);
7715
+
7716
+ const api = new ToothFairyAPI(
7717
+ config.baseUrl,
7718
+ config.aiUrl,
7719
+ config.aiStreamUrl,
7720
+ config.apiKey,
7721
+ config.workspaceId,
7722
+ globalOptions.verbose || options.verbose
7723
+ );
7724
+
7725
+ const spinner = ora('Fetching charting settings...').start();
7726
+ const result = await api.getChartingSettings(settingsId);
7727
+ spinner.stop();
7728
+
7729
+ if (options.output === 'json') {
7730
+ console.log(JSON.stringify(result, null, 2));
7731
+ } else {
7732
+ console.log(chalk.green.bold('Charting Settings'));
7733
+ console.log(chalk.dim(`ID: ${result.id}`));
7734
+ console.log(chalk.dim(`Primary Color: ${result.primaryColor || 'N/A'}`));
7735
+ console.log(chalk.dim(`Secondary Color: ${result.secondaryColor || 'N/A'}`));
7736
+ }
7737
+ } catch (error) {
7738
+ console.error(chalk.red(`Error getting charting settings: ${error.message}`));
7739
+ process.exit(1);
7740
+ }
7741
+ });
7742
+
7743
+ program
7744
+ .command('update-charting-settings')
7745
+ .description('Update charting settings for the workspace')
7746
+ .argument('<id>', 'Settings ID')
7747
+ .option('--config <config>', 'Settings configuration (JSON)')
7748
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7749
+ .option('-v, --verbose', 'Show detailed information')
7750
+ .action(async (settingsId, options, command) => {
7751
+ try {
7752
+ const globalOptions = command.parent.opts();
7753
+ const config = loadConfig(globalOptions.config);
7754
+ validateConfiguration(config);
7755
+
7756
+ const api = new ToothFairyAPI(
7757
+ config.baseUrl,
7758
+ config.aiUrl,
7759
+ config.aiStreamUrl,
7760
+ config.apiKey,
7761
+ config.workspaceId,
7762
+ globalOptions.verbose || options.verbose
7763
+ );
7764
+
7765
+ let settingsConfig = {};
7766
+ if (options.config) {
7767
+ try {
7768
+ settingsConfig = JSON.parse(options.config);
7769
+ } catch (e) {
7770
+ console.error(chalk.red('Invalid JSON in config'));
7771
+ process.exit(1);
7772
+ }
7773
+ }
7774
+
7775
+ const spinner = ora('Updating charting settings...').start();
7776
+ const result = await api.updateChartingSettings({
7777
+ id: settingsId,
7778
+ ...settingsConfig,
7779
+ });
7780
+ spinner.stop();
7781
+
7782
+ if (options.output === 'json') {
7783
+ console.log(JSON.stringify(result, null, 2));
7784
+ } else {
7785
+ console.log(chalk.green.bold('✅ Charting settings updated successfully!'));
7786
+ }
7787
+ } catch (error) {
7788
+ console.error(chalk.red(`Error updating charting settings: ${error.message}`));
7789
+ process.exit(1);
7790
+ }
7791
+ });
7792
+
7793
+ program
7794
+ .command('get-embeddings-settings')
7795
+ .description('Get embeddings settings for the workspace')
7796
+ .argument('<id>', 'Settings ID')
7797
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7798
+ .option('-v, --verbose', 'Show detailed information')
7799
+ .action(async (settingsId, options, command) => {
7800
+ try {
7801
+ const globalOptions = command.parent.opts();
7802
+ const config = loadConfig(globalOptions.config);
7803
+ validateConfiguration(config);
7804
+
7805
+ const api = new ToothFairyAPI(
7806
+ config.baseUrl,
7807
+ config.aiUrl,
7808
+ config.aiStreamUrl,
7809
+ config.apiKey,
7810
+ config.workspaceId,
7811
+ globalOptions.verbose || options.verbose
7812
+ );
7813
+
7814
+ const spinner = ora('Fetching embeddings settings...').start();
7815
+ const result = await api.getEmbeddingsSettings(settingsId);
7816
+ spinner.stop();
7817
+
7818
+ if (options.output === 'json') {
7819
+ console.log(JSON.stringify(result, null, 2));
7820
+ } else {
7821
+ console.log(chalk.green.bold('Embeddings Settings'));
7822
+ console.log(chalk.dim(`ID: ${result.id}`));
7823
+ console.log(chalk.dim(`Max Chunk Words: ${result.maxChunkWords || 'N/A'}`));
7824
+ console.log(chalk.dim(`Chunking Strategy: ${result.chunkingStrategy || 'N/A'}`));
7825
+ }
7826
+ } catch (error) {
7827
+ console.error(chalk.red(`Error getting embeddings settings: ${error.message}`));
7828
+ process.exit(1);
7829
+ }
7830
+ });
7831
+
7832
+ program
7833
+ .command('update-embeddings-settings')
7834
+ .description('Update embeddings settings for the workspace')
7835
+ .argument('<id>', 'Settings ID')
7836
+ .option('--config <config>', 'Settings configuration (JSON)')
7837
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7838
+ .option('-v, --verbose', 'Show detailed information')
7839
+ .action(async (settingsId, options, command) => {
7840
+ try {
7841
+ const globalOptions = command.parent.opts();
7842
+ const config = loadConfig(globalOptions.config);
7843
+ validateConfiguration(config);
7844
+
7845
+ const api = new ToothFairyAPI(
7846
+ config.baseUrl,
7847
+ config.aiUrl,
7848
+ config.aiStreamUrl,
7849
+ config.apiKey,
7850
+ config.workspaceId,
7851
+ globalOptions.verbose || options.verbose
7852
+ );
7853
+
7854
+ let settingsConfig = {};
7855
+ if (options.config) {
7856
+ try {
7857
+ settingsConfig = JSON.parse(options.config);
7858
+ } catch (e) {
7859
+ console.error(chalk.red('Invalid JSON in config'));
7860
+ process.exit(1);
7861
+ }
7862
+ }
7863
+
7864
+ const spinner = ora('Updating embeddings settings...').start();
7865
+ const result = await api.updateEmbeddingsSettings({
7866
+ id: settingsId,
7867
+ ...settingsConfig,
7868
+ });
7869
+ spinner.stop();
7870
+
7871
+ if (options.output === 'json') {
7872
+ console.log(JSON.stringify(result, null, 2));
7873
+ } else {
7874
+ console.log(chalk.green.bold('✅ Embeddings settings updated successfully!'));
7875
+ }
7876
+ } catch (error) {
7877
+ console.error(chalk.red(`Error updating embeddings settings: ${error.message}`));
7878
+ process.exit(1);
7879
+ }
7880
+ });
7881
+
7882
+ // Stream Management Commands
7883
+ program
7884
+ .command('list-streams')
7885
+ .description('List all streams')
7886
+ .option('--limit <number>', 'Maximum number to return', '50')
7887
+ .option('--offset <number>', 'Number to skip', '0')
7888
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7889
+ .option('-v, --verbose', 'Show detailed information')
7890
+ .action(async (options, command) => {
7891
+ try {
7892
+ const globalOptions = command.parent.opts();
7893
+ const config = loadConfig(globalOptions.config);
7894
+ validateConfiguration(config);
7895
+
7896
+ const api = new ToothFairyAPI(
7897
+ config.baseUrl,
7898
+ config.aiUrl,
7899
+ config.aiStreamUrl,
7900
+ config.apiKey,
7901
+ config.workspaceId,
7902
+ globalOptions.verbose || options.verbose
7903
+ );
7904
+
7905
+ const spinner = ora('Fetching streams...').start();
7906
+ const result = await api.listStreams(
7907
+ parseInt(options.limit),
7908
+ parseInt(options.offset)
7909
+ );
7910
+ spinner.stop();
7911
+
7912
+ if (options.output === 'json') {
7913
+ console.log(JSON.stringify(result, null, 2));
7914
+ } else {
7915
+ const streams = Array.isArray(result) ? result : result.items || [];
7916
+ console.log(chalk.green.bold(`Found ${streams.length} stream(s)`));
7917
+ streams.forEach(stream => {
7918
+ console.log(chalk.cyan(` • ${stream.id}`));
7919
+ });
7920
+ }
7921
+ } catch (error) {
7922
+ console.error(chalk.red(`Error listing streams: ${error.message}`));
7923
+ process.exit(1);
7924
+ }
7925
+ });
7926
+
7927
+ program
7928
+ .command('get-stream')
7929
+ .description('Get details of a specific stream')
7930
+ .argument('<id>', 'Stream ID')
7931
+ .option('-o, --output <format>', 'Output format (json|text)', 'text')
7932
+ .option('-v, --verbose', 'Show detailed information')
7933
+ .action(async (streamId, options, command) => {
7934
+ try {
7935
+ const globalOptions = command.parent.opts();
7936
+ const config = loadConfig(globalOptions.config);
7937
+ validateConfiguration(config);
7938
+
7939
+ const api = new ToothFairyAPI(
7940
+ config.baseUrl,
7941
+ config.aiUrl,
7942
+ config.aiStreamUrl,
7943
+ config.apiKey,
7944
+ config.workspaceId,
7945
+ globalOptions.verbose || options.verbose
7946
+ );
7947
+
7948
+ const spinner = ora('Fetching stream...').start();
7949
+ const result = await api.getStream(streamId);
7950
+ spinner.stop();
7951
+
7952
+ if (options.output === 'json') {
7953
+ console.log(JSON.stringify(result, null, 2));
7954
+ } else {
7955
+ console.log(chalk.green.bold('Stream Details'));
7956
+ console.log(chalk.dim(`ID: ${result.id}`));
7957
+ console.log(chalk.dim(`Type: ${result.type || 'N/A'}`));
7958
+ console.log(chalk.dim(`Status: ${result.status || 'N/A'}`));
7959
+ }
7960
+ } catch (error) {
7961
+ console.error(chalk.red(`Error getting stream: ${error.message}`));
7962
+ process.exit(1);
7963
+ }
7964
+ });
7965
+
3390
7966
  // Parse command line arguments
3391
7967
  program.parse();