@asd412id/mcp-context-manager 1.0.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 +183 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +35 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +127 -0
- package/dist/storage/file-store.d.ts +18 -0
- package/dist/storage/file-store.js +84 -0
- package/dist/tools/checkpoint.d.ts +2 -0
- package/dist/tools/checkpoint.js +192 -0
- package/dist/tools/loader.d.ts +2 -0
- package/dist/tools/loader.js +263 -0
- package/dist/tools/memory.d.ts +2 -0
- package/dist/tools/memory.js +182 -0
- package/dist/tools/summarizer.d.ts +2 -0
- package/dist/tools/summarizer.js +228 -0
- package/dist/tools/tracker.d.ts +2 -0
- package/dist/tools/tracker.js +196 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# MCP Context Manager
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) tools untuk manajemen context pada AI coding agents. Membantu mengatasi masalah out-of-context pada chat yang panjang.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Memory Store** - Persistent key-value storage antar session
|
|
8
|
+
- **Context Summarizer** - Ringkas chat/text, extract key points, decisions, action items
|
|
9
|
+
- **Project Tracker** - Track decisions, changes, todos, notes, errors
|
|
10
|
+
- **Session Checkpoint** - Save/restore session state
|
|
11
|
+
- **Smart File Loader** - Load file dengan filter relevansi
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @asd412id/mcp-context-manager
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Atau jalankan langsung dengan npx:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @asd412id/mcp-context-manager
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
### Claude Desktop
|
|
28
|
+
|
|
29
|
+
Tambahkan ke `claude_desktop_config.json`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"context-manager": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["-y", "@asd412id/mcp-context-manager"],
|
|
37
|
+
"env": {
|
|
38
|
+
"MCP_CONTEXT_PATH": "/path/to/your/project/.context"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Environment Variables
|
|
46
|
+
|
|
47
|
+
| Variable | Description | Default |
|
|
48
|
+
|----------|-------------|---------|
|
|
49
|
+
| `MCP_CONTEXT_PATH` | Path untuk menyimpan context data | `{cwd}/.context` |
|
|
50
|
+
|
|
51
|
+
## Available Tools
|
|
52
|
+
|
|
53
|
+
### Memory Store (6 tools)
|
|
54
|
+
|
|
55
|
+
| Tool | Description |
|
|
56
|
+
|------|-------------|
|
|
57
|
+
| `memory_set` | Simpan key-value ke memory |
|
|
58
|
+
| `memory_get` | Ambil value dari memory |
|
|
59
|
+
| `memory_search` | Cari memory by pattern/tags |
|
|
60
|
+
| `memory_delete` | Hapus memory entry |
|
|
61
|
+
| `memory_list` | List semua memory keys |
|
|
62
|
+
| `memory_clear` | Clear memory (all/by tags) |
|
|
63
|
+
|
|
64
|
+
### Context Summarizer (4 tools)
|
|
65
|
+
|
|
66
|
+
| Tool | Description |
|
|
67
|
+
|------|-------------|
|
|
68
|
+
| `context_summarize` | Ringkas text, extract key points, decisions, action items |
|
|
69
|
+
| `context_get_summary` | Ambil summary by ID |
|
|
70
|
+
| `context_list_summaries` | List semua summaries |
|
|
71
|
+
| `context_merge_summaries` | Gabungkan beberapa summaries |
|
|
72
|
+
|
|
73
|
+
### Project Tracker (6 tools)
|
|
74
|
+
|
|
75
|
+
| Tool | Description |
|
|
76
|
+
|------|-------------|
|
|
77
|
+
| `tracker_log` | Log decision/change/todo/note/error |
|
|
78
|
+
| `tracker_status` | Get project status overview |
|
|
79
|
+
| `tracker_todo_update` | Update todo status |
|
|
80
|
+
| `tracker_search` | Search tracker entries |
|
|
81
|
+
| `tracker_set_project` | Set project name |
|
|
82
|
+
| `tracker_export` | Export tracker as markdown |
|
|
83
|
+
|
|
84
|
+
### Session Checkpoint (5 tools)
|
|
85
|
+
|
|
86
|
+
| Tool | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `checkpoint_save` | Save session state |
|
|
89
|
+
| `checkpoint_load` | Load checkpoint |
|
|
90
|
+
| `checkpoint_list` | List all checkpoints |
|
|
91
|
+
| `checkpoint_delete` | Delete checkpoint |
|
|
92
|
+
| `checkpoint_compare` | Compare 2 checkpoints |
|
|
93
|
+
|
|
94
|
+
### Smart File Loader (4 tools)
|
|
95
|
+
|
|
96
|
+
| Tool | Description |
|
|
97
|
+
|------|-------------|
|
|
98
|
+
| `file_smart_read` | Read file with smart options (lines, keywords, structure) |
|
|
99
|
+
| `file_info` | Get file metadata |
|
|
100
|
+
| `file_search_content` | Search pattern in file |
|
|
101
|
+
| `file_list_dir` | List directory files |
|
|
102
|
+
|
|
103
|
+
## Usage Examples
|
|
104
|
+
|
|
105
|
+
### Memory Store
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
// Save important context
|
|
109
|
+
memory_set({ key: "api_base_url", value: "https://api.example.com", tags: ["config"] })
|
|
110
|
+
|
|
111
|
+
// Retrieve later
|
|
112
|
+
memory_get({ key: "api_base_url" })
|
|
113
|
+
|
|
114
|
+
// Search by tags
|
|
115
|
+
memory_search({ tags: ["config"] })
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Context Summarizer
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
// Summarize long conversation
|
|
122
|
+
context_summarize({
|
|
123
|
+
text: "..long conversation..",
|
|
124
|
+
maxLength: 2000,
|
|
125
|
+
sessionId: "session-123"
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Merge multiple summaries
|
|
129
|
+
context_merge_summaries({ ids: ["sum_1", "sum_2", "sum_3"] })
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Project Tracker
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
// Log a decision
|
|
136
|
+
tracker_log({ type: "decision", content: "Using PostgreSQL for database", tags: ["database"] })
|
|
137
|
+
|
|
138
|
+
// Log a todo
|
|
139
|
+
tracker_log({ type: "todo", content: "Implement user authentication" })
|
|
140
|
+
|
|
141
|
+
// Get project status
|
|
142
|
+
tracker_status()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Session Checkpoint
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
// Save checkpoint before major changes
|
|
149
|
+
checkpoint_save({
|
|
150
|
+
name: "before-refactor",
|
|
151
|
+
description: "Completed user module, starting refactor",
|
|
152
|
+
state: { currentTask: "refactoring", completedFiles: ["user.ts"] },
|
|
153
|
+
files: ["src/user.ts", "src/auth.ts"]
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Load checkpoint in new session
|
|
157
|
+
checkpoint_load({ name: "before-refactor" })
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Smart File Loader
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
// Read only code structure
|
|
164
|
+
file_smart_read({ path: "src/index.ts", structureOnly: true })
|
|
165
|
+
|
|
166
|
+
// Read specific lines
|
|
167
|
+
file_smart_read({ path: "src/index.ts", startLine: 50, endLine: 100 })
|
|
168
|
+
|
|
169
|
+
// Search by keywords
|
|
170
|
+
file_smart_read({ path: "src/index.ts", keywords: ["function", "export"] })
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Best Practices untuk Optimasi Context
|
|
174
|
+
|
|
175
|
+
1. **Gunakan memory_set** untuk menyimpan informasi penting yang perlu diingat
|
|
176
|
+
2. **Buat checkpoint** sebelum context terlalu panjang
|
|
177
|
+
3. **Summarize conversation** secara berkala
|
|
178
|
+
4. **Track decisions** agar tidak perlu explain ulang
|
|
179
|
+
5. **Load file secara smart** - hanya bagian yang diperlukan
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { initStore } from './storage/file-store.js';
|
|
6
|
+
import { registerMemoryTools } from './tools/memory.js';
|
|
7
|
+
import { registerSummarizerTools } from './tools/summarizer.js';
|
|
8
|
+
import { registerTrackerTools } from './tools/tracker.js';
|
|
9
|
+
import { registerCheckpointTools } from './tools/checkpoint.js';
|
|
10
|
+
import { registerLoaderTools } from './tools/loader.js';
|
|
11
|
+
import { registerPrompts } from './prompts.js';
|
|
12
|
+
const SERVER_NAME = 'mcp-context-manager';
|
|
13
|
+
const SERVER_VERSION = '1.0.0';
|
|
14
|
+
async function main() {
|
|
15
|
+
const contextPath = process.env.MCP_CONTEXT_PATH || path.join(process.cwd(), '.context');
|
|
16
|
+
initStore(contextPath);
|
|
17
|
+
const server = new McpServer({
|
|
18
|
+
name: SERVER_NAME,
|
|
19
|
+
version: SERVER_VERSION
|
|
20
|
+
});
|
|
21
|
+
registerMemoryTools(server);
|
|
22
|
+
registerSummarizerTools(server);
|
|
23
|
+
registerTrackerTools(server);
|
|
24
|
+
registerCheckpointTools(server);
|
|
25
|
+
registerLoaderTools(server);
|
|
26
|
+
registerPrompts(server);
|
|
27
|
+
const transport = new StdioServerTransport();
|
|
28
|
+
await server.connect(transport);
|
|
29
|
+
console.error(`${SERVER_NAME} v${SERVER_VERSION} started`);
|
|
30
|
+
console.error(`Context path: ${contextPath}`);
|
|
31
|
+
}
|
|
32
|
+
main().catch((error) => {
|
|
33
|
+
console.error('Fatal error:', error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
export function registerPrompts(server) {
|
|
3
|
+
// ctx-init - Load previous context
|
|
4
|
+
server.registerPrompt('ctx-init', {
|
|
5
|
+
title: 'Init Session',
|
|
6
|
+
description: 'Load previous context at session start'
|
|
7
|
+
}, () => ({
|
|
8
|
+
messages: [{
|
|
9
|
+
role: 'user',
|
|
10
|
+
content: {
|
|
11
|
+
type: 'text',
|
|
12
|
+
text: `Load previous context:
|
|
13
|
+
1. checkpoint_load() - get last state
|
|
14
|
+
2. tracker_status() - see todos & decisions
|
|
15
|
+
3. memory_list() - check saved data`
|
|
16
|
+
}
|
|
17
|
+
}]
|
|
18
|
+
}));
|
|
19
|
+
// ctx-save - Quick save current state
|
|
20
|
+
server.registerPrompt('ctx-save', {
|
|
21
|
+
title: 'Save State',
|
|
22
|
+
description: 'Quick save current session state',
|
|
23
|
+
argsSchema: {
|
|
24
|
+
name: z.string().optional().describe('Checkpoint name')
|
|
25
|
+
}
|
|
26
|
+
}, ({ name }) => ({
|
|
27
|
+
messages: [{
|
|
28
|
+
role: 'user',
|
|
29
|
+
content: {
|
|
30
|
+
type: 'text',
|
|
31
|
+
text: `Save current state to checkpoint "${name || 'auto'}". Include: current task, progress, important decisions, modified files.`
|
|
32
|
+
}
|
|
33
|
+
}]
|
|
34
|
+
}));
|
|
35
|
+
// ctx-remember - Save to memory
|
|
36
|
+
server.registerPrompt('ctx-remember', {
|
|
37
|
+
title: 'Remember',
|
|
38
|
+
description: 'Save important info to memory',
|
|
39
|
+
argsSchema: {
|
|
40
|
+
what: z.string().describe('What to remember')
|
|
41
|
+
}
|
|
42
|
+
}, ({ what }) => ({
|
|
43
|
+
messages: [{
|
|
44
|
+
role: 'user',
|
|
45
|
+
content: {
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: `Save to memory: "${what}". Use descriptive key and appropriate tags.`
|
|
48
|
+
}
|
|
49
|
+
}]
|
|
50
|
+
}));
|
|
51
|
+
// ctx-todo - Add todo item
|
|
52
|
+
server.registerPrompt('ctx-todo', {
|
|
53
|
+
title: 'Add Todo',
|
|
54
|
+
description: 'Log a todo item',
|
|
55
|
+
argsSchema: {
|
|
56
|
+
task: z.string().describe('Task to do')
|
|
57
|
+
}
|
|
58
|
+
}, ({ task }) => ({
|
|
59
|
+
messages: [{
|
|
60
|
+
role: 'user',
|
|
61
|
+
content: {
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: `Log todo: "${task}"`
|
|
64
|
+
}
|
|
65
|
+
}]
|
|
66
|
+
}));
|
|
67
|
+
// ctx-decide - Log decision
|
|
68
|
+
server.registerPrompt('ctx-decide', {
|
|
69
|
+
title: 'Log Decision',
|
|
70
|
+
description: 'Record a decision',
|
|
71
|
+
argsSchema: {
|
|
72
|
+
decision: z.string().describe('Decision made')
|
|
73
|
+
}
|
|
74
|
+
}, ({ decision }) => ({
|
|
75
|
+
messages: [{
|
|
76
|
+
role: 'user',
|
|
77
|
+
content: {
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: `Log decision: "${decision}"`
|
|
80
|
+
}
|
|
81
|
+
}]
|
|
82
|
+
}));
|
|
83
|
+
// ctx-status - Get current status
|
|
84
|
+
server.registerPrompt('ctx-status', {
|
|
85
|
+
title: 'Status',
|
|
86
|
+
description: 'Show project status'
|
|
87
|
+
}, () => ({
|
|
88
|
+
messages: [{
|
|
89
|
+
role: 'user',
|
|
90
|
+
content: {
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: `Show: tracker_status(), memory_list(), checkpoint_list()`
|
|
93
|
+
}
|
|
94
|
+
}]
|
|
95
|
+
}));
|
|
96
|
+
// ctx-compress - Summarize long context
|
|
97
|
+
server.registerPrompt('ctx-compress', {
|
|
98
|
+
title: 'Compress Context',
|
|
99
|
+
description: 'Summarize to save context space'
|
|
100
|
+
}, () => ({
|
|
101
|
+
messages: [{
|
|
102
|
+
role: 'user',
|
|
103
|
+
content: {
|
|
104
|
+
type: 'text',
|
|
105
|
+
text: `Context getting long. Summarize conversation with context_summarize(), save checkpoint, store key info to memory.`
|
|
106
|
+
}
|
|
107
|
+
}]
|
|
108
|
+
}));
|
|
109
|
+
// ctx-recall - Search memories
|
|
110
|
+
server.registerPrompt('ctx-recall', {
|
|
111
|
+
title: 'Recall',
|
|
112
|
+
description: 'Search saved memories',
|
|
113
|
+
argsSchema: {
|
|
114
|
+
query: z.string().optional().describe('Search term')
|
|
115
|
+
}
|
|
116
|
+
}, ({ query }) => ({
|
|
117
|
+
messages: [{
|
|
118
|
+
role: 'user',
|
|
119
|
+
content: {
|
|
120
|
+
type: 'text',
|
|
121
|
+
text: query
|
|
122
|
+
? `Search memories for: "${query}"`
|
|
123
|
+
: `List all memories with memory_list()`
|
|
124
|
+
}
|
|
125
|
+
}]
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface StoreOptions {
|
|
2
|
+
basePath: string;
|
|
3
|
+
}
|
|
4
|
+
export declare class FileStore {
|
|
5
|
+
private basePath;
|
|
6
|
+
constructor(options: StoreOptions);
|
|
7
|
+
private ensureDir;
|
|
8
|
+
private getFilePath;
|
|
9
|
+
read<T>(filename: string, defaultValue: T): Promise<T>;
|
|
10
|
+
write<T>(filename: string, data: T): Promise<void>;
|
|
11
|
+
append<T>(filename: string, item: T): Promise<void>;
|
|
12
|
+
delete(filename: string): Promise<boolean>;
|
|
13
|
+
exists(filename: string): Promise<boolean>;
|
|
14
|
+
list(subdir?: string): Promise<string[]>;
|
|
15
|
+
getSubStore(subdir: string): FileStore;
|
|
16
|
+
}
|
|
17
|
+
export declare function getStore(basePath?: string): FileStore;
|
|
18
|
+
export declare function initStore(basePath: string): FileStore;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
export class FileStore {
|
|
4
|
+
basePath;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.basePath = options.basePath;
|
|
7
|
+
this.ensureDir(this.basePath);
|
|
8
|
+
}
|
|
9
|
+
ensureDir(dirPath) {
|
|
10
|
+
if (!fs.existsSync(dirPath)) {
|
|
11
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
getFilePath(filename) {
|
|
15
|
+
return path.join(this.basePath, filename);
|
|
16
|
+
}
|
|
17
|
+
async read(filename, defaultValue) {
|
|
18
|
+
const filePath = this.getFilePath(filename);
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(filePath)) {
|
|
21
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
22
|
+
return JSON.parse(content);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error(`Error reading ${filename}:`, error);
|
|
27
|
+
}
|
|
28
|
+
return defaultValue;
|
|
29
|
+
}
|
|
30
|
+
async write(filename, data) {
|
|
31
|
+
const filePath = this.getFilePath(filename);
|
|
32
|
+
const dir = path.dirname(filePath);
|
|
33
|
+
this.ensureDir(dir);
|
|
34
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
35
|
+
}
|
|
36
|
+
async append(filename, item) {
|
|
37
|
+
const existing = await this.read(filename, []);
|
|
38
|
+
existing.push(item);
|
|
39
|
+
await this.write(filename, existing);
|
|
40
|
+
}
|
|
41
|
+
async delete(filename) {
|
|
42
|
+
const filePath = this.getFilePath(filename);
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(filePath)) {
|
|
45
|
+
fs.unlinkSync(filePath);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error(`Error deleting ${filename}:`, error);
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
async exists(filename) {
|
|
55
|
+
return fs.existsSync(this.getFilePath(filename));
|
|
56
|
+
}
|
|
57
|
+
async list(subdir) {
|
|
58
|
+
const dirPath = subdir ? path.join(this.basePath, subdir) : this.basePath;
|
|
59
|
+
this.ensureDir(dirPath);
|
|
60
|
+
try {
|
|
61
|
+
return fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
getSubStore(subdir) {
|
|
68
|
+
return new FileStore({
|
|
69
|
+
basePath: path.join(this.basePath, subdir)
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
let storeInstance = null;
|
|
74
|
+
export function getStore(basePath) {
|
|
75
|
+
if (!storeInstance) {
|
|
76
|
+
const defaultPath = path.join(process.cwd(), '.context');
|
|
77
|
+
storeInstance = new FileStore({ basePath: basePath || defaultPath });
|
|
78
|
+
}
|
|
79
|
+
return storeInstance;
|
|
80
|
+
}
|
|
81
|
+
export function initStore(basePath) {
|
|
82
|
+
storeInstance = new FileStore({ basePath });
|
|
83
|
+
return storeInstance;
|
|
84
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
import { getStore } from '../storage/file-store.js';
|
|
3
|
+
const CHECKPOINTS_DIR = 'checkpoints';
|
|
4
|
+
async function getCheckpointStore() {
|
|
5
|
+
const store = getStore().getSubStore(CHECKPOINTS_DIR);
|
|
6
|
+
return store.read('index.json', { checkpoints: [] });
|
|
7
|
+
}
|
|
8
|
+
async function saveCheckpointStore(data) {
|
|
9
|
+
const store = getStore().getSubStore(CHECKPOINTS_DIR);
|
|
10
|
+
await store.write('index.json', data);
|
|
11
|
+
}
|
|
12
|
+
async function saveCheckpointData(id, data) {
|
|
13
|
+
const store = getStore().getSubStore(CHECKPOINTS_DIR);
|
|
14
|
+
await store.write(`${id}.json`, data);
|
|
15
|
+
}
|
|
16
|
+
async function loadCheckpointData(id) {
|
|
17
|
+
const store = getStore().getSubStore(CHECKPOINTS_DIR);
|
|
18
|
+
if (await store.exists(`${id}.json`)) {
|
|
19
|
+
return store.read(`${id}.json`, {});
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export function registerCheckpointTools(server) {
|
|
24
|
+
server.registerTool('checkpoint_save', {
|
|
25
|
+
title: 'Save Checkpoint',
|
|
26
|
+
description: 'Save current session state as a checkpoint. Use before context gets too long or at important milestones.',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
name: z.string().describe('Checkpoint name (e.g., "before-refactor", "feature-complete")'),
|
|
29
|
+
description: z.string().optional().describe('Description of what was accomplished'),
|
|
30
|
+
state: z.record(z.unknown()).describe('State data to save (conversation summary, current task, etc.)'),
|
|
31
|
+
files: z.array(z.string()).optional().describe('List of relevant file paths')
|
|
32
|
+
}
|
|
33
|
+
}, async ({ name, description, state, files }) => {
|
|
34
|
+
const checkpointStore = await getCheckpointStore();
|
|
35
|
+
const checkpoint = {
|
|
36
|
+
id: `cp_${Date.now()}`,
|
|
37
|
+
name,
|
|
38
|
+
description,
|
|
39
|
+
state,
|
|
40
|
+
files: files || [],
|
|
41
|
+
createdAt: new Date().toISOString()
|
|
42
|
+
};
|
|
43
|
+
await saveCheckpointData(checkpoint.id, state);
|
|
44
|
+
checkpointStore.checkpoints.push({
|
|
45
|
+
...checkpoint,
|
|
46
|
+
state: {}
|
|
47
|
+
});
|
|
48
|
+
await saveCheckpointStore(checkpointStore);
|
|
49
|
+
return {
|
|
50
|
+
content: [{
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: `Checkpoint saved: "${name}" (ID: ${checkpoint.id})\nDescription: ${description || 'N/A'}\nFiles: ${files?.length || 0}`
|
|
53
|
+
}]
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
server.registerTool('checkpoint_load', {
|
|
57
|
+
title: 'Load Checkpoint',
|
|
58
|
+
description: 'Load a previously saved checkpoint to restore context.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
id: z.string().optional().describe('Checkpoint ID (loads latest if not specified)'),
|
|
61
|
+
name: z.string().optional().describe('Checkpoint name to search for')
|
|
62
|
+
}
|
|
63
|
+
}, async ({ id, name }) => {
|
|
64
|
+
const checkpointStore = await getCheckpointStore();
|
|
65
|
+
let checkpoint;
|
|
66
|
+
if (id) {
|
|
67
|
+
checkpoint = checkpointStore.checkpoints.find(cp => cp.id === id);
|
|
68
|
+
}
|
|
69
|
+
else if (name) {
|
|
70
|
+
checkpoint = checkpointStore.checkpoints
|
|
71
|
+
.filter(cp => cp.name.toLowerCase().includes(name.toLowerCase()))
|
|
72
|
+
.pop();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
checkpoint = checkpointStore.checkpoints[checkpointStore.checkpoints.length - 1];
|
|
76
|
+
}
|
|
77
|
+
if (!checkpoint) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: 'text', text: 'Checkpoint not found' }]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const stateData = await loadCheckpointData(checkpoint.id);
|
|
83
|
+
const output = {
|
|
84
|
+
id: checkpoint.id,
|
|
85
|
+
name: checkpoint.name,
|
|
86
|
+
description: checkpoint.description,
|
|
87
|
+
createdAt: checkpoint.createdAt,
|
|
88
|
+
files: checkpoint.files,
|
|
89
|
+
state: stateData
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }]
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
server.registerTool('checkpoint_list', {
|
|
96
|
+
title: 'List Checkpoints',
|
|
97
|
+
description: 'List all saved checkpoints.',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
limit: z.number().optional().describe('Maximum checkpoints to return (default: 10)')
|
|
100
|
+
}
|
|
101
|
+
}, async ({ limit = 10 }) => {
|
|
102
|
+
const checkpointStore = await getCheckpointStore();
|
|
103
|
+
const checkpoints = checkpointStore.checkpoints.slice(-limit);
|
|
104
|
+
if (checkpoints.length === 0) {
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: 'text', text: 'No checkpoints saved' }]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const list = checkpoints.map(cp => ({
|
|
110
|
+
id: cp.id,
|
|
111
|
+
name: cp.name,
|
|
112
|
+
description: cp.description,
|
|
113
|
+
createdAt: cp.createdAt,
|
|
114
|
+
filesCount: cp.files.length
|
|
115
|
+
}));
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: 'text', text: JSON.stringify(list, null, 2) }]
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
server.registerTool('checkpoint_delete', {
|
|
121
|
+
title: 'Delete Checkpoint',
|
|
122
|
+
description: 'Delete a checkpoint by ID.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
id: z.string().describe('Checkpoint ID to delete')
|
|
125
|
+
}
|
|
126
|
+
}, async ({ id }) => {
|
|
127
|
+
const checkpointStore = await getCheckpointStore();
|
|
128
|
+
const index = checkpointStore.checkpoints.findIndex(cp => cp.id === id);
|
|
129
|
+
if (index === -1) {
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: 'text', text: `Checkpoint not found: ${id}` }]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const removed = checkpointStore.checkpoints.splice(index, 1)[0];
|
|
135
|
+
await saveCheckpointStore(checkpointStore);
|
|
136
|
+
const store = getStore().getSubStore(CHECKPOINTS_DIR);
|
|
137
|
+
await store.delete(`${id}.json`);
|
|
138
|
+
return {
|
|
139
|
+
content: [{ type: 'text', text: `Deleted checkpoint: "${removed.name}" (${id})` }]
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
server.registerTool('checkpoint_compare', {
|
|
143
|
+
title: 'Compare Checkpoints',
|
|
144
|
+
description: 'Compare two checkpoints to see what changed.',
|
|
145
|
+
inputSchema: {
|
|
146
|
+
id1: z.string().describe('First checkpoint ID'),
|
|
147
|
+
id2: z.string().describe('Second checkpoint ID')
|
|
148
|
+
}
|
|
149
|
+
}, async ({ id1, id2 }) => {
|
|
150
|
+
const checkpointStore = await getCheckpointStore();
|
|
151
|
+
const cp1 = checkpointStore.checkpoints.find(cp => cp.id === id1);
|
|
152
|
+
const cp2 = checkpointStore.checkpoints.find(cp => cp.id === id2);
|
|
153
|
+
if (!cp1 || !cp2) {
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: 'text', text: 'One or both checkpoints not found' }]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const state1 = await loadCheckpointData(id1) || {};
|
|
159
|
+
const state2 = await loadCheckpointData(id2) || {};
|
|
160
|
+
const allKeys = new Set([...Object.keys(state1), ...Object.keys(state2)]);
|
|
161
|
+
const changes = [];
|
|
162
|
+
for (const key of allKeys) {
|
|
163
|
+
const v1 = JSON.stringify(state1[key]);
|
|
164
|
+
const v2 = JSON.stringify(state2[key]);
|
|
165
|
+
if (v1 !== v2) {
|
|
166
|
+
if (!state1[key]) {
|
|
167
|
+
changes.push({ key, change: 'added' });
|
|
168
|
+
}
|
|
169
|
+
else if (!state2[key]) {
|
|
170
|
+
changes.push({ key, change: 'removed' });
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
changes.push({ key, change: 'modified' });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const newFiles = cp2.files.filter(f => !cp1.files.includes(f));
|
|
178
|
+
const removedFiles = cp1.files.filter(f => !cp2.files.includes(f));
|
|
179
|
+
const comparison = {
|
|
180
|
+
checkpoint1: { id: cp1.id, name: cp1.name, date: cp1.createdAt },
|
|
181
|
+
checkpoint2: { id: cp2.id, name: cp2.name, date: cp2.createdAt },
|
|
182
|
+
stateChanges: changes,
|
|
183
|
+
fileChanges: {
|
|
184
|
+
added: newFiles,
|
|
185
|
+
removed: removedFiles
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: 'text', text: JSON.stringify(comparison, null, 2) }]
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|