273 lines
10 KiB
JavaScript
Executable file
273 lines
10 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
// DocFast Support CLI — FreeScout API client for support ticket management
|
|
// Env vars (in .credentials/docfast.env):
|
|
// FREESCOUT_URL — FreeScout instance URL (e.g. https://support.cloonar.com)
|
|
// FREESCOUT_API_KEY — API key (FreeScout → Manage → API Keys)
|
|
// FREESCOUT_MAILBOX_ID — Mailbox ID for support@docfast.dev
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const https = require('https');
|
|
const http = require('http');
|
|
|
|
const credFile = process.env.CRED_FILE || path.join(__dirname, '..', '.credentials', 'docfast.env');
|
|
const env = {};
|
|
if (fs.existsSync(credFile)) {
|
|
fs.readFileSync(credFile, 'utf8').split('\n').forEach(l => {
|
|
const m = l.match(/^([A-Z_]+)=(.+)$/);
|
|
if (m) env[m[1]] = m[2].trim();
|
|
});
|
|
}
|
|
|
|
const BASE_URL = env.FREESCOUT_URL;
|
|
const API_KEY = env.FREESCOUT_API_KEY;
|
|
const MAILBOX_ID = env.FREESCOUT_MAILBOX_ID;
|
|
|
|
if (!BASE_URL || !API_KEY) {
|
|
console.error('Missing FREESCOUT_URL or FREESCOUT_API_KEY in credentials.');
|
|
process.exit(1);
|
|
}
|
|
|
|
// --- HTTP helper ---
|
|
function apiRequest(method, endpoint, body = null) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = new URL(`/api${endpoint}`, BASE_URL);
|
|
const mod = url.protocol === 'https:' ? https : http;
|
|
const opts = {
|
|
method,
|
|
hostname: url.hostname,
|
|
port: url.port,
|
|
path: url.pathname + url.search,
|
|
headers: {
|
|
'X-FreeScout-API-Key': API_KEY,
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
},
|
|
};
|
|
const req = mod.request(opts, res => {
|
|
let data = '';
|
|
res.on('data', c => data += c);
|
|
res.on('end', () => {
|
|
try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
|
|
catch { resolve({ status: res.statusCode, data }); }
|
|
});
|
|
});
|
|
req.on('error', reject);
|
|
if (body) req.write(JSON.stringify(body));
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// --- Commands ---
|
|
|
|
async function cmdTickets(args) {
|
|
const status = getFlag(args, '--status') || 'active'; // active|pending|closed|spam
|
|
const page = getFlag(args, '--page') || '1';
|
|
let endpoint = `/conversations?page=${page}`;
|
|
if (MAILBOX_ID) endpoint += `&mailboxId=${MAILBOX_ID}`;
|
|
if (status) endpoint += `&status=${status}`;
|
|
|
|
const res = await apiRequest('GET', endpoint);
|
|
if (res.status !== 200) { console.error('Error:', res.status, JSON.stringify(res.data)); process.exit(1); }
|
|
|
|
const conversations = res.data._embedded?.conversations || [];
|
|
if (conversations.length === 0) { console.log('No tickets found.'); return; }
|
|
|
|
console.log('ID\tSTATUS\tSUBJECT\tCUSTOMER\tCREATED\tTHREADS');
|
|
for (const c of conversations) {
|
|
const customer = c.customer?.email || c.customer?.firstName || 'unknown';
|
|
const created = c.createdAt?.split('T')[0] || '-';
|
|
console.log(`${c.id}\t${c.status}\t${c.subject}\t${customer}\t${created}\t${c.threads || '-'}`);
|
|
}
|
|
if (res.data.page?.totalPages > 1) {
|
|
console.log(`\nPage ${res.data.page.current}/${res.data.page.totalPages} (${res.data.page.total} total)`);
|
|
}
|
|
}
|
|
|
|
async function cmdView(args) {
|
|
const id = args[0];
|
|
if (!id) { console.error('Usage: docfast-support view <ticket-id>'); process.exit(1); }
|
|
|
|
const res = await apiRequest('GET', `/conversations/${id}`);
|
|
if (res.status !== 200) { console.error('Error:', res.status, JSON.stringify(res.data)); process.exit(1); }
|
|
|
|
const c = res.data;
|
|
console.log(`Ticket #${c.id}: ${c.subject}`);
|
|
console.log(`Status: ${c.status} | Customer: ${c.customer?.email || 'unknown'} | Created: ${c.createdAt}`);
|
|
console.log('---');
|
|
|
|
// Get threads (messages) — must use embed=threads on conversation endpoint
|
|
const tRes = await apiRequest('GET', `/conversations/${id}?embed=threads`);
|
|
if (tRes.status === 200) {
|
|
const threads = tRes.data._embedded?.threads || [];
|
|
for (const t of threads) {
|
|
const from = t.createdBy?.email || t.customer?.email || 'system';
|
|
const type = t.type || 'message'; // customer|message|note
|
|
const date = t.createdAt?.replace('T', ' ').slice(0, 16) || '-';
|
|
console.log(`\n[${date}] ${type.toUpperCase()} from ${from}:`);
|
|
// Strip HTML tags for readability
|
|
const body = (t.body || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').trim();
|
|
console.log(body);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function cmdReply(args) {
|
|
const id = getFlag(args, '--ticket');
|
|
const message = getFlag(args, '--message');
|
|
const draft = args.includes('--draft');
|
|
const status = getFlag(args, '--status'); // active|pending|closed
|
|
|
|
if (!id || !message) {
|
|
console.error('Usage: docfast-support reply --ticket <id> --message "..." [--draft] [--status active|pending|closed]');
|
|
process.exit(1);
|
|
}
|
|
|
|
// FreeScout body field is HTML — convert plain text newlines to <br> tags
|
|
const htmlBody = message.replace(/\n/g, '<br>\n');
|
|
const body = {
|
|
type: draft ? 'note' : 'message',
|
|
text: message,
|
|
body: htmlBody,
|
|
user: 6,
|
|
status: status || (draft ? undefined : 'active'),
|
|
};
|
|
|
|
// If draft, create as internal note (visible to agents only)
|
|
if (draft) {
|
|
body.type = 'note';
|
|
}
|
|
|
|
const res = await apiRequest('POST', `/conversations/${id}/threads`, body);
|
|
if (res.status === 201 || res.status === 200) {
|
|
console.log(draft ? `Draft note added to ticket #${id}` : `Reply sent to ticket #${id}`);
|
|
if (status) console.log(`Status set to: ${status}`);
|
|
} else {
|
|
console.error('Error:', res.status, JSON.stringify(res.data));
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function cmdNeedsReply(args) {
|
|
// Show only tickets assigned to the AI agent (franz.hubert@docfast.dev) where the last non-note thread is from a customer
|
|
const AI_AGENT_EMAIL = 'franz.hubert@docfast.dev';
|
|
const page = getFlag(args, '--page') || '1';
|
|
let endpoint = `/conversations?page=${page}&status=active`;
|
|
if (MAILBOX_ID) endpoint += `&mailboxId=${MAILBOX_ID}`;
|
|
|
|
const res = await apiRequest('GET', endpoint);
|
|
if (res.status !== 200) { console.error('Error:', res.status, JSON.stringify(res.data)); process.exit(1); }
|
|
|
|
const conversations = res.data._embedded?.conversations || [];
|
|
if (conversations.length === 0) { console.log('No tickets need a reply.'); return; }
|
|
|
|
const needsReply = [];
|
|
for (const c of conversations) {
|
|
// Only tickets assigned to the AI agent or unassigned
|
|
const assignee = c.assignee?.email || '';
|
|
if (assignee && assignee.toLowerCase() !== AI_AGENT_EMAIL.toLowerCase()) continue;
|
|
|
|
const tRes = await apiRequest('GET', `/conversations/${c.id}?embed=threads`);
|
|
if (tRes.status !== 200) { console.error(` Ticket ${c.id}: API returned ${tRes.status}`); continue; }
|
|
const threads = (tRes.data._embedded?.threads || [])
|
|
.filter(t => t.type !== 'note' && t.type !== 'lineitem');
|
|
if (threads.length === 0) { continue; }
|
|
const lastThread = threads[0]; // FreeScout returns threads in reverse chronological order (newest first)
|
|
// type 'customer' = from customer, 'message' = from agent
|
|
if (lastThread.type === 'customer') {
|
|
needsReply.push(c);
|
|
}
|
|
}
|
|
|
|
if (needsReply.length === 0) { console.log('No tickets need a reply.'); return; }
|
|
|
|
console.log('ID\tSUBJECT\tCUSTOMER\tCREATED');
|
|
for (const c of needsReply) {
|
|
const customer = c.customer?.email || c.customer?.firstName || 'unknown';
|
|
const created = c.createdAt?.split('T')[0] || '-';
|
|
console.log(`${c.id}\t${c.subject}\t${customer}\t${created}`);
|
|
}
|
|
console.log(`\n${needsReply.length} ticket(s) need a reply.`);
|
|
}
|
|
|
|
async function cmdClose(args) {
|
|
const id = args[0];
|
|
if (!id) { console.error('Usage: docfast-support close <ticket-id>'); process.exit(1); }
|
|
|
|
const res = await apiRequest('PUT', `/conversations/${id}`, { status: 'closed' });
|
|
if (res.status === 200 || res.status === 204) {
|
|
console.log(`Ticket #${id} closed.`);
|
|
} else {
|
|
console.error('Error:', res.status, JSON.stringify(res.data));
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function cmdStats() {
|
|
const statuses = ['active', 'pending', 'closed'];
|
|
console.log('STATUS\tCOUNT');
|
|
for (const s of statuses) {
|
|
let endpoint = `/conversations?status=${s}&page=1`;
|
|
if (MAILBOX_ID) endpoint += `&mailboxId=${MAILBOX_ID}`;
|
|
const res = await apiRequest('GET', endpoint);
|
|
const total = res.data?.page?.total || 0;
|
|
console.log(`${s}\t${total}`);
|
|
}
|
|
}
|
|
|
|
async function cmdMailboxes() {
|
|
const res = await apiRequest('GET', '/mailboxes');
|
|
if (res.status !== 200) { console.error('Error:', res.status, JSON.stringify(res.data)); process.exit(1); }
|
|
const mailboxes = res.data._embedded?.mailboxes || [];
|
|
console.log('ID\tNAME\tEMAIL');
|
|
for (const m of mailboxes) {
|
|
console.log(`${m.id}\t${m.name}\t${m.email || '-'}`);
|
|
}
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
function getFlag(args, flag) {
|
|
const idx = args.indexOf(flag);
|
|
if (idx === -1) return null;
|
|
return args[idx + 1] || null;
|
|
}
|
|
|
|
// --- Main ---
|
|
|
|
const [cmd, ...args] = process.argv.slice(2);
|
|
|
|
const commands = {
|
|
tickets: cmdTickets,
|
|
'needs-reply': cmdNeedsReply,
|
|
view: cmdView,
|
|
reply: cmdReply,
|
|
close: cmdClose,
|
|
stats: cmdStats,
|
|
mailboxes: cmdMailboxes,
|
|
};
|
|
|
|
if (!cmd || !commands[cmd]) {
|
|
console.log(`DocFast Support — FreeScout ticket management
|
|
|
|
Usage: docfast-support <command> [args]
|
|
|
|
Commands:
|
|
tickets [--status active|pending|closed|spam] List tickets (default: active)
|
|
needs-reply Tickets waiting for agent reply
|
|
view <ticket-id> View ticket with all messages
|
|
reply --ticket <id> --message "..." Send reply to customer
|
|
[--draft] Save as internal note instead
|
|
[--status active|pending|closed] Set ticket status after reply
|
|
close <ticket-id> Close a ticket
|
|
stats Ticket counts by status
|
|
mailboxes List mailboxes (find your mailbox ID)
|
|
|
|
Env vars (in .credentials/docfast.env):
|
|
FREESCOUT_URL FreeScout instance URL
|
|
FREESCOUT_API_KEY API key
|
|
FREESCOUT_MAILBOX_ID Mailbox ID for filtering (optional)`);
|
|
process.exit(0);
|
|
}
|
|
|
|
commands[cmd](args).catch(e => { console.error(e.message); process.exit(1); });
|