@xxkeefer/mrkl 0.1.0 → 0.2.4

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 +232 -0
  2. package/dist/cli.mjs +205 -31
  3. package/package.json +6 -3
package/README.md ADDED
@@ -0,0 +1,232 @@
1
+ <p align="center">
2
+ <h1 align="center">mrkl</h1>
3
+ <p align="center">
4
+ Lightweight CLI for structured markdown task tracking.
5
+ <br />
6
+ Track work in your repo, not in a separate app.
7
+ </p>
8
+ </p>
9
+
10
+ <p align="center">
11
+ <a href="https://www.npmjs.com/package/@xxkeefer/mrkl"><img src="https://img.shields.io/npm/v/@xxkeefer/mrkl" alt="npm version" /></a>
12
+ <a href="https://github.com/xxKeefer/mrkl/blob/main/LICENSE"><img src="https://img.shields.io/github/license/xxKeefer/mrkl" alt="license" /></a>
13
+ <a href="https://github.com/xxKeefer/mrkl"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs welcome" /></a>
14
+ </p>
15
+
16
+ ---
17
+
18
+ ## Why mrkl?
19
+
20
+ 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
+
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
27
+
28
+ ## Install
29
+
30
+ ```sh
31
+ npm install -g @xxkeefer/mrkl
32
+ ```
33
+
34
+ Or use without installing:
35
+
36
+ ```sh
37
+ npx @xxkeefer/mrkl init MY_PROJECT
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```sh
43
+ # Initialize in your project root
44
+ mrkl init PROJ
45
+
46
+ # Create tasks
47
+ mrkl create feat "user authentication"
48
+ mrkl create fix "login redirect loop" --desc "Users get stuck after OAuth callback"
49
+ mrkl create feat "dark mode" --ac "toggle in settings" --ac "persists across sessions"
50
+
51
+ # View active tasks
52
+ mrkl list
53
+ # PROJ-001 feat todo user authentication
54
+ # PROJ-002 fix todo login redirect loop
55
+ # PROJ-003 feat todo dark mode
56
+
57
+ # Filter by type or status
58
+ mrkl list --type fix
59
+ mrkl list --status todo
60
+
61
+ # Archive a completed task
62
+ mrkl done PROJ-001
63
+ ```
64
+
65
+ ## Commands
66
+
67
+ ### `mrkl init <prefix>`
68
+
69
+ Initializes mrkl in the current directory.
70
+
71
+ | Argument | Description |
72
+ |----------|-------------|
73
+ | `prefix` | Project prefix for task IDs (e.g., `PROJ`, `API`, `WEB`) |
74
+
75
+ Creates:
76
+ - `.config/mrkl/mrkl.toml` — project configuration
77
+ - `.config/mrkl/mrkl_counter` — auto-incrementing ID tracker
78
+ - `.tasks/` — active task directory
79
+ - `.tasks/.archive/` — completed task storage
80
+
81
+ Safe to run multiple times — existing config and counter are preserved.
82
+
83
+ ### `mrkl create <type> <title> [options]`
84
+
85
+ Creates a new task file.
86
+
87
+ | Argument | Description |
88
+ |----------|-------------|
89
+ | `type` | Task type (see [Task Types](#task-types)) |
90
+ | `title` | Short description of the task |
91
+
92
+ | Option | Description |
93
+ |--------|-------------|
94
+ | `--desc <text>` | Detailed description |
95
+ | `--ac <text>` | Acceptance criterion (repeatable) |
96
+
97
+ ```sh
98
+ mrkl create feat "search functionality" \
99
+ --desc "Full-text search across all documents" \
100
+ --ac "search bar in header" \
101
+ --ac "results update as you type" \
102
+ --ac "highlights matching terms"
103
+ ```
104
+
105
+ ### `mrkl list [options]`
106
+
107
+ Lists all active tasks.
108
+
109
+ | Option | Description |
110
+ |--------|-------------|
111
+ | `--type <type>` | Filter by task type |
112
+ | `--status <status>` | Filter by status (`todo`, `in-progress`, `done`) |
113
+
114
+ Non-conforming markdown files in the tasks directory are silently skipped.
115
+
116
+ ### `mrkl done <id>`
117
+
118
+ Archives a completed task.
119
+
120
+ | Argument | Description |
121
+ |----------|-------------|
122
+ | `id` | Task ID to archive (e.g., `PROJ-001`) |
123
+
124
+ Moves the task file to `.tasks/.archive/` and sets its status to `done`.
125
+
126
+ ## Task Types
127
+
128
+ mrkl uses [conventional commit](https://www.conventionalcommits.org/) types:
129
+
130
+ | Type | Purpose |
131
+ |------|---------|
132
+ | `feat` | New feature |
133
+ | `fix` | Bug fix |
134
+ | `chore` | Maintenance |
135
+ | `docs` | Documentation |
136
+ | `perf` | Performance improvement |
137
+ | `refactor` | Code restructuring |
138
+ | `test` | Testing |
139
+ | `ci` | CI/CD changes |
140
+ | `build` | Build system changes |
141
+ | `style` | Code style/formatting |
142
+
143
+ ## Task File Format
144
+
145
+ Each task is a markdown file with YAML frontmatter:
146
+
147
+ ```
148
+ .tasks/PROJ-001 feat - user authentication.md
149
+ ```
150
+
151
+ ```markdown
152
+ ---
153
+ id: PROJ-001
154
+ type: feat
155
+ status: todo
156
+ created: '2026-03-01'
157
+ ---
158
+ ## Description
159
+
160
+ Implement user authentication with OAuth2.
161
+
162
+ ## Acceptance Criteria
163
+
164
+ - [ ] login page renders
165
+ - [ ] OAuth flow completes
166
+ - [ ] session persists across refreshes
167
+ ```
168
+
169
+ The format is intentionally simple — edit task files directly when you need to update descriptions, change status, or check off criteria.
170
+
171
+ ## Project Structure
172
+
173
+ After initialization, mrkl adds the following to your project:
174
+
175
+ ```
176
+ your-project/
177
+ .config/mrkl/
178
+ mrkl.toml # project configuration
179
+ mrkl_counter # current task number
180
+ .tasks/
181
+ PROJ-001 feat - first task.md
182
+ PROJ-002 fix - second task.md
183
+ .archive/
184
+ PROJ-000 chore - archived task.md
185
+ ```
186
+
187
+ Commit `.config/mrkl/` and `.tasks/` to version control. They're designed to be tracked alongside your code.
188
+
189
+ ## Configuration
190
+
191
+ Configuration lives in `.config/mrkl/mrkl.toml` (or `mrkl.toml` at the project root):
192
+
193
+ ```toml
194
+ prefix = "PROJ"
195
+ tasks_dir = ".tasks"
196
+ ```
197
+
198
+ | Key | Default | Description |
199
+ |-----|---------|-------------|
200
+ | `prefix` | *(required)* | Project prefix for task IDs |
201
+ | `tasks_dir` | `".tasks"` | Directory for task files |
202
+
203
+ ## Development
204
+
205
+ ```sh
206
+ git clone https://github.com/xxKeefer/mrkl.git
207
+ cd mrkl
208
+ npm install
209
+
210
+ # Run tests
211
+ npm test
212
+
213
+ # Run CLI in development
214
+ npx tsx src/cli.ts list
215
+
216
+ # Build
217
+ npm run build
218
+ ```
219
+
220
+ ## Contributing
221
+
222
+ Contributions are welcome. Please open an issue first to discuss what you'd like to change.
223
+
224
+ 1. Fork the repo
225
+ 2. Create your branch (`git checkout -b feat/my-feature`)
226
+ 3. Commit your changes using [conventional commits](https://www.conventionalcommits.org/)
227
+ 4. Push to your branch (`git push origin feat/my-feature`)
228
+ 5. Open a Pull Request
229
+
230
+ ## License
231
+
232
+ [MIT](LICENSE)
package/dist/cli.mjs CHANGED
@@ -1,9 +1,45 @@
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';
6
+ import { stringify, parse as parse$1 } from 'smol-toml';
7
+ import matter from 'gray-matter';
4
8
 
5
- function initConfig(_dir, _opts) {
6
- throw new Error("not implemented");
9
+ const CONFIG_PATHS = [
10
+ join(".config", "mrkl", "mrkl.toml"),
11
+ "mrkl.toml"
12
+ ];
13
+ function loadConfig(dir) {
14
+ const configPath = CONFIG_PATHS.map((p) => join(dir, p)).find((p) => existsSync(p));
15
+ if (!configPath) {
16
+ throw new Error(`mrkl.toml not found in ${dir}`);
17
+ }
18
+ const raw = readFileSync(configPath, "utf-8");
19
+ const parsed = parse$1(raw);
20
+ return {
21
+ prefix: parsed.prefix,
22
+ tasks_dir: parsed.tasks_dir ?? ".tasks"
23
+ };
24
+ }
25
+ function initConfig(dir, opts) {
26
+ const configDir = join(dir, ".config", "mrkl");
27
+ const configPath = join(configDir, "mrkl.toml");
28
+ if (!existsSync(configPath)) {
29
+ const config2 = {
30
+ prefix: opts?.prefix ?? "TASK",
31
+ tasks_dir: opts?.tasks_dir ?? ".tasks"
32
+ };
33
+ mkdirSync(configDir, { recursive: true });
34
+ writeFileSync(configPath, stringify(config2));
35
+ }
36
+ const config = loadConfig(dir);
37
+ const tasksDir = join(dir, config.tasks_dir);
38
+ mkdirSync(join(tasksDir, ".archive"), { recursive: true });
39
+ const counterPath = join(dir, ".config", "mrkl", "mrkl_counter");
40
+ if (!existsSync(counterPath)) {
41
+ writeFileSync(counterPath, "0");
42
+ }
7
43
  }
8
44
 
9
45
  const initCommand = defineCommand({
@@ -19,21 +55,138 @@ const initCommand = defineCommand({
19
55
  },
20
56
  run({ args }) {
21
57
  const dir = process.cwd();
22
- initConfig(dir, { prefix: args.prefix });
23
- consola.success("mrkl initialized");
58
+ try {
59
+ initConfig(dir, { prefix: args.prefix });
60
+ consola.success("mrkl initialized");
61
+ } catch (err) {
62
+ consola.error(String(err.message));
63
+ process.exit(1);
64
+ }
24
65
  }
25
66
  });
26
67
 
27
- function createTask(_opts) {
28
- throw new Error("not implemented");
68
+ const COUNTER_FILE = join(".config", "mrkl", "mrkl_counter");
69
+ function nextId(dir) {
70
+ const filePath = join(dir, COUNTER_FILE);
71
+ const current = existsSync(filePath) ? parseInt(readFileSync(filePath, "utf-8").trim(), 10) : 0;
72
+ const next = current + 1;
73
+ writeFileSync(filePath, String(next));
74
+ return next;
29
75
  }
30
- function listTasks(_filter) {
31
- throw new Error("not implemented");
76
+
77
+ function render(task) {
78
+ const frontmatter = {
79
+ id: task.id,
80
+ type: task.type,
81
+ status: task.status,
82
+ created: task.created
83
+ };
84
+ const sections = [];
85
+ sections.push("## Description");
86
+ sections.push("");
87
+ sections.push(task.description || "");
88
+ sections.push("");
89
+ sections.push("## Acceptance Criteria");
90
+ sections.push("");
91
+ for (const item of task.acceptance_criteria) {
92
+ sections.push(`- [ ] ${item}`);
93
+ }
94
+ const body = sections.join("\n") + "\n";
95
+ return matter.stringify(body, frontmatter);
32
96
  }
33
- function archiveTask(_dir, _id) {
34
- throw new Error("not implemented");
97
+ function parse(content, filename) {
98
+ const { data, content: body } = matter(content);
99
+ const titleMatch = filename.replace(/\.md$/, "").match(/^\S+\s+\S+\s+-\s+(.+)$/);
100
+ const title = titleMatch ? titleMatch[1] : "";
101
+ const acRegex = /^- \[[ x]\] (.+)$/gm;
102
+ const acceptance_criteria = [];
103
+ let match;
104
+ while ((match = acRegex.exec(body)) !== null) {
105
+ acceptance_criteria.push(match[1]);
106
+ }
107
+ const descMatch = body.match(/## Description\n\n([\s\S]*?)(?:\n\n## Acceptance Criteria|$)/);
108
+ const description = descMatch ? descMatch[1].trim() : "";
109
+ return {
110
+ id: data.id,
111
+ type: data.type,
112
+ status: data.status,
113
+ created: data.created,
114
+ title,
115
+ description,
116
+ acceptance_criteria
117
+ };
35
118
  }
36
119
 
120
+ function createTask(opts) {
121
+ const config = loadConfig(opts.dir);
122
+ const num = nextId(opts.dir);
123
+ const id = `${config.prefix}-${String(num).padStart(3, "0")}`;
124
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
125
+ const task = {
126
+ id,
127
+ type: opts.type,
128
+ status: "todo",
129
+ created: today,
130
+ title: opts.title,
131
+ description: opts.description ?? "",
132
+ acceptance_criteria: opts.acceptance_criteria ?? []
133
+ };
134
+ const filename = `${id} ${task.type} - ${task.title}.md`;
135
+ const tasksDir = join(opts.dir, config.tasks_dir);
136
+ writeFileSync(join(tasksDir, filename), render(task));
137
+ return task;
138
+ }
139
+ function listTasks(filter) {
140
+ const config = loadConfig(filter.dir);
141
+ const tasksDir = join(filter.dir, config.tasks_dir);
142
+ const files = readdirSync(tasksDir).filter(
143
+ (f) => f.endsWith(".md") && !f.startsWith(".")
144
+ );
145
+ let tasks = files.flatMap((f) => {
146
+ try {
147
+ const content = readFileSync(join(tasksDir, f), "utf-8");
148
+ const task = parse(content, f);
149
+ if (!task.id || !task.type || !task.status) return [];
150
+ return [task];
151
+ } catch {
152
+ return [];
153
+ }
154
+ });
155
+ if (filter.type) tasks = tasks.filter((t) => t.type === filter.type);
156
+ if (filter.status) tasks = tasks.filter((t) => t.status === filter.status);
157
+ return tasks;
158
+ }
159
+ function archiveTask(dir, id) {
160
+ const config = loadConfig(dir);
161
+ const tasksDir = join(dir, config.tasks_dir);
162
+ const file = readdirSync(tasksDir).find(
163
+ (f) => f.endsWith(".md") && f.startsWith(id)
164
+ );
165
+ if (!file) {
166
+ throw new Error(`Task ${id} not found`);
167
+ }
168
+ const filePath = join(tasksDir, file);
169
+ const content = readFileSync(filePath, "utf-8");
170
+ const task = parse(content, file);
171
+ task.status = "done";
172
+ const archivePath = join(tasksDir, ".archive", file);
173
+ writeFileSync(archivePath, render(task));
174
+ unlinkSync(filePath);
175
+ }
176
+
177
+ const TASK_TYPES = [
178
+ "feat",
179
+ "fix",
180
+ "chore",
181
+ "docs",
182
+ "perf",
183
+ "refactor",
184
+ "test",
185
+ "ci",
186
+ "build",
187
+ "style"
188
+ ];
189
+
37
190
  const createCommand = defineCommand({
38
191
  meta: {
39
192
  name: "create",
@@ -60,14 +213,24 @@ const createCommand = defineCommand({
60
213
  }
61
214
  },
62
215
  run({ args }) {
63
- process.cwd();
64
- const task = createTask({
65
- type: args.type,
66
- title: args.title,
67
- description: args.desc,
68
- acceptance_criteria: args.ac ? [args.ac] : void 0
69
- });
70
- consola.success(`Created ${task.id}: ${task.title}`);
216
+ if (!TASK_TYPES.includes(args.type)) {
217
+ consola.error(`Invalid type "${args.type}". Must be one of: ${TASK_TYPES.join(", ")}`);
218
+ process.exit(1);
219
+ }
220
+ const dir = process.cwd();
221
+ try {
222
+ const task = createTask({
223
+ dir,
224
+ type: args.type,
225
+ title: args.title,
226
+ description: args.desc,
227
+ acceptance_criteria: args.ac ? [args.ac] : void 0
228
+ });
229
+ consola.success(`Created ${task.id}: ${task.title}`);
230
+ } catch (err) {
231
+ consola.error(String(err.message));
232
+ process.exit(1);
233
+ }
71
234
  }
72
235
  });
73
236
 
@@ -87,17 +250,23 @@ const listCommand = defineCommand({
87
250
  }
88
251
  },
89
252
  run({ args }) {
90
- process.cwd();
91
- const tasks = listTasks({
92
- type: args.type,
93
- status: args.status
94
- });
95
- if (tasks.length === 0) {
96
- consola.info("No tasks found");
97
- return;
98
- }
99
- for (const task of tasks) {
100
- consola.log(`${task.id} ${task.type.padEnd(10)} ${task.status.padEnd(12)} ${task.title}`);
253
+ const dir = process.cwd();
254
+ try {
255
+ const tasks = listTasks({
256
+ dir,
257
+ type: args.type,
258
+ status: args.status
259
+ });
260
+ if (tasks.length === 0) {
261
+ consola.info("No tasks found");
262
+ return;
263
+ }
264
+ for (const task of tasks) {
265
+ consola.log(`${task.id} ${task.type.padEnd(10)} ${task.status.padEnd(12)} ${task.title}`);
266
+ }
267
+ } catch (err) {
268
+ consola.error(String(err.message));
269
+ process.exit(1);
101
270
  }
102
271
  }
103
272
  });
@@ -116,8 +285,13 @@ const doneCommand = defineCommand({
116
285
  },
117
286
  run({ args }) {
118
287
  const dir = process.cwd();
119
- archiveTask(dir, args.id);
120
- consola.success(`Archived ${args.id}`);
288
+ try {
289
+ archiveTask(dir, args.id);
290
+ consola.success(`Archived ${args.id}`);
291
+ } catch (err) {
292
+ consola.error(String(err.message));
293
+ process.exit(1);
294
+ }
121
295
  }
122
296
  });
123
297
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xxkeefer/mrkl",
3
- "version": "0.1.0",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "description": "Lightweight CLI tool for structured markdown task tracking",
6
6
  "bin": {
@@ -9,7 +9,8 @@
9
9
  "scripts": {
10
10
  "build": "unbuild",
11
11
  "dev": "node --import tsx src/cli.ts",
12
- "test": "vitest run"
12
+ "test": "vitest run",
13
+ "release": "./scripts/release.sh"
13
14
  },
14
15
  "keywords": [
15
16
  "cli",
@@ -18,7 +19,9 @@
18
19
  "markdown"
19
20
  ],
20
21
  "license": "MIT",
21
- "files": ["dist"],
22
+ "files": [
23
+ "dist"
24
+ ],
22
25
  "repository": {
23
26
  "type": "git",
24
27
  "url": "git+https://github.com/xxKeefer/mrkl.git"