clawvault 2.4.4 → 2.4.6

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 (51) hide show
  1. package/bin/clawvault.js +9 -0
  2. package/bin/command-registration.test.js +13 -1
  3. package/bin/help-contract.test.js +2 -0
  4. package/bin/register-config-commands.js +153 -0
  5. package/bin/register-config-route-commands.test.js +114 -0
  6. package/bin/register-kanban-commands.js +56 -0
  7. package/bin/register-kanban-commands.test.js +83 -0
  8. package/bin/register-resilience-commands.js +37 -2
  9. package/bin/register-resilience-commands.test.js +81 -0
  10. package/bin/register-route-commands.js +114 -0
  11. package/bin/register-task-commands.js +58 -7
  12. package/bin/register-task-commands.test.js +46 -0
  13. package/bin/test-helpers/cli-command-fixtures.js +20 -0
  14. package/dist/{chunk-XDCFXFGH.js → chunk-22WE3J4F.js} +1 -1
  15. package/dist/chunk-3PJIGGWV.js +49 -0
  16. package/dist/{chunk-FDJIZKCW.js → chunk-6B3JWM7J.js} +12 -48
  17. package/dist/{chunk-DEFBIVQ3.js → chunk-6QLRSPLZ.js} +170 -7
  18. package/dist/{chunk-MZZJLQNQ.js → chunk-F55HGNU4.js} +6 -0
  19. package/dist/{chunk-SNEMCQP7.js → chunk-FD2ZA65C.js} +1 -1
  20. package/dist/{chunk-GBIDDDSL.js → chunk-FKQJB6XC.js} +1 -1
  21. package/dist/{chunk-FEFPBHH4.js → chunk-H6WQUUNK.js} +453 -23
  22. package/dist/{chunk-BMOQI62Q.js → chunk-HNMFXFYP.js} +5 -3
  23. package/dist/{chunk-DHJPXGC7.js → chunk-JTO7NZLS.js} +1 -1
  24. package/dist/chunk-LLN5SPGL.js +399 -0
  25. package/dist/chunk-OIWVQYQF.js +284 -0
  26. package/dist/{chunk-IFTEGE4D.js → chunk-P2ZH6AN5.js} +4 -2
  27. package/dist/commands/backlog.js +1 -1
  28. package/dist/commands/blocked.js +1 -1
  29. package/dist/commands/canvas.js +1 -1
  30. package/dist/commands/checkpoint.js +1 -1
  31. package/dist/commands/context.js +4 -3
  32. package/dist/commands/doctor.js +6 -5
  33. package/dist/commands/kanban.d.ts +63 -0
  34. package/dist/commands/kanban.js +21 -0
  35. package/dist/commands/observe.js +4 -3
  36. package/dist/commands/rebuild.js +4 -3
  37. package/dist/commands/recover.d.ts +13 -1
  38. package/dist/commands/recover.js +10 -2
  39. package/dist/commands/replay.js +4 -3
  40. package/dist/commands/setup.js +3 -2
  41. package/dist/commands/sleep.js +6 -5
  42. package/dist/commands/status.js +6 -5
  43. package/dist/commands/task.d.ts +18 -6
  44. package/dist/commands/task.js +69 -12
  45. package/dist/commands/wake.js +5 -4
  46. package/dist/index.d.ts +29 -1
  47. package/dist/index.js +60 -15
  48. package/dist/lib/task-utils.d.ts +41 -10
  49. package/dist/lib/task-utils.js +5 -1
  50. package/package.json +2 -2
  51. package/dist/chunk-IWYZAXKJ.js +0 -146
@@ -0,0 +1,399 @@
1
+ import {
2
+ listTasks,
3
+ readTask,
4
+ updateTask
5
+ } from "./chunk-6QLRSPLZ.js";
6
+
7
+ // src/commands/kanban.ts
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ import matter from "gray-matter";
11
+ var STATUS_LANES = [
12
+ { status: "open", name: "Open" },
13
+ { status: "in-progress", name: "In Progress" },
14
+ { status: "blocked", name: "Blocked" },
15
+ { status: "done", name: "Done" }
16
+ ];
17
+ var PRIORITY_LANES = [
18
+ { priority: "critical", name: "\u{1F525} Critical" },
19
+ { priority: "high", name: "\u{1F534} High" },
20
+ { priority: "medium", name: "\u{1F7E1} Medium" },
21
+ { priority: "low", name: "\u{1F7E2} Low" },
22
+ { priority: null, name: "\u26AA Unset" }
23
+ ];
24
+ var PRIORITY_EMOJI = {
25
+ critical: "\u{1F525}",
26
+ high: "\u{1F534}",
27
+ medium: "\u{1F7E1}",
28
+ low: "\u{1F7E2}"
29
+ };
30
+ function normalizeGroupBy(value) {
31
+ const normalized = String(value || "status").trim().toLowerCase();
32
+ if (normalized === "status" || normalized === "priority" || normalized === "project" || normalized === "owner") {
33
+ return normalized;
34
+ }
35
+ throw new Error(`Unsupported kanban group field: ${normalized}`);
36
+ }
37
+ function resolveBoardPath(vaultPath, output) {
38
+ const resolvedVaultPath = path.resolve(vaultPath);
39
+ if (!output) {
40
+ return path.join(resolvedVaultPath, "Board.md");
41
+ }
42
+ if (path.isAbsolute(output)) {
43
+ return output;
44
+ }
45
+ return path.join(resolvedVaultPath, output);
46
+ }
47
+ function toHashTag(value) {
48
+ return value.trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-9/_-]/g, "");
49
+ }
50
+ function toMention(value) {
51
+ return value.trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-9._-]/g, "");
52
+ }
53
+ function dateOnly(value) {
54
+ return value.includes("T") ? value.split("T")[0] : value;
55
+ }
56
+ function dueTimestamp(task) {
57
+ if (!task.frontmatter.due) return Number.POSITIVE_INFINITY;
58
+ const timestamp = Date.parse(task.frontmatter.due);
59
+ return Number.isNaN(timestamp) ? Number.POSITIVE_INFINITY : timestamp;
60
+ }
61
+ function sortTasksForCards(tasks) {
62
+ return [...tasks].sort((left, right) => {
63
+ const dueDiff = dueTimestamp(left) - dueTimestamp(right);
64
+ if (dueDiff !== 0) return dueDiff;
65
+ return new Date(right.frontmatter.created).getTime() - new Date(left.frontmatter.created).getTime();
66
+ });
67
+ }
68
+ function statusLaneName(status) {
69
+ const lane = STATUS_LANES.find((entry) => entry.status === status);
70
+ return lane ? lane.name : "Open";
71
+ }
72
+ function priorityLaneName(priority) {
73
+ const lane = PRIORITY_LANES.find((entry) => entry.priority === (priority ?? null));
74
+ return lane ? lane.name : "\u26AA Unset";
75
+ }
76
+ function laneNameForTask(task, groupBy) {
77
+ switch (groupBy) {
78
+ case "status":
79
+ return statusLaneName(task.frontmatter.status);
80
+ case "priority":
81
+ return priorityLaneName(task.frontmatter.priority);
82
+ case "project":
83
+ return task.frontmatter.project?.trim() || "No Project";
84
+ case "owner":
85
+ return task.frontmatter.owner?.trim() || "Unassigned";
86
+ default:
87
+ return statusLaneName(task.frontmatter.status);
88
+ }
89
+ }
90
+ function defaultLaneOrder(groupBy, tasks) {
91
+ if (groupBy === "status") {
92
+ return STATUS_LANES.map((entry) => entry.name);
93
+ }
94
+ if (groupBy === "priority") {
95
+ return PRIORITY_LANES.map((entry) => entry.name);
96
+ }
97
+ const fallback = groupBy === "project" ? "No Project" : "Unassigned";
98
+ const values = /* @__PURE__ */ new Set();
99
+ for (const task of tasks) {
100
+ values.add(laneNameForTask(task, groupBy));
101
+ }
102
+ if (values.size === 0) {
103
+ return [fallback];
104
+ }
105
+ const sorted = Array.from(values).sort((left, right) => left.localeCompare(right));
106
+ if (sorted.includes(fallback)) {
107
+ return [...sorted.filter((value) => value !== fallback), fallback];
108
+ }
109
+ return sorted;
110
+ }
111
+ function formatKanbanCard(task) {
112
+ const checkbox = task.frontmatter.status === "done" ? "x" : " ";
113
+ const parts = [];
114
+ if (task.frontmatter.priority) {
115
+ parts.push(PRIORITY_EMOJI[task.frontmatter.priority]);
116
+ }
117
+ parts.push(`[[${task.slug}|${task.title}]]`);
118
+ if (task.frontmatter.project) {
119
+ const projectTag = toHashTag(task.frontmatter.project);
120
+ if (projectTag) parts.push(`#${projectTag}`);
121
+ }
122
+ if (task.frontmatter.owner) {
123
+ const mention = toMention(task.frontmatter.owner);
124
+ if (mention) parts.push(`@${mention}`);
125
+ }
126
+ if (task.frontmatter.tags && task.frontmatter.tags.length > 0) {
127
+ for (const tag of task.frontmatter.tags) {
128
+ const normalizedTag = toHashTag(tag);
129
+ if (normalizedTag) parts.push(`#${normalizedTag}`);
130
+ }
131
+ }
132
+ if (task.frontmatter.due) {
133
+ parts.push(`\u{1F4C5} ${dateOnly(task.frontmatter.due)}`);
134
+ }
135
+ if (task.frontmatter.status === "blocked" || task.frontmatter.blocked_by) {
136
+ parts.push("\u26D4");
137
+ }
138
+ return `- [${checkbox}] ${parts.join(" ")}`;
139
+ }
140
+ function buildKanbanLanes(tasks, groupBy) {
141
+ const laneOrder = defaultLaneOrder(groupBy, tasks);
142
+ const lanes = /* @__PURE__ */ new Map();
143
+ for (const laneName of laneOrder) {
144
+ lanes.set(laneName, []);
145
+ }
146
+ for (const task of sortTasksForCards(tasks)) {
147
+ const laneName = laneNameForTask(task, groupBy);
148
+ if (!lanes.has(laneName)) {
149
+ lanes.set(laneName, []);
150
+ }
151
+ lanes.get(laneName)?.push(formatKanbanCard(task));
152
+ }
153
+ return Array.from(lanes.entries()).map(([name, cards]) => ({ name, cards }));
154
+ }
155
+ function generateKanbanMarkdown(tasks, options = {}) {
156
+ const groupBy = normalizeGroupBy(options.groupBy);
157
+ const syncedAt = (options.now || /* @__PURE__ */ new Date()).toISOString();
158
+ const lanes = buildKanbanLanes(tasks, groupBy);
159
+ const sections = lanes.map((lane) => {
160
+ const cardsBlock = lane.cards.length > 0 ? lane.cards.join("\n") : "";
161
+ return `## ${lane.name}
162
+
163
+ ${cardsBlock}`.trimEnd();
164
+ }).join("\n\n");
165
+ return [
166
+ "---",
167
+ "kanban-plugin: basic",
168
+ `clawvault-group-by: ${groupBy}`,
169
+ `clawvault-last-sync: '${syncedAt}'`,
170
+ "---",
171
+ "",
172
+ sections,
173
+ "",
174
+ "%% kanban:settings",
175
+ '{"kanban-plugin":"basic","list-collapse":["Done"],"show-checkboxes":true}',
176
+ "%%",
177
+ ""
178
+ ].join("\n");
179
+ }
180
+ function syncKanbanBoard(vaultPath, options = {}) {
181
+ const groupBy = normalizeGroupBy(options.groupBy);
182
+ const outputPath = resolveBoardPath(vaultPath, options.output);
183
+ let tasks = listTasks(vaultPath);
184
+ if (options.filterProject) {
185
+ tasks = tasks.filter((task) => task.frontmatter.project === options.filterProject);
186
+ }
187
+ if (options.filterOwner) {
188
+ tasks = tasks.filter((task) => task.frontmatter.owner === options.filterOwner);
189
+ }
190
+ if (!options.includeDone) {
191
+ tasks = tasks.filter((task) => task.frontmatter.status !== "done");
192
+ }
193
+ const markdown = generateKanbanMarkdown(tasks, {
194
+ groupBy,
195
+ now: options.now
196
+ });
197
+ fs.writeFileSync(outputPath, markdown);
198
+ return {
199
+ outputPath,
200
+ groupBy,
201
+ markdown,
202
+ lanes: buildKanbanLanes(tasks, groupBy),
203
+ taskCount: tasks.length
204
+ };
205
+ }
206
+ function normalizeLaneKey(laneName) {
207
+ return laneName.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").replace(/\s+/g, " ").trim();
208
+ }
209
+ function statusFromLaneName(laneName) {
210
+ const key = normalizeLaneKey(laneName);
211
+ if (key.includes("in progress") || key.includes("in-progress") || key === "active") return "in-progress";
212
+ if (key.includes("blocked")) return "blocked";
213
+ if (key.includes("done") || key.includes("complete")) return "done";
214
+ if (key.includes("open")) return "open";
215
+ return null;
216
+ }
217
+ function priorityFromLaneName(laneName) {
218
+ const key = normalizeLaneKey(laneName);
219
+ if (key.includes("critical")) return "critical";
220
+ if (key.includes("high")) return "high";
221
+ if (key.includes("medium")) return "medium";
222
+ if (key.includes("low")) return "low";
223
+ if (key.includes("unset") || key.includes("none") || key.includes("no priority")) return null;
224
+ return void 0;
225
+ }
226
+ function isProjectFallbackLane(laneName) {
227
+ const key = normalizeLaneKey(laneName);
228
+ return key === "no project" || key === "none";
229
+ }
230
+ function isOwnerFallbackLane(laneName) {
231
+ const key = normalizeLaneKey(laneName);
232
+ return key === "unassigned" || key === "none";
233
+ }
234
+ function extractCardSlug(line) {
235
+ const wikiMatch = line.match(/\[\[([^\]]+)\]\]/);
236
+ if (!wikiMatch) return null;
237
+ let target = wikiMatch[1].split("|")[0].trim();
238
+ if (!target) return null;
239
+ target = target.split("#")[0].trim();
240
+ const filePart = target.split("/").pop() || target;
241
+ const slug = filePart.replace(/\.md$/i, "").trim();
242
+ return slug || null;
243
+ }
244
+ function parseKanbanMarkdown(markdown) {
245
+ const parsed = matter(markdown);
246
+ const groupBy = normalizeGroupBy(
247
+ typeof parsed.data["clawvault-group-by"] === "string" ? parsed.data["clawvault-group-by"] : void 0
248
+ );
249
+ const lanes = [];
250
+ const laneByName = /* @__PURE__ */ new Map();
251
+ let currentLane = null;
252
+ const lines = parsed.content.split(/\r?\n/);
253
+ for (const line of lines) {
254
+ const headerMatch = line.match(/^##\s+(.+?)\s*$/);
255
+ if (headerMatch) {
256
+ const laneName = headerMatch[1].trim();
257
+ if (!laneByName.has(laneName)) {
258
+ const lane = { name: laneName, slugs: [] };
259
+ laneByName.set(laneName, lane);
260
+ lanes.push(lane);
261
+ }
262
+ currentLane = laneByName.get(laneName) || null;
263
+ continue;
264
+ }
265
+ if (!currentLane || !/^\s*-\s*\[[ xX]\]\s+/.test(line)) {
266
+ continue;
267
+ }
268
+ const slug = extractCardSlug(line);
269
+ if (slug) {
270
+ currentLane.slugs.push(slug);
271
+ }
272
+ }
273
+ return { groupBy, lanes };
274
+ }
275
+ function hasUpdates(updates) {
276
+ return Object.keys(updates).length > 0;
277
+ }
278
+ function importKanbanBoard(vaultPath, options = {}) {
279
+ const outputPath = resolveBoardPath(vaultPath, options.output);
280
+ if (!fs.existsSync(outputPath)) {
281
+ throw new Error(`Kanban board not found: ${outputPath}`);
282
+ }
283
+ const markdown = fs.readFileSync(outputPath, "utf-8");
284
+ const parsed = parseKanbanMarkdown(markdown);
285
+ const changes = [];
286
+ const missingSlugs = [];
287
+ const seenSlugs = /* @__PURE__ */ new Set();
288
+ for (const lane of parsed.lanes) {
289
+ for (const slug of lane.slugs) {
290
+ if (seenSlugs.has(slug)) continue;
291
+ seenSlugs.add(slug);
292
+ const task = readTask(vaultPath, slug);
293
+ if (!task) {
294
+ missingSlugs.push(slug);
295
+ continue;
296
+ }
297
+ const updates = {};
298
+ if (parsed.groupBy === "status") {
299
+ const desiredStatus = statusFromLaneName(lane.name);
300
+ if (desiredStatus && task.frontmatter.status !== desiredStatus) {
301
+ updates.status = desiredStatus;
302
+ changes.push({
303
+ slug,
304
+ field: "status",
305
+ from: task.frontmatter.status,
306
+ to: desiredStatus
307
+ });
308
+ }
309
+ } else if (parsed.groupBy === "priority") {
310
+ const desiredPriority = priorityFromLaneName(lane.name);
311
+ if (desiredPriority !== void 0) {
312
+ const currentPriority = task.frontmatter.priority ?? null;
313
+ if (currentPriority !== desiredPriority) {
314
+ updates.priority = desiredPriority;
315
+ changes.push({
316
+ slug,
317
+ field: "priority",
318
+ from: currentPriority,
319
+ to: desiredPriority
320
+ });
321
+ }
322
+ }
323
+ } else if (parsed.groupBy === "project") {
324
+ const desiredProject = isProjectFallbackLane(lane.name) ? null : lane.name.trim();
325
+ const currentProject = task.frontmatter.project ?? null;
326
+ if (currentProject !== desiredProject) {
327
+ updates.project = desiredProject;
328
+ changes.push({
329
+ slug,
330
+ field: "project",
331
+ from: currentProject,
332
+ to: desiredProject
333
+ });
334
+ }
335
+ } else if (parsed.groupBy === "owner") {
336
+ const desiredOwner = isOwnerFallbackLane(lane.name) ? null : lane.name.trim();
337
+ const currentOwner = task.frontmatter.owner ?? null;
338
+ if (currentOwner !== desiredOwner) {
339
+ updates.owner = desiredOwner;
340
+ changes.push({
341
+ slug,
342
+ field: "owner",
343
+ from: currentOwner,
344
+ to: desiredOwner
345
+ });
346
+ }
347
+ }
348
+ if (hasUpdates(updates)) {
349
+ updateTask(vaultPath, slug, updates);
350
+ }
351
+ }
352
+ }
353
+ return {
354
+ outputPath,
355
+ groupBy: parsed.groupBy,
356
+ changes,
357
+ missingSlugs
358
+ };
359
+ }
360
+ async function kanbanCommand(vaultPath, action, options = {}) {
361
+ if (action === "sync") {
362
+ const result = syncKanbanBoard(vaultPath, options);
363
+ console.log(`\u2713 Synced kanban board: ${result.outputPath}`);
364
+ console.log(` Grouped by: ${result.groupBy}`);
365
+ console.log(` Tasks included: ${result.taskCount}`);
366
+ return;
367
+ }
368
+ if (action === "import") {
369
+ const result = importKanbanBoard(vaultPath, options);
370
+ console.log(`\u2713 Imported kanban board: ${result.outputPath}`);
371
+ console.log(` Grouped by: ${result.groupBy}`);
372
+ if (result.changes.length === 0) {
373
+ console.log(" No task updates required.");
374
+ } else {
375
+ console.log(` Updated ${result.changes.length} task field(s):`);
376
+ for (const change of result.changes) {
377
+ const from = change.from ?? "(unset)";
378
+ const to = change.to ?? "(unset)";
379
+ console.log(` - ${change.slug}: ${change.field} ${from} -> ${to}`);
380
+ }
381
+ }
382
+ if (result.missingSlugs.length > 0) {
383
+ console.log(` Missing tasks (${result.missingSlugs.length}): ${result.missingSlugs.join(", ")}`);
384
+ }
385
+ return;
386
+ }
387
+ throw new Error(`Unknown kanban action: ${action}`);
388
+ }
389
+
390
+ export {
391
+ formatKanbanCard,
392
+ buildKanbanLanes,
393
+ generateKanbanMarkdown,
394
+ syncKanbanBoard,
395
+ extractCardSlug,
396
+ parseKanbanMarkdown,
397
+ importKanbanBoard,
398
+ kanbanCommand
399
+ };
@@ -0,0 +1,284 @@
1
+ import {
2
+ formatAge
3
+ } from "./chunk-7ZRP733D.js";
4
+ import {
5
+ checkDirtyDeath,
6
+ clearDirtyFlag
7
+ } from "./chunk-F55HGNU4.js";
8
+
9
+ // src/commands/recover.ts
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ var CLAWVAULT_DIR = ".clawvault";
13
+ var CHECKPOINT_FILE = "last-checkpoint.json";
14
+ var CHECKPOINT_HISTORY_DIR = "checkpoints";
15
+ function parseCheckpointFile(filePath) {
16
+ try {
17
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
18
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
19
+ return null;
20
+ }
21
+ const record = parsed;
22
+ const timestamp = typeof record.timestamp === "string" ? record.timestamp.trim() : "";
23
+ if (!timestamp) {
24
+ return null;
25
+ }
26
+ const checkpoint = {
27
+ timestamp,
28
+ workingOn: typeof record.workingOn === "string" ? record.workingOn : null,
29
+ focus: typeof record.focus === "string" ? record.focus : null,
30
+ blocked: typeof record.blocked === "string" ? record.blocked : null
31
+ };
32
+ if (typeof record.sessionId === "string") {
33
+ checkpoint.sessionId = record.sessionId;
34
+ }
35
+ if (typeof record.sessionKey === "string") {
36
+ checkpoint.sessionKey = record.sessionKey;
37
+ }
38
+ if (typeof record.model === "string") {
39
+ checkpoint.model = record.model;
40
+ }
41
+ if (typeof record.tokenEstimate === "number" && Number.isFinite(record.tokenEstimate)) {
42
+ checkpoint.tokenEstimate = record.tokenEstimate;
43
+ }
44
+ if (typeof record.sessionStartedAt === "string") {
45
+ checkpoint.sessionStartedAt = record.sessionStartedAt;
46
+ }
47
+ if (typeof record.urgent === "boolean") {
48
+ checkpoint.urgent = record.urgent;
49
+ }
50
+ return checkpoint;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function compareByTimestampDesc(left, right) {
56
+ const leftTime = Date.parse(left.timestamp);
57
+ const rightTime = Date.parse(right.timestamp);
58
+ if (!Number.isNaN(leftTime) && !Number.isNaN(rightTime)) {
59
+ return rightTime - leftTime;
60
+ }
61
+ return right.timestamp.localeCompare(left.timestamp);
62
+ }
63
+ async function checkRecoveryStatus(vaultPath) {
64
+ const { died, checkpoint, deathTime } = await checkDirtyDeath(vaultPath);
65
+ return { died, checkpoint, deathTime };
66
+ }
67
+ function listCheckpoints(vaultPath) {
68
+ const resolvedVaultPath = path.resolve(vaultPath);
69
+ const clawvaultDir = path.join(resolvedVaultPath, CLAWVAULT_DIR);
70
+ const historyDir = path.join(clawvaultDir, CHECKPOINT_HISTORY_DIR);
71
+ const checkpoints = [];
72
+ if (fs.existsSync(historyDir)) {
73
+ const files = fs.readdirSync(historyDir).filter((entry) => entry.endsWith(".json")).sort().reverse();
74
+ for (const fileName of files) {
75
+ const absolutePath = path.join(historyDir, fileName);
76
+ const parsed = parseCheckpointFile(absolutePath);
77
+ if (!parsed) {
78
+ continue;
79
+ }
80
+ checkpoints.push({
81
+ ...parsed,
82
+ filePath: absolutePath
83
+ });
84
+ }
85
+ }
86
+ if (checkpoints.length === 0) {
87
+ const latestCheckpointPath = path.join(clawvaultDir, CHECKPOINT_FILE);
88
+ if (fs.existsSync(latestCheckpointPath)) {
89
+ const fallback = parseCheckpointFile(latestCheckpointPath);
90
+ if (fallback) {
91
+ checkpoints.push({
92
+ ...fallback,
93
+ filePath: latestCheckpointPath
94
+ });
95
+ }
96
+ }
97
+ }
98
+ return checkpoints.sort(compareByTimestampDesc);
99
+ }
100
+ async function recover(vaultPath, options = {}) {
101
+ const { clearFlag = false } = options;
102
+ const { died, checkpoint, deathTime } = await checkRecoveryStatus(vaultPath);
103
+ if (!died) {
104
+ return {
105
+ died: false,
106
+ deathTime: null,
107
+ checkpoint: null,
108
+ handoffPath: null,
109
+ handoffContent: null,
110
+ recoveryMessage: "No context death detected. Clean startup."
111
+ };
112
+ }
113
+ const handoffsDir = path.join(vaultPath, "handoffs");
114
+ let handoffPath = null;
115
+ let handoffContent = null;
116
+ if (fs.existsSync(handoffsDir)) {
117
+ const files = fs.readdirSync(handoffsDir).filter((f) => f.startsWith("handoff-") && f.endsWith(".md")).sort().reverse();
118
+ if (files.length > 0) {
119
+ handoffPath = path.join(handoffsDir, files[0]);
120
+ handoffContent = fs.readFileSync(handoffPath, "utf-8");
121
+ }
122
+ }
123
+ let message = "\u26A0\uFE0F **CONTEXT DEATH DETECTED**\n\n";
124
+ message += `Your previous session died at ${deathTime}.
125
+
126
+ `;
127
+ if (checkpoint) {
128
+ message += "**Last known state:**\n";
129
+ if (checkpoint.workingOn) {
130
+ message += `- Working on: ${checkpoint.workingOn}
131
+ `;
132
+ }
133
+ if (checkpoint.focus) {
134
+ message += `- Focus: ${checkpoint.focus}
135
+ `;
136
+ }
137
+ if (checkpoint.blocked) {
138
+ message += `- Blocked: ${checkpoint.blocked}
139
+ `;
140
+ }
141
+ message += "\n";
142
+ }
143
+ if (handoffPath) {
144
+ message += `**Last handoff:** ${path.basename(handoffPath)}
145
+ `;
146
+ message += "Review and resume from where you left off.\n";
147
+ } else {
148
+ message += "**No handoff found.** You may have lost context.\n";
149
+ }
150
+ if (clearFlag) {
151
+ await clearDirtyFlag(vaultPath);
152
+ }
153
+ return {
154
+ died: true,
155
+ deathTime,
156
+ checkpoint,
157
+ handoffPath,
158
+ handoffContent,
159
+ recoveryMessage: message
160
+ };
161
+ }
162
+ function formatRecoveryCheckStatus(info) {
163
+ if (!info.died) {
164
+ return "\u2713 Dirty death flag is clear.";
165
+ }
166
+ let output = "\u26A0\uFE0F Dirty death flag is set.\n";
167
+ output += `Death time: ${info.deathTime}
168
+ `;
169
+ if (info.checkpoint?.timestamp) {
170
+ const age = formatAge(Date.now() - new Date(info.checkpoint.timestamp).getTime());
171
+ output += `Last checkpoint: ${info.checkpoint.timestamp} (${age} ago)
172
+ `;
173
+ } else {
174
+ output += "Last checkpoint: unavailable\n";
175
+ }
176
+ output += "Use `clawvault recover --clear` after reviewing recovery details.";
177
+ return output;
178
+ }
179
+ function formatCheckpointList(checkpoints) {
180
+ if (checkpoints.length === 0) {
181
+ return "No checkpoints found.";
182
+ }
183
+ const headers = ["TIMESTAMP", "WORKING_ON", "FOCUS", "FILE"];
184
+ const rows = checkpoints.map((checkpoint) => ({
185
+ timestamp: checkpoint.timestamp,
186
+ workingOn: checkpoint.workingOn ?? "-",
187
+ focus: checkpoint.focus ?? "-",
188
+ file: path.basename(checkpoint.filePath)
189
+ }));
190
+ const timestampWidth = Math.max(headers[0].length, ...rows.map((row) => row.timestamp.length));
191
+ const workingOnWidth = Math.max(headers[1].length, ...rows.map((row) => row.workingOn.length));
192
+ const focusWidth = Math.max(headers[2].length, ...rows.map((row) => row.focus.length));
193
+ const fileWidth = Math.max(headers[3].length, ...rows.map((row) => row.file.length));
194
+ const lines = [];
195
+ lines.push(
196
+ `${headers[0].padEnd(timestampWidth)} ${headers[1].padEnd(workingOnWidth)} ${headers[2].padEnd(focusWidth)} ${headers[3].padEnd(fileWidth)}`
197
+ );
198
+ lines.push(
199
+ `${"-".repeat(timestampWidth)} ${"-".repeat(workingOnWidth)} ${"-".repeat(focusWidth)} ${"-".repeat(fileWidth)}`
200
+ );
201
+ for (const row of rows) {
202
+ lines.push(
203
+ `${row.timestamp.padEnd(timestampWidth)} ${row.workingOn.padEnd(workingOnWidth)} ${row.focus.padEnd(focusWidth)} ${row.file}`
204
+ );
205
+ }
206
+ return lines.join("\n");
207
+ }
208
+ function formatRecoveryInfo(info, options = {}) {
209
+ const { verbose = false } = options;
210
+ if (!info.died) {
211
+ return "\u2713 Clean startup - no context death detected.";
212
+ }
213
+ let output = "\n\u26A0\uFE0F CONTEXT DEATH DETECTED\n";
214
+ output += "\u2550".repeat(40) + "\n\n";
215
+ output += `Death time: ${info.deathTime}
216
+ `;
217
+ if (info.checkpoint?.timestamp) {
218
+ const age = formatAge(Date.now() - new Date(info.checkpoint.timestamp).getTime());
219
+ output += `Checkpoint: ${info.checkpoint.timestamp} (${age} ago)
220
+ `;
221
+ }
222
+ output += "\n";
223
+ if (info.checkpoint) {
224
+ output += "Last checkpoint:\n";
225
+ if (info.checkpoint.workingOn) {
226
+ output += ` \u2022 Working on: ${info.checkpoint.workingOn}
227
+ `;
228
+ }
229
+ if (info.checkpoint.focus) {
230
+ output += ` \u2022 Focus: ${info.checkpoint.focus}
231
+ `;
232
+ }
233
+ if (info.checkpoint.blocked) {
234
+ output += ` \u2022 Blocked: ${info.checkpoint.blocked}
235
+ `;
236
+ }
237
+ if (info.checkpoint.sessionKey || info.checkpoint.model || info.checkpoint.tokenEstimate !== void 0) {
238
+ output += " \u2022 Session:\n";
239
+ if (info.checkpoint.sessionKey) {
240
+ output += ` - Key: ${info.checkpoint.sessionKey}
241
+ `;
242
+ }
243
+ if (info.checkpoint.model) {
244
+ output += ` - Model: ${info.checkpoint.model}
245
+ `;
246
+ }
247
+ if (info.checkpoint.tokenEstimate !== void 0) {
248
+ output += ` - Token estimate: ${info.checkpoint.tokenEstimate}
249
+ `;
250
+ }
251
+ }
252
+ output += "\n";
253
+ } else {
254
+ output += "No checkpoint data found.\n\n";
255
+ }
256
+ if (info.handoffPath) {
257
+ output += `Last handoff: ${path.basename(info.handoffPath)}
258
+ `;
259
+ } else {
260
+ output += "No handoff found - context may be lost.\n";
261
+ }
262
+ if (verbose) {
263
+ if (info.checkpoint) {
264
+ output += "\nCheckpoint JSON:\n";
265
+ output += JSON.stringify(info.checkpoint, null, 2) + "\n";
266
+ }
267
+ if (info.handoffContent) {
268
+ output += "\nHandoff content:\n";
269
+ output += info.handoffContent.trim() + "\n";
270
+ }
271
+ }
272
+ output += "\n" + "\u2550".repeat(40) + "\n";
273
+ output += "Run `clawvault recap` to see full context.\n";
274
+ return output;
275
+ }
276
+
277
+ export {
278
+ checkRecoveryStatus,
279
+ listCheckpoints,
280
+ recover,
281
+ formatRecoveryCheckStatus,
282
+ formatCheckpointList,
283
+ formatRecoveryInfo
284
+ };
@@ -1,7 +1,9 @@
1
1
  import {
2
- DEFAULT_CATEGORIES,
3
2
  hasQmd
4
- } from "./chunk-FDJIZKCW.js";
3
+ } from "./chunk-6B3JWM7J.js";
4
+ import {
5
+ DEFAULT_CATEGORIES
6
+ } from "./chunk-3PJIGGWV.js";
5
7
 
6
8
  // src/commands/setup.ts
7
9
  import * as fs from "fs";
@@ -2,7 +2,7 @@ import {
2
2
  createBacklogItem,
3
3
  listBacklogItems,
4
4
  promoteBacklogItem
5
- } from "../chunk-DEFBIVQ3.js";
5
+ } from "../chunk-6QLRSPLZ.js";
6
6
 
7
7
  // src/commands/backlog.ts
8
8
  function toDateStr(val) {