memtask 0.0.1__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.
- memtask/__init__.py +5 -0
- memtask/__main__.py +5 -0
- memtask/api.py +328 -0
- memtask/app.py +73 -0
- memtask/cli.py +303 -0
- memtask/manager.py +744 -0
- memtask/storage.py +150 -0
- memtask-0.0.1.dist-info/METADATA +104 -0
- memtask-0.0.1.dist-info/RECORD +12 -0
- memtask-0.0.1.dist-info/WHEEL +5 -0
- memtask-0.0.1.dist-info/entry_points.txt +2 -0
- memtask-0.0.1.dist-info/top_level.txt +1 -0
memtask/storage.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sqlite3
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
10
|
+
MEMTASK_HOME_ENV = "MEMTASK_HOME"
|
|
11
|
+
MEMTASK_DB_PATH_ENV = "MEMTASK_DB_PATH"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def default_home() -> Path:
|
|
15
|
+
return Path(os.environ.get(MEMTASK_HOME_ENV, Path.home() / ".memtask")).expanduser()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_default_db_path() -> Path:
|
|
19
|
+
if os.environ.get(MEMTASK_DB_PATH_ENV):
|
|
20
|
+
return Path(os.environ[MEMTASK_DB_PATH_ENV]).expanduser()
|
|
21
|
+
|
|
22
|
+
repo_db_path = PROJECT_ROOT / "data" / "tasks.sqlite"
|
|
23
|
+
if repo_db_path.exists():
|
|
24
|
+
return repo_db_path
|
|
25
|
+
|
|
26
|
+
return default_home() / "tasks.sqlite"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
DEFAULT_DB_PATH = resolve_default_db_path()
|
|
30
|
+
DB_PATH = DEFAULT_DB_PATH
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def set_db_path(path: str | Path) -> None:
|
|
34
|
+
"""Set the SQLite path used by subsequent manager calls."""
|
|
35
|
+
global DB_PATH
|
|
36
|
+
DB_PATH = Path(path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_db_path() -> Path:
|
|
40
|
+
return DB_PATH
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_connection() -> sqlite3.Connection:
|
|
44
|
+
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
conn = sqlite3.connect(DB_PATH, detect_types=sqlite3.PARSE_DECLTYPES)
|
|
46
|
+
conn.row_factory = sqlite3.Row
|
|
47
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
48
|
+
return conn
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def init_db() -> None:
|
|
52
|
+
with get_connection() as conn:
|
|
53
|
+
conn.executescript(
|
|
54
|
+
"""
|
|
55
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
56
|
+
task_ref TEXT PRIMARY KEY,
|
|
57
|
+
description TEXT NOT NULL,
|
|
58
|
+
project TEXT,
|
|
59
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
60
|
+
memory_refs_json TEXT NOT NULL DEFAULT '[]',
|
|
61
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
62
|
+
is_current INTEGER NOT NULL DEFAULT 0,
|
|
63
|
+
created_at REAL NOT NULL,
|
|
64
|
+
updated_at REAL NOT NULL,
|
|
65
|
+
completed_at REAL
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks (status);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_is_current ON tasks (is_current);
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
72
|
+
memory_id TEXT PRIMARY KEY,
|
|
73
|
+
content TEXT NOT NULL,
|
|
74
|
+
memory_scope TEXT NOT NULL DEFAULT 'global',
|
|
75
|
+
kind TEXT NOT NULL DEFAULT 'fact',
|
|
76
|
+
confidence INTEGER NOT NULL DEFAULT 100,
|
|
77
|
+
parent_memory_id TEXT,
|
|
78
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
79
|
+
created_at REAL NOT NULL,
|
|
80
|
+
updated_at REAL NOT NULL,
|
|
81
|
+
last_accessed_at REAL,
|
|
82
|
+
FOREIGN KEY (parent_memory_id) REFERENCES memories (memory_id) ON DELETE SET NULL
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories (memory_scope);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_memories_kind ON memories (kind);
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_memories_confidence ON memories (confidence);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_memories_parent ON memories (parent_memory_id);
|
|
89
|
+
|
|
90
|
+
CREATE TABLE IF NOT EXISTS task_dependencies (
|
|
91
|
+
task_ref TEXT NOT NULL,
|
|
92
|
+
depends_on_task_ref TEXT NOT NULL,
|
|
93
|
+
PRIMARY KEY (task_ref, depends_on_task_ref),
|
|
94
|
+
FOREIGN KEY (task_ref) REFERENCES tasks (task_ref) ON DELETE CASCADE,
|
|
95
|
+
FOREIGN KEY (depends_on_task_ref) REFERENCES tasks (task_ref) ON DELETE CASCADE
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_task_dependencies_task_ref ON task_dependencies (task_ref);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_task_dependencies_depends_on ON task_dependencies (depends_on_task_ref);
|
|
100
|
+
"""
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def normalize_non_empty_string(value: str, label: str) -> str:
|
|
105
|
+
text = str(value).strip()
|
|
106
|
+
if not text:
|
|
107
|
+
raise ValueError(f"{label} is required")
|
|
108
|
+
return text
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def normalize_confidence(confidence: int) -> int:
|
|
112
|
+
if isinstance(confidence, bool) or not isinstance(confidence, int):
|
|
113
|
+
raise ValueError("confidence must be an integer between 0 and 100")
|
|
114
|
+
if not 0 <= confidence <= 100:
|
|
115
|
+
raise ValueError("confidence must be between 0 and 100")
|
|
116
|
+
return confidence
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def encode_string_list(values: list[str] | None) -> str:
|
|
120
|
+
if values is None:
|
|
121
|
+
return "[]"
|
|
122
|
+
normalized = []
|
|
123
|
+
seen = set()
|
|
124
|
+
for value in values:
|
|
125
|
+
item = str(value).strip()
|
|
126
|
+
if not item or item in seen:
|
|
127
|
+
continue
|
|
128
|
+
seen.add(item)
|
|
129
|
+
normalized.append(item)
|
|
130
|
+
return json.dumps(normalized)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def decode_string_list(value: str | None) -> list[str]:
|
|
134
|
+
if not value:
|
|
135
|
+
return []
|
|
136
|
+
try:
|
|
137
|
+
parsed = json.loads(value)
|
|
138
|
+
except (TypeError, json.JSONDecodeError):
|
|
139
|
+
return []
|
|
140
|
+
if isinstance(parsed, list):
|
|
141
|
+
return [str(item) for item in parsed]
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def encode_tags(tags: list[str] | None) -> str:
|
|
146
|
+
return encode_string_list(tags)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def decode_tags(value: str | None) -> list[str]:
|
|
150
|
+
return decode_string_list(value)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memtask
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A local MCP server for durable agent task state and lightweight memory.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: mcp
|
|
8
|
+
Provides-Extra: test
|
|
9
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="docs/assets/memtask-logo.png" alt="MemTask logo" width="220">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<h1 align="center">MemTask</h1>
|
|
16
|
+
|
|
17
|
+
MemTask is a local MCP server for developer-built agents that need durable task state and lightweight memory. It combines task planning, dependency tracking, active work selection, completion history, and scoped memories in a small SQLite-backed service. The goal is to give agents a structured place to manage agency: what to do next, what depends on what, and what context should persist across sessions.
|
|
18
|
+
|
|
19
|
+
## What It Provides
|
|
20
|
+
|
|
21
|
+
MemTask exposes two local primitives over MCP:
|
|
22
|
+
|
|
23
|
+
- Tasks: pending work, stable task references, parent/child dependency edges, active task selection, and completed task state.
|
|
24
|
+
- Memories: scoped pieces of context with confidence scores, optional parent memory relationships, tags, and task-memory references.
|
|
25
|
+
|
|
26
|
+
The server is intentionally local-first. State lives in SQLite, the tool surface is small, and the manager layer can be tested directly without running an MCP transport.
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
Install MemTask:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install MemTask
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then ask MemTask for the MCP config to add to your agent:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
memtask install-help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Most MCP clients should launch MemTask over stdio:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
memtask start --transport stdio
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If your client connects to a running HTTP server instead, start it in the background:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
memtask start --transport http --host 127.0.0.1 --port 8000
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For local development from this repo:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
PYTHONPATH=src python -m memtask start --transport stdio
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Useful HTTP commands: `memtask status` and `memtask stop`.
|
|
61
|
+
|
|
62
|
+
## Storage
|
|
63
|
+
|
|
64
|
+
Runtime state in this repo is stored in `data/tasks.sqlite`.
|
|
65
|
+
|
|
66
|
+
When installed outside this repo, MemTask uses `~/.memtask/tasks.sqlite` by default. Set `MEMTASK_DB_PATH` to choose a specific SQLite path.
|
|
67
|
+
|
|
68
|
+
The server creates the required SQLite tables on startup using `CREATE TABLE IF NOT EXISTS`. There is no migration framework.
|
|
69
|
+
|
|
70
|
+
## Tools
|
|
71
|
+
|
|
72
|
+
Task tools:
|
|
73
|
+
|
|
74
|
+
- `list_tasks`
|
|
75
|
+
- `get_task`
|
|
76
|
+
- `add_task`
|
|
77
|
+
- `add_batch_tasks`
|
|
78
|
+
- `complete_task`
|
|
79
|
+
- `remove_task`
|
|
80
|
+
- `remove_all_tasks`
|
|
81
|
+
- `current_tasks`
|
|
82
|
+
- `set_current_task`
|
|
83
|
+
|
|
84
|
+
Memory tools:
|
|
85
|
+
|
|
86
|
+
- `remember`
|
|
87
|
+
- `recall`
|
|
88
|
+
- `get_memory`
|
|
89
|
+
- `update_memory`
|
|
90
|
+
- `delete_memory`
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
Run tests:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python -m pytest
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Compile-check the package:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
python -m py_compile src/memtask/*.py tests/*.py
|
|
104
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
memtask/__init__.py,sha256=divNnoUi6IWmEF-sDvLArNGhV7TGKo1NAQizsRdrj08,95
|
|
2
|
+
memtask/__main__.py,sha256=wu5N2wk8mvBgyvr2ghmQf4prezAe0_i-p123VVreyYc,62
|
|
3
|
+
memtask/api.py,sha256=an6h6oZndPz7YR_W2X3kDbThQWPuBvPL1HTFz5mxx6U,11389
|
|
4
|
+
memtask/app.py,sha256=rn0dm4GbdbZ2av4sBT3aRV8hXDdKfxkDaTHvGfFNC5Q,1813
|
|
5
|
+
memtask/cli.py,sha256=vIo3LqBL0HxhlY0uNjqLmjvQd4tCCT87dSa5jDNJ0aA,8313
|
|
6
|
+
memtask/manager.py,sha256=92S57jlFUI2hlO4PsrhN-nOyisZlAfmz-Vy5qWob3ak,24316
|
|
7
|
+
memtask/storage.py,sha256=J4x9X0AlB5yLJ9F1szi_aH-5zvhP46dRk6AZoz3MGSw,4928
|
|
8
|
+
memtask-0.0.1.dist-info/METADATA,sha256=jHsV5GZfV543jQXdqniY7tK1N4U71riMOHxGPGrrSa4,2665
|
|
9
|
+
memtask-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
memtask-0.0.1.dist-info/entry_points.txt,sha256=tL-GQyfa41gXgzWQc3vG6chNQ90S6VgfDH2yMzI45o0,45
|
|
11
|
+
memtask-0.0.1.dist-info/top_level.txt,sha256=h6tvWvx4NeUvwA6UG0IXdjQCc_OL6KIM5TwMWl2Poqw,8
|
|
12
|
+
memtask-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
memtask
|