memoryking 0.1.0a1__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.
Files changed (165) hide show
  1. memoryking/__init__.py +3 -0
  2. memoryking/__main__.py +885 -0
  3. memoryking/adapters/__init__.py +1 -0
  4. memoryking/adapters/adapter_state.py +242 -0
  5. memoryking/adapters/atlas.py +356 -0
  6. memoryking/adapters/browser_commands.py +112 -0
  7. memoryking/adapters/dashboard.py +750 -0
  8. memoryking/adapters/dense_search.py +86 -0
  9. memoryking/adapters/egress_policy.py +229 -0
  10. memoryking/adapters/egress_schema.sql +13 -0
  11. memoryking/adapters/embedding.schema.json +302 -0
  12. memoryking/adapters/embedding_store.py +469 -0
  13. memoryking/adapters/execution.py +368 -0
  14. memoryking/adapters/experience.py +423 -0
  15. memoryking/adapters/experience.schema.json +1508 -0
  16. memoryking/adapters/experience_records.py +239 -0
  17. memoryking/adapters/experience_schema.sql +23 -0
  18. memoryking/adapters/handoff.py +494 -0
  19. memoryking/adapters/handoff.schema.json +299 -0
  20. memoryking/adapters/handoff_schema.sql +17 -0
  21. memoryking/adapters/index_schema.sql +11 -0
  22. memoryking/adapters/inference.py +434 -0
  23. memoryking/adapters/inference.schema.json +541 -0
  24. memoryking/adapters/inspection.py +392 -0
  25. memoryking/adapters/inspection.schema.json +248 -0
  26. memoryking/adapters/knowledge.schema.json +588 -0
  27. memoryking/adapters/knowledge_schema.sql +37 -0
  28. memoryking/adapters/knowledge_store.py +795 -0
  29. memoryking/adapters/legacy_snapshot.py +358 -0
  30. memoryking/adapters/legacy_tables.py +49 -0
  31. memoryking/adapters/local_contract.py +20 -0
  32. memoryking/adapters/local_embeddings.py +107 -0
  33. memoryking/adapters/local_files.py +242 -0
  34. memoryking/adapters/local_ipc.py +247 -0
  35. memoryking/adapters/local_schema.sql +44 -0
  36. memoryking/adapters/local_store.py +623 -0
  37. memoryking/adapters/local_wire.schema.json +6625 -0
  38. memoryking/adapters/models/minilm-l6-v2-onnx.json +57 -0
  39. memoryking/adapters/optional_schema.sql +43 -0
  40. memoryking/adapters/postgres_admin.py +294 -0
  41. memoryking/adapters/postgres_counter.sql +14 -0
  42. memoryking/adapters/postgres_egress_schema.sql +15 -0
  43. memoryking/adapters/postgres_experience_schema.sql +39 -0
  44. memoryking/adapters/postgres_index_schema.sql +7 -0
  45. memoryking/adapters/postgres_optional_schema.sql +71 -0
  46. memoryking/adapters/postgres_pool.py +71 -0
  47. memoryking/adapters/postgres_recovery.py +517 -0
  48. memoryking/adapters/postgres_schema.sql +365 -0
  49. memoryking/adapters/postgres_sql.py +102 -0
  50. memoryking/adapters/postgres_store.py +402 -0
  51. memoryking/adapters/postgres_summary_schema.sql +12 -0
  52. memoryking/adapters/processing.schema.json +471 -0
  53. memoryking/adapters/processing_store.py +346 -0
  54. memoryking/adapters/projects.py +83 -0
  55. memoryking/adapters/ranking.py +315 -0
  56. memoryking/adapters/repository.schema.json +664 -0
  57. memoryking/adapters/repository_git.py +264 -0
  58. memoryking/adapters/repository_schema.sql +25 -0
  59. memoryking/adapters/repository_store.py +960 -0
  60. memoryking/adapters/retrieval.py +1033 -0
  61. memoryking/adapters/retrieval.schema.json +534 -0
  62. memoryking/adapters/retrieval_schema.sql +10 -0
  63. memoryking/adapters/shared_control.py +40 -0
  64. memoryking/adapters/shared_dashboard.py +610 -0
  65. memoryking/adapters/shared_gateway.py +63 -0
  66. memoryking/adapters/shared_http.py +214 -0
  67. memoryking/adapters/shared_mcp.py +154 -0
  68. memoryking/adapters/shared_service.py +81 -0
  69. memoryking/adapters/stdio.py +193 -0
  70. memoryking/adapters/stdio.tools.json +4580 -0
  71. memoryking/adapters/summary_schema.sql +10 -0
  72. memoryking/adapters/team.py +249 -0
  73. memoryking/adapters/timeline.py +79 -0
  74. memoryking/adapters/token_count.py +55 -0
  75. memoryking/adapters/tokenizers/LICENSE +21 -0
  76. memoryking/adapters/tokenizers/cl100k_base.tiktoken +100256 -0
  77. memoryking/adapters/web/app.css +1353 -0
  78. memoryking/adapters/web/app.js +984 -0
  79. memoryking/adapters/web/brand.js +44 -0
  80. memoryking/adapters/web/crown-mono.svg +1 -0
  81. memoryking/adapters/web/crown.svg +8 -0
  82. memoryking/adapters/web/elephant.png +0 -0
  83. memoryking/adapters/web/favicon.svg +8 -0
  84. memoryking/adapters/web/graph-layout.js +52 -0
  85. memoryking/adapters/web/graph-worker.js +10 -0
  86. memoryking/adapters/web/graph.js +477 -0
  87. memoryking/adapters/web/index.html +259 -0
  88. memoryking/adapters/web/observatory.js +913 -0
  89. memoryking/adapters/web/team.js +151 -0
  90. memoryking/adapters/web/wordmark.svg +1 -0
  91. memoryking/adapters/working.py +900 -0
  92. memoryking/adapters/working.schema.json +1271 -0
  93. memoryking/adapters/working_schema.sql +24 -0
  94. memoryking/application/__init__.py +1 -0
  95. memoryking/application/conformance.py +50 -0
  96. memoryking/application/legacy_import.py +337 -0
  97. memoryking/application/local_port.py +9 -0
  98. memoryking/application/storage.py +113 -0
  99. memoryking/application/store_core.py +1190 -0
  100. memoryking/backups.py +120 -0
  101. memoryking/contracts/__init__.py +6 -0
  102. memoryking/contracts/__main__.py +20 -0
  103. memoryking/contracts/canonical.py +85 -0
  104. memoryking/contracts/embedding.schema.json +302 -0
  105. memoryking/contracts/embedding_generated.py +65 -0
  106. memoryking/contracts/experience.schema.json +1508 -0
  107. memoryking/contracts/experience_generated.py +239 -0
  108. memoryking/contracts/generated.py +122 -0
  109. memoryking/contracts/handoff.schema.json +299 -0
  110. memoryking/contracts/handoff_generated.py +69 -0
  111. memoryking/contracts/inference.schema.json +541 -0
  112. memoryking/contracts/inference_generated.py +106 -0
  113. memoryking/contracts/processing.schema.json +471 -0
  114. memoryking/contracts/processing_generated.py +82 -0
  115. memoryking/contracts/py.typed +0 -0
  116. memoryking/contracts/repository.schema.json +664 -0
  117. memoryking/contracts/repository_generated.py +144 -0
  118. memoryking/contracts/retrieval.schema.json +534 -0
  119. memoryking/contracts/retrieval_generated.py +107 -0
  120. memoryking/contracts/schema.json +696 -0
  121. memoryking/contracts/validation.py +92 -0
  122. memoryking/contracts/working.schema.json +1271 -0
  123. memoryking/contracts/working_generated.py +213 -0
  124. memoryking/domain/__init__.py +1 -0
  125. memoryking/domain/briefing.py +108 -0
  126. memoryking/domain/briefing_text.py +217 -0
  127. memoryking/domain/checkpoint.py +111 -0
  128. memoryking/domain/compression.py +64 -0
  129. memoryking/domain/delta.py +129 -0
  130. memoryking/domain/embeddings.py +90 -0
  131. memoryking/domain/hints.py +444 -0
  132. memoryking/domain/http_status.py +34 -0
  133. memoryking/domain/learning.py +62 -0
  134. memoryking/domain/local.py +65 -0
  135. memoryking/domain/repository.py +118 -0
  136. memoryking/domain/selectors.py +356 -0
  137. memoryking/domain/summary.py +88 -0
  138. memoryking/domain/usage.py +163 -0
  139. memoryking/embedding_loop.py +98 -0
  140. memoryking/embedding_worker.py +131 -0
  141. memoryking/harness.py +882 -0
  142. memoryking/harness_context.py +376 -0
  143. memoryking/hooks.py +138 -0
  144. memoryking/lean.py +323 -0
  145. memoryking/migration.py +194 -0
  146. memoryking/model_install.py +71 -0
  147. memoryking/observability.py +101 -0
  148. memoryking/observability_metrics.py +100 -0
  149. memoryking/profiles.py +136 -0
  150. memoryking/providers.py +325 -0
  151. memoryking/py.typed +0 -0
  152. memoryking/pytest_plugin.py +68 -0
  153. memoryking/reference_agent.py +152 -0
  154. memoryking/repository_worker.py +57 -0
  155. memoryking/resume_adapter.py +154 -0
  156. memoryking/sdk.py +283 -0
  157. memoryking/shared.py +303 -0
  158. memoryking/shared_sdk.py +101 -0
  159. memoryking-0.1.0a1.dist-info/METADATA +144 -0
  160. memoryking-0.1.0a1.dist-info/RECORD +165 -0
  161. memoryking-0.1.0a1.dist-info/WHEEL +5 -0
  162. memoryking-0.1.0a1.dist-info/entry_points.txt +7 -0
  163. memoryking-0.1.0a1.dist-info/licenses/LICENSE +202 -0
  164. memoryking-0.1.0a1.dist-info/licenses/NOTICE +12 -0
  165. memoryking-0.1.0a1.dist-info/top_level.txt +1 -0
memoryking/__main__.py ADDED
@@ -0,0 +1,885 @@
1
+ """MemoryKing local service CLI. Administrative recovery requires an offline owner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import signal
9
+ import sys
10
+ import time
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from memoryking import __version__, profiles
16
+ from memoryking.adapters.dashboard import Dashboard
17
+ from memoryking.adapters.local_files import atomic, private_dir, private_file
18
+ from memoryking.adapters.local_ipc import Server, request
19
+ from memoryking.adapters.local_store import LocalStore
20
+ from memoryking.contracts.canonical import ContractError, canonical_bytes, load_strict
21
+ from memoryking.domain.hints import hint
22
+ from memoryking.domain.local import LocalError
23
+ from memoryking.observability import LEVELS, configure, event
24
+
25
+
26
+ def bar(used: int, limit: int, width: int = 12) -> str:
27
+ filled = min(width, round(width * used / limit)) if limit else 0
28
+ return "[" + "#" * filled + "." * (width - filled) + "]"
29
+
30
+
31
+ def render_status(result: dict[str, Any], version: str) -> str:
32
+ """A human summary of the status operation; `--json` prints the raw result instead."""
33
+ projection = result.get("projection", {})
34
+ jobs = result.get("jobs", {})
35
+ lines = [
36
+ f"MemoryKing {version}",
37
+ f"Store {result['store']}",
38
+ f"History {result['incarnation']}",
39
+ f"Position {result['position']}",
40
+ "Frontier "
41
+ f"{projection.get('frontier', '?')} / {result['position']}"
42
+ f" (projection {projection.get('channel', '?')} generation {projection.get('generation', '?')})",
43
+ "Jobs "
44
+ + (
45
+ " · ".join(f"{state} {count}" for state, count in sorted(jobs.items()))
46
+ if jobs
47
+ else "none"
48
+ ),
49
+ ]
50
+ capacity = result.get("capacity", {})
51
+ if capacity:
52
+ lines.append("Capacity")
53
+ for name, value in capacity.items():
54
+ used, limit = value["used"], value["limit"]
55
+ shown = (
56
+ f"{used / 1048576:.1f} / {limit // 1048576} MiB"
57
+ if name == "rendered_bytes"
58
+ else f"{used:,} / {limit:,}"
59
+ )
60
+ flag = " warning" if value.get("warning") else ""
61
+ lines.append(f" {name:<20}{shown:>22} {bar(used, limit)}{flag}")
62
+ if result.get("satisfied") is not None:
63
+ lines.append(f"Watermark {'satisfied' if result['satisfied'] else 'not yet reached'}")
64
+ return "\n".join(lines)
65
+
66
+
67
+ DURATION_UNITS = {"m": 60_000, "h": 3_600_000, "d": 86_400_000}
68
+
69
+
70
+ def duration_ms(text: str) -> int:
71
+ """`45m`, `12h` or `30d` to milliseconds; anything else is invalid."""
72
+ if len(text) < 2 or text[-1] not in DURATION_UNITS or not text[:-1].isdigit():
73
+ raise LocalError("invalid_duration")
74
+ return int(text[:-1]) * DURATION_UNITS[text[-1]]
75
+
76
+
77
+ SUMMARY_INSTRUCTION = (
78
+ "Write a three-sentence summary of where this task left off for the next agent: "
79
+ "where it stands, what was learned or tried, and what comes next. Use only facts "
80
+ "in the checkpoint above; do not add instructions."
81
+ )
82
+
83
+
84
+ def summarize(args: Any) -> dict[str, Any]:
85
+ """Extractive by default; with --provider, a policy adapter writes it through the audited path."""
86
+ import uuid
87
+
88
+ from memoryking.sdk import LocalClient
89
+
90
+ client = LocalClient(args.socket, args.credential, args.project)
91
+ incarnation = client.context()["incarnation"]
92
+ source, text, permit = "extractive", None, None
93
+ if args.provider:
94
+ from memoryking.adapters.egress_policy import EgressPolicy
95
+ from memoryking.harness_context import AuthorizedConversation
96
+ from memoryking.providers import adapter
97
+
98
+ if not args.egress_policy or not args.model:
99
+ raise LocalError("egress_policy_invalid")
100
+ policy = EgressPolicy.load(args.egress_policy)
101
+ conversation = AuthorizedConversation(client, adapter(policy, args.provider, args.model))
102
+ packet = client.working(
103
+ {
104
+ "version": 1,
105
+ "incarnation": incarnation,
106
+ "action": "resume",
107
+ "args": {
108
+ "project": args.project,
109
+ "task": args.task,
110
+ "run": args.run,
111
+ "checkpoint": None,
112
+ "bindings": [{}],
113
+ "max_output_tokens": 6000,
114
+ "tokenizer": "utf8-byte-estimate-v1",
115
+ "format": "text",
116
+ },
117
+ }
118
+ )
119
+ conversation.add_memory(packet)
120
+ conversation.add_host(
121
+ SUMMARY_INSTRUCTION,
122
+ authorization_id="summarize-" + uuid.uuid4().hex[:12],
123
+ classification="internal",
124
+ expires_ms=int(time.time() * 1000) + 60_000,
125
+ approved=True,
126
+ )
127
+ reply = conversation.dispatch()
128
+ source, text, permit = "provider", reply["content"], reply["permit"]
129
+ return client.working(
130
+ {
131
+ "version": 1,
132
+ "incarnation": incarnation,
133
+ "action": "summarize",
134
+ "key": "summarize-" + uuid.uuid4().hex,
135
+ "args": {
136
+ "project": args.project,
137
+ "task": args.task,
138
+ "run": args.run,
139
+ "checkpoint": None,
140
+ "source": source,
141
+ "text": text,
142
+ "permit": permit,
143
+ "readers": None,
144
+ },
145
+ }
146
+ )
147
+
148
+
149
+ def render_audit(result: dict[str, Any]) -> str:
150
+ """A human table of egress records; hashes are shortened, nothing else is."""
151
+ lines = [f"egress policy {result['policy_revision']}"]
152
+ items = result["items"]
153
+ if not items:
154
+ lines.append("no egress records in the retention window")
155
+ return "\n".join(lines)
156
+ header = (
157
+ "when (utc)",
158
+ "status",
159
+ "provider",
160
+ "model",
161
+ "host",
162
+ "class",
163
+ "bytes",
164
+ "receipts",
165
+ "permit",
166
+ )
167
+ rows = [
168
+ (
169
+ time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(item["evaluated_ms"] / 1000)),
170
+ item["status"],
171
+ item["provider"],
172
+ item["model"],
173
+ item["host"],
174
+ item["classification"],
175
+ str(item["request_bytes"]),
176
+ str(len(item["receipts"])),
177
+ item["permit"],
178
+ )
179
+ for item in items
180
+ ]
181
+ widths = [max(len(r[i]) for r in [header, *rows]) for i in range(len(header))]
182
+ for row in [header, *rows]:
183
+ lines.append(" ".join(cell.ljust(widths[i]) for i, cell in enumerate(row)).rstrip())
184
+ if result.get("next_before") is not None:
185
+ lines.append(f"more: --before {result['next_before']}")
186
+ return "\n".join(lines)
187
+
188
+
189
+ def render_grants(items: list[dict[str, Any]]) -> str:
190
+ if not items:
191
+ return "No grants."
192
+ rows = [("PRINCIPAL", "PROJECT", "CAPABILITIES", "VALID", "EXPIRES")]
193
+ for item in items:
194
+ expires = item["expires"]
195
+ rows.append(
196
+ (
197
+ item["principal"],
198
+ item["project"],
199
+ ",".join(item["capabilities"]),
200
+ "yes" if item["valid_now"] else "no",
201
+ "never"
202
+ if expires is None
203
+ else datetime.fromtimestamp(expires / 1000, timezone.utc).strftime(
204
+ "%Y-%m-%d %H:%MZ"
205
+ ),
206
+ )
207
+ )
208
+ widths = [max(len(r[i]) for r in rows) for i in range(5)]
209
+ return "\n".join(" ".join(c.ljust(widths[i]) for i, c in enumerate(r)).rstrip() for r in rows)
210
+
211
+
212
+ def read_credential(path: Path) -> str:
213
+ private_file(path)
214
+ return path.read_text().strip()
215
+
216
+
217
+ def doctor(
218
+ store: Path,
219
+ control: Path,
220
+ socket: Path | None,
221
+ credential: Path | None,
222
+ embedding_model: Path | None = None,
223
+ backup_dir: Path | None = None,
224
+ backup_every: str | None = None,
225
+ ) -> list[tuple[str, str, str]]:
226
+ """Health checks an operator can run while the daemon is up. Each is (level, name, detail)."""
227
+ import sqlite3
228
+
229
+ from memoryking.adapters.local_files import Control, private_dir
230
+ from memoryking.adapters.token_count import encoding
231
+
232
+ findings: list[tuple[str, str, str]] = []
233
+
234
+ def record(level: str, name: str, detail: str) -> None:
235
+ findings.append((level, name, detail))
236
+
237
+ if sys.version_info[:2] == (3, 12):
238
+ record("ok", "python", sys.version.split()[0])
239
+ else:
240
+ record("fail", "python", f"{sys.version.split()[0]} found; MemoryKing needs 3.12")
241
+ for label, path in (("store", store), ("control", control)):
242
+ try:
243
+ private_dir(path)
244
+ record("ok", label + " directory", f"{path} is owner-private")
245
+ except (LocalError, OSError) as exc:
246
+ code = exc.code if isinstance(exc, LocalError) else type(exc).__name__
247
+ record("fail", label + " directory", f"{path}: {code}; chmod 700 and check ownership")
248
+ try:
249
+ from memoryking.adapters.local_store import paths
250
+
251
+ paths(store, control)
252
+ record("ok", "directory layout", "store and control are independent")
253
+ except LocalError as exc:
254
+ record("fail", "directory layout", f"{exc.code}; neither directory may contain the other")
255
+ sock = socket or store / "service.sock"
256
+ if len(os.fsencode(sock)) > 100:
257
+ record("fail", "socket path", f"{len(os.fsencode(sock))} bytes; must fit in 100")
258
+ elif sock.exists():
259
+ record("ok", "socket", f"{sock} present")
260
+ else:
261
+ record("warn", "socket", f"{sock} absent; is `memoryking serve` running?")
262
+ database = store / "store.sqlite"
263
+ if database.exists():
264
+ try:
265
+ with sqlite3.connect(f"file:{database}?mode=ro", uri=True) as db:
266
+ version = db.execute("PRAGMA user_version").fetchone()[0]
267
+ record(
268
+ "ok" if version == 9 else "warn",
269
+ "schema",
270
+ f"SQLite schema {version}"
271
+ + ("" if version == 9 else "; the daemon migrates to 9 on next start"),
272
+ )
273
+ except sqlite3.Error as exc:
274
+ record("fail", "schema", f"cannot read {database}: {type(exc).__name__}")
275
+ else:
276
+ record("fail", "schema", f"{database} missing; run `memoryking init`")
277
+ try:
278
+ controller = Control(control)
279
+ anchor = controller.read_anchor()
280
+ records = controller.journal(anchor)
281
+ detail = f"{len(records)} records, state {anchor['state']}"
282
+ record("ok" if anchor["state"] == "active" else "warn", "journal", detail)
283
+ except LocalError as exc:
284
+ record("fail", "journal", f"{exc.code}; stay offline and use the recovery guide")
285
+ except OSError as exc:
286
+ record("fail", "journal", f"{type(exc).__name__} reading {control}")
287
+ try:
288
+ encoding()
289
+ record("ok", "tokenizer", "exact cl100k_base available")
290
+ except LocalError:
291
+ record("warn", "tokenizer", "exact tokenizer missing; byte estimate is used and disclosed")
292
+ from importlib.util import find_spec
293
+
294
+ if find_spec("onnxruntime") is not None:
295
+ record("ok", "embeddings runtime", "onnxruntime installed")
296
+ else:
297
+ record(
298
+ "warn",
299
+ "embeddings runtime",
300
+ "not installed; `pip install --require-hashes -r requirements-embeddings.lock` enables local semantic search",
301
+ )
302
+ if embedding_model is not None:
303
+ try:
304
+ from memoryking.adapters.local_embeddings import model_files
305
+
306
+ model_files(embedding_model)
307
+ record("ok", "embedding model", f"{embedding_model} verified")
308
+ except (LocalError, OSError) as exc:
309
+ code = exc.code if isinstance(exc, LocalError) else type(exc).__name__
310
+ record(
311
+ "fail",
312
+ "embedding model",
313
+ f"{embedding_model}: {code}; run `python -m memoryking.model_install --destination DIR`",
314
+ )
315
+ else:
316
+ record(
317
+ "warn",
318
+ "embedding model",
319
+ "no model configured; `serve --embedding-model DIR` after installing one enables semantic search",
320
+ )
321
+ from memoryking.backups import check as backup_check
322
+
323
+ level, detail = backup_check(backup_dir, backup_every, int(time.time() * 1000))
324
+ record(level, "backups", detail)
325
+ if socket is not None and credential is not None and sock.exists():
326
+ try:
327
+ result = request(sock, read_credential(credential), "status", {"project": "main"})
328
+ warnings = [k for k, v in result.get("capacity", {}).items() if v.get("warning")]
329
+ frontier = result["projection"]["frontier"]
330
+ detail = f"position {result['position']}, frontier {frontier}"
331
+ if warnings:
332
+ record("warn", "capacity", ", ".join(warnings) + " at 80 percent or more")
333
+ else:
334
+ record("ok", "capacity", "all resources under 80 percent")
335
+ record("ok" if frontier == result["position"] else "warn", "daemon", detail)
336
+ except LocalError as exc:
337
+ record("fail", "daemon", f"status request failed: {exc.code}")
338
+ except OSError as exc:
339
+ record("fail", "daemon", f"socket unreachable: {type(exc).__name__}")
340
+ return findings
341
+
342
+
343
+ def main() -> int:
344
+ parser = argparse.ArgumentParser(description=__doc__)
345
+ parser.add_argument("--version", action="version", version=f"memoryking {__version__}")
346
+ parser.add_argument(
347
+ "--profile",
348
+ default=os.environ.get("MEMORYKING_PROFILE"),
349
+ help="Named store from the profiles config; fills omitted --store, --control, "
350
+ "--socket and --credential (MEMORYKING_PROFILE)",
351
+ )
352
+ parser.add_argument(
353
+ "--config", type=Path, help="Profiles file; default ~/.memoryking/config.toml"
354
+ )
355
+ commands = parser.add_subparsers(dest="command", required=True)
356
+ prof = commands.add_parser("profile", help="Name a store so commands need no paths")
357
+ prof_commands = prof.add_subparsers(dest="profile_command", required=True)
358
+ adder = prof_commands.add_parser("add")
359
+ adder.add_argument("name")
360
+ adder.add_argument("--store", type=Path, required=True)
361
+ adder.add_argument("--control", type=Path, required=True)
362
+ adder.add_argument("--credential", type=Path, help="Default admin credential file")
363
+ adder.add_argument("--socket", type=Path, help="Default: STORE/service.sock")
364
+ adder.add_argument("--dashboard-port", type=int)
365
+ adder.add_argument("--embedding-model", type=Path, help="Installed local model directory")
366
+ adder.add_argument("--backup-dir", type=Path)
367
+ adder.add_argument("--backup-every", help="Such as 12h or 1d")
368
+ adder.add_argument("--backup-keep", type=int)
369
+ prof_commands.add_parser("list")
370
+ prof_commands.add_parser("remove").add_argument("name")
371
+ for name in ("init", "serve", "backup", "restore", "gc", "control-checkpoint"):
372
+ cmd = commands.add_parser(name)
373
+ cmd.add_argument("--store", type=Path)
374
+ cmd.add_argument("--control", type=Path)
375
+ if name == "serve":
376
+ cmd.add_argument(
377
+ "--dashboard-port",
378
+ type=int,
379
+ help="Opt-in loopback port; 0 chooses a free port; a profile may set it",
380
+ )
381
+ cmd.add_argument("--plan-root", type=Path, help="Explicit trusted plan/report checkout")
382
+ cmd.add_argument("--backup-dir", type=Path, help="Directory for scheduled backups")
383
+ cmd.add_argument("--backup-every", help="Backup interval such as 12h or 1d")
384
+ cmd.add_argument(
385
+ "--backup-keep", type=int, help="Complete backups to retain; default 14"
386
+ )
387
+ cmd.add_argument(
388
+ "--embedding-model",
389
+ type=Path,
390
+ help="Installed local model directory; the daemon then embeds queries and "
391
+ "indexes every enabled project itself (a profile may set it)",
392
+ )
393
+ cmd.add_argument(
394
+ "--log-level",
395
+ default="warning",
396
+ choices=LEVELS,
397
+ help="Redacted operator log level on stderr; request ids link replies to lines",
398
+ )
399
+ cmd.add_argument(
400
+ "--trace",
401
+ action="store_true",
402
+ help="Log one timing line per request (operation, action, outcome, elapsed, "
403
+ "lock wait, transaction time, response size); implies --log-level info",
404
+ )
405
+ cmd.add_argument(
406
+ "--egress-policy",
407
+ type=Path,
408
+ help="Private TOML policy naming the providers, models and hosts retained "
409
+ "context may reach; without it only the local recorder is accepted",
410
+ )
411
+ cmd.add_argument(
412
+ "--summary-idle",
413
+ default="30m",
414
+ help="Derive a 'where this left off' summary for a task idle this long "
415
+ "(e.g. 30m, 2h); 0 disables",
416
+ )
417
+ if name == "init":
418
+ cmd.add_argument("--tenant", default="main")
419
+ cmd.add_argument("--principal", default="owner")
420
+ if name in {"backup", "restore"}:
421
+ cmd.add_argument("--backup", type=Path, required=True)
422
+ if name == "restore":
423
+ cmd.add_argument(
424
+ "--lost-anchor-journal-head",
425
+ help="Current independently retained journal head; only for a missing anchor",
426
+ )
427
+ status = commands.add_parser(
428
+ "status", help="Show store health, frontier, jobs and capacity through the daemon"
429
+ )
430
+ status.add_argument("--socket", type=Path)
431
+ status.add_argument("--credential", type=Path)
432
+ status.add_argument("--project", default="main")
433
+ status.add_argument("--json", action="store_true", help="Print the raw status result")
434
+ doc = commands.add_parser("doctor", help="Check the installation and report fixes")
435
+ doc.add_argument("--store", type=Path)
436
+ doc.add_argument("--control", type=Path)
437
+ doc.add_argument("--socket", type=Path, help="Also query the running daemon")
438
+ doc.add_argument("--credential", type=Path, help="Admin credential for the daemon query")
439
+ doc.add_argument("--embedding-model", type=Path, help="Model directory to verify")
440
+ doc.add_argument("--backup-dir", type=Path, help="Scheduled backup directory to check")
441
+ doc.add_argument("--backup-every", help="Backup policy interval the directory should meet")
442
+ cred = commands.add_parser("credential", help="Issue, list and revoke agent credentials")
443
+ cred_commands = cred.add_subparsers(dest="credential_command", required=True)
444
+ for name in ("issue", "list", "revoke"):
445
+ item = cred_commands.add_parser(name)
446
+ item.add_argument("--socket", type=Path)
447
+ item.add_argument("--credential", type=Path, help="Admin credential")
448
+ if name in {"issue", "revoke"}:
449
+ item.add_argument("principal")
450
+ item.add_argument("--project", required=True)
451
+ if name == "issue":
452
+ item.add_argument(
453
+ "--capabilities", required=True, help="Comma-separated, for example capture,read"
454
+ )
455
+ item.add_argument("--expires", help="Validity such as 45m, 12h or 30d")
456
+ item.add_argument("--output", type=Path, required=True, help="New private file")
457
+ if name == "list":
458
+ item.add_argument("--json", action="store_true")
459
+ summ = commands.add_parser(
460
+ "summarize", help="Derive a 'where this left off' summary of a task, with its own receipt"
461
+ )
462
+ summ.add_argument("--socket", type=Path)
463
+ summ.add_argument("--credential", type=Path)
464
+ summ.add_argument("--project", required=True)
465
+ summ.add_argument("--task", required=True)
466
+ summ.add_argument("--run")
467
+ summ.add_argument("--egress-policy", type=Path, help="With --provider: the daemon's policy")
468
+ summ.add_argument("--provider", help="Write the summary with this policy adapter")
469
+ summ.add_argument("--model", help="A model the policy allows for that provider")
470
+ summ.add_argument("--json", action="store_true")
471
+ audit = commands.add_parser(
472
+ "egress-audit",
473
+ help="List what left this store for a model provider, and whether it arrived",
474
+ )
475
+ audit.add_argument("--socket", type=Path)
476
+ audit.add_argument("--credential", type=Path, help="Credential with inference capability")
477
+ audit.add_argument("--project", required=True)
478
+ audit.add_argument("--before", type=int, help="Page further back from this evaluated_ms")
479
+ audit.add_argument("--json", action="store_true")
480
+ dash = commands.add_parser("dashboard", help="Open the dashboard in a browser")
481
+ dash_commands = dash.add_subparsers(dest="dashboard_command", required=True)
482
+ opener = dash_commands.add_parser("open")
483
+ opener.add_argument("--socket", type=Path)
484
+ opener.add_argument("--credential", type=Path)
485
+ opener.add_argument(
486
+ "--print-only", action="store_true", help="Print the sign-in link instead of opening it"
487
+ )
488
+ emb = commands.add_parser("embeddings", help="Enable or inspect local semantic search")
489
+ emb_commands = emb.add_subparsers(dest="embeddings_command", required=True)
490
+ for name in ("enable", "status"):
491
+ item = emb_commands.add_parser(name)
492
+ item.add_argument("--socket", type=Path)
493
+ item.add_argument("--credential", type=Path, help="Admin credential")
494
+ item.add_argument("--project", required=True)
495
+ cap = commands.add_parser("capture", help="Save one observation from text or stdin")
496
+ cap.add_argument("--socket", type=Path)
497
+ cap.add_argument("--credential", type=Path)
498
+ cap.add_argument("--project", required=True)
499
+ cap.add_argument("--text", help="The observation; omit to read it from stdin")
500
+ cap.add_argument("--source", default="cli", help="Where it came from, e.g. git or pytest")
501
+ cap.add_argument("--event", help="Stable event id so a rerun replays; default: a text hash")
502
+ cap.add_argument(
503
+ "--readers", help="Comma-separated principals; default everyone in the project"
504
+ )
505
+ cap.add_argument(
506
+ "--classification", default="internal", choices=("public", "internal", "restricted")
507
+ )
508
+ cap.add_argument(
509
+ "--observed-ms",
510
+ type=int,
511
+ help="Observation time in Unix ms; default is today to the day so a rerun replays",
512
+ )
513
+ rec = commands.add_parser("recall", help="Print a cited Markdown briefing for a query")
514
+ rec.add_argument("--socket", type=Path)
515
+ rec.add_argument("--credential", type=Path)
516
+ rec.add_argument("--project", required=True)
517
+ rec.add_argument("--query", required=True)
518
+ rec.add_argument("--budget", type=int, default=6000)
519
+ rec.add_argument("--task")
520
+ rec.add_argument("--json", action="store_true", help="Print the structured packet instead")
521
+ rpc = commands.add_parser(
522
+ "request", help="Read a versioned operation/args JSON object from stdin"
523
+ )
524
+ rpc.add_argument("--socket", type=Path)
525
+ rpc.add_argument("--credential", type=Path)
526
+ rpc.add_argument(
527
+ "--credential-output", type=Path, help="Required for grant; saves the new secret privately"
528
+ )
529
+ login = commands.add_parser(
530
+ "dashboard-login", help="Save a one-use browser bootstrap code privately"
531
+ )
532
+ login.add_argument("--socket", type=Path)
533
+ login.add_argument("--credential", type=Path)
534
+ login.add_argument("--output", type=Path, required=True)
535
+ args = parser.parse_args()
536
+ try:
537
+ if args.command == "profile":
538
+ if args.profile_command == "add":
539
+ values: dict[str, Any] = {
540
+ "store": str(args.store.resolve()),
541
+ "control": str(args.control.resolve()),
542
+ }
543
+ for field in ("credential", "socket", "embedding_model", "backup_dir"):
544
+ if getattr(args, field) is not None:
545
+ values[field] = str(getattr(args, field).resolve())
546
+ for field in ("dashboard_port", "backup_every", "backup_keep"):
547
+ if getattr(args, field) is not None:
548
+ values[field] = getattr(args, field)
549
+ path = profiles.save(args.name, values, args.config)
550
+ print(json.dumps({"profile": args.name, "config": str(path)}))
551
+ elif args.profile_command == "remove":
552
+ path = profiles.remove(args.name, args.config)
553
+ print(json.dumps({"removed": args.name, "config": str(path)}))
554
+ else:
555
+ for name, values in sorted(profiles.load(args.config).items()):
556
+ print(name)
557
+ for field, value in values.items():
558
+ print(f" {field} = {value}")
559
+ return 0
560
+ profiles.apply(args, args.profile, args.config)
561
+ optional = (
562
+ {"socket", "credential"} if args.command in {"doctor", "capture", "recall"} else set()
563
+ )
564
+ for field in ("store", "control", "socket", "credential"):
565
+ if hasattr(args, field) and getattr(args, field) is None and field not in optional:
566
+ raise LocalError(field + "_required")
567
+ if args.command == "init":
568
+ path = LocalStore.initialize(args.store, args.control, args.tenant, args.principal)
569
+ print(json.dumps({"credential_file": str(path)}))
570
+ elif args.command == "restore":
571
+ path = LocalStore.restore(
572
+ args.store,
573
+ args.control,
574
+ args.backup,
575
+ lost_anchor_journal_head=args.lost_anchor_journal_head,
576
+ )
577
+ print(json.dumps({"credential_file": str(path), "reconciliation_required": True}))
578
+ elif args.command == "dashboard-login":
579
+ private_file(args.credential)
580
+ private_dir(args.output.parent)
581
+ if args.output.exists() or args.output.is_symlink():
582
+ raise LocalError("credential_output_exists")
583
+ result = request(
584
+ args.socket, args.credential.read_text().strip(), "dashboard_bootstrap", {}
585
+ )
586
+ atomic(args.output, json.dumps(result).encode())
587
+ print(json.dumps({"bootstrap_file": str(args.output), "url": result["url"]}))
588
+ elif args.command == "embeddings":
589
+ from memoryking.adapters.local_embeddings import model_manifest
590
+ from memoryking.domain.embeddings import fingerprint
591
+
592
+ token = read_credential(args.credential)
593
+ context = request(args.socket, token, "context", {"project": args.project})
594
+ fp = fingerprint(model_manifest())
595
+
596
+ def embedding(action: str, extra: dict[str, Any], key: str | None = None) -> Any:
597
+ command: dict[str, Any] = {
598
+ "version": 1,
599
+ "incarnation": context["incarnation"],
600
+ "action": action,
601
+ "args": {"project": args.project, **extra},
602
+ }
603
+ if key:
604
+ command["key"] = key
605
+ return request(args.socket, token, "embedding", {"command": command})
606
+
607
+ if args.embeddings_command == "status":
608
+ print(json.dumps(embedding("status", {"fingerprint": fp})))
609
+ else:
610
+ try:
611
+ current = embedding("status", {"fingerprint": fp})
612
+ except LocalError as exc:
613
+ if exc.code != "embedding_partition_unavailable":
614
+ raise
615
+ current = {}
616
+ generation = int(current.get("generation") or 0)
617
+ if current.get("state") == "experimental":
618
+ print(json.dumps({**current, "already_enabled": True}))
619
+ else:
620
+ result = embedding(
621
+ "configure",
622
+ {
623
+ "model": "minilm-l6-v2-onnx",
624
+ "state": "experimental",
625
+ "expected_generation": generation,
626
+ },
627
+ key="enable-" + str(generation),
628
+ )
629
+ print(json.dumps(result))
630
+ elif args.command == "capture":
631
+ from memoryking import hooks
632
+
633
+ text = args.text if args.text is not None else sys.stdin.read()
634
+ result = hooks.capture(
635
+ text,
636
+ source=args.source,
637
+ event=args.event,
638
+ readers=args.readers.split(",") if args.readers else None,
639
+ classification=args.classification,
640
+ observed_ms=args.observed_ms,
641
+ socket=args.socket,
642
+ credential=args.credential,
643
+ project=args.project,
644
+ )
645
+ print(json.dumps({k: result[k] for k in ("observation", "position") if k in result}))
646
+ elif args.command == "recall":
647
+ from memoryking import hooks
648
+
649
+ packet = hooks.recall(
650
+ args.query,
651
+ budget=args.budget,
652
+ text=not args.json,
653
+ task=args.task,
654
+ socket=args.socket,
655
+ credential=args.credential,
656
+ project=args.project,
657
+ )
658
+ if args.json:
659
+ sys.stdout.buffer.write(canonical_bytes(packet) + b"\n")
660
+ else:
661
+ print(packet["text"], end="")
662
+ elif args.command == "doctor":
663
+ findings = doctor(
664
+ args.store,
665
+ args.control,
666
+ args.socket,
667
+ args.credential,
668
+ args.embedding_model,
669
+ args.backup_dir,
670
+ args.backup_every,
671
+ )
672
+ width = max(len(name) for _, name, _ in findings)
673
+ for level, name, detail in findings:
674
+ print(f"{level.upper():<5} {name.ljust(width)} {detail}")
675
+ failed = sum(level == "fail" for level, _, _ in findings)
676
+ print(f"{len(findings)} checks, {failed} failed")
677
+ return 1 if failed else 0
678
+ elif args.command == "credential":
679
+ credential = read_credential(args.credential)
680
+ if args.credential_command == "list":
681
+ items = request(args.socket, credential, "grants", {})["items"]
682
+ if args.json:
683
+ sys.stdout.buffer.write(canonical_bytes({"items": items}) + b"\n")
684
+ else:
685
+ print(render_grants(items))
686
+ elif args.credential_command == "revoke":
687
+ request(
688
+ args.socket,
689
+ credential,
690
+ "revoke",
691
+ {"principal": args.principal, "project": args.project},
692
+ )
693
+ print(json.dumps({"revoked": args.principal, "project": args.project}))
694
+ else:
695
+ private_dir(args.output.parent)
696
+ if args.output.exists() or args.output.is_symlink():
697
+ raise LocalError("credential_output_exists")
698
+ grant: dict[str, Any] = {
699
+ "principal": args.principal,
700
+ "project": args.project,
701
+ "capabilities": sorted(set(args.capabilities.split(","))),
702
+ }
703
+ if args.expires:
704
+ grant["expires"] = int(time.time() * 1000) + duration_ms(args.expires)
705
+ result = request(args.socket, credential, "grant", grant)
706
+ atomic(args.output, result.pop("credential").encode())
707
+ result.update(credential_file=str(args.output), **grant)
708
+ print(json.dumps(result))
709
+ elif args.command == "summarize":
710
+ result = summarize(args)
711
+ if args.json:
712
+ sys.stdout.buffer.write(canonical_bytes(result) + b"\n")
713
+ else:
714
+ print(result["summary"])
715
+ print(
716
+ f"receipt {result['receipt']} · {result['method']} · "
717
+ f"checkpoint {result['checkpoint']} revision {result['revision']}"
718
+ )
719
+ elif args.command == "egress-audit":
720
+ result = request(
721
+ args.socket,
722
+ read_credential(args.credential),
723
+ "inference",
724
+ {
725
+ "command": {
726
+ "version": 1,
727
+ "action": "audit",
728
+ "args": {"project": args.project, "before": args.before},
729
+ }
730
+ },
731
+ )
732
+ if args.json:
733
+ sys.stdout.buffer.write(canonical_bytes(result) + b"\n")
734
+ else:
735
+ print(render_audit(result))
736
+ elif args.command == "dashboard":
737
+ result = request(
738
+ args.socket, read_credential(args.credential), "dashboard_bootstrap", {}
739
+ )
740
+ link = result["url"] + "/#code=" + result["code"]
741
+ if args.print_only:
742
+ print(link)
743
+ else:
744
+ import webbrowser
745
+
746
+ opened = webbrowser.open(link)
747
+ print(
748
+ json.dumps(
749
+ {
750
+ "url": result["url"],
751
+ "opened": opened,
752
+ "expires_ms": result["expires_ms"],
753
+ "note": "The one-use code is in the link fragment and expires in 60 seconds.",
754
+ }
755
+ )
756
+ )
757
+ elif args.command == "status":
758
+ private_file(args.credential)
759
+ result = request(
760
+ args.socket,
761
+ args.credential.read_text().strip(),
762
+ "status",
763
+ {"project": args.project},
764
+ )
765
+ if args.json:
766
+ sys.stdout.buffer.write(canonical_bytes(result) + b"\n")
767
+ else:
768
+ print(render_status(result, __version__))
769
+ elif args.command == "request":
770
+ private_file(args.credential)
771
+ body = load_strict(sys.stdin.buffer.read(262145))
772
+ if not isinstance(body, dict) or set(body) != {"operation", "args"}:
773
+ raise LocalError("invalid_request")
774
+ if body["operation"] == "grant" and args.credential_output is None:
775
+ raise LocalError("credential_output_required")
776
+ if body["operation"] == "grant":
777
+ private_dir(args.credential_output.parent)
778
+ if args.credential_output.exists() or args.credential_output.is_symlink():
779
+ raise LocalError("credential_output_exists")
780
+ result = request(
781
+ args.socket, args.credential.read_text().strip(), body["operation"], body["args"]
782
+ )
783
+ if "credential" in result:
784
+ atomic(args.credential_output, result.pop("credential").encode())
785
+ result["credential_file"] = str(args.credential_output)
786
+ sys.stdout.buffer.write(canonical_bytes(result) + b"\n")
787
+ else:
788
+ with LocalStore(args.store, args.control) as store:
789
+ if args.command == "serve":
790
+ configure(args.log_level, trace=args.trace)
791
+ if args.egress_policy is not None:
792
+ from memoryking.adapters.egress_policy import EgressPolicy
793
+
794
+ store.egress_policy = EgressPolicy.load(args.egress_policy)
795
+ store.summary_idle_ms = (
796
+ 0 if args.summary_idle in {"0", "off"} else duration_ms(args.summary_idle)
797
+ )
798
+
799
+ def stop(signum: int, frame: object) -> None:
800
+ raise KeyboardInterrupt
801
+
802
+ signal.signal(signal.SIGTERM, stop)
803
+ dashboard = None
804
+ embeddings = None
805
+ if args.backup_dir is not None:
806
+ from memoryking.backups import Policy
807
+
808
+ store.backup_policy = Policy(
809
+ args.backup_dir, args.backup_every or "1d", args.backup_keep or 14
810
+ )
811
+ if args.embedding_model is not None:
812
+ from memoryking.adapters.local_embeddings import LocalOnnxEmbedding
813
+ from memoryking.embedding_loop import EmbeddingLoop
814
+
815
+ store.embedder = LocalOnnxEmbedding(args.embedding_model)
816
+ if args.dashboard_port is not None:
817
+ if not 0 <= args.dashboard_port <= 65535:
818
+ raise LocalError("invalid_dashboard_configuration")
819
+ dashboard = Dashboard(store, args.plan_root, args.dashboard_port)
820
+ store.dashboard = dashboard
821
+ dashboard.start()
822
+ try:
823
+ with Server(store) as server:
824
+ print(
825
+ json.dumps({"ready": True, "socket": str(server.path)}), flush=True
826
+ )
827
+ event("serve", "ready", dashboard=dashboard is not None)
828
+ if store.embedder is not None:
829
+ embeddings = EmbeddingLoop(
830
+ store,
831
+ server.path,
832
+ args.control / "owner.credential",
833
+ store.embedder,
834
+ )
835
+ embeddings.start()
836
+ server.serve_forever(poll_interval=0.2)
837
+ except KeyboardInterrupt:
838
+ pass
839
+ finally:
840
+ if embeddings is not None:
841
+ embeddings.stop()
842
+ if dashboard:
843
+ dashboard.stop()
844
+ elif args.command == "backup":
845
+ print(json.dumps(store.backup(args.backup)))
846
+ elif args.command == "gc":
847
+ print(
848
+ json.dumps(
849
+ {
850
+ "expired_results": store.expire_results(),
851
+ "orphan_blobs_removed": store.gc(),
852
+ }
853
+ )
854
+ )
855
+ elif args.command == "control-checkpoint":
856
+ print(
857
+ json.dumps(
858
+ {
859
+ "store": store.store_id,
860
+ "incarnation": store.incarnation,
861
+ "journal_sequence": store.anchor["journal_sequence"],
862
+ "journal_head": store.anchor["journal_head"],
863
+ "independent_retention_required": True,
864
+ }
865
+ )
866
+ )
867
+ return 0
868
+ except (LocalError, ContractError) as exc:
869
+ print(
870
+ json.dumps(
871
+ {"error": code, "hint": hint(code)}
872
+ if (code := exc.code if isinstance(exc, LocalError) else "invalid_request")
873
+ else {}
874
+ ),
875
+ file=sys.stderr,
876
+ )
877
+ return 2
878
+ except (OSError, ValueError):
879
+ code = "local_io_or_configuration_error"
880
+ print(json.dumps({"error": code, "hint": hint(code)}), file=sys.stderr)
881
+ return 2
882
+
883
+
884
+ if __name__ == "__main__":
885
+ raise SystemExit(main())