Initial commit
This commit is contained in:
240
skills/freshservice-manager/SKILL.md
Normal file
240
skills/freshservice-manager/SKILL.md
Normal file
@@ -0,0 +1,240 @@
|
||||
---
|
||||
name: freshservice-manager
|
||||
description: Manage Freshservice tickets, approvals, and get team performance reports across all workspaces
|
||||
triggers:
|
||||
- "freshservice"
|
||||
- "ticket"
|
||||
- "helpdesk"
|
||||
- "what happened in freshservice"
|
||||
- "tech team metrics"
|
||||
- "daily summary"
|
||||
- "weekly summary"
|
||||
- "approvals"
|
||||
- "assign to"
|
||||
- "add note to ticket"
|
||||
- "close ticket"
|
||||
allowed-tools: Read, Bash
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Freshservice Manager Skill
|
||||
|
||||
## Configuration
|
||||
|
||||
- **Domain**: psd401.freshservice.com
|
||||
- **Agent ID**: 6000130414 (Kris Hagel)
|
||||
- **Primary Workspace**: 2 (Technology)
|
||||
- **API Key**: Stored in `~/Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env`
|
||||
|
||||
## Workspaces
|
||||
|
||||
| ID | Name |
|
||||
|----|------|
|
||||
| 2 | Technology (primary) |
|
||||
| 3 | Employee Support Services |
|
||||
| 4 | Business Services |
|
||||
| 5 | Teaching & Learning |
|
||||
| 6 | Maintenance |
|
||||
| 8 | Investigations |
|
||||
| 9 | Transportation |
|
||||
| 10 | Safety & Security |
|
||||
| 11 | Communications |
|
||||
| 13 | Software Development |
|
||||
|
||||
## Team Context
|
||||
|
||||
- **TSD Generic Account** (6000875582) - Shared by high school interns for Chromebook repairs
|
||||
- **David Edwards** - Desktop Support Tech, handles most varied workload including incidents
|
||||
- **Carol Winget** - Student Database Admin, PowerSchool specialist
|
||||
- **Laura Durkin** - Admin Secretary, handles new students and badges
|
||||
|
||||
## Reports & Summaries
|
||||
|
||||
### Daily Summary
|
||||
Get a narrative summary of what happened in Technology on a specific day.
|
||||
|
||||
**Natural language triggers:**
|
||||
- "What happened in Freshservice yesterday?"
|
||||
- "Give me today's tech summary"
|
||||
- "What did the team do on Wednesday?"
|
||||
|
||||
**Script:** `bun get_daily_summary.js [date]`
|
||||
|
||||
Date options:
|
||||
- `today` (default)
|
||||
- `yesterday`
|
||||
- Day names: `monday`, `tuesday`, `wednesday`, etc.
|
||||
- `last wednesday`, `last friday`
|
||||
- Specific date: `2025-11-20`
|
||||
|
||||
**Output includes:**
|
||||
- Total tickets closed
|
||||
- Breakdown by category (Chromebook, Schoology, Security Alert, etc.)
|
||||
- Breakdown by agent with their tickets
|
||||
- Automated ticket count (password resets)
|
||||
|
||||
### Weekly Summary
|
||||
Get trends and metrics for the entire week.
|
||||
|
||||
**Natural language triggers:**
|
||||
- "Weekly tech summary"
|
||||
- "How did the team do this week?"
|
||||
- "Give me the weekly Freshservice report"
|
||||
|
||||
**Script:** `bun get_weekly_summary.js [weeks_ago]`
|
||||
|
||||
Options:
|
||||
- `0` = this week (default)
|
||||
- `1` = last week
|
||||
- `2` = two weeks ago
|
||||
|
||||
**Output includes:**
|
||||
- Total closed and daily average
|
||||
- Peak day and slow day
|
||||
- Daily trend by volume
|
||||
- Category breakdown with percentages
|
||||
- Category trends (which days had spikes)
|
||||
- Top agents with ticket counts and focus areas
|
||||
- Agent daily breakdown
|
||||
|
||||
### Narrative Style
|
||||
|
||||
When presenting summaries, write a 1-minute narrative that:
|
||||
- Highlights the main story of the day/week (outages, big pushes, etc.)
|
||||
- Calls out specific people and what they handled
|
||||
- Notes any concerning patterns (security alerts, cut wires, etc.)
|
||||
- Converts UTC timestamps to Pacific time
|
||||
- Uses specific numbers and ticket counts
|
||||
|
||||
## Ticket Operations
|
||||
|
||||
### List Tickets
|
||||
```bash
|
||||
bun list_tickets.js '{"workspace_id": 2, "filter": "new_and_my_open"}'
|
||||
```
|
||||
|
||||
Filters: `new_and_my_open`, `watching`, `spam`, `deleted`, `archived`
|
||||
|
||||
### Search Tickets
|
||||
```bash
|
||||
bun search_tickets.js "status:2 AND priority:3" 2
|
||||
```
|
||||
|
||||
Query syntax: `field:value AND/OR field:value`
|
||||
Fields: `status`, `priority`, `agent_id`, `group_id`, `created_at`, `updated_at`
|
||||
|
||||
### Get Ticket Details
|
||||
```bash
|
||||
bun get_ticket.js <ticket_id>
|
||||
```
|
||||
|
||||
### Get Service Request (with form data)
|
||||
```bash
|
||||
bun get_service_request.js <ticket_id>
|
||||
```
|
||||
|
||||
Includes requester info, custom form fields, requested items.
|
||||
|
||||
### Create Ticket
|
||||
```bash
|
||||
bun create_ticket.js '<json>'
|
||||
```
|
||||
|
||||
Required: `subject`, `description`, `email` or `requester_id`
|
||||
Optional: `priority`, `status`, `workspace_id`
|
||||
|
||||
### Update Ticket
|
||||
```bash
|
||||
bun update_ticket.js <ticket_id> '<json>'
|
||||
```
|
||||
|
||||
Can update: `status`, `priority`, `responder_id`, `group_id`
|
||||
|
||||
### Add Note
|
||||
```bash
|
||||
bun add_note.js <ticket_id> '{"body": "Note text", "private": true}'
|
||||
```
|
||||
|
||||
Optional: `notify_emails` array to alert specific agents.
|
||||
|
||||
## Agent Operations
|
||||
|
||||
### List Agents
|
||||
```bash
|
||||
bun list_agents.js [query]
|
||||
```
|
||||
|
||||
Query filters by first name, last name, or email.
|
||||
Returns: id, name, email, job_title
|
||||
|
||||
Use this to resolve "assign to Mark" → find Mark's agent ID → update ticket.
|
||||
|
||||
### Get Agent by Email
|
||||
```bash
|
||||
bun get_agent.js <email>
|
||||
```
|
||||
|
||||
## Approvals
|
||||
|
||||
### Get Pending Approvals
|
||||
```bash
|
||||
bun get_approvals.js [status]
|
||||
```
|
||||
|
||||
Status: `requested` (default), `approved`, `rejected`, `cancelled`
|
||||
|
||||
**Note:** Freshservice API does not support approving service requests programmatically. User must approve via:
|
||||
- Web UI: `https://psd401.freshservice.com/helpdesk/tickets/<id>`
|
||||
- Email reply to approval request
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### "Add a note to Jodi on ticket 151501"
|
||||
1. Find Jodi's agent ID: `bun list_agents.js jodi` → 6000542935
|
||||
2. Add note: `bun add_note.js 151501 '{"body": "...", "notify_emails": ["miloj@psd401.net"]}'`
|
||||
|
||||
### "Assign ticket to Mark"
|
||||
1. Find Mark's ID: `bun list_agents.js mark`
|
||||
2. Update ticket: `bun update_ticket.js <id> '{"responder_id": <mark_id>}'`
|
||||
|
||||
### "What approvals do I have?"
|
||||
1. Get approvals: `bun get_approvals.js`
|
||||
2. For each approval, get details: `bun get_service_request.js <ticket_id>`
|
||||
|
||||
### Cross-skill workflow
|
||||
"Add note to ticket, create OmniFocus task, reassign ticket" - can combine Freshservice note + OmniFocus task creation + ticket update in one flow.
|
||||
|
||||
## Category Detection
|
||||
|
||||
Tickets are auto-categorized by subject keywords:
|
||||
- **Password Reset**: "password reset"
|
||||
- **Security Alert**: "security alert", "compromised", "breach"
|
||||
- **Schoology**: "schoology"
|
||||
- **PowerSchool**: "powerschool"
|
||||
- **Promethean Board**: "promethean"
|
||||
- **Chromebook**: "chromebook"
|
||||
- **Phone/Voicemail**: "phone", "voicemail", "ext."
|
||||
- **Badge Request**: "badge"
|
||||
- **New Student**: "new student", "enrollee"
|
||||
- **Intercom**: "intercom"
|
||||
- **Raptor**: "raptor"
|
||||
- **GoGuardian**: "goguardian", "go guardian"
|
||||
- **Access/Login**: "login", "access", "mfa"
|
||||
|
||||
## Status Codes
|
||||
|
||||
| Code | Status |
|
||||
|------|--------|
|
||||
| 2 | Open |
|
||||
| 3 | Pending |
|
||||
| 4 | Resolved |
|
||||
| 5 | Closed |
|
||||
|
||||
## Priority Codes
|
||||
|
||||
| Code | Priority |
|
||||
|------|----------|
|
||||
| 1 | Low |
|
||||
| 2 | Medium |
|
||||
| 3 | High |
|
||||
| 4 | Urgent |
|
||||
81
skills/freshservice-manager/scripts/add_note.js
Normal file
81
skills/freshservice-manager/scripts/add_note.js
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// Add a note to a ticket
|
||||
// Usage: bun add_note.js <ticket_id> '<json>'
|
||||
// JSON: {"body": "Note text", "private": true, "notify_emails": ["email@example.com"]}
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
const ticketId = process.argv[2];
|
||||
let noteData;
|
||||
|
||||
if (!ticketId) {
|
||||
console.error(JSON.stringify({ error: 'Ticket ID required' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
noteData = JSON.parse(process.argv[3]);
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: 'Invalid JSON. Required: {"body": "note text"}' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!noteData.body) {
|
||||
console.error(JSON.stringify({ error: 'Note body is required' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Default to private note
|
||||
if (noteData.private === undefined) {
|
||||
noteData.private = true;
|
||||
}
|
||||
|
||||
async function addNote(ticketId, data) {
|
||||
const response = await fetch(`${baseUrl}/tickets/${ticketId}/notes`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return { error: `API error ${response.status}: ${error}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await addNote(ticketId, noteData);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
76
skills/freshservice-manager/scripts/create_ticket.js
Normal file
76
skills/freshservice-manager/scripts/create_ticket.js
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// Create a new ticket
|
||||
// Usage: bun create_ticket.js '<json>'
|
||||
// JSON: {"subject": "...", "description": "...", "email": "requester@email.com", "priority": 2, "status": 2, "workspace_id": 2}
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
// Status values: 2=Open, 3=Pending, 4=Resolved, 5=Closed
|
||||
// Priority values: 1=Low, 2=Medium, 3=High, 4=Urgent
|
||||
|
||||
let ticketData;
|
||||
try {
|
||||
ticketData = JSON.parse(process.argv[2]);
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: 'Invalid JSON. Required: subject, description, email' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!ticketData.subject || !ticketData.description || !ticketData.email) {
|
||||
console.error(JSON.stringify({ error: 'Required fields: subject, description, email' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
ticketData.status = ticketData.status || 2; // Open
|
||||
ticketData.priority = ticketData.priority || 2; // Medium
|
||||
|
||||
async function createTicket(data) {
|
||||
const response = await fetch(`${baseUrl}/tickets`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return { error: `API error ${response.status}: ${error}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createTicket(ticketData);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
67
skills/freshservice-manager/scripts/get_agent.js
Normal file
67
skills/freshservice-manager/scripts/get_agent.js
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// Get agent info by email
|
||||
// Usage: bun get_agent.js [email]
|
||||
// If no email provided, returns current agent (API key owner)
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
// Load environment from iCloud secrets
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
|
||||
if (!domain || !apiKey) {
|
||||
console.error(JSON.stringify({ error: 'Missing FRESHSERVICE_DOMAIN or FRESHSERVICE_API_KEY in .env' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const email = process.argv[2];
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
async function getAgents(email) {
|
||||
let url = `${baseUrl}/agents`;
|
||||
if (email) {
|
||||
url += `?email=${encodeURIComponent(email)}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return { error: `API error ${response.status}: ${error}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await getAgents(email);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
94
skills/freshservice-manager/scripts/get_approvals.js
Normal file
94
skills/freshservice-manager/scripts/get_approvals.js
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// Get approvals for the current agent
|
||||
// Usage: bun get_approvals.js [status]
|
||||
// Status: requested (pending), approved, rejected, cancelled
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
// Agent ID for Kris Hagel
|
||||
const agentId = 6000130414;
|
||||
const status = process.argv[2] || 'requested';
|
||||
|
||||
async function getApprovals(agentId, status, parent) {
|
||||
const url = `${baseUrl}/approvals?approver_id=${agentId}&status=${status}&parent=${parent}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return { error: `API error ${response.status}: ${error}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Simplify output
|
||||
const approvals = data.approvals.map(a => ({
|
||||
id: a.id,
|
||||
approval_type: a.approval_type,
|
||||
approver_id: a.approver_id,
|
||||
status: a.status,
|
||||
created_at: a.created_at,
|
||||
updated_at: a.updated_at,
|
||||
approvable_id: a.approvable_id,
|
||||
approvable_type: a.approvable_type,
|
||||
delegator: a.delegator,
|
||||
latest_remark: a.latest_remark
|
||||
}));
|
||||
|
||||
return {
|
||||
count: approvals.length,
|
||||
status: status,
|
||||
approvals
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Check both tickets and changes for approvals
|
||||
const ticketApprovals = await getApprovals(agentId, status, 'ticket');
|
||||
const changeApprovals = await getApprovals(agentId, status, 'change');
|
||||
|
||||
const allApprovals = [];
|
||||
if (!ticketApprovals.error) {
|
||||
allApprovals.push(...ticketApprovals.approvals.map(a => ({...a, parent_type: 'ticket'})));
|
||||
}
|
||||
if (!changeApprovals.error) {
|
||||
allApprovals.push(...changeApprovals.approvals.map(a => ({...a, parent_type: 'change'})));
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
count: allApprovals.length,
|
||||
status: status,
|
||||
approvals: allApprovals
|
||||
}, null, 2));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
285
skills/freshservice-manager/scripts/get_daily_summary.js
Normal file
285
skills/freshservice-manager/scripts/get_daily_summary.js
Normal file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// Get daily ticket summary for Technology workspace
|
||||
// Usage: bun get_daily_summary.js [date]
|
||||
// Date: "yesterday", "today", or specific date like "2025-11-20"
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
// Parse date argument
|
||||
function parseDate(dateArg) {
|
||||
const now = new Date();
|
||||
|
||||
if (!dateArg || dateArg === 'today') {
|
||||
return {
|
||||
start: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0),
|
||||
end: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59),
|
||||
label: 'today'
|
||||
};
|
||||
}
|
||||
|
||||
if (dateArg === 'yesterday') {
|
||||
const yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return {
|
||||
start: new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 0, 0, 0),
|
||||
end: new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 23, 59, 59),
|
||||
label: 'yesterday'
|
||||
};
|
||||
}
|
||||
|
||||
// Handle "last <day>" format (e.g., "last wednesday", "last friday")
|
||||
const dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
||||
const lower = dateArg.toLowerCase();
|
||||
|
||||
if (lower.startsWith('last ')) {
|
||||
const dayName = lower.replace('last ', '').trim();
|
||||
const targetDay = dayNames.indexOf(dayName);
|
||||
|
||||
if (targetDay !== -1) {
|
||||
const currentDay = now.getDay();
|
||||
let daysBack = currentDay - targetDay;
|
||||
if (daysBack <= 0) daysBack += 7; // Go to previous week
|
||||
|
||||
const targetDate = new Date(now);
|
||||
targetDate.setDate(targetDate.getDate() - daysBack);
|
||||
|
||||
return {
|
||||
start: new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate(), 0, 0, 0),
|
||||
end: new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate(), 23, 59, 59),
|
||||
label: `last ${dayName.charAt(0).toUpperCase() + dayName.slice(1)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle just day name (e.g., "wednesday" means this past wednesday)
|
||||
const justDay = dayNames.indexOf(lower);
|
||||
if (justDay !== -1) {
|
||||
const currentDay = now.getDay();
|
||||
let daysBack = currentDay - justDay;
|
||||
if (daysBack < 0) daysBack += 7;
|
||||
if (daysBack === 0) daysBack = 7; // If today is that day, go back a week
|
||||
|
||||
const targetDate = new Date(now);
|
||||
targetDate.setDate(targetDate.getDate() - daysBack);
|
||||
|
||||
return {
|
||||
start: new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate(), 0, 0, 0),
|
||||
end: new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate(), 23, 59, 59),
|
||||
label: targetDate.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })
|
||||
};
|
||||
}
|
||||
|
||||
// Parse specific date
|
||||
const date = new Date(dateArg);
|
||||
return {
|
||||
start: new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0),
|
||||
end: new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59),
|
||||
label: date.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })
|
||||
};
|
||||
}
|
||||
|
||||
// Convert Pacific time to UTC for API query
|
||||
function toUTC(date) {
|
||||
// Pacific is UTC-8 (or UTC-7 during DST)
|
||||
// Add 8 hours to convert Pacific to UTC
|
||||
const utc = new Date(date.getTime() + (8 * 60 * 60 * 1000));
|
||||
return utc.toISOString();
|
||||
}
|
||||
|
||||
// Get all agents for name mapping
|
||||
async function getAgents() {
|
||||
const response = await fetch(`${baseUrl}/agents?per_page=100`, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) return {};
|
||||
|
||||
const data = await response.json();
|
||||
const agentMap = {};
|
||||
for (const agent of data.agents) {
|
||||
agentMap[agent.id] = {
|
||||
name: `${agent.first_name} ${agent.last_name}`,
|
||||
first_name: agent.first_name,
|
||||
job_title: agent.job_title
|
||||
};
|
||||
}
|
||||
return agentMap;
|
||||
}
|
||||
|
||||
// Search tickets closed on date (with pagination)
|
||||
async function searchTickets(startDate, endDate, workspaceId = 2) {
|
||||
const startUTC = toUTC(startDate);
|
||||
const endUTC = toUTC(endDate);
|
||||
|
||||
const query = `(status:4 OR status:5) AND updated_at:>'${startUTC.split('T')[0]}T00:00:00Z' AND updated_at:<'${endUTC.split('T')[0]}T23:59:59Z'`;
|
||||
|
||||
let allTickets = [];
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const url = `${baseUrl}/tickets/filter?query="${encodeURIComponent(query)}"&workspace_id=${workspaceId}&page=${page}&per_page=100`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return { error: `API error ${response.status}: ${error}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
allTickets = allTickets.concat(data.tickets || []);
|
||||
|
||||
// Check if there are more pages
|
||||
hasMore = (data.tickets || []).length === 100;
|
||||
page++;
|
||||
}
|
||||
|
||||
return { tickets: allTickets };
|
||||
}
|
||||
|
||||
// Categorize ticket by subject
|
||||
function categorizeTicket(subject) {
|
||||
const lower = subject.toLowerCase();
|
||||
|
||||
if (lower.includes('password reset')) return 'Password Reset';
|
||||
if (lower.includes('security alert') || lower.includes('compromised') || lower.includes('breach')) return 'Security Alert';
|
||||
if (lower.includes('schoology')) return 'Schoology';
|
||||
if (lower.includes('powerschool')) return 'PowerSchool';
|
||||
if (lower.includes('promethean')) return 'Promethean Board';
|
||||
if (lower.includes('chromebook')) return 'Chromebook';
|
||||
if (lower.includes('phone') || lower.includes('voicemail') || lower.includes('ext.')) return 'Phone/Voicemail';
|
||||
if (lower.includes('badge')) return 'Badge Request';
|
||||
if (lower.includes('new student') || lower.includes('enrollee')) return 'New Student';
|
||||
if (lower.includes('intercom')) return 'Intercom';
|
||||
if (lower.includes('raptor')) return 'Raptor';
|
||||
if (lower.includes('goguardian') || lower.includes('go guardian')) return 'GoGuardian';
|
||||
if (lower.includes('login') || lower.includes('access') || lower.includes('mfa')) return 'Access/Login';
|
||||
|
||||
return 'Other';
|
||||
}
|
||||
|
||||
const dateArg = process.argv[2];
|
||||
const dateRange = parseDate(dateArg);
|
||||
|
||||
try {
|
||||
const [ticketData, agentMap] = await Promise.all([
|
||||
searchTickets(dateRange.start, dateRange.end),
|
||||
getAgents()
|
||||
]);
|
||||
|
||||
if (ticketData.error) {
|
||||
console.error(JSON.stringify(ticketData));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tickets = ticketData.tickets || [];
|
||||
|
||||
// Group by agent
|
||||
const byAgent = {};
|
||||
const byCategory = {};
|
||||
const automated = [];
|
||||
|
||||
for (const ticket of tickets) {
|
||||
const category = categorizeTicket(ticket.subject);
|
||||
byCategory[category] = (byCategory[category] || 0) + 1;
|
||||
|
||||
if (!ticket.responder_id) {
|
||||
automated.push({
|
||||
id: ticket.id,
|
||||
subject: ticket.subject,
|
||||
category,
|
||||
updated_at: ticket.updated_at
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const agentId = ticket.responder_id;
|
||||
if (!byAgent[agentId]) {
|
||||
byAgent[agentId] = {
|
||||
agent: agentMap[agentId] || { name: `Agent ${agentId}`, first_name: 'Unknown' },
|
||||
tickets: [],
|
||||
categories: {}
|
||||
};
|
||||
}
|
||||
|
||||
byAgent[agentId].tickets.push({
|
||||
id: ticket.id,
|
||||
subject: ticket.subject,
|
||||
category,
|
||||
status: ticket.status,
|
||||
priority: ticket.priority,
|
||||
created_at: ticket.created_at,
|
||||
updated_at: ticket.updated_at
|
||||
});
|
||||
|
||||
byAgent[agentId].categories[category] = (byAgent[agentId].categories[category] || 0) + 1;
|
||||
}
|
||||
|
||||
// Sort agents by ticket count
|
||||
const sortedAgents = Object.entries(byAgent)
|
||||
.map(([id, data]) => ({
|
||||
id,
|
||||
name: data.agent.name,
|
||||
first_name: data.agent.first_name,
|
||||
job_title: data.agent.job_title,
|
||||
count: data.tickets.length,
|
||||
categories: data.categories,
|
||||
tickets: data.tickets.sort((a, b) => new Date(a.updated_at) - new Date(b.updated_at))
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
// Build summary
|
||||
const summary = {
|
||||
date: dateRange.label,
|
||||
date_range: {
|
||||
start: dateRange.start.toISOString(),
|
||||
end: dateRange.end.toISOString()
|
||||
},
|
||||
total_closed: tickets.length,
|
||||
by_category: Object.entries(byCategory)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.reduce((acc, [k, v]) => { acc[k] = v; return acc; }, {}),
|
||||
by_agent: sortedAgents,
|
||||
automated: {
|
||||
count: automated.length,
|
||||
tickets: automated
|
||||
}
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
139
skills/freshservice-manager/scripts/get_service_request.js
Normal file
139
skills/freshservice-manager/scripts/get_service_request.js
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// Get service request details including form data
|
||||
// Usage: bun get_service_request.js <ticket_id>
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
const ticketId = process.argv[2];
|
||||
|
||||
if (!ticketId) {
|
||||
console.error(JSON.stringify({ error: 'Ticket ID required' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function getTicket(id) {
|
||||
const response = await fetch(`${baseUrl}/tickets/${id}?include=conversations,requester`, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return { error: `API error ${response.status}: ${error}` };
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function getRequestedItems(ticketId) {
|
||||
const response = await fetch(`${baseUrl}/tickets/${ticketId}/requested_items`, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return { requested_items: [] };
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function getRequester(requesterId) {
|
||||
const response = await fetch(`${baseUrl}/requesters/${requesterId}`, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.requester;
|
||||
}
|
||||
|
||||
try {
|
||||
const ticketData = await getTicket(ticketId);
|
||||
if (ticketData.error) {
|
||||
console.error(JSON.stringify(ticketData));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const ticket = ticketData.ticket;
|
||||
const itemsData = await getRequestedItems(ticketId);
|
||||
const requester = await getRequester(ticket.requester_id);
|
||||
|
||||
// Build comprehensive response
|
||||
const result = {
|
||||
ticket: {
|
||||
id: ticket.id,
|
||||
subject: ticket.subject,
|
||||
type: ticket.type,
|
||||
status: ticket.status,
|
||||
priority: ticket.priority,
|
||||
category: ticket.category,
|
||||
sub_category: ticket.sub_category,
|
||||
item_category: ticket.item_category,
|
||||
workspace_id: ticket.workspace_id,
|
||||
created_at: ticket.created_at,
|
||||
due_by: ticket.due_by,
|
||||
is_escalated: ticket.is_escalated,
|
||||
approval_status: ticket.approval_status,
|
||||
approval_status_name: ticket.approval_status_name
|
||||
},
|
||||
requester: requester ? {
|
||||
name: `${requester.first_name} ${requester.last_name}`,
|
||||
email: requester.primary_email,
|
||||
department: requester.department_names?.[0] || null,
|
||||
job_title: requester.job_title
|
||||
} : null,
|
||||
form_data: itemsData.requested_items?.[0]?.custom_fields || {},
|
||||
service_item: itemsData.requested_items?.[0] ? {
|
||||
id: itemsData.requested_items[0].service_item_id,
|
||||
name: itemsData.requested_items[0].service_item_name,
|
||||
quantity: itemsData.requested_items[0].quantity,
|
||||
cost: itemsData.requested_items[0].cost_per_request
|
||||
} : null,
|
||||
conversations: ticket.conversations?.map(c => ({
|
||||
id: c.id,
|
||||
body_text: c.body_text,
|
||||
private: c.private,
|
||||
created_at: c.created_at,
|
||||
user_id: c.user_id
|
||||
})) || []
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
67
skills/freshservice-manager/scripts/get_ticket.js
Normal file
67
skills/freshservice-manager/scripts/get_ticket.js
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// Get ticket details by ID
|
||||
// Usage: bun get_ticket.js <ticket_id> [include]
|
||||
// Include options: conversations, requester, problem, stats, assets, change, related_tickets
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
const ticketId = process.argv[2];
|
||||
const include = process.argv[3];
|
||||
|
||||
if (!ticketId) {
|
||||
console.error(JSON.stringify({ error: 'Ticket ID required' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function getTicket(id, include) {
|
||||
let url = `${baseUrl}/tickets/${id}`;
|
||||
if (include) {
|
||||
url += `?include=${include}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return { error: `API error ${response.status}: ${error}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await getTicket(ticketId, include);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
280
skills/freshservice-manager/scripts/get_weekly_summary.js
Normal file
280
skills/freshservice-manager/scripts/get_weekly_summary.js
Normal file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// Get weekly ticket summary for Technology workspace with trends
|
||||
// Usage: bun get_weekly_summary.js [weeks_ago]
|
||||
// weeks_ago: 0 = this week (default), 1 = last week, etc.
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
// Get week date range (Monday to Sunday)
|
||||
function getWeekRange(weeksAgo = 0) {
|
||||
const now = new Date();
|
||||
const currentDay = now.getDay();
|
||||
const mondayOffset = currentDay === 0 ? -6 : 1 - currentDay;
|
||||
|
||||
const monday = new Date(now);
|
||||
monday.setDate(monday.getDate() + mondayOffset - (weeksAgo * 7));
|
||||
monday.setHours(0, 0, 0, 0);
|
||||
|
||||
const sunday = new Date(monday);
|
||||
sunday.setDate(sunday.getDate() + 6);
|
||||
sunday.setHours(23, 59, 59, 999);
|
||||
|
||||
const weekNum = getWeekNumber(monday);
|
||||
|
||||
return {
|
||||
start: monday,
|
||||
end: sunday,
|
||||
label: `Week ${weekNum} (${monday.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${sunday.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })})`
|
||||
};
|
||||
}
|
||||
|
||||
function getWeekNumber(date) {
|
||||
const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
|
||||
const pastDaysOfYear = (date - firstDayOfYear) / 86400000;
|
||||
return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
|
||||
}
|
||||
|
||||
// Convert Pacific time to UTC
|
||||
function toUTC(date) {
|
||||
const utc = new Date(date.getTime() + (8 * 60 * 60 * 1000));
|
||||
return utc.toISOString();
|
||||
}
|
||||
|
||||
// Get all agents
|
||||
async function getAgents() {
|
||||
let allAgents = [];
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await fetch(`${baseUrl}/agents?per_page=100&page=${page}`, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) break;
|
||||
|
||||
const data = await response.json();
|
||||
allAgents = allAgents.concat(data.agents);
|
||||
hasMore = data.agents.length === 100;
|
||||
page++;
|
||||
}
|
||||
|
||||
const agentMap = {};
|
||||
for (const agent of allAgents) {
|
||||
agentMap[agent.id] = {
|
||||
name: `${agent.first_name} ${agent.last_name}`,
|
||||
first_name: agent.first_name,
|
||||
job_title: agent.job_title
|
||||
};
|
||||
}
|
||||
return agentMap;
|
||||
}
|
||||
|
||||
// Search tickets with pagination
|
||||
async function searchTickets(startDate, endDate, workspaceId = 2) {
|
||||
const startUTC = toUTC(startDate);
|
||||
const endUTC = toUTC(endDate);
|
||||
|
||||
const query = `(status:4 OR status:5) AND updated_at:>'${startUTC.split('T')[0]}T00:00:00Z' AND updated_at:<'${endUTC.split('T')[0]}T23:59:59Z'`;
|
||||
|
||||
let allTickets = [];
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const url = `${baseUrl}/tickets/filter?query="${encodeURIComponent(query)}"&workspace_id=${workspaceId}&page=${page}&per_page=100`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return { error: `API error ${response.status}: ${error}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
allTickets = allTickets.concat(data.tickets || []);
|
||||
hasMore = (data.tickets || []).length === 100;
|
||||
page++;
|
||||
}
|
||||
|
||||
return { tickets: allTickets };
|
||||
}
|
||||
|
||||
// Categorize ticket
|
||||
function categorizeTicket(subject) {
|
||||
const lower = subject.toLowerCase();
|
||||
|
||||
if (lower.includes('password reset')) return 'Password Reset';
|
||||
if (lower.includes('security alert') || lower.includes('compromised') || lower.includes('breach')) return 'Security Alert';
|
||||
if (lower.includes('schoology')) return 'Schoology';
|
||||
if (lower.includes('powerschool')) return 'PowerSchool';
|
||||
if (lower.includes('promethean')) return 'Promethean Board';
|
||||
if (lower.includes('chromebook')) return 'Chromebook';
|
||||
if (lower.includes('phone') || lower.includes('voicemail') || lower.includes('ext.')) return 'Phone/Voicemail';
|
||||
if (lower.includes('badge')) return 'Badge Request';
|
||||
if (lower.includes('new student') || lower.includes('enrollee')) return 'New Student';
|
||||
if (lower.includes('intercom')) return 'Intercom';
|
||||
if (lower.includes('raptor')) return 'Raptor';
|
||||
if (lower.includes('goguardian') || lower.includes('go guardian')) return 'GoGuardian';
|
||||
if (lower.includes('login') || lower.includes('access') || lower.includes('mfa')) return 'Access/Login';
|
||||
|
||||
return 'Other';
|
||||
}
|
||||
|
||||
// Get day name from date
|
||||
function getDayName(date) {
|
||||
return date.toLocaleDateString('en-US', { weekday: 'short' });
|
||||
}
|
||||
|
||||
const weeksAgo = parseInt(process.argv[2] || '0');
|
||||
const weekRange = getWeekRange(weeksAgo);
|
||||
|
||||
try {
|
||||
const [ticketData, agentMap] = await Promise.all([
|
||||
searchTickets(weekRange.start, weekRange.end),
|
||||
getAgents()
|
||||
]);
|
||||
|
||||
if (ticketData.error) {
|
||||
console.error(JSON.stringify(ticketData));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tickets = ticketData.tickets || [];
|
||||
|
||||
// Group by day
|
||||
const byDay = {};
|
||||
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
for (const day of dayNames) {
|
||||
byDay[day] = { count: 0, categories: {}, agents: {} };
|
||||
}
|
||||
|
||||
// Group by agent and category
|
||||
const byAgent = {};
|
||||
const byCategory = {};
|
||||
const byCategoryByDay = {};
|
||||
|
||||
for (const ticket of tickets) {
|
||||
// Determine day (convert UTC to Pacific)
|
||||
const updatedAt = new Date(ticket.updated_at);
|
||||
const pacificTime = new Date(updatedAt.getTime() - (8 * 60 * 60 * 1000));
|
||||
const dayName = getDayName(pacificTime);
|
||||
|
||||
const category = categorizeTicket(ticket.subject);
|
||||
|
||||
// By day
|
||||
if (byDay[dayName]) {
|
||||
byDay[dayName].count++;
|
||||
byDay[dayName].categories[category] = (byDay[dayName].categories[category] || 0) + 1;
|
||||
|
||||
if (ticket.responder_id) {
|
||||
byDay[dayName].agents[ticket.responder_id] = (byDay[dayName].agents[ticket.responder_id] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// By category
|
||||
byCategory[category] = (byCategory[category] || 0) + 1;
|
||||
|
||||
// By category by day
|
||||
if (!byCategoryByDay[category]) {
|
||||
byCategoryByDay[category] = {};
|
||||
}
|
||||
byCategoryByDay[category][dayName] = (byCategoryByDay[category][dayName] || 0) + 1;
|
||||
|
||||
// By agent
|
||||
if (ticket.responder_id) {
|
||||
if (!byAgent[ticket.responder_id]) {
|
||||
byAgent[ticket.responder_id] = {
|
||||
agent: agentMap[ticket.responder_id] || { name: `Agent ${ticket.responder_id}`, first_name: 'Unknown' },
|
||||
count: 0,
|
||||
categories: {},
|
||||
byDay: {}
|
||||
};
|
||||
}
|
||||
byAgent[ticket.responder_id].count++;
|
||||
byAgent[ticket.responder_id].categories[category] = (byAgent[ticket.responder_id].categories[category] || 0) + 1;
|
||||
byAgent[ticket.responder_id].byDay[dayName] = (byAgent[ticket.responder_id].byDay[dayName] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort agents by count
|
||||
const sortedAgents = Object.entries(byAgent)
|
||||
.map(([id, data]) => ({
|
||||
id,
|
||||
name: data.agent.name,
|
||||
first_name: data.agent.first_name,
|
||||
job_title: data.agent.job_title,
|
||||
count: data.count,
|
||||
categories: data.categories,
|
||||
byDay: data.byDay,
|
||||
avg_per_day: (data.count / 5).toFixed(1) // Assuming 5 work days
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
// Calculate trends
|
||||
const dailyCounts = dayNames.slice(0, 5).map(d => byDay[d].count); // Mon-Fri
|
||||
const avgDaily = dailyCounts.reduce((a, b) => a + b, 0) / 5;
|
||||
const peakDay = dayNames.slice(0, 5).reduce((max, day) => byDay[day].count > byDay[max].count ? day : max, 'Mon');
|
||||
const slowDay = dayNames.slice(0, 5).reduce((min, day) => byDay[day].count < byDay[min].count ? day : min, 'Mon');
|
||||
|
||||
// Build summary
|
||||
const summary = {
|
||||
week: weekRange.label,
|
||||
date_range: {
|
||||
start: weekRange.start.toISOString(),
|
||||
end: weekRange.end.toISOString()
|
||||
},
|
||||
total_closed: tickets.length,
|
||||
daily_average: avgDaily.toFixed(1),
|
||||
trends: {
|
||||
peak_day: { day: peakDay, count: byDay[peakDay].count },
|
||||
slow_day: { day: slowDay, count: byDay[slowDay].count },
|
||||
daily_counts: byDay
|
||||
},
|
||||
by_category: Object.entries(byCategory)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.reduce((acc, [k, v]) => {
|
||||
acc[k] = { total: v, pct: ((v / tickets.length) * 100).toFixed(1) + '%' };
|
||||
return acc;
|
||||
}, {}),
|
||||
category_trends: byCategoryByDay,
|
||||
top_agents: sortedAgents.slice(0, 10),
|
||||
all_agents: sortedAgents
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
70
skills/freshservice-manager/scripts/get_workspaces.js
Normal file
70
skills/freshservice-manager/scripts/get_workspaces.js
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// Get workspace details
|
||||
// Usage: bun get_workspaces.js [workspace_id]
|
||||
// If no ID provided, gets all workspaces the agent has access to
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
async function getWorkspace(id) {
|
||||
const response = await fetch(`${baseUrl}/workspaces/${id}`, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return { id, error: `${response.status}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.workspace;
|
||||
}
|
||||
|
||||
// Known workspace IDs from agent profile
|
||||
const workspaceIds = [2, 3, 4, 5, 6, 8, 9, 10, 11, 13];
|
||||
const specificId = process.argv[2];
|
||||
|
||||
try {
|
||||
if (specificId) {
|
||||
const workspace = await getWorkspace(parseInt(specificId));
|
||||
console.log(JSON.stringify(workspace, null, 2));
|
||||
} else {
|
||||
const results = await Promise.all(workspaceIds.map(getWorkspace));
|
||||
const workspaces = results
|
||||
.filter(w => !w.error)
|
||||
.map(w => ({
|
||||
id: w.id,
|
||||
name: w.name,
|
||||
primary: w.primary,
|
||||
state: w.state
|
||||
}));
|
||||
console.log(JSON.stringify({ workspaces }, null, 2));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
95
skills/freshservice-manager/scripts/list_agents.js
Normal file
95
skills/freshservice-manager/scripts/list_agents.js
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// List all agents
|
||||
// Usage: bun list_agents.js [query]
|
||||
// Query can be first name, last name, or email to filter
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
const query = process.argv[2]?.toLowerCase();
|
||||
|
||||
async function listAgents() {
|
||||
let allAgents = [];
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await fetch(`${baseUrl}/agents?per_page=100&page=${page}`, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return { error: `API error ${response.status}: ${error}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
allAgents = allAgents.concat(data.agents);
|
||||
|
||||
// Check if there are more pages
|
||||
hasMore = data.agents.length === 100;
|
||||
page++;
|
||||
}
|
||||
|
||||
// Filter and simplify
|
||||
let agents = allAgents
|
||||
.filter(a => a.active)
|
||||
.map(a => ({
|
||||
id: a.id,
|
||||
name: `${a.first_name} ${a.last_name}`,
|
||||
first_name: a.first_name,
|
||||
last_name: a.last_name,
|
||||
email: a.email,
|
||||
job_title: a.job_title
|
||||
}));
|
||||
|
||||
// Filter by query if provided
|
||||
if (query) {
|
||||
agents = agents.filter(a =>
|
||||
a.first_name.toLowerCase().includes(query) ||
|
||||
a.last_name.toLowerCase().includes(query) ||
|
||||
a.email.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort by first name
|
||||
agents.sort((a, b) => a.first_name.localeCompare(b.first_name));
|
||||
|
||||
return {
|
||||
count: agents.length,
|
||||
agents
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await listAgents();
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
119
skills/freshservice-manager/scripts/list_tickets.js
Normal file
119
skills/freshservice-manager/scripts/list_tickets.js
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// List tickets with filters
|
||||
// Usage: bun list_tickets.js [options]
|
||||
// Options passed as JSON: {"workspace_id": 2, "filter": "open", "agent_id": 123, "per_page": 30}
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
// Parse options
|
||||
let options = {};
|
||||
if (process.argv[2]) {
|
||||
try {
|
||||
options = JSON.parse(process.argv[2]);
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: 'Invalid JSON options' }));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function listTickets(options) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Workspace filter (0 = all workspaces)
|
||||
if (options.workspace_id !== undefined) {
|
||||
params.append('workspace_id', options.workspace_id);
|
||||
}
|
||||
|
||||
// Predefined filters: new_and_my_open, watching, spam, deleted
|
||||
if (options.filter) {
|
||||
params.append('filter', options.filter);
|
||||
}
|
||||
|
||||
// Include related data
|
||||
if (options.include) {
|
||||
params.append('include', options.include);
|
||||
}
|
||||
|
||||
// Pagination
|
||||
params.append('per_page', options.per_page || 30);
|
||||
if (options.page) {
|
||||
params.append('page', options.page);
|
||||
}
|
||||
|
||||
// Order
|
||||
if (options.order_type) {
|
||||
params.append('order_type', options.order_type);
|
||||
}
|
||||
|
||||
// Updated since (for older tickets)
|
||||
if (options.updated_since) {
|
||||
params.append('updated_since', options.updated_since);
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/tickets?${params.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return { error: `API error ${response.status}: ${error}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Simplify output for readability
|
||||
const tickets = data.tickets.map(t => ({
|
||||
id: t.id,
|
||||
subject: t.subject,
|
||||
status: t.status,
|
||||
priority: t.priority,
|
||||
requester_id: t.requester_id,
|
||||
responder_id: t.responder_id,
|
||||
group_id: t.group_id,
|
||||
workspace_id: t.workspace_id,
|
||||
created_at: t.created_at,
|
||||
updated_at: t.updated_at,
|
||||
due_by: t.due_by
|
||||
}));
|
||||
|
||||
return {
|
||||
count: tickets.length,
|
||||
tickets
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await listTickets(options);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
80
skills/freshservice-manager/scripts/search_tickets.js
Normal file
80
skills/freshservice-manager/scripts/search_tickets.js
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// Search tickets with query
|
||||
// Usage: bun search_tickets.js '<query>' [workspace_id]
|
||||
// Query examples: "responder_id:123", "status:2 AND priority:3", "agent_id:123"
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
const query = process.argv[2];
|
||||
const workspaceId = process.argv[3] || '0';
|
||||
|
||||
if (!query) {
|
||||
console.error(JSON.stringify({ error: 'Query required. Example: "responder_id:6000130414"' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function searchTickets(query, workspaceId) {
|
||||
const url = `${baseUrl}/tickets/filter?query="${encodeURIComponent(query)}"&workspace_id=${workspaceId}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return { error: `API error ${response.status}: ${error}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const tickets = data.tickets.map(t => ({
|
||||
id: t.id,
|
||||
subject: t.subject,
|
||||
status: t.status,
|
||||
priority: t.priority,
|
||||
workspace_id: t.workspace_id,
|
||||
responder_id: t.responder_id,
|
||||
created_at: t.created_at,
|
||||
updated_at: t.updated_at,
|
||||
due_by: t.due_by
|
||||
}));
|
||||
|
||||
return {
|
||||
count: tickets.length,
|
||||
tickets
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await searchTickets(query, workspaceId);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
74
skills/freshservice-manager/scripts/update_ticket.js
Normal file
74
skills/freshservice-manager/scripts/update_ticket.js
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// Update an existing ticket
|
||||
// Usage: bun update_ticket.js <ticket_id> '<json>'
|
||||
// JSON: {"status": 4, "priority": 3, "responder_id": 123, "group_id": 456}
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(homedir(), 'Library/Mobile Documents/com~apple~CloudDocs/Geoffrey/secrets/.env');
|
||||
const content = readFileSync(envPath, 'utf-8');
|
||||
const env = {};
|
||||
for (const line of content.split('\n')) {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
if (key && valueParts.length) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const env = loadEnv();
|
||||
const domain = env.FRESHSERVICE_DOMAIN;
|
||||
const apiKey = env.FRESHSERVICE_API_KEY;
|
||||
const baseUrl = `https://${domain}/api/v2`;
|
||||
|
||||
// Status: 2=Open, 3=Pending, 4=Resolved, 5=Closed
|
||||
// Priority: 1=Low, 2=Medium, 3=High, 4=Urgent
|
||||
|
||||
const ticketId = process.argv[2];
|
||||
let updateData;
|
||||
|
||||
if (!ticketId) {
|
||||
console.error(JSON.stringify({ error: 'Ticket ID required' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
updateData = JSON.parse(process.argv[3]);
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: 'Invalid JSON for update data' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function updateTicket(id, data) {
|
||||
const response = await fetch(`${baseUrl}/tickets/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:X`).toString('base64'),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
return { error: `API error ${response.status}: ${error}` };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await updateTicket(ticketId, updateData);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ error: e.message }));
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user