pin-memory 0.1.0__py3-none-any.whl

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.
pin_memory/__init__.py ADDED
File without changes
pin_memory/main.py ADDED
@@ -0,0 +1,4 @@
1
+ from .server import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
pin_memory/server.py ADDED
@@ -0,0 +1,242 @@
1
+ from pathlib import Path
2
+ from typing import Literal
3
+
4
+ import click
5
+ from mcp.server.fastmcp import FastMCP
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+
10
+ class MemoryManager:
11
+ """Manages factoids stored in various scopes."""
12
+
13
+ def __init__(self):
14
+ self.global_dir = Path.home() / '.scartill' / 'pin'
15
+ self.workspaces_root = self.global_dir / 'workspaces'
16
+ self.project_dir = Path('.pin')
17
+
18
+ def _get_dir(self, location: str) -> Path:
19
+ """Resolve the directory for a given location string."""
20
+ if location == 'global':
21
+ return self.global_dir
22
+ if location == 'project':
23
+ return self.project_dir
24
+ if location.startswith('workspace/'):
25
+ workspace_name = location.split('/', 1)[1]
26
+ if not workspace_name:
27
+ raise ValueError('Workspace name cannot be empty')
28
+ return self.workspaces_root / workspace_name
29
+ raise ValueError(f'Invalid location: {location}')
30
+
31
+ def pin(self, factoid_name: str, factoid: str, location: str = 'global') -> str:
32
+ """Store a factoid in memory at the specified location."""
33
+ try:
34
+ target_dir = self._get_dir(location)
35
+ except ValueError as e:
36
+ return f'Error: {e}'
37
+
38
+ # Ensure name is safe for filesystem
39
+ safe_name = ''.join(c for c in factoid_name if c.isalnum() or c in ('-', '_')).strip()
40
+ if not safe_name:
41
+ return 'Error: factoid_name must contain alphanumeric, dashes, or underscores.'
42
+
43
+ target_dir.mkdir(parents=True, exist_ok=True)
44
+ file_path = target_dir / f'{safe_name}.md'
45
+ try:
46
+ file_path.write_text(factoid, encoding='utf-8')
47
+ return f"Factoid '{factoid_name}' pinned to '{location}' successfully."
48
+ except Exception as e:
49
+ return f'Error writing factoid: {e}'
50
+
51
+ def _read_factoids(self, target_dir: Path) -> list[str]:
52
+ """Read all factoids from a specific directory."""
53
+ factoids = []
54
+ if target_dir.exists() and target_dir.is_dir():
55
+ for file in sorted(target_dir.glob('*.md')):
56
+ try:
57
+ factoids.append(file.read_text(encoding='utf-8'))
58
+ except Exception:
59
+ continue
60
+ return factoids
61
+
62
+ def _get_factoids(self, target_dir: Path) -> list[dict]:
63
+ """Get all factoids from a specific directory as list of dicts {name, content}."""
64
+ results = []
65
+ if target_dir.exists() and target_dir.is_dir():
66
+ for file in sorted(target_dir.glob('*.md')):
67
+ content = None
68
+ # Try common encodings
69
+ for enc in ['utf-8-sig', 'utf-16', 'latin-1']:
70
+ try:
71
+ content = file.read_text(encoding=enc).strip()
72
+ break
73
+ except (UnicodeDecodeError, Exception):
74
+ continue
75
+
76
+ if content is not None:
77
+ results.append({'name': file.stem, 'content': content})
78
+ else:
79
+ # If we really can't read it, add a placeholder so user knows something is there
80
+ results.append(
81
+ {'name': file.stem, 'content': '[ERROR: Could not decode file content]'}
82
+ )
83
+ return results
84
+
85
+ def recall(self, workspace: str | None = None) -> str:
86
+ """Recall pinned factoids from global, optional workspace, and local project scopes."""
87
+ all_factoids: list[str] = []
88
+
89
+ # 1. Global factoids - always
90
+ all_factoids.extend(self._read_factoids(self.global_dir))
91
+
92
+ # 2. Workspace factoids - if workspace is provided
93
+ if workspace:
94
+ workspace_dir = self.workspaces_root / workspace
95
+ all_factoids.extend(self._read_factoids(workspace_dir))
96
+
97
+ # 3. Project factoids - if present
98
+ all_factoids.extend(self._read_factoids(self.project_dir))
99
+
100
+ if not all_factoids:
101
+ return 'No factoids found in memory.'
102
+
103
+ return '\n\n'.join(all_factoids)
104
+
105
+ def list_all(self) -> dict:
106
+ """List all factoids grouped by location."""
107
+ results = {
108
+ 'global': self._get_factoids(self.global_dir),
109
+ 'workspaces': {},
110
+ 'local': self._get_factoids(self.project_dir),
111
+ }
112
+
113
+ if self.workspaces_root.exists() and self.workspaces_root.is_dir():
114
+ for workspace_dir in sorted(self.workspaces_root.iterdir()):
115
+ if workspace_dir.is_dir():
116
+ workspace_factoids = self._get_factoids(workspace_dir)
117
+ if workspace_factoids:
118
+ results['workspaces'][workspace_dir.name] = workspace_factoids
119
+
120
+ return results
121
+
122
+
123
+ @click.group()
124
+ def cli():
125
+ """Pin MCP – Memory management across global, workspace, and project scopes."""
126
+ pass
127
+
128
+
129
+ @cli.command('show')
130
+ def show():
131
+ """List all factoids grouped by location (global, workspaces, and local)."""
132
+ manager = MemoryManager()
133
+ all_factoids = manager.list_all()
134
+ console = Console()
135
+
136
+ def print_section(title: str, factoids: list[dict]):
137
+ if not factoids:
138
+ return
139
+ table = Table(title=title, show_header=True, header_style='bold magenta', box=None)
140
+ table.add_column('Name', style='cyan', no_wrap=True)
141
+ table.add_column('Content', style='green')
142
+ for f in factoids:
143
+ table.add_row(f['name'], f['content'])
144
+ console.print(table)
145
+ console.print()
146
+
147
+ # 1. Global
148
+ print_section('Global Factoids', all_factoids['global'])
149
+
150
+ # 2. Workspaces
151
+ for ws_name, ws_factoids in sorted(all_factoids['workspaces'].items()):
152
+ print_section(f'Workspace: {ws_name}', ws_factoids)
153
+
154
+ # 3. Local
155
+ print_section('Local Project Factoids', all_factoids['local'])
156
+
157
+ if not all_factoids['global'] and not all_factoids['workspaces'] and not all_factoids['local']:
158
+ console.print('[yellow]No factoids found in memory.[/yellow]')
159
+
160
+
161
+ @cli.command('install')
162
+ @click.option('--workspace', '-w', help='Optional workspace name to include in the instructions.')
163
+ def install(workspace: str | None) -> None:
164
+ """Add agent instructions fragment to AGENTS.md."""
165
+ console = Console()
166
+ agents_file = Path('AGENTS.md')
167
+
168
+ start_marker = '<!-- pin mcp start -->'
169
+ stop_marker = '<!-- pin mcp stop -->'
170
+
171
+ instruction = (
172
+ 'Before staring the first task, fetch all global and project factoids via Pin MCP '
173
+ 'and use them as **important** instructions.'
174
+ )
175
+ if workspace:
176
+ instruction += f' The Pin workspace for this project is `{workspace}`.'
177
+
178
+ fragment = f'{start_marker}\n{instruction}\n{stop_marker}\n'
179
+
180
+ if agents_file.exists():
181
+ content = agents_file.read_text(encoding='utf-8')
182
+ if start_marker in content:
183
+ console.print(
184
+ '[yellow]AGENTS.md already contains the Pin MCP fragment. Skipping.[/yellow]'
185
+ )
186
+ return
187
+ # Append to existing file
188
+ try:
189
+ with agents_file.open('a', encoding='utf-8') as f:
190
+ # Ensure we start on a new line if the file doesn't end with one
191
+ if content and not content.endswith('\n'):
192
+ f.write('\n')
193
+ f.write(f'\n{fragment}')
194
+ console.print('[green]Pin MCP fragment added to AGENTS.md.[/green]')
195
+ except Exception as e:
196
+ console.print(f'[red]Error updating AGENTS.md: {e}[/red]')
197
+ else:
198
+ # Create new file
199
+ try:
200
+ agents_file.write_text(fragment, encoding='utf-8')
201
+ console.print('[green]Created AGENTS.md with Pin MCP fragment.[/green]')
202
+ except Exception as e:
203
+ console.print(f'[red]Error creating AGENTS.md: {e}[/red]')
204
+
205
+
206
+ @cli.command('mcp')
207
+ def mcp_cmd():
208
+ """Launch the Pin MCP server (stdio)."""
209
+ mcp = FastMCP('Pin')
210
+ manager = MemoryManager()
211
+
212
+ @mcp.tool()
213
+ def pin(factoid_name: str, factoid: str, location: Literal['global', 'project'] | str) -> str:
214
+ """Store a factoid in memory.
215
+
216
+ If asked to pin to a workspace, use "workspace/<name>" for location. Example: "workspace/my-workspace".
217
+
218
+ If asked to pin locally or for the current projects, use "project" for location.
219
+
220
+ Args:
221
+ factoid_name: The name of the factoid.
222
+ factoid: The fact content.
223
+ location: Where to store it ("global", "workspace/<name>", or "project").
224
+ """
225
+ return manager.pin(factoid_name, factoid, location)
226
+
227
+ @mcp.tool()
228
+ def recall(workspace: str | None = None) -> str:
229
+ """Recall pinned factoids.
230
+
231
+ Returns global, requested workspace, and local project factoids.
232
+
233
+ Args:
234
+ workspace: Optional name of the workspace to include.
235
+ """
236
+ return manager.recall(workspace)
237
+
238
+ mcp.run()
239
+
240
+
241
+ def main():
242
+ cli()
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: pin-memory
3
+ Version: 0.1.0
4
+ Summary: A Memory MCP server for pinning and recalling factoids.
5
+ Project-URL: Homepage, https://github.com/scartill/pin-memory
6
+ Project-URL: Changelog, https://github.com/scartill/pin-memory/blob/main/CHANGELOG.md
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: click
9
+ Requires-Dist: mcp[cli]
10
+ Requires-Dist: rich
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Pin Memory
14
+
15
+ A Model Context Protocol (MCP) server for managing factoids across different scopes (global, workspace, and project).
16
+
17
+ ## Features
18
+
19
+ - **Pin**: Store factoids in global, workspace-specific, or project-local storage.
20
+ - **Recall**: Retrieve factoids based on current context.
21
+ - **MCP Integration**: Exposes tools for LLMs to autonomously remember and recall information.
22
+
23
+ ## Installation
24
+
25
+ Using `uv` (recommended):
26
+
27
+ ```bash
28
+ # Install as a global tool
29
+ uv tool install .
30
+
31
+ # Or run directly
32
+ uv run pin-memory mcp
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### CLI Commands
38
+
39
+ Pin Memory provides a CLI for managing factoids:
40
+
41
+ ```bash
42
+ # List all factoids
43
+ pin-memory show
44
+
45
+ # Install agent instructions to AGENTS.md
46
+ pin-memory install --workspace my-workspace
47
+
48
+ # Launch the MCP server
49
+ pin-memory mcp
50
+ ```
51
+
52
+ ### Scopes
53
+
54
+ - **Global**: Stored in `~/.scartill/pin`.
55
+ - **Workspace**: Stored in `~/.scartill/pin/workspaces/<workspace_name>`.
56
+ - **Project**: Stored in `.pin` in the current working directory.
57
+
58
+ ## MCP Configuration
59
+
60
+ To use this with an MCP client (e.g. Claude Desktop, Gemini CLI), add it to your configuration:
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "pin-memory": {
66
+ "command": "uv",
67
+ "args": [
68
+ "--directory",
69
+ "C:/path/to/pin-memory",
70
+ "run",
71
+ "pin-memory",
72
+ "mcp"
73
+ ]
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ## MCP Tools
80
+
81
+ ### `pin`
82
+
83
+ Stores a factoid in memory.
84
+
85
+ - `factoid_name`: Name of the factoid.
86
+ - `factoid`: The content of the factoid.
87
+ - `location`: "global", "project", or "workspace/<name>".
88
+
89
+ ### `recall`
90
+
91
+ Recalls all pinned factoids from global, requested workspace, and local project scopes.
92
+
93
+ - `workspace`: Optional name of the workspace.
94
+
95
+ ## Development
96
+
97
+ ```bash
98
+ # Clone the repository
99
+ git clone https://github.com/scartill/pin-memory.git
100
+ cd pin-memory
101
+
102
+ # Sync dependencies
103
+ uv sync
104
+
105
+ # Run tests (if applicable)
106
+ uv run pytest
107
+ ```
@@ -0,0 +1,7 @@
1
+ pin_memory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pin_memory/main.py,sha256=tsUTknY-RMVcmeThCU7_f58cuicrPOSGRGmGD7zLjCU,68
3
+ pin_memory/server.py,sha256=BkX3KZ5K7BlYazOcQnUke8Fi_uz_9CIhEaGcZlpUl98,9203
4
+ pin_memory-0.1.0.dist-info/METADATA,sha256=wegGLNFInx1Kzogv5i84QfdFnOie68aBmDYIFx50y7o,2245
5
+ pin_memory-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ pin_memory-0.1.0.dist-info/entry_points.txt,sha256=Mvj7xw_kHPpvyxxgmYEKIs55Oa3ZTyonpRSeWeH9jU8,52
7
+ pin_memory-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pin-memory = pin_memory.main:main