cortexdb-mcp 0.3.0__tar.gz → 0.3.1__tar.gz
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.
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/.gitignore +8 -0
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/Dockerfile +1 -1
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/PKG-INFO +24 -2
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/README.md +23 -1
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/cortexdb_mcp/__init__.py +1 -1
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/cortexdb_mcp/api.py +2 -2
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/cortexdb_mcp/config.py +145 -145
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/cortexdb_mcp/insights.py +1 -1
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/cortexdb_mcp/server.py +1028 -904
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/pyproject.toml +1 -1
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/tests/test_server.py +189 -28
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/cortexdb_mcp/__main__.py +0 -0
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/tests/__init__.py +0 -0
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/tests/test_insights.py +0 -0
- {cortexdb_mcp-0.3.0 → cortexdb_mcp-0.3.1}/tests/test_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cortexdb-mcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: MCP Server for CortexDB — expose memory operations to AI agents
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -56,6 +56,28 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
> **Windows note (audit POL-3):** if Claude Desktop / Cursor / Windsurf
|
|
60
|
+
> can't find `cortexdb-mcp` on PATH, give it the full path to the
|
|
61
|
+
> installed executable:
|
|
62
|
+
>
|
|
63
|
+
> ```json
|
|
64
|
+
> {
|
|
65
|
+
> "mcpServers": {
|
|
66
|
+
> "cortexdb": {
|
|
67
|
+
> "command": "C:\\Python313\\Scripts\\cortexdb-mcp.exe"
|
|
68
|
+
> }
|
|
69
|
+
> }
|
|
70
|
+
> }
|
|
71
|
+
> ```
|
|
72
|
+
>
|
|
73
|
+
> Locate it with `where cortexdb-mcp` in `cmd.exe` or
|
|
74
|
+
> `(Get-Command cortexdb-mcp).Source` in PowerShell.
|
|
75
|
+
>
|
|
76
|
+
> Some Windows terminals don't have UTF-8 enabled by default, which can
|
|
77
|
+
> garble emoji in `--help` output (they show as `?`). The server itself
|
|
78
|
+
> isn't affected — only the CLI banner. Run
|
|
79
|
+
> `chcp 65001` once per terminal session to fix.
|
|
80
|
+
|
|
59
81
|
### Claude Code (CLI)
|
|
60
82
|
|
|
61
83
|
Edit `~/.claude/mcp.json`:
|
|
@@ -200,7 +222,7 @@ Pre-built prompt templates:
|
|
|
200
222
|
|
|
201
223
|
| Environment Variable | Default | Description |
|
|
202
224
|
|---|---|---|
|
|
203
|
-
| `CORTEXDB_URL` | `https://api.cortexdb.ai` | CortexDB server URL |
|
|
225
|
+
| `CORTEXDB_URL` | `https://api-v1.cortexdb.ai` | CortexDB server URL |
|
|
204
226
|
| `CORTEXDB_API_KEY` | (none) | API key for authentication |
|
|
205
227
|
| `CORTEXDB_TIMEOUT` | `30.0` | HTTP request timeout (seconds) |
|
|
206
228
|
|
|
@@ -43,6 +43,28 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o
|
|
|
43
43
|
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
> **Windows note (audit POL-3):** if Claude Desktop / Cursor / Windsurf
|
|
47
|
+
> can't find `cortexdb-mcp` on PATH, give it the full path to the
|
|
48
|
+
> installed executable:
|
|
49
|
+
>
|
|
50
|
+
> ```json
|
|
51
|
+
> {
|
|
52
|
+
> "mcpServers": {
|
|
53
|
+
> "cortexdb": {
|
|
54
|
+
> "command": "C:\\Python313\\Scripts\\cortexdb-mcp.exe"
|
|
55
|
+
> }
|
|
56
|
+
> }
|
|
57
|
+
> }
|
|
58
|
+
> ```
|
|
59
|
+
>
|
|
60
|
+
> Locate it with `where cortexdb-mcp` in `cmd.exe` or
|
|
61
|
+
> `(Get-Command cortexdb-mcp).Source` in PowerShell.
|
|
62
|
+
>
|
|
63
|
+
> Some Windows terminals don't have UTF-8 enabled by default, which can
|
|
64
|
+
> garble emoji in `--help` output (they show as `?`). The server itself
|
|
65
|
+
> isn't affected — only the CLI banner. Run
|
|
66
|
+
> `chcp 65001` once per terminal session to fix.
|
|
67
|
+
|
|
46
68
|
### Claude Code (CLI)
|
|
47
69
|
|
|
48
70
|
Edit `~/.claude/mcp.json`:
|
|
@@ -187,7 +209,7 @@ Pre-built prompt templates:
|
|
|
187
209
|
|
|
188
210
|
| Environment Variable | Default | Description |
|
|
189
211
|
|---|---|---|
|
|
190
|
-
| `CORTEXDB_URL` | `https://api.cortexdb.ai` | CortexDB server URL |
|
|
212
|
+
| `CORTEXDB_URL` | `https://api-v1.cortexdb.ai` | CortexDB server URL |
|
|
191
213
|
| `CORTEXDB_API_KEY` | (none) | API key for authentication |
|
|
192
214
|
| `CORTEXDB_TIMEOUT` | `30.0` | HTTP request timeout (seconds) |
|
|
193
215
|
|
|
@@ -6,7 +6,7 @@ display proactive insights. Results are cached with a configurable TTL (default
|
|
|
6
6
|
|
|
7
7
|
Configuration is read from environment variables:
|
|
8
8
|
|
|
9
|
-
- ``CORTEXDB_URL`` -- CortexDB base URL (default ``https://api.cortexdb.ai``)
|
|
9
|
+
- ``CORTEXDB_URL`` -- CortexDB base URL (default ``https://api-v1.cortexdb.ai``)
|
|
10
10
|
- ``CORTEXDB_API_KEY`` -- Optional bearer token
|
|
11
11
|
- ``CORTEXDB_TENANT_ID``-- Optional tenant scope
|
|
12
12
|
- ``INSIGHTS_CACHE_TTL``-- Cache lifetime in seconds (default ``300``)
|
|
@@ -34,7 +34,7 @@ logger = logging.getLogger("cortexdb_mcp.api")
|
|
|
34
34
|
# Configuration
|
|
35
35
|
# ---------------------------------------------------------------------------
|
|
36
36
|
|
|
37
|
-
CORTEXDB_URL = os.environ.get("CORTEXDB_URL", "https://api.cortexdb.ai")
|
|
37
|
+
CORTEXDB_URL = os.environ.get("CORTEXDB_URL", "https://api-v1.cortexdb.ai")
|
|
38
38
|
CORTEXDB_API_KEY = os.environ.get("CORTEXDB_API_KEY")
|
|
39
39
|
CORTEXDB_TENANT_ID = os.environ.get("CORTEXDB_TENANT_ID")
|
|
40
40
|
INSIGHTS_CACHE_TTL = int(os.environ.get("INSIGHTS_CACHE_TTL", "300"))
|
|
@@ -1,145 +1,145 @@
|
|
|
1
|
-
"""Configuration management for the CortexDB MCP server.
|
|
2
|
-
|
|
3
|
-
Reads settings from environment variables with sensible defaults.
|
|
4
|
-
|
|
5
|
-
The MCP server targets the v1 API surface by default
|
|
6
|
-
(``https://api-v1.cortexdb.ai``). Three auth paths are supported:
|
|
7
|
-
|
|
8
|
-
1. **Anonymous one-click** — leave ``CORTEXDB_API_KEY`` unset and the server
|
|
9
|
-
will call ``POST /v1/auth/signup`` on first launch, caching the resulting
|
|
10
|
-
PASETO token (+ actor + scope) under ``~/.config/cortexdb-mcp/state.json``.
|
|
11
|
-
Re-launches reuse the cached token until it expires.
|
|
12
|
-
|
|
13
|
-
2. **Bring your own PASETO** — set ``CORTEXDB_API_KEY`` to a PASETO ``v4.public.*``
|
|
14
|
-
token. The server still needs ``CORTEXDB_ACTOR`` and ``CORTEXDB_SCOPE`` so
|
|
15
|
-
it can send the matching ``X-Cortex-Actor`` header.
|
|
16
|
-
|
|
17
|
-
3. **Legacy v0 key** — set ``CORTEXDB_API_KEY=cx_live_...`` and point
|
|
18
|
-
``CORTEXDB_URL`` at the legacy v0 surface (``https://api.cortexdb.ai``).
|
|
19
|
-
Several MCP tools won't work in this mode because v0 lacks the
|
|
20
|
-
layer-read endpoints.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
from __future__ import annotations
|
|
24
|
-
|
|
25
|
-
import json
|
|
26
|
-
import os
|
|
27
|
-
from dataclasses import dataclass, field
|
|
28
|
-
from pathlib import Path
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _state_path() -> Path:
|
|
32
|
-
"""Where the cached anonymous-signup token lives.
|
|
33
|
-
|
|
34
|
-
Uses XDG_CONFIG_HOME on Linux/macOS, %APPDATA% on Windows, with a
|
|
35
|
-
sensible fallback for both.
|
|
36
|
-
"""
|
|
37
|
-
if os.name == "nt":
|
|
38
|
-
base = Path(os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming")))
|
|
39
|
-
else:
|
|
40
|
-
base = Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config")))
|
|
41
|
-
return base / "cortexdb-mcp" / "state.json"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@dataclass
|
|
45
|
-
class CortexMCPConfig:
|
|
46
|
-
"""Configuration for connecting to a CortexDB instance.
|
|
47
|
-
|
|
48
|
-
Attributes:
|
|
49
|
-
url: Base URL of the CortexDB HTTP API.
|
|
50
|
-
api_key: PASETO bearer token (or legacy cx_live_ key in v0 mode).
|
|
51
|
-
actor: ActorId sent as the X-Cortex-Actor header. Required for v1.
|
|
52
|
-
scope: Default scope path for write/recall calls. Required for v1.
|
|
53
|
-
tenant_id: Legacy tenant id for v0 calls. Defaults to None.
|
|
54
|
-
timeout: HTTP request timeout in seconds.
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
url: str = "https://api-v1.cortexdb.ai"
|
|
58
|
-
api_key: str | None = None
|
|
59
|
-
actor: str | None = None
|
|
60
|
-
scope: str | None = None
|
|
61
|
-
tenant_id: str | None = None
|
|
62
|
-
timeout: float = 30.0
|
|
63
|
-
|
|
64
|
-
# Set by save_state() / from_env() when an anonymous signup happens; used
|
|
65
|
-
# by the server to indicate whether persistence is enabled. Kept off the
|
|
66
|
-
# dataclass init signature so tests can construct configs directly.
|
|
67
|
-
state_file: Path = field(default_factory=_state_path)
|
|
68
|
-
|
|
69
|
-
@classmethod
|
|
70
|
-
def from_env(cls) -> "CortexMCPConfig":
|
|
71
|
-
"""Build configuration from environment variables.
|
|
72
|
-
|
|
73
|
-
Environment variables
|
|
74
|
-
---------------------
|
|
75
|
-
CORTEXDB_URL -- API base URL (default ``https://api-v1.cortexdb.ai``).
|
|
76
|
-
CORTEXDB_API_KEY -- PASETO token (preferred) or legacy v0 key.
|
|
77
|
-
CORTEXDB_ACTOR -- ActorId for X-Cortex-Actor (e.g. ``user:alice``).
|
|
78
|
-
CORTEXDB_SCOPE -- Default scope path for tool calls.
|
|
79
|
-
CORTEXDB_TENANT_ID -- Legacy tenant id (v0 callers only).
|
|
80
|
-
CORTEXDB_TIMEOUT -- Request timeout in seconds (default ``30.0``).
|
|
81
|
-
|
|
82
|
-
When CORTEXDB_API_KEY is unset, the server will look for a cached
|
|
83
|
-
state file written by a previous anonymous signup; failing that,
|
|
84
|
-
the next outgoing request triggers a fresh ``/v1/auth/signup``.
|
|
85
|
-
"""
|
|
86
|
-
url = os.environ.get("CORTEXDB_URL", cls.url)
|
|
87
|
-
cfg = cls(
|
|
88
|
-
url=url,
|
|
89
|
-
api_key=os.environ.get("CORTEXDB_API_KEY"),
|
|
90
|
-
actor=os.environ.get("CORTEXDB_ACTOR"),
|
|
91
|
-
scope=os.environ.get("CORTEXDB_SCOPE"),
|
|
92
|
-
tenant_id=os.environ.get("CORTEXDB_TENANT_ID"),
|
|
93
|
-
timeout=float(os.environ.get("CORTEXDB_TIMEOUT", str(cls.timeout))),
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
# If env didn't provide credentials, hydrate from the on-disk cache
|
|
97
|
-
# (anonymous signups from prior MCP-server launches). The cache only
|
|
98
|
-
# applies when CORTEXDB_API_KEY is unset, so an explicit env key
|
|
99
|
-
# always wins. We call `_state_path()` here (vs. reading
|
|
100
|
-
# cfg.state_file) so tests can monkeypatch the module-level
|
|
101
|
-
# function and influence resolution.
|
|
102
|
-
cfg.state_file = _state_path()
|
|
103
|
-
if cfg.api_key is None:
|
|
104
|
-
cached = _load_state(cfg.state_file)
|
|
105
|
-
if cached:
|
|
106
|
-
cfg.api_key = cfg.api_key or cached.get("token")
|
|
107
|
-
cfg.actor = cfg.actor or cached.get("actor")
|
|
108
|
-
cfg.scope = cfg.scope or cached.get("scope")
|
|
109
|
-
return cfg
|
|
110
|
-
|
|
111
|
-
def save_state(self, token: str, actor: str, scope: str) -> None:
|
|
112
|
-
"""Persist an anonymous-signup outcome so subsequent launches reuse
|
|
113
|
-
the same identity. Writes are best-effort — failures (perm denied,
|
|
114
|
-
full disk, etc.) are silently swallowed because the server is still
|
|
115
|
-
functional with an in-memory token."""
|
|
116
|
-
try:
|
|
117
|
-
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
118
|
-
self.state_file.write_text(
|
|
119
|
-
json.dumps(
|
|
120
|
-
{"token": token, "actor": actor, "scope": scope},
|
|
121
|
-
indent=2,
|
|
122
|
-
),
|
|
123
|
-
encoding="utf-8",
|
|
124
|
-
)
|
|
125
|
-
# Best-effort tighten permissions on the secret-bearing file.
|
|
126
|
-
try:
|
|
127
|
-
os.chmod(self.state_file, 0o600)
|
|
128
|
-
except OSError:
|
|
129
|
-
pass
|
|
130
|
-
except OSError:
|
|
131
|
-
pass
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def _load_state(path: Path) -> dict[str, str] | None:
|
|
135
|
-
"""Read a cached signup state. Returns None on any error — the caller
|
|
136
|
-
falls back to env-only config."""
|
|
137
|
-
try:
|
|
138
|
-
if not path.exists():
|
|
139
|
-
return None
|
|
140
|
-
data = json.loads(path.read_text(encoding="utf-8"))
|
|
141
|
-
if isinstance(data, dict) and "token" in data:
|
|
142
|
-
return {k: str(v) for k, v in data.items() if isinstance(v, str)}
|
|
143
|
-
except (OSError, json.JSONDecodeError):
|
|
144
|
-
pass
|
|
145
|
-
return None
|
|
1
|
+
"""Configuration management for the CortexDB MCP server.
|
|
2
|
+
|
|
3
|
+
Reads settings from environment variables with sensible defaults.
|
|
4
|
+
|
|
5
|
+
The MCP server targets the v1 API surface by default
|
|
6
|
+
(``https://api-v1.cortexdb.ai``). Three auth paths are supported:
|
|
7
|
+
|
|
8
|
+
1. **Anonymous one-click** — leave ``CORTEXDB_API_KEY`` unset and the server
|
|
9
|
+
will call ``POST /v1/auth/signup`` on first launch, caching the resulting
|
|
10
|
+
PASETO token (+ actor + scope) under ``~/.config/cortexdb-mcp/state.json``.
|
|
11
|
+
Re-launches reuse the cached token until it expires.
|
|
12
|
+
|
|
13
|
+
2. **Bring your own PASETO** — set ``CORTEXDB_API_KEY`` to a PASETO ``v4.public.*``
|
|
14
|
+
token. The server still needs ``CORTEXDB_ACTOR`` and ``CORTEXDB_SCOPE`` so
|
|
15
|
+
it can send the matching ``X-Cortex-Actor`` header.
|
|
16
|
+
|
|
17
|
+
3. **Legacy v0 key** — set ``CORTEXDB_API_KEY=cx_live_...`` and point
|
|
18
|
+
``CORTEXDB_URL`` at the legacy v0 surface (``https://api-v1.cortexdb.ai``).
|
|
19
|
+
Several MCP tools won't work in this mode because v0 lacks the
|
|
20
|
+
layer-read endpoints.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _state_path() -> Path:
|
|
32
|
+
"""Where the cached anonymous-signup token lives.
|
|
33
|
+
|
|
34
|
+
Uses XDG_CONFIG_HOME on Linux/macOS, %APPDATA% on Windows, with a
|
|
35
|
+
sensible fallback for both.
|
|
36
|
+
"""
|
|
37
|
+
if os.name == "nt":
|
|
38
|
+
base = Path(os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming")))
|
|
39
|
+
else:
|
|
40
|
+
base = Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config")))
|
|
41
|
+
return base / "cortexdb-mcp" / "state.json"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class CortexMCPConfig:
|
|
46
|
+
"""Configuration for connecting to a CortexDB instance.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
url: Base URL of the CortexDB HTTP API.
|
|
50
|
+
api_key: PASETO bearer token (or legacy cx_live_ key in v0 mode).
|
|
51
|
+
actor: ActorId sent as the X-Cortex-Actor header. Required for v1.
|
|
52
|
+
scope: Default scope path for write/recall calls. Required for v1.
|
|
53
|
+
tenant_id: Legacy tenant id for v0 calls. Defaults to None.
|
|
54
|
+
timeout: HTTP request timeout in seconds.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
url: str = "https://api-v1.cortexdb.ai"
|
|
58
|
+
api_key: str | None = None
|
|
59
|
+
actor: str | None = None
|
|
60
|
+
scope: str | None = None
|
|
61
|
+
tenant_id: str | None = None
|
|
62
|
+
timeout: float = 30.0
|
|
63
|
+
|
|
64
|
+
# Set by save_state() / from_env() when an anonymous signup happens; used
|
|
65
|
+
# by the server to indicate whether persistence is enabled. Kept off the
|
|
66
|
+
# dataclass init signature so tests can construct configs directly.
|
|
67
|
+
state_file: Path = field(default_factory=_state_path)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_env(cls) -> "CortexMCPConfig":
|
|
71
|
+
"""Build configuration from environment variables.
|
|
72
|
+
|
|
73
|
+
Environment variables
|
|
74
|
+
---------------------
|
|
75
|
+
CORTEXDB_URL -- API base URL (default ``https://api-v1.cortexdb.ai``).
|
|
76
|
+
CORTEXDB_API_KEY -- PASETO token (preferred) or legacy v0 key.
|
|
77
|
+
CORTEXDB_ACTOR -- ActorId for X-Cortex-Actor (e.g. ``user:alice``).
|
|
78
|
+
CORTEXDB_SCOPE -- Default scope path for tool calls.
|
|
79
|
+
CORTEXDB_TENANT_ID -- Legacy tenant id (v0 callers only).
|
|
80
|
+
CORTEXDB_TIMEOUT -- Request timeout in seconds (default ``30.0``).
|
|
81
|
+
|
|
82
|
+
When CORTEXDB_API_KEY is unset, the server will look for a cached
|
|
83
|
+
state file written by a previous anonymous signup; failing that,
|
|
84
|
+
the next outgoing request triggers a fresh ``/v1/auth/signup``.
|
|
85
|
+
"""
|
|
86
|
+
url = os.environ.get("CORTEXDB_URL", cls.url)
|
|
87
|
+
cfg = cls(
|
|
88
|
+
url=url,
|
|
89
|
+
api_key=os.environ.get("CORTEXDB_API_KEY"),
|
|
90
|
+
actor=os.environ.get("CORTEXDB_ACTOR"),
|
|
91
|
+
scope=os.environ.get("CORTEXDB_SCOPE"),
|
|
92
|
+
tenant_id=os.environ.get("CORTEXDB_TENANT_ID"),
|
|
93
|
+
timeout=float(os.environ.get("CORTEXDB_TIMEOUT", str(cls.timeout))),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# If env didn't provide credentials, hydrate from the on-disk cache
|
|
97
|
+
# (anonymous signups from prior MCP-server launches). The cache only
|
|
98
|
+
# applies when CORTEXDB_API_KEY is unset, so an explicit env key
|
|
99
|
+
# always wins. We call `_state_path()` here (vs. reading
|
|
100
|
+
# cfg.state_file) so tests can monkeypatch the module-level
|
|
101
|
+
# function and influence resolution.
|
|
102
|
+
cfg.state_file = _state_path()
|
|
103
|
+
if cfg.api_key is None:
|
|
104
|
+
cached = _load_state(cfg.state_file)
|
|
105
|
+
if cached:
|
|
106
|
+
cfg.api_key = cfg.api_key or cached.get("token")
|
|
107
|
+
cfg.actor = cfg.actor or cached.get("actor")
|
|
108
|
+
cfg.scope = cfg.scope or cached.get("scope")
|
|
109
|
+
return cfg
|
|
110
|
+
|
|
111
|
+
def save_state(self, token: str, actor: str, scope: str) -> None:
|
|
112
|
+
"""Persist an anonymous-signup outcome so subsequent launches reuse
|
|
113
|
+
the same identity. Writes are best-effort — failures (perm denied,
|
|
114
|
+
full disk, etc.) are silently swallowed because the server is still
|
|
115
|
+
functional with an in-memory token."""
|
|
116
|
+
try:
|
|
117
|
+
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
self.state_file.write_text(
|
|
119
|
+
json.dumps(
|
|
120
|
+
{"token": token, "actor": actor, "scope": scope},
|
|
121
|
+
indent=2,
|
|
122
|
+
),
|
|
123
|
+
encoding="utf-8",
|
|
124
|
+
)
|
|
125
|
+
# Best-effort tighten permissions on the secret-bearing file.
|
|
126
|
+
try:
|
|
127
|
+
os.chmod(self.state_file, 0o600)
|
|
128
|
+
except OSError:
|
|
129
|
+
pass
|
|
130
|
+
except OSError:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _load_state(path: Path) -> dict[str, str] | None:
|
|
135
|
+
"""Read a cached signup state. Returns None on any error — the caller
|
|
136
|
+
falls back to env-only config."""
|
|
137
|
+
try:
|
|
138
|
+
if not path.exists():
|
|
139
|
+
return None
|
|
140
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
141
|
+
if isinstance(data, dict) and "token" in data:
|
|
142
|
+
return {k: str(v) for k, v in data.items() if isinstance(v, str)}
|
|
143
|
+
except (OSError, json.JSONDecodeError):
|
|
144
|
+
pass
|
|
145
|
+
return None
|