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/cli.py ADDED
@@ -0,0 +1,303 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import signal
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ from pathlib import Path
11
+
12
+ from .storage import default_home, get_db_path
13
+
14
+
15
+ DEFAULT_HOST = "127.0.0.1"
16
+ DEFAULT_PORT = 8000
17
+ DEFAULT_SERVER_NAME = "MemTask"
18
+ PID_FILE = "memtask.pid"
19
+ LOG_FILE = "memtask.log"
20
+
21
+
22
+ def _runtime_dir() -> Path:
23
+ path = default_home()
24
+ path.mkdir(parents=True, exist_ok=True)
25
+ return path
26
+
27
+
28
+ def _pid_path() -> Path:
29
+ return _runtime_dir() / PID_FILE
30
+
31
+
32
+ def _log_path() -> Path:
33
+ return _runtime_dir() / LOG_FILE
34
+
35
+
36
+ def _pid_is_running(pid: int) -> bool:
37
+ if pid <= 0:
38
+ return False
39
+ try:
40
+ os.kill(pid, 0)
41
+ except ProcessLookupError:
42
+ return False
43
+ except PermissionError:
44
+ return True
45
+ return True
46
+
47
+
48
+ def _read_pid_record() -> dict[str, object] | None:
49
+ path = _pid_path()
50
+ if not path.exists():
51
+ return None
52
+ try:
53
+ return json.loads(path.read_text())
54
+ except (OSError, json.JSONDecodeError):
55
+ return None
56
+
57
+
58
+ def _write_pid_record(record: dict[str, object]) -> None:
59
+ _pid_path().write_text(json.dumps(record, indent=2, sort_keys=True))
60
+
61
+
62
+ def _clear_pid_record() -> None:
63
+ try:
64
+ _pid_path().unlink()
65
+ except FileNotFoundError:
66
+ pass
67
+
68
+
69
+ def _mcp_url(host: str, port: int) -> str:
70
+ return f"http://{host}:{port}/mcp"
71
+
72
+
73
+ def _stdio_config_text() -> str:
74
+ return json.dumps(
75
+ {
76
+ "mcpServers": {
77
+ "memtask": {
78
+ "command": "memtask",
79
+ "args": ["start", "--transport", "stdio"],
80
+ }
81
+ }
82
+ },
83
+ indent=2,
84
+ )
85
+
86
+
87
+ def _http_config_text(host: str, port: int) -> str:
88
+ return json.dumps(
89
+ {
90
+ "mcpServers": {
91
+ "memtask": {
92
+ "url": _mcp_url(host, port),
93
+ }
94
+ }
95
+ },
96
+ indent=2,
97
+ )
98
+
99
+
100
+ def _print_stdio_guidance() -> None:
101
+ print("MemTask starting over stdio.", file=sys.stderr)
102
+ print("For MCP clients that launch stdio servers, configure:", file=sys.stderr)
103
+ print(_stdio_config_text(), file=sys.stderr)
104
+ print(f"SQLite state: {get_db_path()}", file=sys.stderr)
105
+
106
+
107
+ def _print_http_guidance(host: str, port: int, pid: int | None = None) -> None:
108
+ print("MemTask HTTP server started.")
109
+ if pid is not None:
110
+ print(f"PID: {pid}")
111
+ print(f"URL: {_mcp_url(host, port)}")
112
+ print(f"SQLite state: {get_db_path()}")
113
+ print(f"Log file: {_log_path()}")
114
+ print()
115
+ print("For MCP clients that connect to HTTP servers, configure:")
116
+ print(_http_config_text(host, port))
117
+
118
+
119
+ def _print_install_help(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> None:
120
+ print("MemTask MCP configuration examples")
121
+ print()
122
+ print("Stdio launch configuration:")
123
+ print(_stdio_config_text())
124
+ print()
125
+ print("HTTP mode:")
126
+ print(f" memtask start --transport http --host {host} --port {port}")
127
+ print()
128
+ print("HTTP configuration:")
129
+ print(_http_config_text(host, port))
130
+ print()
131
+ print(f"Default SQLite state: {get_db_path()}")
132
+
133
+
134
+ def _run_server(**kwargs) -> None:
135
+ from .app import run_server
136
+
137
+ run_server(**kwargs)
138
+
139
+
140
+ def _start_stdio(args: argparse.Namespace) -> None:
141
+ _print_stdio_guidance()
142
+ _run_server(
143
+ transport="stdio",
144
+ host=args.host,
145
+ port=args.port,
146
+ server_name=args.server_name,
147
+ )
148
+
149
+
150
+ def _start_http(args: argparse.Namespace) -> None:
151
+ record = _read_pid_record()
152
+ if record is not None:
153
+ pid = int(record.get("pid", 0))
154
+ if _pid_is_running(pid):
155
+ print("MemTask HTTP server is already running.")
156
+ print(f"PID: {pid}")
157
+ print(f"URL: {record.get('url', _mcp_url(args.host, args.port))}")
158
+ print(f"Stop it with: memtask stop")
159
+ return
160
+ _clear_pid_record()
161
+
162
+ log_handle = _log_path().open("ab")
163
+ command = [
164
+ sys.executable,
165
+ "-m",
166
+ "memtask.app",
167
+ "--transport",
168
+ "streamable-http",
169
+ "--host",
170
+ args.host,
171
+ "--port",
172
+ str(args.port),
173
+ "--server-name",
174
+ args.server_name,
175
+ ]
176
+ popen_kwargs = {
177
+ "stdin": subprocess.DEVNULL,
178
+ "stdout": log_handle,
179
+ "stderr": log_handle,
180
+ "close_fds": True,
181
+ }
182
+ if os.name == "posix":
183
+ popen_kwargs["start_new_session"] = True
184
+
185
+ try:
186
+ process = subprocess.Popen(command, **popen_kwargs)
187
+ finally:
188
+ log_handle.close()
189
+ time.sleep(0.2)
190
+ if process.poll() is not None:
191
+ raise RuntimeError(f"MemTask server exited during startup. See log: {_log_path()}")
192
+
193
+ record = {
194
+ "pid": process.pid,
195
+ "transport": "streamable-http",
196
+ "host": args.host,
197
+ "port": args.port,
198
+ "url": _mcp_url(args.host, args.port),
199
+ "log_file": str(_log_path()),
200
+ "db_path": str(get_db_path()),
201
+ "started_at": time.time(),
202
+ }
203
+ _write_pid_record(record)
204
+ _print_http_guidance(args.host, args.port, pid=process.pid)
205
+
206
+
207
+ def _start(args: argparse.Namespace) -> None:
208
+ if args.transport == "stdio":
209
+ _start_stdio(args)
210
+ return
211
+ _start_http(args)
212
+
213
+
214
+ def _stop(_: argparse.Namespace) -> None:
215
+ record = _read_pid_record()
216
+ if record is None:
217
+ print("No MemTask background server is registered.")
218
+ print("If you are using stdio mode, your MCP client starts and stops MemTask itself.")
219
+ return
220
+
221
+ pid = int(record.get("pid", 0))
222
+ if not _pid_is_running(pid):
223
+ _clear_pid_record()
224
+ print("Removed stale MemTask PID file.")
225
+ return
226
+
227
+ os.kill(pid, signal.SIGTERM)
228
+ deadline = time.time() + 5
229
+ while time.time() < deadline:
230
+ if not _pid_is_running(pid):
231
+ break
232
+ time.sleep(0.1)
233
+
234
+ _clear_pid_record()
235
+ print("MemTask HTTP server stopped.")
236
+ print("For stdio MCP clients, no stop command is needed; the client owns the server process.")
237
+
238
+
239
+ def _status(_: argparse.Namespace) -> None:
240
+ record = _read_pid_record()
241
+ if record is None:
242
+ print("MemTask background HTTP server is not running.")
243
+ print(f"SQLite state: {get_db_path()}")
244
+ print("For MCP client setup, run: memtask install-help")
245
+ return
246
+
247
+ pid = int(record.get("pid", 0))
248
+ if not _pid_is_running(pid):
249
+ _clear_pid_record()
250
+ print("MemTask background HTTP server is not running. Removed stale PID file.")
251
+ print(f"SQLite state: {get_db_path()}")
252
+ return
253
+
254
+ print("MemTask background HTTP server is running.")
255
+ print(f"PID: {pid}")
256
+ print(f"URL: {record.get('url')}")
257
+ print(f"SQLite state: {record.get('db_path', get_db_path())}")
258
+ print(f"Log file: {record.get('log_file', _log_path())}")
259
+
260
+
261
+ def build_parser() -> argparse.ArgumentParser:
262
+ parser = argparse.ArgumentParser(prog="memtask")
263
+ subcommands = parser.add_subparsers(dest="command", required=True)
264
+
265
+ start = subcommands.add_parser("start", help="Start the MemTask MCP server")
266
+ start.add_argument(
267
+ "--transport",
268
+ choices=("stdio", "http", "streamable-http"),
269
+ default="stdio",
270
+ help="Use stdio in the foreground or HTTP as a background server.",
271
+ )
272
+ start.add_argument("--host", default=DEFAULT_HOST)
273
+ start.add_argument("--port", type=int, default=DEFAULT_PORT)
274
+ start.add_argument("--server-name", default=DEFAULT_SERVER_NAME)
275
+ start.set_defaults(handler=_start)
276
+
277
+ stop = subcommands.add_parser("stop", help="Stop the background HTTP server")
278
+ stop.set_defaults(handler=_stop)
279
+
280
+ status = subcommands.add_parser("status", help="Show background server status")
281
+ status.set_defaults(handler=_status)
282
+
283
+ install_help = subcommands.add_parser(
284
+ "install-help",
285
+ help="Print MCP client configuration examples",
286
+ )
287
+ install_help.add_argument("--host", default=DEFAULT_HOST)
288
+ install_help.add_argument("--port", type=int, default=DEFAULT_PORT)
289
+ install_help.set_defaults(
290
+ handler=lambda args: _print_install_help(host=args.host, port=args.port)
291
+ )
292
+
293
+ return parser
294
+
295
+
296
+ def main(argv: list[str] | None = None) -> None:
297
+ parser = build_parser()
298
+ args = parser.parse_args(argv)
299
+ args.handler(args)
300
+
301
+
302
+ if __name__ == "__main__":
303
+ main()