@xxkeefer/mrkl 0.2.6 → 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 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(id)
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: true
243
+ required: false
205
244
  },
206
245
  title: {
207
246
  type: "positional",
208
247
  description: "Task title",
209
- required: true
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
- if (!TASK_TYPES.includes(args.type)) {
222
- consola.error(`Invalid type "${args.type}". Must be one of: ${TASK_TYPES.join(", ")}`);
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 task = createTask({
269
+ const opts = interactive ? await promptForTask(dir) : {
228
270
  dir,
229
- type: args.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
- done: doneCommand
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.2.6",
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",
@@ -41,8 +42,7 @@
41
42
  "build": "unbuild",
42
43
  "dev": "node --import tsx src/cli.ts",
43
44
  "test": "vitest run",
44
- "release": "./scripts/release.sh",
45
- "changelog": "changelogen",
45
+ "changelog": "changelogen --output",
46
46
  "changelog:preview": "changelogen --no-output"
47
47
  }
48
48
  }
@@ -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