@toothfairyai/cli 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # ToothFairy AI CLI (Node.js)
2
+
3
+ A Node.js command-line interface for interacting with ToothFairy AI agents.
4
+
5
+ ## Installation
6
+
7
+ ### From NPM (when published)
8
+ ```bash
9
+ npm install -g toothfairy-cli
10
+ ```
11
+
12
+ ### From Source
13
+ ```bash
14
+ git clone <repository-url>
15
+ cd toothfairy-cli-js
16
+ npm install
17
+ npm link # Makes 'toothfairy' command available globally
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ 1. **Configure your credentials:**
23
+ ```bash
24
+ toothfairy configure \
25
+ --base-url "https://api.toothfairyai.com" \
26
+ --ai-url "https://ai.toothfairyai.com" \
27
+ --api-key "your-api-key" \
28
+ --workspace-id "your-workspace-id"
29
+ ```
30
+
31
+ 2. **Send a message to an agent:**
32
+ ```bash
33
+ toothfairy send "Hello, I need help with my appointment" \
34
+ --agent-id "agent-123" \
35
+ --phone-number "+1234567890" \
36
+ --customer-id "customer-456" \
37
+ --provider-id "sms-provider-789"
38
+ ```
39
+
40
+ 3. **List your chats:**
41
+ ```bash
42
+ toothfairy chats
43
+ ```
44
+
45
+ For full documentation, see the main [README.md](../README.md) in the parent directory.
46
+
47
+ ## Development
48
+
49
+ ```bash
50
+ npm install
51
+ npm run lint
52
+ npm test
53
+ ```
54
+
55
+ ## License
56
+
57
+ MIT
@@ -0,0 +1,666 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require("commander");
4
+ const chalk = require("chalk");
5
+ const ora = require("ora");
6
+ const { table } = require("table");
7
+ require("dotenv").config();
8
+
9
+ const ToothFairyAPI = require("../src/api");
10
+ const {
11
+ loadConfig,
12
+ saveConfig,
13
+ ToothFairyConfig,
14
+ getConfigPath,
15
+ validateConfiguration,
16
+ } = require("../src/config");
17
+
18
+ const program = new Command();
19
+
20
+ program
21
+ .name("toothfairy")
22
+ .description(
23
+ "ToothFairy AI CLI - Interact with ToothFairy AI agents via command line"
24
+ )
25
+ .version("1.0.0");
26
+
27
+ program
28
+ .option("-c, --config <path>", "path to configuration file")
29
+ .option("-v, --verbose", "enable verbose logging");
30
+
31
+ // Configure command
32
+ program
33
+ .command("configure")
34
+ .description("Configure ToothFairy CLI credentials and settings")
35
+ .option(
36
+ "--base-url <url>",
37
+ "ToothFairy API base URL",
38
+ "https://api.toothfairyai.com"
39
+ )
40
+ .option("--ai-url <url>", "ToothFairy AI URL", "https://ai.toothfairyai.com")
41
+ .requiredOption("--api-key <key>", "API key")
42
+ .requiredOption("--workspace-id <id>", "Workspace ID")
43
+ .option("--config-path <path>", "Path to save config file")
44
+ .action(async (options) => {
45
+ try {
46
+ const config = new ToothFairyConfig(
47
+ options.baseUrl,
48
+ options.aiUrl,
49
+ options.apiKey,
50
+ options.workspaceId
51
+ );
52
+
53
+ saveConfig(config, options.configPath);
54
+ console.log(chalk.green("Configuration saved successfully!"));
55
+ if (!options.configPath) {
56
+ console.log(`Config saved to: ${getConfigPath()}`);
57
+ }
58
+ } catch (error) {
59
+ console.error(chalk.red(`Error saving configuration: ${error.message}`));
60
+ process.exit(1);
61
+ }
62
+ });
63
+
64
+ // Send command
65
+ program
66
+ .command("send")
67
+ .description("Send a message to a ToothFairy AI agent")
68
+ .argument("<message>", "message to send")
69
+ .requiredOption("--agent-id <id>", "Agent ID to send message to")
70
+ .option("--phone-number <number>", "Phone number for SMS channel")
71
+ .option("--customer-id <id>", "Customer ID")
72
+ .option("--provider-id <id>", "SMS provider ID")
73
+ .option("--customer-info <json>", "Customer info as JSON string")
74
+ .option("-o, --output <format>", "Output format (json|text)", "text")
75
+ .option("-v, --verbose", "Show detailed response information")
76
+ .action(async (message, options, command) => {
77
+ try {
78
+ const globalOptions = command.parent.opts();
79
+ const config = loadConfig(globalOptions.config);
80
+ validateConfiguration(config);
81
+
82
+ const api = new ToothFairyAPI(
83
+ config.baseUrl,
84
+ config.aiUrl,
85
+ config.apiKey,
86
+ config.workspaceId,
87
+ globalOptions.verbose || options.verbose
88
+ );
89
+
90
+ // Parse customer info if provided
91
+ let customerInfo = {};
92
+ if (options.customerInfo) {
93
+ try {
94
+ customerInfo = JSON.parse(options.customerInfo);
95
+ } catch (error) {
96
+ console.error(chalk.red("Invalid JSON in customer-info"));
97
+ process.exit(1);
98
+ }
99
+ }
100
+
101
+ const spinner = ora("Sending message to agent...").start();
102
+
103
+ const response = await api.sendMessageToAgent(
104
+ message,
105
+ options.agentId,
106
+ options.phoneNumber,
107
+ options.customerId,
108
+ options.providerId,
109
+ customerInfo
110
+ );
111
+
112
+ spinner.stop();
113
+
114
+ if (options.output === "json") {
115
+ console.log(JSON.stringify(response, null, 2));
116
+ } else {
117
+ const agentResp = response.agentResponse;
118
+
119
+ if (options.verbose) {
120
+ // Verbose mode: show all details
121
+ console.log(chalk.green.bold("Message sent successfully!"));
122
+ console.log();
123
+
124
+ const data = [
125
+ ["Field", "Value"],
126
+ ["Chat ID", response.chatId],
127
+ ["Message ID", response.messageId],
128
+ ];
129
+
130
+ console.log(
131
+ table(data, {
132
+ header: {
133
+ alignment: "center",
134
+ content: "Response Details",
135
+ },
136
+ })
137
+ );
138
+
139
+ // Show full agent response
140
+ console.log(chalk.blue.bold("Agent Response (Full):"));
141
+ console.log("─".repeat(50));
142
+ console.log(JSON.stringify(agentResp, null, 2));
143
+ console.log("─".repeat(50));
144
+ } else {
145
+ // Default mode: show only the clean agent text
146
+ if (agentResp.contents && agentResp.contents.content) {
147
+ // Extract clean content from the response
148
+ const cleanContent = agentResp.contents.content.trim();
149
+ console.log(cleanContent);
150
+ } else if (agentResp.text) {
151
+ console.log(agentResp.text);
152
+ } else {
153
+ // Fallback if no recognizable text format
154
+ console.log(
155
+ chalk.yellow(
156
+ "No text response found. Use --verbose for full details."
157
+ )
158
+ );
159
+ }
160
+ }
161
+ }
162
+ } catch (error) {
163
+ console.error(chalk.red(`Error sending message: ${error.message}`));
164
+ process.exit(1);
165
+ }
166
+ });
167
+
168
+ // Search command
169
+ program
170
+ .command("search")
171
+ .description("Search for documents in the knowledge hub")
172
+ .argument("<query>", "search query text")
173
+ .option(
174
+ "-k, --top-k <number>",
175
+ "Number of documents to retrieve (1-50)",
176
+ "10"
177
+ )
178
+ .option(
179
+ "--status <status>",
180
+ "Filter by document status (published|suspended)"
181
+ )
182
+ .option("--document-id <id>", "Search within specific document ID")
183
+ .option("--topics <topics>", "Comma-separated topic IDs to filter by")
184
+ .option("-o, --output <format>", "Output format (json|text)", "text")
185
+ .option("-v, --verbose", "Show detailed search information")
186
+ .action(async (query, options, command) => {
187
+ try {
188
+ const globalOptions = command.parent.opts();
189
+ const config = loadConfig(globalOptions.config);
190
+ validateConfiguration(config);
191
+
192
+ const api = new ToothFairyAPI(
193
+ config.baseUrl,
194
+ config.aiUrl,
195
+ config.apiKey,
196
+ config.workspaceId,
197
+ globalOptions.verbose || options.verbose
198
+ );
199
+
200
+ // Validate top-k parameter
201
+ const topK = parseInt(options.topK);
202
+ if (isNaN(topK) || topK < 1 || topK > 50) {
203
+ console.error(
204
+ chalk.red("Error: --top-k must be a number between 1 and 50")
205
+ );
206
+ process.exit(1);
207
+ }
208
+
209
+ // Build metadata filters
210
+ const metadata = {};
211
+ if (options.status) {
212
+ if (!["published", "suspended"].includes(options.status)) {
213
+ console.error(
214
+ chalk.red(
215
+ 'Error: --status must be either "published" or "suspended"'
216
+ )
217
+ );
218
+ process.exit(1);
219
+ }
220
+ metadata.status = options.status;
221
+ }
222
+ if (options.documentId) {
223
+ metadata.documentId = options.documentId;
224
+ }
225
+ if (options.topics) {
226
+ const topicList = options.topics
227
+ .split(",")
228
+ .map((t) => t.trim())
229
+ .filter((t) => t);
230
+ if (topicList.length > 0) {
231
+ metadata.topic = topicList;
232
+ }
233
+ }
234
+
235
+ const spinner = ora("Searching knowledge hub...").start();
236
+
237
+ const results = await api.searchDocuments(
238
+ query,
239
+ topK,
240
+ Object.keys(metadata).length > 0 ? metadata : null
241
+ );
242
+
243
+ spinner.stop();
244
+
245
+ if (options.output === "json") {
246
+ console.log(JSON.stringify(results, null, 2));
247
+ } else {
248
+ // Handle different response formats - results might be array or dict
249
+ const documents = Array.isArray(results)
250
+ ? results
251
+ : results.results || [];
252
+
253
+ if (!documents || documents.length === 0) {
254
+ console.log(chalk.yellow("No documents found for your query"));
255
+ return;
256
+ }
257
+
258
+ console.log(chalk.green.bold(`Found ${documents.length} document(s)`));
259
+ console.log("=".repeat(50));
260
+
261
+ documents.forEach((doc, index) => {
262
+ const score = doc.cosinesim || 0;
263
+ const docId = doc.doc_id || doc.chunk_id || "N/A";
264
+
265
+ // Extract text content directly from document
266
+ const textContent = doc.raw_text || "No content available";
267
+ const docStatus = doc.status || "unknown";
268
+ const docTopics = doc.topics || [];
269
+ const docTitle = doc.title || "Untitled";
270
+
271
+ console.log(
272
+ chalk.cyan.bold(`\n${docTitle}`),
273
+ chalk.gray(`(Score: ${score.toFixed(3)})`)
274
+ );
275
+
276
+ if (options.verbose) {
277
+ // Verbose mode: show all details
278
+ const data = [
279
+ ["Field", "Value"],
280
+ ["Document ID", docId],
281
+ ["Title", docTitle],
282
+ ["Relevance Score", score.toFixed(4)],
283
+ ["Status", docStatus],
284
+ ["Topics", docTopics.length > 0 ? docTopics.join(", ") : "None"],
285
+ [
286
+ "Content Preview",
287
+ textContent.length > 200
288
+ ? textContent.substring(0, 200) + "..."
289
+ : textContent,
290
+ ],
291
+ ];
292
+
293
+ console.log(table(data));
294
+ } else {
295
+ // Default mode: show clean content
296
+ console.log("─".repeat(50));
297
+ const displayContent =
298
+ textContent.length > 500
299
+ ? textContent.substring(0, 500) + "..."
300
+ : textContent;
301
+ console.log(displayContent);
302
+ console.log("─".repeat(50));
303
+ }
304
+ });
305
+ }
306
+ } catch (error) {
307
+ console.error(chalk.red(`Error searching documents: ${error.message}`));
308
+ process.exit(1);
309
+ }
310
+ });
311
+
312
+ // Chats command
313
+ program
314
+ .command("chats")
315
+ .description("List all chats in the workspace")
316
+ .option("-o, --output <format>", "Output format (json|text)", "text")
317
+ .action(async (options, command) => {
318
+ try {
319
+ const globalOptions = command.parent.opts();
320
+ const config = loadConfig(globalOptions.config);
321
+ validateConfiguration(config);
322
+
323
+ const api = new ToothFairyAPI(
324
+ config.baseUrl,
325
+ config.aiUrl,
326
+ config.apiKey,
327
+ config.workspaceId,
328
+ globalOptions.verbose || options.verbose
329
+ );
330
+
331
+ const spinner = ora("Fetching chats...").start();
332
+ const chatsData = await api.getAllChats();
333
+ spinner.stop();
334
+
335
+ if (options.output === "json") {
336
+ console.log(JSON.stringify(chatsData, null, 2));
337
+ } else {
338
+ if (
339
+ !chatsData ||
340
+ (Array.isArray(chatsData) && chatsData.length === 0)
341
+ ) {
342
+ console.log(chalk.yellow("No chats found"));
343
+ return;
344
+ }
345
+
346
+ const chatList = Array.isArray(chatsData)
347
+ ? chatsData
348
+ : chatsData.items || [];
349
+
350
+ const data = [["Chat ID", "Name", "Customer ID", "Created"]];
351
+
352
+ chatList.forEach((chat) => {
353
+ data.push([
354
+ chat.id || "N/A",
355
+ chat.name || "N/A",
356
+ chat.customerId || "N/A",
357
+ chat.createdAt || "N/A",
358
+ ]);
359
+ });
360
+
361
+ console.log(
362
+ table(data, {
363
+ header: {
364
+ alignment: "center",
365
+ content: "Workspace Chats",
366
+ },
367
+ })
368
+ );
369
+ }
370
+ } catch (error) {
371
+ console.error(chalk.red(`Error fetching chats: ${error.message}`));
372
+ process.exit(1);
373
+ }
374
+ });
375
+
376
+ // Chat command (get specific chat)
377
+ program
378
+ .command("chat")
379
+ .description("Get details of a specific chat")
380
+ .argument("<chat-id>", "Chat ID to retrieve")
381
+ .option("-o, --output <format>", "Output format (json|text)", "text")
382
+ .action(async (chatId, options, command) => {
383
+ try {
384
+ const globalOptions = command.parent.opts();
385
+ const config = loadConfig(globalOptions.config);
386
+ validateConfiguration(config);
387
+
388
+ const api = new ToothFairyAPI(
389
+ config.baseUrl,
390
+ config.aiUrl,
391
+ config.apiKey,
392
+ config.workspaceId,
393
+ globalOptions.verbose || options.verbose
394
+ );
395
+
396
+ const spinner = ora(`Fetching chat ${chatId}...`).start();
397
+ const chatData = await api.getChat(chatId);
398
+ spinner.stop();
399
+
400
+ if (options.output === "json") {
401
+ console.log(JSON.stringify(chatData, null, 2));
402
+ } else {
403
+ console.log(chalk.green.bold("Chat Details"));
404
+ console.log();
405
+
406
+ const data = [["Field", "Value"]];
407
+
408
+ Object.entries(chatData).forEach(([key, value]) => {
409
+ let displayValue = value;
410
+ if (typeof value === "object" && value !== null) {
411
+ displayValue = JSON.stringify(value, null, 2);
412
+ }
413
+ data.push([key, String(displayValue)]);
414
+ });
415
+
416
+ console.log(table(data));
417
+ }
418
+ } catch (error) {
419
+ console.error(chalk.red(`Error fetching chat: ${error.message}`));
420
+ process.exit(1);
421
+ }
422
+ });
423
+
424
+ // Config show command
425
+ program
426
+ .command("config-show")
427
+ .description("Show current configuration")
428
+ .action(async (options, command) => {
429
+ try {
430
+ const globalOptions = command.parent.opts();
431
+ const config = loadConfig(globalOptions.config);
432
+
433
+ const data = [
434
+ ["Setting", "Value"],
435
+ ["Base URL", config.baseUrl],
436
+ ["AI URL", config.aiUrl],
437
+ [
438
+ "API Key",
439
+ config.apiKey
440
+ ? `${"*".repeat(20)}...${config.apiKey.slice(-4)}`
441
+ : "Not set",
442
+ ],
443
+ ["Workspace ID", config.workspaceId],
444
+ ];
445
+
446
+ console.log(
447
+ table(data, {
448
+ header: {
449
+ alignment: "center",
450
+ content: "Current Configuration",
451
+ },
452
+ })
453
+ );
454
+ } catch (error) {
455
+ console.error(chalk.red(`Configuration error: ${error.message}`));
456
+ process.exit(1);
457
+ }
458
+ });
459
+
460
+ // Help guide command
461
+ program
462
+ .command("help-guide")
463
+ .description("Show detailed help and usage examples")
464
+ .action(() => {
465
+ console.log(chalk.cyan.bold("🚀 ToothFairy AI CLI - Quick Start Guide"));
466
+ console.log("=".repeat(50));
467
+
468
+ console.log(chalk.green.bold("\n🚀 Getting Started"));
469
+ console.log("1. First, configure your credentials:");
470
+ console.log(
471
+ chalk.dim(
472
+ " tf configure --api-key YOUR_KEY --workspace-id YOUR_WORKSPACE"
473
+ )
474
+ );
475
+
476
+ console.log("\n2. Send a message to an agent:");
477
+ console.log(
478
+ chalk.dim(' tf send "Hello, I need help" --agent-id YOUR_AGENT_ID')
479
+ );
480
+
481
+ console.log("\n3. Search the knowledge hub:");
482
+ console.log(chalk.dim(' tf search "AI configuration help"'));
483
+
484
+ console.log("\n4. Explore your workspace:");
485
+ console.log(
486
+ chalk.dim(" tf chats # List all conversations")
487
+ );
488
+ console.log(
489
+ chalk.dim(" tf config-show # View current settings")
490
+ );
491
+
492
+ console.log(chalk.blue.bold("\n💬 Agent Communication Examples"));
493
+
494
+ const agentExamples = [
495
+ [
496
+ "Simple message",
497
+ 'tf send "What are your hours?" --agent-id "info-agent"',
498
+ ],
499
+ [
500
+ "With customer info",
501
+ 'tf send "Schedule appointment" --agent-id "scheduler" --customer-info \'{"name": "John"}\'',
502
+ ],
503
+ ["Verbose output", 'tf send "Hello" --agent-id "agent-123" --verbose'],
504
+ [
505
+ "JSON for scripting",
506
+ 'tf send "Help" --agent-id "agent-123" --output json',
507
+ ],
508
+ ];
509
+
510
+ const agentData = [["Use Case", "Command"], ...agentExamples];
511
+ console.log(
512
+ table(agentData, {
513
+ header: {
514
+ alignment: "center",
515
+ content: "Agent Communication",
516
+ },
517
+ })
518
+ );
519
+
520
+ console.log(chalk.magenta.bold("\n🔍 Knowledge Hub Search Examples"));
521
+
522
+ const searchExamples = [
523
+ ["Basic search", 'tf search "AI agent configuration"'],
524
+ ["Filter by status", 'tf search "machine learning" --status published'],
525
+ ["Limit results", 'tf search "troubleshooting" --top-k 3'],
526
+ [
527
+ "Topic filtering",
528
+ 'tf search "automation" --topics "topic_123,topic_456"',
529
+ ],
530
+ ["Specific document", 'tf search "settings" --document-id "doc_550..."'],
531
+ ["Verbose details", 'tf search "deployment" --verbose'],
532
+ ["JSON output", 'tf search "API docs" --output json'],
533
+ ];
534
+
535
+ const searchData = [["Use Case", "Command"], ...searchExamples];
536
+ console.log(
537
+ table(searchData, {
538
+ header: {
539
+ alignment: "center",
540
+ content: "Knowledge Hub Search",
541
+ },
542
+ })
543
+ );
544
+
545
+ console.log(chalk.green.bold("\n📋 Workspace Management Examples"));
546
+
547
+ const mgmtExamples = [
548
+ ["List all chats", "tf chats"],
549
+ ["View chat details", "tf chat CHAT_ID"],
550
+ ["Show config", "tf config-show"],
551
+ ["Detailed help", "tf help-guide"],
552
+ ];
553
+
554
+ const mgmtData = [["Use Case", "Command"], ...mgmtExamples];
555
+ console.log(
556
+ table(mgmtData, {
557
+ header: {
558
+ alignment: "center",
559
+ content: "Workspace Management",
560
+ },
561
+ })
562
+ );
563
+
564
+ console.log(chalk.yellow.bold("\n🔧 Configuration Options"));
565
+ const configOptions = [
566
+ ["Method", "Description", "Example"],
567
+ [
568
+ "Environment",
569
+ "Set environment variables",
570
+ "export TF_API_KEY=your_key",
571
+ ],
572
+ [
573
+ "Config file",
574
+ "Use ~/.toothfairy/config.yml",
575
+ "api_key: your_key\\nworkspace_id: your_workspace",
576
+ ],
577
+ [
578
+ "CLI arguments",
579
+ "Pass config file path",
580
+ "tf --config /path/to/config.yml send ...",
581
+ ],
582
+ ];
583
+
584
+ console.log(table(configOptions));
585
+
586
+ console.log(chalk.red.bold("\n⚠️ Common Issues & Solutions"));
587
+ const issues = [
588
+ ["Issue", "Solution"],
589
+ [
590
+ "Configuration incomplete",
591
+ "Run: tf configure --api-key YOUR_KEY --workspace-id YOUR_WORKSPACE",
592
+ ],
593
+ [
594
+ "No text response found",
595
+ "Use --verbose flag to see full response details",
596
+ ],
597
+ ["Agent not responding", "Check agent-id is correct and agent is active"],
598
+ [
599
+ "Network errors",
600
+ "Verify API endpoints are accessible and credentials are valid",
601
+ ],
602
+ ];
603
+
604
+ console.log(table(issues));
605
+
606
+ console.log(chalk.cyan.bold("\n🔍 Search Filtering Guide"));
607
+ console.log("Knowledge Hub search supports powerful filtering options:");
608
+ console.log(
609
+ "• " +
610
+ chalk.cyan("--status") +
611
+ ": Filter documents by 'published' or 'suspended' status"
612
+ );
613
+ console.log(
614
+ "• " +
615
+ chalk.cyan("--topics") +
616
+ ": Use topic IDs from ToothFairyAI (comma-separated)"
617
+ );
618
+ console.log(
619
+ "• " + chalk.cyan("--document-id") + ": Search within a specific document"
620
+ );
621
+ console.log(
622
+ "• " + chalk.cyan("--top-k") + ": Control number of results (1-50)"
623
+ );
624
+ console.log(
625
+ "• " + chalk.cyan("--verbose") + ": Show relevance scores and metadata"
626
+ );
627
+
628
+ console.log(chalk.magenta.bold("\n📖 More Help"));
629
+ console.log(
630
+ "• Use " + chalk.cyan("tf COMMAND --help") + " for command-specific help"
631
+ );
632
+ console.log(
633
+ "• Use " +
634
+ chalk.cyan("--verbose") +
635
+ " flag to see detailed request/response information"
636
+ );
637
+ console.log(
638
+ "• Use " + chalk.cyan("--output json") + " for machine-readable output"
639
+ );
640
+ console.log(
641
+ "• Configuration is loaded from: environment variables → ~/.toothfairy/config.yml → CLI args"
642
+ );
643
+
644
+ console.log(chalk.green.bold("\n✨ Pro Tips"));
645
+ const tips = [
646
+ "💾 Save time: Configure once with 'tf configure', then just use 'tf send' and 'tf search'",
647
+ "🔍 Debug issues: Use '--verbose' to see full API responses and troubleshoot",
648
+ "📝 Scripting: Use '--output json' and tools like 'jq' to parse responses",
649
+ "⚡ Quick tests: Only --agent-id is required for send, only query for search",
650
+ "🎯 Better search: Use --status, --topics, and --document-id for targeted results",
651
+ "🔧 Multiple environments: Use different config files with '--config' flag",
652
+ ];
653
+
654
+ tips.forEach((tip) => {
655
+ console.log(` ${tip}`);
656
+ });
657
+
658
+ console.log(
659
+ chalk.dim(
660
+ "\nToothFairy CLI v1.0.0 - For more help, visit the documentation"
661
+ )
662
+ );
663
+ });
664
+
665
+ // Parse command line arguments
666
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@toothfairyai/cli",
3
+ "version": "1.0.2",
4
+ "description": "Command-line interface for ToothFairy AI API",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "toothfairy": "./bin/toothfairy.js",
8
+ "tf": "./bin/toothfairy.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/toothfairy.js",
12
+ "test": "jest --passWithNoTests",
13
+ "test:watch": "jest --watch --passWithNoTests",
14
+ "lint": "eslint src/**/*.js bin/**/*.js || true",
15
+ "lint:fix": "eslint src/**/*.js bin/**/*.js --fix || true",
16
+ "prepack": "npm run test"
17
+ },
18
+ "keywords": [
19
+ "toothfairy",
20
+ "ai",
21
+ "cli",
22
+ "api",
23
+ "chatbot",
24
+ "agent"
25
+ ],
26
+ "author": "ToothFairy AI <support@toothfairyai.com>",
27
+ "license": "MIT",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://gitea.your-domain.com/toothfairyai/toothfairy-cli.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://gitea.your-domain.com/toothfairyai/toothfairy-cli/issues"
37
+ },
38
+ "homepage": "https://gitea.your-domain.com/toothfairyai/toothfairy-cli#readme",
39
+ "dependencies": {
40
+ "commander": "^9.4.1",
41
+ "axios": "^1.4.0",
42
+ "chalk": "^4.1.2",
43
+ "dotenv": "^16.0.3",
44
+ "js-yaml": "^4.1.0",
45
+ "ora": "^5.4.1",
46
+ "table": "^6.8.1"
47
+ },
48
+ "devDependencies": {
49
+ "eslint": "^8.32.0",
50
+ "jest": "^29.3.1"
51
+ },
52
+ "engines": {
53
+ "node": ">=14.0.0"
54
+ },
55
+ "files": [
56
+ "bin/",
57
+ "src/",
58
+ "README.md",
59
+ "LICENSE"
60
+ ]
61
+ }
package/src/api.js ADDED
@@ -0,0 +1,247 @@
1
+ const axios = require("axios");
2
+
3
+ class ToothFairyAPI {
4
+ constructor(baseUrl, aiUrl, apiKey, workspaceId, verbose = false) {
5
+ this.baseUrl = baseUrl;
6
+ this.aiUrl = aiUrl;
7
+ this.workspaceId = workspaceId;
8
+ this.verbose = verbose;
9
+ this.headers = {
10
+ "Content-Type": "application/json",
11
+ "x-api-key": apiKey,
12
+ };
13
+ }
14
+
15
+ async _makeRequest(method, endpoint, data = null) {
16
+ const config = {
17
+ method,
18
+ url: `${this.baseUrl}/${endpoint}`,
19
+ headers: this.headers,
20
+ };
21
+
22
+ if (method === "POST" || method === "PUT") {
23
+ if (data) {
24
+ data = { workspaceid: this.workspaceId, ...data };
25
+ }
26
+ config.data = data;
27
+ } else if (method === "GET" && data) {
28
+ // For GET requests with data, add as query parameters
29
+ const params = new URLSearchParams(data);
30
+ config.url += `?${params.toString()}`;
31
+ }
32
+
33
+ if (this.verbose) {
34
+ const chalk = require("chalk");
35
+ console.error(chalk.dim("\n--- API Request Debug ---"));
36
+ console.error(chalk.dim(`Method: ${method}`));
37
+ console.error(chalk.dim(`URL: ${config.url}`));
38
+ console.error(chalk.dim(`Headers: ${JSON.stringify(config.headers, null, 2)}`));
39
+ if (config.data) {
40
+ console.error(chalk.dim(`Data: ${JSON.stringify(config.data, null, 2)}`));
41
+ }
42
+ console.error(chalk.dim("----------------------\n"));
43
+ }
44
+
45
+ try {
46
+ const response = await axios(config);
47
+
48
+ if (this.verbose) {
49
+ const chalk = require("chalk");
50
+ console.error(chalk.dim("\n--- API Response Debug ---"));
51
+ console.error(chalk.dim(`Status: ${response.status} ${response.statusText}`));
52
+ console.error(chalk.dim(`Response Headers: ${JSON.stringify(response.headers, null, 2)}`));
53
+ console.error(chalk.dim(`Response Data: ${JSON.stringify(response.data, null, 2)}`));
54
+ console.error(chalk.dim("------------------------\n"));
55
+ }
56
+
57
+ return response.data;
58
+ } catch (error) {
59
+ if (this.verbose) {
60
+ const chalk = require("chalk");
61
+ console.error(chalk.red("\n--- API Error Debug ---"));
62
+ console.error(chalk.red(`Error: ${error.message}`));
63
+ if (error.response) {
64
+ console.error(chalk.red(`Status: ${error.response.status} ${error.response.statusText}`));
65
+ console.error(chalk.red(`Response Headers: ${JSON.stringify(error.response.headers, null, 2)}`));
66
+ console.error(chalk.red(`Response Data: ${JSON.stringify(error.response.data, null, 2)}`));
67
+ }
68
+ if (error.request) {
69
+ console.error(chalk.red(`Request Config: ${JSON.stringify(error.config, null, 2)}`));
70
+ }
71
+ console.error(chalk.red("---------------------\n"));
72
+ }
73
+
74
+ if (error.response) {
75
+ throw new Error(
76
+ `HTTP ${error.response.status}: ${
77
+ error.response.data.message || "API request failed"
78
+ }`
79
+ );
80
+ }
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ async createChat(chatData) {
86
+ return this._makeRequest("POST", "chat/create", chatData);
87
+ }
88
+
89
+ async updateChat(chatData) {
90
+ return this._makeRequest("POST", "chat/update", chatData);
91
+ }
92
+
93
+ async getChat(chatId) {
94
+ return this._makeRequest("GET", `chat/get/${chatId}`);
95
+ }
96
+
97
+ async createMessage(messageData) {
98
+ return this._makeRequest("POST", "chat_message/create", messageData);
99
+ }
100
+
101
+ async getMessage(messageId) {
102
+ return this._makeRequest("GET", `chat_message/get/${messageId}`);
103
+ }
104
+
105
+ async getAllChats() {
106
+ return this._makeRequest("GET", "chat/list", {
107
+ workspaceid: this.workspaceId,
108
+ });
109
+ }
110
+
111
+ async getAgentResponse(agentData) {
112
+ const config = {
113
+ method: "POST",
114
+ url: `${this.aiUrl}/chatter`,
115
+ headers: this.headers,
116
+ data: { workspaceid: this.workspaceId, ...agentData },
117
+ };
118
+
119
+ try {
120
+ const response = await axios(config);
121
+ return response.data;
122
+ } catch (error) {
123
+ if (error.response) {
124
+ throw new Error(
125
+ `HTTP ${error.response.status}: ${
126
+ error.response.data.message || "Agent request failed"
127
+ }`
128
+ );
129
+ }
130
+ throw error;
131
+ }
132
+ }
133
+
134
+ async sendMessageToAgent(
135
+ message,
136
+ agentId,
137
+ phoneNumber = null,
138
+ customerId = null,
139
+ providerId = null,
140
+ customerInfo = {}
141
+ ) {
142
+ try {
143
+ // Use defaults for optional parameters
144
+ customerId =
145
+ customerId ||
146
+ `cli-user-${
147
+ Math.abs(
148
+ message.split("").reduce((a, b) => {
149
+ a = (a << 5) - a + b.charCodeAt(0);
150
+ return a & a;
151
+ }, 0)
152
+ ) % 10000
153
+ }`;
154
+ phoneNumber = phoneNumber || "+1234567890";
155
+ providerId = providerId || "default-sms-provider";
156
+
157
+ const chatData = {
158
+ name: customerId,
159
+ primaryRole: agentId,
160
+ externalParticipantId: phoneNumber,
161
+ channelSettings: {
162
+ sms: {
163
+ isEnabled: true,
164
+ recipient: phoneNumber,
165
+ providerID: providerId,
166
+ },
167
+ },
168
+ customerId: customerId,
169
+ customerInfo: customerInfo,
170
+ isAIReplying: true,
171
+ };
172
+
173
+ const createdChat = await this.createChat(chatData);
174
+ // console.log(`Chat created: ${createdChat.id}`);
175
+
176
+ const messageData = {
177
+ chatID: createdChat.id,
178
+ text: message,
179
+ role: "user",
180
+ userID: "CLI",
181
+ };
182
+ const createdMessage = await this.createMessage(messageData);
183
+ // console.log(`Message created: ${createdMessage.id}`);;
184
+
185
+ const agentData = {
186
+ chatid: createdChat.id,
187
+ messages: [
188
+ {
189
+ text: createdMessage.text,
190
+ role: createdMessage.role,
191
+ userID: createdMessage.userID || "System User",
192
+ },
193
+ ],
194
+ agentid: agentId,
195
+ };
196
+ const agentResponse = await this.getAgentResponse(agentData);
197
+ // console.log('Agent response received');
198
+
199
+ return {
200
+ chatId: createdChat.id,
201
+ messageId: createdMessage.id,
202
+ agentResponse: agentResponse,
203
+ };
204
+ } catch (error) {
205
+ console.error(`Error in sendMessageToAgent: ${error.message}`);
206
+ throw error;
207
+ }
208
+ }
209
+
210
+ async searchDocuments(text, topK = 10, metadata = null) {
211
+ if (topK < 1 || topK > 50) {
212
+ throw new Error("topK must be between 1 and 50");
213
+ }
214
+
215
+ const searchData = {
216
+ text: text,
217
+ topK: topK,
218
+ };
219
+
220
+ if (metadata) {
221
+ searchData.metadata = metadata;
222
+ }
223
+
224
+ const config = {
225
+ method: "POST",
226
+ url: `${this.aiUrl}/searcher`,
227
+ headers: this.headers,
228
+ data: { workspaceid: this.workspaceId, ...searchData },
229
+ };
230
+
231
+ try {
232
+ const response = await axios(config);
233
+ return response.data;
234
+ } catch (error) {
235
+ if (error.response) {
236
+ throw new Error(
237
+ `HTTP ${error.response.status}: ${
238
+ error.response.data.message || "Search request failed"
239
+ }`
240
+ );
241
+ }
242
+ throw error;
243
+ }
244
+ }
245
+ }
246
+
247
+ module.exports = ToothFairyAPI;
package/src/config.js ADDED
@@ -0,0 +1,146 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const yaml = require('js-yaml');
5
+
6
+ class ToothFairyConfig {
7
+ constructor(baseUrl, aiUrl, apiKey, workspaceId) {
8
+ this.baseUrl = baseUrl;
9
+ this.aiUrl = aiUrl;
10
+ this.apiKey = apiKey;
11
+ this.workspaceId = workspaceId;
12
+ }
13
+
14
+ static fromEnv() {
15
+ return new ToothFairyConfig(
16
+ process.env.TF_BASE_URL || 'https://api.toothfairyai.com',
17
+ process.env.TF_AI_URL || 'https://ai.toothfairyai.com',
18
+ process.env.TF_API_KEY || '',
19
+ process.env.TF_WORKSPACE_ID || ''
20
+ );
21
+ }
22
+
23
+ static fromFile(configPath) {
24
+ if (!fs.existsSync(configPath)) {
25
+ throw new Error(`Config file not found: ${configPath}`);
26
+ }
27
+
28
+ const content = fs.readFileSync(configPath, 'utf8');
29
+ let data;
30
+
31
+ if (configPath.endsWith('.yml') || configPath.endsWith('.yaml')) {
32
+ data = yaml.load(content);
33
+ } else {
34
+ data = JSON.parse(content);
35
+ }
36
+
37
+ return new ToothFairyConfig(
38
+ data.base_url || 'https://api.toothfairyai.com',
39
+ data.ai_url || 'https://ai.toothfairyai.com',
40
+ data.api_key || '',
41
+ data.workspace_id || ''
42
+ );
43
+ }
44
+
45
+ validate() {
46
+ return !!(this.apiKey && this.workspaceId);
47
+ }
48
+
49
+ toObject() {
50
+ return {
51
+ base_url: this.baseUrl,
52
+ ai_url: this.aiUrl,
53
+ api_key: this.apiKey,
54
+ workspace_id: this.workspaceId
55
+ };
56
+ }
57
+ }
58
+
59
+ function getConfigPath() {
60
+ return path.join(os.homedir(), '.toothfairy', 'config.yml');
61
+ }
62
+
63
+ function loadConfig(configPath = null) {
64
+ // Priority order:
65
+ // 1. Provided config file path
66
+ // 2. Default config file (~/.toothfairy/config.yml)
67
+ // 3. Environment variables
68
+
69
+ if (configPath) {
70
+ return ToothFairyConfig.fromFile(configPath);
71
+ }
72
+
73
+ const defaultConfig = getConfigPath();
74
+ if (fs.existsSync(defaultConfig)) {
75
+ return ToothFairyConfig.fromFile(defaultConfig);
76
+ }
77
+
78
+ const config = ToothFairyConfig.fromEnv();
79
+ if (!config.validate()) {
80
+ throw new Error(
81
+ 'Configuration incomplete. Please provide configuration via:\n' +
82
+ '1. Config file at ~/.toothfairy/config.yml\n' +
83
+ '2. Environment variables: TF_API_KEY, TF_WORKSPACE_ID\n' +
84
+ '3. CLI arguments\n' +
85
+ 'Note: TF_BASE_URL and TF_AI_URL default to production endpoints'
86
+ );
87
+ }
88
+
89
+ return config;
90
+ }
91
+
92
+ function saveConfig(config, configPath = null) {
93
+ const filePath = configPath || getConfigPath();
94
+ const dir = path.dirname(filePath);
95
+
96
+ // Create directory if it doesn't exist
97
+ if (!fs.existsSync(dir)) {
98
+ fs.mkdirSync(dir, { recursive: true });
99
+ }
100
+
101
+ const content = yaml.dump(config.toObject());
102
+ fs.writeFileSync(filePath, content, 'utf8');
103
+ }
104
+
105
+ function validateConfiguration(config) {
106
+ const missingFields = [];
107
+
108
+ if (!config.apiKey) {
109
+ missingFields.push('API Key');
110
+ }
111
+ if (!config.workspaceId) {
112
+ missingFields.push('Workspace ID');
113
+ }
114
+
115
+ if (missingFields.length > 0) {
116
+ const missingStr = missingFields.join(' and ');
117
+ const chalk = require('chalk');
118
+
119
+ console.error(chalk.red(`Error: Missing required configuration: ${missingStr}`));
120
+ console.error();
121
+ console.error(chalk.yellow('To fix this, run the configure command:'));
122
+ console.error(chalk.dim('tf configure --api-key YOUR_API_KEY --workspace-id YOUR_WORKSPACE_ID'));
123
+ console.error();
124
+ console.error(chalk.yellow('Or set environment variables:'));
125
+ if (!config.apiKey) {
126
+ console.error(chalk.dim('export TF_API_KEY=\'your-api-key\''));
127
+ }
128
+ if (!config.workspaceId) {
129
+ console.error(chalk.dim('export TF_WORKSPACE_ID=\'your-workspace-id\''));
130
+ }
131
+ console.error();
132
+ console.error(chalk.yellow('Or create a config file at ~/.toothfairy/config.yml:'));
133
+ console.error(chalk.dim('api_key: your-api-key'));
134
+ console.error(chalk.dim('workspace_id: your-workspace-id'));
135
+
136
+ throw new Error(`Configuration incomplete: missing ${missingStr}`);
137
+ }
138
+ }
139
+
140
+ module.exports = {
141
+ ToothFairyConfig,
142
+ loadConfig,
143
+ saveConfig,
144
+ getConfigPath,
145
+ validateConfiguration
146
+ };