Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

mdbase-tasknotes

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mdbase-tasknotes - npm Package Compare versions

Comparing version
0.1.2
to
0.1.3
+289
dist/collection.js
// src/collection.ts
import { Collection } from "@callumalpass/mdbase";
import { basename as basename2 } from "path";
// src/config.ts
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
var CONFIG_DIR = path.join(
os.homedir(),
".config",
"mdbase-tasknotes"
);
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
var DEFAULT_CONFIG = {
collectionPath: null,
language: "en"
};
function load() {
try {
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
} catch {
return { ...DEFAULT_CONFIG };
}
}
function resolveUserPath(userPath) {
return path.resolve(expandHomeDirectory(userPath));
}
function resolveCollectionPath(flagPath) {
if (flagPath) return resolveUserPath(flagPath);
const envPath = process.env.MDBASE_TASKNOTES_PATH;
if (envPath) return resolveUserPath(envPath);
const config = load();
if (config.collectionPath) return resolveUserPath(config.collectionPath);
return process.cwd();
}
function expandHomeDirectory(userPath) {
if (userPath === "~") {
return os.homedir();
}
if (userPath.startsWith("~/") || userPath.startsWith("~\\")) {
return path.join(os.homedir(), userPath.slice(2));
}
return userPath;
}
// src/field-mapping.ts
import { loadConfig, getType } from "@callumalpass/mdbase";
import { basename } from "path";
var ALL_ROLES = [
"title",
"status",
"priority",
"due",
"scheduled",
"completedDate",
"tags",
"contexts",
"projects",
"timeEstimate",
"dateCreated",
"dateModified",
"recurrence",
"recurrenceAnchor",
"completeInstances",
"skippedInstances",
"timeEntries"
];
function defaultFieldMapping() {
const roleToField = {};
const fieldToRole = {};
for (const role of ALL_ROLES) {
roleToField[role] = role;
fieldToRole[role] = role;
}
return {
roleToField,
fieldToRole,
displayNameKey: "title",
completedStatuses: ["done", "cancelled"]
};
}
function buildFieldMapping(fields, displayNameKey) {
const roleToField = {};
const fieldToRole = {};
const rolesSet = new Set(ALL_ROLES);
for (const [fieldName, def] of Object.entries(fields)) {
if (def && typeof def === "object" && typeof def.tn_role === "string") {
const role = def.tn_role;
if (!rolesSet.has(role)) continue;
if (roleToField[role] !== void 0) {
console.warn(`[mtn] Duplicate tn_role "${role}" on field "${fieldName}", ignoring.`);
continue;
}
roleToField[role] = fieldName;
fieldToRole[fieldName] = role;
}
}
for (const role of ALL_ROLES) {
if (roleToField[role] === void 0) {
if (fields[role] !== void 0) {
roleToField[role] = role;
if (fieldToRole[role] === void 0) {
fieldToRole[role] = role;
}
} else {
roleToField[role] = role;
}
}
}
const completedStatuses = inferCompletedStatuses(fields, roleToField.status);
return {
roleToField,
fieldToRole,
displayNameKey: displayNameKey && typeof displayNameKey === "string" && displayNameKey.trim().length > 0 ? displayNameKey : roleToField.title,
completedStatuses
};
}
function inferCompletedStatuses(fields, statusFieldName) {
const statusDef = fields[statusFieldName];
if (!statusDef || typeof statusDef !== "object") {
return ["done", "cancelled"];
}
if (Array.isArray(statusDef.tn_completed_values)) {
const explicit = statusDef.tn_completed_values.filter((v) => typeof v === "string").map((v) => v.trim()).filter((v) => v.length > 0);
if (explicit.length > 0) return explicit;
}
if (Array.isArray(statusDef.values)) {
const inferred = statusDef.values.filter((v) => typeof v === "string").filter((v) => {
const lower = v.toLowerCase();
return lower.includes("done") || lower.includes("complete") || lower.includes("cancel") || lower.includes("finish");
});
if (inferred.length > 0) return inferred;
}
return ["done", "cancelled"];
}
async function loadFieldMapping(flagPath) {
try {
const collectionPath = resolveCollectionPath(flagPath);
const configResult = await loadConfig(collectionPath);
if (!configResult.valid || !configResult.config) {
return defaultFieldMapping();
}
const typeResult = await getType(collectionPath, configResult.config, "task");
if (!typeResult.valid || !typeResult.type) {
return defaultFieldMapping();
}
const displayNameKey = typeof typeResult.type.display_name_key === "string" ? typeResult.type.display_name_key : typeof typeResult.type.displayNameKey === "string" ? typeResult.type.displayNameKey : void 0;
return buildFieldMapping(typeResult.type.fields || {}, displayNameKey);
} catch {
return defaultFieldMapping();
}
}
function resolveField(mapping, role) {
return mapping.roleToField[role];
}
// src/collection.ts
async function openCollection(flagPath) {
const collectionPath = resolveCollectionPath(flagPath);
const { collection, error } = await Collection.open(collectionPath);
if (error) {
throw new Error(`Failed to open collection at ${collectionPath}: ${error.message}`);
}
return collection;
}
async function withCollection(fn, flagPath) {
const collection = await openCollection(flagPath);
const mapping = await loadFieldMapping(flagPath);
try {
return await fn(collection, mapping);
} finally {
await collection.close();
}
}
async function resolveTaskPath(collection, pathOrTitle, mapping) {
if (pathOrTitle.includes("/") || pathOrTitle.endsWith(".md")) {
return pathOrTitle;
}
const titleField = resolveField(mapping, "title");
const query = pathOrTitle.trim();
const escaped = query.replace(/"/g, '\\"');
const exact = await queryTasks(collection, `${titleField} == "${escaped}"`, 20);
if (exact.length === 1) {
return exact[0].path;
}
if (exact.length > 1) {
throw new Error(formatAmbiguousTaskError(query, exact, titleField));
}
const exactBasename = await queryTasks(collection, `file.basename == "${escaped}"`, 20);
if (exactBasename.length === 1) {
return exactBasename[0].path;
}
if (exactBasename.length > 1) {
throw new Error(formatAmbiguousTaskError(query, exactBasename, titleField));
}
const fuzzyTitle = await queryTasks(collection, `${titleField}.contains("${escaped}")`, 20);
const fuzzyBasename = await queryTasks(collection, `file.basename.contains("${escaped}")`, 20);
const fuzzy = dedupeByPath([...fuzzyTitle, ...fuzzyBasename]);
if (fuzzy.length === 1) {
return fuzzy[0].path;
}
if (fuzzy.length > 1) {
throw new Error(
formatAmbiguousTaskError(
query,
rankCandidates(query, fuzzy, titleField),
titleField
)
);
}
throw new Error(`No task found matching "${query}"`);
}
async function queryTasks(collection, where, limit) {
try {
const result = await collection.query({
types: ["task"],
where,
limit
});
return result.results || [];
} catch {
return [];
}
}
function dedupeByPath(candidates) {
const seen = /* @__PURE__ */ new Set();
const deduped = [];
for (const candidate of candidates) {
if (seen.has(candidate.path)) continue;
seen.add(candidate.path);
deduped.push(candidate);
}
return deduped;
}
function rankCandidates(query, candidates, titleField) {
const q = query.toLowerCase();
return [...candidates].sort((a, b) => {
const scoreA = scoreCandidate(q, a, titleField);
const scoreB = scoreCandidate(q, b, titleField);
if (scoreA !== scoreB) return scoreB - scoreA;
const titleA = getTaskTitle(a, titleField).toLowerCase();
const titleB = getTaskTitle(b, titleField).toLowerCase();
if (titleA !== titleB) return titleA.localeCompare(titleB);
return a.path.localeCompare(b.path);
});
}
function scoreCandidate(query, candidate, titleField) {
const title = getTaskTitle(candidate, titleField).toLowerCase();
const path2 = candidate.path.toLowerCase();
let score = 0;
if (title === query) score += 100;
if (title.startsWith(query)) score += 50;
if (title.includes(query)) score += 25;
if (path2.includes(query)) score += 10;
score += Math.max(0, 10 - Math.abs(title.length - query.length));
return score;
}
function formatAmbiguousTaskError(query, candidates, titleField) {
const preview = candidates.slice(0, 5).map((candidate, index) => {
const title = getTaskTitle(candidate, titleField);
return ` ${index + 1}. ${title} (${candidate.path})`;
}).join("\n");
const more = candidates.length > 5 ? `
...and ${candidates.length - 5} more` : "";
const examplePath = candidates[0]?.path || "tasks/<task>.md";
return [
`Ambiguous task reference "${query}".`,
"Matches (best first):",
`${preview}${more}`,
`Use a full path to disambiguate (for example: ${examplePath}).`
].join("\n");
}
function getTaskTitle(candidate, titleField) {
if (candidate.frontmatter && titleField) {
const raw = candidate.frontmatter[titleField];
if (typeof raw === "string" && raw.trim().length > 0) {
return raw;
}
}
const fromPath = basename2(candidate.path, ".md").trim();
return fromPath.length > 0 ? fromPath : candidate.path;
}
export {
openCollection,
resolveTaskPath,
withCollection
};
// src/config.ts
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
var CONFIG_DIR = path.join(
os.homedir(),
".config",
"mdbase-tasknotes"
);
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
var DEFAULT_CONFIG = {
collectionPath: null,
language: "en"
};
function load() {
try {
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
} catch {
return { ...DEFAULT_CONFIG };
}
}
function save(config) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
}
function getConfig() {
return load();
}
function setConfig(key, value) {
const config = load();
if (key === "collectionPath") {
config.collectionPath = value;
} else if (key === "language") {
config.language = value ?? "en";
}
save(config);
}
function getConfigPath() {
return CONFIG_FILE;
}
function resolveUserPath(userPath) {
return path.resolve(expandHomeDirectory(userPath));
}
function resolveCollectionPath(flagPath) {
if (flagPath) return resolveUserPath(flagPath);
const envPath = process.env.MDBASE_TASKNOTES_PATH;
if (envPath) return resolveUserPath(envPath);
const config = load();
if (config.collectionPath) return resolveUserPath(config.collectionPath);
return process.cwd();
}
function expandHomeDirectory(userPath) {
if (userPath === "~") {
return os.homedir();
}
if (userPath.startsWith("~/") || userPath.startsWith("~\\")) {
return path.join(os.homedir(), userPath.slice(2));
}
return userPath;
}
export {
getConfig,
getConfigPath,
resolveCollectionPath,
resolveUserPath,
setConfig
};

Sorry, the diff of this file is too big to display

// src/create-compat.ts
import { format } from "date-fns";
// src/field-mapping.ts
import { loadConfig, getType } from "@callumalpass/mdbase";
import { basename } from "path";
// src/config.ts
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
var CONFIG_DIR = path.join(
os.homedir(),
".config",
"mdbase-tasknotes"
);
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
// src/field-mapping.ts
var ALL_ROLES = [
"title",
"status",
"priority",
"due",
"scheduled",
"completedDate",
"tags",
"contexts",
"projects",
"timeEstimate",
"dateCreated",
"dateModified",
"recurrence",
"recurrenceAnchor",
"completeInstances",
"skippedInstances",
"timeEntries"
];
function denormalizeFrontmatter(roleData, mapping) {
const result = {};
const rolesSet = new Set(ALL_ROLES);
for (const [key, value] of Object.entries(roleData)) {
if (rolesSet.has(key)) {
result[mapping.roleToField[key]] = value;
} else {
result[key] = value;
}
}
return result;
}
function resolveField(mapping, role) {
return mapping.roleToField[role];
}
// src/create-compat.ts
async function createTaskWithCompat(collection, mapping, roleFrontmatter, body) {
const taskType = getTaskTypeDef(collection);
const denormalized = denormalizeFrontmatter(roleFrontmatter, mapping);
applyFieldDefaults(denormalized, taskType);
applyTimestampDefaults(denormalized, mapping, taskType);
applyMatchDefaults(denormalized, taskType);
const input = {
type: "task",
frontmatter: denormalized,
body
};
const firstAttempt = await collection.create(input);
if (!firstAttempt.error || firstAttempt.error.code !== "path_required") {
return firstAttempt;
}
const pathResolution = derivePathFromType(
taskType,
denormalized,
mapping,
/* @__PURE__ */ new Date()
);
if (!pathResolution.path) {
if (pathResolution.errorMessage) {
return {
...firstAttempt,
error: {
...firstAttempt.error,
message: pathResolution.errorMessage
}
};
}
if (pathResolution.missingKeys && pathResolution.missingKeys.length > 0) {
const missing = pathResolution.missingKeys.join(", ");
return {
...firstAttempt,
warnings: [
`Cannot resolve path_pattern "${pathResolution.template}": missing template values for ${missing}.`
]
};
}
return firstAttempt;
}
return await collection.create({
...input,
path: pathResolution.path
});
}
function getTaskTypeDef(collection) {
const maybeCollection = collection;
if (!maybeCollection.typeDefs || typeof maybeCollection.typeDefs.get !== "function") {
return void 0;
}
return maybeCollection.typeDefs.get("task");
}
function applyTimestampDefaults(frontmatter, mapping, taskType) {
const fields = taskType?.fields;
if (!fields) return;
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
const createdField = resolveField(mapping, "dateCreated");
if (fields[createdField] && !hasValue(frontmatter[createdField])) {
frontmatter[createdField] = nowIso;
}
const modifiedField = resolveField(mapping, "dateModified");
if (fields[modifiedField] && !hasValue(frontmatter[modifiedField])) {
frontmatter[modifiedField] = nowIso;
}
}
function applyFieldDefaults(frontmatter, taskType) {
const fields = taskType?.fields;
if (!fields) return;
for (const [fieldName, fieldDef] of Object.entries(fields)) {
if (fieldDef.default !== void 0 && !hasValue(frontmatter[fieldName])) {
frontmatter[fieldName] = fieldDef.default;
}
}
}
function applyMatchDefaults(frontmatter, taskType) {
const where = taskType?.match?.where;
if (!where || typeof where !== "object") return;
for (const [field, condition] of Object.entries(where)) {
if (condition === null || condition === void 0) continue;
if (typeof condition !== "object" || Array.isArray(condition)) {
if (!hasValue(frontmatter[field])) {
frontmatter[field] = condition;
}
continue;
}
const ops = condition;
if ("eq" in ops && !hasValue(frontmatter[field])) {
frontmatter[field] = ops.eq;
continue;
}
if ("contains" in ops) {
const expected = ops.contains;
const current = frontmatter[field];
if (Array.isArray(current)) {
if (!current.some((v) => String(v) === String(expected))) {
current.push(expected);
frontmatter[field] = current;
}
continue;
}
if (typeof current === "string") {
if (!current.includes(String(expected))) {
frontmatter[field] = `${current} ${String(expected)}`.trim();
}
continue;
}
if (!hasValue(current)) {
frontmatter[field] = [expected];
}
continue;
}
if ("exists" in ops && ops.exists === true && !hasValue(frontmatter[field])) {
frontmatter[field] = true;
}
}
}
function derivePathFromType(taskType, frontmatter, mapping, now) {
if (!taskType || typeof taskType.path_pattern !== "string" || taskType.path_pattern.trim().length === 0) {
return {
errorMessage: buildMissingPathPatternMessage(taskType)
};
}
const values = buildTemplateValues(frontmatter, mapping, now);
const renderedPattern = renderTemplate(taskType.path_pattern, values);
if (renderedPattern.path) {
return { path: ensureMarkdownExt(renderedPattern.path), template: taskType.path_pattern };
}
return {
template: taskType.path_pattern,
missingKeys: renderedPattern.missingKeys
};
}
function buildMissingPathPatternMessage(taskType) {
const pathGlob = readString(taskType?.match?.path_glob);
if (!pathGlob) {
return [
"Cannot create task because the task type does not define path_pattern.",
"Add path_pattern to _types/task.md to tell mtn where new task files should be written."
].join(" ");
}
const suggestion = suggestPathPatternFromGlob(pathGlob);
return [
`Cannot create task because _types/task.md defines match.path_glob "${pathGlob}" but no path_pattern.`,
"match.path_glob only identifies existing files; it is not a template for creating new files.",
`Add path_pattern to tell mtn where to write new tasks, for example: ${suggestion}.`
].join(" ");
}
function suggestPathPatternFromGlob(pathGlob) {
const normalized = normalizeRelativePath(pathGlob);
const withoutGlob = normalized.replace(/\*\*\/\*\.md$/u, "{{titleKebab}}.md").replace(/\*\.md$/u, "{{titleKebab}}.md").replace(/\*\*$/u, "{{titleKebab}}.md").replace(/\*$/u, "{{titleKebab}}.md");
const suggestion = withoutGlob === normalized || withoutGlob.length === 0 ? "tasks/{{titleKebab}}.md" : withoutGlob;
return `path_pattern: "${suggestion}"`;
}
function renderTemplate(template, values) {
const missingKeys = /* @__PURE__ */ new Set();
const rendered = template.replace(/\{\{(\w+)\}\}|\{(\w+)\}/g, (_, a, b) => {
const key = a ?? b;
const value = values[key];
if (value === void 0 || value === null || String(value).trim().length === 0) {
missingKeys.add(key);
return "";
}
return String(value);
});
if (missingKeys.size > 0) {
return { missingKeys: Array.from(missingKeys).sort() };
}
const normalized = normalizeRelativePath(rendered);
if (!normalized || normalized.includes("..") || normalized.includes("\0")) {
return { missingKeys: [] };
}
return { path: normalized, missingKeys: [] };
}
function buildTemplateValues(frontmatter, mapping, now) {
const values = {};
const titleField = resolveField(mapping, "title");
const priorityField = resolveField(mapping, "priority");
const statusField = resolveField(mapping, "status");
const dueField = resolveField(mapping, "due");
const scheduledField = resolveField(mapping, "scheduled");
const contextsField = resolveField(mapping, "contexts");
const projectsField = resolveField(mapping, "projects");
const tagsField = resolveField(mapping, "tags");
const estimateField = resolveField(mapping, "timeEstimate");
const rawTitle = readString(frontmatter[titleField]) || readString(frontmatter.title) || "task";
const title = sanitizeForPathSegment(rawTitle);
const priority = sanitizeForPathSegment(
readString(frontmatter[priorityField]) || readString(frontmatter.priority) || "normal"
);
const status = sanitizeForPathSegment(
readString(frontmatter[statusField]) || readString(frontmatter.status) || "open"
);
const dueDateRaw = readString(frontmatter[dueField]) || readString(frontmatter.due) || "";
const scheduledDateRaw = readString(frontmatter[scheduledField]) || readString(frontmatter.scheduled) || "";
const todayDate = format(now, "yyyy-MM-dd");
const dueDate = dueDateRaw || scheduledDateRaw || todayDate;
const scheduledDate = scheduledDateRaw || dueDateRaw || todayDate;
const contexts = readStringList(frontmatter[contextsField] ?? frontmatter.contexts).map((v) => sanitizeForPathSegment(v)).filter(Boolean);
const projects = readStringList(frontmatter[projectsField] ?? frontmatter.projects).map(extractProjectName).map((v) => sanitizeForPathSegment(v)).filter(Boolean);
const tags = readStringList(frontmatter[tagsField] ?? frontmatter.tags).map((v) => sanitizeForPathSegment(v)).filter(Boolean);
const timeEstimate = frontmatter[estimateField] ?? frontmatter.timeEstimate;
const zettel = generateZettel(now);
const titleLower = title.toLowerCase();
const titleUpper = title.toUpperCase();
const titleSnake = titleLower.replace(/\s+/g, "_");
const titleKebab = titleLower.replace(/\s+/g, "-");
const titleCamel = toCamelCase(title, false);
const titlePascal = toCamelCase(title, true);
const base = {
title,
priority,
status,
dueDate,
scheduledDate,
context: contexts[0] ?? "",
contexts: contexts.join("/"),
project: projects[0] ?? "",
projects: projects.join("/"),
tags: tags.join(", "),
hashtags: tags.map((t) => `#${t}`).join(" "),
timeEstimate: timeEstimate != null ? String(timeEstimate) : "",
details: "",
parentNote: "",
date: format(now, "yyyy-MM-dd"),
time: format(now, "HHmmss"),
timestamp: format(now, "yyyy-MM-dd-HHmmss"),
dateTime: format(now, "yyyy-MM-dd-HHmm"),
year: format(now, "yyyy"),
month: format(now, "MM"),
day: format(now, "dd"),
hour: format(now, "HH"),
minute: format(now, "mm"),
second: format(now, "ss"),
shortDate: format(now, "yyMMdd"),
shortYear: format(now, "yy"),
monthName: format(now, "MMMM"),
monthNameShort: format(now, "MMM"),
dayName: format(now, "EEEE"),
dayNameShort: format(now, "EEE"),
week: format(now, "ww"),
quarter: format(now, "q"),
time12: sanitizeForPathSegment(format(now, "hh:mm a")),
time24: sanitizeForPathSegment(format(now, "HH:mm")),
hourPadded: format(now, "HH"),
hour12: format(now, "hh"),
ampm: format(now, "a"),
unix: String(Math.floor(now.getTime() / 1e3)),
unixMs: String(now.getTime()),
milliseconds: format(now, "SSS"),
ms: format(now, "SSS"),
timezone: sanitizeForPathSegment(format(now, "xxx")),
timezoneShort: sanitizeForPathSegment(format(now, "xx")),
utcOffset: sanitizeForPathSegment(format(now, "xxx")),
utcOffsetShort: sanitizeForPathSegment(format(now, "xx")),
utcZ: "Z",
priorityShort: priority ? priority.substring(0, 1).toUpperCase() : "",
statusShort: status ? status.substring(0, 1).toUpperCase() : "",
titleLower,
titleUpper,
titleSnake,
titleKebab,
titleCamel,
titlePascal,
zettel,
nano: `${Date.now()}${Math.random().toString(36).slice(2, 7)}`
};
Object.assign(values, base);
values[titleField] = title;
values[priorityField] = priority;
values[statusField] = status;
values[dueField] = dueDate;
values[scheduledField] = scheduledDate;
for (const [key, value] of Object.entries(frontmatter)) {
if (values[key] !== void 0) continue;
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
values[key] = sanitizeForPathSegment(String(value));
}
}
return values;
}
function readString(value) {
if (typeof value !== "string") return void 0;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : void 0;
}
function readStringList(value) {
if (!Array.isArray(value)) return [];
return value.filter((v) => typeof v === "string").map((v) => v.trim()).filter(Boolean);
}
function extractProjectName(project) {
const wiki = project.match(/\[\[(?:.*\/)?([^\]|]+)(?:\|[^\]]+)?\]\]/);
if (wiki) return wiki[1];
return project;
}
function toCamelCase(value, pascal) {
const words = value.replace(/[^a-zA-Z0-9\s]/g, " ").trim().split(/\s+/).filter(Boolean);
if (words.length === 0) return "";
return words.map((word, index) => {
const lower = word.toLowerCase();
if (index === 0 && !pascal) return lower;
return lower.charAt(0).toUpperCase() + lower.slice(1);
}).join("");
}
function generateZettel(now) {
const datePart = format(now, "yyMMdd");
const midnight = new Date(now);
midnight.setHours(0, 0, 0, 0);
const secondsSinceMidnight = Math.floor((now.getTime() - midnight.getTime()) / 1e3);
return `${datePart}${secondsSinceMidnight.toString(36)}`;
}
function sanitizeForPathSegment(value) {
return value.trim().replace(/\s+/g, " ").replace(/[<>:"/\\|?*#[\]]/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, "").replace(/^\.+|\.+$/g, "").trim();
}
function normalizeRelativePath(value) {
return value.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "").trim();
}
function ensureMarkdownExt(pathValue) {
const normalized = normalizeRelativePath(pathValue);
if (!normalized) return normalized;
if (normalized.toLowerCase().endsWith(".md")) return normalized;
return `${normalized}.md`;
}
function hasValue(value) {
return value !== null && value !== void 0;
}
export {
createTaskWithCompat
};
// src/date.ts
import { isValid, parseISO } from "date-fns";
var DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
var DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,3})?(?:Z|([+-])(\d{2}):(\d{2}))?$/;
var RELAXED_DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})(?:T| )(\d{2}):(\d{2})(?::(\d{2})(\.\d{1,3})?)?(Z|([+-])(\d{2}):(\d{2}))?$/;
function parseDateToUTC(dateString) {
if (!dateString || dateString.trim().length === 0) {
throw new Error("Date string cannot be empty");
}
const trimmed = dateString.trim();
const dateOnlyMatch = trimmed.match(DATE_ONLY_RE);
if (dateOnlyMatch) {
const [, year, month, day] = dateOnlyMatch;
const y = Number(year);
const m = Number(month);
const d = Number(day);
const parsed2 = new Date(Date.UTC(y, m - 1, d, 0, 0, 0, 0));
if (parsed2.getUTCFullYear() !== y || parsed2.getUTCMonth() !== m - 1 || parsed2.getUTCDate() !== d) {
throw new Error(`Invalid date "${dateString}".`);
}
return parsed2;
}
if (!isStrictDateTime(trimmed)) {
throw new Error(`Invalid date "${dateString}".`);
}
const parsed = parseISO(trimmed);
if (!isValid(parsed)) {
throw new Error(`Invalid date "${dateString}".`);
}
return parsed;
}
function parseDateToLocal(dateString) {
if (!dateString || dateString.trim().length === 0) {
throw new Error("Date string cannot be empty");
}
const trimmed = dateString.trim();
const dateOnlyMatch = trimmed.match(DATE_ONLY_RE);
if (dateOnlyMatch) {
const [, year, month, day] = dateOnlyMatch;
const y = Number(year);
const m = Number(month);
const d = Number(day);
const parsed2 = new Date(y, m - 1, d, 0, 0, 0, 0);
if (parsed2.getFullYear() !== y || parsed2.getMonth() !== m - 1 || parsed2.getDate() !== d) {
throw new Error(`Invalid date "${dateString}".`);
}
return parsed2;
}
if (!isStrictDateTime(trimmed)) {
throw new Error(`Invalid date "${dateString}".`);
}
const parsed = parseISO(trimmed);
if (!isValid(parsed)) {
throw new Error(`Invalid date "${dateString}".`);
}
return parsed;
}
function formatDateForStorage(date) {
if (!date || Number.isNaN(date.getTime())) {
return "";
}
const y = date.getUTCFullYear();
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
const d = String(date.getUTCDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
function getCurrentDateString() {
const now = /* @__PURE__ */ new Date();
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, "0");
const d = String(now.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
function resolveDateOrToday(date) {
if (!date) {
return getCurrentDateString();
}
return validateDateString(date);
}
function resolveOperationTargetDate(explicitDate, scheduled, due) {
if (explicitDate) {
return validateDateString(explicitDate);
}
const scheduledDatePart = extractValidDatePartOrUndefined(scheduled);
if (scheduledDatePart) {
return scheduledDatePart;
}
const dueDatePart = extractValidDatePartOrUndefined(due);
if (dueDatePart) {
return dueDatePart;
}
return getCurrentDateString();
}
function validateDateString(date) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
throw new Error(`Invalid date "${date}". Expected YYYY-MM-DD.`);
}
parseDateToUTC(date);
return date;
}
function resolveDateTimeRangeBound(value, bound) {
if (!value || value.trim().length === 0) {
throw new Error("Datetime cannot be empty.");
}
const trimmed = value.trim();
const dateOnlyMatch = trimmed.match(DATE_ONLY_RE);
if (dateOnlyMatch) {
const [, year2, month2, day2] = dateOnlyMatch;
const y2 = Number(year2);
const m2 = Number(month2);
const d2 = Number(day2);
if (!isValidCalendarDate(y2, m2, d2)) {
throw new Error(`Invalid datetime "${value}".`);
}
return bound === "from" ? new Date(y2, m2 - 1, d2, 0, 0, 0, 0) : new Date(y2, m2 - 1, d2, 23, 59, 59, 999);
}
const match = trimmed.match(RELAXED_DATE_TIME_RE);
if (!match) {
throw new Error(
`Invalid datetime "${value}". Expected YYYY-MM-DD, YYYY-MM-DD HH:mm, or YYYY-MM-DDTHH:mm.`
);
}
const [, year, month, day, hours, minutes, seconds, fraction, tz, tzSign, tzHours, tzMinutes] = match;
const y = Number(year);
const m = Number(month);
const d = Number(day);
const hh = Number(hours);
const mm = Number(minutes);
const ss = seconds === void 0 ? bound === "to" ? 59 : 0 : Number(seconds);
const ms = fraction ? Number(fraction.slice(1).padEnd(3, "0")) : bound === "to" ? 999 : 0;
if (!isValidCalendarDate(y, m, d) || !isValidClockTime(hh, mm, ss) || !isValidOffset(tzSign, tzHours, tzMinutes)) {
throw new Error(`Invalid datetime "${value}".`);
}
const normalized = `${year}-${month}-${day}T${hours}:${minutes}:${String(ss).padStart(2, "0")}.${String(ms).padStart(3, "0")}${tz || ""}`;
const parsed = parseISO(normalized);
if (!isValid(parsed)) {
throw new Error(`Invalid datetime "${value}".`);
}
return parsed;
}
function hasTimeComponent(dateString) {
if (!dateString) return false;
return /T\d{2}:\d{2}/.test(dateString);
}
function getDatePart(dateString) {
if (!dateString) return "";
if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
return dateString;
}
const tIndex = dateString.indexOf("T");
if (tIndex > -1) {
return dateString.slice(0, tIndex);
}
return formatDateForStorage(parseDateToUTC(dateString));
}
function extractValidDatePartOrUndefined(dateString) {
if (!dateString || dateString.trim().length === 0) {
return void 0;
}
try {
const datePart = getDatePart(dateString.trim());
return validateDateString(datePart);
} catch {
return void 0;
}
}
function isSameDateSafe(date1, date2) {
try {
const d1 = parseDateToUTC(getDatePart(date1));
const d2 = parseDateToUTC(getDatePart(date2));
return d1.getTime() === d2.getTime();
} catch {
return false;
}
}
function isBeforeDateSafe(date1, date2) {
try {
const d1 = parseDateToUTC(getDatePart(date1));
const d2 = parseDateToUTC(getDatePart(date2));
return d1.getTime() < d2.getTime();
} catch {
return false;
}
}
function isStrictDateTime(value) {
const match = value.match(DATE_TIME_RE);
if (!match) return false;
const [, year, month, day, hours, minutes, seconds, , tzSign, tzHours, tzMinutes] = match;
const y = Number(year);
const m = Number(month);
const d = Number(day);
const hh = Number(hours);
const mm = Number(minutes);
const ss = Number(seconds);
if (!isValidClockTime(hh, mm, ss) || !isValidCalendarDate(y, m, d)) {
return false;
}
return isValidOffset(tzSign, tzHours, tzMinutes);
}
function isValidCalendarDate(year, month, day) {
const date = new Date(Date.UTC(year, month - 1, day, 0, 0, 0, 0));
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
}
function isValidClockTime(hours, minutes, seconds) {
return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59 && seconds >= 0 && seconds <= 59;
}
function isValidOffset(tzSign, tzHours, tzMinutes) {
if (!tzSign) return true;
const offsetHours = Number(tzHours);
const offsetMinutes = Number(tzMinutes);
if (offsetHours > 14 || offsetMinutes > 59) return false;
if (offsetHours === 14 && offsetMinutes !== 0) return false;
return true;
}
export {
formatDateForStorage,
getCurrentDateString,
getDatePart,
hasTimeComponent,
isBeforeDateSafe,
isSameDateSafe,
parseDateToLocal,
parseDateToUTC,
resolveDateOrToday,
resolveDateTimeRangeBound,
resolveOperationTargetDate,
validateDateString
};
// src/field-mapping.ts
import { loadConfig, getType } from "@callumalpass/mdbase";
import { basename } from "path";
// src/config.ts
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
var CONFIG_DIR = path.join(
os.homedir(),
".config",
"mdbase-tasknotes"
);
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
var DEFAULT_CONFIG = {
collectionPath: null,
language: "en"
};
function load() {
try {
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
} catch {
return { ...DEFAULT_CONFIG };
}
}
function resolveUserPath(userPath) {
return path.resolve(expandHomeDirectory(userPath));
}
function resolveCollectionPath(flagPath) {
if (flagPath) return resolveUserPath(flagPath);
const envPath = process.env.MDBASE_TASKNOTES_PATH;
if (envPath) return resolveUserPath(envPath);
const config = load();
if (config.collectionPath) return resolveUserPath(config.collectionPath);
return process.cwd();
}
function expandHomeDirectory(userPath) {
if (userPath === "~") {
return os.homedir();
}
if (userPath.startsWith("~/") || userPath.startsWith("~\\")) {
return path.join(os.homedir(), userPath.slice(2));
}
return userPath;
}
// src/field-mapping.ts
var ALL_ROLES = [
"title",
"status",
"priority",
"due",
"scheduled",
"completedDate",
"tags",
"contexts",
"projects",
"timeEstimate",
"dateCreated",
"dateModified",
"recurrence",
"recurrenceAnchor",
"completeInstances",
"skippedInstances",
"timeEntries"
];
function defaultFieldMapping() {
const roleToField = {};
const fieldToRole = {};
for (const role of ALL_ROLES) {
roleToField[role] = role;
fieldToRole[role] = role;
}
return {
roleToField,
fieldToRole,
displayNameKey: "title",
completedStatuses: ["done", "cancelled"]
};
}
function buildFieldMapping(fields, displayNameKey) {
const roleToField = {};
const fieldToRole = {};
const rolesSet = new Set(ALL_ROLES);
for (const [fieldName, def] of Object.entries(fields)) {
if (def && typeof def === "object" && typeof def.tn_role === "string") {
const role = def.tn_role;
if (!rolesSet.has(role)) continue;
if (roleToField[role] !== void 0) {
console.warn(`[mtn] Duplicate tn_role "${role}" on field "${fieldName}", ignoring.`);
continue;
}
roleToField[role] = fieldName;
fieldToRole[fieldName] = role;
}
}
for (const role of ALL_ROLES) {
if (roleToField[role] === void 0) {
if (fields[role] !== void 0) {
roleToField[role] = role;
if (fieldToRole[role] === void 0) {
fieldToRole[role] = role;
}
} else {
roleToField[role] = role;
}
}
}
const completedStatuses = inferCompletedStatuses(fields, roleToField.status);
return {
roleToField,
fieldToRole,
displayNameKey: displayNameKey && typeof displayNameKey === "string" && displayNameKey.trim().length > 0 ? displayNameKey : roleToField.title,
completedStatuses
};
}
function inferCompletedStatuses(fields, statusFieldName) {
const statusDef = fields[statusFieldName];
if (!statusDef || typeof statusDef !== "object") {
return ["done", "cancelled"];
}
if (Array.isArray(statusDef.tn_completed_values)) {
const explicit = statusDef.tn_completed_values.filter((v) => typeof v === "string").map((v) => v.trim()).filter((v) => v.length > 0);
if (explicit.length > 0) return explicit;
}
if (Array.isArray(statusDef.values)) {
const inferred = statusDef.values.filter((v) => typeof v === "string").filter((v) => {
const lower = v.toLowerCase();
return lower.includes("done") || lower.includes("complete") || lower.includes("cancel") || lower.includes("finish");
});
if (inferred.length > 0) return inferred;
}
return ["done", "cancelled"];
}
function isCompletedStatus(mapping, status) {
if (!status) return false;
return mapping.completedStatuses.includes(status);
}
function getDefaultCompletedStatus(mapping) {
return mapping.completedStatuses[0] || "done";
}
async function loadFieldMapping(flagPath) {
try {
const collectionPath = resolveCollectionPath(flagPath);
const configResult = await loadConfig(collectionPath);
if (!configResult.valid || !configResult.config) {
return defaultFieldMapping();
}
const typeResult = await getType(collectionPath, configResult.config, "task");
if (!typeResult.valid || !typeResult.type) {
return defaultFieldMapping();
}
const displayNameKey = typeof typeResult.type.display_name_key === "string" ? typeResult.type.display_name_key : typeof typeResult.type.displayNameKey === "string" ? typeResult.type.displayNameKey : void 0;
return buildFieldMapping(typeResult.type.fields || {}, displayNameKey);
} catch {
return defaultFieldMapping();
}
}
function normalizeFrontmatter(raw, mapping) {
const result = {};
for (const [key, value] of Object.entries(raw)) {
const role = mapping.fieldToRole[key];
result[role ?? key] = value;
}
return result;
}
function denormalizeFrontmatter(roleData, mapping) {
const result = {};
const rolesSet = new Set(ALL_ROLES);
for (const [key, value] of Object.entries(roleData)) {
if (rolesSet.has(key)) {
result[mapping.roleToField[key]] = value;
} else {
result[key] = value;
}
}
return result;
}
function resolveField(mapping, role) {
return mapping.roleToField[role];
}
function resolveDisplayTitle(frontmatter, mapping, taskPath) {
const candidates = [mapping.displayNameKey, "title"];
const seen = /* @__PURE__ */ new Set();
for (const key of candidates) {
if (seen.has(key)) continue;
seen.add(key);
const value = frontmatter[key];
if (typeof value === "string" && value.trim().length > 0) {
return value;
}
}
if (typeof taskPath === "string" && taskPath.trim().length > 0) {
const fromPath = basename(taskPath, ".md").trim();
if (fromPath.length > 0) {
return fromPath;
}
}
return void 0;
}
export {
buildFieldMapping,
defaultFieldMapping,
denormalizeFrontmatter,
getDefaultCompletedStatus,
isCompletedStatus,
loadFieldMapping,
normalizeFrontmatter,
resolveDisplayTitle,
resolveField
};
// src/mapper.ts
function mapToFrontmatter(parsed) {
const fm = {};
fm.title = parsed.title;
if (parsed.dueDate) fm.due = parsed.dueDate;
if (parsed.scheduledDate) fm.scheduled = parsed.scheduledDate;
if (parsed.priority) fm.priority = parsed.priority;
if (parsed.status) fm.status = parsed.status;
if (parsed.tags && parsed.tags.length > 0) fm.tags = parsed.tags;
if (parsed.contexts && parsed.contexts.length > 0) fm.contexts = parsed.contexts;
if (parsed.projects && parsed.projects.length > 0) {
fm.projects = parsed.projects.map(toProjectWikilink);
}
if (parsed.recurrence) fm.recurrence = parsed.recurrence;
if (parsed.estimate) fm.timeEstimate = parsed.estimate;
const body = parsed.details || void 0;
return { frontmatter: fm, body };
}
function toProjectWikilink(project) {
const trimmed = project.trim();
return isWikilink(trimmed) ? trimmed : `[[projects/${trimmed}]]`;
}
function isWikilink(value) {
return /^\[\[[^\]]+\]\]$/.test(value);
}
function extractProjectNames(projects) {
if (!projects) return [];
return projects.filter(Boolean).map((p) => {
const match = p.match(/\[\[(?:.*\/)?([^\]]+)\]\]/);
return match ? match[1] : p;
});
}
export {
extractProjectNames,
mapToFrontmatter
};
// src/recurrence.ts
import { createRequire } from "module";
// src/date.ts
import { isValid, parseISO } from "date-fns";
var DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
var DATE_TIME_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,3})?(?:Z|([+-])(\d{2}):(\d{2}))?$/;
function parseDateToUTC(dateString) {
if (!dateString || dateString.trim().length === 0) {
throw new Error("Date string cannot be empty");
}
const trimmed = dateString.trim();
const dateOnlyMatch = trimmed.match(DATE_ONLY_RE);
if (dateOnlyMatch) {
const [, year, month, day] = dateOnlyMatch;
const y = Number(year);
const m = Number(month);
const d = Number(day);
const parsed2 = new Date(Date.UTC(y, m - 1, d, 0, 0, 0, 0));
if (parsed2.getUTCFullYear() !== y || parsed2.getUTCMonth() !== m - 1 || parsed2.getUTCDate() !== d) {
throw new Error(`Invalid date "${dateString}".`);
}
return parsed2;
}
if (!isStrictDateTime(trimmed)) {
throw new Error(`Invalid date "${dateString}".`);
}
const parsed = parseISO(trimmed);
if (!isValid(parsed)) {
throw new Error(`Invalid date "${dateString}".`);
}
return parsed;
}
function isStrictDateTime(value) {
const match = value.match(DATE_TIME_RE);
if (!match) return false;
const [, year, month, day, hours, minutes, seconds, , tzSign, tzHours, tzMinutes] = match;
const y = Number(year);
const m = Number(month);
const d = Number(day);
const hh = Number(hours);
const mm = Number(minutes);
const ss = Number(seconds);
if (!isValidClockTime(hh, mm, ss) || !isValidCalendarDate(y, m, d)) {
return false;
}
return isValidOffset(tzSign, tzHours, tzMinutes);
}
function isValidCalendarDate(year, month, day) {
const date = new Date(Date.UTC(year, month - 1, day, 0, 0, 0, 0));
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
}
function isValidClockTime(hours, minutes, seconds) {
return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59 && seconds >= 0 && seconds <= 59;
}
function isValidOffset(tzSign, tzHours, tzMinutes) {
if (!tzSign) return true;
const offsetHours = Number(tzHours);
const offsetMinutes = Number(tzMinutes);
if (offsetHours > 14 || offsetMinutes > 59) return false;
if (offsetHours === 14 && offsetMinutes !== 0) return false;
return true;
}
// src/recurrence.ts
var require2 = createRequire(import.meta.url);
var { RRule } = require2("rrule");
var DTSTART_RE = /DTSTART:(\d{8}(?:T\d{6}Z?)?);?/;
function completeRecurringTask(input) {
const completionDate = input.completionDate;
const completeInstances = Array.isArray(input.completeInstances) ? [...input.completeInstances] : [];
const skippedInstances = Array.isArray(input.skippedInstances) ? [...input.skippedInstances] : [];
if (!completeInstances.includes(completionDate)) {
completeInstances.push(completionDate);
}
const nextSkippedInstances = skippedInstances.filter((d) => d !== completionDate);
const schedule = recalculateRecurringScheduleInternal({
recurrence: input.recurrence,
recurrenceAnchor: input.recurrenceAnchor,
scheduled: input.scheduled,
due: input.due,
dateCreated: input.dateCreated,
completeInstances,
skippedInstances: nextSkippedInstances,
referenceDate: completionDate,
completionDateForAnchor: completionDate
});
return {
updatedRecurrence: schedule.updatedRecurrence,
nextScheduled: schedule.nextScheduled,
nextDue: schedule.nextDue,
completeInstances,
skippedInstances: nextSkippedInstances
};
}
function recalculateRecurringSchedule(input) {
return recalculateRecurringScheduleInternal({
...input
});
}
function recalculateRecurringScheduleInternal(input) {
const anchor = input.recurrenceAnchor === "completion" ? "completion" : "scheduled";
const sourceDate = input.scheduled || input.dateCreated || input.referenceDate;
let updatedRecurrence = input.recurrence;
if (anchor === "completion") {
const anchorDate = input.completionDateForAnchor || input.referenceDate || sourceDate;
updatedRecurrence = updateDTSTARTInRecurrenceRule(updatedRecurrence, anchorDate) || updatedRecurrence;
} else {
updatedRecurrence = addDTSTARTToRecurrenceRule(updatedRecurrence, sourceDate) || updatedRecurrence;
}
const referenceDate = parseDateString(input.referenceDate) || parseDateString(input.scheduled);
if (!referenceDate) {
return { updatedRecurrence, nextScheduled: null, nextDue: null };
}
const completionDay = parseDateString(input.referenceDate);
const completeInstances = Array.isArray(input.completeInstances) ? input.completeInstances : [];
const skippedInstances = Array.isArray(input.skippedInstances) ? input.skippedInstances : [];
const processedDates = /* @__PURE__ */ new Set([
...completeInstances,
...skippedInstances,
formatDateUTC(referenceDate)
]);
let nextOccurrence = getNextOccurrenceDate(updatedRecurrence, sourceDate, referenceDate, true);
if (completionDay) {
let guard = 0;
while (nextOccurrence && nextOccurrence.getTime() < completionDay.getTime() && guard < 1e3) {
nextOccurrence = getNextOccurrenceDate(
updatedRecurrence,
sourceDate,
nextOccurrence,
false
);
guard++;
}
}
let processedGuard = 0;
while (nextOccurrence && processedGuard < 1e3) {
const dateStr = formatDateUTC(nextOccurrence);
if (!processedDates.has(dateStr)) break;
nextOccurrence = getNextOccurrenceDate(updatedRecurrence, sourceDate, nextOccurrence, false);
processedGuard++;
}
if (!nextOccurrence) {
return { updatedRecurrence, nextScheduled: null, nextDue: null };
}
const nextScheduled = formatLikeExisting(input.scheduled, nextOccurrence);
const nextDue = computeNextDue(input, nextOccurrence);
return { updatedRecurrence, nextScheduled, nextDue };
}
function computeNextDue(input, nextScheduledDate) {
if (!input.due || !input.scheduled) {
return null;
}
const originalDue = parseDateString(input.due);
const originalScheduled = parseDateString(input.scheduled);
if (!originalDue || !originalScheduled) {
return null;
}
const offsetMs = originalDue.getTime() - originalScheduled.getTime();
const nextDueDate = new Date(nextScheduledDate.getTime() + offsetMs);
return formatLikeExisting(input.due, nextDueDate);
}
function getNextOccurrenceDate(recurrence, sourceDate, afterDate, inclusive) {
const rule = buildRRule(recurrence, sourceDate);
if (!rule) return null;
return rule.after(afterDate, inclusive);
}
function buildRRule(recurrence, sourceDate) {
try {
const dtstartMatch = recurrence.match(DTSTART_RE);
const rruleString = recurrence.replace(DTSTART_RE, "").replace(/^;/, "").trim();
if (!rruleString.includes("FREQ=")) {
return null;
}
const options = RRule.parseString(rruleString);
const dtstart = parseDTSTARTValue(dtstartMatch?.[1]) || parseDateString(sourceDate);
if (dtstart) {
options.dtstart = dtstart;
}
return new RRule(options);
} catch {
return null;
}
}
function addDTSTARTToRecurrenceRule(recurrence, sourceDate) {
if (!recurrence || recurrence.includes("DTSTART:")) {
return recurrence;
}
const dtstart = formatDTSTARTValue(sourceDate);
if (!dtstart) return null;
return `DTSTART:${dtstart};${recurrence}`;
}
function updateDTSTARTInRecurrenceRule(recurrence, dateStr) {
if (!recurrence) return null;
const dtstart = formatDTSTARTValue(dateStr);
if (!dtstart) return null;
if (recurrence.includes("DTSTART:")) {
return recurrence.replace(DTSTART_RE, `DTSTART:${dtstart};`);
}
return `DTSTART:${dtstart};${recurrence}`;
}
function formatDTSTARTValue(dateStr) {
if (!dateStr) return null;
if (dateStr.includes("T")) {
const parsed2 = parseDateString(dateStr);
if (!parsed2) return null;
const year2 = parsed2.getUTCFullYear();
const month2 = String(parsed2.getUTCMonth() + 1).padStart(2, "0");
const day2 = String(parsed2.getUTCDate()).padStart(2, "0");
const hours = String(parsed2.getUTCHours()).padStart(2, "0");
const minutes = String(parsed2.getUTCMinutes()).padStart(2, "0");
const seconds = String(parsed2.getUTCSeconds()).padStart(2, "0");
return `${year2}${month2}${day2}T${hours}${minutes}${seconds}Z`;
}
const parsed = parseDateString(dateStr);
if (!parsed) return null;
const year = parsed.getUTCFullYear();
const month = String(parsed.getUTCMonth() + 1).padStart(2, "0");
const day = String(parsed.getUTCDate()).padStart(2, "0");
return `${year}${month}${day}`;
}
function parseDTSTARTValue(value) {
if (!value) return null;
if (value.length === 8) {
const year = Number(value.slice(0, 4));
const month = Number(value.slice(4, 6)) - 1;
const day = Number(value.slice(6, 8));
return new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
}
const dtMatch = value.match(
/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z?$/
);
if (!dtMatch) return null;
const [, y, m, d, hh, mm, ss] = dtMatch;
return new Date(
Date.UTC(Number(y), Number(m) - 1, Number(d), Number(hh), Number(mm), Number(ss), 0)
);
}
function parseDateString(dateStr) {
if (!dateStr) return null;
try {
return parseDateToUTC(dateStr);
} catch {
return null;
}
}
function formatLikeExisting(existingValue, date) {
const datePart = formatDateUTC(date);
if (existingValue && existingValue.includes("T")) {
return `${datePart}T${existingValue.split("T")[1]}`;
}
return datePart;
}
function formatDateUTC(date) {
const y = date.getUTCFullYear();
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
const d = String(date.getUTCDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
export {
completeRecurringTask,
recalculateRecurringSchedule
};
+3
-3
{
"name": "mdbase-tasknotes",
"version": "0.1.2",
"version": "0.1.3",
"description": "Standalone CLI for managing markdown tasks via mdbase",

@@ -35,3 +35,3 @@ "type": "module",

"dependencies": {
"@callumalpass/mdbase": "^0.2.1",
"@callumalpass/mdbase": "^0.2.2",
"chalk": "^4.1.2",

@@ -45,3 +45,3 @@ "commander": "^12.1.0",

"@types/node": "^20.0.0",
"tasknotes-nlp-core": "^0.1.0",
"tasknotes-nlp-core": "^0.1.2",
"tsup": "^8.0.0",

@@ -48,0 +48,0 @@ "typescript": "^5.5.0"

@@ -94,4 +94,20 @@ # mdbase-tasknotes

## Creating Tasks With Custom Paths
`match.path_glob` and `path_pattern` do different jobs in `_types/task.md`:
- `match.path_glob` tells mdbase which existing files should be treated as tasks.
- `path_pattern` tells `mtn create` where to write a new task file.
If your task type only has `match.path_glob`, listing existing tasks can work, but creating a new task without an explicit path cannot choose a filename. Add `path_pattern` for creation:
```yaml
path_pattern: "calendar/{{year}}/{{month}}-{{monthNameShort}}/{{titleKebab}}.md"
match:
path_glob: "calendar/**/*.md"
```
## License
MIT

Sorry, the diff of this file is too big to display