@xxkeefer/mrkl 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +35 -6
  2. package/dist/cli.mjs +102 -10
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -80,6 +80,7 @@ mrkl x PROJ-002 # close
80
80
  | `done` | `d` | Mark a task as done and archive it |
81
81
  | `close` | `x` | Close a task (won't do, duplicate, etc.) and archive it |
82
82
  | `prune` | `p` | Delete archived tasks created on or before a given date |
83
+ | `migrate_prior_verbose` | — | Migrate legacy verbose-filename tasks to frontmatter-based format |
83
84
  | `install-skills` | — | Install bundled Claude Code skills |
84
85
 
85
86
  ### `mrkl init <prefix>`
@@ -175,6 +176,25 @@ mrkl prune 2026-01-31
175
176
  mrkl prune 2026-01-31 --force
176
177
  ```
177
178
 
179
+ ### `mrkl migrate_prior_verbose`
180
+
181
+ Migrates task files from the legacy verbose-filename format to the current format. This is a **one-time migration** for projects that were using mrkl before v0.4.0.
182
+
183
+ **What it does:**
184
+
185
+ 1. Scans all task files in `.tasks/` and `.tasks/.archive/`
186
+ 2. Extracts the title from the verbose filename (e.g., `PROJ-001 feat - user auth.md`)
187
+ 3. Writes the title into YAML frontmatter
188
+ 4. If `verbose_files = false` (default): renames files to short format (`PROJ-001.md`)
189
+ 5. If `verbose_files = true`: keeps verbose filenames as-is
190
+
191
+ ```sh
192
+ mrkl migrate_prior_verbose
193
+ # ✅ Migrated 12 file(s), skipped 3 file(s).
194
+ ```
195
+
196
+ > **Note:** After upgrading to v0.4.0+, existing task files will fail to parse until migrated. Run this command once to fix them.
197
+
178
198
  ### `mrkl install-skills`
179
199
 
180
200
  Installs bundled Claude Code skills into the current project.
@@ -211,15 +231,16 @@ mrkl uses [conventional commit](https://www.conventionalcommits.org/) types:
211
231
 
212
232
  ## Task File Format 📄
213
233
 
214
- Each task is a markdown file with YAML frontmatter:
234
+ Each task is a markdown file with YAML frontmatter. By default, filenames use the short format:
215
235
 
216
236
  ```
217
- .tasks/PROJ-001 feat - user authentication.md
237
+ .tasks/PROJ-001.md
218
238
  ```
219
239
 
220
240
  ```markdown
221
241
  ---
222
242
  id: PROJ-001
243
+ title: user authentication
223
244
  type: feat
224
245
  status: todo
225
246
  created: '2026-03-01'
@@ -235,7 +256,13 @@ Implement user authentication with OAuth2.
235
256
  - [ ] session persists across refreshes
236
257
  ```
237
258
 
238
- The format is intentionally simple edit task files directly when you need to update descriptions, change status, or check off criteria.
259
+ With `verbose_files = true`, filenames include the type and title:
260
+
261
+ ```
262
+ .tasks/PROJ-001 feat - user authentication.md
263
+ ```
264
+
265
+ The `title` is always stored in frontmatter regardless of filename format. Edit task files directly when you need to update descriptions, change status, or check off criteria.
239
266
 
240
267
  ## Project Structure 🗂️
241
268
 
@@ -247,10 +274,10 @@ your-project/
247
274
  mrkl.toml # project configuration
248
275
  mrkl_counter # current task number
249
276
  .tasks/
250
- PROJ-001 feat - first task.md
251
- PROJ-002 fix - second task.md
277
+ PROJ-001.md
278
+ PROJ-002.md
252
279
  .archive/
253
- PROJ-000 chore - archived task.md
280
+ PROJ-000.md
254
281
  ```
255
282
 
256
283
  Commit `.config/mrkl/` and `.tasks/` to version control. They're designed to be tracked alongside your code.
@@ -286,12 +313,14 @@ Configuration lives in `.config/mrkl/mrkl.toml` (or `mrkl.toml` at the project r
286
313
  ```toml
287
314
  prefix = "PROJ"
288
315
  tasks_dir = ".tasks"
316
+ verbose_files = false
289
317
  ```
290
318
 
291
319
  | Key | Default | Description |
292
320
  |-----|---------|-------------|
293
321
  | `prefix` | *(required)* | Project prefix for task IDs |
294
322
  | `tasks_dir` | `".tasks"` | Directory for task files |
323
+ | `verbose_files` | `false` | Use verbose filenames (`PROJ-001 feat - title.md` vs `PROJ-001.md`) |
295
324
 
296
325
  ## Development 🧑‍💻
297
326
 
package/dist/cli.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain } from 'citty';
3
3
  import consola from 'consola';
4
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync, statSync } from 'node:fs';
5
- import { join, dirname } from 'node:path';
4
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync, renameSync, statSync } from 'node:fs';
5
+ import { join, basename, dirname } from 'node:path';
6
6
  import { stringify, parse as parse$1 } from 'smol-toml';
7
7
  import matter from 'gray-matter';
8
8
  import { fileURLToPath } from 'node:url';
@@ -20,7 +20,8 @@ function loadConfig(dir) {
20
20
  const parsed = parse$1(raw);
21
21
  return {
22
22
  prefix: parsed.prefix,
23
- tasks_dir: parsed.tasks_dir ?? ".tasks"
23
+ tasks_dir: parsed.tasks_dir ?? ".tasks",
24
+ verbose_files: parsed.verbose_files ?? false
24
25
  };
25
26
  }
26
27
  function initConfig(dir, opts) {
@@ -29,7 +30,8 @@ function initConfig(dir, opts) {
29
30
  if (!existsSync(configPath)) {
30
31
  const config2 = {
31
32
  prefix: opts?.prefix ?? "TASK",
32
- tasks_dir: opts?.tasks_dir ?? ".tasks"
33
+ tasks_dir: opts?.tasks_dir ?? ".tasks",
34
+ verbose_files: opts?.verbose_files ?? false
33
35
  };
34
36
  mkdirSync(configDir, { recursive: true });
35
37
  writeFileSync(configPath, stringify(config2));
@@ -78,6 +80,7 @@ function nextId(dir) {
78
80
  function render(task) {
79
81
  const frontmatter = {
80
82
  id: task.id,
83
+ title: task.title,
81
84
  type: task.type,
82
85
  status: task.status,
83
86
  created: task.created
@@ -97,8 +100,12 @@ function render(task) {
97
100
  }
98
101
  function parse(content, filename) {
99
102
  const { data, content: body } = matter(content);
100
- const titleMatch = filename.replace(/\.md$/, "").match(/^\S+\s+\S+\s+-\s+(.+)$/);
101
- const title = titleMatch ? titleMatch[1] : "";
103
+ const title = data.title;
104
+ if (!title) {
105
+ throw new Error(
106
+ "Task file missing title in frontmatter. Run 'mrkl migrate_prior_verbose' to fix."
107
+ );
108
+ }
102
109
  const acRegex = /^- \[[ x]\] (.+)$/gm;
103
110
  const acceptance_criteria = [];
104
111
  let match;
@@ -137,7 +144,7 @@ function createTask(opts) {
137
144
  description: opts.description ?? "",
138
145
  acceptance_criteria: opts.acceptance_criteria ?? []
139
146
  };
140
- const filename = `${id} ${task.type} - ${task.title}.md`;
147
+ const filename = config.verbose_files ? `${id} ${task.type} - ${task.title}.md` : `${id}.md`;
141
148
  const tasksDir = join(opts.dir, config.tasks_dir);
142
149
  writeFileSync(join(tasksDir, filename), render(task));
143
150
  return task;
@@ -172,7 +179,7 @@ function archiveTask(dir, id) {
172
179
  }
173
180
  const filePath = join(tasksDir, file);
174
181
  const content = readFileSync(filePath, "utf-8");
175
- const task = parse(content, file);
182
+ const task = parse(content);
176
183
  task.status = "done";
177
184
  const archivePath = join(tasksDir, ".archive", file);
178
185
  writeFileSync(archivePath, render(task));
@@ -236,7 +243,7 @@ function closeTask(dir, id) {
236
243
  }
237
244
  const filePath = join(tasksDir, file);
238
245
  const content = readFileSync(filePath, "utf-8");
239
- const task = parse(content, file);
246
+ const task = parse(content);
240
247
  task.status = "closed";
241
248
  const archivePath = join(tasksDir, ".archive", file);
242
249
  writeFileSync(archivePath, render(task));
@@ -282,7 +289,7 @@ async function promptForTask(dir) {
282
289
  criteria.length === 0 ? "Acceptance criterion (Esc to skip)" : `Criterion #${criteria.length + 1} (Esc to finish)`,
283
290
  { type: "text" }
284
291
  );
285
- if (typeof ac === "symbol") break;
292
+ if (typeof ac !== "string") break;
286
293
  if (ac.trim()) criteria.push(ac.trim());
287
294
  }
288
295
  return {
@@ -487,6 +494,90 @@ const closeCommand = defineCommand({
487
494
  }
488
495
  });
489
496
 
497
+ const VERBOSE_REGEX = /^(\S+)\s+(\S+)\s+-\s+(.+)$/;
498
+ function migrateDir(dirPath, verboseFiles) {
499
+ let migrated = 0;
500
+ let skipped = 0;
501
+ const warnings = [];
502
+ let files;
503
+ try {
504
+ files = readdirSync(dirPath).filter((f) => f.endsWith(".md") && !f.startsWith("."));
505
+ } catch {
506
+ return { migrated, skipped, warnings };
507
+ }
508
+ for (const file of files) {
509
+ const filePath = join(dirPath, file);
510
+ const raw = readFileSync(filePath, "utf-8");
511
+ const { data, content: body } = matter(raw);
512
+ if (data.title) {
513
+ skipped++;
514
+ continue;
515
+ }
516
+ const stem = basename(file, ".md");
517
+ const match = stem.match(VERBOSE_REGEX);
518
+ if (!match) {
519
+ warnings.push(`Could not extract title from filename: ${file}`);
520
+ skipped++;
521
+ continue;
522
+ }
523
+ const title = match[3];
524
+ const task = {
525
+ id: data.id,
526
+ type: data.type,
527
+ status: data.status,
528
+ created: data.created instanceof Date ? data.created.toISOString().slice(0, 10) : String(data.created),
529
+ title,
530
+ description: "",
531
+ acceptance_criteria: []
532
+ };
533
+ const descMatch = body.match(/## Description\n\n([\s\S]*?)(?:\n\n## Acceptance Criteria|$)/);
534
+ task.description = descMatch ? descMatch[1].trim() : "";
535
+ const acRegex = /^- \[[ x]\] (.+)$/gm;
536
+ let acMatch;
537
+ while ((acMatch = acRegex.exec(body)) !== null) {
538
+ task.acceptance_criteria.push(acMatch[1]);
539
+ }
540
+ writeFileSync(filePath, render(task));
541
+ if (!verboseFiles) {
542
+ const newName = `${data.id}.md`;
543
+ if (file !== newName) {
544
+ renameSync(filePath, join(dirPath, newName));
545
+ }
546
+ }
547
+ migrated++;
548
+ }
549
+ return { migrated, skipped, warnings };
550
+ }
551
+ const migrateCommand = defineCommand({
552
+ meta: {
553
+ name: "migrate",
554
+ description: "Migrate task files to include title in frontmatter"
555
+ },
556
+ run() {
557
+ const dir = process.cwd();
558
+ try {
559
+ const config = loadConfig(dir);
560
+ const tasksDir = join(dir, config.tasks_dir);
561
+ const archiveDir = join(tasksDir, ".archive");
562
+ const active = migrateDir(tasksDir, config.verbose_files);
563
+ const archived = migrateDir(archiveDir, config.verbose_files);
564
+ const totalMigrated = active.migrated + archived.migrated;
565
+ const totalSkipped = active.skipped + archived.skipped;
566
+ const allWarnings = [...active.warnings, ...archived.warnings];
567
+ consola.success(`Migrated ${totalMigrated} file(s), skipped ${totalSkipped} file(s).`);
568
+ for (const w of allWarnings) {
569
+ consola.warn(w);
570
+ }
571
+ if (totalMigrated > 0) {
572
+ consola.info("Breaking change: title is now stored in frontmatter. Old parsers may need updating.");
573
+ }
574
+ } catch (err) {
575
+ consola.error(String(err.message));
576
+ process.exit(1);
577
+ }
578
+ }
579
+ });
580
+
490
581
  function findPackageRoot() {
491
582
  let dir = dirname(fileURLToPath(import.meta.url));
492
583
  while (dir !== dirname(dir)) {
@@ -563,6 +654,7 @@ const main = defineCommand({
563
654
  p: pruneCommand,
564
655
  close: closeCommand,
565
656
  x: closeCommand,
657
+ migrate_prior_verbose: migrateCommand,
566
658
  "install-skills": installSkillsCommand
567
659
  }
568
660
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xxkeefer/mrkl",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Lightweight CLI tool for structured markdown task tracking",
6
6
  "bin": {
@@ -34,13 +34,14 @@
34
34
  "devDependencies": {
35
35
  "@types/node": "^25.3.3",
36
36
  "changelogen": "^0.6.2",
37
+ "tsx": "^4.21.0",
37
38
  "typescript": "^5.7.0",
38
39
  "unbuild": "^3.3.1",
39
40
  "vitest": "^3.0.0"
40
41
  },
41
42
  "scripts": {
42
43
  "build": "unbuild",
43
- "dev": "node --import tsx src/cli.ts",
44
+ "dev": "tsx src/cli.ts",
44
45
  "test": "vitest run",
45
46
  "changelog": "changelogen --output",
46
47
  "changelog:preview": "changelogen --no-output"