@xxkeefer/mrkl 0.2.14 → 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.
- package/README.md +94 -25
- package/dist/cli.mjs +272 -20
- package/package.json +3 -2
- package/skills/plan-from-task/SKILL.md +73 -0
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,7 +143,56 @@ Archives a completed task.
|
|
|
123
143
|
|
|
124
144
|
Moves the task file to `.tasks/.archive/` and sets its status to `done`.
|
|
125
145
|
|
|
126
|
-
|
|
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
|
+
|
|
178
|
+
### `mrkl install-skills`
|
|
179
|
+
|
|
180
|
+
Installs bundled Claude Code skills into the current project.
|
|
181
|
+
|
|
182
|
+
Copies skill directories from the mrkl package into `.claude/skills/` so they are available to Claude Code agents working in this project.
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
mrkl install-skills
|
|
186
|
+
# 🧩 Installed plan-from-task
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Currently ships with:
|
|
190
|
+
|
|
191
|
+
| Skill | Description |
|
|
192
|
+
|-------|-------------|
|
|
193
|
+
| `plan-from-task` | Generate and execute implementation plans from mrkl task files |
|
|
194
|
+
|
|
195
|
+
## Task Types 🏷️
|
|
127
196
|
|
|
128
197
|
mrkl uses [conventional commit](https://www.conventionalcommits.org/) types:
|
|
129
198
|
|
|
@@ -140,7 +209,7 @@ mrkl uses [conventional commit](https://www.conventionalcommits.org/) types:
|
|
|
140
209
|
| `build` | Build system changes |
|
|
141
210
|
| `style` | Code style/formatting |
|
|
142
211
|
|
|
143
|
-
## Task File Format
|
|
212
|
+
## Task File Format 📄
|
|
144
213
|
|
|
145
214
|
Each task is a markdown file with YAML frontmatter:
|
|
146
215
|
|
|
@@ -168,7 +237,7 @@ Implement user authentication with OAuth2.
|
|
|
168
237
|
|
|
169
238
|
The format is intentionally simple — edit task files directly when you need to update descriptions, change status, or check off criteria.
|
|
170
239
|
|
|
171
|
-
## Project Structure
|
|
240
|
+
## Project Structure 🗂️
|
|
172
241
|
|
|
173
242
|
After initialization, mrkl adds the following to your project:
|
|
174
243
|
|
|
@@ -186,7 +255,7 @@ your-project/
|
|
|
186
255
|
|
|
187
256
|
Commit `.config/mrkl/` and `.tasks/` to version control. They're designed to be tracked alongside your code.
|
|
188
257
|
|
|
189
|
-
## Team Workflow
|
|
258
|
+
## Team Workflow 👥
|
|
190
259
|
|
|
191
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.**
|
|
192
261
|
|
|
@@ -210,7 +279,7 @@ mrkl done MRKL-019
|
|
|
210
279
|
|
|
211
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.
|
|
212
281
|
|
|
213
|
-
## Configuration
|
|
282
|
+
## Configuration ⚙️
|
|
214
283
|
|
|
215
284
|
Configuration lives in `.config/mrkl/mrkl.toml` (or `mrkl.toml` at the project root):
|
|
216
285
|
|
|
@@ -224,7 +293,7 @@ tasks_dir = ".tasks"
|
|
|
224
293
|
| `prefix` | *(required)* | Project prefix for task IDs |
|
|
225
294
|
| `tasks_dir` | `".tasks"` | Directory for task files |
|
|
226
295
|
|
|
227
|
-
## Development
|
|
296
|
+
## Development 🧑💻
|
|
228
297
|
|
|
229
298
|
```sh
|
|
230
299
|
git clone https://github.com/xxKeefer/mrkl.git
|
|
@@ -241,10 +310,10 @@ pnpm tsx src/cli.ts list
|
|
|
241
310
|
pnpm build
|
|
242
311
|
```
|
|
243
312
|
|
|
244
|
-
## Contributing
|
|
313
|
+
## Contributing 🤝
|
|
245
314
|
|
|
246
315
|
Contributions are welcome! See **[CONTRIBUTING.md](CONTRIBUTING.md)** for branch protection rules, merge strategy, and development setup.
|
|
247
316
|
|
|
248
|
-
## License
|
|
317
|
+
## License 📜
|
|
249
318
|
|
|
250
319
|
[MIT](LICENSE)
|
package/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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 } from 'node:fs';
|
|
5
|
-
import { join } from 'node:path';
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync, statSync } from 'node:fs';
|
|
5
|
+
import { join, dirname } from 'node:path';
|
|
6
6
|
import { stringify, parse as parse$1 } from 'smol-toml';
|
|
7
7
|
import matter from 'gray-matter';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
8
9
|
|
|
9
10
|
const CONFIG_PATHS = [
|
|
10
11
|
join(".config", "mrkl", "mrkl.toml"),
|
|
@@ -57,7 +58,7 @@ const initCommand = defineCommand({
|
|
|
57
58
|
const dir = process.cwd();
|
|
58
59
|
try {
|
|
59
60
|
initConfig(dir, { prefix: args.prefix });
|
|
60
|
-
consola.success("mrkl initialized");
|
|
61
|
+
consola.success("\u{1F389} mrkl initialized");
|
|
61
62
|
} catch (err) {
|
|
62
63
|
consola.error(String(err.message));
|
|
63
64
|
process.exit(1);
|
|
@@ -144,9 +145,7 @@ function createTask(opts) {
|
|
|
144
145
|
function listTasks(filter) {
|
|
145
146
|
const config = loadConfig(filter.dir);
|
|
146
147
|
const tasksDir = join(filter.dir, config.tasks_dir);
|
|
147
|
-
const files = readdirSync(tasksDir).filter(
|
|
148
|
-
(f) => f.endsWith(".md") && !f.startsWith(".")
|
|
149
|
-
);
|
|
148
|
+
const files = readdirSync(tasksDir).filter((f) => f.endsWith(".md") && !f.startsWith("."));
|
|
150
149
|
let tasks = files.flatMap((f) => {
|
|
151
150
|
try {
|
|
152
151
|
const content = readFileSync(join(tasksDir, f), "utf-8");
|
|
@@ -164,8 +163,9 @@ function listTasks(filter) {
|
|
|
164
163
|
function archiveTask(dir, id) {
|
|
165
164
|
const config = loadConfig(dir);
|
|
166
165
|
const tasksDir = join(dir, config.tasks_dir);
|
|
166
|
+
const idUpper = id.toUpperCase();
|
|
167
167
|
const file = readdirSync(tasksDir).find(
|
|
168
|
-
(f) => f.endsWith(".md") && f.startsWith(
|
|
168
|
+
(f) => f.endsWith(".md") && f.toUpperCase().startsWith(idUpper)
|
|
169
169
|
);
|
|
170
170
|
if (!file) {
|
|
171
171
|
throw new Error(`Task ${id} not found`);
|
|
@@ -178,6 +178,70 @@ function archiveTask(dir, id) {
|
|
|
178
178
|
writeFileSync(archivePath, render(task));
|
|
179
179
|
unlinkSync(filePath);
|
|
180
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
|
+
}
|
|
181
245
|
|
|
182
246
|
const TASK_TYPES = [
|
|
183
247
|
"feat",
|
|
@@ -192,6 +256,43 @@ const TASK_TYPES = [
|
|
|
192
256
|
"style"
|
|
193
257
|
];
|
|
194
258
|
|
|
259
|
+
async function promptForTask(dir) {
|
|
260
|
+
const type = await consola.prompt("Task type", {
|
|
261
|
+
type: "select",
|
|
262
|
+
options: TASK_TYPES.map((t) => t)
|
|
263
|
+
});
|
|
264
|
+
if (typeof type === "symbol") process.exit(0);
|
|
265
|
+
const title = await consola.prompt("Task title", {
|
|
266
|
+
type: "text",
|
|
267
|
+
placeholder: "e.g. add user authentication"
|
|
268
|
+
});
|
|
269
|
+
if (typeof title === "symbol") process.exit(0);
|
|
270
|
+
if (!title.trim()) {
|
|
271
|
+
consola.error("\u274C Title cannot be empty");
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
const desc = await consola.prompt("Description (optional, enter to skip)", {
|
|
275
|
+
type: "text",
|
|
276
|
+
placeholder: "Describe the task in detail"
|
|
277
|
+
});
|
|
278
|
+
if (typeof desc === "symbol") process.exit(0);
|
|
279
|
+
const criteria = [];
|
|
280
|
+
while (true) {
|
|
281
|
+
const ac = await consola.prompt(
|
|
282
|
+
criteria.length === 0 ? "Acceptance criterion (Esc to skip)" : `Criterion #${criteria.length + 1} (Esc to finish)`,
|
|
283
|
+
{ type: "text" }
|
|
284
|
+
);
|
|
285
|
+
if (typeof ac === "symbol") break;
|
|
286
|
+
if (ac.trim()) criteria.push(ac.trim());
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
dir,
|
|
290
|
+
type,
|
|
291
|
+
title,
|
|
292
|
+
description: desc || void 0,
|
|
293
|
+
acceptance_criteria: criteria.length > 0 ? criteria : void 0
|
|
294
|
+
};
|
|
295
|
+
}
|
|
195
296
|
const createCommand = defineCommand({
|
|
196
297
|
meta: {
|
|
197
298
|
name: "create",
|
|
@@ -201,37 +302,47 @@ const createCommand = defineCommand({
|
|
|
201
302
|
type: {
|
|
202
303
|
type: "positional",
|
|
203
304
|
description: "Task type (feat, fix, chore, docs, perf, refactor, test, ci, build, style)",
|
|
204
|
-
required:
|
|
305
|
+
required: false
|
|
205
306
|
},
|
|
206
307
|
title: {
|
|
207
308
|
type: "positional",
|
|
208
309
|
description: "Task title",
|
|
209
|
-
required:
|
|
310
|
+
required: false
|
|
210
311
|
},
|
|
211
312
|
desc: {
|
|
212
313
|
type: "string",
|
|
314
|
+
alias: "d",
|
|
213
315
|
description: "Task description"
|
|
214
316
|
},
|
|
215
317
|
ac: {
|
|
216
318
|
type: "string",
|
|
319
|
+
alias: "a",
|
|
217
320
|
description: "Acceptance criterion (can be specified multiple times)"
|
|
218
321
|
}
|
|
219
322
|
},
|
|
220
|
-
run({ args }) {
|
|
221
|
-
|
|
222
|
-
|
|
323
|
+
async run({ args }) {
|
|
324
|
+
const dir = process.cwd();
|
|
325
|
+
const interactive = !args.type && !args.title;
|
|
326
|
+
if (!interactive && (!args.type || !args.title)) {
|
|
327
|
+
consola.error("\u274C Both type and title are required, or omit both for interactive mode");
|
|
223
328
|
process.exit(1);
|
|
224
329
|
}
|
|
225
|
-
const dir = process.cwd();
|
|
226
330
|
try {
|
|
227
|
-
const
|
|
331
|
+
const opts = interactive ? await promptForTask(dir) : {
|
|
228
332
|
dir,
|
|
229
|
-
type:
|
|
333
|
+
type: (() => {
|
|
334
|
+
if (!TASK_TYPES.includes(args.type)) {
|
|
335
|
+
consola.error(`\u274C Invalid type "${args.type}". Must be one of: ${TASK_TYPES.join(", ")}`);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
return args.type;
|
|
339
|
+
})(),
|
|
230
340
|
title: args.title,
|
|
231
341
|
description: args.desc,
|
|
232
342
|
acceptance_criteria: args.ac ? Array.isArray(args.ac) ? args.ac : [args.ac] : void 0
|
|
233
|
-
}
|
|
234
|
-
|
|
343
|
+
};
|
|
344
|
+
const task = createTask(opts);
|
|
345
|
+
consola.success(`\u{1F4DD} Created ${task.id}: ${task.title}`);
|
|
235
346
|
} catch (err) {
|
|
236
347
|
consola.error(String(err.message));
|
|
237
348
|
process.exit(1);
|
|
@@ -247,10 +358,12 @@ const listCommand = defineCommand({
|
|
|
247
358
|
args: {
|
|
248
359
|
type: {
|
|
249
360
|
type: "string",
|
|
361
|
+
alias: "t",
|
|
250
362
|
description: "Filter by task type"
|
|
251
363
|
},
|
|
252
364
|
status: {
|
|
253
365
|
type: "string",
|
|
366
|
+
alias: "s",
|
|
254
367
|
description: "Filter by status (todo, in-progress, done)"
|
|
255
368
|
}
|
|
256
369
|
},
|
|
@@ -263,7 +376,7 @@ const listCommand = defineCommand({
|
|
|
263
376
|
status: args.status
|
|
264
377
|
});
|
|
265
378
|
if (tasks.length === 0) {
|
|
266
|
-
consola.info("No tasks found");
|
|
379
|
+
consola.info("\u{1F4ED} No tasks found");
|
|
267
380
|
return;
|
|
268
381
|
}
|
|
269
382
|
for (const task of tasks) {
|
|
@@ -292,7 +405,138 @@ const doneCommand = defineCommand({
|
|
|
292
405
|
const dir = process.cwd();
|
|
293
406
|
try {
|
|
294
407
|
archiveTask(dir, args.id);
|
|
295
|
-
consola.success(
|
|
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}`);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
consola.error(String(err.message));
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
function findPackageRoot() {
|
|
491
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
492
|
+
while (dir !== dirname(dir)) {
|
|
493
|
+
if (existsSync(join(dir, "package.json"))) {
|
|
494
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
|
|
495
|
+
if (pkg.name === "@xxkeefer/mrkl") return dir;
|
|
496
|
+
}
|
|
497
|
+
dir = dirname(dir);
|
|
498
|
+
}
|
|
499
|
+
throw new Error("Could not locate mrkl package root");
|
|
500
|
+
}
|
|
501
|
+
function copyDirSync(src, dest) {
|
|
502
|
+
mkdirSync(dest, { recursive: true });
|
|
503
|
+
for (const entry of readdirSync(src)) {
|
|
504
|
+
const srcPath = join(src, entry);
|
|
505
|
+
const destPath = join(dest, entry);
|
|
506
|
+
if (statSync(srcPath).isDirectory()) {
|
|
507
|
+
copyDirSync(srcPath, destPath);
|
|
508
|
+
} else {
|
|
509
|
+
writeFileSync(destPath, readFileSync(srcPath));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const installSkillsCommand = defineCommand({
|
|
514
|
+
meta: {
|
|
515
|
+
name: "install-skills",
|
|
516
|
+
description: "Install mrkl Claude Code skills into the current project"
|
|
517
|
+
},
|
|
518
|
+
run() {
|
|
519
|
+
const dest = join(process.cwd(), ".claude", "skills");
|
|
520
|
+
try {
|
|
521
|
+
const packageRoot = findPackageRoot();
|
|
522
|
+
const skillsDir = join(packageRoot, "skills");
|
|
523
|
+
if (!existsSync(skillsDir)) {
|
|
524
|
+
consola.error("\u274C No skills directory found in mrkl package");
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
const skills = readdirSync(skillsDir).filter(
|
|
528
|
+
(f) => statSync(join(skillsDir, f)).isDirectory()
|
|
529
|
+
);
|
|
530
|
+
if (skills.length === 0) {
|
|
531
|
+
consola.info("\u{1F4ED} No skills to install");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
for (const skill of skills) {
|
|
535
|
+
const src = join(skillsDir, skill);
|
|
536
|
+
const target = join(dest, skill);
|
|
537
|
+
copyDirSync(src, target);
|
|
538
|
+
consola.success(`\u{1F9E9} Installed ${skill}`);
|
|
539
|
+
}
|
|
296
540
|
} catch (err) {
|
|
297
541
|
consola.error(String(err.message));
|
|
298
542
|
process.exit(1);
|
|
@@ -308,10 +552,18 @@ const main = defineCommand({
|
|
|
308
552
|
},
|
|
309
553
|
subCommands: {
|
|
310
554
|
init: initCommand,
|
|
555
|
+
i: initCommand,
|
|
311
556
|
create: createCommand,
|
|
312
557
|
c: createCommand,
|
|
313
558
|
list: listCommand,
|
|
314
|
-
|
|
559
|
+
ls: listCommand,
|
|
560
|
+
done: doneCommand,
|
|
561
|
+
d: doneCommand,
|
|
562
|
+
prune: pruneCommand,
|
|
563
|
+
p: pruneCommand,
|
|
564
|
+
close: closeCommand,
|
|
565
|
+
x: closeCommand,
|
|
566
|
+
"install-skills": installSkillsCommand
|
|
315
567
|
}
|
|
316
568
|
});
|
|
317
569
|
runMain(main);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xxkeefer/mrkl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lightweight CLI tool for structured markdown task tracking",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
],
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"skills"
|
|
18
19
|
],
|
|
19
20
|
"repository": {
|
|
20
21
|
"type": "git",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan-from-task
|
|
3
|
+
description: Generate and execute implementation plans from mrkl task files (.tasks/ directory). Use when user references a task ID (e.g., MRKL-001, PROJ-042) or a @path to a .tasks/ markdown file and wants to plan or implement it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Plan From Task
|
|
7
|
+
|
|
8
|
+
Create implementation plans from mrkl task files and execute them.
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
The user provides a task reference in one of two forms:
|
|
13
|
+
|
|
14
|
+
- **File path**: `@".tasks/PROJ-001 feat - user auth.md"` or `implement .tasks/PROJ-001...`
|
|
15
|
+
- **Task ID**: `PROJ-001` or `mrkl-042` (case-insensitive)
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
### 1. Locate and read the task
|
|
20
|
+
|
|
21
|
+
- **File path given**: read the file directly
|
|
22
|
+
- **Task ID given**: glob for `.tasks/<ID>*.md` (case-insensitive) and read the match
|
|
23
|
+
|
|
24
|
+
Parse the YAML frontmatter and markdown body. Task files follow this structure:
|
|
25
|
+
|
|
26
|
+
```markdown
|
|
27
|
+
---
|
|
28
|
+
id: PROJ-001
|
|
29
|
+
type: feat
|
|
30
|
+
status: todo
|
|
31
|
+
created: '2026-03-01'
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Description
|
|
35
|
+
|
|
36
|
+
[What needs to be done]
|
|
37
|
+
|
|
38
|
+
## Acceptance Criteria
|
|
39
|
+
|
|
40
|
+
- [ ] first criterion
|
|
41
|
+
- [ ] second criterion
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. Assess completeness
|
|
45
|
+
|
|
46
|
+
If the description or acceptance criteria are too vague to plan from:
|
|
47
|
+
|
|
48
|
+
- Ask the user targeted clarifying questions about scope, approach, or constraints
|
|
49
|
+
- Gather enough context to produce a concrete implementation plan
|
|
50
|
+
|
|
51
|
+
### 3. Plan the implementation
|
|
52
|
+
|
|
53
|
+
1. Enter plan mode
|
|
54
|
+
2. Explore the codebase to understand relevant architecture and patterns
|
|
55
|
+
3. Write a step-by-step implementation plan that addresses every acceptance criterion
|
|
56
|
+
4. Present the plan for user approval
|
|
57
|
+
|
|
58
|
+
### 4. Execute the plan
|
|
59
|
+
|
|
60
|
+
After approval, implement in this order:
|
|
61
|
+
|
|
62
|
+
1. **Update the task file first** — rewrite the `## Description` body and `## Acceptance Criteria`
|
|
63
|
+
checklist to reflect the refined, concrete plan of what will be delivered.
|
|
64
|
+
The task file doubles as the PR description, so make it clear and accurate.
|
|
65
|
+
2. Then implement the code changes following the plan.
|
|
66
|
+
|
|
67
|
+
## Rules
|
|
68
|
+
|
|
69
|
+
- **NEVER modify YAML frontmatter** — `id`, `type`, `status`, `created` are immutable
|
|
70
|
+
- **NEVER modify markdown headings** — `## Description` and `## Acceptance Criteria` must stay as-is
|
|
71
|
+
- You MAY rewrite the content under those headings to match the approved plan
|
|
72
|
+
- Use `- [ ]` checkbox format for acceptance criteria
|
|
73
|
+
- Follow existing codebase conventions discovered during exploration
|