@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/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;
@@ -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
+ }