@yektoo/cli 0.1.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/README.md +361 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +733 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# Yekto CLI & MCP Server
|
|
2
|
+
|
|
3
|
+
Manage your [Yektoo](https://yektoo.com) customer support helpdesk from the terminal — or let AI agents do it for you.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm install -g @yektoo/cli
|
|
7
|
+
yektoo login
|
|
8
|
+
yektoo inbox --status open
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What's Included
|
|
14
|
+
|
|
15
|
+
| Package | Purpose |
|
|
16
|
+
|---|---|
|
|
17
|
+
| **`@yektoo/cli`** | Terminal commands for managing your helpdesk |
|
|
18
|
+
| **`@yektoo/mcp-server`** | MCP server that gives AI agents access to your helpdesk |
|
|
19
|
+
|
|
20
|
+
Both packages share the same authentication — log in once with the CLI, and the MCP server uses the same session.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### 1. Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Install globally
|
|
30
|
+
npm install -g @yektoo/cli
|
|
31
|
+
|
|
32
|
+
# Or run without installing
|
|
33
|
+
npx @yektoo/cli login
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Log In
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
yektoo login
|
|
40
|
+
# Enter your Yektooo email and password when prompted
|
|
41
|
+
|
|
42
|
+
# Or non-interactive (for CI/scripts)
|
|
43
|
+
yektoo login -e you@company.com -p yourpassword
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3. Start Using
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
yektoo inbox # List all conversations
|
|
50
|
+
yektoo inbox --status open # Open tickets only
|
|
51
|
+
yektoo read <email-id> # Read a conversation thread
|
|
52
|
+
yektoo reply <email-id> "Your order has shipped!"
|
|
53
|
+
yektoo close <email-id> # Close the ticket
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## CLI Commands
|
|
59
|
+
|
|
60
|
+
### Authentication
|
|
61
|
+
|
|
62
|
+
| Command | Description |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `yektoo login` | Log in with email and password (interactive) |
|
|
65
|
+
| `yektoo login -e EMAIL -p PASS` | Log in non-interactively |
|
|
66
|
+
| `yektoo logout` | Clear stored session |
|
|
67
|
+
| `yektoo whoami` | Show current user and business |
|
|
68
|
+
|
|
69
|
+
### Accounts
|
|
70
|
+
|
|
71
|
+
| Command | Description |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `yektoo email-accounts` | List connected IMAP / Outlook email accounts |
|
|
74
|
+
| `yektoo shopify-accounts` | List Shopify stores and their linked email accounts |
|
|
75
|
+
|
|
76
|
+
### Inbox & Conversations
|
|
77
|
+
|
|
78
|
+
| Command | Description |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `yektoo inbox` | List all conversations (newest first, 20 per page) |
|
|
81
|
+
| `yektoo inbox --status open` | Filter by status: `open`, `resolved`, `pending` |
|
|
82
|
+
| `yektoo inbox --store <id>` | Filter by email account |
|
|
83
|
+
| `yektoo inbox --search "refund"` | Search by subject or sender |
|
|
84
|
+
| `yektoo inbox --tag <id>` | Filter by tag (use `list-tags` to find IDs) |
|
|
85
|
+
| `yektoo inbox --tag <id1> <id2>` | Filter by multiple tags (OR logic) |
|
|
86
|
+
| `yektoo inbox --from 2026-03-01 --to 2026-03-31` | Filter by date range |
|
|
87
|
+
| `yektoo inbox --page 2` | Go to page 2 |
|
|
88
|
+
| `yektoo inbox --limit 50` | Show 50 results per page |
|
|
89
|
+
| `yektoo open-tickets --store <id>` | Open tickets for a specific email account |
|
|
90
|
+
| `yektoo read <email-id>` | Read full conversation thread (emails + replies + notes) |
|
|
91
|
+
|
|
92
|
+
### Actions
|
|
93
|
+
|
|
94
|
+
| Command | Description |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `yektoo reply <email-id> "message"` | Send a reply (auto-routes to IMAP or Outlook) |
|
|
97
|
+
| `yektoo reply-and-close <email-id> "message"` | Reply and close the ticket in one step |
|
|
98
|
+
| `yektoo close <email-id>` | Close/resolve a ticket (entire thread) |
|
|
99
|
+
| `yektoo open <email-id>` | Reopen a closed ticket |
|
|
100
|
+
| `yektoo note <email-id> "internal note"` | Add an internal note (not visible to customer) |
|
|
101
|
+
|
|
102
|
+
### Customer & Shopify
|
|
103
|
+
|
|
104
|
+
| Command | Description |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `yektoo customer <email>` | Look up a customer in Shopify — profile, orders, tracking |
|
|
107
|
+
|
|
108
|
+
### Tags & Templates
|
|
109
|
+
|
|
110
|
+
| Command | Description |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `yektoo list-tags` | List all email tags (AI Auto Tags + custom) |
|
|
113
|
+
| `yektoo list-templates` | List all reply templates |
|
|
114
|
+
| `yektoo get-template <id>` | View full template content and detected variables |
|
|
115
|
+
|
|
116
|
+
### Reports
|
|
117
|
+
|
|
118
|
+
| Command | Description |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `yektoo csat` | CSAT survey report (last 30 days) |
|
|
121
|
+
| `yektoo csat --days 90` | CSAT report for last 90 days |
|
|
122
|
+
|
|
123
|
+
### Help
|
|
124
|
+
|
|
125
|
+
| Command | Description |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `yektoo guide` | Detailed usage guide with examples |
|
|
128
|
+
| `yektoo --help` | List all commands |
|
|
129
|
+
| `yektoo <command> --help` | Help for a specific command |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## JSON Output
|
|
134
|
+
|
|
135
|
+
Every command supports `--json` for machine-readable output:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Pipe to jq
|
|
139
|
+
yektoo --json inbox --status open | jq '.[].id'
|
|
140
|
+
|
|
141
|
+
# Use in scripts
|
|
142
|
+
OPEN_COUNT=$(yektoo --json inbox --status open | jq 'length')
|
|
143
|
+
echo "You have $OPEN_COUNT open tickets"
|
|
144
|
+
|
|
145
|
+
# Get customer data as JSON
|
|
146
|
+
yektoo --json customer john@example.com
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Examples
|
|
152
|
+
|
|
153
|
+
### Daily workflow
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# Check open tickets
|
|
157
|
+
yektoo inbox --status open
|
|
158
|
+
|
|
159
|
+
# Read a ticket
|
|
160
|
+
yektoo read abc123-def456
|
|
161
|
+
|
|
162
|
+
# Look up the customer
|
|
163
|
+
yektoo customer jane@example.com
|
|
164
|
+
|
|
165
|
+
# Reply and close
|
|
166
|
+
yektoo reply-and-close abc123-def456 "Your refund has been processed. Have a great day!"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Filter by tag
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# List available tags
|
|
173
|
+
yektoo list-tags
|
|
174
|
+
|
|
175
|
+
# Show all refund-related conversations
|
|
176
|
+
yektoo inbox --tag aeb5a381-fea0-444f-950e-e66b54559d42
|
|
177
|
+
|
|
178
|
+
# Combine filters
|
|
179
|
+
yektoo inbox --status open --tag <urgent-tag-id> --store <store-id>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Use templates
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# Browse templates
|
|
186
|
+
yektoo list-templates
|
|
187
|
+
|
|
188
|
+
# View a template's content
|
|
189
|
+
yektoo get-template 3adcdc58-7df4-4480-b19a-8b7d6f881b8a
|
|
190
|
+
|
|
191
|
+
# Use the template content in a reply
|
|
192
|
+
yektoo reply <email-id> "$(yektoo --json get-template <template-id> | jq -r '.content')"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Date range queries
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# Last week's conversations
|
|
199
|
+
yektoo inbox --from 2026-03-25 --to 2026-03-31
|
|
200
|
+
|
|
201
|
+
# Open tickets from this month
|
|
202
|
+
yektoo inbox --status open --from 2026-04-01
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## MCP Server for AI Agents
|
|
208
|
+
|
|
209
|
+
The MCP server lets AI agents (Claude, Cursor, Windsurf, etc.) interact with your helpdesk using natural language.
|
|
210
|
+
|
|
211
|
+
### Setup
|
|
212
|
+
|
|
213
|
+
**Prerequisite:** Log in with the CLI first (one-time):
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npx @yektoo/cli login
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Then add the MCP server to your AI tool:
|
|
220
|
+
|
|
221
|
+
#### Claude Desktop
|
|
222
|
+
|
|
223
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"mcpServers": {
|
|
228
|
+
"yektoo": {
|
|
229
|
+
"command": "npx",
|
|
230
|
+
"args": ["-y", "@yektoo/mcp-server"]
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### Cursor
|
|
237
|
+
|
|
238
|
+
Edit `.cursor/mcp.json` in your project root:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"mcpServers": {
|
|
243
|
+
"yektoo": {
|
|
244
|
+
"command": "npx",
|
|
245
|
+
"args": ["-y", "@yektoo/mcp-server"]
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### Claude Code (pi)
|
|
252
|
+
|
|
253
|
+
Edit `.mcp.json` in your project root:
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"mcpServers": {
|
|
258
|
+
"yektoo": {
|
|
259
|
+
"command": "npx",
|
|
260
|
+
"args": ["-y", "@yektoo/mcp-server"]
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### Windsurf
|
|
267
|
+
|
|
268
|
+
Edit `~/.codeium/windsurf/mcp_config.json`:
|
|
269
|
+
|
|
270
|
+
```json
|
|
271
|
+
{
|
|
272
|
+
"mcpServers": {
|
|
273
|
+
"yektoo": {
|
|
274
|
+
"command": "npx",
|
|
275
|
+
"args": ["-y", "@yektoo/mcp-server"]
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
After adding the config, restart the AI tool. The Yektoo tools will be available immediately.
|
|
282
|
+
|
|
283
|
+
### Available MCP Tools
|
|
284
|
+
|
|
285
|
+
| Tool | Description | Parameters |
|
|
286
|
+
|---|---|---|
|
|
287
|
+
| `list_email_accounts` | Connected IMAP/Outlook accounts | — |
|
|
288
|
+
| `list_shopify_accounts` | Shopify stores + linked email accounts | — |
|
|
289
|
+
| `list_conversations` | Threaded inbox with filters | `store_id?`, `status?`, `search?`, `date_from?`, `date_to?`, `tag_ids?`, `limit?`, `page?` |
|
|
290
|
+
| `list_open_tickets` | Open tickets for an email account | `store_id`, `search?`, `date_from?`, `date_to?`, `tag_ids?`, `limit?`, `page?` |
|
|
291
|
+
| `read_thread` | Full conversation timeline | `email_id` |
|
|
292
|
+
| `send_reply` | Send a reply to a customer | `email_id`, `content` |
|
|
293
|
+
| `reply_and_close` | Reply and close in one step | `email_id`, `content` |
|
|
294
|
+
| `close_ticket` | Resolve/close entire thread | `email_id` |
|
|
295
|
+
| `reopen_ticket` | Reopen a closed ticket | `email_id` |
|
|
296
|
+
| `add_note` | Add internal note (invisible to customer) | `email_id`, `content` |
|
|
297
|
+
| `lookup_customer` | Shopify customer lookup | `customer_email` |
|
|
298
|
+
| `list_tags` | Email tags (AI Auto Tags + custom) | — |
|
|
299
|
+
| `list_templates` | Reply templates | — |
|
|
300
|
+
| `get_template` | Full template content + variables | `template_id` |
|
|
301
|
+
| `get_csat_report` | CSAT survey report | `days?` |
|
|
302
|
+
|
|
303
|
+
### What AI Agents Can Do
|
|
304
|
+
|
|
305
|
+
Once connected, you can ask your AI agent things like:
|
|
306
|
+
|
|
307
|
+
- *"Show me all open tickets"*
|
|
308
|
+
- *"Read the conversation with jane@example.com"*
|
|
309
|
+
- *"Reply to this customer saying their order shipped with tracking number 123456"*
|
|
310
|
+
- *"Close all resolved tickets tagged as 'refund'"*
|
|
311
|
+
- *"Look up this customer's Shopify orders"*
|
|
312
|
+
- *"What's our CSAT score this month?"*
|
|
313
|
+
- *"Use the 'shipping delay' template to reply to this ticket"*
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Troubleshooting
|
|
318
|
+
|
|
319
|
+
### "Not logged in"
|
|
320
|
+
|
|
321
|
+
Run `yektoo login` to authenticate. Your session persists across terminal sessions.
|
|
322
|
+
|
|
323
|
+
### "Session expired"
|
|
324
|
+
|
|
325
|
+
Run `yektoo login` again to refresh your tokens.
|
|
326
|
+
|
|
327
|
+
### "No business found"
|
|
328
|
+
|
|
329
|
+
Your Yektoo account isn't associated with a business. Log in to [app.yektoo.com](https://app.yektoo.com) and complete the setup.
|
|
330
|
+
|
|
331
|
+
### MCP server not showing tools
|
|
332
|
+
|
|
333
|
+
1. Make sure you ran `npx @yektoo/cli login` first
|
|
334
|
+
2. Restart your AI tool after editing the config
|
|
335
|
+
3. Check that the config file path is correct for your OS
|
|
336
|
+
|
|
337
|
+
### Reply not delivered
|
|
338
|
+
|
|
339
|
+
Replies are queued and processed asynchronously (same as the web app). Check your Yektooo inbox in a few seconds — the reply will appear in the thread once delivered.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Development
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
# CLI
|
|
347
|
+
cd cli
|
|
348
|
+
npm install
|
|
349
|
+
npm run dev -- inbox --status open # Run in dev mode
|
|
350
|
+
|
|
351
|
+
# MCP Server
|
|
352
|
+
cd mcp-server
|
|
353
|
+
npm install
|
|
354
|
+
npm run dev # Run in dev mode
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## License
|
|
360
|
+
|
|
361
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { createInterface } from "readline";
|
|
6
|
+
|
|
7
|
+
// src/client.ts
|
|
8
|
+
import { createClient } from "@supabase/supabase-js";
|
|
9
|
+
import Conf from "conf";
|
|
10
|
+
var SUPABASE_URL = "https://vjkofswgtffzyeuiainf.supabase.co";
|
|
11
|
+
var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZqa29mc3dndGZmenlldWlhaW5mIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDgwMDA3MzYsImV4cCI6MjA2MzU3NjczNn0.7SqqkSgvxF5zXz8Bdq1BzdNN0Hv_H9vty2aZsaz_T5s";
|
|
12
|
+
var config = new Conf({
|
|
13
|
+
projectName: "yektoo-cli",
|
|
14
|
+
encryptionKey: "yektoo-cli-v1"
|
|
15
|
+
// conf encrypts values at rest with this key
|
|
16
|
+
});
|
|
17
|
+
var _client = null;
|
|
18
|
+
function getClient() {
|
|
19
|
+
if (_client) return _client;
|
|
20
|
+
const accessToken = config.get("accessToken");
|
|
21
|
+
const refreshToken = config.get("refreshToken");
|
|
22
|
+
if (!accessToken || !refreshToken) {
|
|
23
|
+
throw new Error("Not logged in. Run `yektoo login` first.");
|
|
24
|
+
}
|
|
25
|
+
_client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
26
|
+
auth: {
|
|
27
|
+
autoRefreshToken: true,
|
|
28
|
+
persistSession: false
|
|
29
|
+
// We manage persistence ourselves via conf
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
_client.auth.setSession({ access_token: accessToken, refresh_token: refreshToken });
|
|
33
|
+
return _client;
|
|
34
|
+
}
|
|
35
|
+
async function login(email, password) {
|
|
36
|
+
const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
37
|
+
auth: { autoRefreshToken: false, persistSession: false }
|
|
38
|
+
});
|
|
39
|
+
const { data, error } = await client.auth.signInWithPassword({ email, password });
|
|
40
|
+
if (error) throw new Error(`Login failed: ${error.message}`);
|
|
41
|
+
if (!data.session) throw new Error("Login failed: no session returned");
|
|
42
|
+
const { data: profile } = await client.from("user_profiles").select("business_id").eq("user_id", data.user.id).single();
|
|
43
|
+
config.set("accessToken", data.session.access_token);
|
|
44
|
+
config.set("refreshToken", data.session.refresh_token);
|
|
45
|
+
config.set("userId", data.user.id);
|
|
46
|
+
config.set("email", data.user.email || email);
|
|
47
|
+
config.set("businessId", profile?.business_id || void 0);
|
|
48
|
+
_client = null;
|
|
49
|
+
return { userId: data.user.id, email: data.user.email || email };
|
|
50
|
+
}
|
|
51
|
+
function logout() {
|
|
52
|
+
config.clear();
|
|
53
|
+
_client = null;
|
|
54
|
+
}
|
|
55
|
+
function getStoredUser() {
|
|
56
|
+
const userId = config.get("userId");
|
|
57
|
+
const email = config.get("email");
|
|
58
|
+
if (!userId || !email) return null;
|
|
59
|
+
return { userId, email, businessId: config.get("businessId") };
|
|
60
|
+
}
|
|
61
|
+
function getSupabaseUrl() {
|
|
62
|
+
return SUPABASE_URL;
|
|
63
|
+
}
|
|
64
|
+
async function getAuthHeader() {
|
|
65
|
+
const client = getClient();
|
|
66
|
+
const { data } = await client.auth.getSession();
|
|
67
|
+
if (!data.session?.access_token) throw new Error("Session expired. Run `yektoo login` again.");
|
|
68
|
+
return {
|
|
69
|
+
"Authorization": `Bearer ${data.session.access_token}`,
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
"apikey": SUPABASE_ANON_KEY
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/format.ts
|
|
76
|
+
import chalk from "chalk";
|
|
77
|
+
var _format = "table";
|
|
78
|
+
function setOutputFormat(format) {
|
|
79
|
+
_format = format;
|
|
80
|
+
}
|
|
81
|
+
function getOutputFormat() {
|
|
82
|
+
return _format;
|
|
83
|
+
}
|
|
84
|
+
function printData(data, options) {
|
|
85
|
+
if (_format === "json") {
|
|
86
|
+
console.log(JSON.stringify(data, null, 2));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (options?.title) {
|
|
90
|
+
console.log(chalk.bold(`
|
|
91
|
+
${options.title}`));
|
|
92
|
+
console.log(chalk.gray("\u2500".repeat(60)));
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(data)) {
|
|
95
|
+
if (data.length === 0) {
|
|
96
|
+
console.log(chalk.gray(" No results."));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
console.table(data);
|
|
100
|
+
} else {
|
|
101
|
+
for (const [key, value] of Object.entries(data)) {
|
|
102
|
+
console.log(` ${chalk.gray(key + ":")} ${value}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
console.log();
|
|
106
|
+
}
|
|
107
|
+
function printSuccess(message) {
|
|
108
|
+
if (_format === "json") {
|
|
109
|
+
console.log(JSON.stringify({ success: true, message }));
|
|
110
|
+
} else {
|
|
111
|
+
console.log(chalk.green("\u2713") + " " + message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function printError(message) {
|
|
115
|
+
if (_format === "json") {
|
|
116
|
+
console.error(JSON.stringify({ success: false, error: message }));
|
|
117
|
+
} else {
|
|
118
|
+
console.error(chalk.red("\u2717") + " " + message);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function truncate(str, maxLen = 60) {
|
|
122
|
+
if (!str) return "";
|
|
123
|
+
const clean = str.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
124
|
+
return clean.length > maxLen ? clean.slice(0, maxLen - 1) + "\u2026" : clean;
|
|
125
|
+
}
|
|
126
|
+
function formatDate(dateStr) {
|
|
127
|
+
if (!dateStr) return "";
|
|
128
|
+
const d = new Date(dateStr);
|
|
129
|
+
const now = /* @__PURE__ */ new Date();
|
|
130
|
+
const diff = now.getTime() - d.getTime();
|
|
131
|
+
const mins = Math.floor(diff / 6e4);
|
|
132
|
+
if (mins < 1) return "just now";
|
|
133
|
+
if (mins < 60) return `${mins}m ago`;
|
|
134
|
+
const hours = Math.floor(mins / 60);
|
|
135
|
+
if (hours < 24) return `${hours}h ago`;
|
|
136
|
+
const days = Math.floor(hours / 24);
|
|
137
|
+
if (days < 7) return `${days}d ago`;
|
|
138
|
+
return d.toISOString().slice(0, 10);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/index.ts
|
|
142
|
+
import chalk2 from "chalk";
|
|
143
|
+
var program = new Command();
|
|
144
|
+
program.name("yektoo").description("Yektoo CLI \u2014 manage your customer support helpdesk").version("0.1.0").option("--json", "Output as JSON (for piping and AI agents)").hook("preAction", (thisCommand) => {
|
|
145
|
+
if (thisCommand.opts().json) setOutputFormat("json");
|
|
146
|
+
});
|
|
147
|
+
program.command("login").description("Log in with your Yektoo account").option("-e, --email <email>", "Account email").option("-p, --password <password>", "Account password").action(async (opts) => {
|
|
148
|
+
try {
|
|
149
|
+
let email = opts.email;
|
|
150
|
+
let password = opts.password;
|
|
151
|
+
if (!email || !password) {
|
|
152
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
153
|
+
const ask = (q, hidden = false) => new Promise((resolve) => {
|
|
154
|
+
if (hidden) {
|
|
155
|
+
process.stderr.write(q);
|
|
156
|
+
let input = "";
|
|
157
|
+
process.stdin.setRawMode?.(true);
|
|
158
|
+
process.stdin.resume();
|
|
159
|
+
process.stdin.setEncoding("utf8");
|
|
160
|
+
const onData = (ch) => {
|
|
161
|
+
if (ch === "\n" || ch === "\r") {
|
|
162
|
+
process.stdin.setRawMode?.(false);
|
|
163
|
+
process.stdin.removeListener("data", onData);
|
|
164
|
+
process.stderr.write("\n");
|
|
165
|
+
resolve(input);
|
|
166
|
+
} else if (ch === "\x7F" || ch === "\b") {
|
|
167
|
+
input = input.slice(0, -1);
|
|
168
|
+
} else if (ch === "") {
|
|
169
|
+
process.exit(0);
|
|
170
|
+
} else {
|
|
171
|
+
input += ch;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
process.stdin.on("data", onData);
|
|
175
|
+
} else {
|
|
176
|
+
rl.question(q, (answer) => resolve(answer));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
if (!email) email = await ask("Email: ");
|
|
180
|
+
if (!password) password = await ask("Password: ", true);
|
|
181
|
+
rl.close();
|
|
182
|
+
}
|
|
183
|
+
const result = await login(email, password);
|
|
184
|
+
printSuccess(`Logged in as ${result.email}`);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
printError(err.message);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
program.command("logout").description("Log out and clear stored session").action(() => {
|
|
191
|
+
logout();
|
|
192
|
+
printSuccess("Logged out");
|
|
193
|
+
});
|
|
194
|
+
program.command("whoami").description("Show current authenticated user").action(() => {
|
|
195
|
+
const user = getStoredUser();
|
|
196
|
+
if (!user) {
|
|
197
|
+
printError("Not logged in. Run `yektoo login` first.");
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
printData({ email: user.email, userId: user.userId, businessId: user.businessId || "(not set)" });
|
|
201
|
+
});
|
|
202
|
+
program.command("email-accounts").description("List connected email accounts (IMAP and Outlook)").action(async () => {
|
|
203
|
+
try {
|
|
204
|
+
const client = getClient();
|
|
205
|
+
const user = getStoredUser();
|
|
206
|
+
if (!user?.businessId) throw new Error("No business found. Please log in again.");
|
|
207
|
+
const { data, error } = await client.from("stores").select("id, name, email, platform, connection_method, connected").eq("business_id", user.businessId).order("name");
|
|
208
|
+
if (error) throw new Error(error.message);
|
|
209
|
+
const rows = (data || []).map((s) => ({
|
|
210
|
+
id: s.id,
|
|
211
|
+
name: s.name || "(unnamed)",
|
|
212
|
+
email: s.email,
|
|
213
|
+
type: s.connection_method === "imap" ? "IMAP" : s.platform === "imap" ? "IMAP" : "Outlook",
|
|
214
|
+
connected: s.connected ? "\u2713" : "\u2717"
|
|
215
|
+
}));
|
|
216
|
+
printData(rows, { title: "Email Accounts" });
|
|
217
|
+
} catch (err) {
|
|
218
|
+
printError(err.message);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
program.command("shopify-accounts").description("List connected Shopify stores and their linked email accounts").action(async () => {
|
|
223
|
+
try {
|
|
224
|
+
const client = getClient();
|
|
225
|
+
const user = getStoredUser();
|
|
226
|
+
if (!user?.businessId) throw new Error("No business found. Please log in again.");
|
|
227
|
+
const { data: shopifyStores, error } = await client.from("shopify_stores").select(`
|
|
228
|
+
id,
|
|
229
|
+
shop_domain,
|
|
230
|
+
created_at,
|
|
231
|
+
associated_email_store_id,
|
|
232
|
+
store:store_id ( id, name, connected )
|
|
233
|
+
`).order("created_at", { ascending: false });
|
|
234
|
+
if (error) throw new Error(error.message);
|
|
235
|
+
const rows = await Promise.all(
|
|
236
|
+
(shopifyStores || []).map(async (ss) => {
|
|
237
|
+
let linkedEmail = "(none)";
|
|
238
|
+
if (ss.associated_email_store_id) {
|
|
239
|
+
const { data: emailStore } = await client.from("stores").select("name, email").eq("id", ss.associated_email_store_id).single();
|
|
240
|
+
if (emailStore) {
|
|
241
|
+
linkedEmail = `${emailStore.name || ""} (${emailStore.email})`.trim();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
id: ss.id,
|
|
246
|
+
domain: ss.shop_domain,
|
|
247
|
+
name: ss.store?.name || "(unnamed)",
|
|
248
|
+
connected: ss.store?.connected ? "\u2713" : "\u2717",
|
|
249
|
+
linkedEmailAccount: linkedEmail
|
|
250
|
+
};
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
printData(rows, { title: "Shopify Accounts" });
|
|
254
|
+
} catch (err) {
|
|
255
|
+
printError(err.message);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
program.command("inbox").description("List conversations (threaded email view)").option("-s, --store <storeId>", "Filter by store ID").option("--status <status>", "Filter by status: open, resolved, pending", "all").option("-n, --limit <number>", "Number of results per page", "20").option("-p, --page <number>", "Page number", "1").option("--search <query>", "Search by subject or sender").option("--from <date>", "Filter from date (e.g. 2026-03-01)").option("--to <date>", "Filter to date (e.g. 2026-03-31)").option("--tag <tagIds...>", "Filter by tag ID(s) \u2014 use list-tags to find IDs").action(async (opts) => {
|
|
260
|
+
try {
|
|
261
|
+
const client = getClient();
|
|
262
|
+
const user = getStoredUser();
|
|
263
|
+
if (!user?.businessId) throw new Error("No business found.");
|
|
264
|
+
const page = parseInt(opts.page) || 1;
|
|
265
|
+
const limit = parseInt(opts.limit) || 20;
|
|
266
|
+
const tags = opts.tag && opts.tag.length > 0 ? opts.tag : null;
|
|
267
|
+
const { data, error } = await client.rpc("get_threaded_emails_v5", {
|
|
268
|
+
p_user_id: user.userId,
|
|
269
|
+
p_store_filter: opts.store || "all",
|
|
270
|
+
p_status: opts.status || "all",
|
|
271
|
+
p_search: opts.search || "",
|
|
272
|
+
p_date_from: opts.from || null,
|
|
273
|
+
p_date_to: opts.to || null,
|
|
274
|
+
p_page: page,
|
|
275
|
+
p_page_size: limit,
|
|
276
|
+
p_sort_by: "date",
|
|
277
|
+
p_sort_order: "desc",
|
|
278
|
+
p_assigned_to: "all",
|
|
279
|
+
p_read_status: "all",
|
|
280
|
+
p_priority: null,
|
|
281
|
+
p_tags: tags,
|
|
282
|
+
p_folder_source: "inbox"
|
|
283
|
+
});
|
|
284
|
+
if (error) throw new Error(error.message);
|
|
285
|
+
const rows = (data || []).map((r) => ({
|
|
286
|
+
id: r.latest_email_id,
|
|
287
|
+
from: truncate(r.latest_email_from, 30),
|
|
288
|
+
subject: truncate(r.thread_subject || r.latest_email_subject, 45),
|
|
289
|
+
status: r.latest_email_status,
|
|
290
|
+
date: formatDate(r.latest_email_date),
|
|
291
|
+
msgs: r.thread_email_count,
|
|
292
|
+
unread: r.thread_has_unread ? "\u25CF" : ""
|
|
293
|
+
}));
|
|
294
|
+
const total = (data || []).length > 0 ? data[0].total_threads : 0;
|
|
295
|
+
const totalPages = Math.ceil(total / limit);
|
|
296
|
+
printData(rows, { title: `Inbox (page ${page}/${totalPages}, ${total} total)` });
|
|
297
|
+
if (page < totalPages) {
|
|
298
|
+
console.log(chalk2.gray(` \u2192 Next page: yektoo inbox --page ${page + 1}${opts.store ? " --store " + opts.store : ""}${opts.status !== "all" ? " --status " + opts.status : ""}`));
|
|
299
|
+
console.log();
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
printError(err.message);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
program.command("open-tickets").description("List open tickets for a specific email account").option("-s, --store <storeId>", "Filter by email account ID (required)").option("-n, --limit <number>", "Number of results per page", "20").option("-p, --page <number>", "Page number", "1").option("--search <query>", "Search by subject or sender").option("--from <date>", "Filter from date (e.g. 2026-03-01)").option("--to <date>", "Filter to date (e.g. 2026-03-31)").option("--tag <tagIds...>", "Filter by tag ID(s) \u2014 use list-tags to find IDs").action(async (opts) => {
|
|
307
|
+
try {
|
|
308
|
+
const client = getClient();
|
|
309
|
+
const user = getStoredUser();
|
|
310
|
+
if (!user?.businessId) throw new Error("No business found.");
|
|
311
|
+
if (!opts.store) {
|
|
312
|
+
const { data: stores } = await client.from("stores").select("id, name, email").eq("business_id", user.businessId).eq("connected", true).order("name");
|
|
313
|
+
printError("Please specify an email account with --store <id>");
|
|
314
|
+
if (stores?.length) {
|
|
315
|
+
printData(
|
|
316
|
+
stores.map((s) => ({ id: s.id, name: s.name, email: s.email })),
|
|
317
|
+
{ title: "Available email accounts" }
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
const tags = opts.tag && opts.tag.length > 0 ? opts.tag : null;
|
|
323
|
+
const { data, error } = await client.rpc("get_threaded_emails_v5", {
|
|
324
|
+
p_user_id: user.userId,
|
|
325
|
+
p_store_filter: opts.store,
|
|
326
|
+
p_status: "open",
|
|
327
|
+
p_search: opts.search || "",
|
|
328
|
+
p_date_from: opts.from || null,
|
|
329
|
+
p_date_to: opts.to || null,
|
|
330
|
+
p_page: parseInt(opts.page) || 1,
|
|
331
|
+
p_page_size: parseInt(opts.limit) || 20,
|
|
332
|
+
p_sort_by: "date",
|
|
333
|
+
p_sort_order: "desc",
|
|
334
|
+
p_assigned_to: "all",
|
|
335
|
+
p_read_status: "all",
|
|
336
|
+
p_priority: null,
|
|
337
|
+
p_tags: tags,
|
|
338
|
+
p_folder_source: "inbox"
|
|
339
|
+
});
|
|
340
|
+
if (error) throw new Error(error.message);
|
|
341
|
+
const rows = (data || []).map((r) => ({
|
|
342
|
+
id: r.latest_email_id,
|
|
343
|
+
from: truncate(r.latest_email_from, 30),
|
|
344
|
+
subject: truncate(r.thread_subject || r.latest_email_subject, 45),
|
|
345
|
+
date: formatDate(r.latest_email_date),
|
|
346
|
+
msgs: r.thread_email_count,
|
|
347
|
+
unread: r.thread_has_unread ? "\u25CF" : ""
|
|
348
|
+
}));
|
|
349
|
+
const page = parseInt(opts.page) || 1;
|
|
350
|
+
const limit = parseInt(opts.limit) || 20;
|
|
351
|
+
const total = (data || []).length > 0 ? data[0].total_threads : 0;
|
|
352
|
+
const totalPages = Math.ceil(total / limit);
|
|
353
|
+
printData(rows, { title: `Open Tickets (page ${page}/${totalPages}, ${total} total)` });
|
|
354
|
+
if (page < totalPages) {
|
|
355
|
+
console.log(chalk2.gray(` \u2192 Next page: yektoo open-tickets --store ${opts.store} --page ${page + 1}`));
|
|
356
|
+
console.log();
|
|
357
|
+
}
|
|
358
|
+
} catch (err) {
|
|
359
|
+
printError(err.message);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
program.command("read <emailId>").description("Read a conversation thread (emails + replies + notes)").action(async (emailId) => {
|
|
364
|
+
try {
|
|
365
|
+
const client = getClient();
|
|
366
|
+
const user = getStoredUser();
|
|
367
|
+
if (!user) throw new Error("Not logged in.");
|
|
368
|
+
const { data: emailData, error: emailErr } = await client.from("emails").select("id, thread_id, subject, from, recipient, status, date").eq("id", emailId).single();
|
|
369
|
+
if (emailErr || !emailData) throw new Error("Email not found");
|
|
370
|
+
const { data, error } = await client.rpc("get_email_thread_complete", {
|
|
371
|
+
p_email_id: emailId,
|
|
372
|
+
p_user_id: user.userId,
|
|
373
|
+
p_thread_id: emailData.thread_id
|
|
374
|
+
});
|
|
375
|
+
if (error) throw new Error(error.message);
|
|
376
|
+
if (!data || data.length === 0) throw new Error("Thread not found");
|
|
377
|
+
const result = data[0];
|
|
378
|
+
const threadEmails = result.thread_emails || [];
|
|
379
|
+
const replies = result.replies || [];
|
|
380
|
+
const notes = result.notes || [];
|
|
381
|
+
const timeline = [
|
|
382
|
+
...threadEmails.map((e) => ({
|
|
383
|
+
type: "email",
|
|
384
|
+
from: e.from,
|
|
385
|
+
date: e.date,
|
|
386
|
+
content: truncate(e.content, 200),
|
|
387
|
+
direction: e.direction
|
|
388
|
+
})),
|
|
389
|
+
...replies.map((r) => ({
|
|
390
|
+
type: "reply",
|
|
391
|
+
from: r.from || "(agent)",
|
|
392
|
+
date: r.sent_at,
|
|
393
|
+
content: truncate(r.content, 200),
|
|
394
|
+
direction: "outbound"
|
|
395
|
+
})),
|
|
396
|
+
...notes.map((n) => ({
|
|
397
|
+
type: "note",
|
|
398
|
+
from: "(internal)",
|
|
399
|
+
date: n.created_at,
|
|
400
|
+
content: truncate(n.content, 200),
|
|
401
|
+
direction: "internal"
|
|
402
|
+
}))
|
|
403
|
+
].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
|
404
|
+
printData(
|
|
405
|
+
{
|
|
406
|
+
id: emailData.id,
|
|
407
|
+
subject: emailData.subject,
|
|
408
|
+
status: emailData.status,
|
|
409
|
+
from: emailData.from,
|
|
410
|
+
date: emailData.date,
|
|
411
|
+
messages: timeline.length
|
|
412
|
+
},
|
|
413
|
+
{ title: "Thread" }
|
|
414
|
+
);
|
|
415
|
+
printData(timeline, { title: "Timeline" });
|
|
416
|
+
} catch (err) {
|
|
417
|
+
printError(err.message);
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
program.command("reply <emailId> <content>").description("Send a reply to a conversation").action(async (emailId, content) => {
|
|
422
|
+
try {
|
|
423
|
+
const client = getClient();
|
|
424
|
+
const user = getStoredUser();
|
|
425
|
+
if (!user) throw new Error("Not logged in.");
|
|
426
|
+
const { data: email, error: emailErr } = await client.from("emails").select("id, store_id, stores(id, platform, connection_method, business_id, email)").eq("id", emailId).single();
|
|
427
|
+
if (emailErr || !email) throw new Error("Email not found");
|
|
428
|
+
const store = email.stores;
|
|
429
|
+
if (!store) throw new Error("Store not found for this email");
|
|
430
|
+
if (store.business_id !== user.businessId) throw new Error("Access denied: email does not belong to your business");
|
|
431
|
+
const isImap = store.platform === "imap" || store.connection_method === "imap";
|
|
432
|
+
const functionType = isImap ? "send-email-imap" : "send-email";
|
|
433
|
+
const { error: queueError } = await client.from("email_send_queue").insert({
|
|
434
|
+
business_id: store.business_id,
|
|
435
|
+
store_id: store.id,
|
|
436
|
+
user_id: user.userId,
|
|
437
|
+
function_type: functionType,
|
|
438
|
+
payload: {
|
|
439
|
+
emailId,
|
|
440
|
+
content,
|
|
441
|
+
attachments: [],
|
|
442
|
+
isScheduledSend: true,
|
|
443
|
+
originalUserId: user.userId
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
if (queueError) throw new Error(`Failed to queue reply: ${queueError.message}`);
|
|
447
|
+
printSuccess(`Reply queued for delivery via ${functionType}`);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
printError(err.message);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
program.command("close <emailId>").description("Resolve/close a ticket (closes entire thread)").action(async (emailId) => {
|
|
454
|
+
try {
|
|
455
|
+
const client = getClient();
|
|
456
|
+
const { data, error } = await client.rpc("mark_thread_resolved", {
|
|
457
|
+
p_email_id: emailId
|
|
458
|
+
});
|
|
459
|
+
if (error) throw new Error(error.message);
|
|
460
|
+
const result = data?.[0];
|
|
461
|
+
if (!result || result.updated_count === 0) throw new Error("Email not found or already resolved");
|
|
462
|
+
printSuccess(`Closed thread: ${result.updated_count} email(s) marked as resolved`);
|
|
463
|
+
} catch (err) {
|
|
464
|
+
printError(err.message);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
program.command("open <emailId>").description("Reopen a ticket").action(async (emailId) => {
|
|
469
|
+
try {
|
|
470
|
+
const client = getClient();
|
|
471
|
+
const { error } = await client.from("emails").update({ status: "open" }).eq("id", emailId);
|
|
472
|
+
if (error) throw new Error(error.message);
|
|
473
|
+
printSuccess("Ticket reopened");
|
|
474
|
+
} catch (err) {
|
|
475
|
+
printError(err.message);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
program.command("reply-and-close <emailId> <content>").description("Send a reply and close the ticket in one step").action(async (emailId, content) => {
|
|
480
|
+
try {
|
|
481
|
+
const client = getClient();
|
|
482
|
+
const user = getStoredUser();
|
|
483
|
+
if (!user) throw new Error("Not logged in.");
|
|
484
|
+
const { data: email, error: emailErr } = await client.from("emails").select("id, store_id, stores(id, platform, connection_method, business_id, email)").eq("id", emailId).single();
|
|
485
|
+
if (emailErr || !email) throw new Error("Email not found");
|
|
486
|
+
const store = email.stores;
|
|
487
|
+
if (!store) throw new Error("Store not found for this email");
|
|
488
|
+
if (store.business_id !== user.businessId) throw new Error("Access denied: email does not belong to your business");
|
|
489
|
+
const isImap = store.platform === "imap" || store.connection_method === "imap";
|
|
490
|
+
const functionType = isImap ? "send-email-imap" : "send-email";
|
|
491
|
+
const { error: queueError } = await client.from("email_send_queue").insert({
|
|
492
|
+
business_id: store.business_id,
|
|
493
|
+
store_id: store.id,
|
|
494
|
+
user_id: user.userId,
|
|
495
|
+
function_type: functionType,
|
|
496
|
+
payload: {
|
|
497
|
+
emailId,
|
|
498
|
+
content,
|
|
499
|
+
attachments: [],
|
|
500
|
+
isScheduledSend: true,
|
|
501
|
+
originalUserId: user.userId
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
if (queueError) throw new Error(`Failed to queue reply: ${queueError.message}`);
|
|
505
|
+
const { data: closeData, error: closeError } = await client.rpc("mark_thread_resolved", {
|
|
506
|
+
p_email_id: emailId
|
|
507
|
+
});
|
|
508
|
+
if (closeError) throw new Error(`Reply queued but close failed: ${closeError.message}`);
|
|
509
|
+
const result = closeData?.[0];
|
|
510
|
+
printSuccess(`Reply queued via ${functionType} and ticket closed (${result?.updated_count || 1} email(s) resolved)`);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
printError(err.message);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
program.command("note <emailId> <content>").description("Add an internal note to a conversation").action(async (emailId, content) => {
|
|
517
|
+
try {
|
|
518
|
+
const client = getClient();
|
|
519
|
+
const user = getStoredUser();
|
|
520
|
+
if (!user) throw new Error("Not logged in.");
|
|
521
|
+
const { error } = await client.from("internal_notes").insert({
|
|
522
|
+
email_id: emailId,
|
|
523
|
+
user_id: user.userId,
|
|
524
|
+
content
|
|
525
|
+
});
|
|
526
|
+
if (error) throw new Error(error.message);
|
|
527
|
+
printSuccess("Internal note added");
|
|
528
|
+
} catch (err) {
|
|
529
|
+
printError(err.message);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
program.command("list-tags").description("List all email tags (AI Auto Tags and custom tags)").action(async () => {
|
|
534
|
+
try {
|
|
535
|
+
const client = getClient();
|
|
536
|
+
const { data, error } = await client.from("email_tags").select("id, name, color, description, tag_type, created_at").order("name");
|
|
537
|
+
if (error) throw new Error(error.message);
|
|
538
|
+
const rows = (data || []).map((t) => ({
|
|
539
|
+
id: t.id,
|
|
540
|
+
name: t.name,
|
|
541
|
+
type: t.tag_type || "custom",
|
|
542
|
+
color: t.color,
|
|
543
|
+
description: t.description || ""
|
|
544
|
+
}));
|
|
545
|
+
printData(rows, { title: `Tags (${rows.length})` });
|
|
546
|
+
} catch (err) {
|
|
547
|
+
printError(err.message);
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
program.command("list-templates").description("List all reply templates").action(async () => {
|
|
552
|
+
try {
|
|
553
|
+
const client = getClient();
|
|
554
|
+
const { data, error } = await client.from("reply_templates").select("id, name, content, created_at").order("created_at", { ascending: false });
|
|
555
|
+
if (error) throw new Error(error.message);
|
|
556
|
+
const rows = (data || []).map((t) => ({
|
|
557
|
+
id: t.id,
|
|
558
|
+
name: t.name,
|
|
559
|
+
preview: truncate(t.content, 60),
|
|
560
|
+
created: formatDate(t.created_at)
|
|
561
|
+
}));
|
|
562
|
+
printData(rows, { title: `Reply Templates (${rows.length})` });
|
|
563
|
+
} catch (err) {
|
|
564
|
+
printError(err.message);
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
program.command("get-template <templateId>").description("Get the full content of a reply template").action(async (templateId) => {
|
|
569
|
+
try {
|
|
570
|
+
const client = getClient();
|
|
571
|
+
const { data, error } = await client.from("reply_templates").select("id, name, content, created_at").eq("id", templateId).single();
|
|
572
|
+
if (error || !data) throw new Error("Template not found");
|
|
573
|
+
const plainContent = data.content.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/\n{3,}/g, "\n\n").trim();
|
|
574
|
+
printData({
|
|
575
|
+
id: data.id,
|
|
576
|
+
name: data.name,
|
|
577
|
+
created: formatDate(data.created_at),
|
|
578
|
+
variables: (data.content.match(/\{\{[a-z_]+\}\}/gi) || []).join(", ") || "(none)"
|
|
579
|
+
}, { title: "Template" });
|
|
580
|
+
if (getOutputFormat() === "json") {
|
|
581
|
+
printData({ content: plainContent, html: data.content });
|
|
582
|
+
} else {
|
|
583
|
+
console.log(chalk2.gray("\u2500".repeat(60)));
|
|
584
|
+
console.log(plainContent);
|
|
585
|
+
console.log(chalk2.gray("\u2500".repeat(60)));
|
|
586
|
+
console.log();
|
|
587
|
+
}
|
|
588
|
+
} catch (err) {
|
|
589
|
+
printError(err.message);
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
program.command("customer <email>").description("Look up a customer in Shopify (orders, spend, tracking)").action(async (customerEmail) => {
|
|
594
|
+
try {
|
|
595
|
+
const headers = await getAuthHeader();
|
|
596
|
+
const url = `${getSupabaseUrl()}/functions/v1/shopify-lookup?email=${encodeURIComponent(customerEmail)}`;
|
|
597
|
+
const response = await fetch(url, { headers });
|
|
598
|
+
if (!response.ok) {
|
|
599
|
+
const err = await response.json().catch(() => ({}));
|
|
600
|
+
throw new Error(err.error || `HTTP ${response.status}`);
|
|
601
|
+
}
|
|
602
|
+
const data = await response.json();
|
|
603
|
+
if (!data.found || !data.stores?.length) {
|
|
604
|
+
printError(`No Shopify customer found for ${customerEmail}`);
|
|
605
|
+
process.exit(0);
|
|
606
|
+
}
|
|
607
|
+
for (const entry of data.stores) {
|
|
608
|
+
printData({
|
|
609
|
+
store: entry.store?.name || "(unknown)",
|
|
610
|
+
name: `${entry.customer?.firstName || ""} ${entry.customer?.lastName || ""}`.trim(),
|
|
611
|
+
email: entry.customer?.email,
|
|
612
|
+
phone: entry.customer?.phone || "(none)",
|
|
613
|
+
orders: entry.customer?.ordersCount,
|
|
614
|
+
totalSpent: entry.customer?.totalSpent
|
|
615
|
+
}, { title: `Customer \u2014 ${entry.store?.name || "Shopify"}` });
|
|
616
|
+
if (entry.orders?.length) {
|
|
617
|
+
const orderRows = entry.orders.slice(0, 5).map((o) => ({
|
|
618
|
+
order: `#${o.number}`,
|
|
619
|
+
date: formatDate(o.date),
|
|
620
|
+
total: o.totalPrice,
|
|
621
|
+
status: o.fulfillmentStatus || "unfulfilled",
|
|
622
|
+
tracking: o.tracking?.number || ""
|
|
623
|
+
}));
|
|
624
|
+
printData(orderRows, { title: "Recent Orders" });
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
} catch (err) {
|
|
628
|
+
printError(err.message);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
program.command("csat").description("View CSAT survey report").option("-d, --days <number>", "Number of days to report on", "30").action(async (opts) => {
|
|
633
|
+
try {
|
|
634
|
+
const client = getClient();
|
|
635
|
+
const user = getStoredUser();
|
|
636
|
+
if (!user?.businessId) throw new Error("No business found.");
|
|
637
|
+
const { data, error } = await client.rpc("get_csat_report", {
|
|
638
|
+
p_business_id: user.businessId,
|
|
639
|
+
p_days: parseInt(opts.days) || 30
|
|
640
|
+
});
|
|
641
|
+
if (error) throw new Error(error.message);
|
|
642
|
+
if (!data) throw new Error("No CSAT data available");
|
|
643
|
+
const summary = data.summary;
|
|
644
|
+
printData({
|
|
645
|
+
avgRating: summary.avg_rating ? `${summary.avg_rating} / 5` : "\u2014",
|
|
646
|
+
responseRate: summary.response_rate != null ? `${summary.response_rate}%` : "\u2014",
|
|
647
|
+
sent: summary.total_sent,
|
|
648
|
+
responded: summary.total_responded,
|
|
649
|
+
distribution: `\u26051:${summary.distribution["1"]} \u26052:${summary.distribution["2"]} \u26053:${summary.distribution["3"]} \u26054:${summary.distribution["4"]} \u26055:${summary.distribution["5"]}`
|
|
650
|
+
}, { title: `CSAT Report (last ${opts.days} days)` });
|
|
651
|
+
if (data.by_agent?.length) {
|
|
652
|
+
const agentRows = data.by_agent.map((a) => ({
|
|
653
|
+
agent: a.agent_name,
|
|
654
|
+
avgRating: a.avg_rating ? a.avg_rating.toFixed(1) : "\u2014",
|
|
655
|
+
responded: `${a.responded}/${a.total_sent}`
|
|
656
|
+
}));
|
|
657
|
+
printData(agentRows, { title: "By Agent" });
|
|
658
|
+
}
|
|
659
|
+
if (data.recent?.length) {
|
|
660
|
+
const recentRows = data.recent.slice(0, 10).map((r) => ({
|
|
661
|
+
customer: truncate(r.customer_email, 30),
|
|
662
|
+
rating: "\u2605".repeat(r.rating),
|
|
663
|
+
comment: truncate(r.comment, 40) || "\u2014",
|
|
664
|
+
date: formatDate(r.responded_at)
|
|
665
|
+
}));
|
|
666
|
+
printData(recentRows, { title: "Recent Responses" });
|
|
667
|
+
}
|
|
668
|
+
} catch (err) {
|
|
669
|
+
printError(err.message);
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
program.command("guide").description("Show detailed usage guide with examples").action(() => {
|
|
674
|
+
const guide = `
|
|
675
|
+
${chalk2.bold("Yektoo CLI \u2014 Customer Support Helpdesk")}
|
|
676
|
+
${chalk2.gray("Manage your inbox, tickets, and customers from the terminal.")}
|
|
677
|
+
|
|
678
|
+
${chalk2.bold.underline("Authentication")}
|
|
679
|
+
${chalk2.cyan("yektoo login")} Log in with email and password
|
|
680
|
+
${chalk2.cyan("yektoo login -e you@co.com -p pass")} Non-interactive login
|
|
681
|
+
${chalk2.cyan("yektoo logout")} Clear stored session
|
|
682
|
+
${chalk2.cyan("yektoo whoami")} Show current user and business
|
|
683
|
+
|
|
684
|
+
${chalk2.bold.underline("Accounts")}
|
|
685
|
+
${chalk2.cyan("yektoo email-accounts")} List connected IMAP / Outlook accounts
|
|
686
|
+
${chalk2.cyan("yektoo shopify-accounts")} List Shopify stores and linked email accounts
|
|
687
|
+
|
|
688
|
+
${chalk2.bold.underline("Inbox & Conversations")}
|
|
689
|
+
${chalk2.cyan("yektoo inbox")} List all conversations (newest first)
|
|
690
|
+
${chalk2.cyan("yektoo inbox --status open")} Only open tickets
|
|
691
|
+
${chalk2.cyan("yektoo inbox --store <id> --limit 10")} Filter by store, limit results
|
|
692
|
+
${chalk2.cyan('yektoo inbox --search "refund"')} Search by subject or sender
|
|
693
|
+
${chalk2.cyan("yektoo open-tickets --store <id>")} List open tickets for a specific email account
|
|
694
|
+
${chalk2.cyan("yektoo read <email-id>")} Read full thread (emails + replies + notes)
|
|
695
|
+
|
|
696
|
+
${chalk2.bold.underline("Actions")}
|
|
697
|
+
${chalk2.cyan('yektoo reply <email-id> "message"')} Send a reply (auto-routes IMAP vs Outlook)
|
|
698
|
+
${chalk2.cyan('yektoo reply-and-close <email-id> "msg"')} Reply and close the ticket in one step
|
|
699
|
+
${chalk2.cyan("yektoo close <email-id>")} Close/resolve a ticket (entire thread)
|
|
700
|
+
${chalk2.cyan("yektoo open <email-id>")} Reopen a closed ticket
|
|
701
|
+
${chalk2.cyan('yektoo note <email-id> "internal note"')} Add an internal note (not visible to customer)
|
|
702
|
+
|
|
703
|
+
${chalk2.bold.underline("Customer & Shopify")}
|
|
704
|
+
${chalk2.cyan("yektoo customer john@example.com")} Look up Shopify customer, orders, tracking
|
|
705
|
+
|
|
706
|
+
${chalk2.bold.underline("Templates")}
|
|
707
|
+
${chalk2.cyan("yektoo list-templates")} List all reply templates
|
|
708
|
+
${chalk2.cyan("yektoo get-template <id>")} View full template content and variables
|
|
709
|
+
|
|
710
|
+
${chalk2.bold.underline("Reports")}
|
|
711
|
+
${chalk2.cyan("yektoo csat")} CSAT survey report (last 30 days)
|
|
712
|
+
${chalk2.cyan("yektoo csat --days 90")} CSAT report for last 90 days
|
|
713
|
+
|
|
714
|
+
${chalk2.bold.underline("Options")}
|
|
715
|
+
${chalk2.cyan("--json")} Output as JSON (for piping to other tools / AI agents)
|
|
716
|
+
${chalk2.cyan("--help")} Show help for any command
|
|
717
|
+
|
|
718
|
+
${chalk2.bold.underline("Examples")}
|
|
719
|
+
${chalk2.gray("# View open tickets, then read and reply to one")}
|
|
720
|
+
${chalk2.cyan("yektoo inbox --status open")}
|
|
721
|
+
${chalk2.cyan("yektoo read abc123-def456")}
|
|
722
|
+
${chalk2.cyan('yektoo reply abc123-def456 "Your order has shipped!"')}
|
|
723
|
+
|
|
724
|
+
${chalk2.gray("# Look up a customer and close their ticket")}
|
|
725
|
+
${chalk2.cyan("yektoo customer jane@example.com")}
|
|
726
|
+
${chalk2.cyan('yektoo reply-and-close abc123 "Issue resolved. Your refund is on the way."')}
|
|
727
|
+
|
|
728
|
+
${chalk2.gray("# Pipe JSON to jq for scripting")}
|
|
729
|
+
${chalk2.cyan('yektoo --json inbox --status open | jq ".[].id"')}
|
|
730
|
+
`;
|
|
731
|
+
console.log(guide);
|
|
732
|
+
});
|
|
733
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yektoo/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Yekto CLI — manage your customer support helpdesk from the terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"yektoo": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"lint": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@supabase/supabase-js": "^2.49.0",
|
|
19
|
+
"commander": "^13.0.0",
|
|
20
|
+
"chalk": "^5.4.0",
|
|
21
|
+
"conf": "^13.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.0.0",
|
|
25
|
+
"tsup": "^8.0.0",
|
|
26
|
+
"tsx": "^4.0.0",
|
|
27
|
+
"typescript": "^5.7.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"yektoo",
|
|
34
|
+
"helpdesk",
|
|
35
|
+
"customer-support",
|
|
36
|
+
"cli",
|
|
37
|
+
"mcp"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|