@xxkeefer/mrkl 0.3.0 → 0.3.1

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 +78 -26
  2. package/dist/cli.mjs +153 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  <p align="center">
2
2
  <h1 align="center">mrkl</h1>
3
3
  <p align="center">
4
+ 📝 <i>mrkl, rhymes with sparkle</i> ✨
5
+ <br />
4
6
  Lightweight CLI for structured markdown task tracking.
5
7
  <br />
6
8
  Track work in your repo, not in a separate app.
@@ -15,17 +17,17 @@
15
17
 
16
18
  ---
17
19
 
18
- ## Why mrkl?
20
+ ## Why mrkl? 🤔
19
21
 
20
22
  Most task trackers live outside your codebase. mrkl keeps tasks as markdown files right alongside your code — version-controlled, greppable, and readable by both humans and AI agents.
21
23
 
22
- - **No external service** — tasks live in `.tasks/` as structured markdown
23
- - **Git-native** — commit, branch, and diff your tasks like any other file
24
- - **AI-agent friendly** — consistent YAML frontmatter makes tasks easy to parse programmatically
25
- - **Conventional commits vocabulary** — task types mirror what you already use (`feat`, `fix`, `chore`, etc.)
26
- - **Zero config** — one command to set up, sensible defaults for everything
24
+ - 🗂️ **No external service** — tasks live in `.tasks/` as structured markdown
25
+ - 🌿 **Git-native** — commit, branch, and diff your tasks like any other file
26
+ - 🤖 **AI-agent friendly** — consistent YAML frontmatter makes tasks easy to parse programmatically
27
+ - 📏 **Conventional commits vocabulary** — task types mirror what you already use (`feat`, `fix`, `chore`, etc.)
28
+ - **Zero config** — one command to set up, sensible defaults for everything
27
29
 
28
- ## Install
30
+ ## Install 📦
29
31
 
30
32
  ```sh
31
33
  pnpm add -g @xxkeefer/mrkl
@@ -37,7 +39,7 @@ Or use without installing:
37
39
  npx @xxkeefer/mrkl init MY_PROJECT
38
40
  ```
39
41
 
40
- ## Quick Start
42
+ ## Quick Start 🚀
41
43
 
42
44
  ```sh
43
45
  # Initialize in your project root
@@ -60,9 +62,25 @@ mrkl list --status todo
60
62
 
61
63
  # Archive a completed task
62
64
  mrkl done PROJ-001
65
+
66
+ # All commands have short aliases
67
+ mrkl c feat "dark mode" # create
68
+ mrkl ls --type fix # list
69
+ mrkl d PROJ-001 # done
70
+ mrkl x PROJ-002 # close
63
71
  ```
64
72
 
65
- ## Commands
73
+ ## Commands 🛠️
74
+
75
+ | Command | Alias | Description |
76
+ |---------|-------|-------------|
77
+ | `init` | `i` | Initialize mrkl in the current project |
78
+ | `create` | `c` | Create a new task |
79
+ | `list` | `ls` | List active tasks |
80
+ | `done` | `d` | Mark a task as done and archive it |
81
+ | `close` | `x` | Close a task (won't do, duplicate, etc.) and archive it |
82
+ | `prune` | `p` | Delete archived tasks created on or before a given date |
83
+ | `install-skills` | — | Install bundled Claude Code skills |
66
84
 
67
85
  ### `mrkl init <prefix>`
68
86
 
@@ -89,10 +107,10 @@ Creates a new task file.
89
107
  | `type` | Task type (see [Task Types](#task-types)) |
90
108
  | `title` | Short description of the task |
91
109
 
92
- | Option | Description |
93
- |--------|-------------|
94
- | `--desc <text>` | Detailed description |
95
- | `--ac <text>` | Acceptance criterion (repeatable) |
110
+ | Option | Alias | Description |
111
+ |--------|-------|-------------|
112
+ | `--desc <text>` | `-d` | Detailed description |
113
+ | `--ac <text>` | `-a` | Acceptance criterion (repeatable) |
96
114
 
97
115
  ```sh
98
116
  mrkl create feat "search functionality" \
@@ -102,14 +120,16 @@ mrkl create feat "search functionality" \
102
120
  --ac "highlights matching terms"
103
121
  ```
104
122
 
123
+ Running `mrkl create` with no arguments enters **interactive mode**, prompting for type, title, description, and acceptance criteria.
124
+
105
125
  ### `mrkl list [options]`
106
126
 
107
127
  Lists all active tasks.
108
128
 
109
- | Option | Description |
110
- |--------|-------------|
111
- | `--type <type>` | Filter by task type |
112
- | `--status <status>` | Filter by status (`todo`, `in-progress`, `done`) |
129
+ | Option | Alias | Description |
130
+ |--------|-------|-------------|
131
+ | `--type <type>` | `-t` | Filter by task type |
132
+ | `--status <status>` | `-s` | Filter by status (`todo`, `in-progress`, `done`) |
113
133
 
114
134
  Non-conforming markdown files in the tasks directory are silently skipped.
115
135
 
@@ -123,6 +143,38 @@ Archives a completed task.
123
143
 
124
144
  Moves the task file to `.tasks/.archive/` and sets its status to `done`.
125
145
 
146
+ ### `mrkl close <id>`
147
+
148
+ Closes a task that won't be done — duplicates, out-of-scope work, etc.
149
+
150
+ | Argument | Description |
151
+ |----------|-------------|
152
+ | `id` | Task ID to close (e.g., `PROJ-002`) |
153
+
154
+ Sets the task status to `closed` and moves it to `.tasks/.archive/`.
155
+
156
+ ### `mrkl prune <date> [options]`
157
+
158
+ Permanently deletes archived tasks created on or before a cutoff date.
159
+
160
+ | Argument | Description |
161
+ |----------|-------------|
162
+ | `date` | Cutoff date (`YYYY-MM-DD` or `YYYYMMDD`) |
163
+
164
+ | Option | Alias | Description |
165
+ |--------|-------|-------------|
166
+ | `--force` | `-f` | Skip confirmation prompt |
167
+
168
+ Shows a confirmation prompt listing tasks to be deleted unless `--force` is used.
169
+
170
+ ```sh
171
+ # Delete archived tasks from January or earlier
172
+ mrkl prune 2026-01-31
173
+
174
+ # Skip confirmation
175
+ mrkl prune 2026-01-31 --force
176
+ ```
177
+
126
178
  ### `mrkl install-skills`
127
179
 
128
180
  Installs bundled Claude Code skills into the current project.
@@ -131,7 +183,7 @@ Copies skill directories from the mrkl package into `.claude/skills/` so they ar
131
183
 
132
184
  ```sh
133
185
  mrkl install-skills
134
- # Installed plan-from-task
186
+ # 🧩 Installed plan-from-task
135
187
  ```
136
188
 
137
189
  Currently ships with:
@@ -140,7 +192,7 @@ Currently ships with:
140
192
  |-------|-------------|
141
193
  | `plan-from-task` | Generate and execute implementation plans from mrkl task files |
142
194
 
143
- ## Task Types
195
+ ## Task Types 🏷️
144
196
 
145
197
  mrkl uses [conventional commit](https://www.conventionalcommits.org/) types:
146
198
 
@@ -157,7 +209,7 @@ mrkl uses [conventional commit](https://www.conventionalcommits.org/) types:
157
209
  | `build` | Build system changes |
158
210
  | `style` | Code style/formatting |
159
211
 
160
- ## Task File Format
212
+ ## Task File Format 📄
161
213
 
162
214
  Each task is a markdown file with YAML frontmatter:
163
215
 
@@ -185,7 +237,7 @@ Implement user authentication with OAuth2.
185
237
 
186
238
  The format is intentionally simple — edit task files directly when you need to update descriptions, change status, or check off criteria.
187
239
 
188
- ## Project Structure
240
+ ## Project Structure 🗂️
189
241
 
190
242
  After initialization, mrkl adds the following to your project:
191
243
 
@@ -203,7 +255,7 @@ your-project/
203
255
 
204
256
  Commit `.config/mrkl/` and `.tasks/` to version control. They're designed to be tracked alongside your code.
205
257
 
206
- ## Team Workflow
258
+ ## Team Workflow 👥
207
259
 
208
260
  When using mrkl with **git worktrees** or **protected branches**, task IDs can conflict if multiple branches create tasks concurrently. The fix is a simple convention: **separate planning from execution.**
209
261
 
@@ -227,7 +279,7 @@ mrkl done MRKL-019
227
279
 
228
280
  The counter only increments on planning branches — one at a time — so IDs never conflict. See **[docs/workflow.md](docs/workflow.md)** for the full guide with examples and edge cases.
229
281
 
230
- ## Configuration
282
+ ## Configuration ⚙️
231
283
 
232
284
  Configuration lives in `.config/mrkl/mrkl.toml` (or `mrkl.toml` at the project root):
233
285
 
@@ -241,7 +293,7 @@ tasks_dir = ".tasks"
241
293
  | `prefix` | *(required)* | Project prefix for task IDs |
242
294
  | `tasks_dir` | `".tasks"` | Directory for task files |
243
295
 
244
- ## Development
296
+ ## Development 🧑‍💻
245
297
 
246
298
  ```sh
247
299
  git clone https://github.com/xxKeefer/mrkl.git
@@ -258,10 +310,10 @@ pnpm tsx src/cli.ts list
258
310
  pnpm build
259
311
  ```
260
312
 
261
- ## Contributing
313
+ ## Contributing 🤝
262
314
 
263
315
  Contributions are welcome! See **[CONTRIBUTING.md](CONTRIBUTING.md)** for branch protection rules, merge strategy, and development setup.
264
316
 
265
- ## License
317
+ ## License 📜
266
318
 
267
319
  [MIT](LICENSE)
package/dist/cli.mjs CHANGED
@@ -58,7 +58,7 @@ const initCommand = defineCommand({
58
58
  const dir = process.cwd();
59
59
  try {
60
60
  initConfig(dir, { prefix: args.prefix });
61
- consola.success("mrkl initialized");
61
+ consola.success("\u{1F389} mrkl initialized");
62
62
  } catch (err) {
63
63
  consola.error(String(err.message));
64
64
  process.exit(1);
@@ -145,9 +145,7 @@ function createTask(opts) {
145
145
  function listTasks(filter) {
146
146
  const config = loadConfig(filter.dir);
147
147
  const tasksDir = join(filter.dir, config.tasks_dir);
148
- const files = readdirSync(tasksDir).filter(
149
- (f) => f.endsWith(".md") && !f.startsWith(".")
150
- );
148
+ const files = readdirSync(tasksDir).filter((f) => f.endsWith(".md") && !f.startsWith("."));
151
149
  let tasks = files.flatMap((f) => {
152
150
  try {
153
151
  const content = readFileSync(join(tasksDir, f), "utf-8");
@@ -180,6 +178,70 @@ function archiveTask(dir, id) {
180
178
  writeFileSync(archivePath, render(task));
181
179
  unlinkSync(filePath);
182
180
  }
181
+ function parseCutoffDate(input) {
182
+ const normalized = input.replace(/^(\d{4})(\d{2})(\d{2})$/, "$1-$2-$3");
183
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
184
+ throw new Error(`Invalid date format: "${input}". Expected YYYY-MM-DD or YYYYMMDD.`);
185
+ }
186
+ const [y, m, d] = normalized.split("-").map(Number);
187
+ const date = new Date(y, m - 1, d);
188
+ if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {
189
+ throw new Error(`Invalid date: "${input}" is not a real calendar date.`);
190
+ }
191
+ return normalized;
192
+ }
193
+ function normalizeCreatedDate(created) {
194
+ if (created instanceof Date) {
195
+ return created.toISOString().slice(0, 10);
196
+ }
197
+ return String(created);
198
+ }
199
+ function pruneTasks(dir, cutoff) {
200
+ const config = loadConfig(dir);
201
+ const archiveDir = join(dir, config.tasks_dir, ".archive");
202
+ if (!existsSync(archiveDir)) {
203
+ return { deleted: [], total: 0 };
204
+ }
205
+ const files = readdirSync(archiveDir).filter((f) => f.endsWith(".md") && !f.startsWith("."));
206
+ const deleted = [];
207
+ for (const f of files) {
208
+ try {
209
+ const content = readFileSync(join(archiveDir, f), "utf-8");
210
+ const task = parse(content, f);
211
+ const created = normalizeCreatedDate(task.created);
212
+ if (created <= cutoff) {
213
+ deleted.push({ id: task.id, title: task.title, created, filename: f });
214
+ }
215
+ } catch {
216
+ }
217
+ }
218
+ return { deleted, total: files.length };
219
+ }
220
+ function executePrune(dir, filenames) {
221
+ const config = loadConfig(dir);
222
+ const archiveDir = join(dir, config.tasks_dir, ".archive");
223
+ for (const f of filenames) {
224
+ unlinkSync(join(archiveDir, f));
225
+ }
226
+ }
227
+ function closeTask(dir, id) {
228
+ const config = loadConfig(dir);
229
+ const tasksDir = join(dir, config.tasks_dir);
230
+ const idUpper = id.toUpperCase();
231
+ const file = readdirSync(tasksDir).find(
232
+ (f) => f.endsWith(".md") && f.toUpperCase().startsWith(idUpper)
233
+ );
234
+ if (!file) {
235
+ throw new Error(`Task ${id} not found`);
236
+ }
237
+ const filePath = join(tasksDir, file);
238
+ const content = readFileSync(filePath, "utf-8");
239
+ const task = parse(content, file);
240
+ task.status = "closed";
241
+ const archivePath = join(tasksDir, ".archive", file);
242
+ writeFileSync(archivePath, render(task));
243
+ unlinkSync(filePath);
244
+ }
183
245
 
184
246
  const TASK_TYPES = [
185
247
  "feat",
@@ -206,7 +268,7 @@ async function promptForTask(dir) {
206
268
  });
207
269
  if (typeof title === "symbol") process.exit(0);
208
270
  if (!title.trim()) {
209
- consola.error("Title cannot be empty");
271
+ consola.error("\u274C Title cannot be empty");
210
272
  process.exit(1);
211
273
  }
212
274
  const desc = await consola.prompt("Description (optional, enter to skip)", {
@@ -262,7 +324,7 @@ const createCommand = defineCommand({
262
324
  const dir = process.cwd();
263
325
  const interactive = !args.type && !args.title;
264
326
  if (!interactive && (!args.type || !args.title)) {
265
- consola.error("Both type and title are required, or omit both for interactive mode");
327
+ consola.error("\u274C Both type and title are required, or omit both for interactive mode");
266
328
  process.exit(1);
267
329
  }
268
330
  try {
@@ -270,7 +332,7 @@ const createCommand = defineCommand({
270
332
  dir,
271
333
  type: (() => {
272
334
  if (!TASK_TYPES.includes(args.type)) {
273
- consola.error(`Invalid type "${args.type}". Must be one of: ${TASK_TYPES.join(", ")}`);
335
+ consola.error(`\u274C Invalid type "${args.type}". Must be one of: ${TASK_TYPES.join(", ")}`);
274
336
  process.exit(1);
275
337
  }
276
338
  return args.type;
@@ -280,7 +342,7 @@ const createCommand = defineCommand({
280
342
  acceptance_criteria: args.ac ? Array.isArray(args.ac) ? args.ac : [args.ac] : void 0
281
343
  };
282
344
  const task = createTask(opts);
283
- consola.success(`Created ${task.id}: ${task.title}`);
345
+ consola.success(`\u{1F4DD} Created ${task.id}: ${task.title}`);
284
346
  } catch (err) {
285
347
  consola.error(String(err.message));
286
348
  process.exit(1);
@@ -314,7 +376,7 @@ const listCommand = defineCommand({
314
376
  status: args.status
315
377
  });
316
378
  if (tasks.length === 0) {
317
- consola.info("No tasks found");
379
+ consola.info("\u{1F4ED} No tasks found");
318
380
  return;
319
381
  }
320
382
  for (const task of tasks) {
@@ -343,7 +405,81 @@ const doneCommand = defineCommand({
343
405
  const dir = process.cwd();
344
406
  try {
345
407
  archiveTask(dir, args.id);
346
- consola.success(`Archived ${args.id}`);
408
+ consola.success(`\u2705 Archived ${args.id}`);
409
+ } catch (err) {
410
+ consola.error(String(err.message));
411
+ process.exit(1);
412
+ }
413
+ }
414
+ });
415
+
416
+ const pruneCommand = defineCommand({
417
+ meta: {
418
+ name: "prune",
419
+ description: "Delete archived tasks created on or before a given date"
420
+ },
421
+ args: {
422
+ date: {
423
+ type: "positional",
424
+ description: "Cutoff date (YYYY-MM-DD or YYYYMMDD)",
425
+ required: true
426
+ },
427
+ force: {
428
+ type: "boolean",
429
+ alias: "f",
430
+ description: "Skip confirmation prompt",
431
+ default: false
432
+ }
433
+ },
434
+ async run({ args }) {
435
+ const dir = process.cwd();
436
+ let cutoff;
437
+ try {
438
+ cutoff = parseCutoffDate(args.date);
439
+ } catch (err) {
440
+ consola.error(String(err.message));
441
+ process.exit(1);
442
+ }
443
+ const result = pruneTasks(dir, cutoff);
444
+ if (result.deleted.length === 0) {
445
+ consola.info(`\u{1F4ED} No archived tasks found on or before ${cutoff}`);
446
+ return;
447
+ }
448
+ consola.info(`\u{1F50D} Found ${result.deleted.length} task(s) to prune:`);
449
+ for (const task of result.deleted) {
450
+ consola.log(` ${task.id} \u2014 ${task.title} (${task.created})`);
451
+ }
452
+ if (!args.force) {
453
+ const confirm = await consola.prompt("Delete these tasks?", {
454
+ type: "confirm"
455
+ });
456
+ if (typeof confirm === "symbol" || !confirm) {
457
+ consola.info("\u{1F44B} Aborted");
458
+ return;
459
+ }
460
+ }
461
+ executePrune(dir, result.deleted.map((t) => t.filename));
462
+ consola.success(`\u{1F9F9} Pruned ${result.deleted.length} archived task(s)`);
463
+ }
464
+ });
465
+
466
+ const closeCommand = defineCommand({
467
+ meta: {
468
+ name: "close",
469
+ description: "Close a task (won't do, duplicate, etc.) and archive it"
470
+ },
471
+ args: {
472
+ id: {
473
+ type: "positional",
474
+ description: "Task ID to close (e.g., VON-001)",
475
+ required: true
476
+ }
477
+ },
478
+ run({ args }) {
479
+ const dir = process.cwd();
480
+ try {
481
+ closeTask(dir, args.id);
482
+ consola.success(`\u{1F6AB} Closed ${args.id}`);
347
483
  } catch (err) {
348
484
  consola.error(String(err.message));
349
485
  process.exit(1);
@@ -385,21 +521,21 @@ const installSkillsCommand = defineCommand({
385
521
  const packageRoot = findPackageRoot();
386
522
  const skillsDir = join(packageRoot, "skills");
387
523
  if (!existsSync(skillsDir)) {
388
- consola.error("No skills directory found in mrkl package");
524
+ consola.error("\u274C No skills directory found in mrkl package");
389
525
  process.exit(1);
390
526
  }
391
527
  const skills = readdirSync(skillsDir).filter(
392
528
  (f) => statSync(join(skillsDir, f)).isDirectory()
393
529
  );
394
530
  if (skills.length === 0) {
395
- consola.info("No skills to install");
531
+ consola.info("\u{1F4ED} No skills to install");
396
532
  return;
397
533
  }
398
534
  for (const skill of skills) {
399
535
  const src = join(skillsDir, skill);
400
536
  const target = join(dest, skill);
401
537
  copyDirSync(src, target);
402
- consola.success(`Installed ${skill}`);
538
+ consola.success(`\u{1F9E9} Installed ${skill}`);
403
539
  }
404
540
  } catch (err) {
405
541
  consola.error(String(err.message));
@@ -423,6 +559,10 @@ const main = defineCommand({
423
559
  ls: listCommand,
424
560
  done: doneCommand,
425
561
  d: doneCommand,
562
+ prune: pruneCommand,
563
+ p: pruneCommand,
564
+ close: closeCommand,
565
+ x: closeCommand,
426
566
  "install-skills": installSkillsCommand
427
567
  }
428
568
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xxkeefer/mrkl",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Lightweight CLI tool for structured markdown task tracking",
6
6
  "bin": {