Add docfast-support CLI tool for FreeScout ticket management
This commit is contained in:
parent
8b6712736a
commit
57fa33c6df
4 changed files with 288 additions and 14 deletions
225
bin/docfast-support
Executable file
225
bin/docfast-support
Executable file
|
|
@ -0,0 +1,225 @@
|
||||||
|
#!/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)
|
||||||
|
const tRes = await apiRequest('GET', `/conversations/${id}/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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
type: draft ? 'note' : 'message',
|
||||||
|
body: message,
|
||||||
|
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 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,
|
||||||
|
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)
|
||||||
|
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); });
|
||||||
|
|
@ -978,3 +978,37 @@
|
||||||
- **Budget:** €181.71 remaining, Revenue: €9
|
- **Budget:** €181.71 remaining, Revenue: €9
|
||||||
- **Open bugs:** 0 CRITICAL, 1 HIGH (BUG-049 — investor action needed), 5 MEDIUM, 3 LOW
|
- **Open bugs:** 0 CRITICAL, 1 HIGH (BUG-049 — investor action needed), 5 MEDIUM, 3 LOW
|
||||||
- **Blockers:** BUG-049 requires investor to enable Stripe invoice emails in Dashboard
|
- **Blockers:** BUG-049 requires investor to enable Stripe invoice emails in Dashboard
|
||||||
|
|
||||||
|
## Session 46 — 2026-02-16 19:41 UTC (Monday Evening — Subagent)
|
||||||
|
- **Server health:** UP, PostgreSQL 16.11, pool 15/15, container healthy ✅
|
||||||
|
- **Completed work (all deployed + verified on production):**
|
||||||
|
1. ✅ **STATUS PAGE** — Created styled /status page at https://docfast.dev/status
|
||||||
|
- Professional dark theme matching site design
|
||||||
|
- Shows: overall status indicator, database connectivity, PDF engine pool stats, uptime
|
||||||
|
- Auto-refreshes every 30 seconds, shows last checked timestamp
|
||||||
|
- External JS file (CSP-compliant, no inline scripts)
|
||||||
|
- Updated footer link from /health to /status on all pages
|
||||||
|
- Updated terms page link from /health to /status
|
||||||
|
- Raw /health JSON endpoint preserved for monitoring
|
||||||
|
- Commits: 6cc30db, 09c6feb pushed to Forgejo
|
||||||
|
2. ✅ **Audit #17 FIXED** — Duplicate session_id check on billing success
|
||||||
|
- Added in-memory Set tracking provisioned checkout sessions
|
||||||
|
- Returns 409 if same session_id is reused (prevents duplicate key creation)
|
||||||
|
- Covers both GET /billing/success and webhook handler
|
||||||
|
3. ✅ **Audit #14 FIXED** — Per-endpoint body size limits
|
||||||
|
- Conversion routes now limited to 500KB (was global 2MB)
|
||||||
|
- Prevents memory abuse via oversized HTML payloads
|
||||||
|
4. ✅ **Audit #22 FIXED** — Removed unused `getPoolStats` import from convert.ts
|
||||||
|
- **FreeScout check:** No FreeScout instance found on server (no container, no directory). FreeScout credentials not in docfast.env. Investor needs to provide FreeScout API URL + key, or set up the instance.
|
||||||
|
- **BUG-049:** Noted. Invoice toggle is a Stripe Dashboard setting. Requires investor action (Settings → Emails → enable invoice emails). Not blocking launch.
|
||||||
|
- **Investor Test:**
|
||||||
|
1. Trust with money? **Yes** ✅
|
||||||
|
2. Data loss? **Protected** ✅ — Local + off-site BorgBackup
|
||||||
|
3. Free tier abuse? **Mitigated** ✅
|
||||||
|
4. Key recovery? **Yes** ✅
|
||||||
|
5. False features? **Clean** ✅
|
||||||
|
- **Budget:** €181.71 remaining, Revenue: €9
|
||||||
|
- **Open bugs:** 0 CRITICAL, 1 HIGH (BUG-049 — investor action), 3 MEDIUM (#10, #12, #15), 2 LOW (#18, #25)
|
||||||
|
- **Blockers:**
|
||||||
|
- BUG-049: Investor needs to enable Stripe invoice emails
|
||||||
|
- FreeScout: No instance running, need API access or setup instructions
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"phaseLabel": "Build Production-Grade Product",
|
"phaseLabel": "Build Production-Grade Product",
|
||||||
"status": "near-launch-ready",
|
"status": "near-launch-ready",
|
||||||
"product": "DocFast \u2014 HTML/Markdown to PDF API",
|
"product": "DocFast \u2014 HTML/Markdown to PDF API",
|
||||||
"currentPriority": "1) BUG-049 invoice fix (investor action). 2) Marketing launch prep. 3) Remaining MEDIUM audit items.",
|
"currentPriority": "1) BUG-049 invoice fix (investor action). 2) FreeScout setup needed. 3) Remaining MEDIUM audit items (#10, #12, #15). 4) Marketing launch prep.",
|
||||||
"ownerDirectives_PRIORITY": "Process these IN ORDER. Do not skip.",
|
"ownerDirectives_PRIORITY": "Process these IN ORDER. Do not skip.",
|
||||||
"ownerDirectives": [
|
"ownerDirectives": [
|
||||||
"Stripe: owner has existing Stripe account from another project \u2014 use same account, just create separate Product + webhook endpoint for DocFast.",
|
"Stripe: owner has existing Stripe account from another project \u2014 use same account, just create separate Product + webhook endpoint for DocFast.",
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"CI/CD PIPELINE: Forgejo Actions workflow created. Needs 3 repository secrets added in Forgejo settings (SERVER_HOST, SERVER_USER, SSH_PRIVATE_KEY).",
|
"CI/CD PIPELINE: Forgejo Actions workflow created. Needs 3 repository secrets added in Forgejo settings (SERVER_HOST, SERVER_USER, SSH_PRIVATE_KEY).",
|
||||||
"REPRODUCIBLE INFRASTRUCTURE: DONE \u2014 setup.sh, docker-compose, configs, disaster recovery docs all in infrastructure/ directory.",
|
"REPRODUCIBLE INFRASTRUCTURE: DONE \u2014 setup.sh, docker-compose, configs, disaster recovery docs all in infrastructure/ directory.",
|
||||||
"PRO PLAN LIMITS: DONE \u2014 Set to 2,500 PDFs/month at \u20ac9/mo. Competitive with html2pdf.app. Enforced in code, updated on landing page + JSON-LD + Stripe.",
|
"PRO PLAN LIMITS: DONE \u2014 Set to 2,500 PDFs/month at \u20ac9/mo. Competitive with html2pdf.app. Enforced in code, updated on landing page + JSON-LD + Stripe.",
|
||||||
"STATUS PAGE: The health link on the website currently points to the raw API /health endpoint which returns JSON — unprofessional. Create a proper /status page with a nice UI showing service status, uptime, response time, etc. Keep the raw /health API endpoint for monitoring, but the public-facing link should be a styled status page.",
|
"STATUS PAGE: The health link on the website currently points to the raw API /health endpoint which returns JSON \u2014 unprofessional. Create a proper /status page with a nice UI showing service status, uptime, response time, etc. Keep the raw /health API endpoint for monitoring, but the public-facing link should be a styled status page.",
|
||||||
"SUPPORT EMAIL LIVE: support@docfast.dev is now active in FreeScout. The CEO can spawn a support agent that accesses FreeScout via API to handle customer inquiries. Update the website contact/support references to use this address.",
|
"SUPPORT EMAIL LIVE: support@docfast.dev is now active in FreeScout. The CEO can spawn a support agent that accesses FreeScout via API to handle customer inquiries. Update the website contact/support references to use this address.",
|
||||||
"BUG-049 HIGH: Pro customers do not receive an invoice after payment. This is legally required in Austria/EU. Stripe can auto-generate invoices for subscriptions \u2014 enable Stripe Invoicing or implement invoice generation. Customer must receive a proper invoice with: company name, ATU number, invoice number, date, amount, VAT breakdown.",
|
"BUG-049 HIGH: Pro customers do not receive an invoice after payment. This is legally required in Austria/EU. Stripe can auto-generate invoices for subscriptions \u2014 enable Stripe Invoicing or implement invoice generation. Customer must receive a proper invoice with: company name, ATU number, invoice number, date, amount, VAT breakdown.",
|
||||||
"WEBSITE TEMPLATING: DONE \u2014 Build-time system with partials (nav/footer/styles). Source in public/src/, build with node scripts/build-html.cjs."
|
"WEBSITE TEMPLATING: DONE \u2014 Build-time system with partials (nav/footer/styles). Source in public/src/, build with node scripts/build-html.cjs."
|
||||||
|
|
@ -51,7 +51,9 @@
|
||||||
"websiteTemplating": true,
|
"websiteTemplating": true,
|
||||||
"websiteTemplatingNote": "Build-time HTML templating with shared nav/footer partials. npm run build:pages",
|
"websiteTemplatingNote": "Build-time HTML templating with shared nav/footer partials. npm run build:pages",
|
||||||
"supportEmailLive": true,
|
"supportEmailLive": true,
|
||||||
"supportEmailNote": "support@docfast.dev on footer, impressum, terms, openapi.json, landing page"
|
"supportEmailNote": "support@docfast.dev on footer, impressum, terms, openapi.json, landing page",
|
||||||
|
"statusPage": true,
|
||||||
|
"statusPageNote": "Styled /status page live at https://docfast.dev/status. Auto-refreshes, shows DB + pool stats."
|
||||||
},
|
},
|
||||||
"loadTestResults": {
|
"loadTestResults": {
|
||||||
"sequential": "~2.1s per PDF, ~28/min",
|
"sequential": "~2.1s per PDF, ~28/min",
|
||||||
|
|
@ -99,16 +101,13 @@
|
||||||
"MEDIUM": [
|
"MEDIUM": [
|
||||||
"Audit #10: Usage data written on every request (should batch)",
|
"Audit #10: Usage data written on every request (should batch)",
|
||||||
"Audit #12: In-memory caches can diverge from DB",
|
"Audit #12: In-memory caches can diverge from DB",
|
||||||
"Audit #14: No per-endpoint body size limits",
|
"Audit #15: Browser pool queue no per-key fairness"
|
||||||
"Audit #15: Browser pool queue no per-key fairness",
|
|
||||||
"Audit #17: No duplicate session_id check on billing success"
|
|
||||||
],
|
],
|
||||||
"LOW": [
|
"LOW": [
|
||||||
"Audit #18: Rate limit store potential memory growth",
|
"Audit #18: Rate limit store potential memory growth",
|
||||||
"Audit #22: Unused import in convert.ts",
|
|
||||||
"Audit #25: Inconsistent error response shapes"
|
"Audit #25: Inconsistent error response shapes"
|
||||||
],
|
],
|
||||||
"note": "Session 45: Fixed audit #3 (Critical), #6, #7, #11 (HIGH). Added support@docfast.dev to all pages."
|
"note": "Session 46: Fixed audit #14, #17, #22. Added styled /status page. 3 MEDIUM, 2 LOW remaining."
|
||||||
},
|
},
|
||||||
"blockers": [],
|
"blockers": [],
|
||||||
"resolvedBlockers": [
|
"resolvedBlockers": [
|
||||||
|
|
@ -117,5 +116,5 @@
|
||||||
"Off-site backups \u2014 DONE 2026-02-16, Hetzner Storage Box configured with BorgBackup"
|
"Off-site backups \u2014 DONE 2026-02-16, Hetzner Storage Box configured with BorgBackup"
|
||||||
],
|
],
|
||||||
"startDate": "2026-02-14",
|
"startDate": "2026-02-14",
|
||||||
"sessionCount": 45
|
"sessionCount": 46
|
||||||
}
|
}
|
||||||
|
|
@ -106,11 +106,27 @@ Report EVERY issue to projects/business/memory/bugs.md
|
||||||
|
|
||||||
## Support Monitoring
|
## Support Monitoring
|
||||||
|
|
||||||
Every session, spawn a support agent to check FreeScout for new support requests at support@docfast.dev. The agent should:
|
Every session, spawn a support agent to check FreeScout for new support requests at support@docfast.dev.
|
||||||
1. Check for unread/open tickets
|
|
||||||
2. Triage and respond to simple inquiries autonomously (API usage questions, docs links, etc.)
|
**Support tool:** `~/.openclaw/workspace/bin/docfast-support`
|
||||||
3. Escalate complex issues (billing disputes, bugs, feature requests) to the CEO for decision
|
```bash
|
||||||
4. Log all interactions in `projects/business/memory/support-log.md`
|
docfast-support tickets # List open tickets
|
||||||
|
docfast-support tickets --status pending # List pending tickets
|
||||||
|
docfast-support view <ticket-id> # View ticket + all messages
|
||||||
|
docfast-support reply --ticket <id> --message "..." # Send reply to customer
|
||||||
|
docfast-support reply --ticket <id> --message "..." --draft # Save as internal note
|
||||||
|
docfast-support reply --ticket <id> --message "..." --status closed # Reply + close
|
||||||
|
docfast-support close <ticket-id> # Close ticket
|
||||||
|
docfast-support stats # Ticket counts by status
|
||||||
|
docfast-support mailboxes # List mailboxes
|
||||||
|
```
|
||||||
|
|
||||||
|
The support agent should:
|
||||||
|
1. Run `docfast-support tickets` to check for open tickets
|
||||||
|
2. `docfast-support view <id>` to read the full conversation
|
||||||
|
3. Triage and respond to simple inquiries autonomously (API usage, docs, how-to)
|
||||||
|
4. For complex issues (billing, bugs, feature requests): create a draft note with analysis, then escalate to CEO
|
||||||
|
5. Log all interactions in `projects/business/memory/support-log.md`
|
||||||
|
|
||||||
Never let support tickets go unanswered. Response time matters for customer trust.
|
Never let support tickets go unanswered. Response time matters for customer trust.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue