clawvault 1.1.0
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/README.md +216 -0
- package/bin/clawvault.js +845 -0
- package/dist/chunk-4KDZZW4X.js +13 -0
- package/dist/chunk-4XJDHIKE.js +103 -0
- package/dist/chunk-J7ZWCI2C.js +49 -0
- package/dist/chunk-NITF7AHR.js +95 -0
- package/dist/commands/checkpoint.d.ts +27 -0
- package/dist/commands/checkpoint.js +14 -0
- package/dist/commands/entities.d.ts +6 -0
- package/dist/commands/entities.js +44 -0
- package/dist/commands/link.d.ts +7 -0
- package/dist/commands/link.js +85 -0
- package/dist/commands/recover.d.ts +21 -0
- package/dist/commands/recover.js +108 -0
- package/dist/index.d.ts +438 -0
- package/dist/index.js +1233 -0
- package/dist/lib/auto-linker.d.ts +18 -0
- package/dist/lib/auto-linker.js +9 -0
- package/dist/lib/config.d.ts +6 -0
- package/dist/lib/config.js +6 -0
- package/dist/lib/entity-index.d.ts +26 -0
- package/dist/lib/entity-index.js +8 -0
- package/package.json +70 -0
- package/templates/daily.md +22 -0
- package/templates/decision.md +23 -0
- package/templates/handoff.md +23 -0
- package/templates/lesson.md +18 -0
- package/templates/person.md +34 -0
- package/templates/project.md +35 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/lib/config.ts
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
function getVaultPath() {
|
|
4
|
+
const vaultPath = process.env.CLAWVAULT_PATH;
|
|
5
|
+
if (!vaultPath) {
|
|
6
|
+
throw new Error("CLAWVAULT_PATH environment variable not set");
|
|
7
|
+
}
|
|
8
|
+
return path.resolve(vaultPath);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
getVaultPath
|
|
13
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSortedAliases
|
|
3
|
+
} from "./chunk-J7ZWCI2C.js";
|
|
4
|
+
|
|
5
|
+
// src/lib/auto-linker.ts
|
|
6
|
+
function findProtectedRanges(content) {
|
|
7
|
+
const ranges = [];
|
|
8
|
+
const fmMatch = content.match(/^---\n[\s\S]*?\n---/);
|
|
9
|
+
if (fmMatch) {
|
|
10
|
+
ranges.push({ start: 0, end: fmMatch[0].length });
|
|
11
|
+
}
|
|
12
|
+
const codeBlockRegex = /```[\s\S]*?```|~~~[\s\S]*?~~~/g;
|
|
13
|
+
let match;
|
|
14
|
+
while ((match = codeBlockRegex.exec(content)) !== null) {
|
|
15
|
+
ranges.push({ start: match.index, end: match.index + match[0].length });
|
|
16
|
+
}
|
|
17
|
+
const inlineCodeRegex = /`[^`]+`/g;
|
|
18
|
+
while ((match = inlineCodeRegex.exec(content)) !== null) {
|
|
19
|
+
ranges.push({ start: match.index, end: match.index + match[0].length });
|
|
20
|
+
}
|
|
21
|
+
const wikiLinkRegex = /\[\[[^\]]+\]\]/g;
|
|
22
|
+
while ((match = wikiLinkRegex.exec(content)) !== null) {
|
|
23
|
+
ranges.push({ start: match.index, end: match.index + match[0].length });
|
|
24
|
+
}
|
|
25
|
+
const urlRegex = /https?:\/\/[^\s)>\]]+/g;
|
|
26
|
+
while ((match = urlRegex.exec(content)) !== null) {
|
|
27
|
+
ranges.push({ start: match.index, end: match.index + match[0].length });
|
|
28
|
+
}
|
|
29
|
+
return ranges;
|
|
30
|
+
}
|
|
31
|
+
function isProtected(pos, ranges) {
|
|
32
|
+
return ranges.some((r) => pos >= r.start && pos < r.end);
|
|
33
|
+
}
|
|
34
|
+
function autoLink(content, index) {
|
|
35
|
+
const protectedRanges = findProtectedRanges(content);
|
|
36
|
+
const sortedAliases = getSortedAliases(index);
|
|
37
|
+
const linkedEntities = /* @__PURE__ */ new Set();
|
|
38
|
+
let result = content;
|
|
39
|
+
let offset = 0;
|
|
40
|
+
for (const { alias, path } of sortedAliases) {
|
|
41
|
+
if (linkedEntities.has(path)) continue;
|
|
42
|
+
const escapedAlias = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
43
|
+
const regex = new RegExp(`\\b${escapedAlias}\\b`, "gi");
|
|
44
|
+
let match;
|
|
45
|
+
while ((match = regex.exec(content)) !== null) {
|
|
46
|
+
const originalPos = match.index;
|
|
47
|
+
const adjustedPos = originalPos + offset;
|
|
48
|
+
if (isProtected(originalPos, protectedRanges)) continue;
|
|
49
|
+
const beforeMatch = result.substring(0, adjustedPos);
|
|
50
|
+
const openBrackets = (beforeMatch.match(/\[\[/g) || []).length;
|
|
51
|
+
const closeBrackets = (beforeMatch.match(/\]\]/g) || []).length;
|
|
52
|
+
if (openBrackets > closeBrackets) continue;
|
|
53
|
+
const originalText = match[0];
|
|
54
|
+
const replacement = originalText.toLowerCase() === path.split("/").pop()?.toLowerCase() ? `[[${path}]]` : `[[${path}|${originalText}]]`;
|
|
55
|
+
result = result.substring(0, adjustedPos) + replacement + result.substring(adjustedPos + originalText.length);
|
|
56
|
+
offset += replacement.length - originalText.length;
|
|
57
|
+
linkedEntities.add(path);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function dryRunLink(content, index) {
|
|
64
|
+
const protectedRanges = findProtectedRanges(content);
|
|
65
|
+
const sortedAliases = getSortedAliases(index);
|
|
66
|
+
const linkedEntities = /* @__PURE__ */ new Set();
|
|
67
|
+
const matches = [];
|
|
68
|
+
const lines = content.split("\n");
|
|
69
|
+
let charPos = 0;
|
|
70
|
+
const lineStarts = [];
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
lineStarts.push(charPos);
|
|
73
|
+
charPos += line.length + 1;
|
|
74
|
+
}
|
|
75
|
+
function getLineNumber(pos) {
|
|
76
|
+
for (let i = lineStarts.length - 1; i >= 0; i--) {
|
|
77
|
+
if (pos >= lineStarts[i]) return i + 1;
|
|
78
|
+
}
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
for (const { alias, path } of sortedAliases) {
|
|
82
|
+
if (linkedEntities.has(path)) continue;
|
|
83
|
+
const escapedAlias = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
84
|
+
const regex = new RegExp(`\\b${escapedAlias}\\b`, "gi");
|
|
85
|
+
let match;
|
|
86
|
+
while ((match = regex.exec(content)) !== null) {
|
|
87
|
+
if (isProtected(match.index, protectedRanges)) continue;
|
|
88
|
+
matches.push({
|
|
89
|
+
alias: match[0],
|
|
90
|
+
path,
|
|
91
|
+
line: getLineNumber(match.index)
|
|
92
|
+
});
|
|
93
|
+
linkedEntities.add(path);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return matches;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
autoLink,
|
|
102
|
+
dryRunLink
|
|
103
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/lib/entity-index.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import matter from "gray-matter";
|
|
5
|
+
function buildEntityIndex(vaultPath) {
|
|
6
|
+
const entries = /* @__PURE__ */ new Map();
|
|
7
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
8
|
+
const entityFolders = ["people", "projects", "agents", "lessons", "decisions", "commitments"];
|
|
9
|
+
for (const folder of entityFolders) {
|
|
10
|
+
const folderPath = path.join(vaultPath, folder);
|
|
11
|
+
if (!fs.existsSync(folderPath)) continue;
|
|
12
|
+
const files = fs.readdirSync(folderPath).filter((f) => f.endsWith(".md"));
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const filePath = path.join(folderPath, file);
|
|
15
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
16
|
+
const { data: frontmatter } = matter(content);
|
|
17
|
+
const relativePath = `${folder}/${file.replace(".md", "")}`;
|
|
18
|
+
const baseName = file.replace(".md", "");
|
|
19
|
+
const aliases = [baseName];
|
|
20
|
+
if (frontmatter.title && frontmatter.title.toLowerCase() !== baseName.toLowerCase()) {
|
|
21
|
+
aliases.push(frontmatter.title);
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(frontmatter.aliases)) {
|
|
24
|
+
aliases.push(...frontmatter.aliases);
|
|
25
|
+
}
|
|
26
|
+
for (const alias of aliases) {
|
|
27
|
+
const key = alias.toLowerCase();
|
|
28
|
+
if (!entries.has(key)) {
|
|
29
|
+
entries.set(key, relativePath);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
byPath.set(relativePath, { path: relativePath, aliases });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { entries, byPath };
|
|
36
|
+
}
|
|
37
|
+
function getSortedAliases(index) {
|
|
38
|
+
const result = [];
|
|
39
|
+
for (const [alias, path2] of index.entries) {
|
|
40
|
+
result.push({ alias, path: path2 });
|
|
41
|
+
}
|
|
42
|
+
result.sort((a, b) => b.alias.length - a.alias.length);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
buildEntityIndex,
|
|
48
|
+
getSortedAliases
|
|
49
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/commands/checkpoint.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
var CLAWVAULT_DIR = ".clawvault";
|
|
5
|
+
var CHECKPOINT_FILE = "last-checkpoint.json";
|
|
6
|
+
var SESSION_STATE_FILE = "session-state.json";
|
|
7
|
+
var DIRTY_DEATH_FLAG = "dirty-death.flag";
|
|
8
|
+
var pendingCheckpoint = null;
|
|
9
|
+
var pendingData = null;
|
|
10
|
+
function ensureClawvaultDir(vaultPath) {
|
|
11
|
+
const dir = path.join(vaultPath, CLAWVAULT_DIR);
|
|
12
|
+
if (!fs.existsSync(dir)) {
|
|
13
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
function writeCheckpointToDisk(dir, data) {
|
|
18
|
+
const checkpointPath = path.join(dir, CHECKPOINT_FILE);
|
|
19
|
+
fs.writeFileSync(checkpointPath, JSON.stringify(data, null, 2));
|
|
20
|
+
const flagPath = path.join(dir, DIRTY_DEATH_FLAG);
|
|
21
|
+
fs.writeFileSync(flagPath, data.timestamp);
|
|
22
|
+
}
|
|
23
|
+
async function flush() {
|
|
24
|
+
if (pendingCheckpoint) {
|
|
25
|
+
clearTimeout(pendingCheckpoint);
|
|
26
|
+
pendingCheckpoint = null;
|
|
27
|
+
}
|
|
28
|
+
if (!pendingData) return null;
|
|
29
|
+
const { dir, data } = pendingData;
|
|
30
|
+
pendingData = null;
|
|
31
|
+
writeCheckpointToDisk(dir, data);
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
async function checkpoint(options) {
|
|
35
|
+
const dir = ensureClawvaultDir(options.vaultPath);
|
|
36
|
+
const data = {
|
|
37
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
38
|
+
workingOn: options.workingOn || null,
|
|
39
|
+
focus: options.focus || null,
|
|
40
|
+
blocked: options.blocked || null
|
|
41
|
+
};
|
|
42
|
+
const sessionStatePath = path.join(dir, SESSION_STATE_FILE);
|
|
43
|
+
if (fs.existsSync(sessionStatePath)) {
|
|
44
|
+
try {
|
|
45
|
+
const sessionState = JSON.parse(fs.readFileSync(sessionStatePath, "utf-8"));
|
|
46
|
+
data.sessionId = sessionState.sessionId;
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
pendingData = { dir, data };
|
|
51
|
+
if (pendingCheckpoint) clearTimeout(pendingCheckpoint);
|
|
52
|
+
pendingCheckpoint = setTimeout(() => {
|
|
53
|
+
void flush();
|
|
54
|
+
}, 1e3);
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
async function clearDirtyFlag(vaultPath) {
|
|
58
|
+
const flagPath = path.join(vaultPath, CLAWVAULT_DIR, DIRTY_DEATH_FLAG);
|
|
59
|
+
if (fs.existsSync(flagPath)) {
|
|
60
|
+
fs.unlinkSync(flagPath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function checkDirtyDeath(vaultPath) {
|
|
64
|
+
const dir = path.join(vaultPath, CLAWVAULT_DIR);
|
|
65
|
+
const flagPath = path.join(dir, DIRTY_DEATH_FLAG);
|
|
66
|
+
const checkpointPath = path.join(dir, CHECKPOINT_FILE);
|
|
67
|
+
if (!fs.existsSync(flagPath)) {
|
|
68
|
+
return { died: false, checkpoint: null, deathTime: null };
|
|
69
|
+
}
|
|
70
|
+
const deathTime = fs.readFileSync(flagPath, "utf-8").trim();
|
|
71
|
+
let checkpoint2 = null;
|
|
72
|
+
if (fs.existsSync(checkpointPath)) {
|
|
73
|
+
try {
|
|
74
|
+
checkpoint2 = JSON.parse(fs.readFileSync(checkpointPath, "utf-8"));
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { died: true, checkpoint: checkpoint2, deathTime };
|
|
79
|
+
}
|
|
80
|
+
async function setSessionState(vaultPath, sessionId) {
|
|
81
|
+
const dir = ensureClawvaultDir(vaultPath);
|
|
82
|
+
const sessionStatePath = path.join(dir, SESSION_STATE_FILE);
|
|
83
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify({
|
|
84
|
+
sessionId,
|
|
85
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
86
|
+
}, null, 2));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
flush,
|
|
91
|
+
checkpoint,
|
|
92
|
+
clearDirtyFlag,
|
|
93
|
+
checkDirtyDeath,
|
|
94
|
+
setSessionState
|
|
95
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick checkpoint command - fast state save for context death resilience
|
|
3
|
+
*/
|
|
4
|
+
interface CheckpointOptions {
|
|
5
|
+
workingOn?: string;
|
|
6
|
+
focus?: string;
|
|
7
|
+
blocked?: string;
|
|
8
|
+
vaultPath: string;
|
|
9
|
+
}
|
|
10
|
+
interface CheckpointData {
|
|
11
|
+
timestamp: string;
|
|
12
|
+
workingOn: string | null;
|
|
13
|
+
focus: string | null;
|
|
14
|
+
blocked: string | null;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
}
|
|
17
|
+
declare function flush(): Promise<CheckpointData | null>;
|
|
18
|
+
declare function checkpoint(options: CheckpointOptions): Promise<CheckpointData>;
|
|
19
|
+
declare function clearDirtyFlag(vaultPath: string): Promise<void>;
|
|
20
|
+
declare function checkDirtyDeath(vaultPath: string): Promise<{
|
|
21
|
+
died: boolean;
|
|
22
|
+
checkpoint: CheckpointData | null;
|
|
23
|
+
deathTime: string | null;
|
|
24
|
+
}>;
|
|
25
|
+
declare function setSessionState(vaultPath: string, sessionId: string): Promise<void>;
|
|
26
|
+
|
|
27
|
+
export { type CheckpointData, type CheckpointOptions, checkDirtyDeath, checkpoint, clearDirtyFlag, flush, setSessionState };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getVaultPath
|
|
3
|
+
} from "../chunk-4KDZZW4X.js";
|
|
4
|
+
import {
|
|
5
|
+
buildEntityIndex
|
|
6
|
+
} from "../chunk-J7ZWCI2C.js";
|
|
7
|
+
|
|
8
|
+
// src/commands/entities.ts
|
|
9
|
+
async function entitiesCommand(options) {
|
|
10
|
+
const vaultPath = getVaultPath();
|
|
11
|
+
const index = buildEntityIndex(vaultPath);
|
|
12
|
+
if (options.json) {
|
|
13
|
+
const output = {};
|
|
14
|
+
for (const [path, entry] of index.byPath) {
|
|
15
|
+
output[path] = entry.aliases;
|
|
16
|
+
}
|
|
17
|
+
console.log(JSON.stringify(output, null, 2));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const byFolder = {};
|
|
21
|
+
for (const [path, entry] of index.byPath) {
|
|
22
|
+
const folder = path.split("/")[0];
|
|
23
|
+
if (!byFolder[folder]) byFolder[folder] = [];
|
|
24
|
+
byFolder[folder].push({ path, aliases: entry.aliases });
|
|
25
|
+
}
|
|
26
|
+
console.log("\u{1F4DA} Linkable Entities\n");
|
|
27
|
+
for (const [folder, entities] of Object.entries(byFolder)) {
|
|
28
|
+
console.log(`## ${folder}/`);
|
|
29
|
+
for (const entity of entities) {
|
|
30
|
+
const name = entity.path.split("/")[1];
|
|
31
|
+
const otherAliases = entity.aliases.filter((a) => a.toLowerCase() !== name.toLowerCase());
|
|
32
|
+
if (otherAliases.length > 0) {
|
|
33
|
+
console.log(` - ${name} (${otherAliases.join(", ")})`);
|
|
34
|
+
} else {
|
|
35
|
+
console.log(` - ${name}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
console.log();
|
|
39
|
+
}
|
|
40
|
+
console.log(`Total: ${index.byPath.size} entities, ${index.entries.size} linkable aliases`);
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
entitiesCommand
|
|
44
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
autoLink,
|
|
3
|
+
dryRunLink
|
|
4
|
+
} from "../chunk-4XJDHIKE.js";
|
|
5
|
+
import {
|
|
6
|
+
getVaultPath
|
|
7
|
+
} from "../chunk-4KDZZW4X.js";
|
|
8
|
+
import {
|
|
9
|
+
buildEntityIndex
|
|
10
|
+
} from "../chunk-J7ZWCI2C.js";
|
|
11
|
+
|
|
12
|
+
// src/commands/link.ts
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
async function linkCommand(file, options) {
|
|
16
|
+
const vaultPath = getVaultPath();
|
|
17
|
+
const index = buildEntityIndex(vaultPath);
|
|
18
|
+
if (options.all) {
|
|
19
|
+
await linkAllFiles(vaultPath, index, options.dryRun);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!file) {
|
|
23
|
+
console.error("Error: Specify a file or use --all");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const filePath = path.isAbsolute(file) ? file : path.join(process.cwd(), file);
|
|
27
|
+
if (!fs.existsSync(filePath)) {
|
|
28
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
await linkFile(filePath, index, options.dryRun);
|
|
32
|
+
}
|
|
33
|
+
async function linkFile(filePath, index, dryRun) {
|
|
34
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
35
|
+
if (dryRun) {
|
|
36
|
+
const matches = dryRunLink(content, index);
|
|
37
|
+
if (matches.length > 0) {
|
|
38
|
+
console.log(`
|
|
39
|
+
\u{1F4C4} ${filePath}`);
|
|
40
|
+
for (const m of matches) {
|
|
41
|
+
console.log(` Line ${m.line}: "${m.alias}" \u2192 [[${m.path}]]`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return matches.length;
|
|
45
|
+
}
|
|
46
|
+
const linked = autoLink(content, index);
|
|
47
|
+
if (linked !== content) {
|
|
48
|
+
fs.writeFileSync(filePath, linked);
|
|
49
|
+
const matches = dryRunLink(content, index);
|
|
50
|
+
console.log(`\u2713 Linked ${matches.length} entities in ${path.basename(filePath)}`);
|
|
51
|
+
return matches.length;
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
async function linkAllFiles(vaultPath, index, dryRun) {
|
|
56
|
+
const files = [];
|
|
57
|
+
function walk(dir) {
|
|
58
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const fullPath = path.join(dir, entry.name);
|
|
61
|
+
if (entry.isDirectory()) {
|
|
62
|
+
if (!entry.name.startsWith(".") && entry.name !== "archive" && entry.name !== "templates") {
|
|
63
|
+
walk(fullPath);
|
|
64
|
+
}
|
|
65
|
+
} else if (entry.name.endsWith(".md")) {
|
|
66
|
+
files.push(fullPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
walk(vaultPath);
|
|
71
|
+
let totalLinks = 0;
|
|
72
|
+
let filesModified = 0;
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
const links = await linkFile(file, index, dryRun);
|
|
75
|
+
if (links > 0) {
|
|
76
|
+
totalLinks += links;
|
|
77
|
+
filesModified++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
console.log(`
|
|
81
|
+
${dryRun ? "(dry run) " : ""}${totalLinks} links in ${filesModified} files`);
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
linkCommand
|
|
85
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CheckpointData } from './checkpoint.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recovery command - detect dirty death and provide recovery info
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface RecoveryInfo {
|
|
8
|
+
died: boolean;
|
|
9
|
+
deathTime: string | null;
|
|
10
|
+
checkpoint: CheckpointData | null;
|
|
11
|
+
handoffPath: string | null;
|
|
12
|
+
handoffContent: string | null;
|
|
13
|
+
recoveryMessage: string;
|
|
14
|
+
}
|
|
15
|
+
declare function recover(vaultPath: string, clearFlag?: boolean): Promise<RecoveryInfo>;
|
|
16
|
+
/**
|
|
17
|
+
* Format recovery info for CLI output
|
|
18
|
+
*/
|
|
19
|
+
declare function formatRecoveryInfo(info: RecoveryInfo): string;
|
|
20
|
+
|
|
21
|
+
export { type RecoveryInfo, formatRecoveryInfo, recover };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkDirtyDeath,
|
|
3
|
+
clearDirtyFlag
|
|
4
|
+
} from "../chunk-NITF7AHR.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/recover.ts
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
async function recover(vaultPath, clearFlag = false) {
|
|
10
|
+
const { died, checkpoint, deathTime } = await checkDirtyDeath(vaultPath);
|
|
11
|
+
if (!died) {
|
|
12
|
+
return {
|
|
13
|
+
died: false,
|
|
14
|
+
deathTime: null,
|
|
15
|
+
checkpoint: null,
|
|
16
|
+
handoffPath: null,
|
|
17
|
+
handoffContent: null,
|
|
18
|
+
recoveryMessage: "No context death detected. Clean startup."
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const handoffsDir = path.join(vaultPath, "handoffs");
|
|
22
|
+
let handoffPath = null;
|
|
23
|
+
let handoffContent = null;
|
|
24
|
+
if (fs.existsSync(handoffsDir)) {
|
|
25
|
+
const files = fs.readdirSync(handoffsDir).filter((f) => f.startsWith("handoff-") && f.endsWith(".md")).sort().reverse();
|
|
26
|
+
if (files.length > 0) {
|
|
27
|
+
handoffPath = path.join(handoffsDir, files[0]);
|
|
28
|
+
handoffContent = fs.readFileSync(handoffPath, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
let message = "\u26A0\uFE0F **CONTEXT DEATH DETECTED**\n\n";
|
|
32
|
+
message += `Your previous session died at ${deathTime}.
|
|
33
|
+
|
|
34
|
+
`;
|
|
35
|
+
if (checkpoint) {
|
|
36
|
+
message += "**Last known state:**\n";
|
|
37
|
+
if (checkpoint.workingOn) {
|
|
38
|
+
message += `- Working on: ${checkpoint.workingOn}
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
if (checkpoint.focus) {
|
|
42
|
+
message += `- Focus: ${checkpoint.focus}
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
if (checkpoint.blocked) {
|
|
46
|
+
message += `- Blocked: ${checkpoint.blocked}
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
message += "\n";
|
|
50
|
+
}
|
|
51
|
+
if (handoffPath) {
|
|
52
|
+
message += `**Last handoff:** ${path.basename(handoffPath)}
|
|
53
|
+
`;
|
|
54
|
+
message += "Review and resume from where you left off.\n";
|
|
55
|
+
} else {
|
|
56
|
+
message += "**No handoff found.** You may have lost context.\n";
|
|
57
|
+
}
|
|
58
|
+
if (clearFlag) {
|
|
59
|
+
await clearDirtyFlag(vaultPath);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
died: true,
|
|
63
|
+
deathTime,
|
|
64
|
+
checkpoint,
|
|
65
|
+
handoffPath,
|
|
66
|
+
handoffContent,
|
|
67
|
+
recoveryMessage: message
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function formatRecoveryInfo(info) {
|
|
71
|
+
if (!info.died) {
|
|
72
|
+
return "\u2713 Clean startup - no context death detected.";
|
|
73
|
+
}
|
|
74
|
+
let output = "\n\u26A0\uFE0F CONTEXT DEATH DETECTED\n";
|
|
75
|
+
output += "\u2550".repeat(40) + "\n\n";
|
|
76
|
+
output += `Death time: ${info.deathTime}
|
|
77
|
+
|
|
78
|
+
`;
|
|
79
|
+
if (info.checkpoint) {
|
|
80
|
+
output += "Last checkpoint:\n";
|
|
81
|
+
if (info.checkpoint.workingOn) {
|
|
82
|
+
output += ` \u2022 Working on: ${info.checkpoint.workingOn}
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
if (info.checkpoint.focus) {
|
|
86
|
+
output += ` \u2022 Focus: ${info.checkpoint.focus}
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
if (info.checkpoint.blocked) {
|
|
90
|
+
output += ` \u2022 Blocked: ${info.checkpoint.blocked}
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
output += "\n";
|
|
94
|
+
}
|
|
95
|
+
if (info.handoffPath) {
|
|
96
|
+
output += `Last handoff: ${path.basename(info.handoffPath)}
|
|
97
|
+
`;
|
|
98
|
+
} else {
|
|
99
|
+
output += "No handoff found - context may be lost.\n";
|
|
100
|
+
}
|
|
101
|
+
output += "\n" + "\u2550".repeat(40) + "\n";
|
|
102
|
+
output += "Run `clawvault recap` to see full context.\n";
|
|
103
|
+
return output;
|
|
104
|
+
}
|
|
105
|
+
export {
|
|
106
|
+
formatRecoveryInfo,
|
|
107
|
+
recover
|
|
108
|
+
};
|