claude-mneme 2.9.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/.claude-plugin/plugin.json +17 -0
- package/CLAUDE.md +98 -0
- package/CONFIG_REFERENCE.md +495 -0
- package/README.md +40 -0
- package/commands/entity.md +64 -0
- package/commands/forget.md +69 -0
- package/commands/remember.md +60 -0
- package/commands/status.md +90 -0
- package/commands/summarize.md +69 -0
- package/hooks/hooks.json +123 -0
- package/package.json +12 -0
- package/scripts/mem-add.mjs +59 -0
- package/scripts/mem-entity.mjs +143 -0
- package/scripts/mem-forget.mjs +245 -0
- package/scripts/mem-status.mjs +319 -0
- package/scripts/mem-summarize.mjs +338 -0
- package/scripts/post-compact.mjs +132 -0
- package/scripts/post-tool-use.mjs +353 -0
- package/scripts/pre-compact.mjs +491 -0
- package/scripts/session-start.mjs +283 -0
- package/scripts/session-stop.mjs +31 -0
- package/scripts/stop-capture.mjs +294 -0
- package/scripts/subagent-stop.mjs +203 -0
- package/scripts/summarize.mjs +428 -0
- package/scripts/sync.mjs +609 -0
- package/scripts/user-prompt-submit.mjs +77 -0
- package/scripts/utils.mjs +2142 -0
- package/scripts/utils.test.mjs +1465 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse Hook - Task and Git Commit Capture
|
|
4
|
+
* Captures task context from TodoWrite, TaskCreate, TaskUpdate
|
|
5
|
+
* and commit messages from Bash git commits
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { ensureMemoryDirs, appendLogEntry, withFileLock, logError } from './utils.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read the persisted todo hash from disk (survives across process invocations)
|
|
14
|
+
*/
|
|
15
|
+
function readLastTodoHash(projectDir) {
|
|
16
|
+
const hashPath = join(projectDir, '.last-todo-hash');
|
|
17
|
+
try {
|
|
18
|
+
return existsSync(hashPath) ? readFileSync(hashPath, 'utf-8').trim() : '';
|
|
19
|
+
} catch {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeLastTodoHash(projectDir, hash) {
|
|
25
|
+
try {
|
|
26
|
+
writeFileSync(join(projectDir, '.last-todo-hash'), hash);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
logError(e, 'post-tool-use:writeLastTodoHash');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Read hook input from stdin
|
|
33
|
+
let input = '';
|
|
34
|
+
process.stdin.setEncoding('utf8');
|
|
35
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
36
|
+
process.stdin.on('end', () => {
|
|
37
|
+
try {
|
|
38
|
+
const hookData = JSON.parse(input);
|
|
39
|
+
processToolUse(hookData);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
logError(e, 'post-tool-use');
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract commit message from git commit command
|
|
48
|
+
*/
|
|
49
|
+
function extractCommitMessage(command) {
|
|
50
|
+
if (!command || typeof command !== 'string') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Match various git commit patterns
|
|
55
|
+
// HEREDOC style: git commit -m "$(cat <<'EOF'\nmessage\nEOF\n)"
|
|
56
|
+
// git commit -m "message"
|
|
57
|
+
// git commit -m 'message'
|
|
58
|
+
// git commit -am "message"
|
|
59
|
+
// git commit --message="message"
|
|
60
|
+
|
|
61
|
+
// HEREDOC pattern first — must check before simple -m (which would match the shell wrapper)
|
|
62
|
+
let match = command.match(/<<['"]?EOF['"]?\s*\n\s*([^\n]+)/);
|
|
63
|
+
if (match) {
|
|
64
|
+
return match[1].trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Simple -m flag patterns (also handles combined flags like -am)
|
|
68
|
+
match = command.match(/git\s+commit\s+[^"']*-[a-z]*m\s*["']([^"']+)["']/);
|
|
69
|
+
if (match) {
|
|
70
|
+
return match[1].trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --message= pattern
|
|
74
|
+
match = command.match(/git\s+commit\s+[^"']*--message=["']([^"']+)["']/);
|
|
75
|
+
if (match) {
|
|
76
|
+
return match[1].trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fallback: try to find any quoted string after commit
|
|
80
|
+
match = command.match(/git\s+commit.*["']([^"']{10,})["']/);
|
|
81
|
+
if (match) {
|
|
82
|
+
return match[1].trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Process TodoWrite tool usage
|
|
90
|
+
*/
|
|
91
|
+
function processTodoWrite(hookData) {
|
|
92
|
+
const { tool_input, cwd } = hookData;
|
|
93
|
+
|
|
94
|
+
const todos = tool_input?.todos;
|
|
95
|
+
if (!todos || !Array.isArray(todos) || todos.length === 0) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Extract meaningful task info
|
|
100
|
+
const inProgress = todos.filter(t => t.status === 'in_progress').map(t => t.content);
|
|
101
|
+
const completed = todos.filter(t => t.status === 'completed').map(t => t.content);
|
|
102
|
+
const pending = todos.filter(t => t.status === 'pending').map(t => t.content);
|
|
103
|
+
|
|
104
|
+
// Create a hash to detect significant changes
|
|
105
|
+
const todoHash = JSON.stringify({ inProgress, completed: completed.length, pending: pending.length });
|
|
106
|
+
|
|
107
|
+
// Only log if there's a meaningful change (new in_progress task or completions)
|
|
108
|
+
const paths = ensureMemoryDirs(cwd || process.cwd());
|
|
109
|
+
if (todoHash === readLastTodoHash(paths.project)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
writeLastTodoHash(paths.project, todoHash);
|
|
113
|
+
|
|
114
|
+
// Build content focusing on what's being worked on
|
|
115
|
+
const parts = [];
|
|
116
|
+
if (inProgress.length > 0) {
|
|
117
|
+
parts.push(`Working on: ${inProgress.join(', ')}`);
|
|
118
|
+
}
|
|
119
|
+
if (completed.length > 0 && pending.length > 0) {
|
|
120
|
+
parts.push(`(${completed.length} done, ${pending.length} remaining)`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (parts.length === 0) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const entry = {
|
|
128
|
+
ts: new Date().toISOString(),
|
|
129
|
+
type: 'task',
|
|
130
|
+
content: parts.join(' ')
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
appendLogEntry(entry, cwd || process.cwd());
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Read the task subject tracking file
|
|
139
|
+
* Enhanced to track full task lifecycle for outcome tracking
|
|
140
|
+
*/
|
|
141
|
+
function readTaskTracking(projectDir) {
|
|
142
|
+
const trackingPath = join(projectDir, 'active-tasks.json');
|
|
143
|
+
if (existsSync(trackingPath)) {
|
|
144
|
+
try {
|
|
145
|
+
const data = JSON.parse(readFileSync(trackingPath, 'utf-8'));
|
|
146
|
+
// Migrate old format (simple subjects) to new format (task objects)
|
|
147
|
+
if (data.subjects && typeof Object.values(data.subjects)[0] === 'string') {
|
|
148
|
+
const migrated = { nextId: data.nextId, tasks: {} };
|
|
149
|
+
for (const [id, subject] of Object.entries(data.subjects)) {
|
|
150
|
+
migrated.tasks[id] = { subject, status: 'pending', createdAt: null };
|
|
151
|
+
}
|
|
152
|
+
return migrated;
|
|
153
|
+
}
|
|
154
|
+
return data;
|
|
155
|
+
} catch {
|
|
156
|
+
return { nextId: 1, tasks: {} };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { nextId: 1, tasks: {} };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Write the task subject tracking file
|
|
164
|
+
*/
|
|
165
|
+
function writeTaskTracking(projectDir, tracking) {
|
|
166
|
+
const trackingPath = join(projectDir, 'active-tasks.json');
|
|
167
|
+
writeFileSync(trackingPath, JSON.stringify(tracking));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Process TaskCreate tool usage
|
|
172
|
+
* Stores the task with metadata for outcome tracking
|
|
173
|
+
*/
|
|
174
|
+
function processTaskCreate(hookData) {
|
|
175
|
+
const { tool_input, cwd } = hookData;
|
|
176
|
+
|
|
177
|
+
const { subject, description } = tool_input || {};
|
|
178
|
+
if (!subject) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const paths = ensureMemoryDirs(cwd || process.cwd());
|
|
183
|
+
const taskLockPath = join(paths.project, 'active-tasks.json.lock');
|
|
184
|
+
const result = withFileLock(taskLockPath, () => {
|
|
185
|
+
const tracking = readTaskTracking(paths.project);
|
|
186
|
+
|
|
187
|
+
tracking.tasks = tracking.tasks || {};
|
|
188
|
+
const task = {
|
|
189
|
+
subject,
|
|
190
|
+
status: 'pending',
|
|
191
|
+
createdAt: new Date().toISOString()
|
|
192
|
+
};
|
|
193
|
+
if (description) task.description = description;
|
|
194
|
+
tracking.tasks[String(tracking.nextId)] = task;
|
|
195
|
+
tracking.nextId++;
|
|
196
|
+
|
|
197
|
+
writeTaskTracking(paths.project, tracking);
|
|
198
|
+
return true;
|
|
199
|
+
}, 5);
|
|
200
|
+
return result === true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Process TaskUpdate tool usage
|
|
205
|
+
* Tracks task lifecycle and logs meaningful state changes with outcomes
|
|
206
|
+
*/
|
|
207
|
+
function processTaskUpdate(hookData) {
|
|
208
|
+
const { tool_input, cwd } = hookData;
|
|
209
|
+
|
|
210
|
+
const { taskId, status, subject } = tool_input || {};
|
|
211
|
+
if (!taskId) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const effectiveCwd = cwd || process.cwd();
|
|
216
|
+
const paths = ensureMemoryDirs(effectiveCwd);
|
|
217
|
+
const taskLockPath = join(paths.project, 'active-tasks.json.lock');
|
|
218
|
+
|
|
219
|
+
// Collect log entries to append outside the lock (appendLogEntry has its own locking)
|
|
220
|
+
let logEntry = null;
|
|
221
|
+
|
|
222
|
+
const result = withFileLock(taskLockPath, () => {
|
|
223
|
+
const tracking = readTaskTracking(paths.project);
|
|
224
|
+
tracking.tasks = tracking.tasks || {};
|
|
225
|
+
|
|
226
|
+
const task = tracking.tasks[String(taskId)];
|
|
227
|
+
|
|
228
|
+
// Handle deleted tasks - log as abandoned if was in progress
|
|
229
|
+
if (status === 'deleted') {
|
|
230
|
+
if (task && task.status === 'in_progress') {
|
|
231
|
+
logEntry = {
|
|
232
|
+
ts: new Date().toISOString(),
|
|
233
|
+
type: 'task',
|
|
234
|
+
action: 'abandoned',
|
|
235
|
+
outcome: 'abandoned',
|
|
236
|
+
subject: task.subject,
|
|
237
|
+
duration: task.createdAt ? Date.now() - new Date(task.createdAt).getTime() : null
|
|
238
|
+
};
|
|
239
|
+
if (task.description) logEntry.description = task.description;
|
|
240
|
+
}
|
|
241
|
+
delete tracking.tasks[String(taskId)];
|
|
242
|
+
writeTaskTracking(paths.project, tracking);
|
|
243
|
+
return task?.status === 'in_progress';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Update task status
|
|
247
|
+
if (task) {
|
|
248
|
+
task.status = status;
|
|
249
|
+
|
|
250
|
+
if (status === 'in_progress' && !task.startedAt) {
|
|
251
|
+
task.startedAt = new Date().toISOString();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (status === 'completed') {
|
|
255
|
+
const resolvedSubject = subject || task.subject;
|
|
256
|
+
if (!resolvedSubject) return false;
|
|
257
|
+
|
|
258
|
+
logEntry = {
|
|
259
|
+
ts: new Date().toISOString(),
|
|
260
|
+
type: 'task',
|
|
261
|
+
action: 'completed',
|
|
262
|
+
outcome: 'completed',
|
|
263
|
+
subject: resolvedSubject,
|
|
264
|
+
duration: task.startedAt ? Date.now() - new Date(task.startedAt).getTime() : null
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
delete tracking.tasks[String(taskId)];
|
|
268
|
+
writeTaskTracking(paths.project, tracking);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
writeTaskTracking(paths.project, tracking);
|
|
273
|
+
} else {
|
|
274
|
+
tracking.tasks[String(taskId)] = {
|
|
275
|
+
subject: subject || `Task ${taskId}`,
|
|
276
|
+
status: status || 'pending',
|
|
277
|
+
createdAt: new Date().toISOString(),
|
|
278
|
+
startedAt: status === 'in_progress' ? new Date().toISOString() : null
|
|
279
|
+
};
|
|
280
|
+
writeTaskTracking(paths.project, tracking);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return false;
|
|
284
|
+
}, 5);
|
|
285
|
+
|
|
286
|
+
// Append log entry outside the task lock to avoid nested lock contention
|
|
287
|
+
if (logEntry) {
|
|
288
|
+
appendLogEntry(logEntry, effectiveCwd);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return result === true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Process Bash tool usage - only capture git commits
|
|
296
|
+
*/
|
|
297
|
+
function processBash(hookData) {
|
|
298
|
+
const { tool_input, cwd } = hookData;
|
|
299
|
+
|
|
300
|
+
const command = tool_input?.command;
|
|
301
|
+
if (!command || typeof command !== 'string') {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Only capture git commit commands
|
|
306
|
+
if (!command.includes('git') || !command.includes('commit')) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// More specific check - must be a git commit command
|
|
311
|
+
if (!/git\s+commit/.test(command)) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const commitMessage = extractCommitMessage(command);
|
|
316
|
+
if (!commitMessage) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Truncate very long commit messages
|
|
321
|
+
let message = commitMessage;
|
|
322
|
+
if (message.length > 200) {
|
|
323
|
+
message = message.substring(0, 200) + '...';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const entry = {
|
|
327
|
+
ts: new Date().toISOString(),
|
|
328
|
+
type: 'commit',
|
|
329
|
+
content: message
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
appendLogEntry(entry, cwd || process.cwd());
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function processToolUse(hookData) {
|
|
337
|
+
const { tool_name } = hookData;
|
|
338
|
+
|
|
339
|
+
if (tool_name === 'TodoWrite') {
|
|
340
|
+
processTodoWrite(hookData);
|
|
341
|
+
} else if (tool_name === 'TaskCreate') {
|
|
342
|
+
processTaskCreate(hookData);
|
|
343
|
+
} else if (tool_name === 'TaskUpdate') {
|
|
344
|
+
processTaskUpdate(hookData);
|
|
345
|
+
} else if (tool_name === 'Bash') {
|
|
346
|
+
processBash(hookData);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
process.exit(0);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Timeout fallback
|
|
353
|
+
setTimeout(() => process.exit(0), 5000);
|