clawvault 2.2.1 → 2.3.1
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/bin/clawvault.js +12 -0
- package/bin/register-tailscale-commands.js +106 -0
- package/bin/register-task-commands.js +257 -0
- package/dist/{chunk-2HM7ZI4X.js → chunk-2AYPFUGX.js} +1 -1
- package/dist/chunk-4GBPTBFJ.js +628 -0
- package/dist/{chunk-LB6P4CD5.js → chunk-6AQZIPLV.js} +7 -7
- package/dist/chunk-CLE2HHNT.js +513 -0
- package/dist/{chunk-VR5NE7PZ.js → chunk-HVTTYDCJ.js} +1 -1
- package/dist/{chunk-GQVYQCY5.js → chunk-JVAWKNIZ.js} +2 -2
- package/dist/chunk-MDIH26GC.js +183 -0
- package/dist/{chunk-Z2XBWN7A.js → chunk-NAMFB7ZA.js} +2 -0
- package/dist/chunk-NGVAEFT2.js +352 -0
- package/dist/chunk-NZ4ZZNSR.js +373 -0
- package/dist/{chunk-MQUJNOHK.js → chunk-QALB2V3E.js} +1 -1
- package/dist/{chunk-L6NB43WV.js → chunk-RARDNTUP.js} +4 -4
- package/dist/{chunk-I5X6J4FX.js → chunk-TB3BM2PQ.js} +6 -6
- package/dist/{chunk-GJEGPO7U.js → chunk-TT3FXYCN.js} +1 -1
- package/dist/{chunk-73P7XCQM.js → chunk-USZU5CBB.js} +5 -5
- package/dist/{chunk-WZI3OAE5.js → chunk-VBVEXNI5.js} +4 -4
- package/dist/commands/archive.js +3 -3
- package/dist/commands/backlog.d.ts +53 -0
- package/dist/commands/backlog.js +119 -0
- package/dist/commands/blocked.d.ts +25 -0
- package/dist/commands/blocked.js +43 -0
- package/dist/commands/canvas.d.ts +20 -0
- package/dist/commands/canvas.js +683 -0
- package/dist/commands/context.js +3 -3
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/graph.js +2 -2
- package/dist/commands/link.js +3 -3
- package/dist/commands/migrate-observations.js +3 -3
- package/dist/commands/observe.js +5 -5
- package/dist/commands/rebuild.js +4 -4
- package/dist/commands/recover.js +2 -2
- package/dist/commands/reflect.js +5 -5
- package/dist/commands/replay.js +6 -6
- package/dist/commands/sleep.js +7 -7
- package/dist/commands/status.js +1 -1
- package/dist/commands/tailscale.d.ts +52 -0
- package/dist/commands/tailscale.js +25 -0
- package/dist/commands/task.d.ts +71 -0
- package/dist/commands/task.js +189 -0
- package/dist/commands/wake.js +8 -8
- package/dist/index.d.ts +4 -0
- package/dist/index.js +104 -42
- package/dist/lib/canvas-layout.d.ts +115 -0
- package/dist/lib/canvas-layout.js +34 -0
- package/dist/lib/tailscale.d.ts +225 -0
- package/dist/lib/tailscale.js +49 -0
- package/dist/lib/task-utils.d.ts +159 -0
- package/dist/lib/task-utils.js +46 -0
- package/dist/lib/webdav.d.ts +109 -0
- package/dist/lib/webdav.js +34 -0
- package/package.json +2 -2
- package/dist/{chunk-MILVYUPK.js → chunk-IWYZAXKJ.js} +3 -3
- package/dist/{chunk-H7JW4L7H.js → chunk-OZ7RIXTO.js} +3 -3
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// src/lib/canvas-layout.ts
|
|
2
|
+
import * as crypto from "crypto";
|
|
3
|
+
var CANVAS_COLORS = {
|
|
4
|
+
RED: "1",
|
|
5
|
+
// Critical, blocked
|
|
6
|
+
ORANGE: "2",
|
|
7
|
+
// High priority
|
|
8
|
+
YELLOW: "3",
|
|
9
|
+
// Medium priority
|
|
10
|
+
GREEN: "4",
|
|
11
|
+
// Done, success
|
|
12
|
+
CYAN: "5",
|
|
13
|
+
// Stats
|
|
14
|
+
PURPLE: "6"
|
|
15
|
+
// Knowledge graph
|
|
16
|
+
};
|
|
17
|
+
var LAYOUT = {
|
|
18
|
+
LEFT_COLUMN_X: 0,
|
|
19
|
+
LEFT_COLUMN_WIDTH: 500,
|
|
20
|
+
RIGHT_COLUMN_X: 550,
|
|
21
|
+
RIGHT_COLUMN_WIDTH: 450,
|
|
22
|
+
GROUP_PADDING: 20,
|
|
23
|
+
NODE_SPACING: 15,
|
|
24
|
+
GROUP_SPACING: 50,
|
|
25
|
+
DEFAULT_NODE_WIDTH: 280,
|
|
26
|
+
DEFAULT_NODE_HEIGHT: 80,
|
|
27
|
+
FILE_NODE_HEIGHT: 60,
|
|
28
|
+
SMALL_NODE_HEIGHT: 50,
|
|
29
|
+
GROUP_HEADER_HEIGHT: 40
|
|
30
|
+
};
|
|
31
|
+
function generateId() {
|
|
32
|
+
return crypto.randomBytes(8).toString("hex");
|
|
33
|
+
}
|
|
34
|
+
function createTextNode(x, y, width, height, text, color) {
|
|
35
|
+
const node = {
|
|
36
|
+
id: generateId(),
|
|
37
|
+
type: "text",
|
|
38
|
+
x,
|
|
39
|
+
y,
|
|
40
|
+
width,
|
|
41
|
+
height,
|
|
42
|
+
text
|
|
43
|
+
};
|
|
44
|
+
if (color) node.color = color;
|
|
45
|
+
return node;
|
|
46
|
+
}
|
|
47
|
+
function createFileNode(x, y, width, height, file, color) {
|
|
48
|
+
const node = {
|
|
49
|
+
id: generateId(),
|
|
50
|
+
type: "file",
|
|
51
|
+
x,
|
|
52
|
+
y,
|
|
53
|
+
width,
|
|
54
|
+
height,
|
|
55
|
+
file
|
|
56
|
+
};
|
|
57
|
+
if (color) node.color = color;
|
|
58
|
+
return node;
|
|
59
|
+
}
|
|
60
|
+
function createGroupNode(x, y, width, height, label, color) {
|
|
61
|
+
const node = {
|
|
62
|
+
id: generateId(),
|
|
63
|
+
type: "group",
|
|
64
|
+
x,
|
|
65
|
+
y,
|
|
66
|
+
width,
|
|
67
|
+
height,
|
|
68
|
+
label
|
|
69
|
+
};
|
|
70
|
+
if (color) node.color = color;
|
|
71
|
+
return node;
|
|
72
|
+
}
|
|
73
|
+
function createEdge(fromNode, fromSide, toNode, toSide, label, color) {
|
|
74
|
+
const edge = {
|
|
75
|
+
id: generateId(),
|
|
76
|
+
fromNode,
|
|
77
|
+
fromSide,
|
|
78
|
+
toNode,
|
|
79
|
+
toSide
|
|
80
|
+
};
|
|
81
|
+
if (label) edge.label = label;
|
|
82
|
+
if (color) edge.color = color;
|
|
83
|
+
return edge;
|
|
84
|
+
}
|
|
85
|
+
function stackNodesVertically(nodes, startX, startY, spacing = LAYOUT.NODE_SPACING) {
|
|
86
|
+
let currentY = startY;
|
|
87
|
+
const positionedNodes = [];
|
|
88
|
+
for (const node of nodes) {
|
|
89
|
+
positionedNodes.push({
|
|
90
|
+
...node,
|
|
91
|
+
x: startX,
|
|
92
|
+
y: currentY
|
|
93
|
+
});
|
|
94
|
+
currentY += node.height + spacing;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
nodes: positionedNodes,
|
|
98
|
+
totalHeight: currentY - startY - spacing
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function createGroupWithNodes(groupX, groupY, groupWidth, label, childNodes, color) {
|
|
102
|
+
const padding = LAYOUT.GROUP_PADDING;
|
|
103
|
+
const headerHeight = LAYOUT.GROUP_HEADER_HEIGHT;
|
|
104
|
+
const stacked = stackNodesVertically(
|
|
105
|
+
childNodes,
|
|
106
|
+
groupX + padding,
|
|
107
|
+
groupY + headerHeight + padding
|
|
108
|
+
);
|
|
109
|
+
const groupHeight = headerHeight + padding * 2 + stacked.totalHeight + LAYOUT.NODE_SPACING;
|
|
110
|
+
const group = createGroupNode(groupX, groupY, groupWidth, groupHeight, label, color);
|
|
111
|
+
return {
|
|
112
|
+
group,
|
|
113
|
+
nodes: stacked.nodes
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function getPriorityColor(priority) {
|
|
117
|
+
switch (priority) {
|
|
118
|
+
case "critical":
|
|
119
|
+
return CANVAS_COLORS.RED;
|
|
120
|
+
case "high":
|
|
121
|
+
return CANVAS_COLORS.ORANGE;
|
|
122
|
+
case "medium":
|
|
123
|
+
return CANVAS_COLORS.YELLOW;
|
|
124
|
+
default:
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function truncateText(text, maxChars) {
|
|
129
|
+
if (text.length <= maxChars) return text;
|
|
130
|
+
return text.slice(0, maxChars - 3) + "...";
|
|
131
|
+
}
|
|
132
|
+
function formatCanvasText(lines) {
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
function calculateColumnHeight(groups) {
|
|
136
|
+
let height = 0;
|
|
137
|
+
for (let i = 0; i < groups.length; i++) {
|
|
138
|
+
height += groups[i].group.height;
|
|
139
|
+
if (i < groups.length - 1) {
|
|
140
|
+
height += LAYOUT.GROUP_SPACING;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return height;
|
|
144
|
+
}
|
|
145
|
+
function positionGroupsVertically(groups, startY = 0) {
|
|
146
|
+
let currentY = startY;
|
|
147
|
+
const positioned = [];
|
|
148
|
+
for (const { group, nodes } of groups) {
|
|
149
|
+
const yOffset = currentY - group.y;
|
|
150
|
+
positioned.push({
|
|
151
|
+
group: { ...group, y: currentY },
|
|
152
|
+
nodes: nodes.map((n) => ({ ...n, y: n.y + yOffset }))
|
|
153
|
+
});
|
|
154
|
+
currentY += group.height + LAYOUT.GROUP_SPACING;
|
|
155
|
+
}
|
|
156
|
+
return positioned;
|
|
157
|
+
}
|
|
158
|
+
function flattenGroups(groups) {
|
|
159
|
+
const nodes = [];
|
|
160
|
+
for (const { group, nodes: childNodes } of groups) {
|
|
161
|
+
nodes.push(group);
|
|
162
|
+
nodes.push(...childNodes);
|
|
163
|
+
}
|
|
164
|
+
return nodes;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export {
|
|
168
|
+
CANVAS_COLORS,
|
|
169
|
+
LAYOUT,
|
|
170
|
+
generateId,
|
|
171
|
+
createTextNode,
|
|
172
|
+
createFileNode,
|
|
173
|
+
createGroupNode,
|
|
174
|
+
createEdge,
|
|
175
|
+
stackNodesVertically,
|
|
176
|
+
createGroupWithNodes,
|
|
177
|
+
getPriorityColor,
|
|
178
|
+
truncateText,
|
|
179
|
+
formatCanvasText,
|
|
180
|
+
calculateColumnHeight,
|
|
181
|
+
positionGroupsVertically,
|
|
182
|
+
flattenGroups
|
|
183
|
+
};
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// src/lib/task-utils.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import matter from "gray-matter";
|
|
5
|
+
function slugify(text) {
|
|
6
|
+
return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").trim();
|
|
7
|
+
}
|
|
8
|
+
function getTasksDir(vaultPath) {
|
|
9
|
+
return path.join(path.resolve(vaultPath), "tasks");
|
|
10
|
+
}
|
|
11
|
+
function getBacklogDir(vaultPath) {
|
|
12
|
+
return path.join(path.resolve(vaultPath), "backlog");
|
|
13
|
+
}
|
|
14
|
+
function ensureTasksDir(vaultPath) {
|
|
15
|
+
const tasksDir = getTasksDir(vaultPath);
|
|
16
|
+
if (!fs.existsSync(tasksDir)) {
|
|
17
|
+
fs.mkdirSync(tasksDir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function ensureBacklogDir(vaultPath) {
|
|
21
|
+
const backlogDir = getBacklogDir(vaultPath);
|
|
22
|
+
if (!fs.existsSync(backlogDir)) {
|
|
23
|
+
fs.mkdirSync(backlogDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function getTaskPath(vaultPath, slug) {
|
|
27
|
+
return path.join(getTasksDir(vaultPath), `${slug}.md`);
|
|
28
|
+
}
|
|
29
|
+
function getBacklogPath(vaultPath, slug) {
|
|
30
|
+
return path.join(getBacklogDir(vaultPath), `${slug}.md`);
|
|
31
|
+
}
|
|
32
|
+
function extractTitle(content) {
|
|
33
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
34
|
+
return match ? match[1].trim() : "";
|
|
35
|
+
}
|
|
36
|
+
function readTask(vaultPath, slug) {
|
|
37
|
+
const taskPath = getTaskPath(vaultPath, slug);
|
|
38
|
+
if (!fs.existsSync(taskPath)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const raw = fs.readFileSync(taskPath, "utf-8");
|
|
43
|
+
const { data, content } = matter(raw);
|
|
44
|
+
const title = extractTitle(content) || slug;
|
|
45
|
+
return {
|
|
46
|
+
slug,
|
|
47
|
+
title,
|
|
48
|
+
content,
|
|
49
|
+
frontmatter: data,
|
|
50
|
+
path: taskPath
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function readBacklogItem(vaultPath, slug) {
|
|
57
|
+
const backlogPath = getBacklogPath(vaultPath, slug);
|
|
58
|
+
if (!fs.existsSync(backlogPath)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const raw = fs.readFileSync(backlogPath, "utf-8");
|
|
63
|
+
const { data, content } = matter(raw);
|
|
64
|
+
const title = extractTitle(content) || slug;
|
|
65
|
+
return {
|
|
66
|
+
slug,
|
|
67
|
+
title,
|
|
68
|
+
content,
|
|
69
|
+
frontmatter: data,
|
|
70
|
+
path: backlogPath
|
|
71
|
+
};
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function listTasks(vaultPath, filters) {
|
|
77
|
+
const tasksDir = getTasksDir(vaultPath);
|
|
78
|
+
if (!fs.existsSync(tasksDir)) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const tasks = [];
|
|
82
|
+
const entries = fs.readdirSync(tasksDir, { withFileTypes: true });
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const slug = entry.name.replace(/\.md$/, "");
|
|
88
|
+
const task = readTask(vaultPath, slug);
|
|
89
|
+
if (!task) continue;
|
|
90
|
+
if (filters) {
|
|
91
|
+
if (filters.status && task.frontmatter.status !== filters.status) continue;
|
|
92
|
+
if (filters.owner && task.frontmatter.owner !== filters.owner) continue;
|
|
93
|
+
if (filters.project && task.frontmatter.project !== filters.project) continue;
|
|
94
|
+
if (filters.priority && task.frontmatter.priority !== filters.priority) continue;
|
|
95
|
+
}
|
|
96
|
+
tasks.push(task);
|
|
97
|
+
}
|
|
98
|
+
const priorityOrder = {
|
|
99
|
+
critical: 0,
|
|
100
|
+
high: 1,
|
|
101
|
+
medium: 2,
|
|
102
|
+
low: 3
|
|
103
|
+
};
|
|
104
|
+
return tasks.sort((a, b) => {
|
|
105
|
+
const aPriority = priorityOrder[a.frontmatter.priority || "low"];
|
|
106
|
+
const bPriority = priorityOrder[b.frontmatter.priority || "low"];
|
|
107
|
+
if (aPriority !== bPriority) {
|
|
108
|
+
return aPriority - bPriority;
|
|
109
|
+
}
|
|
110
|
+
return new Date(b.frontmatter.created).getTime() - new Date(a.frontmatter.created).getTime();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function listBacklogItems(vaultPath, filters) {
|
|
114
|
+
const backlogDir = getBacklogDir(vaultPath);
|
|
115
|
+
if (!fs.existsSync(backlogDir)) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const items = [];
|
|
119
|
+
const entries = fs.readdirSync(backlogDir, { withFileTypes: true });
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const slug = entry.name.replace(/\.md$/, "");
|
|
125
|
+
const item = readBacklogItem(vaultPath, slug);
|
|
126
|
+
if (!item) continue;
|
|
127
|
+
if (filters) {
|
|
128
|
+
if (filters.project && item.frontmatter.project !== filters.project) continue;
|
|
129
|
+
if (filters.source && item.frontmatter.source !== filters.source) continue;
|
|
130
|
+
}
|
|
131
|
+
items.push(item);
|
|
132
|
+
}
|
|
133
|
+
return items.sort((a, b) => {
|
|
134
|
+
return new Date(b.frontmatter.created).getTime() - new Date(a.frontmatter.created).getTime();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function createTask(vaultPath, title, options = {}) {
|
|
138
|
+
ensureTasksDir(vaultPath);
|
|
139
|
+
const slug = slugify(title);
|
|
140
|
+
const taskPath = getTaskPath(vaultPath, slug);
|
|
141
|
+
if (fs.existsSync(taskPath)) {
|
|
142
|
+
throw new Error(`Task already exists: ${slug}`);
|
|
143
|
+
}
|
|
144
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
145
|
+
const frontmatter = {
|
|
146
|
+
status: "open",
|
|
147
|
+
created: now,
|
|
148
|
+
updated: now
|
|
149
|
+
};
|
|
150
|
+
if (options.owner) frontmatter.owner = options.owner;
|
|
151
|
+
if (options.project) frontmatter.project = options.project;
|
|
152
|
+
if (options.priority) frontmatter.priority = options.priority;
|
|
153
|
+
if (options.due) frontmatter.due = options.due;
|
|
154
|
+
if (options.tags && options.tags.length > 0) frontmatter.tags = options.tags;
|
|
155
|
+
let content = `# ${title}
|
|
156
|
+
`;
|
|
157
|
+
const links = [];
|
|
158
|
+
if (options.owner) links.push(`[[${options.owner}]]`);
|
|
159
|
+
if (options.project) links.push(`[[${options.project}]]`);
|
|
160
|
+
if (links.length > 0) {
|
|
161
|
+
content += `
|
|
162
|
+
${links.join(" | ")}
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
if (options.content) {
|
|
166
|
+
content += `
|
|
167
|
+
${options.content}
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
const fileContent = matter.stringify(content, frontmatter);
|
|
171
|
+
fs.writeFileSync(taskPath, fileContent);
|
|
172
|
+
return {
|
|
173
|
+
slug,
|
|
174
|
+
title,
|
|
175
|
+
content,
|
|
176
|
+
frontmatter,
|
|
177
|
+
path: taskPath
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function updateTask(vaultPath, slug, updates) {
|
|
181
|
+
const task = readTask(vaultPath, slug);
|
|
182
|
+
if (!task) {
|
|
183
|
+
throw new Error(`Task not found: ${slug}`);
|
|
184
|
+
}
|
|
185
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
186
|
+
const newFrontmatter = {
|
|
187
|
+
...task.frontmatter,
|
|
188
|
+
updated: now
|
|
189
|
+
};
|
|
190
|
+
if (updates.status !== void 0) newFrontmatter.status = updates.status;
|
|
191
|
+
if (updates.owner !== void 0) newFrontmatter.owner = updates.owner;
|
|
192
|
+
if (updates.project !== void 0) newFrontmatter.project = updates.project;
|
|
193
|
+
if (updates.priority !== void 0) newFrontmatter.priority = updates.priority;
|
|
194
|
+
if (updates.due !== void 0) newFrontmatter.due = updates.due;
|
|
195
|
+
if (updates.tags !== void 0) newFrontmatter.tags = updates.tags;
|
|
196
|
+
if (updates.blocked_by !== void 0) {
|
|
197
|
+
newFrontmatter.blocked_by = updates.blocked_by;
|
|
198
|
+
} else if (updates.status && updates.status !== "blocked") {
|
|
199
|
+
delete newFrontmatter.blocked_by;
|
|
200
|
+
}
|
|
201
|
+
const fileContent = matter.stringify(task.content, newFrontmatter);
|
|
202
|
+
fs.writeFileSync(task.path, fileContent);
|
|
203
|
+
return {
|
|
204
|
+
...task,
|
|
205
|
+
frontmatter: newFrontmatter
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function completeTask(vaultPath, slug) {
|
|
209
|
+
const task = readTask(vaultPath, slug);
|
|
210
|
+
if (!task) {
|
|
211
|
+
throw new Error(`Task not found: ${slug}`);
|
|
212
|
+
}
|
|
213
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
214
|
+
const newFrontmatter = {
|
|
215
|
+
...task.frontmatter,
|
|
216
|
+
status: "done",
|
|
217
|
+
updated: now,
|
|
218
|
+
completed: now
|
|
219
|
+
};
|
|
220
|
+
delete newFrontmatter.blocked_by;
|
|
221
|
+
const fileContent = matter.stringify(task.content, newFrontmatter);
|
|
222
|
+
fs.writeFileSync(task.path, fileContent);
|
|
223
|
+
return {
|
|
224
|
+
...task,
|
|
225
|
+
frontmatter: newFrontmatter
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function createBacklogItem(vaultPath, title, options = {}) {
|
|
229
|
+
ensureBacklogDir(vaultPath);
|
|
230
|
+
const slug = slugify(title);
|
|
231
|
+
const backlogPath = getBacklogPath(vaultPath, slug);
|
|
232
|
+
if (fs.existsSync(backlogPath)) {
|
|
233
|
+
throw new Error(`Backlog item already exists: ${slug}`);
|
|
234
|
+
}
|
|
235
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
236
|
+
const frontmatter = {
|
|
237
|
+
created: now
|
|
238
|
+
};
|
|
239
|
+
if (options.source) frontmatter.source = options.source;
|
|
240
|
+
if (options.project) frontmatter.project = options.project;
|
|
241
|
+
if (options.tags && options.tags.length > 0) frontmatter.tags = options.tags;
|
|
242
|
+
let content = `# ${title}
|
|
243
|
+
`;
|
|
244
|
+
const links = [];
|
|
245
|
+
if (options.source) links.push(`[[${options.source}]]`);
|
|
246
|
+
if (options.project) links.push(`[[${options.project}]]`);
|
|
247
|
+
if (links.length > 0) {
|
|
248
|
+
content += `
|
|
249
|
+
${links.join(" | ")}
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
if (options.content) {
|
|
253
|
+
content += `
|
|
254
|
+
${options.content}
|
|
255
|
+
`;
|
|
256
|
+
}
|
|
257
|
+
const fileContent = matter.stringify(content, frontmatter);
|
|
258
|
+
fs.writeFileSync(backlogPath, fileContent);
|
|
259
|
+
return {
|
|
260
|
+
slug,
|
|
261
|
+
title,
|
|
262
|
+
content,
|
|
263
|
+
frontmatter,
|
|
264
|
+
path: backlogPath
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function promoteBacklogItem(vaultPath, slug, options = {}) {
|
|
268
|
+
const backlogItem = readBacklogItem(vaultPath, slug);
|
|
269
|
+
if (!backlogItem) {
|
|
270
|
+
throw new Error(`Backlog item not found: ${slug}`);
|
|
271
|
+
}
|
|
272
|
+
const task = createTask(vaultPath, backlogItem.title, {
|
|
273
|
+
owner: options.owner,
|
|
274
|
+
project: backlogItem.frontmatter.project,
|
|
275
|
+
priority: options.priority,
|
|
276
|
+
due: options.due,
|
|
277
|
+
content: backlogItem.content.replace(/^#\s+.+\n/, "").trim(),
|
|
278
|
+
// Remove title from content
|
|
279
|
+
tags: backlogItem.frontmatter.tags
|
|
280
|
+
});
|
|
281
|
+
fs.unlinkSync(backlogItem.path);
|
|
282
|
+
return task;
|
|
283
|
+
}
|
|
284
|
+
function getBlockedTasks(vaultPath, project) {
|
|
285
|
+
const filters = { status: "blocked" };
|
|
286
|
+
if (project) filters.project = project;
|
|
287
|
+
return listTasks(vaultPath, filters);
|
|
288
|
+
}
|
|
289
|
+
function getActiveTasks(vaultPath, filters) {
|
|
290
|
+
const allTasks = listTasks(vaultPath, filters);
|
|
291
|
+
return allTasks.filter((t) => t.frontmatter.status === "open" || t.frontmatter.status === "in-progress");
|
|
292
|
+
}
|
|
293
|
+
function getRecentlyCompletedTasks(vaultPath, limit = 10) {
|
|
294
|
+
const allTasks = listTasks(vaultPath, { status: "done" });
|
|
295
|
+
return allTasks.filter((t) => t.frontmatter.completed).sort((a, b) => {
|
|
296
|
+
const aCompleted = new Date(a.frontmatter.completed || 0).getTime();
|
|
297
|
+
const bCompleted = new Date(b.frontmatter.completed || 0).getTime();
|
|
298
|
+
return bCompleted - aCompleted;
|
|
299
|
+
}).slice(0, limit);
|
|
300
|
+
}
|
|
301
|
+
function getStatusIcon(status) {
|
|
302
|
+
switch (status) {
|
|
303
|
+
case "in-progress":
|
|
304
|
+
return "\u25CF";
|
|
305
|
+
case "blocked":
|
|
306
|
+
return "\u25A0";
|
|
307
|
+
case "open":
|
|
308
|
+
return "\u25CB";
|
|
309
|
+
case "done":
|
|
310
|
+
return "\u2713";
|
|
311
|
+
default:
|
|
312
|
+
return "\u25CB";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function getStatusDisplay(status) {
|
|
316
|
+
switch (status) {
|
|
317
|
+
case "in-progress":
|
|
318
|
+
return "active";
|
|
319
|
+
case "blocked":
|
|
320
|
+
return "blocked";
|
|
321
|
+
case "open":
|
|
322
|
+
return "open";
|
|
323
|
+
case "done":
|
|
324
|
+
return "done";
|
|
325
|
+
default:
|
|
326
|
+
return status;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export {
|
|
331
|
+
slugify,
|
|
332
|
+
getTasksDir,
|
|
333
|
+
getBacklogDir,
|
|
334
|
+
ensureTasksDir,
|
|
335
|
+
ensureBacklogDir,
|
|
336
|
+
getTaskPath,
|
|
337
|
+
getBacklogPath,
|
|
338
|
+
readTask,
|
|
339
|
+
readBacklogItem,
|
|
340
|
+
listTasks,
|
|
341
|
+
listBacklogItems,
|
|
342
|
+
createTask,
|
|
343
|
+
updateTask,
|
|
344
|
+
completeTask,
|
|
345
|
+
createBacklogItem,
|
|
346
|
+
promoteBacklogItem,
|
|
347
|
+
getBlockedTasks,
|
|
348
|
+
getActiveTasks,
|
|
349
|
+
getRecentlyCompletedTasks,
|
|
350
|
+
getStatusIcon,
|
|
351
|
+
getStatusDisplay
|
|
352
|
+
};
|