@xxkeefer/mrkl 0.2.14 → 0.3.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.
- package/README.md +17 -0
- package/dist/cli.mjs +125 -13
- package/package.json +3 -2
- package/skills/plan-from-task/SKILL.md +73 -0
package/README.md
CHANGED
|
@@ -123,6 +123,23 @@ Archives a completed task.
|
|
|
123
123
|
|
|
124
124
|
Moves the task file to `.tasks/.archive/` and sets its status to `done`.
|
|
125
125
|
|
|
126
|
+
### `mrkl install-skills`
|
|
127
|
+
|
|
128
|
+
Installs bundled Claude Code skills into the current project.
|
|
129
|
+
|
|
130
|
+
Copies skill directories from the mrkl package into `.claude/skills/` so they are available to Claude Code agents working in this project.
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
mrkl install-skills
|
|
134
|
+
# ✔ Installed plan-from-task
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Currently ships with:
|
|
138
|
+
|
|
139
|
+
| Skill | Description |
|
|
140
|
+
|-------|-------------|
|
|
141
|
+
| `plan-from-task` | Generate and execute implementation plans from mrkl task files |
|
|
142
|
+
|
|
126
143
|
## Task Types
|
|
127
144
|
|
|
128
145
|
mrkl uses [conventional commit](https://www.conventionalcommits.org/) types:
|
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"),
|
|
@@ -164,8 +165,9 @@ function listTasks(filter) {
|
|
|
164
165
|
function archiveTask(dir, id) {
|
|
165
166
|
const config = loadConfig(dir);
|
|
166
167
|
const tasksDir = join(dir, config.tasks_dir);
|
|
168
|
+
const idUpper = id.toUpperCase();
|
|
167
169
|
const file = readdirSync(tasksDir).find(
|
|
168
|
-
(f) => f.endsWith(".md") && f.startsWith(
|
|
170
|
+
(f) => f.endsWith(".md") && f.toUpperCase().startsWith(idUpper)
|
|
169
171
|
);
|
|
170
172
|
if (!file) {
|
|
171
173
|
throw new Error(`Task ${id} not found`);
|
|
@@ -192,6 +194,43 @@ const TASK_TYPES = [
|
|
|
192
194
|
"style"
|
|
193
195
|
];
|
|
194
196
|
|
|
197
|
+
async function promptForTask(dir) {
|
|
198
|
+
const type = await consola.prompt("Task type", {
|
|
199
|
+
type: "select",
|
|
200
|
+
options: TASK_TYPES.map((t) => t)
|
|
201
|
+
});
|
|
202
|
+
if (typeof type === "symbol") process.exit(0);
|
|
203
|
+
const title = await consola.prompt("Task title", {
|
|
204
|
+
type: "text",
|
|
205
|
+
placeholder: "e.g. add user authentication"
|
|
206
|
+
});
|
|
207
|
+
if (typeof title === "symbol") process.exit(0);
|
|
208
|
+
if (!title.trim()) {
|
|
209
|
+
consola.error("Title cannot be empty");
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
const desc = await consola.prompt("Description (optional, enter to skip)", {
|
|
213
|
+
type: "text",
|
|
214
|
+
placeholder: "Describe the task in detail"
|
|
215
|
+
});
|
|
216
|
+
if (typeof desc === "symbol") process.exit(0);
|
|
217
|
+
const criteria = [];
|
|
218
|
+
while (true) {
|
|
219
|
+
const ac = await consola.prompt(
|
|
220
|
+
criteria.length === 0 ? "Acceptance criterion (Esc to skip)" : `Criterion #${criteria.length + 1} (Esc to finish)`,
|
|
221
|
+
{ type: "text" }
|
|
222
|
+
);
|
|
223
|
+
if (typeof ac === "symbol") break;
|
|
224
|
+
if (ac.trim()) criteria.push(ac.trim());
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
dir,
|
|
228
|
+
type,
|
|
229
|
+
title,
|
|
230
|
+
description: desc || void 0,
|
|
231
|
+
acceptance_criteria: criteria.length > 0 ? criteria : void 0
|
|
232
|
+
};
|
|
233
|
+
}
|
|
195
234
|
const createCommand = defineCommand({
|
|
196
235
|
meta: {
|
|
197
236
|
name: "create",
|
|
@@ -201,36 +240,46 @@ const createCommand = defineCommand({
|
|
|
201
240
|
type: {
|
|
202
241
|
type: "positional",
|
|
203
242
|
description: "Task type (feat, fix, chore, docs, perf, refactor, test, ci, build, style)",
|
|
204
|
-
required:
|
|
243
|
+
required: false
|
|
205
244
|
},
|
|
206
245
|
title: {
|
|
207
246
|
type: "positional",
|
|
208
247
|
description: "Task title",
|
|
209
|
-
required:
|
|
248
|
+
required: false
|
|
210
249
|
},
|
|
211
250
|
desc: {
|
|
212
251
|
type: "string",
|
|
252
|
+
alias: "d",
|
|
213
253
|
description: "Task description"
|
|
214
254
|
},
|
|
215
255
|
ac: {
|
|
216
256
|
type: "string",
|
|
257
|
+
alias: "a",
|
|
217
258
|
description: "Acceptance criterion (can be specified multiple times)"
|
|
218
259
|
}
|
|
219
260
|
},
|
|
220
|
-
run({ args }) {
|
|
221
|
-
|
|
222
|
-
|
|
261
|
+
async run({ args }) {
|
|
262
|
+
const dir = process.cwd();
|
|
263
|
+
const interactive = !args.type && !args.title;
|
|
264
|
+
if (!interactive && (!args.type || !args.title)) {
|
|
265
|
+
consola.error("Both type and title are required, or omit both for interactive mode");
|
|
223
266
|
process.exit(1);
|
|
224
267
|
}
|
|
225
|
-
const dir = process.cwd();
|
|
226
268
|
try {
|
|
227
|
-
const
|
|
269
|
+
const opts = interactive ? await promptForTask(dir) : {
|
|
228
270
|
dir,
|
|
229
|
-
type:
|
|
271
|
+
type: (() => {
|
|
272
|
+
if (!TASK_TYPES.includes(args.type)) {
|
|
273
|
+
consola.error(`Invalid type "${args.type}". Must be one of: ${TASK_TYPES.join(", ")}`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
return args.type;
|
|
277
|
+
})(),
|
|
230
278
|
title: args.title,
|
|
231
279
|
description: args.desc,
|
|
232
280
|
acceptance_criteria: args.ac ? Array.isArray(args.ac) ? args.ac : [args.ac] : void 0
|
|
233
|
-
}
|
|
281
|
+
};
|
|
282
|
+
const task = createTask(opts);
|
|
234
283
|
consola.success(`Created ${task.id}: ${task.title}`);
|
|
235
284
|
} catch (err) {
|
|
236
285
|
consola.error(String(err.message));
|
|
@@ -247,10 +296,12 @@ const listCommand = defineCommand({
|
|
|
247
296
|
args: {
|
|
248
297
|
type: {
|
|
249
298
|
type: "string",
|
|
299
|
+
alias: "t",
|
|
250
300
|
description: "Filter by task type"
|
|
251
301
|
},
|
|
252
302
|
status: {
|
|
253
303
|
type: "string",
|
|
304
|
+
alias: "s",
|
|
254
305
|
description: "Filter by status (todo, in-progress, done)"
|
|
255
306
|
}
|
|
256
307
|
},
|
|
@@ -300,6 +351,63 @@ const doneCommand = defineCommand({
|
|
|
300
351
|
}
|
|
301
352
|
});
|
|
302
353
|
|
|
354
|
+
function findPackageRoot() {
|
|
355
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
356
|
+
while (dir !== dirname(dir)) {
|
|
357
|
+
if (existsSync(join(dir, "package.json"))) {
|
|
358
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
|
|
359
|
+
if (pkg.name === "@xxkeefer/mrkl") return dir;
|
|
360
|
+
}
|
|
361
|
+
dir = dirname(dir);
|
|
362
|
+
}
|
|
363
|
+
throw new Error("Could not locate mrkl package root");
|
|
364
|
+
}
|
|
365
|
+
function copyDirSync(src, dest) {
|
|
366
|
+
mkdirSync(dest, { recursive: true });
|
|
367
|
+
for (const entry of readdirSync(src)) {
|
|
368
|
+
const srcPath = join(src, entry);
|
|
369
|
+
const destPath = join(dest, entry);
|
|
370
|
+
if (statSync(srcPath).isDirectory()) {
|
|
371
|
+
copyDirSync(srcPath, destPath);
|
|
372
|
+
} else {
|
|
373
|
+
writeFileSync(destPath, readFileSync(srcPath));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const installSkillsCommand = defineCommand({
|
|
378
|
+
meta: {
|
|
379
|
+
name: "install-skills",
|
|
380
|
+
description: "Install mrkl Claude Code skills into the current project"
|
|
381
|
+
},
|
|
382
|
+
run() {
|
|
383
|
+
const dest = join(process.cwd(), ".claude", "skills");
|
|
384
|
+
try {
|
|
385
|
+
const packageRoot = findPackageRoot();
|
|
386
|
+
const skillsDir = join(packageRoot, "skills");
|
|
387
|
+
if (!existsSync(skillsDir)) {
|
|
388
|
+
consola.error("No skills directory found in mrkl package");
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
const skills = readdirSync(skillsDir).filter(
|
|
392
|
+
(f) => statSync(join(skillsDir, f)).isDirectory()
|
|
393
|
+
);
|
|
394
|
+
if (skills.length === 0) {
|
|
395
|
+
consola.info("No skills to install");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
for (const skill of skills) {
|
|
399
|
+
const src = join(skillsDir, skill);
|
|
400
|
+
const target = join(dest, skill);
|
|
401
|
+
copyDirSync(src, target);
|
|
402
|
+
consola.success(`Installed ${skill}`);
|
|
403
|
+
}
|
|
404
|
+
} catch (err) {
|
|
405
|
+
consola.error(String(err.message));
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
303
411
|
const main = defineCommand({
|
|
304
412
|
meta: {
|
|
305
413
|
name: "mrkl",
|
|
@@ -308,10 +416,14 @@ const main = defineCommand({
|
|
|
308
416
|
},
|
|
309
417
|
subCommands: {
|
|
310
418
|
init: initCommand,
|
|
419
|
+
i: initCommand,
|
|
311
420
|
create: createCommand,
|
|
312
421
|
c: createCommand,
|
|
313
422
|
list: listCommand,
|
|
314
|
-
|
|
423
|
+
ls: listCommand,
|
|
424
|
+
done: doneCommand,
|
|
425
|
+
d: doneCommand,
|
|
426
|
+
"install-skills": installSkillsCommand
|
|
315
427
|
}
|
|
316
428
|
});
|
|
317
429
|
runMain(main);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xxkeefer/mrkl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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
|