clawmem 0.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.
Files changed (50) hide show
  1. package/AGENTS.md +660 -0
  2. package/CLAUDE.md +660 -0
  3. package/LICENSE +21 -0
  4. package/README.md +993 -0
  5. package/SKILL.md +717 -0
  6. package/bin/clawmem +75 -0
  7. package/package.json +72 -0
  8. package/src/amem.ts +797 -0
  9. package/src/beads.ts +263 -0
  10. package/src/clawmem.ts +1849 -0
  11. package/src/collections.ts +405 -0
  12. package/src/config.ts +178 -0
  13. package/src/consolidation.ts +123 -0
  14. package/src/directory-context.ts +248 -0
  15. package/src/errors.ts +41 -0
  16. package/src/formatter.ts +427 -0
  17. package/src/graph-traversal.ts +247 -0
  18. package/src/hooks/context-surfacing.ts +317 -0
  19. package/src/hooks/curator-nudge.ts +89 -0
  20. package/src/hooks/decision-extractor.ts +639 -0
  21. package/src/hooks/feedback-loop.ts +214 -0
  22. package/src/hooks/handoff-generator.ts +345 -0
  23. package/src/hooks/postcompact-inject.ts +226 -0
  24. package/src/hooks/precompact-extract.ts +314 -0
  25. package/src/hooks/pretool-inject.ts +79 -0
  26. package/src/hooks/session-bootstrap.ts +324 -0
  27. package/src/hooks/staleness-check.ts +130 -0
  28. package/src/hooks.ts +367 -0
  29. package/src/indexer.ts +327 -0
  30. package/src/intent.ts +294 -0
  31. package/src/limits.ts +26 -0
  32. package/src/llm.ts +1175 -0
  33. package/src/mcp.ts +2138 -0
  34. package/src/memory.ts +336 -0
  35. package/src/mmr.ts +93 -0
  36. package/src/observer.ts +269 -0
  37. package/src/openclaw/engine.ts +283 -0
  38. package/src/openclaw/index.ts +221 -0
  39. package/src/openclaw/plugin.json +83 -0
  40. package/src/openclaw/shell.ts +207 -0
  41. package/src/openclaw/tools.ts +304 -0
  42. package/src/profile.ts +346 -0
  43. package/src/promptguard.ts +218 -0
  44. package/src/retrieval-gate.ts +106 -0
  45. package/src/search-utils.ts +127 -0
  46. package/src/server.ts +783 -0
  47. package/src/splitter.ts +325 -0
  48. package/src/store.ts +4062 -0
  49. package/src/validation.ts +67 -0
  50. package/src/watcher.ts +58 -0
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Per-Folder CLAUDE.md Generation
3
+ *
4
+ * Automatically maintains CLAUDE.md files in project directories with
5
+ * relevant decisions and recent activity extracted from ClawMem.
6
+ * Opt-in via `directoryContext: true` in ~/.config/clawmem/index.yml.
7
+ */
8
+
9
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
10
+ import { dirname, join, resolve } from "path";
11
+ import type { Store, DocumentRow, SessionRecord } from "./store.ts";
12
+ import { listCollections } from "./collections.ts";
13
+
14
+ // =============================================================================
15
+ // Config
16
+ // =============================================================================
17
+
18
+ const MARKER = "<!-- ClawMem Auto-Generated Context — do not edit below this line -->";
19
+ const MAX_DECISIONS_PER_DIR = 5;
20
+ const MAX_SESSIONS_PER_DIR = 3;
21
+ const DECISION_LOOKBACK_DAYS = 30;
22
+ const EXCLUDED_PATHS = ["_clawmem/", "_PRIVATE/", "node_modules/", ".git/"];
23
+
24
+ // =============================================================================
25
+ // Public API
26
+ // =============================================================================
27
+
28
+ /**
29
+ * Update CLAUDE.md files for directories containing the given touched paths.
30
+ * Returns the number of directories updated.
31
+ */
32
+ export function updateDirectoryContext(
33
+ store: Store,
34
+ touchedPaths: string[]
35
+ ): number {
36
+ if (touchedPaths.length === 0) return 0;
37
+
38
+ const collections = listCollections();
39
+ if (collections.length === 0) return 0;
40
+
41
+ // Group touched paths by directory
42
+ const dirs = new Set<string>();
43
+ for (const filePath of touchedPaths) {
44
+ const dir = dirname(filePath);
45
+ if (dir && dir !== "." && !isExcluded(dir)) {
46
+ dirs.add(dir);
47
+ }
48
+ }
49
+
50
+ if (dirs.size === 0) return 0;
51
+
52
+ // For each directory, check if it's within a collection
53
+ let updatedCount = 0;
54
+ for (const dir of dirs) {
55
+ const absDir = resolve(dir);
56
+
57
+ // Find the collection this directory belongs to (with path boundary check)
58
+ const col = collections.find(c => {
59
+ const colPath = resolve(c.path);
60
+ return absDir === colPath || absDir.startsWith(colPath + "/");
61
+ });
62
+ if (!col) continue;
63
+
64
+ // Get decisions mentioning files in this directory
65
+ const decisions = getDecisionsForDirectory(store, dir);
66
+
67
+ // Get recent sessions touching this directory
68
+ const sessions = getSessionsForDirectory(store, dir);
69
+
70
+ if (decisions.length === 0 && sessions.length === 0) continue;
71
+
72
+ // Generate the context block
73
+ const block = generateDirectoryBlock(decisions, sessions, dir);
74
+ if (!block) continue;
75
+
76
+ // Write to CLAUDE.md
77
+ const claudeMdPath = join(absDir, "CLAUDE.md");
78
+ writeClaudeMd(claudeMdPath, block);
79
+ updatedCount++;
80
+ }
81
+
82
+ return updatedCount;
83
+ }
84
+
85
+ /**
86
+ * Regenerate CLAUDE.md for all directories that have relevant context.
87
+ * Used by `clawmem update-context` CLI command.
88
+ */
89
+ export function regenerateAllDirectoryContexts(store: Store): number {
90
+ const collections = listCollections();
91
+ if (collections.length === 0) return 0;
92
+
93
+ // Get all directories from active documents
94
+ const allDirs = new Set<string>();
95
+ for (const col of collections) {
96
+ const paths = store.getActiveDocumentPaths(col.name);
97
+ for (const p of paths) {
98
+ const dir = dirname(p);
99
+ if (dir && dir !== "." && !isExcluded(dir)) {
100
+ allDirs.add(join(col.path, dir));
101
+ }
102
+ }
103
+ }
104
+
105
+ let updatedCount = 0;
106
+ for (const absDir of allDirs) {
107
+ const decisions = getDecisionsForDirectory(store, absDir);
108
+ const sessions = getSessionsForDirectory(store, absDir);
109
+
110
+ if (decisions.length === 0 && sessions.length === 0) continue;
111
+
112
+ const block = generateDirectoryBlock(decisions, sessions, absDir);
113
+ if (!block) continue;
114
+
115
+ const claudeMdPath = join(absDir, "CLAUDE.md");
116
+ writeClaudeMd(claudeMdPath, block);
117
+ updatedCount++;
118
+ }
119
+
120
+ return updatedCount;
121
+ }
122
+
123
+ // =============================================================================
124
+ // Context Generation
125
+ // =============================================================================
126
+
127
+ export function generateDirectoryBlock(
128
+ decisions: DocumentRow[],
129
+ sessions: SessionRecord[],
130
+ dirPath: string
131
+ ): string | null {
132
+ const lines: string[] = [];
133
+
134
+ if (decisions.length > 0) {
135
+ lines.push("## Decisions");
136
+ lines.push("");
137
+ for (const d of decisions.slice(0, MAX_DECISIONS_PER_DIR)) {
138
+ lines.push(`- **${d.title}** (${d.modifiedAt.slice(0, 10)})`);
139
+ }
140
+ lines.push("");
141
+ }
142
+
143
+ if (sessions.length > 0) {
144
+ lines.push("## Recent Activity");
145
+ lines.push("");
146
+ for (const s of sessions.slice(0, MAX_SESSIONS_PER_DIR)) {
147
+ const date = (s.endedAt || s.startedAt).slice(0, 10);
148
+ const summary = s.summary || "Session activity";
149
+ lines.push(`- ${date}: ${summary}`);
150
+ }
151
+ lines.push("");
152
+ }
153
+
154
+ return lines.length > 0 ? lines.join("\n") : null;
155
+ }
156
+
157
+ // =============================================================================
158
+ // Data Retrieval
159
+ // =============================================================================
160
+
161
+ function getDecisionsForDirectory(store: Store, dirPath: string): DocumentRow[] {
162
+ const cutoff = new Date();
163
+ cutoff.setDate(cutoff.getDate() - DECISION_LOOKBACK_DAYS);
164
+ const cutoffStr = cutoff.toISOString();
165
+
166
+ const allDecisions = store.getDocumentsByType("decision", 50);
167
+ const results: DocumentRow[] = [];
168
+
169
+ for (const d of allDecisions) {
170
+ if (d.modifiedAt < cutoffStr) continue;
171
+
172
+ // Check if any files_modified in this decision are in the target directory
173
+ // files_modified is stored as JSON array in the observation columns
174
+ const body = store.getDocumentBody({
175
+ filepath: `${d.collection}/${d.path}`,
176
+ displayPath: `${d.collection}/${d.path}`,
177
+ } as any);
178
+
179
+ if (!body) continue;
180
+
181
+ // Check if the decision body mentions any files in this directory
182
+ const normalizedDir = dirPath.replace(/\/$/, "");
183
+ if (body.includes(normalizedDir) || body.includes(dirname(normalizedDir))) {
184
+ results.push(d);
185
+ if (results.length >= MAX_DECISIONS_PER_DIR) break;
186
+ }
187
+ }
188
+
189
+ return results;
190
+ }
191
+
192
+ function getSessionsForDirectory(store: Store, dirPath: string): SessionRecord[] {
193
+ const sessions = store.getRecentSessions(10);
194
+ const results: SessionRecord[] = [];
195
+
196
+ const normalizedDir = dirPath.replace(/\/$/, "");
197
+
198
+ for (const s of sessions) {
199
+ if (s.filesChanged.length === 0) continue;
200
+
201
+ // Check if any changed files are in this directory (with path boundary)
202
+ const resolvedDir = resolve(normalizedDir);
203
+ const hasMatch = s.filesChanged.some(f => {
204
+ const resolvedFile = resolve(f);
205
+ return resolvedFile.startsWith(resolvedDir + "/") || dirname(resolvedFile) === resolvedDir;
206
+ });
207
+
208
+ if (hasMatch) {
209
+ results.push(s);
210
+ if (results.length >= MAX_SESSIONS_PER_DIR) break;
211
+ }
212
+ }
213
+
214
+ return results;
215
+ }
216
+
217
+ // =============================================================================
218
+ // File I/O
219
+ // =============================================================================
220
+
221
+ function writeClaudeMd(filePath: string, generatedBlock: string): void {
222
+ const dir = dirname(filePath);
223
+ if (!existsSync(dir)) return; // Don't create directories
224
+
225
+ let existing = "";
226
+ if (existsSync(filePath)) {
227
+ existing = readFileSync(filePath, "utf-8");
228
+ }
229
+
230
+ const markerIdx = existing.indexOf(MARKER);
231
+ const userContent = markerIdx >= 0
232
+ ? existing.slice(0, markerIdx).trimEnd()
233
+ : existing.trimEnd();
234
+
235
+ const output = userContent
236
+ ? `${userContent}\n\n${MARKER}\n\n${generatedBlock}`
237
+ : `${MARKER}\n\n${generatedBlock}`;
238
+
239
+ writeFileSync(filePath, output, "utf-8");
240
+ }
241
+
242
+ // =============================================================================
243
+ // Helpers
244
+ // =============================================================================
245
+
246
+ function isExcluded(path: string): boolean {
247
+ return EXCLUDED_PATHS.some(p => path.includes(p));
248
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Standardized error types for ClawMem.
3
+ */
4
+
5
+ export class ClawMemError extends Error {
6
+ code: string;
7
+ details?: Record<string, unknown>;
8
+
9
+ constructor(code: string, message: string, details?: Record<string, unknown>, cause?: Error) {
10
+ super(message);
11
+ this.name = "ClawMemError";
12
+ this.code = code;
13
+ this.details = details;
14
+ if (cause) this.cause = cause;
15
+ }
16
+
17
+ toJSON(): { ok: false; error: { code: string; message: string; details?: Record<string, unknown> } } {
18
+ return {
19
+ ok: false,
20
+ error: {
21
+ code: this.code,
22
+ message: this.message,
23
+ ...(this.details ? { details: this.details } : {}),
24
+ },
25
+ };
26
+ }
27
+ }
28
+
29
+ /** Format any error into a user-facing message (no stack traces). */
30
+ export function toUserError(err: unknown): string {
31
+ if (err instanceof ClawMemError) return `[${err.code}] ${err.message}`;
32
+ if (err instanceof Error) return err.message;
33
+ return String(err);
34
+ }
35
+
36
+ /** Format any error into structured JSON for hooks/MCP boundaries. */
37
+ export function toErrorResponse(err: unknown): { ok: false; error: { code: string; message: string } } {
38
+ if (err instanceof ClawMemError) return err.toJSON();
39
+ const message = err instanceof Error ? err.message : String(err);
40
+ return { ok: false, error: { code: "INTERNAL_ERROR", message } };
41
+ }