@esparkman/pensieve 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.
- package/README.md +238 -0
- package/dist/__tests__/database.test.d.ts +1 -0
- package/dist/__tests__/database.test.js +210 -0
- package/dist/__tests__/security.test.d.ts +1 -0
- package/dist/__tests__/security.test.js +216 -0
- package/dist/database.d.ts +106 -0
- package/dist/database.js +344 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +524 -0
- package/dist/security.d.ts +20 -0
- package/dist/security.js +79 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { MemoryDatabase } from './database.js';
|
|
6
|
+
import { checkFieldsForSecrets, formatSecretWarning } from './security.js';
|
|
7
|
+
// Initialize database
|
|
8
|
+
const db = new MemoryDatabase();
|
|
9
|
+
// Create MCP server
|
|
10
|
+
const server = new Server({
|
|
11
|
+
name: 'pensieve',
|
|
12
|
+
version: '0.1.0',
|
|
13
|
+
}, {
|
|
14
|
+
capabilities: {
|
|
15
|
+
tools: {},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
// Define tools
|
|
19
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
20
|
+
return {
|
|
21
|
+
tools: [
|
|
22
|
+
{
|
|
23
|
+
name: 'pensieve_remember',
|
|
24
|
+
description: 'Save a decision, preference, discovery, or entity to persistent memory. Use this to record important information that should persist across conversations.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
type: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
enum: ['decision', 'preference', 'discovery', 'entity', 'question'],
|
|
31
|
+
description: 'The type of information to remember'
|
|
32
|
+
},
|
|
33
|
+
// For decisions
|
|
34
|
+
topic: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'Topic of the decision (e.g., "authentication", "styling")'
|
|
37
|
+
},
|
|
38
|
+
decision: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'The decision that was made'
|
|
41
|
+
},
|
|
42
|
+
rationale: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Why this decision was made'
|
|
45
|
+
},
|
|
46
|
+
// For preferences
|
|
47
|
+
category: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Category of preference (e.g., "coding_style", "testing")'
|
|
50
|
+
},
|
|
51
|
+
key: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: 'Preference key'
|
|
54
|
+
},
|
|
55
|
+
value: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
description: 'Preference value'
|
|
58
|
+
},
|
|
59
|
+
// For discoveries
|
|
60
|
+
name: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'Name of the discovered item'
|
|
63
|
+
},
|
|
64
|
+
location: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: 'File path or location'
|
|
67
|
+
},
|
|
68
|
+
description: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Description of the item'
|
|
71
|
+
},
|
|
72
|
+
// For entities
|
|
73
|
+
relationships: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description: 'JSON string of relationships (e.g., {"belongs_to": ["Tenant"], "has_many": ["Orders"]})'
|
|
76
|
+
},
|
|
77
|
+
attributes: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description: 'JSON string of key attributes'
|
|
80
|
+
},
|
|
81
|
+
// For questions
|
|
82
|
+
question: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'The question to record'
|
|
85
|
+
},
|
|
86
|
+
context: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'Context for the question'
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
required: ['type']
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'pensieve_recall',
|
|
96
|
+
description: 'Query the memory database to retrieve past decisions, preferences, discoveries, or entities. Use this to understand prior context.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
query: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
description: 'Search query to find relevant memories'
|
|
103
|
+
},
|
|
104
|
+
type: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
enum: ['all', 'decisions', 'preferences', 'discoveries', 'entities', 'questions', 'session'],
|
|
107
|
+
description: 'Type of memories to search (default: all)'
|
|
108
|
+
},
|
|
109
|
+
category: {
|
|
110
|
+
type: 'string',
|
|
111
|
+
description: 'Filter by category (for preferences or discoveries)'
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'pensieve_session_start',
|
|
118
|
+
description: 'Start a new session and load context from the last session. Call this at the beginning of a conversation to restore prior context.',
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'pensieve_session_end',
|
|
126
|
+
description: 'End the current session and save a summary. Call this before ending a conversation to persist learnings.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
summary: {
|
|
131
|
+
type: 'string',
|
|
132
|
+
description: 'Summary of what was accomplished this session'
|
|
133
|
+
},
|
|
134
|
+
work_in_progress: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
description: 'Description of work that is still in progress'
|
|
137
|
+
},
|
|
138
|
+
next_steps: {
|
|
139
|
+
type: 'string',
|
|
140
|
+
description: 'Planned next steps for the next session'
|
|
141
|
+
},
|
|
142
|
+
key_files: {
|
|
143
|
+
type: 'array',
|
|
144
|
+
items: { type: 'string' },
|
|
145
|
+
description: 'List of key files that were worked on'
|
|
146
|
+
},
|
|
147
|
+
tags: {
|
|
148
|
+
type: 'array',
|
|
149
|
+
items: { type: 'string' },
|
|
150
|
+
description: 'Tags for categorizing this session'
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
required: ['summary']
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'pensieve_resolve_question',
|
|
158
|
+
description: 'Mark an open question as resolved with the resolution.',
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: {
|
|
162
|
+
question_id: {
|
|
163
|
+
type: 'number',
|
|
164
|
+
description: 'ID of the question to resolve'
|
|
165
|
+
},
|
|
166
|
+
resolution: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
description: 'How the question was resolved'
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
required: ['question_id', 'resolution']
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'pensieve_status',
|
|
176
|
+
description: 'Get the current memory status including database location and counts.',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
// Handle tool calls
|
|
186
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
187
|
+
const { name, arguments: args } = request.params;
|
|
188
|
+
try {
|
|
189
|
+
switch (name) {
|
|
190
|
+
case 'pensieve_remember': {
|
|
191
|
+
const { type } = args;
|
|
192
|
+
switch (type) {
|
|
193
|
+
case 'decision': {
|
|
194
|
+
const { topic, decision, rationale } = args;
|
|
195
|
+
if (!topic || !decision) {
|
|
196
|
+
return { content: [{ type: 'text', text: 'Error: topic and decision are required for decisions' }] };
|
|
197
|
+
}
|
|
198
|
+
// Check for secrets
|
|
199
|
+
const secretCheck = checkFieldsForSecrets({ topic, decision, rationale });
|
|
200
|
+
if (secretCheck.containsSecret) {
|
|
201
|
+
return { content: [{ type: 'text', text: formatSecretWarning(secretCheck) }] };
|
|
202
|
+
}
|
|
203
|
+
const id = db.addDecision({ topic, decision, rationale, source: 'user' });
|
|
204
|
+
return {
|
|
205
|
+
content: [{
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: `✓ Remembered decision #${id}:\n Topic: ${topic}\n Decision: ${decision}${rationale ? `\n Rationale: ${rationale}` : ''}`
|
|
208
|
+
}]
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
case 'preference': {
|
|
212
|
+
const { category, key, value } = args;
|
|
213
|
+
if (!category || !key || !value) {
|
|
214
|
+
return { content: [{ type: 'text', text: 'Error: category, key, and value are required for preferences' }] };
|
|
215
|
+
}
|
|
216
|
+
// Check for secrets
|
|
217
|
+
const secretCheck = checkFieldsForSecrets({ category, key, value });
|
|
218
|
+
if (secretCheck.containsSecret) {
|
|
219
|
+
return { content: [{ type: 'text', text: formatSecretWarning(secretCheck) }] };
|
|
220
|
+
}
|
|
221
|
+
db.setPreference({ category, key, value });
|
|
222
|
+
return {
|
|
223
|
+
content: [{
|
|
224
|
+
type: 'text',
|
|
225
|
+
text: `✓ Remembered preference:\n ${category}/${key} = ${value}`
|
|
226
|
+
}]
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
case 'discovery': {
|
|
230
|
+
const { category, name: itemName, location, description } = args;
|
|
231
|
+
if (!category || !itemName) {
|
|
232
|
+
return { content: [{ type: 'text', text: 'Error: category and name are required for discoveries' }] };
|
|
233
|
+
}
|
|
234
|
+
// Check for secrets
|
|
235
|
+
const secretCheck = checkFieldsForSecrets({ category, name: itemName, location, description });
|
|
236
|
+
if (secretCheck.containsSecret) {
|
|
237
|
+
return { content: [{ type: 'text', text: formatSecretWarning(secretCheck) }] };
|
|
238
|
+
}
|
|
239
|
+
const id = db.addDiscovery({ category, name: itemName, location, description });
|
|
240
|
+
return {
|
|
241
|
+
content: [{
|
|
242
|
+
type: 'text',
|
|
243
|
+
text: `✓ Remembered discovery #${id}:\n Category: ${category}\n Name: ${itemName}${location ? `\n Location: ${location}` : ''}${description ? `\n Description: ${description}` : ''}`
|
|
244
|
+
}]
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
case 'entity': {
|
|
248
|
+
const { name: entityName, description, relationships, attributes, location } = args;
|
|
249
|
+
if (!entityName) {
|
|
250
|
+
return { content: [{ type: 'text', text: 'Error: name is required for entities' }] };
|
|
251
|
+
}
|
|
252
|
+
// Check for secrets
|
|
253
|
+
const secretCheck = checkFieldsForSecrets({ name: entityName, description, relationships, attributes, location });
|
|
254
|
+
if (secretCheck.containsSecret) {
|
|
255
|
+
return { content: [{ type: 'text', text: formatSecretWarning(secretCheck) }] };
|
|
256
|
+
}
|
|
257
|
+
db.upsertEntity({ name: entityName, description, relationships, attributes, location });
|
|
258
|
+
return {
|
|
259
|
+
content: [{
|
|
260
|
+
type: 'text',
|
|
261
|
+
text: `✓ Remembered entity: ${entityName}${description ? `\n Description: ${description}` : ''}${relationships ? `\n Relationships: ${relationships}` : ''}`
|
|
262
|
+
}]
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
case 'question': {
|
|
266
|
+
const { question, context } = args;
|
|
267
|
+
if (!question) {
|
|
268
|
+
return { content: [{ type: 'text', text: 'Error: question is required' }] };
|
|
269
|
+
}
|
|
270
|
+
// Check for secrets
|
|
271
|
+
const secretCheck = checkFieldsForSecrets({ question, context });
|
|
272
|
+
if (secretCheck.containsSecret) {
|
|
273
|
+
return { content: [{ type: 'text', text: formatSecretWarning(secretCheck) }] };
|
|
274
|
+
}
|
|
275
|
+
const id = db.addQuestion(question, context);
|
|
276
|
+
return {
|
|
277
|
+
content: [{
|
|
278
|
+
type: 'text',
|
|
279
|
+
text: `✓ Recorded open question #${id}:\n ${question}${context ? `\n Context: ${context}` : ''}`
|
|
280
|
+
}]
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
default:
|
|
284
|
+
return { content: [{ type: 'text', text: `Error: Unknown type "${type}"` }] };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
case 'pensieve_recall': {
|
|
288
|
+
const { query, type = 'all', category } = args;
|
|
289
|
+
let result = '';
|
|
290
|
+
if (type === 'session') {
|
|
291
|
+
const session = db.getLastSession();
|
|
292
|
+
if (session) {
|
|
293
|
+
result = `## Last Session\n`;
|
|
294
|
+
result += `Started: ${session.started_at}\n`;
|
|
295
|
+
result += `Ended: ${session.ended_at || 'In progress'}\n`;
|
|
296
|
+
if (session.summary)
|
|
297
|
+
result += `\n**Summary:** ${session.summary}\n`;
|
|
298
|
+
if (session.work_in_progress)
|
|
299
|
+
result += `\n**Work in Progress:** ${session.work_in_progress}\n`;
|
|
300
|
+
if (session.next_steps)
|
|
301
|
+
result += `\n**Next Steps:** ${session.next_steps}\n`;
|
|
302
|
+
if (session.key_files)
|
|
303
|
+
result += `\n**Key Files:** ${session.key_files}\n`;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
result = 'No previous sessions found.';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else if (type === 'preferences') {
|
|
310
|
+
const prefs = category ? db.getPreferencesByCategory(category) : db.getAllPreferences();
|
|
311
|
+
if (prefs.length > 0) {
|
|
312
|
+
result = `## Preferences${category ? ` (${category})` : ''}\n\n`;
|
|
313
|
+
prefs.forEach(p => {
|
|
314
|
+
result += `- **${p.category}/${p.key}:** ${p.value}${p.notes ? ` (${p.notes})` : ''}\n`;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
result = 'No preferences found.';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
else if (type === 'questions') {
|
|
322
|
+
const questions = db.getOpenQuestions();
|
|
323
|
+
if (questions.length > 0) {
|
|
324
|
+
result = `## Open Questions\n\n`;
|
|
325
|
+
questions.forEach(q => {
|
|
326
|
+
result += `- [#${q.id}] ${q.question}${q.context ? ` (Context: ${q.context})` : ''}\n`;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
result = 'No open questions.';
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else if (type === 'entities') {
|
|
334
|
+
const entities = db.getAllEntities();
|
|
335
|
+
if (entities.length > 0) {
|
|
336
|
+
result = `## Entities\n\n`;
|
|
337
|
+
entities.forEach(e => {
|
|
338
|
+
result += `### ${e.name}\n`;
|
|
339
|
+
if (e.description)
|
|
340
|
+
result += `${e.description}\n`;
|
|
341
|
+
if (e.relationships)
|
|
342
|
+
result += `Relationships: ${e.relationships}\n`;
|
|
343
|
+
if (e.location)
|
|
344
|
+
result += `Location: ${e.location}\n`;
|
|
345
|
+
result += '\n';
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
result = 'No entities found.';
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (query) {
|
|
353
|
+
const searchResults = db.search(query);
|
|
354
|
+
if (searchResults.decisions.length > 0) {
|
|
355
|
+
result += `## Decisions matching "${query}"\n\n`;
|
|
356
|
+
searchResults.decisions.forEach(d => {
|
|
357
|
+
result += `- **${d.topic}:** ${d.decision}${d.rationale ? ` (${d.rationale})` : ''}\n`;
|
|
358
|
+
});
|
|
359
|
+
result += '\n';
|
|
360
|
+
}
|
|
361
|
+
if (searchResults.discoveries.length > 0) {
|
|
362
|
+
result += `## Discoveries matching "${query}"\n\n`;
|
|
363
|
+
searchResults.discoveries.forEach(d => {
|
|
364
|
+
result += `- **${d.name}** [${d.category}]: ${d.description || 'No description'}${d.location ? ` at ${d.location}` : ''}\n`;
|
|
365
|
+
});
|
|
366
|
+
result += '\n';
|
|
367
|
+
}
|
|
368
|
+
if (searchResults.entities.length > 0) {
|
|
369
|
+
result += `## Entities matching "${query}"\n\n`;
|
|
370
|
+
searchResults.entities.forEach(e => {
|
|
371
|
+
result += `- **${e.name}:** ${e.description || 'No description'}\n`;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (!result) {
|
|
375
|
+
result = `No memories found matching "${query}"`;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
// Default: show recent decisions and preferences
|
|
380
|
+
const decisions = db.getRecentDecisions(5);
|
|
381
|
+
const prefs = db.getAllPreferences();
|
|
382
|
+
if (decisions.length > 0) {
|
|
383
|
+
result += `## Recent Decisions\n\n`;
|
|
384
|
+
decisions.forEach(d => {
|
|
385
|
+
result += `- **${d.topic}:** ${d.decision}\n`;
|
|
386
|
+
});
|
|
387
|
+
result += '\n';
|
|
388
|
+
}
|
|
389
|
+
if (prefs.length > 0) {
|
|
390
|
+
result += `## Preferences\n\n`;
|
|
391
|
+
prefs.forEach(p => {
|
|
392
|
+
result += `- **${p.category}/${p.key}:** ${p.value}\n`;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
if (!result) {
|
|
396
|
+
result = 'Memory is empty. Use memory_remember to start saving context.';
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return { content: [{ type: 'text', text: result }] };
|
|
400
|
+
}
|
|
401
|
+
case 'pensieve_session_start': {
|
|
402
|
+
const lastSession = db.getLastSession();
|
|
403
|
+
const currentSession = db.getCurrentSession();
|
|
404
|
+
// Start new session if none is active
|
|
405
|
+
let sessionId;
|
|
406
|
+
if (!currentSession) {
|
|
407
|
+
sessionId = db.startSession();
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
sessionId = currentSession.id;
|
|
411
|
+
}
|
|
412
|
+
let result = `## Session Started (#${sessionId})\n\n`;
|
|
413
|
+
if (lastSession && lastSession.ended_at) {
|
|
414
|
+
result += `### Previous Session\n`;
|
|
415
|
+
result += `- **Date:** ${lastSession.started_at}\n`;
|
|
416
|
+
if (lastSession.summary)
|
|
417
|
+
result += `- **Summary:** ${lastSession.summary}\n`;
|
|
418
|
+
if (lastSession.work_in_progress)
|
|
419
|
+
result += `- **Work in Progress:** ${lastSession.work_in_progress}\n`;
|
|
420
|
+
if (lastSession.next_steps)
|
|
421
|
+
result += `- **Next Steps:** ${lastSession.next_steps}\n`;
|
|
422
|
+
result += '\n';
|
|
423
|
+
}
|
|
424
|
+
const decisions = db.getRecentDecisions(5);
|
|
425
|
+
if (decisions.length > 0) {
|
|
426
|
+
result += `### Key Decisions\n`;
|
|
427
|
+
decisions.forEach(d => {
|
|
428
|
+
result += `- **${d.topic}:** ${d.decision}\n`;
|
|
429
|
+
});
|
|
430
|
+
result += '\n';
|
|
431
|
+
}
|
|
432
|
+
const prefs = db.getAllPreferences();
|
|
433
|
+
if (prefs.length > 0) {
|
|
434
|
+
result += `### Preferences\n`;
|
|
435
|
+
prefs.forEach(p => {
|
|
436
|
+
result += `- **${p.category}/${p.key}:** ${p.value}\n`;
|
|
437
|
+
});
|
|
438
|
+
result += '\n';
|
|
439
|
+
}
|
|
440
|
+
const questions = db.getOpenQuestions();
|
|
441
|
+
if (questions.length > 0) {
|
|
442
|
+
result += `### Open Questions\n`;
|
|
443
|
+
questions.forEach(q => {
|
|
444
|
+
result += `- [#${q.id}] ${q.question}\n`;
|
|
445
|
+
});
|
|
446
|
+
result += '\n';
|
|
447
|
+
}
|
|
448
|
+
result += `---\nMemory database: ${db.getPath()}\n`;
|
|
449
|
+
result += `Ready to continue. What would you like to work on?`;
|
|
450
|
+
return { content: [{ type: 'text', text: result }] };
|
|
451
|
+
}
|
|
452
|
+
case 'pensieve_session_end': {
|
|
453
|
+
const { summary, work_in_progress, next_steps, key_files, tags } = args;
|
|
454
|
+
const currentSession = db.getCurrentSession();
|
|
455
|
+
if (!currentSession) {
|
|
456
|
+
return {
|
|
457
|
+
content: [{
|
|
458
|
+
type: 'text',
|
|
459
|
+
text: 'No active session found. Starting a new one and ending it immediately.'
|
|
460
|
+
}]
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
db.endSession(currentSession.id, summary, work_in_progress, next_steps, key_files, tags);
|
|
464
|
+
let result = `## Session Saved\n\n`;
|
|
465
|
+
result += `**Summary:** ${summary}\n`;
|
|
466
|
+
if (work_in_progress)
|
|
467
|
+
result += `**Work in Progress:** ${work_in_progress}\n`;
|
|
468
|
+
if (next_steps)
|
|
469
|
+
result += `**Next Steps:** ${next_steps}\n`;
|
|
470
|
+
if (key_files?.length)
|
|
471
|
+
result += `**Key Files:** ${key_files.join(', ')}\n`;
|
|
472
|
+
if (tags?.length)
|
|
473
|
+
result += `**Tags:** ${tags.join(', ')}\n`;
|
|
474
|
+
result += `\n---\nSession ended. Your context has been saved for next time.`;
|
|
475
|
+
return { content: [{ type: 'text', text: result }] };
|
|
476
|
+
}
|
|
477
|
+
case 'pensieve_resolve_question': {
|
|
478
|
+
const { question_id, resolution } = args;
|
|
479
|
+
db.resolveQuestion(question_id, resolution);
|
|
480
|
+
return {
|
|
481
|
+
content: [{
|
|
482
|
+
type: 'text',
|
|
483
|
+
text: `✓ Question #${question_id} resolved: ${resolution}`
|
|
484
|
+
}]
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
case 'pensieve_status': {
|
|
488
|
+
const decisions = db.getRecentDecisions(100);
|
|
489
|
+
const prefs = db.getAllPreferences();
|
|
490
|
+
const entities = db.getAllEntities();
|
|
491
|
+
const questions = db.getOpenQuestions();
|
|
492
|
+
const lastSession = db.getLastSession();
|
|
493
|
+
let result = `## Memory Status\n\n`;
|
|
494
|
+
result += `**Database:** ${db.getPath()}\n\n`;
|
|
495
|
+
result += `**Counts:**\n`;
|
|
496
|
+
result += `- Decisions: ${decisions.length}\n`;
|
|
497
|
+
result += `- Preferences: ${prefs.length}\n`;
|
|
498
|
+
result += `- Entities: ${entities.length}\n`;
|
|
499
|
+
result += `- Open Questions: ${questions.length}\n`;
|
|
500
|
+
result += `- Last Session: ${lastSession ? lastSession.started_at : 'None'}\n`;
|
|
501
|
+
return { content: [{ type: 'text', text: result }] };
|
|
502
|
+
}
|
|
503
|
+
default:
|
|
504
|
+
return {
|
|
505
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }]
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
return {
|
|
511
|
+
content: [{
|
|
512
|
+
type: 'text',
|
|
513
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
514
|
+
}]
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
// Start server
|
|
519
|
+
async function main() {
|
|
520
|
+
const transport = new StdioServerTransport();
|
|
521
|
+
await server.connect(transport);
|
|
522
|
+
console.error('Pensieve server running on stdio');
|
|
523
|
+
}
|
|
524
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for Pensieve
|
|
3
|
+
* Detects potential secrets and sensitive data
|
|
4
|
+
*/
|
|
5
|
+
export interface SecretDetectionResult {
|
|
6
|
+
containsSecret: boolean;
|
|
7
|
+
warnings: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Check if text contains potential secrets
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectSecrets(text: string): SecretDetectionResult;
|
|
13
|
+
/**
|
|
14
|
+
* Check multiple fields for secrets
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkFieldsForSecrets(fields: Record<string, string | undefined>): SecretDetectionResult;
|
|
17
|
+
/**
|
|
18
|
+
* Generate warning message for detected secrets
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatSecretWarning(result: SecretDetectionResult): string;
|
package/dist/security.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for Pensieve
|
|
3
|
+
* Detects potential secrets and sensitive data
|
|
4
|
+
*/
|
|
5
|
+
// Patterns that indicate potential secrets
|
|
6
|
+
const SECRET_PATTERNS = [
|
|
7
|
+
// API Keys (generic)
|
|
8
|
+
{ pattern: /\b[A-Za-z0-9_-]{20,}\b.*(?:api[_-]?key|apikey)/i, name: 'API key' },
|
|
9
|
+
{ pattern: /(?:api[_-]?key|apikey).*\b[A-Za-z0-9_-]{20,}\b/i, name: 'API key' },
|
|
10
|
+
// AWS
|
|
11
|
+
{ pattern: /AKIA[0-9A-Z]{16}/i, name: 'AWS Access Key ID' },
|
|
12
|
+
{ pattern: /\b[A-Za-z0-9/+=]{40}\b/i, name: 'Potential AWS Secret Key' },
|
|
13
|
+
// GitHub
|
|
14
|
+
{ pattern: /ghp_[A-Za-z0-9]{36}/i, name: 'GitHub Personal Access Token' },
|
|
15
|
+
{ pattern: /github_pat_[A-Za-z0-9_]{22,}/i, name: 'GitHub Fine-grained PAT' },
|
|
16
|
+
{ pattern: /gho_[A-Za-z0-9]{36}/i, name: 'GitHub OAuth Token' },
|
|
17
|
+
// Stripe
|
|
18
|
+
{ pattern: /sk_live_[A-Za-z0-9]{24,}/i, name: 'Stripe Secret Key' },
|
|
19
|
+
{ pattern: /sk_test_[A-Za-z0-9]{24,}/i, name: 'Stripe Test Key' },
|
|
20
|
+
// Database URLs
|
|
21
|
+
{ pattern: /postgres(?:ql)?:\/\/[^:]+:[^@]+@/i, name: 'PostgreSQL connection string with password' },
|
|
22
|
+
{ pattern: /mysql:\/\/[^:]+:[^@]+@/i, name: 'MySQL connection string with password' },
|
|
23
|
+
{ pattern: /mongodb(?:\+srv)?:\/\/[^:]+:[^@]+@/i, name: 'MongoDB connection string with password' },
|
|
24
|
+
// Generic secrets
|
|
25
|
+
{ pattern: /(?:password|passwd|pwd)\s*[:=]\s*["']?[^\s"']{8,}/i, name: 'Password' },
|
|
26
|
+
{ pattern: /(?:secret|token)\s*[:=]\s*["']?[A-Za-z0-9_-]{16,}/i, name: 'Secret/Token' },
|
|
27
|
+
{ pattern: /bearer\s+[A-Za-z0-9_-]{20,}/i, name: 'Bearer token' },
|
|
28
|
+
// Private keys
|
|
29
|
+
{ pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/i, name: 'Private key' },
|
|
30
|
+
{ pattern: /-----BEGIN\s+OPENSSH\s+PRIVATE\s+KEY-----/i, name: 'SSH Private key' },
|
|
31
|
+
// Credit cards (basic pattern)
|
|
32
|
+
{ pattern: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})\b/, name: 'Credit card number' },
|
|
33
|
+
// SSN
|
|
34
|
+
{ pattern: /\b\d{3}-\d{2}-\d{4}\b/, name: 'Social Security Number' },
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Check if text contains potential secrets
|
|
38
|
+
*/
|
|
39
|
+
export function detectSecrets(text) {
|
|
40
|
+
const warnings = [];
|
|
41
|
+
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
42
|
+
if (pattern.test(text)) {
|
|
43
|
+
warnings.push(`Potential ${name} detected`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
containsSecret: warnings.length > 0,
|
|
48
|
+
warnings
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check multiple fields for secrets
|
|
53
|
+
*/
|
|
54
|
+
export function checkFieldsForSecrets(fields) {
|
|
55
|
+
const allWarnings = [];
|
|
56
|
+
for (const [fieldName, value] of Object.entries(fields)) {
|
|
57
|
+
if (value) {
|
|
58
|
+
const result = detectSecrets(value);
|
|
59
|
+
if (result.containsSecret) {
|
|
60
|
+
allWarnings.push(`In field "${fieldName}": ${result.warnings.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
containsSecret: allWarnings.length > 0,
|
|
66
|
+
warnings: allWarnings
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Generate warning message for detected secrets
|
|
71
|
+
*/
|
|
72
|
+
export function formatSecretWarning(result) {
|
|
73
|
+
if (!result.containsSecret)
|
|
74
|
+
return '';
|
|
75
|
+
return `⚠️ SECURITY WARNING: Potential sensitive data detected!\n` +
|
|
76
|
+
result.warnings.map(w => ` • ${w}`).join('\n') + '\n' +
|
|
77
|
+
`\n Pensieve stores data in plaintext. Do NOT store secrets, API keys,\n` +
|
|
78
|
+
` passwords, or other sensitive credentials. This data was NOT saved.`;
|
|
79
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@esparkman/pensieve",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pensieve - persistent memory for Claude Code. Remember decisions, preferences, and context across sessions.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pensieve": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"prepare": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"claude",
|
|
21
|
+
"pensieve",
|
|
22
|
+
"memory",
|
|
23
|
+
"context",
|
|
24
|
+
"ai"
|
|
25
|
+
],
|
|
26
|
+
"author": "Evan Sparkman",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
30
|
+
"better-sqlite3": "^12.5.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
34
|
+
"@types/node": "^25.0.3",
|
|
35
|
+
"tsx": "^4.21.0",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vitest": "^3.2.4"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
]
|
|
45
|
+
}
|