@smekai/taskplanner 2.1.14
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/LICENSE +21 -0
- package/README.md +156 -0
- package/bin/taskplanner-mcp.js +4 -0
- package/dist/ai/aiInstructions.d.ts +24 -0
- package/dist/ai/codexDeepLink.d.ts +4 -0
- package/dist/ai/planMode.d.ts +1 -0
- package/dist/ai/promptComposer.d.ts +3 -0
- package/dist/config/configManager.d.ts +18 -0
- package/dist/filter/taskFilter.d.ts +7 -0
- package/dist/id/idGenerator.d.ts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +25 -0
- package/dist/mcp-server.js +90 -0
- package/dist/model/config.d.ts +15 -0
- package/dist/model/messages.d.ts +65 -0
- package/dist/model/parseResult.d.ts +9 -0
- package/dist/model/state.d.ts +6 -0
- package/dist/model/task.d.ts +19 -0
- package/dist/parser/taskParser.d.ts +7 -0
- package/dist/parser/taskSerializer.d.ts +3 -0
- package/dist/project/projectSync.d.ts +26 -0
- package/dist/store/fileStore.d.ts +18 -0
- package/dist/store/taskStore.d.ts +70 -0
- package/dist/util/time.d.ts +2 -0
- package/dist/validation/duplicateDetector.d.ts +18 -0
- package/dist/view/boardViewModel.d.ts +13 -0
- package/package.json +51 -0
- package/ui/board/index.html +493 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 smekai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# @smekai/taskplanner
|
|
2
|
+
|
|
3
|
+
[TaskPlanner](https://github.com/smekai/taskplanner)'s task board as a standalone npm package — as a
|
|
4
|
+
library you call, and as an MCP server you spawn.
|
|
5
|
+
|
|
6
|
+
TaskPlanner stores tasks as markdown in a repository's `.tasks/` directory. This package reads and
|
|
7
|
+
writes that board: directly from your code, or over stdio for an MCP client. It is the same server
|
|
8
|
+
the VS Code extension and the Cursor/Codex plugin run, built from the same sources. There is one
|
|
9
|
+
board parser, not a fork.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @smekai/taskplanner
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The published bundle is self-contained (the MCP SDK and Zod are bundled in), so the package has no
|
|
16
|
+
runtime dependencies. It is CommonJS and requires Node >= 20.
|
|
17
|
+
|
|
18
|
+
## Two entry points
|
|
19
|
+
|
|
20
|
+
Pick by who is calling:
|
|
21
|
+
|
|
22
|
+
| Entry | Import | Use when |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| Library | `@smekai/taskplanner` | **Your own code** reads or edits the board. Direct calls, typed, no subprocess. |
|
|
25
|
+
| MCP server | `@smekai/taskplanner/mcp-server` | **A model** picks the tools. Spawned over stdio; ships tool schemas, descriptions, and safety annotations. |
|
|
26
|
+
|
|
27
|
+
Both are built from the same `src/core/`, so they cannot disagree about what a board says. Use both
|
|
28
|
+
in one host if it suits — the agent gets MCP tools, your own code calls the library.
|
|
29
|
+
|
|
30
|
+
## Using it as a library
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const { parseTasks, TaskStore, FileStore, ConfigManager } = require('@smekai/taskplanner');
|
|
34
|
+
|
|
35
|
+
// Parse a single state file
|
|
36
|
+
const { tasks, warnings } = parseTasks(fs.readFileSync('.tasks/BACKLOG.md', 'utf8'));
|
|
37
|
+
|
|
38
|
+
// Or drive the whole board
|
|
39
|
+
const configManager = new ConfigManager('/path/to/repo/.tasks');
|
|
40
|
+
configManager.load();
|
|
41
|
+
const store = new TaskStore(configManager, new FileStore('/path/to/repo/.tasks'));
|
|
42
|
+
store.reload();
|
|
43
|
+
store.moveTask('TASK-001', 'In Progress');
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
TypeScript declarations ship with the package. ESM named imports work too:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
import { parseTasks } from '@smekai/taskplanner';
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Requiring the library does **not** start a server — that lives on its own subpath precisely so this
|
|
53
|
+
import stays inert.
|
|
54
|
+
|
|
55
|
+
## Spawning the server
|
|
56
|
+
|
|
57
|
+
**Spawn `process.execPath` with a resolved module path. Never spawn a bare bin name.**
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const { spawn } = require('node:child_process');
|
|
61
|
+
|
|
62
|
+
const serverPath = require.resolve('@smekai/taskplanner/mcp-server');
|
|
63
|
+
|
|
64
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
65
|
+
env: { ...process.env, TASKPLANNER_WORKSPACE_ROOT: '/path/to/the/repo' },
|
|
66
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
From ESM:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { createRequire } from 'node:module';
|
|
74
|
+
const serverPath = createRequire(import.meta.url).resolve('@smekai/taskplanner/mcp-server');
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Why not the bin name? `taskplanner-mcp` resolves to a `.cmd` shim on Windows, and spawning a `.cmd`
|
|
78
|
+
requires `shell: true` under Node >= 20's [command injection
|
|
79
|
+
rule](https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2) — which then
|
|
80
|
+
re-introduces quoting problems on paths with spaces. `process.execPath` plus a resolved path has
|
|
81
|
+
none of that, and behaves identically on Windows, macOS, and Linux. The `taskplanner-mcp` bin
|
|
82
|
+
exists for `npx` and interactive shell use only.
|
|
83
|
+
|
|
84
|
+
## Telling the server which repository to read
|
|
85
|
+
|
|
86
|
+
The server needs a workspace root. In order of precedence:
|
|
87
|
+
|
|
88
|
+
1. The `workspace_root` argument on any tool call — per call, so one client can drive several
|
|
89
|
+
repositories.
|
|
90
|
+
2. The `TASKPLANNER_WORKSPACE_ROOT` environment variable — process-wide, for a host that spawns one
|
|
91
|
+
server per repository.
|
|
92
|
+
3. MCP `roots/list`, if the client advertises the `roots` capability.
|
|
93
|
+
4. The process working directory.
|
|
94
|
+
|
|
95
|
+
Both path 1 and path 2 are supported and stay supported; editor clients tend to use the tool input,
|
|
96
|
+
and hosts driving several projects from one process tend to use the environment variable.
|
|
97
|
+
|
|
98
|
+
Whichever root is supplied, the server walks **up** from it looking for a `.tasks/config.json`, so
|
|
99
|
+
pointing it at a subdirectory of the repository works.
|
|
100
|
+
|
|
101
|
+
If no `.tasks/` is found, tool calls fail with an error listing every directory that was checked.
|
|
102
|
+
|
|
103
|
+
## MCP client configuration
|
|
104
|
+
|
|
105
|
+
For clients that read a JSON config, resolve the path once and write it in:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"mcpServers": {
|
|
110
|
+
"taskplanner": {
|
|
111
|
+
"command": "node",
|
|
112
|
+
"args": ["/absolute/path/to/node_modules/@smekai/taskplanner/dist/mcp-server.js"],
|
|
113
|
+
"env": { "TASKPLANNER_WORKSPACE_ROOT": "/path/to/the/repo" }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Tools
|
|
120
|
+
|
|
121
|
+
| Tool | Purpose |
|
|
122
|
+
| -------------------------- | ------------------------------------------------------------- |
|
|
123
|
+
| `taskplanner_board` | Board overview: task counts per state, optionally every task. |
|
|
124
|
+
| `taskplanner_list` | List tasks, filtered by state and/or a text query. |
|
|
125
|
+
| `taskplanner_get` | Read one task by ID, including its `### Plan`. |
|
|
126
|
+
| `taskplanner_create` | Create a task in a given state. |
|
|
127
|
+
| `taskplanner_update` | Update title, description, priority, tags, assignee, or plan. |
|
|
128
|
+
| `taskplanner_move` | Move a task to another state. |
|
|
129
|
+
| `taskplanner_board_data` | Structured board view model (for UI hosts). |
|
|
130
|
+
| `taskplanner_board_visual` | Board rendered as an MCP App UI resource. |
|
|
131
|
+
|
|
132
|
+
Every tool accepts the optional `workspace_root` argument described above.
|
|
133
|
+
|
|
134
|
+
## Task fields
|
|
135
|
+
|
|
136
|
+
Tasks are plain markdown sections and may be written by tools other than TaskPlanner. All task
|
|
137
|
+
metadata round-trips through this server unchanged, including `**Assignee:**`:
|
|
138
|
+
|
|
139
|
+
```markdown
|
|
140
|
+
## TASK-001: Task title
|
|
141
|
+
**Priority:** P1
|
|
142
|
+
**Tags:** core
|
|
143
|
+
**Assignee:** owner
|
|
144
|
+
|
|
145
|
+
Description.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Reading that task back reports `assignee: "owner"`, and the value survives `taskplanner_update` and
|
|
151
|
+
`taskplanner_move`. (The serializer normalises metadata onto a single `|`-joined line when it
|
|
152
|
+
rewrites a section — the field and its value are preserved, the line layout is not.)
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TaskPlannerConfig } from '../model/config.js';
|
|
2
|
+
declare const MARKER_START = "<!-- TASKPLANNER:START -->";
|
|
3
|
+
declare const MARKER_END = "<!-- TASKPLANNER:END -->";
|
|
4
|
+
declare const ATTRIBUTION_MARKER_START = "<!-- TASKPLANNER:ATTRIBUTION:START -->";
|
|
5
|
+
declare const ATTRIBUTION_MARKER_END = "<!-- TASKPLANNER:ATTRIBUTION:END -->";
|
|
6
|
+
declare const ATTRIBUTION_TEXT = "This project uses [TaskPlanner](https://github.com/smekai/taskplanner) for task planning.";
|
|
7
|
+
export { MARKER_START, MARKER_END, ATTRIBUTION_MARKER_START, ATTRIBUTION_MARKER_END, ATTRIBUTION_TEXT, };
|
|
8
|
+
/** True if synced TaskPlanner AI block is present (e.g. after Initialize AI Instructions). */
|
|
9
|
+
export declare function contentHasTaskPlannerMarkers(content: string): boolean;
|
|
10
|
+
export interface AiInstructions {
|
|
11
|
+
claudeMd: string;
|
|
12
|
+
cursorRules: string;
|
|
13
|
+
agentsMd: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function generateAiInstructions(config: TaskPlannerConfig): AiInstructions;
|
|
16
|
+
/** Default content for `.tasks/WORK_LOG.md` when initializing a new project. */
|
|
17
|
+
export declare const DEFAULT_WORK_LOG_CONTENT = "# Work Log\n\nTop-level trace of completed work and key decisions. One entry per task moved to Done \u2014 newest at top. Keep entries short (3\u20135 lines); detailed steps stay in each task's `### Plan` in `DONE.md`.\n\n**Entry template** (insert after this header, before existing entries):\n\n```markdown\n## TASK-### \u2014 YYYY-MM-DD\n**What:** One-line summary of what was delivered.\n**Decisions:** Key choices made and why (skip if none).\n**Outcome:** Result or follow-ups (skip if obvious from What).\n\n---\n```\n";
|
|
18
|
+
/**
|
|
19
|
+
* Insert or update the TaskPlanner section in an existing file content.
|
|
20
|
+
* Uses marker comments to make the operation idempotent.
|
|
21
|
+
*/
|
|
22
|
+
export declare function upsertMarkedSection(existingContent: string, section: string): string;
|
|
23
|
+
/** Add or refresh the voluntary README attribution without touching surrounding content. */
|
|
24
|
+
export declare function upsertReadmeAttribution(existingContent: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function shouldAutomateCursorPlanMode(aiPlanRequired: boolean, automationEnabled: boolean): boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TaskPlannerConfig } from '../model/config.js';
|
|
2
|
+
export declare class ConfigManager {
|
|
3
|
+
private tasksDir;
|
|
4
|
+
private config;
|
|
5
|
+
private configPath;
|
|
6
|
+
constructor(tasksDir: string);
|
|
7
|
+
load(): TaskPlannerConfig;
|
|
8
|
+
/** Re-read config.json without running migrations — picks up concurrent writes. */
|
|
9
|
+
reloadFromDisk(): void;
|
|
10
|
+
private migrateConfig;
|
|
11
|
+
save(): void;
|
|
12
|
+
get(): TaskPlannerConfig;
|
|
13
|
+
getTasksDir(): string;
|
|
14
|
+
update(partial: Partial<TaskPlannerConfig>): void;
|
|
15
|
+
getNextId(): string;
|
|
16
|
+
/** Raise `nextId` to `floor` if it is below. Returns true when config changed. */
|
|
17
|
+
reconcileNextId(floor: number): boolean;
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Task } from '../model/task.js';
|
|
2
|
+
import { TaskState } from '../model/state.js';
|
|
3
|
+
import { TaskFilter, TaskViewData, GroupViewData } from '../model/messages.js';
|
|
4
|
+
export type TaskListSortBy = 'priority' | 'name' | 'id' | 'file';
|
|
5
|
+
export declare function sortTasks(tasks: Task[], sortBy: TaskListSortBy): Task[];
|
|
6
|
+
export declare function filterAndPaginate(tasksByState: Map<string, Task[]>, states: TaskState[], filter?: TaskFilter, limit?: number | null, sortBy?: TaskListSortBy, stateDisplayCounts?: ReadonlyMap<string, number>): TaskViewData;
|
|
7
|
+
export declare function groupTasks(tasksByState: Map<string, Task[]>, states: TaskState[], groupBy: 'status' | 'assignee' | 'date' | 'none', filter?: TaskFilter, limit?: number | null, sortBy?: TaskListSortBy, stateDisplayCounts?: ReadonlyMap<string, number>): GroupViewData[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ConfigManager } from '../config/configManager.js';
|
|
2
|
+
export declare class IdGenerator {
|
|
3
|
+
private configManager;
|
|
4
|
+
constructor(configManager: ConfigManager);
|
|
5
|
+
next(): string;
|
|
6
|
+
parseId(id: string): {
|
|
7
|
+
prefix: string;
|
|
8
|
+
number: number;
|
|
9
|
+
} | null;
|
|
10
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Task, Priority, isPriority } from './model/task.js';
|
|
2
|
+
export { ParseWarning, ParseResult } from './model/parseResult.js';
|
|
3
|
+
export { TaskState, DEFAULT_STATES } from './model/state.js';
|
|
4
|
+
export { TaskPlannerConfig, createDefaultConfig } from './model/config.js';
|
|
5
|
+
export { ConfigManager } from './config/configManager.js';
|
|
6
|
+
export { parseTasks, findTaskLineNumber, countTaskHeadings } from './parser/taskParser.js';
|
|
7
|
+
export { serializeTask, serializeStateFile } from './parser/taskSerializer.js';
|
|
8
|
+
export { IdGenerator } from './id/idGenerator.js';
|
|
9
|
+
export { FileStore } from './store/fileStore.js';
|
|
10
|
+
export { TaskStore, isDeferredStateName } from './store/taskStore.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";var Q=Object.create;var E=Object.defineProperty;var tt=Object.getOwnPropertyDescriptor;var et=Object.getOwnPropertyNames;var st=Object.getPrototypeOf,nt=Object.prototype.hasOwnProperty;var it=(r,t)=>{for(var e in t)E(r,e,{get:t[e],enumerable:!0})},q=(r,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of et(t))!nt.call(r,i)&&i!==e&&E(r,i,{get:()=>t[i],enumerable:!(s=tt(t,i))||s.enumerable});return r};var v=(r,t,e)=>(e=r!=null?Q(st(r)):{},q(t||!r||!r.__esModule?E(e,"default",{value:r,enumerable:!0}):e,r)),rt=r=>q(E({},"__esModule",{value:!0}),r);var ut={};it(ut,{ConfigManager:()=>b,DEFAULT_STATES:()=>I,FileStore:()=>M,IdGenerator:()=>T,Priority:()=>N,TaskStore:()=>C,countTaskHeadings:()=>O,createDefaultConfig:()=>k,findTaskLineNumber:()=>X,isDeferredStateName:()=>B,isPriority:()=>D,parseTasks:()=>w,serializeStateFile:()=>x,serializeTask:()=>j});module.exports=rt(ut);var N=(n=>(n.P0="P0",n.P1="P1",n.P2="P2",n.P3="P3",n.P4="P4",n))(N||{});function D(r){return Object.values(N).includes(r)}var I=[{name:"Backlog",fileName:"BACKLOG.md",order:0},{name:"Next",fileName:"NEXT.md",order:1},{name:"In Progress",fileName:"IN_PROGRESS.md",order:2},{name:"Done",fileName:"DONE.md",order:3},{name:"Rejected",fileName:"REJECTED.md",order:4}];function k(){return{version:2,taskplannerVersion:"",idPrefix:"TASK",nextId:1,states:[...I],priorities:["P0","P1","P2","P3","P4"],tags:[],insertPosition:"top",aiPlanRequired:!0,readmeAttribution:!0,sortBy:"priority"}}var h=v(require("fs")),J=v(require("path"));var b=class{constructor(t){this.tasksDir=t;this.configPath=J.join(t,"config.json"),this.config=k()}tasksDir;config;configPath;load(){if(h.existsSync(this.configPath)){let t=h.readFileSync(this.configPath,"utf-8"),e=JSON.parse(t);this.config={...k(),...e},this.migrateConfig()}else this.config=k();return this.config}reloadFromDisk(){if(!h.existsSync(this.configPath))return;let t=h.readFileSync(this.configPath,"utf-8"),e=JSON.parse(t);this.config={...k(),...e}}migrateConfig(){let t=!1;this.config.states.some(e=>e.name==="Rejected")||(this.config.states.push({name:"Rejected",fileName:"REJECTED.md",order:4}),t=!0),t&&(this.config.version=2,this.save())}save(){h.existsSync(this.tasksDir)||h.mkdirSync(this.tasksDir,{recursive:!0}),h.writeFileSync(this.configPath,JSON.stringify(this.config,null,2)+`
|
|
2
|
+
`,"utf-8")}get(){return this.config}getTasksDir(){return this.tasksDir}update(t){this.config={...this.config,...t}}getNextId(){let t=`${this.config.idPrefix}-${String(this.config.nextId).padStart(3,"0")}`;return this.config.nextId++,this.save(),t}reconcileNextId(t){return this.config.nextId<t?(this.config.nextId=t,this.save(),!0):!1}};var $=/^## ([A-Z]+-\d+):\s*(.+)$/,at=/^\*\*Priority:\*\*\s*(\S+)/,ot=/^\*\*Tags?:\*\*\s*(.+)/,ft=/^\*\*Epic:\*\*\s*(.+)/,dt=/^\*\*Assignee:\*\*\s*(.+)/,lt=/^\*\*Updated:\*\*\s*(.+)/,z=/^---\s*$/,ct=/^### Plan\s*$/;function _(r){return r.length>0&&r.charCodeAt(0)===65279?r.slice(1):r}function w(r){let e=_(r).split(`
|
|
3
|
+
`),s=[],i=[],n=null,a=0,o=[],f=[],d=!0,y=!1;function u(){if(n?.id&&n?.title){let p=f.join(`
|
|
4
|
+
`).trim();s.push({id:n.id,title:n.title,description:o.join(`
|
|
5
|
+
`).trim(),priority:n.priority??"P4",tags:n.tags??[],epic:n.epic,assignee:n.assignee,updatedAt:n.updatedAt,...p?{plan:p}:{}})}else n&&i.push({line:a,message:"Incomplete task section could not be parsed (invalid or empty title)"});n=null,o=[],f=[],d=!0,y=!1}for(let p=0;p<e.length;p++){let l=e[p],R=p+1,W=l.match($);if(W){u();let P=W[2].trim();if(!P){i.push({line:R,message:"Task heading has no title"}),n=null;continue}n={id:W[1],title:P,tags:[]},a=R,d=!0;continue}if(!n){if(l.trim()===""||z.test(l)||/^#\s/.test(l)&&!l.startsWith("##"))continue;if(/^##\s/.test(l)){i.push({line:R,message:"Invalid task heading (use ## PREFIX-NNN: Title with uppercase prefix and digits)"});continue}i.push({line:R,message:"Content is not part of any task (expected a ## TASK-NNN: Title heading)"});continue}if(z.test(l)){u();continue}if(d){let P=l.includes("|")?l.split("|").map(g=>g.trim()):[l],S=!1;for(let g of P){let G=g.match(at);if(G){let A=G[1].trim();n.priority=D(A)?A:"P4",S=!0;continue}let U=g.match(ot);if(U){n.tags=U[1].split(",").map(A=>A.trim()).filter(Boolean),S=!0;continue}let K=g.match(ft);if(K){n.epic=K[1].trim(),S=!0;continue}let Y=g.match(dt);if(Y){n.assignee=Y[1].trim(),S=!0;continue}let H=g.match(lt);if(H){n.updatedAt=H[1].trim(),S=!0;continue}}if(S)continue;if(l.trim()===""){d=!1;continue}d=!1,o.push(l)}else ct.test(l)?y=!0:y?f.push(l):o.push(l)}return u(),{tasks:s,warnings:i}}function X(r,t){let e=r.split(`
|
|
6
|
+
`);for(let s=0;s<e.length;s++){let i=e[s].match($);if(i&&i[1]===t)return s+1}return 1}function O(r){let t=_(r),e=0;for(let s of t.split(`
|
|
7
|
+
`))$.test(s)&&e++;return e}function V(r,t){let e=_(r),s=new RegExp(`^## ${t}-(\\d+):`),i=0;for(let n of e.split(`
|
|
8
|
+
`)){let a=n.match(s);if(a){let o=parseInt(a[1],10);o>i&&(i=o)}}return i}function j(r){let t=[];t.push(`## ${r.id}: ${r.title}`);let e=[`**Priority:** ${r.priority}`];return r.tags.length>0&&e.push(`**Tags:** ${r.tags.join(", ")}`),r.epic&&e.push(`**Epic:** ${r.epic}`),r.assignee&&e.push(`**Assignee:** ${r.assignee}`),t.push(e.join(" | ")),r.updatedAt&&t.push(`**Updated:** ${r.updatedAt}`),r.description.trim()&&(t.push(""),t.push(r.description.trim())),r.plan?.trim()&&(t.push(""),t.push("### Plan"),t.push(""),t.push(r.plan.trim())),t.join(`
|
|
9
|
+
`)}function x(r,t){let e=[`# ${r}`,""];if(t.length===0)return e.join(`
|
|
10
|
+
`);for(let s=0;s<t.length;s++)e.push(j(t[s])),e.push(""),e.push("---"),e.push("");return e.join(`
|
|
11
|
+
`)}var T=class{constructor(t){this.configManager=t}configManager;next(){return this.configManager.getNextId()}parseId(t){let e=t.match(/^([A-Z]+)-(\d+)$/);return e?{prefix:e[1],number:parseInt(e[2],10)}:null}};var c=v(require("fs")),F=require("fs"),m=v(require("path"));var Z=`# Work Log
|
|
12
|
+
|
|
13
|
+
Top-level trace of completed work and key decisions. One entry per task moved to Done \u2014 newest at top. Keep entries short (3\u20135 lines); detailed steps stay in each task's \`### Plan\` in \`DONE.md\`.
|
|
14
|
+
|
|
15
|
+
**Entry template** (insert after this header, before existing entries):
|
|
16
|
+
|
|
17
|
+
\`\`\`markdown
|
|
18
|
+
## TASK-### \u2014 YYYY-MM-DD
|
|
19
|
+
**What:** One-line summary of what was delivered.
|
|
20
|
+
**Decisions:** Key choices made and why (skip if none).
|
|
21
|
+
**Outcome:** Result or follow-ups (skip if obvious from What).
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
\`\`\`
|
|
25
|
+
`;var M=class{constructor(t){this.tasksDir=t}tasksDir;readState(t){let e=m.join(this.tasksDir,t.fileName);if(!c.existsSync(e))return{tasks:[],warnings:[]};let s=c.readFileSync(e,"utf-8");return w(s)}writeState(t,e){let s=m.join(this.tasksDir,t.fileName),i=x(t.name,e);c.writeFileSync(s,i,"utf-8")}readAllStates(t){let e=new Map;for(let s of t.states)e.set(s.name,this.readState(s));return e}async readStateAsync(t){let e=m.join(this.tasksDir,t.fileName);try{let s=await F.promises.readFile(e,"utf-8");return w(s)}catch(s){if(s.code==="ENOENT")return{tasks:[],warnings:[]};throw s}}async readRawContentAsync(t){let e=m.join(this.tasksDir,t.fileName);try{return await F.promises.readFile(e,"utf-8")}catch(s){if(s.code==="ENOENT")return"";throw s}}async readAllStatesAsync(t){let e=new Map;for(let s of t.states)e.set(s.name,await this.readStateAsync(s));return e}ensureDirectory(){c.existsSync(this.tasksDir)||c.mkdirSync(this.tasksDir,{recursive:!0})}initializeStateFiles(t){this.ensureDirectory();for(let s of t.states){let i=m.join(this.tasksDir,s.fileName);if(!c.existsSync(i)){let n=x(s.name,[]);c.writeFileSync(i,n,"utf-8")}}let e=m.join(this.tasksDir,"WORK_LOG.md");c.existsSync(e)||c.writeFileSync(e,Z,"utf-8")}getStateFilePath(t){return m.join(this.tasksDir,t.fileName)}readRawContent(t){let e=m.join(this.tasksDir,t.fileName);return c.existsSync(e)?c.readFileSync(e,"utf-8"):""}};function L(){return new Date().toISOString().replace("T"," ").slice(0,16)}var ht=new Set(["Done","Rejected"]);function B(r){return ht.has(r)}var C=class r{constructor(t,e){this.configManager=t;this.fileStore=e,this.idGenerator=new T(t)}configManager;tasksByState=new Map;parseWarningsByFile=new Map;deferredUnloadedStates=new Set;deferredSectionCounts=new Map;listeners=[];fileStore;idGenerator;get config(){return this.configManager.get()}reload(){this.reloadSync()}resetReloadState(){this.tasksByState=new Map,this.parseWarningsByFile=new Map,this.deferredUnloadedStates.clear(),this.deferredSectionCounts.clear()}applyDeferredState(t,e){this.deferredSectionCounts.set(t.name,O(e)),this.deferredUnloadedStates.add(t.name),this.tasksByState.set(t.name,[])}applyParsedState(t,e){this.tasksByState.set(t.name,e.tasks),e.warnings.length>0&&this.parseWarningsByFile.set(t.fileName,e.warnings)}reloadSync(){this.resetReloadState();for(let t of this.config.states)B(t.name)?this.applyDeferredState(t,this.fileStore.readRawContent(t)):this.applyParsedState(t,this.fileStore.readState(t));this.notifyListeners()}async reloadAsync(){this.resetReloadState();for(let t of this.config.states)B(t.name)?this.applyDeferredState(t,await this.fileStore.readRawContentAsync(t)):this.applyParsedState(t,await this.fileStore.readStateAsync(t));this.notifyListeners()}parseStateIntoStore(t){let e=this.findState(t);if(!e)return;let s=this.fileStore.readState(e);this.tasksByState.set(t,s.tasks),this.deferredUnloadedStates.delete(t),this.deferredSectionCounts.set(t,s.tasks.length),this.parseWarningsByFile.delete(e.fileName),s.warnings.length>0&&this.parseWarningsByFile.set(e.fileName,s.warnings)}reloadState(t){this.findState(t)&&(this.parseStateIntoStore(t),this.notifyListeners())}ensureStateLoaded(t){this.deferredUnloadedStates.has(t)&&(this.parseStateIntoStore(t),this.notifyListeners())}ensureAllDeferredStatesLoaded(){let t=[...this.deferredUnloadedStates];if(t.length!==0){for(let e of t)this.parseStateIntoStore(e);this.notifyListeners()}}getStateDisplayCounts(){let t=new Map;for(let e of this.config.states){let s=this.tasksByState.get(e.name)??[];this.deferredUnloadedStates.has(e.name)?t.set(e.name,this.deferredSectionCounts.get(e.name)??0):t.set(e.name,s.length)}return t}isStateDeferredUnloaded(t){return this.deferredUnloadedStates.has(t)}getWarnings(){return[...this.parseWarningsByFile.entries()].filter(([,t])=>t.length>0).map(([t,e])=>({fileName:t,warnings:e}))}getTasksByState(t){return this.tasksByState.get(t)??[]}getAllTasks(){return new Map(this.tasksByState)}getMaxTaskIdNumber(){let t=this.config.idPrefix,e=0;for(let[s,i]of this.tasksByState){if(this.deferredUnloadedStates.has(s)){let n=this.findState(s);if(n){let a=this.fileStore.readRawContent(n),o=V(a,t);o>e&&(e=o)}continue}for(let n of i){let a=this.idGenerator.parseId(n.id);a&&a.prefix===t&&a.number>e&&(e=a.number)}}return e}findInMemory(t){for(let[e,s]of this.tasksByState){let i=s.find(n=>n.id===t);if(i)return{task:i,stateName:e}}return null}findTask(t){let e=this.findInMemory(t);if(e)return e;let s=[...this.deferredUnloadedStates];if(s.length===0)return null;for(let i of s)if(this.parseStateIntoStore(i),e=this.findInMemory(t),e)return this.notifyListeners(),e;return this.notifyListeners(),null}createTask(t,e){let s=this.findState(e);if(!s)throw new Error(`Unknown state: ${e}`);this.ensureStateLoaded(e),this.configManager.reloadFromDisk(),this.configManager.reconcileNextId(this.getMaxTaskIdNumber()+1);let i=this.idGenerator.next(),n={...t,id:i,updatedAt:L()},a=this.getTasksByState(e);return this.config.insertPosition==="top"?a.unshift(n):a.push(n),this.tasksByState.set(e,a),this.fileStore.writeState(s,a),this.notifyListeners(),n}moveTask(t,e,s){let i=this.findTask(t);if(!i)return null;this.ensureStateLoaded(e);let n=this.findState(i.stateName),a=this.findState(e);if(!n||!a)return null;if(s!==void 0&&i.stateName===e)return this.reorderTaskToIndex(t,s)?i.task:null;let o=this.getTasksByState(i.stateName).filter(d=>d.id!==t);this.tasksByState.set(i.stateName,o),this.fileStore.writeState(n,o),i.task.updatedAt=L();let f=[...this.getTasksByState(e)].filter(d=>d.id!==t);if(s!==void 0){let d=Math.max(0,Math.min(s,f.length));f.splice(d,0,i.task)}else this.config.insertPosition==="top"?f.unshift(i.task):f.push(i.task);return this.tasksByState.set(e,f),this.fileStore.writeState(a,f),this.notifyListeners(),i.task}deleteTask(t){let e=this.findTask(t);if(!e)return!1;let s=this.findState(e.stateName);if(!s)return!1;let i=this.getTasksByState(e.stateName).filter(n=>n.id!==t);return this.tasksByState.set(e.stateName,i),this.fileStore.writeState(s,i),this.notifyListeners(),!0}updateTask(t,e){let s=this.findTask(t);if(!s)return null;let i=this.findState(s.stateName);if(!i)return null;if(!r.hasChanges(s.task,e))return s.task;let n={...s.task,...e,id:t,updatedAt:L()},a=this.getTasksByState(s.stateName).map(o=>o.id===t?n:o);return this.tasksByState.set(s.stateName,a),this.fileStore.writeState(i,a),this.notifyListeners(),n}static hasChanges(t,e){for(let s of Object.keys(e)){let i=t[s],n=e[s];if(Array.isArray(i)&&Array.isArray(n)){if(i.length!==n.length||i.some((a,o)=>a!==n[o]))return!0}else if(i!==n)return!0}return!1}reorderTaskToIndex(t,e){let s=this.findTask(t);if(!s)return!1;let i=this.findState(s.stateName);if(!i)return!1;let n=[...this.getTasksByState(s.stateName)],a=n.findIndex(d=>d.id===t);if(a===-1)return!1;let o=Math.max(0,Math.min(e,n.length-1));if(a===o)return!0;let[f]=n.splice(a,1);return n.splice(o,0,f),this.tasksByState.set(s.stateName,n),this.fileStore.writeState(i,n),this.notifyListeners(),!0}reorderTask(t,e){let s=this.findTask(t);if(!s)return!1;let i=this.findState(s.stateName);if(!i)return!1;let n=[...this.getTasksByState(s.stateName)],a=n.findIndex(f=>f.id===t);if(a===-1)return!1;let o=e==="up"?a-1:a+1;return o<0||o>=n.length?!1:([n[a],n[o]]=[n[o],n[a]],this.tasksByState.set(s.stateName,n),this.fileStore.writeState(i,n),this.notifyListeners(),!0)}onDidChange(t){return this.listeners.push(t),{dispose:()=>{let e=this.listeners.indexOf(t);e>=0&&this.listeners.splice(e,1)}}}fixDuplicates(t){let e=new Set;for(let a of t){e.add(a.keep.stateName);for(let o of a.remove)e.add(o.stateName)}let s=!1;for(let a of e)this.deferredUnloadedStates.has(a)&&(this.parseStateIntoStore(a),s=!0);let i=0,n=new Map;for(let a of t)for(let o of a.remove){let f=n.get(o.stateName)??new Set;f.add(o.index),n.set(o.stateName,f)}for(let[a,o]of n){let f=this.findState(a);if(!f)continue;let d=[...this.getTasksByState(a)],y=[...o].sort((u,p)=>p-u);for(let u of y)u>=0&&u<d.length&&(d.splice(u,1),i++);this.tasksByState.set(a,d),this.fileStore.writeState(f,d)}return(i>0||s)&&this.notifyListeners(),i}findState(t){return this.config.states.find(e=>e.name===t)}notifyListeners(){for(let t of this.listeners)t()}};0&&(module.exports={ConfigManager,DEFAULT_STATES,FileStore,IdGenerator,Priority,TaskStore,countTaskHeadings,createDefaultConfig,findTaskLineNumber,isDeferredStateName,isPriority,parseTasks,serializeStateFile,serializeTask});
|