UNPKG

@gonzui/claude-task-manager

Version:

Task management extension for Claude Code with archiving and history

169 lines 6.36 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.TaskStore = void 0; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const types_1 = require("../types"); const ProgressTracker_1 = require("./ProgressTracker"); /** * Storage for multiple named tasks. * * `task.md` stays the live file of the *active* task — every other manager * keeps reading and writing exactly that path, unaware of names. This store * owns the per-name snapshots under `.claude-tasks/tasks/<name>.md`, which are * authoritative for the *inactive* tasks. Switching means: write the active * task back to its snapshot, then copy the target snapshot over `task.md`. * * Copies rather than symlinks, deliberately: symlinks are unreliable on * Windows. Multi-task mode is on iff the tasks directory exists, so projects * that never switch behave exactly as they did before. */ class TaskStore { constructor(tasksDir, taskFile, i18n) { this.tasksDir = tasksDir; this.taskFile = taskFile; this.i18n = i18n; } async isEnabled() { return await fs.pathExists(this.tasksDir); } async enable() { await fs.ensureDir(this.tasksDir); } /** * Normalize a user-supplied task name into a safe single path segment. * Throws rather than silently mangling names that could escape the store. */ sanitizeName(name) { const raw = (name || '').trim(); const withoutExt = raw.endsWith('.md') ? raw.slice(0, -3) : raw; const normalized = withoutExt.trim().replace(/\s+/g, '-'); const invalid = normalized === '' || normalized === '.' || normalized === '..' || /[/\\]/.test(normalized) || /[\u0000-\u001f]/.test(normalized); if (invalid) { throw new types_1.TaskManagerError(this.i18n.t('errors.invalidTaskName', { name: raw }), 'INVALID_TASK_NAME'); } return normalized; } getTaskPath(name) { return path.join(this.tasksDir, `${this.sanitizeName(name)}.md`); } async exists(name) { return await fs.pathExists(this.getTaskPath(name)); } async listNames() { if (!await this.isEnabled()) { return []; } const entries = await fs.readdir(this.tasksDir); return entries .filter((file) => file.endsWith('.md')) .map((file) => file.slice(0, -3)) .sort(); } /** * All stored tasks with their titles and subtask progress. The active task is * read from `task.md` so its numbers are live rather than from its snapshot. */ async listTasks(activeName) { const names = await this.listNames(); const items = []; for (const name of names) { const active = name === activeName; const source = active ? this.taskFile : this.getTaskPath(name); let content = ''; if (await fs.pathExists(source)) { content = await fs.readFile(source, 'utf8'); } const progress = (0, ProgressTracker_1.parseProgressContent)(content); items.push({ name, title: progress.title, active, completed: progress.completed, total: progress.total, percentage: progress.percentage }); } return items; } /** Write the live task.md back to `<name>`'s snapshot. */ async syncActive(name) { if (!await fs.pathExists(this.taskFile)) { return; } await this.enable(); await fs.copy(this.taskFile, this.getTaskPath(name), { overwrite: true }); } /** Make `<name>` the live task.md. The caller must have synced the previous one. */ async activate(name) { const source = this.getTaskPath(name); if (!await fs.pathExists(source)) { throw new types_1.TaskManagerError(this.i18n.t('errors.unknownTask', { name }), 'UNKNOWN_TASK'); } await fs.copy(source, this.taskFile, { overwrite: true }); } async remove(name) { await fs.remove(this.getTaskPath(name)); } /** * Derive a store name from a task title, used when migrating a legacy * project whose task.md has no name yet. */ async deriveName(title) { const slug = (title || '') .trim() .replace(/\s+/g, '-') .replace(/[/\\]/g, '-') .replace(/[\u0000-\u001f]/g, '') .replace(/^[.]+/, '') .slice(0, 60); const base = slug || 'task'; if (!await this.exists(base)) { return base; } let suffix = 2; while (await this.exists(`${base}-${suffix}`)) { suffix++; } return `${base}-${suffix}`; } } exports.TaskStore = TaskStore; //# sourceMappingURL=TaskStore.js.map