clawvault 2.1.2 → 2.2.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/command-registration.test.js +6 -1
- package/bin/help-contract.test.js +2 -0
- package/bin/register-core-commands.js +14 -4
- package/bin/register-maintenance-commands.js +111 -0
- package/bin/register-query-commands.js +32 -1
- package/bin/register-session-lifecycle-commands.js +2 -0
- package/dist/{chunk-5MQB7B37.js → chunk-2HM7ZI4X.js} +268 -434
- package/dist/chunk-73P7XCQM.js +104 -0
- package/dist/{chunk-MIIXBNO3.js → chunk-FDJIZKCW.js} +12 -1
- package/dist/chunk-GJEGPO7U.js +49 -0
- package/dist/chunk-GQVYQCY5.js +396 -0
- package/dist/{chunk-TXO34J3O.js → chunk-H7JW4L7H.js} +1 -1
- package/dist/{chunk-TBVI4N53.js → chunk-I5X6J4FX.js} +120 -95
- package/dist/chunk-K6XHCUFL.js +123 -0
- package/dist/chunk-L6NB43WV.js +472 -0
- package/dist/{chunk-FEQ2CQ3Y.js → chunk-LB6P4CD5.js} +20 -7
- package/dist/chunk-MGDEINGP.js +99 -0
- package/dist/chunk-MQUJNOHK.js +58 -0
- package/dist/{chunk-QFBKWDYR.js → chunk-OTQW3OMC.js} +91 -12
- package/dist/chunk-P5EPF6MB.js +182 -0
- package/dist/chunk-VR5NE7PZ.js +45 -0
- package/dist/{chunk-PIJGYMQZ.js → chunk-W463YRED.js} +1 -1
- package/dist/chunk-WZI3OAE5.js +111 -0
- package/dist/chunk-Z2XBWN7A.js +247 -0
- package/dist/{chunk-O5V7SD5C.js → chunk-ZZA73MFY.js} +1 -1
- package/dist/commands/archive.d.ts +11 -0
- package/dist/commands/archive.js +11 -0
- package/dist/commands/context.d.ts +1 -1
- package/dist/commands/context.js +6 -4
- package/dist/commands/doctor.js +6 -6
- package/dist/commands/graph.js +2 -2
- package/dist/commands/link.js +1 -1
- package/dist/commands/migrate-observations.d.ts +19 -0
- package/dist/commands/migrate-observations.js +13 -0
- package/dist/commands/observe.js +5 -2
- package/dist/commands/rebuild.d.ts +11 -0
- package/dist/commands/rebuild.js +12 -0
- package/dist/commands/reflect.d.ts +11 -0
- package/dist/commands/reflect.js +13 -0
- package/dist/commands/replay.d.ts +16 -0
- package/dist/commands/replay.js +14 -0
- package/dist/commands/setup.js +2 -2
- package/dist/commands/sleep.d.ts +1 -0
- package/dist/commands/sleep.js +29 -6
- package/dist/commands/status.js +6 -6
- package/dist/commands/sync-bd.d.ts +10 -0
- package/dist/commands/sync-bd.js +9 -0
- package/dist/commands/wake.js +53 -35
- package/dist/{context-COo8oq1k.d.ts → context-BUGaWpyL.d.ts} +1 -0
- package/dist/index.d.ts +56 -20
- package/dist/index.js +67 -16
- package/hooks/clawvault/HOOK.md +3 -2
- package/hooks/clawvault/handler.js +51 -0
- package/hooks/clawvault/handler.test.js +20 -0
- package/package.json +2 -2
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Observer
|
|
3
|
+
} from "./chunk-2HM7ZI4X.js";
|
|
4
|
+
import {
|
|
5
|
+
getLegacyObservationPath,
|
|
6
|
+
getObservationPath,
|
|
7
|
+
listRawTranscriptFiles
|
|
8
|
+
} from "./chunk-Z2XBWN7A.js";
|
|
9
|
+
import {
|
|
10
|
+
resolveVaultPath
|
|
11
|
+
} from "./chunk-MXSSG3QU.js";
|
|
12
|
+
|
|
13
|
+
// src/commands/rebuild.ts
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
16
|
+
function parseDateFlag(raw, label) {
|
|
17
|
+
if (!raw) return void 0;
|
|
18
|
+
const trimmed = raw.trim();
|
|
19
|
+
if (!DATE_RE.test(trimmed)) {
|
|
20
|
+
throw new Error(`Invalid ${label} date. Expected YYYY-MM-DD: ${raw}`);
|
|
21
|
+
}
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
function loadRawMessages(rawFilePath) {
|
|
25
|
+
const lines = fs.readFileSync(rawFilePath, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
26
|
+
const messages = [];
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(line);
|
|
30
|
+
if (typeof parsed.message === "string" && parsed.message.trim()) {
|
|
31
|
+
messages.push(parsed.message.trim());
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
messages.push(line);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return messages;
|
|
38
|
+
}
|
|
39
|
+
async function rebuildCommand(options) {
|
|
40
|
+
const vaultPath = resolveVaultPath({ explicitPath: options.vaultPath });
|
|
41
|
+
const fromDate = parseDateFlag(options.from, "from");
|
|
42
|
+
const toDate = parseDateFlag(options.to, "to");
|
|
43
|
+
if (fromDate && toDate && fromDate > toDate) {
|
|
44
|
+
throw new Error(`Invalid range: --from ${fromDate} is after --to ${toDate}.`);
|
|
45
|
+
}
|
|
46
|
+
const rawFiles = listRawTranscriptFiles(vaultPath, {
|
|
47
|
+
fromDate,
|
|
48
|
+
toDate
|
|
49
|
+
});
|
|
50
|
+
if (rawFiles.length === 0) {
|
|
51
|
+
console.log("No raw transcripts found for rebuild range.");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const filesByDate = /* @__PURE__ */ new Map();
|
|
55
|
+
for (const file of rawFiles) {
|
|
56
|
+
const bucket = filesByDate.get(file.date) ?? [];
|
|
57
|
+
bucket.push({ source: file.source, path: file.path });
|
|
58
|
+
filesByDate.set(file.date, bucket);
|
|
59
|
+
}
|
|
60
|
+
const dates = [...filesByDate.keys()].sort((left, right) => left.localeCompare(right));
|
|
61
|
+
let rebuiltDates = 0;
|
|
62
|
+
let processedFiles = 0;
|
|
63
|
+
for (const date of dates) {
|
|
64
|
+
const ledgerObservationPath = getObservationPath(vaultPath, date);
|
|
65
|
+
const legacyObservationPath = getLegacyObservationPath(vaultPath, date);
|
|
66
|
+
fs.rmSync(ledgerObservationPath, { force: true });
|
|
67
|
+
fs.rmSync(legacyObservationPath, { force: true });
|
|
68
|
+
const fixedNow = () => /* @__PURE__ */ new Date(`${date}T12:00:00.000Z`);
|
|
69
|
+
const observer = new Observer(vaultPath, {
|
|
70
|
+
tokenThreshold: 1,
|
|
71
|
+
reflectThreshold: Number.MAX_SAFE_INTEGER,
|
|
72
|
+
now: fixedNow,
|
|
73
|
+
rawCapture: false
|
|
74
|
+
});
|
|
75
|
+
for (const file of filesByDate.get(date) ?? []) {
|
|
76
|
+
const messages = loadRawMessages(file.path);
|
|
77
|
+
if (messages.length === 0) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
await observer.processMessages(messages, {
|
|
81
|
+
source: file.source,
|
|
82
|
+
timestamp: fixedNow()
|
|
83
|
+
});
|
|
84
|
+
processedFiles += 1;
|
|
85
|
+
}
|
|
86
|
+
await observer.flush();
|
|
87
|
+
rebuiltDates += 1;
|
|
88
|
+
}
|
|
89
|
+
console.log(`Rebuild complete: ${rebuiltDates} day(s), ${processedFiles} raw file(s) replayed.`);
|
|
90
|
+
}
|
|
91
|
+
function registerRebuildCommand(program) {
|
|
92
|
+
program.command("rebuild").description("Rebuild compiled observations from raw ledger transcripts").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)").option("-v, --vault <path>", "Vault path").action(async (rawOptions) => {
|
|
93
|
+
await rebuildCommand({
|
|
94
|
+
vaultPath: rawOptions.vault,
|
|
95
|
+
from: rawOptions.from,
|
|
96
|
+
to: rawOptions.to
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
rebuildCommand,
|
|
103
|
+
registerRebuildCommand
|
|
104
|
+
};
|
|
@@ -8,7 +8,14 @@ var DEFAULT_CATEGORIES = [
|
|
|
8
8
|
"goals",
|
|
9
9
|
"transcripts",
|
|
10
10
|
"inbox",
|
|
11
|
-
"templates"
|
|
11
|
+
"templates",
|
|
12
|
+
"lessons",
|
|
13
|
+
"agents",
|
|
14
|
+
"commitments",
|
|
15
|
+
"handoffs",
|
|
16
|
+
"research",
|
|
17
|
+
"tasks",
|
|
18
|
+
"backlog"
|
|
12
19
|
];
|
|
13
20
|
var MEMORY_TYPES = [
|
|
14
21
|
"fact",
|
|
@@ -234,6 +241,10 @@ var SearchEngine = class {
|
|
|
234
241
|
for (const qr of qmdResults) {
|
|
235
242
|
const filePath = this.qmdUriToPath(qr.file);
|
|
236
243
|
const relativePath = this.vaultPath ? path.relative(this.vaultPath, filePath) : filePath;
|
|
244
|
+
const normalizedRelativePath = relativePath.split(path.sep).join("/");
|
|
245
|
+
if (normalizedRelativePath.startsWith("ledger/archive/") || normalizedRelativePath.includes("/ledger/archive/")) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
237
248
|
const docId = relativePath.replace(/\.md$/, "");
|
|
238
249
|
let doc = this.documents.get(docId);
|
|
239
250
|
const modifiedAt = this.resolveModifiedAt(doc, filePath);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runReflection
|
|
3
|
+
} from "./chunk-GQVYQCY5.js";
|
|
4
|
+
import {
|
|
5
|
+
resolveVaultPath
|
|
6
|
+
} from "./chunk-MXSSG3QU.js";
|
|
7
|
+
|
|
8
|
+
// src/commands/reflect.ts
|
|
9
|
+
function parsePositiveInteger(raw, label) {
|
|
10
|
+
const parsed = Number.parseInt(raw, 10);
|
|
11
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
12
|
+
throw new Error(`Invalid ${label}: ${raw}`);
|
|
13
|
+
}
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
async function reflectCommand(options) {
|
|
17
|
+
const vaultPath = resolveVaultPath({ explicitPath: options.vaultPath });
|
|
18
|
+
const result = await runReflection({
|
|
19
|
+
vaultPath,
|
|
20
|
+
days: options.days,
|
|
21
|
+
dryRun: options.dryRun
|
|
22
|
+
});
|
|
23
|
+
if (result.writtenWeeks === 0) {
|
|
24
|
+
console.log("No new reflections promoted.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (result.dryRun) {
|
|
28
|
+
console.log(`Dry run: ${result.writtenWeeks} reflection file(s) would be written.`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(`Reflection complete: ${result.writtenWeeks} week file(s) updated.`);
|
|
32
|
+
if (result.archive) {
|
|
33
|
+
console.log(`Archive pass: ${result.archive.archived} observation file(s) archived.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function registerReflectCommand(program) {
|
|
37
|
+
program.command("reflect").description("Promote stable observation patterns into weekly reflections").option("--days <n>", "Observation window in days (default 14)", "14").option("--dry-run", "Show what would be reflected without writing").option("-v, --vault <path>", "Vault path").action(async (rawOptions) => {
|
|
38
|
+
await reflectCommand({
|
|
39
|
+
vaultPath: rawOptions.vault,
|
|
40
|
+
days: parsePositiveInteger(rawOptions.days, "days"),
|
|
41
|
+
dryRun: rawOptions.dryRun
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
reflectCommand,
|
|
48
|
+
registerReflectCommand
|
|
49
|
+
};
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import {
|
|
2
|
+
archiveObservations
|
|
3
|
+
} from "./chunk-MQUJNOHK.js";
|
|
4
|
+
import {
|
|
5
|
+
normalizeObservationContent,
|
|
6
|
+
parseObservationMarkdown
|
|
7
|
+
} from "./chunk-K6XHCUFL.js";
|
|
8
|
+
import {
|
|
9
|
+
formatIsoWeekKey,
|
|
10
|
+
getIsoWeek,
|
|
11
|
+
getIsoWeekRange,
|
|
12
|
+
getReflectionsRoot,
|
|
13
|
+
listObservationFiles,
|
|
14
|
+
parseDateKey
|
|
15
|
+
} from "./chunk-Z2XBWN7A.js";
|
|
16
|
+
|
|
17
|
+
// src/observer/reflection-service.ts
|
|
18
|
+
import * as fs from "fs";
|
|
19
|
+
import * as path from "path";
|
|
20
|
+
var OPEN_LOOP_RE = /\b(open loop|todo|follow[- ]?up|blocked|pending|unresolved|still need)\b/i;
|
|
21
|
+
var CHANGE_RE = /\b(changed?|shift(?:ed)?|switched|moved|instead|no longer|pivot(?:ed)?)\b/i;
|
|
22
|
+
function normalizeDays(days) {
|
|
23
|
+
if (!Number.isFinite(days)) return 14;
|
|
24
|
+
return Math.max(1, Math.floor(days));
|
|
25
|
+
}
|
|
26
|
+
function shouldIncludeDate(date, fromDate, toDate) {
|
|
27
|
+
if (date < fromDate) return false;
|
|
28
|
+
if (date > toDate) return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
function listReflectionFiles(vaultPath) {
|
|
32
|
+
const reflectionsRoot = getReflectionsRoot(vaultPath);
|
|
33
|
+
if (!fs.existsSync(reflectionsRoot)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return fs.readdirSync(reflectionsRoot, { withFileTypes: true }).filter((entry) => entry.isFile() && /^\d{4}-W\d{2}\.md$/.test(entry.name)).map((entry) => path.join(reflectionsRoot, entry.name)).sort((left, right) => left.localeCompare(right));
|
|
37
|
+
}
|
|
38
|
+
function extractPriorReflectionKeys(vaultPath, currentWeek) {
|
|
39
|
+
const keys = /* @__PURE__ */ new Set();
|
|
40
|
+
for (const filePath of listReflectionFiles(vaultPath)) {
|
|
41
|
+
const weekKey = path.basename(filePath, ".md");
|
|
42
|
+
if (weekKey >= currentWeek) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
46
|
+
for (const line of content.split(/\r?\n/)) {
|
|
47
|
+
const match = line.match(/^- (.+)$/);
|
|
48
|
+
if (!match?.[1]) continue;
|
|
49
|
+
const value = match[1].trim();
|
|
50
|
+
if (value.startsWith("ledger/observations/")) continue;
|
|
51
|
+
keys.add(normalizeObservationContent(value));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return keys;
|
|
55
|
+
}
|
|
56
|
+
function mergeUnique(target, incoming) {
|
|
57
|
+
const seen = new Set(target.map((item) => normalizeObservationContent(item)));
|
|
58
|
+
const merged = [...target];
|
|
59
|
+
for (const item of incoming) {
|
|
60
|
+
const normalized = normalizeObservationContent(item);
|
|
61
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
62
|
+
seen.add(normalized);
|
|
63
|
+
merged.push(item);
|
|
64
|
+
}
|
|
65
|
+
return merged;
|
|
66
|
+
}
|
|
67
|
+
function parseExistingReflectionSections(content) {
|
|
68
|
+
const sections = {
|
|
69
|
+
stablePatterns: [],
|
|
70
|
+
keyDecisions: [],
|
|
71
|
+
openLoops: [],
|
|
72
|
+
changes: [],
|
|
73
|
+
citations: []
|
|
74
|
+
};
|
|
75
|
+
let current = null;
|
|
76
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
77
|
+
const line = rawLine.trim();
|
|
78
|
+
if (line === "## Stable Patterns") {
|
|
79
|
+
current = "stablePatterns";
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (line === "## Key Decisions") {
|
|
83
|
+
current = "keyDecisions";
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (line === "## Open Loops") {
|
|
87
|
+
current = "openLoops";
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (line === "## Changes") {
|
|
91
|
+
current = "changes";
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (line === "## Citations") {
|
|
95
|
+
current = "citations";
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!current) continue;
|
|
99
|
+
const bullet = line.match(/^- (.+)$/);
|
|
100
|
+
if (!bullet?.[1]) continue;
|
|
101
|
+
sections[current].push(bullet[1].trim());
|
|
102
|
+
}
|
|
103
|
+
return sections;
|
|
104
|
+
}
|
|
105
|
+
function classifyItem(item) {
|
|
106
|
+
if (OPEN_LOOP_RE.test(item.content)) {
|
|
107
|
+
return "openLoops";
|
|
108
|
+
}
|
|
109
|
+
if (item.type === "decision" || item.type === "commitment" || item.type === "milestone") {
|
|
110
|
+
return "keyDecisions";
|
|
111
|
+
}
|
|
112
|
+
if (CHANGE_RE.test(item.content)) {
|
|
113
|
+
return "changes";
|
|
114
|
+
}
|
|
115
|
+
return "stablePatterns";
|
|
116
|
+
}
|
|
117
|
+
function toObservationCitationPath(date) {
|
|
118
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
119
|
+
return `ledger/observations/${date}.md`;
|
|
120
|
+
}
|
|
121
|
+
const [year, month, day] = date.split("-");
|
|
122
|
+
return `ledger/observations/${year}/${month}/${day}.md`;
|
|
123
|
+
}
|
|
124
|
+
function buildSectionDraft(promoted) {
|
|
125
|
+
const sections = {
|
|
126
|
+
stablePatterns: [],
|
|
127
|
+
keyDecisions: [],
|
|
128
|
+
openLoops: [],
|
|
129
|
+
changes: [],
|
|
130
|
+
citations: []
|
|
131
|
+
};
|
|
132
|
+
for (const item of promoted) {
|
|
133
|
+
sections[classifyItem(item)].push(
|
|
134
|
+
`[${item.type}|c=${item.confidence.toFixed(2)}|i=${item.importance.toFixed(2)}] ${item.content}`
|
|
135
|
+
);
|
|
136
|
+
for (const date of item.dates) {
|
|
137
|
+
sections.citations.push(toObservationCitationPath(date));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
sections.citations = [...new Set(sections.citations)].sort((left, right) => left.localeCompare(right));
|
|
141
|
+
return sections;
|
|
142
|
+
}
|
|
143
|
+
function formatWeekTitle(weekKey) {
|
|
144
|
+
const [yearRaw, weekRaw] = weekKey.split("-W");
|
|
145
|
+
const year = Number.parseInt(yearRaw, 10);
|
|
146
|
+
const week = Number.parseInt(weekRaw, 10);
|
|
147
|
+
const range = getIsoWeekRange(year, week);
|
|
148
|
+
const monthFormatter = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
|
149
|
+
return `# Week ${week}, ${year} (${monthFormatter.format(range.start)}-${monthFormatter.format(range.end)})`;
|
|
150
|
+
}
|
|
151
|
+
function renderReflectionMarkdown(weekKey, sections) {
|
|
152
|
+
const lines = [];
|
|
153
|
+
lines.push(formatWeekTitle(weekKey));
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push("## Stable Patterns");
|
|
156
|
+
for (const item of sections.stablePatterns) lines.push(`- ${item}`);
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push("## Key Decisions");
|
|
159
|
+
for (const item of sections.keyDecisions) lines.push(`- ${item}`);
|
|
160
|
+
lines.push("");
|
|
161
|
+
lines.push("## Open Loops");
|
|
162
|
+
for (const item of sections.openLoops) lines.push(`- ${item}`);
|
|
163
|
+
lines.push("");
|
|
164
|
+
lines.push("## Changes");
|
|
165
|
+
for (const item of sections.changes) lines.push(`- ${item}`);
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push("## Citations");
|
|
168
|
+
for (const item of sections.citations) lines.push(`- ${item}`);
|
|
169
|
+
lines.push("");
|
|
170
|
+
return lines.join("\n").trim();
|
|
171
|
+
}
|
|
172
|
+
function promoteWeekRecords(records) {
|
|
173
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
174
|
+
for (const record of records) {
|
|
175
|
+
const key = normalizeObservationContent(record.content);
|
|
176
|
+
const existing = grouped.get(key);
|
|
177
|
+
if (!existing) {
|
|
178
|
+
grouped.set(key, {
|
|
179
|
+
key,
|
|
180
|
+
type: record.type,
|
|
181
|
+
confidence: record.confidence,
|
|
182
|
+
importance: record.importance,
|
|
183
|
+
content: record.content,
|
|
184
|
+
dates: /* @__PURE__ */ new Set([record.date])
|
|
185
|
+
});
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
existing.dates.add(record.date);
|
|
189
|
+
if (record.importance > existing.importance) {
|
|
190
|
+
existing.importance = record.importance;
|
|
191
|
+
existing.type = record.type;
|
|
192
|
+
existing.content = record.content;
|
|
193
|
+
}
|
|
194
|
+
if (record.confidence > existing.confidence) {
|
|
195
|
+
existing.confidence = record.confidence;
|
|
196
|
+
}
|
|
197
|
+
grouped.set(key, existing);
|
|
198
|
+
}
|
|
199
|
+
const promoted = [];
|
|
200
|
+
for (const item of grouped.values()) {
|
|
201
|
+
if (item.importance >= 0.8) {
|
|
202
|
+
promoted.push(item);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (item.importance >= 0.4 && item.dates.size >= 2) {
|
|
206
|
+
promoted.push(item);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return promoted;
|
|
210
|
+
}
|
|
211
|
+
function resolveProvider() {
|
|
212
|
+
if (process.env.CLAWVAULT_NO_LLM) return null;
|
|
213
|
+
if (process.env.ANTHROPIC_API_KEY) return "anthropic";
|
|
214
|
+
if (process.env.OPENAI_API_KEY) return "openai";
|
|
215
|
+
if (process.env.GEMINI_API_KEY) return "gemini";
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
async function callOpenAI(prompt, model) {
|
|
219
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
220
|
+
if (!apiKey) return "";
|
|
221
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: {
|
|
224
|
+
"content-type": "application/json",
|
|
225
|
+
authorization: `Bearer ${apiKey}`
|
|
226
|
+
},
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
model: model ?? "gpt-4o-mini",
|
|
229
|
+
temperature: 0.1,
|
|
230
|
+
messages: [{ role: "user", content: prompt }]
|
|
231
|
+
})
|
|
232
|
+
});
|
|
233
|
+
if (!response.ok) return "";
|
|
234
|
+
const payload = await response.json();
|
|
235
|
+
return payload.choices?.[0]?.message?.content?.trim() ?? "";
|
|
236
|
+
}
|
|
237
|
+
async function callAnthropic(prompt, model) {
|
|
238
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
239
|
+
if (!apiKey) return "";
|
|
240
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: {
|
|
243
|
+
"content-type": "application/json",
|
|
244
|
+
"x-api-key": apiKey,
|
|
245
|
+
"anthropic-version": "2023-06-01"
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
model: model ?? "claude-3-5-haiku-latest",
|
|
249
|
+
max_tokens: 1200,
|
|
250
|
+
temperature: 0.1,
|
|
251
|
+
messages: [{ role: "user", content: prompt }]
|
|
252
|
+
})
|
|
253
|
+
});
|
|
254
|
+
if (!response.ok) return "";
|
|
255
|
+
const payload = await response.json();
|
|
256
|
+
return payload.content?.filter((entry) => entry.type === "text" && entry.text).map((entry) => entry.text).join("\n").trim() ?? "";
|
|
257
|
+
}
|
|
258
|
+
async function callGemini(prompt, model) {
|
|
259
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
260
|
+
if (!apiKey) return "";
|
|
261
|
+
const response = await fetch(
|
|
262
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${model ?? "gemini-2.0-flash"}:generateContent?key=${apiKey}`,
|
|
263
|
+
{
|
|
264
|
+
method: "POST",
|
|
265
|
+
headers: { "content-type": "application/json" },
|
|
266
|
+
body: JSON.stringify({
|
|
267
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
268
|
+
generationConfig: { temperature: 0.1, maxOutputTokens: 1200 }
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
if (!response.ok) return "";
|
|
273
|
+
const payload = await response.json();
|
|
274
|
+
return payload.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
|
|
275
|
+
}
|
|
276
|
+
async function maybeGenerateLlmReflection(weekKey, sections) {
|
|
277
|
+
const provider = resolveProvider();
|
|
278
|
+
if (!provider) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const prompt = [
|
|
282
|
+
"Rewrite the weekly reflection draft while preserving section structure and bullets.",
|
|
283
|
+
"Return markdown only using these exact headers:",
|
|
284
|
+
"# Week <N>, <YYYY> (...)",
|
|
285
|
+
"## Stable Patterns",
|
|
286
|
+
"## Key Decisions",
|
|
287
|
+
"## Open Loops",
|
|
288
|
+
"## Changes",
|
|
289
|
+
"## Citations",
|
|
290
|
+
"",
|
|
291
|
+
`Week key: ${weekKey}`,
|
|
292
|
+
"",
|
|
293
|
+
renderReflectionMarkdown(weekKey, sections)
|
|
294
|
+
].join("\n");
|
|
295
|
+
try {
|
|
296
|
+
const output = provider === "anthropic" ? await callAnthropic(prompt) : provider === "gemini" ? await callGemini(prompt) : await callOpenAI(prompt);
|
|
297
|
+
if (!output.trim()) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const cleaned = output.replace(/^```(?:markdown)?\s*/i, "").replace(/\s*```$/, "").trim();
|
|
301
|
+
if (cleaned.includes("## Stable Patterns") && cleaned.includes("## Key Decisions") && cleaned.includes("## Open Loops") && cleaned.includes("## Changes") && cleaned.includes("## Citations")) {
|
|
302
|
+
return cleaned;
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function runReflection(options) {
|
|
310
|
+
const days = normalizeDays(options.days);
|
|
311
|
+
const dryRun = options.dryRun ?? false;
|
|
312
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
313
|
+
const nowDate = now();
|
|
314
|
+
const toDate = nowDate.toISOString().slice(0, 10);
|
|
315
|
+
const fromDateDate = new Date(nowDate);
|
|
316
|
+
fromDateDate.setDate(nowDate.getDate() - (days - 1));
|
|
317
|
+
const fromDate = fromDateDate.toISOString().slice(0, 10);
|
|
318
|
+
const observationFiles = listObservationFiles(options.vaultPath, {
|
|
319
|
+
includeLegacy: true,
|
|
320
|
+
includeArchive: false,
|
|
321
|
+
dedupeByDate: true
|
|
322
|
+
}).filter((entry) => shouldIncludeDate(entry.date, fromDate, toDate));
|
|
323
|
+
const recordsByWeek = /* @__PURE__ */ new Map();
|
|
324
|
+
for (const entry of observationFiles) {
|
|
325
|
+
const parsedDate = parseDateKey(entry.date);
|
|
326
|
+
if (!parsedDate) continue;
|
|
327
|
+
const week = getIsoWeek(parsedDate);
|
|
328
|
+
const weekKey = formatIsoWeekKey(week);
|
|
329
|
+
const markdown = fs.readFileSync(entry.path, "utf-8");
|
|
330
|
+
const parsedRecords = parseObservationMarkdown(markdown);
|
|
331
|
+
const bucket = recordsByWeek.get(weekKey) ?? [];
|
|
332
|
+
for (const record of parsedRecords) {
|
|
333
|
+
bucket.push({
|
|
334
|
+
date: record.date,
|
|
335
|
+
type: record.type,
|
|
336
|
+
confidence: record.confidence,
|
|
337
|
+
importance: record.importance,
|
|
338
|
+
content: record.content
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
recordsByWeek.set(weekKey, bucket);
|
|
342
|
+
}
|
|
343
|
+
const processedWeeks = [...recordsByWeek.keys()].sort((left, right) => left.localeCompare(right));
|
|
344
|
+
const writtenFiles = [];
|
|
345
|
+
for (const weekKey of processedWeeks) {
|
|
346
|
+
const promoted = promoteWeekRecords(recordsByWeek.get(weekKey) ?? []);
|
|
347
|
+
const priorKeys = extractPriorReflectionKeys(options.vaultPath, weekKey);
|
|
348
|
+
const unseenPromoted = promoted.filter((item) => !priorKeys.has(item.key));
|
|
349
|
+
if (unseenPromoted.length === 0) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const reflectionPath = path.join(getReflectionsRoot(options.vaultPath), `${weekKey}.md`);
|
|
353
|
+
const existing = fs.existsSync(reflectionPath) ? fs.readFileSync(reflectionPath, "utf-8") : "";
|
|
354
|
+
const existingSections = existing ? parseExistingReflectionSections(existing) : {
|
|
355
|
+
stablePatterns: [],
|
|
356
|
+
keyDecisions: [],
|
|
357
|
+
openLoops: [],
|
|
358
|
+
changes: [],
|
|
359
|
+
citations: []
|
|
360
|
+
};
|
|
361
|
+
const draftSections = buildSectionDraft(unseenPromoted);
|
|
362
|
+
const mergedSections = {
|
|
363
|
+
stablePatterns: mergeUnique(existingSections.stablePatterns, draftSections.stablePatterns),
|
|
364
|
+
keyDecisions: mergeUnique(existingSections.keyDecisions, draftSections.keyDecisions),
|
|
365
|
+
openLoops: mergeUnique(existingSections.openLoops, draftSections.openLoops),
|
|
366
|
+
changes: mergeUnique(existingSections.changes, draftSections.changes),
|
|
367
|
+
citations: mergeUnique(existingSections.citations, draftSections.citations)
|
|
368
|
+
};
|
|
369
|
+
const llmMarkdown = await maybeGenerateLlmReflection(weekKey, mergedSections);
|
|
370
|
+
const markdown = llmMarkdown ?? renderReflectionMarkdown(weekKey, mergedSections);
|
|
371
|
+
if (dryRun) {
|
|
372
|
+
writtenFiles.push(reflectionPath);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
fs.mkdirSync(path.dirname(reflectionPath), { recursive: true });
|
|
376
|
+
fs.writeFileSync(reflectionPath, `${markdown.trim()}
|
|
377
|
+
`, "utf-8");
|
|
378
|
+
writtenFiles.push(reflectionPath);
|
|
379
|
+
}
|
|
380
|
+
const archive = dryRun ? null : archiveObservations(options.vaultPath, {
|
|
381
|
+
olderThanDays: 14,
|
|
382
|
+
dryRun: false,
|
|
383
|
+
now
|
|
384
|
+
});
|
|
385
|
+
return {
|
|
386
|
+
processedWeeks: processedWeeks.length,
|
|
387
|
+
writtenWeeks: writtenFiles.length,
|
|
388
|
+
dryRun,
|
|
389
|
+
files: writtenFiles,
|
|
390
|
+
archive
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export {
|
|
395
|
+
runReflection
|
|
396
|
+
};
|