@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 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,4 @@
1
+ #!/usr/bin/env node
2
+ // For `npx` and shell use. Programmatic consumers should spawn process.execPath with
3
+ // require.resolve('@smekai/taskplanner/mcp-server') instead — see README.
4
+ require('../dist/mcp-server.js');
@@ -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,4 @@
1
+ export interface CodexDeepLinkOptions {
2
+ planMode?: boolean;
3
+ }
4
+ export declare function buildCodexDeepLink(prompt: string, workspacePath: string, options?: CodexDeepLinkOptions): string;
@@ -0,0 +1 @@
1
+ export declare function shouldAutomateCursorPlanMode(aiPlanRequired: boolean, automationEnabled: boolean): boolean;
@@ -0,0 +1,3 @@
1
+ import { Task } from '../model/task.js';
2
+ import { TaskPlannerConfig } from '../model/config.js';
3
+ export declare function composeImplementationPrompt(task: Task, stateName: string, config: TaskPlannerConfig): string;
@@ -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
+ }
@@ -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});