@gonzui/claude-task-manager
Version:
Task management extension for Claude Code with archiving and history
169 lines • 6.36 kB
JavaScript
;
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