obsidian-mcp-server
Version:
Obsidian Knowledge-Management MCP (Model Context Protocol) server that enables AI agents and development tools to interact with an Obsidian vault. It provides a comprehensive suite of tools for reading, writing, searching, and managing notes, tags, and fr
144 lines (143 loc) • 6.54 kB
JavaScript
/**
* @fileoverview Utilities for formatting Obsidian stat objects,
* including timestamps and calculating estimated token counts.
* @module src/utils/obsidian/obsidianStatUtils
*/
import { format } from "date-fns";
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
import { logger } from "../internal/index.js";
import { countTokens } from "../metrics/index.js";
/**
* Default format string for timestamps, providing a human-readable date and time.
* Example output: "08:40:00 PM | 05-02-2025"
*/
const DEFAULT_TIMESTAMP_FORMAT = "hh:mm:ss a | MM-dd-yyyy";
/**
* Formats a Unix timestamp (in milliseconds since the epoch) into a human-readable string.
*
* @param {number | undefined | null} timestampMs - The Unix timestamp in milliseconds.
* @param {RequestContext} context - The request context for logging and error reporting.
* @param {string} [formatString=DEFAULT_TIMESTAMP_FORMAT] - Optional format string adhering to `date-fns` tokens.
* Defaults to 'hh:mm:ss a | MM-dd-yyyy'.
* @returns {string} The formatted timestamp string.
* @throws {McpError} If the provided `timestampMs` is invalid (e.g., undefined, null, not a finite number, or results in an invalid Date object).
*/
export function formatTimestamp(timestampMs, context, formatString = DEFAULT_TIMESTAMP_FORMAT) {
const operation = "formatTimestamp";
if (timestampMs === undefined ||
timestampMs === null ||
!Number.isFinite(timestampMs)) {
const errorMessage = `Invalid timestamp provided for formatting: ${timestampMs}`;
logger.warning(errorMessage, { ...context, operation });
throw new McpError(BaseErrorCode.VALIDATION_ERROR, errorMessage, {
...context,
operation,
});
}
try {
const date = new Date(timestampMs);
if (isNaN(date.getTime())) {
const errorMessage = `Timestamp resulted in an invalid date: ${timestampMs}`;
logger.warning(errorMessage, { ...context, operation });
throw new McpError(BaseErrorCode.VALIDATION_ERROR, errorMessage, {
...context,
operation,
});
}
return format(date, formatString);
}
catch (error) {
const errorMessage = `Failed to format timestamp ${timestampMs}: ${error instanceof Error ? error.message : String(error)}`;
logger.error(errorMessage, error instanceof Error ? error : undefined, {
...context,
operation,
});
throw new McpError(BaseErrorCode.INTERNAL_ERROR, errorMessage, {
...context,
operation,
originalError: error instanceof Error ? error.message : String(error),
});
}
}
/**
* Formats the `ctime` (creation time) and `mtime` (modification time) from an
* Obsidian API Stat object into human-readable strings.
*
* @param {ObsidianStat | undefined | null} stat - The Stat object from the Obsidian API.
* If undefined or null, placeholder strings ('N/A') are returned.
* @param {RequestContext} context - The request context for logging and error reporting.
* @returns {FormattedTimestamps} An object containing `createdTime` and `modifiedTime` strings.
*/
export function formatStatTimestamps(stat, context) {
const operation = "formatStatTimestamps";
if (!stat) {
logger.debug("Stat object is undefined or null, returning N/A for timestamps.", { ...context, operation });
return {
createdTime: "N/A",
modifiedTime: "N/A",
};
}
try {
return {
createdTime: formatTimestamp(stat.ctime, context),
modifiedTime: formatTimestamp(stat.mtime, context),
};
}
catch (error) {
// Log the error from formatTimestamp if it occurs during this higher-level operation
logger.error(`Error formatting timestamps within formatStatTimestamps for ctime: ${stat.ctime}, mtime: ${stat.mtime}`, error instanceof Error ? error : undefined, { ...context, operation });
// Return N/A as a fallback if formatting fails at this stage
return {
createdTime: "N/A",
modifiedTime: "N/A",
};
}
}
/**
* Creates a formatted stat object that includes human-readable timestamps
* (creation and modification times) and an estimated token count for the provided file content.
*
* @param {ObsidianStat | null | undefined} stat - The original Stat object from the Obsidian API.
* If null or undefined, the function will return the input value (null or undefined).
* @param {string} content - The file content string from which to calculate the token count.
* @param {RequestContext} context - The request context for logging and error reporting.
* @returns {Promise<FormattedStatWithTokenCount | null | undefined>} A promise resolving to an object
* containing `createdTime`, `modifiedTime`, and `tokenCountEstimate`. Returns `null` or `undefined`
* if the input `stat` object was `null` or `undefined`, respectively.
*/
export async function createFormattedStatWithTokenCount(stat, content, context) {
const operation = "createFormattedStatWithTokenCount";
if (stat === null || stat === undefined) {
logger.debug("Input stat is null or undefined, returning as is.", {
...context,
operation,
});
return stat; // Return original null/undefined
}
const formattedTimestamps = formatStatTimestamps(stat, context);
let tokenCountEstimate = -1; // Default: indicates error or empty content
if (content && content.trim().length > 0) {
try {
tokenCountEstimate = await countTokens(content, context);
}
catch (tokenError) {
logger.warning(`Failed to count tokens for stat object. Error: ${tokenError instanceof Error ? tokenError.message : String(tokenError)}`, {
...context,
operation,
originalError: tokenError instanceof Error
? tokenError.message
: String(tokenError),
});
// tokenCountEstimate remains -1
}
}
else {
logger.debug("Content is empty or whitespace-only, setting tokenCountEstimate to 0.", { ...context, operation });
tokenCountEstimate = 0;
}
return {
createdTime: formattedTimestamps.createdTime,
modifiedTime: formattedTimestamps.modifiedTime,
tokenCountEstimate: tokenCountEstimate,
};
}