/**
* Response formatters for IT Glue data
* Handles conversion between JSON and Markdown formats
*/
import { ResponseFormat } from '../types.js';
import { CHARACTER_LIMIT } from '../constants.js';
// Generic type for IT Glue resources
type ITGlueResource = { id: string; type: string; attributes: unknown };
/**
* Format a date string to human-readable format
*/
export function formatDate(dateStr: string | undefined | null): string {
if (!dateStr) return 'N/A';
try {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
} catch {
return dateStr;
}
}
/**
* Format a datetime string to human-readable format
*/
export function formatDateTime(dateStr: string | undefined | null): string {
if (!dateStr) return 'N/A';
try {
const date = new Date(dateStr);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return dateStr;
}
}
/**
* Truncate text if it exceeds limit
*/
export function truncateText(text: string, limit: number = CHARACTER_LIMIT): { text: string; truncated: boolean } {
if (text.length <= limit) {
return { text, truncated: false };
}
return {
text: text.slice(0, limit) + '\n\n... [Response truncated. Use pagination or filters to see more results.]',
truncated: true
};
}
/**
* Create pagination info for responses
*/
export function formatPaginationInfo(meta: Record<string, unknown> | undefined): {
currentPage: number;
totalPages: number;
totalCount: number;
hasMore: boolean;
nextPage?: number;
} {
if (!meta) {
return { currentPage: 1, totalPages: 1, totalCount: 0, hasMore: false };
}
const currentPage = (meta['current-page'] as number) || 1;
const totalPages = (meta['total-pages'] as number) || 1;
const totalCount = (meta['total-count'] as number) || 0;
const nextPage = meta['next-page'] as number | undefined;
return {
currentPage,
totalPages,
totalCount,
hasMore: currentPage < totalPages,
nextPage
};
}
/**
* Get attributes as a record for safe access
*/
function getAttrs(item: ITGlueResource): Record<string, unknown> {
return item.attributes as Record<string, unknown>;
}
/**
* Base formatter for list responses
*/
export function formatListResponse<T extends ITGlueResource>(
items: T[],
meta: Record<string, unknown> | undefined,
format: ResponseFormat,
formatItem: (item: T) => string,
title: string
): { content: string; structuredContent: Record<string, unknown> } {
const pagination = formatPaginationInfo(meta);
// Build structured content
const structuredContent: Record<string, unknown> = {
total_count: pagination.totalCount,
count: items.length,
page: pagination.currentPage,
total_pages: pagination.totalPages,
has_more: pagination.hasMore,
...(pagination.nextPage ? { next_page: pagination.nextPage } : {}),
items: items.map(item => ({
id: item.id,
type: item.type,
...getAttrs(item)
}))
};
// Build text content
let textContent: string;
if (format === ResponseFormat.JSON) {
textContent = JSON.stringify(structuredContent, null, 2);
} else {
const lines: string[] = [
`# ${title}`,
'',
`Found ${pagination.totalCount} items (showing ${items.length}, page ${pagination.currentPage} of ${pagination.totalPages})`,
''
];
if (items.length === 0) {
lines.push('*No items found matching your criteria.*');
} else {
for (const item of items) {
lines.push(formatItem(item));
lines.push('');
}
}
if (pagination.hasMore) {
lines.push('---');
lines.push(`*More results available. Use \`page: ${pagination.nextPage}\` to see the next page.*`);
}
textContent = lines.join('\n');
}
// Check truncation
const { text, truncated } = truncateText(textContent);
if (truncated) {
structuredContent.truncated = true;
structuredContent.truncation_message = 'Response truncated. Use pagination or filters.';
}
return { content: text, structuredContent };
}
/**
* Base formatter for single item responses
*/
export function formatSingleResponse<T extends ITGlueResource>(
item: T,
format: ResponseFormat,
formatDetail: (item: T) => string,
title: string
): { content: string; structuredContent: Record<string, unknown> } {
const structuredContent: Record<string, unknown> = {
id: item.id,
type: item.type,
...getAttrs(item)
};
let textContent: string;
if (format === ResponseFormat.JSON) {
textContent = JSON.stringify(structuredContent, null, 2);
} else {
textContent = [
`# ${title}`,
'',
formatDetail(item)
].join('\n');
}
return { content: textContent, structuredContent };
}
/**
* Format organization for markdown display
*/
export function formatOrganizationMarkdown<T extends ITGlueResource>(org: T): string {
const a = getAttrs(org);
const lines: string[] = [
`## ${a.name || 'Unnamed'} (ID: ${org.id})`
];
if (a['short-name']) lines.push(`- **Short Name**: ${a['short-name']}`);
if (a['organization-type-name']) lines.push(`- **Type**: ${a['organization-type-name']}`);
if (a['organization-status-name']) lines.push(`- **Status**: ${a['organization-status-name']}`);
if (a.primary) lines.push(`- **Primary**: Yes`);
if (a['quick-notes']) lines.push(`- **Quick Notes**: ${a['quick-notes']}`);
if (a.alert) lines.push(`- **Alert**: ${a.alert}`);
if (a['psa-id']) lines.push(`- **PSA ID**: ${a['psa-id']} (${a['psa-integration-type'] || 'Unknown'})`);
if (a['resource-url']) lines.push(`- **IT Glue URL**: ${a['resource-url']}`);
return lines.join('\n');
}
/**
* Format configuration for markdown display
*/
export function formatConfigurationMarkdown<T extends ITGlueResource>(config: T): string {
const a = getAttrs(config);
const lines: string[] = [
`## ${a.name || 'Unnamed'} (ID: ${config.id})`
];
if (a['organization-name']) lines.push(`- **Organization**: ${a['organization-name']}`);
if (a['configuration-type-name']) lines.push(`- **Type**: ${a['configuration-type-name']}`);
if (a['configuration-status-name']) lines.push(`- **Status**: ${a['configuration-status-name']}`);
if (a.hostname) lines.push(`- **Hostname**: ${a.hostname}`);
if (a['primary-ip']) lines.push(`- **Primary IP**: ${a['primary-ip']}`);
if (a['mac-address']) lines.push(`- **MAC Address**: ${a['mac-address']}`);
if (a['serial-number']) lines.push(`- **Serial Number**: ${a['serial-number']}`);
if (a['asset-tag']) lines.push(`- **Asset Tag**: ${a['asset-tag']}`);
if (a['manufacturer-name']) lines.push(`- **Manufacturer**: ${a['manufacturer-name']}`);
if (a['model-name']) lines.push(`- **Model**: ${a['model-name']}`);
if (a['operating-system-name']) lines.push(`- **OS**: ${a['operating-system-name']}`);
if (a['location-name']) lines.push(`- **Location**: ${a['location-name']}`);
if (a['warranty-expires-at']) lines.push(`- **Warranty Expires**: ${formatDate(a['warranty-expires-at'] as string)}`);
if (a.archived) lines.push(`- **Archived**: Yes`);
if (a['resource-url']) lines.push(`- **IT Glue URL**: ${a['resource-url']}`);
return lines.join('\n');
}
/**
* Format password for markdown display (without actual password value)
*/
export function formatPasswordMarkdown<T extends ITGlueResource>(password: T): string {
const a = getAttrs(password);
const lines: string[] = [
`## ${a.name || 'Unnamed'} (ID: ${password.id})`
];
if (a['organization-name']) lines.push(`- **Organization**: ${a['organization-name']}`);
if (a['password-category-name']) lines.push(`- **Category**: ${a['password-category-name']}`);
if (a.username) lines.push(`- **Username**: ${a.username}`);
if (a.url) lines.push(`- **URL**: ${a.url}`);
if (a['password-updated-at']) lines.push(`- **Password Updated**: ${formatDateTime(a['password-updated-at'] as string)}`);
if (a.otp_enabled) lines.push(`- **OTP Enabled**: Yes`);
if (a.archived) lines.push(`- **Archived**: Yes`);
if (a['resource-url']) lines.push(`- **IT Glue URL**: ${a['resource-url']}`);
// Note: We intentionally don't include the actual password value in markdown
// Users should use the JSON format or a dedicated get_password tool for the actual value
return lines.join('\n');
}
/**
* Format contact for markdown display
*/
export function formatContactMarkdown<T extends ITGlueResource>(contact: T): string {
const a = getAttrs(contact);
const name = a.name || `${a['first-name'] || ''} ${a['last-name'] || ''}`.trim() || 'Unnamed';
const lines: string[] = [
`## ${name} (ID: ${contact.id})`
];
if (a['organization-name']) lines.push(`- **Organization**: ${a['organization-name']}`);
if (a.title) lines.push(`- **Title**: ${a.title}`);
if (a['contact-type-name']) lines.push(`- **Type**: ${a['contact-type-name']}`);
if (a['location-name']) lines.push(`- **Location**: ${a['location-name']}`);
if (a.important) lines.push(`- **Important**: Yes`);
// Format emails
const emails = a['contact-emails'] as Array<{ value: string; primary: boolean; 'label-name': string }> | undefined;
if (emails && emails.length > 0) {
const primaryEmail = emails.find(e => e.primary) || emails[0];
lines.push(`- **Email**: ${primaryEmail.value}${primaryEmail.primary ? ' (primary)' : ''}`);
if (emails.length > 1) {
lines.push(` - Additional emails: ${emails.filter(e => e !== primaryEmail).map(e => e.value).join(', ')}`);
}
}
// Format phones
const phones = a['contact-phones'] as Array<{ value: string; primary: boolean; extension?: string; 'label-name': string }> | undefined;
if (phones && phones.length > 0) {
const primaryPhone = phones.find(p => p.primary) || phones[0];
lines.push(`- **Phone**: ${primaryPhone.value}${primaryPhone.extension ? ` x${primaryPhone.extension}` : ''}${primaryPhone.primary ? ' (primary)' : ''}`);
}
if (a['resource-url']) lines.push(`- **IT Glue URL**: ${a['resource-url']}`);
return lines.join('\n');
}
/**
* Format flexible asset for markdown display
*/
export function formatFlexibleAssetMarkdown<T extends ITGlueResource>(asset: T): string {
const a = getAttrs(asset);
const lines: string[] = [
`## ${a.name || 'Unnamed'} (ID: ${asset.id})`
];
if (a['organization-name']) lines.push(`- **Organization**: ${a['organization-name']}`);
if (a['flexible-asset-type-name']) lines.push(`- **Asset Type**: ${a['flexible-asset-type-name']}`);
if (a.archived) lines.push(`- **Archived**: Yes`);
if (a['resource-url']) lines.push(`- **IT Glue URL**: ${a['resource-url']}`);
// Format traits (custom fields)
const traits = a.traits as Record<string, unknown> | undefined;
if (traits && Object.keys(traits).length > 0) {
lines.push('');
lines.push('**Traits:**');
for (const [key, value] of Object.entries(traits)) {
if (value !== null && value !== undefined && value !== '') {
// Handle tagged assets (arrays of objects with IDs)
if (Array.isArray(value)) {
const tagValues = value.map((v: unknown) => {
if (typeof v === 'object' && v !== null && 'name' in v) {
return (v as { name: string }).name;
}
return String(v);
});
lines.push(`- **${key}**: ${tagValues.join(', ')}`);
} else if (typeof value === 'object' && value !== null && 'name' in value) {
lines.push(`- **${key}**: ${(value as { name: string }).name}`);
} else {
lines.push(`- **${key}**: ${value}`);
}
}
}
}
return lines.join('\n');
}
/**
* Format location for markdown display
*/
export function formatLocationMarkdown<T extends ITGlueResource>(location: T): string {
const a = getAttrs(location);
const lines: string[] = [
`## ${a.name || 'Unnamed'} (ID: ${location.id})`
];
if (a['organization-name']) lines.push(`- **Organization**: ${a['organization-name']}`);
if (a.primary) lines.push(`- **Primary Location**: Yes`);
if (a['formatted-address']) {
lines.push(`- **Address**: ${a['formatted-address']}`);
} else {
const addressParts = [
a['address-1'],
a['address-2'],
a.city,
a['region-name'],
a['postal-code'],
a['country-name']
].filter(Boolean);
if (addressParts.length > 0) {
lines.push(`- **Address**: ${addressParts.join(', ')}`);
}
}
if (a.phone) lines.push(`- **Phone**: ${a.phone}`);
if (a.fax) lines.push(`- **Fax**: ${a.fax}`);
return lines.join('\n');
}
/**
* Format domain for markdown display
*/
export function formatDomainMarkdown<T extends ITGlueResource>(domain: T): string {
const a = getAttrs(domain);
const lines: string[] = [
`## ${a.name || 'Unnamed'} (ID: ${domain.id})`
];
if (a['organization-name']) lines.push(`- **Organization**: ${a['organization-name']}`);
if (a['registrar-name']) lines.push(`- **Registrar**: ${a['registrar-name']}`);
if (a['expires-on']) lines.push(`- **Expires**: ${formatDate(a['expires-on'] as string)}`);
if (a.notes) lines.push(`- **Notes**: ${a.notes}`);
if (a['resource-url']) lines.push(`- **IT Glue URL**: ${a['resource-url']}`);
return lines.join('\n');
}
/**
* Format expiration for markdown display
*/
export function formatExpirationMarkdown<T extends ITGlueResource>(expiration: T): string {
const a = getAttrs(expiration);
const lines: string[] = [
`## ${a['resource-name'] || 'Unnamed'} (ID: ${expiration.id})`
];
if (a['organization-name']) lines.push(`- **Organization**: ${a['organization-name']}`);
if (a['resource-type-name']) lines.push(`- **Resource Type**: ${a['resource-type-name']}`);
if (a.description) lines.push(`- **Description**: ${a.description}`);
if (a['expiration-date']) lines.push(`- **Expires**: ${formatDate(a['expiration-date'] as string)}`);
if (a['resource-url']) lines.push(`- **IT Glue URL**: ${a['resource-url']}`);
return lines.join('\n');
}