@postnesia/mcp 0.1.4 → 0.1.6
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 +37 -91
- package/dist/index.d.ts +2 -2
- package/dist/index.js +243 -482
- package/package.json +6 -13
- package/dist/access.d.ts +0 -22
- package/dist/access.js +0 -63
- package/dist/importance.d.ts +0 -44
- package/dist/importance.js +0 -145
package/README.md
CHANGED
|
@@ -1,117 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @postnesia/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MCP server for the Postnesia memory system. Exposes all memory, journal, and task operations as Model Context Protocol tools over stdio.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Usage
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
cd postnesia
|
|
9
|
-
npm install
|
|
10
|
-
npm run db:generate
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Configuration
|
|
14
|
-
|
|
15
|
-
Add to your MCP settings (e.g., Claude Desktop config):
|
|
7
|
+
Add to your MCP config (e.g. `.claude/settings.json` or Claude Desktop):
|
|
16
8
|
|
|
17
9
|
```json
|
|
18
10
|
{
|
|
19
11
|
"mcpServers": {
|
|
20
12
|
"postnesia": {
|
|
21
|
-
"command": "
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["postnesia-mcp"],
|
|
22
15
|
"env": {
|
|
23
|
-
"DATABASE_URL": "file
|
|
24
|
-
"GEMINI_API_KEY": "
|
|
16
|
+
"DATABASE_URL": "file:///absolute/path/to/memory.db",
|
|
17
|
+
"GEMINI_API_KEY": "your-gemini-api-key",
|
|
18
|
+
"EMBEDDING_MODEL": "gemini-embedding-001",
|
|
19
|
+
"EMBEDDING_DIMENSIONS": "768"
|
|
25
20
|
}
|
|
26
21
|
}
|
|
27
22
|
}
|
|
28
23
|
}
|
|
29
24
|
```
|
|
30
25
|
|
|
31
|
-
|
|
26
|
+
`DATABASE_URL` must be an absolute `file://` URL.
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
"mcpServers": {
|
|
36
|
-
"openmind": {
|
|
37
|
-
"command": "/absolute/path/to/tsx",
|
|
38
|
-
"env": {
|
|
39
|
-
"DATABASE_URL": "/absolute/path/to/memory.db",
|
|
40
|
-
"GEMINI_API_KEY": "token-for-gemini-embedding-model-api"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Available Tools
|
|
48
|
-
|
|
49
|
-
### memory_search
|
|
50
|
-
Semantic search across memories.
|
|
51
|
-
- `query` (required): Search text
|
|
52
|
-
- `maxResults` (optional): Max results (default: 10)
|
|
53
|
-
- `minScore` (optional): Min similarity 0-1 (default: 0.3)
|
|
54
|
-
|
|
55
|
-
### memory_add
|
|
56
|
-
Create a new memory.
|
|
57
|
-
- `content` (required): Full memory text
|
|
58
|
-
- `contentL1` (optional): Compressed form
|
|
59
|
-
- `type` (required): event|decision|lesson|preference|person|technical
|
|
60
|
-
- `importance` (required): 1-5
|
|
61
|
-
- `tags` (required): Array of tags
|
|
62
|
-
- `context` (optional): Creation context
|
|
63
|
-
|
|
64
|
-
### memory_recent
|
|
65
|
-
Get recent memories.
|
|
66
|
-
- `hours` (optional): Hours to look back (default: 24)
|
|
67
|
-
- `limit` (optional): Max results (default: 20)
|
|
28
|
+
## Tools
|
|
68
29
|
|
|
69
|
-
###
|
|
70
|
-
Get contextually related memories.
|
|
71
|
-
- `query` (required): Context query
|
|
72
|
-
- `maxResults` (optional): Max results (default: 5)
|
|
30
|
+
### Memory
|
|
73
31
|
|
|
74
|
-
|
|
75
|
-
|
|
32
|
+
| Tool | Required | Optional |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `memory_search` | `query` | `limit` (default: 10) |
|
|
35
|
+
| `memory_add` | `content`, `type`, `importance`, `tags` | `contentL1`, `context`, `core` |
|
|
36
|
+
| `memory_update_core` | `memoryId`, `content`, `contentL1` | — |
|
|
37
|
+
| `memory_recent` | — | `hours` (default: 24), `limit` (default: 20) |
|
|
38
|
+
| `memory_stats` | — | — |
|
|
39
|
+
| `memory_consolidate` | — | — |
|
|
40
|
+
| `memory_relationships` | `memoryId` | — |
|
|
76
41
|
|
|
77
|
-
|
|
78
|
-
Run consolidation cycle (decay + boost). No parameters - always applies changes.
|
|
42
|
+
**Memory types:** `event` `decision` `lesson` `preference` `person` `technical`
|
|
79
43
|
|
|
80
|
-
###
|
|
81
|
-
Add daily journal entry.
|
|
82
|
-
- `date` (required): YYYY-MM-DD
|
|
83
|
-
- `content` (required): Full narrative
|
|
84
|
-
- `learned` (optional): What I learned
|
|
85
|
-
- `learnedAboutRye` (optional): What I learned about Rye
|
|
86
|
-
- `keyMoments` (optional): Key moments
|
|
87
|
-
- `mood` (optional): Mood/feeling
|
|
44
|
+
### Journal
|
|
88
45
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
46
|
+
| Tool | Required | Optional |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `journal_add` | `date` (YYYY-MM-DD), `content` | `learned`, `learnedAboutRye`, `keyMoments`, `mood` |
|
|
49
|
+
| `journal_recent` | — | `days` (default: 7) |
|
|
92
50
|
|
|
93
|
-
###
|
|
94
|
-
View relationship graph for a memory.
|
|
95
|
-
- `memoryId` (required): Memory ID to explore
|
|
51
|
+
### Tasks
|
|
96
52
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Test the server manually:
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
npm run mcp
|
|
107
|
-
```
|
|
53
|
+
| Tool | Required | Optional |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `task_create` | `title` | `description`, `session_id`, `memory_id` |
|
|
56
|
+
| `task_update` | `taskId` | `status`, `title`, `description` |
|
|
57
|
+
| `task_list` | — | `status`, `session_id`, `limit` (default: 50) |
|
|
108
58
|
|
|
109
|
-
|
|
59
|
+
**Task statuses:** `pending` `in_progress` `completed` `cancelled`
|
|
110
60
|
|
|
111
|
-
##
|
|
61
|
+
## Implementation
|
|
112
62
|
|
|
113
|
-
|
|
114
|
-
- Automatic access tracking
|
|
115
|
-
- Dynamic importance scoring (decay + boost)
|
|
116
|
-
- Relationship graph support
|
|
117
|
-
- Journal integration
|
|
63
|
+
Built with `McpServer` from `@modelcontextprotocol/sdk` and Zod input schemas. All read operations (`memory_search`, `memory_recent`) automatically log accesses which feed into the importance decay/boost system.
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,514 +1,275 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* MCP Server for
|
|
3
|
+
* MCP Server for Postnesia Memory System
|
|
4
4
|
* Exposes memory operations as Model Context Protocol tools
|
|
5
5
|
*/
|
|
6
|
-
import
|
|
7
|
-
import { Server } from "@modelcontextprotocol/sdk/server";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
-
import {
|
|
8
|
+
import { z } from "zod";
|
|
10
9
|
import { getDb, queries, createMemory } from "@postnesia/db";
|
|
11
10
|
import { embed } from "@postnesia/db/embeddings";
|
|
12
|
-
import { logAccess } from "
|
|
13
|
-
import { runConsolidation } from "
|
|
11
|
+
import { logAccess } from "@postnesia/db/access";
|
|
12
|
+
import { runConsolidation } from "@postnesia/db/importance";
|
|
14
13
|
const db = getDb();
|
|
15
|
-
const server = new
|
|
16
|
-
name: "
|
|
14
|
+
const server = new McpServer({
|
|
15
|
+
name: "postnesia",
|
|
17
16
|
version: "1.0.0",
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
});
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// memory tools
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
server.registerTool("memory_search", {
|
|
22
|
+
description: "Search memories by content. Returns all matching memories.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
query: z.string().describe("Search query"),
|
|
25
|
+
limit: z.number().optional().describe("Maximum results to return (default: 10)"),
|
|
21
26
|
},
|
|
27
|
+
}, async ({ query, limit = 10 }) => {
|
|
28
|
+
const embedding = await embed(query);
|
|
29
|
+
const results = queries.vectorSearch(db).all(Buffer.from(embedding.buffer), limit);
|
|
30
|
+
for (const result of results) {
|
|
31
|
+
logAccess(result.id, "search");
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
35
|
+
};
|
|
22
36
|
});
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
server.registerTool("memory_add", {
|
|
38
|
+
description: "Add a new memory to the database",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
content: z.string().describe("Memory content (full form)"),
|
|
41
|
+
contentL1: z.string().optional().describe("Compressed L1 form (optional, will auto-generate if omitted)"),
|
|
42
|
+
type: z.enum(["event", "decision", "lesson", "preference", "person", "technical"]).describe("Memory type"),
|
|
43
|
+
importance: z.number().describe("Base importance 1-5"),
|
|
44
|
+
tags: z.array(z.string()).describe("Tags for categorization"),
|
|
45
|
+
context: z.string().optional().describe("Optional context about when/why this memory was created"),
|
|
46
|
+
core: z.boolean().optional().describe("Mark as a core memory (always loaded, never decays, cannot be superseded)"),
|
|
47
|
+
},
|
|
48
|
+
}, async ({ content, contentL1, type, importance, tags, context, core }) => {
|
|
49
|
+
const content_l1 = contentL1 || content.slice(0, 200);
|
|
50
|
+
const embedding = await embed(content);
|
|
51
|
+
const id = createMemory(db, {
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
content,
|
|
54
|
+
content_l1,
|
|
55
|
+
type,
|
|
56
|
+
core: core ? 1 : 0,
|
|
57
|
+
importance,
|
|
58
|
+
context: context || undefined,
|
|
59
|
+
tags,
|
|
60
|
+
embedding,
|
|
61
|
+
});
|
|
25
62
|
return {
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
name: "memory_search",
|
|
29
|
-
description: "Search memories by content. Returns all matching memories.",
|
|
30
|
-
inputSchema: {
|
|
31
|
-
type: "object",
|
|
32
|
-
properties: {
|
|
33
|
-
query: {
|
|
34
|
-
type: "string",
|
|
35
|
-
description: "Search query",
|
|
36
|
-
},
|
|
37
|
-
limit: {
|
|
38
|
-
type: "number",
|
|
39
|
-
description: "Maximum results to return (default: 10)",
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
required: ["query"],
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: "memory_add",
|
|
47
|
-
description: "Add a new memory to the database",
|
|
48
|
-
inputSchema: {
|
|
49
|
-
type: "object",
|
|
50
|
-
properties: {
|
|
51
|
-
content: {
|
|
52
|
-
type: "string",
|
|
53
|
-
description: "Memory content (full form)",
|
|
54
|
-
},
|
|
55
|
-
contentL1: {
|
|
56
|
-
type: "string",
|
|
57
|
-
description: "Compressed L1 form (optional, will auto-generate if omitted)",
|
|
58
|
-
},
|
|
59
|
-
type: {
|
|
60
|
-
type: "string",
|
|
61
|
-
enum: ["event", "decision", "lesson", "preference", "person", "technical"],
|
|
62
|
-
description: "Memory type",
|
|
63
|
-
},
|
|
64
|
-
importance: {
|
|
65
|
-
type: "number",
|
|
66
|
-
description: "Base importance 1-5",
|
|
67
|
-
},
|
|
68
|
-
tags: {
|
|
69
|
-
type: "array",
|
|
70
|
-
items: { type: "string" },
|
|
71
|
-
description: "Tags for categorization",
|
|
72
|
-
},
|
|
73
|
-
context: {
|
|
74
|
-
type: "string",
|
|
75
|
-
description: "Optional context about when/why this memory was created",
|
|
76
|
-
},
|
|
77
|
-
core: {
|
|
78
|
-
type: "boolean",
|
|
79
|
-
description: "Mark as a core memory (always loaded, never decays, cannot be superseded)",
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
required: ["content", "type", "importance", "tags"],
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
name: "memory_update_core",
|
|
87
|
-
description: "Update the content of an existing core memory in place (core memories must be updated, never superseded)",
|
|
88
|
-
inputSchema: {
|
|
89
|
-
type: "object",
|
|
90
|
-
properties: {
|
|
91
|
-
memoryId: {
|
|
92
|
-
type: "number",
|
|
93
|
-
description: "ID of the memory to update",
|
|
94
|
-
},
|
|
95
|
-
content: {
|
|
96
|
-
type: "string",
|
|
97
|
-
description: "New full content",
|
|
98
|
-
},
|
|
99
|
-
contentL1: {
|
|
100
|
-
type: "string",
|
|
101
|
-
description: "New compressed L1 summary",
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
required: ["memoryId", "content", "contentL1"],
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
name: "memory_recent",
|
|
109
|
-
description: "Get recent memories within time window",
|
|
110
|
-
inputSchema: {
|
|
111
|
-
type: "object",
|
|
112
|
-
properties: {
|
|
113
|
-
hours: {
|
|
114
|
-
type: "number",
|
|
115
|
-
description: "Hours to look back (default: 24)",
|
|
116
|
-
},
|
|
117
|
-
limit: {
|
|
118
|
-
type: "number",
|
|
119
|
-
description: "Maximum results (default: 20)",
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
name: "memory_stats",
|
|
126
|
-
description: "Get memory database statistics",
|
|
127
|
-
inputSchema: {
|
|
128
|
-
type: "object",
|
|
129
|
-
properties: {},
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
name: "memory_consolidate",
|
|
134
|
-
description: "Run memory consolidation cycle (decay old, boost accessed)",
|
|
135
|
-
inputSchema: {
|
|
136
|
-
type: "object",
|
|
137
|
-
properties: {},
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: "journal_add",
|
|
142
|
-
description: "Add a daily journal entry",
|
|
143
|
-
inputSchema: {
|
|
144
|
-
type: "object",
|
|
145
|
-
properties: {
|
|
146
|
-
date: {
|
|
147
|
-
type: "string",
|
|
148
|
-
description: "Date YYYY-MM-DD",
|
|
149
|
-
},
|
|
150
|
-
content: {
|
|
151
|
-
type: "string",
|
|
152
|
-
description: "Full journal narrative",
|
|
153
|
-
},
|
|
154
|
-
learned: {
|
|
155
|
-
type: "string",
|
|
156
|
-
description: "What I learned (optional)",
|
|
157
|
-
},
|
|
158
|
-
learnedAboutRye: {
|
|
159
|
-
type: "string",
|
|
160
|
-
description: "What I learned about Rye (optional)",
|
|
161
|
-
},
|
|
162
|
-
keyMoments: {
|
|
163
|
-
type: "string",
|
|
164
|
-
description: "Key moments (optional)",
|
|
165
|
-
},
|
|
166
|
-
mood: {
|
|
167
|
-
type: "string",
|
|
168
|
-
description: "Mood/feeling (optional)",
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
required: ["date", "content"],
|
|
172
|
-
},
|
|
173
|
-
},
|
|
63
|
+
content: [
|
|
174
64
|
{
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
inputSchema: {
|
|
178
|
-
type: "object",
|
|
179
|
-
properties: {
|
|
180
|
-
days: {
|
|
181
|
-
type: "number",
|
|
182
|
-
description: "Days to look back (default: 7)",
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
},
|
|
65
|
+
type: "text",
|
|
66
|
+
text: `Created memory #${id}\n Type: ${type}\n Core: ${core ? "yes" : "no"}\n Importance: ${importance}/5\n Tags: ${tags.join(", ")}`,
|
|
186
67
|
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
server.registerTool("memory_update_core", {
|
|
72
|
+
description: "Update the content of an existing core memory in place (core memories must be updated, never superseded)",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
memoryId: z.number().describe("ID of the memory to update"),
|
|
75
|
+
content: z.string().describe("New full content"),
|
|
76
|
+
contentL1: z.string().describe("New compressed L1 summary"),
|
|
77
|
+
},
|
|
78
|
+
}, async ({ memoryId, content, contentL1 }) => {
|
|
79
|
+
const existing = db.prepare("SELECT id, core FROM memory WHERE id = ?").get(memoryId);
|
|
80
|
+
if (!existing)
|
|
81
|
+
throw new Error(`Memory #${memoryId} not found`);
|
|
82
|
+
if (!existing.core)
|
|
83
|
+
throw new Error(`Memory #${memoryId} is not a core memory. Use memory_add with supersedes_id for regular memories.`);
|
|
84
|
+
const embedding = await embed(content);
|
|
85
|
+
db.prepare(`
|
|
86
|
+
UPDATE memory
|
|
87
|
+
SET content = ?, content_l1 = ?, updated_at = datetime('now')
|
|
88
|
+
WHERE id = ?
|
|
89
|
+
`).run(content, contentL1, memoryId);
|
|
90
|
+
db.prepare("DELETE FROM vec_memories WHERE memory_id = ?").run(BigInt(memoryId));
|
|
91
|
+
db.prepare("INSERT INTO vec_memories(memory_id, embedding) VALUES (?, ?)").run(BigInt(memoryId), Buffer.from(embedding.buffer));
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: `Updated core memory #${memoryId}` }],
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
server.registerTool("memory_recent", {
|
|
97
|
+
description: "Get recent memories within time window",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
hours: z.number().optional().describe("Hours to look back (default: 24)"),
|
|
100
|
+
limit: z.number().optional().describe("Maximum results (default: 20)"),
|
|
101
|
+
},
|
|
102
|
+
}, async ({ hours = 24, limit = 20 }) => {
|
|
103
|
+
const results = queries.getRecentMemories(db).all(`-${hours} hours`, limit);
|
|
104
|
+
for (const result of results) {
|
|
105
|
+
logAccess(result.id, "recent");
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
server.registerTool("memory_stats", {
|
|
112
|
+
description: "Get memory database statistics",
|
|
113
|
+
inputSchema: {},
|
|
114
|
+
}, async () => {
|
|
115
|
+
const stats = queries.getStats(db).all();
|
|
116
|
+
const total = db.prepare("SELECT COUNT(*) as count FROM memory").get().count;
|
|
117
|
+
const tags = db.prepare("SELECT COUNT(*) as count FROM tag").get().count;
|
|
118
|
+
const relationships = db.prepare("SELECT COUNT(*) as count FROM relationship").get().count;
|
|
119
|
+
const accessLogs = db.prepare("SELECT COUNT(*) as count FROM access_log").get().count;
|
|
120
|
+
const rows = stats
|
|
121
|
+
.map((s) => ` ${s.type.padEnd(15)} ${s.count.toString().padStart(3)} memories (avg importance: ${s.avg_importance?.toFixed(1)})`)
|
|
122
|
+
.join("\n");
|
|
123
|
+
return {
|
|
124
|
+
content: [
|
|
187
125
|
{
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
inputSchema: {
|
|
191
|
-
type: "object",
|
|
192
|
-
properties: {
|
|
193
|
-
memoryId: {
|
|
194
|
-
type: "number",
|
|
195
|
-
description: "Memory ID to explore",
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
required: ["memoryId"],
|
|
199
|
-
},
|
|
126
|
+
type: "text",
|
|
127
|
+
text: `Memory Statistics:\n\n${rows}\n\n Total: ${total} memories\n Tags: ${tags}\n Relationships: ${relationships}\n Access Logs: ${accessLogs}`,
|
|
200
128
|
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
server.registerTool("memory_consolidate", {
|
|
133
|
+
description: "Run memory consolidation cycle (decay old, boost accessed)",
|
|
134
|
+
inputSchema: {},
|
|
135
|
+
}, async () => {
|
|
136
|
+
const results = runConsolidation();
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
201
139
|
{
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
inputSchema: {
|
|
205
|
-
type: "object",
|
|
206
|
-
properties: {
|
|
207
|
-
title: {
|
|
208
|
-
type: "string",
|
|
209
|
-
description: "Short task title",
|
|
210
|
-
},
|
|
211
|
-
description: {
|
|
212
|
-
type: "string",
|
|
213
|
-
description: "Detailed description of what needs to be done",
|
|
214
|
-
},
|
|
215
|
-
session_id: {
|
|
216
|
-
type: "string",
|
|
217
|
-
description: "Project or feature label to group related tasks (e.g. 'openmind-mcp', 'auth-refactor')",
|
|
218
|
-
},
|
|
219
|
-
memory_id: {
|
|
220
|
-
type: "number",
|
|
221
|
-
description: "Optional ID of a related memory",
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
required: ["title"],
|
|
225
|
-
},
|
|
140
|
+
type: "text",
|
|
141
|
+
text: `Consolidation Complete\n\nReviewed: ${results.reviewed} memories\nBoosted: ${results.boosted} memories\nDecayed: ${results.decayed} memories\nUnchanged: ${results.unchanged} memories\n\nChanges saved to database`,
|
|
226
142
|
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
server.registerTool("memory_relationships", {
|
|
147
|
+
description: "View relationship graph for a memory",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
memoryId: z.number().describe("Memory ID to explore"),
|
|
150
|
+
},
|
|
151
|
+
}, async ({ memoryId }) => {
|
|
152
|
+
const relationships = queries.getMemoryRelationships(db).all(memoryId, memoryId);
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: "text", text: JSON.stringify(relationships, null, 2) }],
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// journal tools
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
server.registerTool("journal_add", {
|
|
161
|
+
description: "Add a daily journal entry",
|
|
162
|
+
inputSchema: {
|
|
163
|
+
date: z.string().describe("Date YYYY-MM-DD"),
|
|
164
|
+
content: z.string().describe("Full journal narrative"),
|
|
165
|
+
learned: z.string().optional().describe("What I learned (optional)"),
|
|
166
|
+
learnedAboutRye: z.string().optional().describe("What I learned about Rye (optional)"),
|
|
167
|
+
keyMoments: z.string().optional().describe("Key moments (optional)"),
|
|
168
|
+
mood: z.string().optional().describe("Mood/feeling (optional)"),
|
|
169
|
+
},
|
|
170
|
+
}, async ({ date, content, learned, learnedAboutRye, keyMoments, mood }) => {
|
|
171
|
+
const combinedLearned = [learned, learnedAboutRye].filter(Boolean).join("\n\n") || null;
|
|
172
|
+
queries.insertJournal(db).run(date, content, combinedLearned, keyMoments || null, mood || null);
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: "text", text: `Journal entry created for ${date}` }],
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
server.registerTool("journal_recent", {
|
|
178
|
+
description: "Get recent journal entries",
|
|
179
|
+
inputSchema: {
|
|
180
|
+
days: z.number().optional().describe("Days to look back (default: 7)"),
|
|
181
|
+
},
|
|
182
|
+
}, async ({ days = 7 }) => {
|
|
183
|
+
const entries = queries.getRecentJournals(db).all(`-${days} days`);
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text", text: JSON.stringify(entries, null, 2) }],
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// task tools
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
server.registerTool("task_create", {
|
|
192
|
+
description: "Create a new task. Use session_id to group tasks by project or feature.",
|
|
193
|
+
inputSchema: {
|
|
194
|
+
title: z.string().describe("Short task title"),
|
|
195
|
+
description: z.string().optional().describe("Detailed description of what needs to be done"),
|
|
196
|
+
session_id: z.string().optional().describe("Project or feature label to group related tasks (e.g. 'openmind-mcp', 'auth-refactor')"),
|
|
197
|
+
memory_id: z.number().optional().describe("Optional ID of a related memory"),
|
|
198
|
+
},
|
|
199
|
+
}, async ({ title, description, session_id, memory_id }) => {
|
|
200
|
+
const result = queries.insertTask(db).run(title, description || null, session_id || null, memory_id || null);
|
|
201
|
+
const id = Number(result.lastInsertRowid);
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
227
204
|
{
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
inputSchema: {
|
|
231
|
-
type: "object",
|
|
232
|
-
properties: {
|
|
233
|
-
taskId: {
|
|
234
|
-
type: "number",
|
|
235
|
-
description: "ID of the task to update",
|
|
236
|
-
},
|
|
237
|
-
status: {
|
|
238
|
-
type: "string",
|
|
239
|
-
enum: ["pending", "in_progress", "completed", "cancelled"],
|
|
240
|
-
description: "New status",
|
|
241
|
-
},
|
|
242
|
-
title: {
|
|
243
|
-
type: "string",
|
|
244
|
-
description: "Updated title",
|
|
245
|
-
},
|
|
246
|
-
description: {
|
|
247
|
-
type: "string",
|
|
248
|
-
description: "Updated description",
|
|
249
|
-
},
|
|
250
|
-
},
|
|
251
|
-
required: ["taskId"],
|
|
252
|
-
},
|
|
205
|
+
type: "text",
|
|
206
|
+
text: `Created task #${id}: ${title}${session_id ? `\n Session: ${session_id}` : ""}`,
|
|
253
207
|
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
server.registerTool("task_update", {
|
|
212
|
+
description: "Update a task's status, title, or description",
|
|
213
|
+
inputSchema: {
|
|
214
|
+
taskId: z.number().describe("ID of the task to update"),
|
|
215
|
+
status: z.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe("New status"),
|
|
216
|
+
title: z.string().optional().describe("Updated title"),
|
|
217
|
+
description: z.string().optional().describe("Updated description"),
|
|
218
|
+
},
|
|
219
|
+
}, async ({ taskId, status, title, description }) => {
|
|
220
|
+
const existing = queries.getTaskById(db).get(taskId);
|
|
221
|
+
if (!existing)
|
|
222
|
+
throw new Error(`Task #${taskId} not found`);
|
|
223
|
+
queries.updateTask(db).run(status || null, title || null, description || null, taskId);
|
|
224
|
+
const updated = queries.getTaskById(db).get(taskId);
|
|
225
|
+
return {
|
|
226
|
+
content: [
|
|
254
227
|
{
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
inputSchema: {
|
|
258
|
-
type: "object",
|
|
259
|
-
properties: {
|
|
260
|
-
status: {
|
|
261
|
-
type: "string",
|
|
262
|
-
enum: ["pending", "in_progress", "completed", "cancelled"],
|
|
263
|
-
description: "Filter by status",
|
|
264
|
-
},
|
|
265
|
-
session_id: {
|
|
266
|
-
type: "string",
|
|
267
|
-
description: "Filter by project/feature label",
|
|
268
|
-
},
|
|
269
|
-
limit: {
|
|
270
|
-
type: "number",
|
|
271
|
-
description: "Maximum results (default: 50)",
|
|
272
|
-
},
|
|
273
|
-
},
|
|
274
|
-
},
|
|
228
|
+
type: "text",
|
|
229
|
+
text: `Task #${taskId} updated\n Status: ${updated.status}\n Title: ${updated.title}`,
|
|
275
230
|
},
|
|
276
231
|
],
|
|
277
232
|
};
|
|
278
233
|
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
content: [
|
|
294
|
-
{
|
|
295
|
-
type: "text",
|
|
296
|
-
text: JSON.stringify(results, null, 2),
|
|
297
|
-
},
|
|
298
|
-
],
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
case "memory_add": {
|
|
302
|
-
const { content, contentL1, type, importance, tags, context, core } = args;
|
|
303
|
-
const content_l1 = contentL1 || content.slice(0, 200);
|
|
304
|
-
const embedding = await embed(content);
|
|
305
|
-
const id = createMemory(db, {
|
|
306
|
-
timestamp: new Date().toISOString(),
|
|
307
|
-
content,
|
|
308
|
-
content_l1,
|
|
309
|
-
type,
|
|
310
|
-
core: core ? 1 : 0,
|
|
311
|
-
importance,
|
|
312
|
-
context: context || undefined,
|
|
313
|
-
tags,
|
|
314
|
-
embedding,
|
|
315
|
-
});
|
|
316
|
-
return {
|
|
317
|
-
content: [
|
|
318
|
-
{
|
|
319
|
-
type: "text",
|
|
320
|
-
text: `✓ Created memory #${id}\n Type: ${type}\n Core: ${core ? 'yes' : 'no'}\n Importance: ${'⭐'.repeat(importance)}\n Tags: ${tags.join(', ')}`,
|
|
321
|
-
},
|
|
322
|
-
],
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
case "memory_update_core": {
|
|
326
|
-
const { memoryId, content, contentL1 } = args;
|
|
327
|
-
const existing = db.prepare('SELECT id, core FROM memory WHERE id = ?').get(memoryId);
|
|
328
|
-
if (!existing) {
|
|
329
|
-
throw new Error(`Memory #${memoryId} not found`);
|
|
330
|
-
}
|
|
331
|
-
if (!existing.core) {
|
|
332
|
-
throw new Error(`Memory #${memoryId} is not a core memory. Use memory_add with supersedes_id for regular memories.`);
|
|
333
|
-
}
|
|
334
|
-
const embedding = await embed(content);
|
|
335
|
-
db.prepare(`
|
|
336
|
-
UPDATE memory
|
|
337
|
-
SET content = ?, content_l1 = ?, updated_at = datetime('now')
|
|
338
|
-
WHERE id = ?
|
|
339
|
-
`).run(content, contentL1, memoryId);
|
|
340
|
-
// Update embedding in vec_memories virtual table
|
|
341
|
-
db.prepare('DELETE FROM vec_memories WHERE memory_id = ?').run(BigInt(memoryId));
|
|
342
|
-
db.prepare('INSERT INTO vec_memories(memory_id, embedding) VALUES (?, ?)').run(BigInt(memoryId), Buffer.from(embedding.buffer));
|
|
343
|
-
return {
|
|
344
|
-
content: [
|
|
345
|
-
{
|
|
346
|
-
type: "text",
|
|
347
|
-
text: `✓ Updated core memory #${memoryId}`,
|
|
348
|
-
},
|
|
349
|
-
],
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
case "memory_recent": {
|
|
353
|
-
const hours = args.hours || 24;
|
|
354
|
-
const limit = args.limit || 20;
|
|
355
|
-
const results = queries.getRecentMemories(db).all(`-${hours} hours`, limit);
|
|
356
|
-
for (const result of results) {
|
|
357
|
-
logAccess(result.id, 'recent');
|
|
358
|
-
}
|
|
359
|
-
return {
|
|
360
|
-
content: [
|
|
361
|
-
{
|
|
362
|
-
type: "text",
|
|
363
|
-
text: JSON.stringify(results, null, 2),
|
|
364
|
-
},
|
|
365
|
-
],
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
case "memory_stats": {
|
|
369
|
-
const stats = queries.getStats(db).all();
|
|
370
|
-
const total = db.prepare('SELECT COUNT(*) as count FROM memory').get().count;
|
|
371
|
-
const tags = db.prepare('SELECT COUNT(*) as count FROM tag').get().count;
|
|
372
|
-
const relationships = db.prepare('SELECT COUNT(*) as count FROM relationship').get().count;
|
|
373
|
-
const accessLogs = db.prepare('SELECT COUNT(*) as count FROM access_log').get().count;
|
|
374
|
-
return {
|
|
375
|
-
content: [
|
|
376
|
-
{
|
|
377
|
-
type: "text",
|
|
378
|
-
text: `📊 Memory Statistics:\n\n${stats.map(s => ` ${s.type.padEnd(15)} ${s.count.toString().padStart(3)} memories (avg importance: ${s.avg_importance?.toFixed(1)})`).join('\n')}\n\n Total: ${total} memories\n Tags: ${tags}\n Relationships: ${relationships}\n Access Logs: ${accessLogs}`,
|
|
379
|
-
},
|
|
380
|
-
],
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
case "memory_consolidate": {
|
|
384
|
-
const results = runConsolidation();
|
|
385
|
-
return {
|
|
386
|
-
content: [
|
|
387
|
-
{
|
|
388
|
-
type: "text",
|
|
389
|
-
text: `🔄 Consolidation Complete\n\nReviewed: ${results.reviewed} memories\nBoosted: ${results.boosted} memories\nDecayed: ${results.decayed} memories\nUnchanged: ${results.unchanged} memories\n\nChanges saved to database`,
|
|
390
|
-
},
|
|
391
|
-
],
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
case "journal_add": {
|
|
395
|
-
const { date, content, learned, learnedAboutRye, keyMoments, mood } = args;
|
|
396
|
-
// Combine learned + learnedAboutRye into the single learned column
|
|
397
|
-
const combinedLearned = [learned, learnedAboutRye].filter(Boolean).join('\n\n') || null;
|
|
398
|
-
queries.insertJournal(db).run(date, content, combinedLearned, keyMoments || null, mood || null);
|
|
399
|
-
return {
|
|
400
|
-
content: [
|
|
401
|
-
{
|
|
402
|
-
type: "text",
|
|
403
|
-
text: `✓ Journal entry created for ${date}`,
|
|
404
|
-
},
|
|
405
|
-
],
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
case "journal_recent": {
|
|
409
|
-
const days = args.days || 7;
|
|
410
|
-
const entries = queries.getRecentJournals(db).all(`-${days} days`);
|
|
411
|
-
return {
|
|
412
|
-
content: [
|
|
413
|
-
{
|
|
414
|
-
type: "text",
|
|
415
|
-
text: JSON.stringify(entries, null, 2),
|
|
416
|
-
},
|
|
417
|
-
],
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
case "memory_relationships": {
|
|
421
|
-
const memoryId = args.memoryId;
|
|
422
|
-
const relationships = queries.getMemoryRelationships(db).all(memoryId, memoryId);
|
|
423
|
-
return {
|
|
424
|
-
content: [
|
|
425
|
-
{
|
|
426
|
-
type: "text",
|
|
427
|
-
text: JSON.stringify(relationships, null, 2),
|
|
428
|
-
},
|
|
429
|
-
],
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
case "task_create": {
|
|
433
|
-
const { title, description, session_id, memory_id } = args;
|
|
434
|
-
const result = queries.insertTask(db).run(title, description || null, session_id || null, memory_id || null);
|
|
435
|
-
const id = Number(result.lastInsertRowid);
|
|
436
|
-
return {
|
|
437
|
-
content: [
|
|
438
|
-
{
|
|
439
|
-
type: "text",
|
|
440
|
-
text: `✓ Created task #${id}: ${title}${session_id ? `\n Session: ${session_id}` : ''}`,
|
|
441
|
-
},
|
|
442
|
-
],
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
case "task_update": {
|
|
446
|
-
const { taskId, status, title, description } = args;
|
|
447
|
-
const existing = queries.getTaskById(db).get(taskId);
|
|
448
|
-
if (!existing)
|
|
449
|
-
throw new Error(`Task #${taskId} not found`);
|
|
450
|
-
queries.updateTask(db).run(status || null, title || null, description || null, taskId);
|
|
451
|
-
const updated = queries.getTaskById(db).get(taskId);
|
|
452
|
-
return {
|
|
453
|
-
content: [
|
|
454
|
-
{
|
|
455
|
-
type: "text",
|
|
456
|
-
text: `✓ Task #${taskId} updated\n Status: ${updated.status}\n Title: ${updated.title}`,
|
|
457
|
-
},
|
|
458
|
-
],
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
case "task_list": {
|
|
462
|
-
const { status, session_id, limit = 50 } = args;
|
|
463
|
-
// Build dynamic WHERE clause
|
|
464
|
-
const conditions = [];
|
|
465
|
-
const params = [];
|
|
466
|
-
if (status) {
|
|
467
|
-
conditions.push('status = ?');
|
|
468
|
-
params.push(status);
|
|
469
|
-
}
|
|
470
|
-
if (session_id) {
|
|
471
|
-
conditions.push('session_id = ?');
|
|
472
|
-
params.push(session_id);
|
|
473
|
-
}
|
|
474
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
475
|
-
params.push(limit);
|
|
476
|
-
const tasks = db.prepare(`
|
|
477
|
-
SELECT * FROM task
|
|
478
|
-
${where}
|
|
479
|
-
ORDER BY created_at ASC
|
|
480
|
-
LIMIT ?
|
|
481
|
-
`).all(...params);
|
|
482
|
-
return {
|
|
483
|
-
content: [
|
|
484
|
-
{
|
|
485
|
-
type: "text",
|
|
486
|
-
text: JSON.stringify(tasks, null, 2),
|
|
487
|
-
},
|
|
488
|
-
],
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
default:
|
|
492
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
493
|
-
}
|
|
234
|
+
server.registerTool("task_list", {
|
|
235
|
+
description: "List tasks, optionally filtered by status and/or session_id. Use at session start to resume open work.",
|
|
236
|
+
inputSchema: {
|
|
237
|
+
status: z.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe("Filter by status"),
|
|
238
|
+
session_id: z.string().optional().describe("Filter by project/feature label"),
|
|
239
|
+
limit: z.number().optional().describe("Maximum results (default: 50)"),
|
|
240
|
+
},
|
|
241
|
+
}, async ({ status, session_id, limit = 50 }) => {
|
|
242
|
+
const conditions = [];
|
|
243
|
+
const params = [];
|
|
244
|
+
if (status) {
|
|
245
|
+
conditions.push("status = ?");
|
|
246
|
+
params.push(status);
|
|
494
247
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
{
|
|
499
|
-
type: "text",
|
|
500
|
-
text: `Error: ${error.message}`,
|
|
501
|
-
},
|
|
502
|
-
],
|
|
503
|
-
isError: true,
|
|
504
|
-
};
|
|
248
|
+
if (session_id) {
|
|
249
|
+
conditions.push("session_id = ?");
|
|
250
|
+
params.push(session_id);
|
|
505
251
|
}
|
|
252
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
253
|
+
params.push(limit);
|
|
254
|
+
const tasks = db.prepare(`
|
|
255
|
+
SELECT * FROM task
|
|
256
|
+
${where}
|
|
257
|
+
ORDER BY created_at ASC
|
|
258
|
+
LIMIT ?
|
|
259
|
+
`).all(...params);
|
|
260
|
+
return {
|
|
261
|
+
content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }],
|
|
262
|
+
};
|
|
506
263
|
});
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
507
265
|
// Start server
|
|
508
|
-
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
async function main() {
|
|
509
268
|
const transport = new StdioServerTransport();
|
|
510
269
|
await server.connect(transport);
|
|
270
|
+
console.error("Postnesia MCP Server running on stdio");
|
|
511
271
|
}
|
|
512
|
-
catch
|
|
513
|
-
console.error("
|
|
514
|
-
|
|
272
|
+
main().catch((error) => {
|
|
273
|
+
console.error("Fatal error:", error);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
});
|
package/package.json
CHANGED
|
@@ -1,27 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postnesia/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "An MCP server to interact with the Postnesia database",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"private": false,
|
|
6
7
|
"files": [
|
|
7
8
|
"dist"
|
|
8
9
|
],
|
|
9
|
-
"exports": {
|
|
10
|
-
"./access": {
|
|
11
|
-
"types": "./dist/access.d.ts",
|
|
12
|
-
"import": "./dist/access.js"
|
|
13
|
-
},
|
|
14
|
-
"./importance": {
|
|
15
|
-
"types": "./dist/importance.d.ts",
|
|
16
|
-
"import": "./dist/importance.js"
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
10
|
"bin": {
|
|
20
11
|
"postnesia-mcp": "./dist/index.js"
|
|
21
12
|
},
|
|
22
13
|
"dependencies": {
|
|
23
14
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
24
|
-
"@postnesia/db": "0.1.
|
|
15
|
+
"@postnesia/db": "^0.1.5",
|
|
16
|
+
"zod": "^4.3.6"
|
|
25
17
|
},
|
|
26
18
|
"devDependencies": {
|
|
27
19
|
"@types/node": "^22.10.5",
|
|
@@ -29,7 +21,8 @@
|
|
|
29
21
|
"typescript": "^5.7.3"
|
|
30
22
|
},
|
|
31
23
|
"scripts": {
|
|
32
|
-
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"build": "pnpm clean && tsc -p tsconfig.json",
|
|
25
|
+
"clean": "rm -rf ./dist",
|
|
33
26
|
"start": "tsx src/index.ts"
|
|
34
27
|
}
|
|
35
28
|
}
|
package/dist/access.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Access Tracking System
|
|
3
|
-
* Logs when memories are retrieved and updates last_accessed on the memory row.
|
|
4
|
-
* Feeds into L1 decay calculations.
|
|
5
|
-
*/
|
|
6
|
-
export interface AccessLogEntry {
|
|
7
|
-
memory_id: number;
|
|
8
|
-
context?: string;
|
|
9
|
-
accessed_at: string;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Record that a memory was accessed (updates both access_log and memory.last_accessed)
|
|
13
|
-
*/
|
|
14
|
-
export declare function logAccess(memory_id: number, context?: string): void;
|
|
15
|
-
export declare function getAccessCount(memory_id: number, daysBack?: number): number;
|
|
16
|
-
export declare function getRecentlyAccessed(limit?: number): number[];
|
|
17
|
-
export declare function getAccessHistory(memory_id: number): AccessLogEntry[];
|
|
18
|
-
/**
|
|
19
|
-
* Calculate relevance boost based on access patterns
|
|
20
|
-
* Recent access + frequency = higher relevance
|
|
21
|
-
*/
|
|
22
|
-
export declare function calculateAccessBoost(memory_id: number): number;
|
package/dist/access.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Access Tracking System
|
|
3
|
-
* Logs when memories are retrieved and updates last_accessed on the memory row.
|
|
4
|
-
* Feeds into L1 decay calculations.
|
|
5
|
-
*/
|
|
6
|
-
import { getDb, recordAccess } from '@postnesia/db';
|
|
7
|
-
/**
|
|
8
|
-
* Record that a memory was accessed (updates both access_log and memory.last_accessed)
|
|
9
|
-
*/
|
|
10
|
-
export function logAccess(memory_id, context) {
|
|
11
|
-
try {
|
|
12
|
-
const db = getDb(false);
|
|
13
|
-
recordAccess(db, memory_id, context);
|
|
14
|
-
}
|
|
15
|
-
catch (error) {
|
|
16
|
-
console.error(`[access-tracker] Warning: Could not log access for #${memory_id}:`, error?.message);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
export function getAccessCount(memory_id, daysBack = 30) {
|
|
20
|
-
const db = getDb(true);
|
|
21
|
-
const result = db.prepare(`
|
|
22
|
-
SELECT COUNT(*) as count
|
|
23
|
-
FROM access_log
|
|
24
|
-
WHERE memory_id = ?
|
|
25
|
-
AND accessed_at > datetime('now', '-${daysBack} days')
|
|
26
|
-
`).get(memory_id);
|
|
27
|
-
return result.count;
|
|
28
|
-
}
|
|
29
|
-
export function getRecentlyAccessed(limit = 20) {
|
|
30
|
-
const db = getDb(true);
|
|
31
|
-
const results = db.prepare(`
|
|
32
|
-
SELECT memory_id, COUNT(*) as access_count
|
|
33
|
-
FROM access_log
|
|
34
|
-
WHERE accessed_at > datetime('now', '-7 days')
|
|
35
|
-
GROUP BY memory_id
|
|
36
|
-
ORDER BY access_count DESC, MAX(accessed_at) DESC
|
|
37
|
-
LIMIT ?
|
|
38
|
-
`).all(limit);
|
|
39
|
-
return results.map(r => r.memory_id);
|
|
40
|
-
}
|
|
41
|
-
export function getAccessHistory(memory_id) {
|
|
42
|
-
const db = getDb(true);
|
|
43
|
-
return db.prepare(`
|
|
44
|
-
SELECT memory_id, accessed_at, context
|
|
45
|
-
FROM access_log
|
|
46
|
-
WHERE memory_id = ?
|
|
47
|
-
ORDER BY accessed_at DESC
|
|
48
|
-
LIMIT 50
|
|
49
|
-
`).all(memory_id);
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Calculate relevance boost based on access patterns
|
|
53
|
-
* Recent access + frequency = higher relevance
|
|
54
|
-
*/
|
|
55
|
-
export function calculateAccessBoost(memory_id) {
|
|
56
|
-
const last7Days = getAccessCount(memory_id, 7);
|
|
57
|
-
const last30Days = getAccessCount(memory_id, 30);
|
|
58
|
-
const recentWeight = last7Days * 2;
|
|
59
|
-
const olderWeight = (last30Days - last7Days);
|
|
60
|
-
const totalScore = recentWeight + olderWeight;
|
|
61
|
-
// Normalize to 0-2 range
|
|
62
|
-
return Math.min(2, totalScore / 5);
|
|
63
|
-
}
|
package/dist/importance.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Importance Dynamics
|
|
3
|
-
* Adjust memory importance based on age, access patterns, and relationships
|
|
4
|
-
*/
|
|
5
|
-
export interface ImportanceFactors {
|
|
6
|
-
baseImportance: number;
|
|
7
|
-
ageDecay: number;
|
|
8
|
-
accessBoost: number;
|
|
9
|
-
relationshipBoost: number;
|
|
10
|
-
finalScore: number;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Calculate age decay factor
|
|
14
|
-
* Recent memories maintain importance, old ones decay unless accessed
|
|
15
|
-
*/
|
|
16
|
-
export declare function calculateAgeDecay(timestamp: string): number;
|
|
17
|
-
/**
|
|
18
|
-
* Calculate dynamic importance score
|
|
19
|
-
* Base importance + access boost - age decay
|
|
20
|
-
*/
|
|
21
|
-
export declare function calculateDynamicImportance(memory_id: number): ImportanceFactors;
|
|
22
|
-
/**
|
|
23
|
-
* Get memories eligible for L1 based on dynamic scoring.
|
|
24
|
-
* Uses the decay query from PHILOSOPHY.md: importance penalized by last_accessed staleness,
|
|
25
|
-
* superseded memories excluded.
|
|
26
|
-
*/
|
|
27
|
-
export declare function getL1Candidates(limit?: number): Array<{
|
|
28
|
-
id: number;
|
|
29
|
-
type: string;
|
|
30
|
-
content_l1: string;
|
|
31
|
-
timestamp: string;
|
|
32
|
-
last_accessed: string;
|
|
33
|
-
baseImportance: number;
|
|
34
|
-
dynamicScore: number;
|
|
35
|
-
}>;
|
|
36
|
-
/**
|
|
37
|
-
* Consolidation: Review and adjust importances based on access patterns
|
|
38
|
-
*/
|
|
39
|
-
export declare function runConsolidation(): {
|
|
40
|
-
reviewed: number;
|
|
41
|
-
boosted: number;
|
|
42
|
-
decayed: number;
|
|
43
|
-
unchanged: number;
|
|
44
|
-
};
|
package/dist/importance.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Importance Dynamics
|
|
3
|
-
* Adjust memory importance based on age, access patterns, and relationships
|
|
4
|
-
*/
|
|
5
|
-
import { getDb, queries } from '@postnesia/db';
|
|
6
|
-
import { calculateAccessBoost } from './access.js';
|
|
7
|
-
/**
|
|
8
|
-
* Calculate relationship boost based on graph connectivity
|
|
9
|
-
* Memories that are hubs in the relationship graph are more valuable
|
|
10
|
-
*/
|
|
11
|
-
function calculateRelationshipBoost(memory_id) {
|
|
12
|
-
const db = getDb(true);
|
|
13
|
-
// Count total connections
|
|
14
|
-
const connections = db.prepare(`
|
|
15
|
-
SELECT COUNT(*) as count
|
|
16
|
-
FROM relationship
|
|
17
|
-
WHERE from_id = ? OR to_id = ?
|
|
18
|
-
`).get(memory_id, memory_id);
|
|
19
|
-
// Count connections to high-importance memories (4+)
|
|
20
|
-
const importantConnections = db.prepare(`
|
|
21
|
-
SELECT COUNT(*) as count
|
|
22
|
-
FROM relationship r
|
|
23
|
-
JOIN memory m ON (
|
|
24
|
-
CASE
|
|
25
|
-
WHEN r.from_id = ? THEN m.id = r.to_id
|
|
26
|
-
ELSE m.id = r.from_id
|
|
27
|
-
END
|
|
28
|
-
)
|
|
29
|
-
WHERE (r.from_id = ? OR r.to_id = ?)
|
|
30
|
-
AND m.importance >= 4
|
|
31
|
-
`).get(memory_id, memory_id, memory_id);
|
|
32
|
-
// Boost calculation:
|
|
33
|
-
// - Base: 0.1 per connection (up to +1.0 for 10 connections)
|
|
34
|
-
// - Bonus: +0.2 per important connection
|
|
35
|
-
const baseBoost = Math.min(1.0, connections.count * 0.1);
|
|
36
|
-
const importantBonus = importantConnections.count * 0.2;
|
|
37
|
-
return Math.min(2.0, baseBoost + importantBonus);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Calculate age decay factor
|
|
41
|
-
* Recent memories maintain importance, old ones decay unless accessed
|
|
42
|
-
*/
|
|
43
|
-
export function calculateAgeDecay(timestamp) {
|
|
44
|
-
const now = Date.now();
|
|
45
|
-
const memoryDate = new Date(timestamp).getTime();
|
|
46
|
-
const ageInDays = (now - memoryDate) / (1000 * 60 * 60 * 24);
|
|
47
|
-
// No decay for first 7 days
|
|
48
|
-
if (ageInDays <= 7)
|
|
49
|
-
return 0;
|
|
50
|
-
// Gradual decay: -0.1 per week after first week
|
|
51
|
-
const weeksOld = Math.floor((ageInDays - 7) / 7);
|
|
52
|
-
const decay = Math.min(2, weeksOld * 0.1); // Max -2 importance
|
|
53
|
-
return -decay;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Calculate dynamic importance score
|
|
57
|
-
* Base importance + access boost - age decay
|
|
58
|
-
*/
|
|
59
|
-
export function calculateDynamicImportance(memory_id) {
|
|
60
|
-
const db = getDb(true);
|
|
61
|
-
const memory = db.prepare(`
|
|
62
|
-
SELECT importance, timestamp
|
|
63
|
-
FROM memory
|
|
64
|
-
WHERE id = ?
|
|
65
|
-
`).get(memory_id);
|
|
66
|
-
if (!memory) {
|
|
67
|
-
return {
|
|
68
|
-
baseImportance: 0,
|
|
69
|
-
ageDecay: 0,
|
|
70
|
-
accessBoost: 0,
|
|
71
|
-
relationshipBoost: 0,
|
|
72
|
-
finalScore: 0,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
const baseImportance = memory.importance;
|
|
76
|
-
const ageDecay = calculateAgeDecay(memory.timestamp);
|
|
77
|
-
const accessBoost = calculateAccessBoost(memory_id);
|
|
78
|
-
const relationshipBoost = calculateRelationshipBoost(memory_id);
|
|
79
|
-
// Final score: base + boosts + decay (decay is negative)
|
|
80
|
-
const finalScore = Math.max(1, Math.min(5, baseImportance + accessBoost + relationshipBoost + ageDecay));
|
|
81
|
-
return {
|
|
82
|
-
baseImportance,
|
|
83
|
-
ageDecay,
|
|
84
|
-
accessBoost,
|
|
85
|
-
relationshipBoost,
|
|
86
|
-
finalScore: Math.round(finalScore * 10) / 10, // Round to 1 decimal
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Get memories eligible for L1 based on dynamic scoring.
|
|
91
|
-
* Uses the decay query from PHILOSOPHY.md: importance penalized by last_accessed staleness,
|
|
92
|
-
* superseded memories excluded.
|
|
93
|
-
*/
|
|
94
|
-
export function getL1Candidates(limit = 50) {
|
|
95
|
-
const db = getDb(true);
|
|
96
|
-
// Use the L1 decay query from db.ts (matches PHILOSOPHY.md)
|
|
97
|
-
const memories = queries.getL1Summaries(db).all();
|
|
98
|
-
return memories.slice(0, limit).map(m => ({
|
|
99
|
-
id: m.id,
|
|
100
|
-
type: m.type,
|
|
101
|
-
content_l1: m.content_l1,
|
|
102
|
-
timestamp: m.timestamp,
|
|
103
|
-
last_accessed: m.last_accessed,
|
|
104
|
-
baseImportance: m.importance,
|
|
105
|
-
dynamicScore: m.effective_importance,
|
|
106
|
-
}));
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Consolidation: Review and adjust importances based on access patterns
|
|
110
|
-
*/
|
|
111
|
-
export function runConsolidation() {
|
|
112
|
-
const db = getDb(false);
|
|
113
|
-
let reviewed = 0;
|
|
114
|
-
let boosted = 0;
|
|
115
|
-
let decayed = 0;
|
|
116
|
-
let unchanged = 0;
|
|
117
|
-
// Get all memories from last 60 days
|
|
118
|
-
const memories = db.prepare(`
|
|
119
|
-
SELECT id, importance, timestamp
|
|
120
|
-
FROM memory
|
|
121
|
-
WHERE timestamp > datetime('now', '-60 days')
|
|
122
|
-
`).all();
|
|
123
|
-
for (const memory of memories) {
|
|
124
|
-
reviewed++;
|
|
125
|
-
const factors = calculateDynamicImportance(memory.id);
|
|
126
|
-
const newImportance = Math.round(factors.finalScore);
|
|
127
|
-
if (newImportance !== memory.importance) {
|
|
128
|
-
db.prepare(`
|
|
129
|
-
UPDATE memory
|
|
130
|
-
SET importance = ?
|
|
131
|
-
WHERE id = ?
|
|
132
|
-
`).run(newImportance, memory.id);
|
|
133
|
-
if (newImportance > memory.importance) {
|
|
134
|
-
boosted++;
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
decayed++;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
unchanged++;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return { reviewed, boosted, decayed, unchanged };
|
|
145
|
-
}
|