@reconcrap/people-network-memory 0.1.0

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 (61) hide show
  1. package/README.md +476 -0
  2. package/docs/mcp_tools.md +138 -0
  3. package/harness_adapters/openclaw/mcp.managed.unix.template.json +25 -0
  4. package/harness_adapters/openclaw/mcp.managed.windows.template.json +26 -0
  5. package/harness_adapters/openclaw/mcp.template.json +14 -0
  6. package/harness_adapters/openclaw/ppl/SKILL.md +114 -0
  7. package/package.json +30 -0
  8. package/pyproject.toml +26 -0
  9. package/scripts/install_windows.ps1 +92 -0
  10. package/scripts/npm/people-memory.js +276 -0
  11. package/scripts/people_memory_bootstrap.py +247 -0
  12. package/scripts/run_graphiti_live_from_liepin.ps1 +87 -0
  13. package/scripts/run_tests_with_artifacts.ps1 +307 -0
  14. package/src/people_network_memory/__init__.py +6 -0
  15. package/src/people_network_memory/application/__init__.py +16 -0
  16. package/src/people_network_memory/application/normalization.py +1441 -0
  17. package/src/people_network_memory/application/services.py +921 -0
  18. package/src/people_network_memory/cli.py +1212 -0
  19. package/src/people_network_memory/config.py +268 -0
  20. package/src/people_network_memory/domain/__init__.py +55 -0
  21. package/src/people_network_memory/domain/identity.py +77 -0
  22. package/src/people_network_memory/domain/models.py +355 -0
  23. package/src/people_network_memory/fixtures/__init__.py +6 -0
  24. package/src/people_network_memory/fixtures/eval.py +398 -0
  25. package/src/people_network_memory/fixtures/extractor_eval.py +364 -0
  26. package/src/people_network_memory/fixtures/generator.py +290 -0
  27. package/src/people_network_memory/fixtures/report.py +252 -0
  28. package/src/people_network_memory/graphiti_adapter/__init__.py +9 -0
  29. package/src/people_network_memory/graphiti_adapter/episode_formatter.py +70 -0
  30. package/src/people_network_memory/graphiti_adapter/graphiti_store.py +655 -0
  31. package/src/people_network_memory/graphiti_adapter/indexer.py +194 -0
  32. package/src/people_network_memory/graphiti_adapter/ontology.py +68 -0
  33. package/src/people_network_memory/harness_adapters/__init__.py +2 -0
  34. package/src/people_network_memory/harness_adapters/openclaw/__init__.py +9 -0
  35. package/src/people_network_memory/harness_adapters/openclaw/installer.py +577 -0
  36. package/src/people_network_memory/harness_adapters/openclaw/integration_eval.py +508 -0
  37. package/src/people_network_memory/harness_adapters/openclaw/smoke.py +292 -0
  38. package/src/people_network_memory/infrastructure/__init__.py +2 -0
  39. package/src/people_network_memory/infrastructure/archive_backup.py +171 -0
  40. package/src/people_network_memory/infrastructure/diagnostics.py +171 -0
  41. package/src/people_network_memory/infrastructure/embeddings.py +155 -0
  42. package/src/people_network_memory/infrastructure/file_store.py +129 -0
  43. package/src/people_network_memory/infrastructure/graphiti_promotion.py +212 -0
  44. package/src/people_network_memory/infrastructure/id_generator.py +40 -0
  45. package/src/people_network_memory/infrastructure/in_memory_store.py +1008 -0
  46. package/src/people_network_memory/infrastructure/llm_extractor.py +476 -0
  47. package/src/people_network_memory/infrastructure/llm_identity_advisor.py +200 -0
  48. package/src/people_network_memory/infrastructure/llm_judge.py +162 -0
  49. package/src/people_network_memory/infrastructure/redaction.py +21 -0
  50. package/src/people_network_memory/infrastructure/release_check.py +186 -0
  51. package/src/people_network_memory/infrastructure/retrieval_intent.py +98 -0
  52. package/src/people_network_memory/infrastructure/semantic_index.py +262 -0
  53. package/src/people_network_memory/mcp_server/__init__.py +2 -0
  54. package/src/people_network_memory/mcp_server/contracts.py +85 -0
  55. package/src/people_network_memory/mcp_server/runtime.py +133 -0
  56. package/src/people_network_memory/mcp_server/tools.py +588 -0
  57. package/src/people_network_memory/ports/__init__.py +2 -0
  58. package/src/people_network_memory/ports/errors.py +25 -0
  59. package/src/people_network_memory/ports/interfaces.py +103 -0
  60. package/src/people_network_memory/projection/__init__.py +6 -0
  61. package/src/people_network_memory/projection/builders.py +46 -0
@@ -0,0 +1,1212 @@
1
+ """Command line entrypoint for people-memory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ import tempfile
9
+ from dataclasses import replace
10
+ from pathlib import Path
11
+
12
+ from people_network_memory.config import PeopleMemoryConfig
13
+ from people_network_memory.fixtures.eval import (
14
+ evaluate_dataset,
15
+ evaluate_retrieval_service,
16
+ evaluate_services,
17
+ )
18
+ from people_network_memory.fixtures.extractor_eval import evaluate_extractor
19
+ from people_network_memory.fixtures.generator import generate_mock_dataset
20
+ from people_network_memory.fixtures.report import write_eval_markdown_report
21
+ from people_network_memory.harness_adapters.openclaw import (
22
+ install_openclaw_adapter,
23
+ openclaw_checks,
24
+ )
25
+ from people_network_memory.harness_adapters.openclaw.integration_eval import (
26
+ run_harness_memory_integration_eval,
27
+ )
28
+ from people_network_memory.harness_adapters.openclaw.smoke import run_openclaw_adapter_smoke
29
+ from people_network_memory.infrastructure.archive_backup import (
30
+ create_archive_backup,
31
+ restore_archive_backup,
32
+ )
33
+ from people_network_memory.infrastructure.diagnostics import graphiti_spike_checks
34
+ from people_network_memory.infrastructure.embeddings import check_embedding_provider
35
+ from people_network_memory.infrastructure.file_store import JsonPeopleStore, local_json_path
36
+ from people_network_memory.infrastructure.graphiti_promotion import run_graphiti_promotion_gate
37
+ from people_network_memory.infrastructure.redaction import redact_sensitive_text
38
+ from people_network_memory.infrastructure.release_check import run_local_release_check
39
+ from people_network_memory.mcp_server.contracts import public_tool_contracts
40
+ from people_network_memory.mcp_server.runtime import build_runtime
41
+ from people_network_memory.ports.errors import PeopleMemoryError
42
+
43
+
44
+ def _print_json(payload: object) -> None:
45
+ print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
46
+
47
+
48
+ def _write_json_output(payload: dict[str, object], output: str | None) -> None:
49
+ if not output:
50
+ _print_json(payload)
51
+ return
52
+ path = Path(output)
53
+ path.parent.mkdir(parents=True, exist_ok=True)
54
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
55
+ _print_json(
56
+ {
57
+ "ok": payload.get("ok", True),
58
+ "output": str(path),
59
+ "checked": payload.get("checked"),
60
+ "passes_v1_thresholds": payload.get("passes_v1_thresholds"),
61
+ }
62
+ )
63
+
64
+
65
+ def _config_from_args(args: argparse.Namespace, *, default_test_mode: bool = False) -> PeopleMemoryConfig:
66
+ config = PeopleMemoryConfig.from_env(
67
+ test_mode=bool(getattr(args, "test_mode", default_test_mode))
68
+ )
69
+ updates = {}
70
+ if getattr(args, "backend", None):
71
+ updates["backend"] = args.backend
72
+ if getattr(args, "data_path", None):
73
+ updates["data_path"] = args.data_path
74
+ if getattr(args, "graphiti_kuzu_path", None):
75
+ updates["graphiti_kuzu_path"] = args.graphiti_kuzu_path
76
+ if getattr(args, "retrieval_judge", None):
77
+ updates["retrieval_judge"] = args.retrieval_judge
78
+ if getattr(args, "ingestion_extractor", None):
79
+ updates["ingestion_extractor"] = args.ingestion_extractor
80
+ if getattr(args, "identity_advisor", None):
81
+ updates["identity_advisor"] = args.identity_advisor
82
+ if updates:
83
+ config = replace(config, **updates)
84
+ return config
85
+
86
+
87
+ def _build_optional_retrieval_judge(config: PeopleMemoryConfig) -> object | None:
88
+ if config.retrieval_judge != "llm":
89
+ return None
90
+ from people_network_memory.infrastructure.llm_judge import OpenAICompatibleRetrievalJudge
91
+
92
+ return OpenAICompatibleRetrievalJudge.from_config(config)
93
+
94
+
95
+ def _read_json_file(path: str) -> dict[str, object]:
96
+ return json.loads(Path(path).read_text(encoding="utf-8"))
97
+
98
+
99
+ def _cmd_start(args: argparse.Namespace) -> int:
100
+ config = _config_from_args(args)
101
+ runtime = build_runtime(config)
102
+ if args.once:
103
+ _print_json(
104
+ {
105
+ "status": "ok",
106
+ "service": "people-network-memory",
107
+ "backend": config.backend,
108
+ "test_mode": config.test_mode,
109
+ "tools": sorted(runtime.tools.tool_names()),
110
+ }
111
+ )
112
+ return 0
113
+ runtime.run_stdio()
114
+ return 0
115
+
116
+
117
+ def _cmd_doctor(args: argparse.Namespace) -> int:
118
+ config = _config_from_args(args)
119
+ checks = []
120
+ try:
121
+ config.validate_runtime()
122
+ checks.append({"name": "config", "ok": True})
123
+ except PeopleMemoryError as exc:
124
+ checks.append({"name": "config", "ok": False, "error": str(exc)})
125
+ checks.append(
126
+ {
127
+ "name": "telemetry",
128
+ "ok": not config.telemetry_enabled,
129
+ "detail": "GRAPHITI_TELEMETRY_ENABLED should be false for local personal data.",
130
+ }
131
+ )
132
+ data_path_check = {"name": "data_path", "ok": bool(config.data_path), "path": config.data_path}
133
+ if config.backend == "local_json":
134
+ target = local_json_path(config)
135
+ nearest = target.parent
136
+ while not nearest.exists() and nearest.parent != nearest:
137
+ nearest = nearest.parent
138
+ data_path_check["target"] = str(target)
139
+ data_path_check["nearest_existing_parent"] = str(nearest)
140
+ data_path_check["ok"] = bool(config.data_path) and nearest.exists()
141
+ checks.append(data_path_check)
142
+ if config.backend == "graphiti":
143
+ checks.extend(check.to_json() for check in graphiti_spike_checks(config))
144
+ if getattr(args, "agent", None) == "openclaw":
145
+ checks.extend(
146
+ check.to_json()
147
+ for check in openclaw_checks(
148
+ getattr(args, "openclaw_home", None) or "~/.openclaw"
149
+ )
150
+ )
151
+ ok = all(check["ok"] for check in checks)
152
+ _print_json({"ok": ok, "checks": checks})
153
+ return 0 if ok else 2
154
+
155
+
156
+ def _cmd_load_fixtures(args: argparse.Namespace) -> int:
157
+ dataset = generate_mock_dataset(seed=args.seed)
158
+ payload = dataset.model_dump(mode="json")
159
+ if args.summary:
160
+ _print_json(dataset.summary())
161
+ return 0
162
+ if args.output:
163
+ path = Path(args.output)
164
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
165
+ _print_json({"ok": True, "output": str(path), "summary": dataset.summary()})
166
+ return 0
167
+ _print_json(payload)
168
+ return 0
169
+
170
+
171
+ def _cmd_eval_fixtures(args: argparse.Namespace) -> int:
172
+ dataset = generate_mock_dataset(seed=args.seed)
173
+ result = evaluate_dataset(
174
+ dataset,
175
+ max_interactions=args.max_interactions,
176
+ max_queries=args.max_queries,
177
+ only_answerable=not args.all_queries,
178
+ )
179
+ payload = result.to_json(
180
+ include_cases=args.include_cases or args.failures_only,
181
+ failures_only=args.failures_only,
182
+ )
183
+ _write_json_output(payload, args.output)
184
+ return 0 if payload["passes_v1_thresholds"] else 2
185
+
186
+
187
+ def _cmd_spike_graphiti(args: argparse.Namespace) -> int:
188
+ config = _config_from_args(args)
189
+ checks = graphiti_spike_checks(config)
190
+ ok = all(check.ok for check in checks)
191
+ _print_json(
192
+ {
193
+ "ok": ok,
194
+ "gate": "graphiti_spike",
195
+ "checks": [check.to_json() for check in checks],
196
+ "next_step": (
197
+ "Run live Graphiti integration tests."
198
+ if ok
199
+ else "Resolve failed checks before committing to Graphiti as the substrate."
200
+ ),
201
+ }
202
+ )
203
+ return 0 if ok else 2
204
+
205
+
206
+ def _cmd_check_embedding(args: argparse.Namespace) -> int:
207
+ config = _config_from_args(args)
208
+ result = check_embedding_provider(config, args.sample)
209
+ _print_json(result.to_json())
210
+ return 0 if result.ok else 2
211
+
212
+
213
+ def _cmd_smoke_graphiti(args: argparse.Namespace) -> int:
214
+ config = _config_from_args(args)
215
+ if config.backend != "graphiti":
216
+ config = replace(config, backend="graphiti")
217
+ if args.isolated:
218
+ temp_dir = tempfile.TemporaryDirectory()
219
+ config = replace(
220
+ config,
221
+ data_path=temp_dir.name,
222
+ graphiti_kuzu_path=str(Path(temp_dir.name) / "graphiti.kuzu"),
223
+ )
224
+ else:
225
+ temp_dir = None
226
+ runtime = None
227
+ try:
228
+ runtime = build_runtime(config)
229
+ record_result = runtime.tools.record_interaction(
230
+ {
231
+ "source_text": (
232
+ "Met Alice Zhang at Blue Bottle Coffee. We discussed robotics hiring. "
233
+ "Alice mentioned Bob Li may be useful for founder intros."
234
+ ),
235
+ "interaction_type": "coffee",
236
+ "place": "Blue Bottle Coffee",
237
+ "participants": [
238
+ {"person": {"label": "Alice Zhang", "person_id": "alice_zhang"}}
239
+ ],
240
+ "mentioned_people": [
241
+ {
242
+ "person": {"label": "Bob Li", "person_id": "bob_li"},
243
+ "mentioned_by": {"label": "Alice Zhang", "person_id": "alice_zhang"},
244
+ "context": "founder intros",
245
+ }
246
+ ],
247
+ "topics": ["robotics hiring", "founder intros"],
248
+ "attributed_claims": [
249
+ {
250
+ "speaker": {"label": "Alice Zhang", "person_id": "alice_zhang"},
251
+ "subject": {"label": "Bob Li", "person_id": "bob_li"},
252
+ "claim_text": "Alice Zhang said Bob Li may be useful for founder intros.",
253
+ }
254
+ ],
255
+ }
256
+ )
257
+ search_result = runtime.tools.retrieve_network_context(
258
+ {"query": "robotics person from Blue Bottle", "limit": 5}
259
+ )
260
+ payload = {
261
+ "ok": bool(search_result.get("results")),
262
+ "record_interaction": record_result,
263
+ "search_result_count": len(search_result.get("results", [])),
264
+ "top_results": search_result.get("results", [])[:3],
265
+ }
266
+ _print_json(payload)
267
+ return 0 if payload["ok"] else 2
268
+ finally:
269
+ if runtime is not None:
270
+ runtime.close()
271
+ if temp_dir is not None:
272
+ temp_dir.cleanup()
273
+
274
+
275
+ def _cmd_eval_graphiti(args: argparse.Namespace) -> int:
276
+ config = _config_from_args(args)
277
+ if config.backend != "graphiti":
278
+ config = replace(config, backend="graphiti")
279
+ if args.isolated:
280
+ temp_dir = tempfile.TemporaryDirectory()
281
+ config = replace(
282
+ config,
283
+ data_path=temp_dir.name,
284
+ graphiti_kuzu_path=str(Path(temp_dir.name) / "graphiti.kuzu"),
285
+ )
286
+ else:
287
+ temp_dir = None
288
+ runtime = None
289
+ try:
290
+ dataset = generate_mock_dataset(seed=args.seed)
291
+ runtime = build_runtime(config)
292
+ result = evaluate_services(
293
+ dataset,
294
+ record_service=runtime.record_service,
295
+ retrieve_service=runtime.retrieve_service,
296
+ max_interactions=args.max_interactions,
297
+ max_queries=args.max_queries,
298
+ only_answerable=not args.all_queries,
299
+ )
300
+ payload = result.to_json(
301
+ include_cases=not args.no_cases or args.failures_only,
302
+ failures_only=args.failures_only,
303
+ )
304
+ payload.update(
305
+ {
306
+ "backend": "graphiti",
307
+ "isolated": args.isolated,
308
+ "fixture_summary": dataset.summary(),
309
+ "enforced_thresholds": args.enforce_thresholds,
310
+ }
311
+ )
312
+ _write_json_output(payload, args.output)
313
+ return 0 if (not args.enforce_thresholds or payload["passes_v1_thresholds"]) else 2
314
+ finally:
315
+ if runtime is not None:
316
+ runtime.close()
317
+ if temp_dir is not None:
318
+ temp_dir.cleanup()
319
+
320
+
321
+ def _cmd_eval_graphiti_search(args: argparse.Namespace) -> int:
322
+ from people_network_memory.application.services import RetrieveContextService
323
+ from people_network_memory.graphiti_adapter.graphiti_store import GraphitiGraphStore
324
+
325
+ config = _config_from_args(args)
326
+ if config.backend != "graphiti":
327
+ config = replace(config, backend="graphiti")
328
+ graph_store = GraphitiGraphStore.from_config(config)
329
+ try:
330
+ dataset = generate_mock_dataset(seed=args.seed)
331
+ retrieve = RetrieveContextService(
332
+ graph_search=graph_store,
333
+ review_queue=graph_store,
334
+ default_sensitivity_policy=config.sensitivity_policy,
335
+ retrieval_judge=_build_optional_retrieval_judge(config),
336
+ )
337
+ indexed_interactions = (
338
+ args.indexed_interactions
339
+ if args.indexed_interactions is not None
340
+ else len(dataset.interactions)
341
+ )
342
+ result = evaluate_retrieval_service(
343
+ dataset,
344
+ retrieve_service=retrieve,
345
+ indexed_interactions=indexed_interactions,
346
+ max_queries=args.max_queries,
347
+ only_answerable=not args.all_queries,
348
+ )
349
+ payload = result.to_json(
350
+ include_cases=not args.no_cases or args.failures_only,
351
+ failures_only=args.failures_only,
352
+ )
353
+ payload.update(
354
+ {
355
+ "backend": "graphiti",
356
+ "mode": "search_existing_graph",
357
+ "seed": args.seed,
358
+ "graphiti_kuzu_path": config.graphiti_kuzu_path,
359
+ "indexed_interactions": indexed_interactions,
360
+ "retrieval_judge": config.retrieval_judge,
361
+ "enforced_thresholds": args.enforce_thresholds,
362
+ }
363
+ )
364
+ _write_json_output(payload, args.output)
365
+ if args.report_output:
366
+ write_eval_markdown_report(
367
+ payload,
368
+ args.report_output,
369
+ title=(
370
+ "Graphiti Semantic Hybrid Retrieval Report"
371
+ if config.retrieval_judge == "off"
372
+ else "Graphiti Semantic Hybrid Retrieval Report With LLM Judge"
373
+ ),
374
+ compare_payload=_read_json_file(args.compare_report_input)
375
+ if args.compare_report_input
376
+ else None,
377
+ )
378
+ return 0 if (not args.enforce_thresholds or payload["passes_v1_thresholds"]) else 2
379
+ finally:
380
+ graph_store.close()
381
+
382
+
383
+ def _cmd_report_eval(args: argparse.Namespace) -> int:
384
+ payload = _read_json_file(args.input)
385
+ compare_payload = _read_json_file(args.compare_input) if args.compare_input else None
386
+ write_eval_markdown_report(
387
+ payload,
388
+ args.output,
389
+ title=args.title,
390
+ compare_payload=compare_payload,
391
+ )
392
+ _print_json(
393
+ {
394
+ "ok": True,
395
+ "input": args.input,
396
+ "compare_input": args.compare_input,
397
+ "output": args.output,
398
+ "checked": payload.get("checked"),
399
+ }
400
+ )
401
+ return 0
402
+
403
+
404
+ def _cmd_graphiti_gate(args: argparse.Namespace) -> int:
405
+ config = _config_from_args(args)
406
+ payload = run_graphiti_promotion_gate(
407
+ config,
408
+ seed=args.seed,
409
+ max_interactions=args.max_interactions,
410
+ max_queries=args.max_queries,
411
+ isolated=args.isolated,
412
+ include_cases=args.include_cases,
413
+ failures_only=args.failures_only,
414
+ skip_live=args.skip_live,
415
+ skip_embedding_check=args.skip_embedding_check,
416
+ )
417
+ _write_json_output(payload, args.output)
418
+ return 0 if payload["ok"] else 2
419
+
420
+
421
+ def _cmd_index_graphiti(args: argparse.Namespace) -> int:
422
+ from people_network_memory.graphiti_adapter.indexer import index_local_json_to_graphiti
423
+
424
+ config = _config_from_args(args)
425
+ if config.backend != "graphiti":
426
+ config = replace(config, backend="graphiti")
427
+ payload = index_local_json_to_graphiti(
428
+ config,
429
+ source_data_path=args.source_data_path,
430
+ state_path=args.state_path,
431
+ limit=args.limit,
432
+ resume=not args.no_resume,
433
+ reset_state=args.reset_state,
434
+ continue_on_error=args.continue_on_error,
435
+ )
436
+ _write_json_output(payload, args.output)
437
+ return 0 if payload["ok"] else 2
438
+
439
+
440
+ def _cmd_hydrate_graphiti_cache(args: argparse.Namespace) -> int:
441
+ from people_network_memory.application.services import RecordInteractionService
442
+ from people_network_memory.infrastructure.id_generator import SequentialIdGenerator
443
+
444
+ config = _config_from_args(args)
445
+ cache_path = Path(config.data_path).expanduser() / "people-memory.graphiti-cache.json"
446
+ cache = JsonPeopleStore(cache_path)
447
+ if args.fixture_seed is not None:
448
+ interactions = generate_mock_dataset(seed=args.fixture_seed).interactions
449
+ source = f"fixture-seed-{args.fixture_seed}"
450
+ elif args.source_data_path:
451
+ source_path = Path(args.source_data_path).expanduser()
452
+ interactions = list(JsonPeopleStore(source_path).interactions.values())
453
+ source = str(source_path)
454
+ else:
455
+ _print_json(
456
+ {
457
+ "ok": False,
458
+ "error": "hydrate-graphiti-cache requires --fixture-seed or --source-data-path",
459
+ }
460
+ )
461
+ return 2
462
+ if args.limit is not None:
463
+ interactions = interactions[: args.limit]
464
+ record = RecordInteractionService(
465
+ memory_store=cache,
466
+ identity_index=cache,
467
+ review_queue=cache,
468
+ id_generator=SequentialIdGenerator(),
469
+ )
470
+ cached = 0
471
+ skipped_existing = 0
472
+ entries: list[dict[str, object]] = []
473
+ existing = {
474
+ (interaction.source_text, interaction.occurred_at)
475
+ for interaction in cache.interactions.values()
476
+ }
477
+ for index, interaction in enumerate(interactions):
478
+ key = (interaction.source_text, interaction.occurred_at)
479
+ if key in existing:
480
+ skipped_existing += 1
481
+ entries.append(
482
+ {
483
+ "interaction_index": index,
484
+ "ok": True,
485
+ "cached": False,
486
+ "reason": "already_cached",
487
+ "source_text": interaction.source_text,
488
+ }
489
+ )
490
+ continue
491
+ result = record.record(interaction)
492
+ existing.add(key)
493
+ cached += 1
494
+ entries.append(
495
+ {
496
+ "interaction_index": index,
497
+ "ok": True,
498
+ "cached": True,
499
+ "source_text": interaction.source_text,
500
+ "created_people": result.created_people,
501
+ "updated_people": result.updated_people,
502
+ }
503
+ )
504
+ payload = {
505
+ "ok": True,
506
+ "source": source,
507
+ "target_cache": str(cache_path),
508
+ "attempted": len(interactions),
509
+ "cached": cached,
510
+ "skipped_existing": skipped_existing,
511
+ "people": len(cache.people),
512
+ "interactions": len(cache.interactions),
513
+ "entries": entries,
514
+ }
515
+ _write_json_output(payload, args.output)
516
+ return 0
517
+
518
+
519
+ def _cmd_build_semantic_cache(args: argparse.Namespace) -> int:
520
+ from people_network_memory.graphiti_adapter.graphiti_store import semantic_index_path
521
+ from people_network_memory.infrastructure.embeddings import (
522
+ EmbeddingSettings,
523
+ OpenAICompatibleEmbeddingClient,
524
+ )
525
+ from people_network_memory.infrastructure.semantic_index import SemanticProjectionIndex
526
+
527
+ config = _config_from_args(args)
528
+ source_path = (
529
+ Path(args.source_data_path).expanduser()
530
+ if args.source_data_path
531
+ else Path(config.data_path).expanduser() / "people-memory.graphiti-cache.json"
532
+ )
533
+ if not source_path.exists():
534
+ _print_json({"ok": False, "error": f"projection cache does not exist: {source_path}"})
535
+ return 2
536
+ source_store = JsonPeopleStore(source_path)
537
+ interactions = list(source_store.interactions.values())
538
+ if args.limit is not None:
539
+ interactions = interactions[: args.limit]
540
+ settings = EmbeddingSettings.from_config(config)
541
+ client = OpenAICompatibleEmbeddingClient(settings)
542
+ index = SemanticProjectionIndex(
543
+ Path(args.index_path).expanduser() if args.index_path else semantic_index_path(config)
544
+ )
545
+ payload = index.build_from_interactions(
546
+ interactions,
547
+ embed_texts=client.embed,
548
+ batch_size=args.batch_size,
549
+ reset=args.reset,
550
+ )
551
+ payload.update(
552
+ {
553
+ "source_cache": str(source_path),
554
+ "provider": settings.provider,
555
+ "model": settings.model,
556
+ "dimension": settings.dimension,
557
+ }
558
+ )
559
+ _write_json_output(payload, args.output)
560
+ return 0
561
+
562
+
563
+ def _cmd_graphiti_ladder(args: argparse.Namespace) -> int:
564
+ from people_network_memory.graphiti_adapter.indexer import index_interactions
565
+ from people_network_memory.graphiti_adapter.graphiti_store import GraphitiGraphStore
566
+
567
+ config = _config_from_args(args)
568
+ if config.backend != "graphiti":
569
+ config = replace(config, backend="graphiti")
570
+ dataset = generate_mock_dataset(seed=args.seed)
571
+ steps = [int(item) for item in args.steps.split(",") if item.strip()]
572
+ output_dir = Path(args.output_dir)
573
+ output_dir.mkdir(parents=True, exist_ok=True)
574
+ results: list[dict[str, object]] = []
575
+ cumulative_config: PeopleMemoryConfig | None = None
576
+ if args.cumulative:
577
+ if args.isolated:
578
+ cumulative_root = output_dir / "cumulative-store"
579
+ cumulative_config = replace(
580
+ config,
581
+ data_path=str(cumulative_root / "projection"),
582
+ graphiti_kuzu_path=str(cumulative_root / "graphiti.kuzu"),
583
+ )
584
+ else:
585
+ cumulative_config = config
586
+ for step in steps:
587
+ if args.cumulative:
588
+ run_config = cumulative_config or config
589
+ temp_dir = None
590
+ elif args.isolated:
591
+ temp_dir = tempfile.TemporaryDirectory()
592
+ run_config = replace(
593
+ config,
594
+ data_path=temp_dir.name,
595
+ graphiti_kuzu_path=str(Path(temp_dir.name) / "graphiti.kuzu"),
596
+ )
597
+ else:
598
+ temp_dir = None
599
+ run_config = config
600
+ graph_store = None
601
+ try:
602
+ graph_store = GraphitiGraphStore.from_config(run_config)
603
+ interactions = (
604
+ dataset.interactions[:step] if args.cumulative else dataset.interactions
605
+ )
606
+ step_payload = index_interactions(
607
+ interactions,
608
+ state_path=(
609
+ output_dir / "ladder-cumulative-state.json"
610
+ if args.cumulative
611
+ else output_dir / f"ladder-{step:03d}-state.json"
612
+ ),
613
+ source_path=Path(f"fixture-seed-{args.seed}.json"),
614
+ graphiti_kuzu_path=Path(run_config.graphiti_kuzu_path).expanduser(),
615
+ limit=None if args.cumulative else step,
616
+ resume=args.cumulative,
617
+ reset_state=args.cumulative
618
+ and step == steps[0]
619
+ and not args.resume_cumulative,
620
+ continue_on_error=args.continue_on_error,
621
+ index_one=graph_store.index_interaction_episode,
622
+ cache_one=graph_store.cache_interaction_projection,
623
+ )
624
+ if args.cumulative:
625
+ step_payload["target_interactions"] = step
626
+ except Exception as exc: # pragma: no cover - live provider defensive path
627
+ step_payload = {
628
+ "ok": False,
629
+ "step": step,
630
+ "error": str(exc),
631
+ }
632
+ finally:
633
+ if graph_store is not None:
634
+ graph_store.close()
635
+ if temp_dir is not None:
636
+ temp_dir.cleanup()
637
+ step_payload["step"] = step
638
+ step_file = output_dir / f"ladder-{step:03d}.json"
639
+ step_file.write_text(
640
+ json.dumps(step_payload, ensure_ascii=False, indent=2, sort_keys=True),
641
+ encoding="utf-8",
642
+ )
643
+ results.append({**step_payload, "artifact": str(step_file)})
644
+ if not step_payload.get("ok") and not args.continue_ladder:
645
+ break
646
+ summary: dict[str, object] = {
647
+ "ok": all(bool(result.get("ok")) for result in results),
648
+ "gate": "graphiti_ingestion_ladder",
649
+ "seed": args.seed,
650
+ "steps_requested": steps,
651
+ "steps_completed": len(results),
652
+ "output_dir": str(output_dir),
653
+ "cumulative": args.cumulative,
654
+ "resume_cumulative": args.resume_cumulative,
655
+ "results": results,
656
+ }
657
+ summary_file = output_dir / "ladder-summary.json"
658
+ summary_file.write_text(
659
+ json.dumps(summary, ensure_ascii=False, indent=2, sort_keys=True),
660
+ encoding="utf-8",
661
+ )
662
+ _print_json(summary)
663
+ return 0 if summary["ok"] else 2
664
+
665
+
666
+ def _cmd_export(args: argparse.Namespace) -> int:
667
+ config = _config_from_args(args, default_test_mode=True)
668
+ runtime = build_runtime(config)
669
+ payload = runtime.store.export_data()
670
+ if args.output:
671
+ Path(args.output).write_text(
672
+ json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
673
+ )
674
+ _print_json({"ok": True, "output": args.output})
675
+ return 0
676
+ _print_json(payload)
677
+ return 0
678
+
679
+
680
+ def _cmd_list_reviews(args: argparse.Namespace) -> int:
681
+ config = _config_from_args(args)
682
+ runtime = build_runtime(config)
683
+ reviews = runtime.review_workflow.list_reviews(status=args.status)
684
+ _print_json(
685
+ {
686
+ "ok": True,
687
+ "count": len(reviews),
688
+ "reviews": [item.model_dump(mode="json") for item in reviews],
689
+ }
690
+ )
691
+ return 0
692
+
693
+
694
+ def _cmd_resolve_review(args: argparse.Namespace) -> int:
695
+ config = _config_from_args(args)
696
+ runtime = build_runtime(config)
697
+ item = runtime.review_workflow.resolve_identity_review(
698
+ review_id=args.review_id,
699
+ source_person_id=args.source_person_id,
700
+ target_person_id=args.target_person_id,
701
+ note=args.note,
702
+ )
703
+ _print_json({"ok": True, "review": item.model_dump(mode="json")})
704
+ return 0
705
+
706
+
707
+ def _cmd_dismiss_review(args: argparse.Namespace) -> int:
708
+ config = _config_from_args(args)
709
+ runtime = build_runtime(config)
710
+ item = runtime.review_workflow.dismiss_review(review_id=args.review_id, note=args.note)
711
+ _print_json({"ok": True, "review": item.model_dump(mode="json")})
712
+ return 0
713
+
714
+
715
+ def _cmd_backup(args: argparse.Namespace) -> int:
716
+ if not args.output:
717
+ _print_json({"ok": False, "error": "backup requires --output"})
718
+ return 2
719
+ config = _config_from_args(args)
720
+ runtime = build_runtime(config)
721
+ payload = runtime.store.export_data()
722
+ output = Path(args.output)
723
+ output.parent.mkdir(parents=True, exist_ok=True)
724
+ output.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
725
+ _print_json({"ok": True, "output": str(output), "people": len(payload.get("people", []))})
726
+ return 0
727
+
728
+
729
+ def _cmd_backup_archive(args: argparse.Namespace) -> int:
730
+ config = _config_from_args(args)
731
+ if config.backend == "inmemory":
732
+ config = replace(config, backend="local_json")
733
+ payload = create_archive_backup(config, args.output)
734
+ _print_json(payload)
735
+ return 0
736
+
737
+
738
+ def _cmd_restore_archive(args: argparse.Namespace) -> int:
739
+ config = _config_from_args(args)
740
+ if config.backend == "inmemory":
741
+ config = replace(config, backend="local_json")
742
+ payload = restore_archive_backup(config, args.input, confirm=args.confirm)
743
+ _print_json(payload)
744
+ return 0
745
+
746
+
747
+ def _cmd_restore(args: argparse.Namespace) -> int:
748
+ config = _config_from_args(args)
749
+ if config.backend != "local_json":
750
+ _print_json({"ok": False, "error": "restore currently supports backend=local_json only"})
751
+ return 2
752
+ source = Path(args.input)
753
+ if not source.exists():
754
+ _print_json({"ok": False, "error": f"restore input does not exist: {source}"})
755
+ return 2
756
+ payload = json.loads(source.read_text(encoding="utf-8"))
757
+ target = local_json_path(config)
758
+ JsonPeopleStore.restore_to_path(target, payload)
759
+ _print_json({"ok": True, "restored_to": str(target), "people": len(payload.get("people", []))})
760
+ return 0
761
+
762
+
763
+ def _cmd_reset(args: argparse.Namespace) -> int:
764
+ if args.confirm != "DELETE":
765
+ _print_json(
766
+ {
767
+ "ok": False,
768
+ "error": "reset requires --confirm DELETE; no user data was deleted",
769
+ }
770
+ )
771
+ return 2
772
+ config = _config_from_args(args)
773
+ if config.backend == "local_json":
774
+ target = local_json_path(config)
775
+ if target.exists():
776
+ target.unlink()
777
+ detail = f"deleted {target}"
778
+ else:
779
+ detail = f"no local data file existed at {target}"
780
+ _print_json({"ok": True, "detail": detail})
781
+ return 0
782
+ _print_json({"ok": True, "detail": f"reset completed for backend={config.backend}"})
783
+ return 0
784
+
785
+
786
+ def _cmd_redact(args: argparse.Namespace) -> int:
787
+ _print_json({"redacted": redact_sensitive_text(args.text)})
788
+ return 0
789
+
790
+
791
+ def _cmd_install_openclaw(args: argparse.Namespace) -> int:
792
+ result = install_openclaw_adapter(
793
+ home=args.home,
794
+ command=args.command,
795
+ command_args=args.command_arg,
796
+ backend=args.backend,
797
+ ingestion_extractor=args.ingestion_extractor,
798
+ identity_advisor=args.identity_advisor,
799
+ managed_bootstrap=args.managed_bootstrap,
800
+ auto_update=args.auto_update,
801
+ dry_run=args.dry_run,
802
+ )
803
+ _print_json(result.to_json())
804
+ return 0
805
+
806
+
807
+ def _cmd_smoke_openclaw(args: argparse.Namespace) -> int:
808
+ payload = run_openclaw_adapter_smoke(
809
+ home=args.home,
810
+ install=not args.no_install,
811
+ backend=args.backend,
812
+ )
813
+ _print_json(payload)
814
+ return 0 if payload["ok"] else 2
815
+
816
+
817
+ def _cmd_eval_harness_integration(args: argparse.Namespace) -> int:
818
+ payload = run_harness_memory_integration_eval()
819
+ _write_json_output(payload, args.output)
820
+ return 0 if payload["ok"] else 2
821
+
822
+
823
+ def _cmd_eval_extractor(args: argparse.Namespace) -> int:
824
+ from people_network_memory.infrastructure.llm_extractor import (
825
+ OpenAICompatibleInteractionExtractor,
826
+ )
827
+
828
+ config = _config_from_args(args)
829
+ extractor = OpenAICompatibleInteractionExtractor.from_config(config)
830
+ payload = evaluate_extractor(extractor, max_cases=args.max_cases)
831
+ payload["min_pass_rate"] = args.min_pass_rate
832
+ payload["ok"] = bool(payload["pass_rate"] >= args.min_pass_rate)
833
+ _write_json_output(payload, args.output)
834
+ return 0 if payload["ok"] else 2
835
+
836
+
837
+ def _cmd_tool_schemas(args: argparse.Namespace) -> int:
838
+ _print_json(
839
+ {
840
+ "ok": True,
841
+ "contract_version": "v1",
842
+ "tools": public_tool_contracts(),
843
+ }
844
+ )
845
+ return 0
846
+
847
+
848
+ def _cmd_release_check(args: argparse.Namespace) -> int:
849
+ payload = run_local_release_check()
850
+ _write_json_output(payload, args.output)
851
+ return 0 if payload["ok"] else 2
852
+
853
+
854
+ def build_parser() -> argparse.ArgumentParser:
855
+ parser = argparse.ArgumentParser(prog="people-memory")
856
+ sub = parser.add_subparsers(dest="command", required=True)
857
+
858
+ start = sub.add_parser("start")
859
+ start.add_argument("--test-mode", action="store_true")
860
+ start.add_argument("--backend", choices=["inmemory", "local_json", "graphiti"])
861
+ start.add_argument("--data-path")
862
+ start.add_argument("--ingestion-extractor", choices=["off", "llm"])
863
+ start.add_argument("--identity-advisor", choices=["off", "llm"])
864
+ start.add_argument("--once", action="store_true")
865
+ start.set_defaults(func=_cmd_start)
866
+
867
+ doctor = sub.add_parser("doctor")
868
+ doctor.add_argument("--test-mode", action="store_true")
869
+ doctor.add_argument("--backend", choices=["inmemory", "local_json", "graphiti"])
870
+ doctor.add_argument("--data-path")
871
+ doctor.add_argument("--ingestion-extractor", choices=["off", "llm"])
872
+ doctor.add_argument("--identity-advisor", choices=["off", "llm"])
873
+ doctor.add_argument("--agent", choices=["openclaw"])
874
+ doctor.add_argument("--openclaw-home", default="~/.openclaw")
875
+ doctor.set_defaults(func=_cmd_doctor)
876
+
877
+ fixtures = sub.add_parser("load-fixtures")
878
+ fixtures.add_argument("--seed", type=int, default=42)
879
+ fixtures.add_argument("--summary", action="store_true")
880
+ fixtures.add_argument("--output")
881
+ fixtures.set_defaults(func=_cmd_load_fixtures)
882
+
883
+ eval_fixtures = sub.add_parser("eval-fixtures")
884
+ eval_fixtures.add_argument("--seed", type=int, default=42)
885
+ eval_fixtures.add_argument("--max-interactions", type=int)
886
+ eval_fixtures.add_argument("--max-queries", type=int)
887
+ eval_fixtures.add_argument("--include-cases", action="store_true")
888
+ eval_fixtures.add_argument("--failures-only", action="store_true")
889
+ eval_fixtures.add_argument(
890
+ "--all-queries",
891
+ action="store_true",
892
+ help="Evaluate selected queries even if max-interactions makes some targets unanswerable.",
893
+ )
894
+ eval_fixtures.add_argument("--output")
895
+ eval_fixtures.set_defaults(func=_cmd_eval_fixtures)
896
+
897
+ graphiti = sub.add_parser("spike-graphiti")
898
+ graphiti.add_argument("--test-mode", action="store_true")
899
+ graphiti.add_argument("--backend", choices=["inmemory", "local_json", "graphiti"])
900
+ graphiti.add_argument("--data-path")
901
+ graphiti.set_defaults(func=_cmd_spike_graphiti)
902
+
903
+ embedding = sub.add_parser("check-embedding")
904
+ embedding.add_argument("--test-mode", action="store_true")
905
+ embedding.add_argument("--backend", choices=["inmemory", "local_json", "graphiti"])
906
+ embedding.add_argument("--data-path")
907
+ embedding.add_argument("--sample", default="Alice likes robotics and coffee.")
908
+ embedding.set_defaults(func=_cmd_check_embedding)
909
+
910
+ smoke_graphiti = sub.add_parser("smoke-graphiti")
911
+ smoke_graphiti.add_argument("--backend", choices=["graphiti"], default="graphiti")
912
+ smoke_graphiti.add_argument("--data-path")
913
+ smoke_graphiti.add_argument("--isolated", action="store_true")
914
+ smoke_graphiti.set_defaults(func=_cmd_smoke_graphiti)
915
+
916
+ eval_graphiti = sub.add_parser("eval-graphiti")
917
+ eval_graphiti.add_argument("--backend", choices=["graphiti"], default="graphiti")
918
+ eval_graphiti.add_argument("--data-path")
919
+ eval_graphiti.add_argument("--isolated", action="store_true")
920
+ eval_graphiti.add_argument("--seed", type=int, default=42)
921
+ eval_graphiti.add_argument("--max-interactions", type=int)
922
+ eval_graphiti.add_argument("--max-queries", type=int)
923
+ eval_graphiti.add_argument("--no-cases", action="store_true")
924
+ eval_graphiti.add_argument("--failures-only", action="store_true")
925
+ eval_graphiti.add_argument(
926
+ "--all-queries",
927
+ action="store_true",
928
+ help="Evaluate selected queries even if max-interactions makes some targets unanswerable.",
929
+ )
930
+ eval_graphiti.add_argument("--output")
931
+ eval_graphiti.add_argument("--enforce-thresholds", action="store_true")
932
+ eval_graphiti.set_defaults(func=_cmd_eval_graphiti)
933
+
934
+ eval_graphiti_search = sub.add_parser("eval-graphiti-search")
935
+ eval_graphiti_search.add_argument("--backend", choices=["graphiti"], default="graphiti")
936
+ eval_graphiti_search.add_argument("--data-path")
937
+ eval_graphiti_search.add_argument("--graphiti-kuzu-path")
938
+ eval_graphiti_search.add_argument("--retrieval-judge", choices=["off", "llm"])
939
+ eval_graphiti_search.add_argument("--seed", type=int, default=42)
940
+ eval_graphiti_search.add_argument("--indexed-interactions", type=int)
941
+ eval_graphiti_search.add_argument("--max-queries", type=int)
942
+ eval_graphiti_search.add_argument("--no-cases", action="store_true")
943
+ eval_graphiti_search.add_argument("--failures-only", action="store_true")
944
+ eval_graphiti_search.add_argument(
945
+ "--all-queries",
946
+ action="store_true",
947
+ help="Evaluate selected queries even if indexed-interactions makes some targets unanswerable.",
948
+ )
949
+ eval_graphiti_search.add_argument("--output")
950
+ eval_graphiti_search.add_argument("--report-output")
951
+ eval_graphiti_search.add_argument(
952
+ "--compare-report-input",
953
+ help="Optional baseline eval JSON to compare in the generated Markdown report.",
954
+ )
955
+ eval_graphiti_search.add_argument("--enforce-thresholds", action="store_true")
956
+ eval_graphiti_search.set_defaults(func=_cmd_eval_graphiti_search)
957
+
958
+ report_eval = sub.add_parser("report-eval")
959
+ report_eval.add_argument("--input", required=True)
960
+ report_eval.add_argument("--output", required=True)
961
+ report_eval.add_argument("--compare-input")
962
+ report_eval.add_argument(
963
+ "--title",
964
+ default="Personal Network Memory Retrieval Test Report",
965
+ )
966
+ report_eval.set_defaults(func=_cmd_report_eval)
967
+
968
+ graphiti_gate = sub.add_parser("graphiti-gate")
969
+ graphiti_gate.add_argument("--backend", choices=["graphiti"], default="graphiti")
970
+ graphiti_gate.add_argument("--data-path")
971
+ graphiti_gate.add_argument("--isolated", action="store_true", default=True)
972
+ graphiti_gate.add_argument(
973
+ "--no-isolated",
974
+ action="store_false",
975
+ dest="isolated",
976
+ help="Use the configured Graphiti/Kuzu paths instead of a temporary isolated database.",
977
+ )
978
+ graphiti_gate.add_argument("--seed", type=int, default=42)
979
+ graphiti_gate.add_argument("--max-interactions", type=int)
980
+ graphiti_gate.add_argument("--max-queries", type=int)
981
+ graphiti_gate.add_argument("--include-cases", action="store_true")
982
+ graphiti_gate.add_argument("--failures-only", action="store_true", default=True)
983
+ graphiti_gate.add_argument(
984
+ "--all-cases",
985
+ action="store_false",
986
+ dest="failures_only",
987
+ help="When cases are included, write every case instead of failures only.",
988
+ )
989
+ graphiti_gate.add_argument(
990
+ "--skip-live",
991
+ action="store_true",
992
+ help="Only render static gate structure; promotion will remain blocked.",
993
+ )
994
+ graphiti_gate.add_argument(
995
+ "--skip-embedding-check",
996
+ action="store_true",
997
+ help="Skip the live embedding ping; promotion will remain blocked.",
998
+ )
999
+ graphiti_gate.add_argument("--output")
1000
+ graphiti_gate.set_defaults(func=_cmd_graphiti_gate)
1001
+
1002
+ index_graphiti = sub.add_parser("index-graphiti")
1003
+ index_graphiti.add_argument("--backend", choices=["graphiti"], default="graphiti")
1004
+ index_graphiti.add_argument("--data-path")
1005
+ index_graphiti.add_argument("--source-data-path")
1006
+ index_graphiti.add_argument("--state-path")
1007
+ index_graphiti.add_argument("--limit", type=int)
1008
+ index_graphiti.add_argument("--output")
1009
+ index_graphiti.add_argument("--no-resume", action="store_true")
1010
+ index_graphiti.add_argument("--reset-state", action="store_true")
1011
+ index_graphiti.add_argument("--continue-on-error", action="store_true")
1012
+ index_graphiti.set_defaults(func=_cmd_index_graphiti)
1013
+
1014
+ hydrate_graphiti_cache = sub.add_parser("hydrate-graphiti-cache")
1015
+ hydrate_graphiti_cache.add_argument("--backend", choices=["graphiti"], default="graphiti")
1016
+ hydrate_graphiti_cache.add_argument("--data-path", required=True)
1017
+ hydrate_graphiti_cache.add_argument("--source-data-path")
1018
+ hydrate_graphiti_cache.add_argument("--fixture-seed", type=int)
1019
+ hydrate_graphiti_cache.add_argument("--limit", type=int)
1020
+ hydrate_graphiti_cache.add_argument("--output")
1021
+ hydrate_graphiti_cache.set_defaults(func=_cmd_hydrate_graphiti_cache)
1022
+
1023
+ build_semantic_cache = sub.add_parser("build-semantic-cache")
1024
+ build_semantic_cache.add_argument("--backend", choices=["graphiti"], default="graphiti")
1025
+ build_semantic_cache.add_argument("--data-path", required=True)
1026
+ build_semantic_cache.add_argument("--source-data-path")
1027
+ build_semantic_cache.add_argument("--index-path")
1028
+ build_semantic_cache.add_argument("--limit", type=int)
1029
+ build_semantic_cache.add_argument("--batch-size", type=int, default=16)
1030
+ build_semantic_cache.add_argument("--reset", action="store_true")
1031
+ build_semantic_cache.add_argument("--output")
1032
+ build_semantic_cache.set_defaults(func=_cmd_build_semantic_cache)
1033
+
1034
+ graphiti_ladder = sub.add_parser("graphiti-ladder")
1035
+ graphiti_ladder.add_argument("--backend", choices=["graphiti"], default="graphiti")
1036
+ graphiti_ladder.add_argument("--data-path")
1037
+ graphiti_ladder.add_argument("--seed", type=int, default=42)
1038
+ graphiti_ladder.add_argument("--steps", default="1,3,10,20,40,90")
1039
+ graphiti_ladder.add_argument(
1040
+ "--output-dir",
1041
+ default=".people-network-memory/test-artifacts/graphiti-ladder",
1042
+ )
1043
+ graphiti_ladder.add_argument("--isolated", action="store_true", default=True)
1044
+ graphiti_ladder.add_argument(
1045
+ "--no-isolated",
1046
+ action="store_false",
1047
+ dest="isolated",
1048
+ help="Reuse configured Graphiti/Kuzu paths instead of one isolated database per step.",
1049
+ )
1050
+ graphiti_ladder.add_argument("--continue-on-error", action="store_true")
1051
+ graphiti_ladder.add_argument("--continue-ladder", action="store_true")
1052
+ graphiti_ladder.add_argument(
1053
+ "--cumulative",
1054
+ action="store_true",
1055
+ help=(
1056
+ "Reuse one artifact-local Graphiti/Kuzu store and state file across steps, "
1057
+ "so larger rungs only index the additional fixture interactions."
1058
+ ),
1059
+ )
1060
+ graphiti_ladder.add_argument(
1061
+ "--resume-cumulative",
1062
+ action="store_true",
1063
+ help="With --cumulative, preserve an existing ladder-cumulative-state.json.",
1064
+ )
1065
+ graphiti_ladder.set_defaults(func=_cmd_graphiti_ladder)
1066
+
1067
+ export = sub.add_parser("export")
1068
+ export.add_argument("--test-mode", action="store_true")
1069
+ export.add_argument("--backend", choices=["inmemory", "local_json", "graphiti"])
1070
+ export.add_argument("--data-path")
1071
+ export.add_argument("--output")
1072
+ export.set_defaults(func=_cmd_export)
1073
+
1074
+ list_reviews = sub.add_parser("list-reviews")
1075
+ list_reviews.add_argument("--test-mode", action="store_true")
1076
+ list_reviews.add_argument("--backend", choices=["inmemory", "local_json", "graphiti"])
1077
+ list_reviews.add_argument("--data-path")
1078
+ list_reviews.add_argument(
1079
+ "--status", choices=["open", "resolved", "dismissed", "all"], default="open"
1080
+ )
1081
+ list_reviews.set_defaults(func=_cmd_list_reviews)
1082
+
1083
+ resolve_review = sub.add_parser("resolve-review")
1084
+ resolve_review.add_argument("--test-mode", action="store_true")
1085
+ resolve_review.add_argument("--backend", choices=["local_json", "graphiti"])
1086
+ resolve_review.add_argument("--data-path")
1087
+ resolve_review.add_argument("--review-id", required=True)
1088
+ resolve_review.add_argument("--source-person-id", required=True)
1089
+ resolve_review.add_argument("--target-person-id", required=True)
1090
+ resolve_review.add_argument("--note")
1091
+ resolve_review.set_defaults(func=_cmd_resolve_review)
1092
+
1093
+ dismiss_review = sub.add_parser("dismiss-review")
1094
+ dismiss_review.add_argument("--test-mode", action="store_true")
1095
+ dismiss_review.add_argument("--backend", choices=["local_json", "graphiti"])
1096
+ dismiss_review.add_argument("--data-path")
1097
+ dismiss_review.add_argument("--review-id", required=True)
1098
+ dismiss_review.add_argument("--note")
1099
+ dismiss_review.set_defaults(func=_cmd_dismiss_review)
1100
+
1101
+ backup = sub.add_parser("backup")
1102
+ backup.add_argument("--backend", choices=["inmemory", "local_json", "graphiti"])
1103
+ backup.add_argument("--data-path")
1104
+ backup.add_argument("--output", required=True)
1105
+ backup.set_defaults(func=_cmd_backup)
1106
+
1107
+ backup_archive = sub.add_parser("backup-archive")
1108
+ backup_archive.add_argument("--test-mode", action="store_true")
1109
+ backup_archive.add_argument("--backend", choices=["local_json", "graphiti"], default="local_json")
1110
+ backup_archive.add_argument("--data-path")
1111
+ backup_archive.add_argument("--output", required=True)
1112
+ backup_archive.set_defaults(func=_cmd_backup_archive)
1113
+
1114
+ restore_archive = sub.add_parser("restore-archive")
1115
+ restore_archive.add_argument("--test-mode", action="store_true")
1116
+ restore_archive.add_argument("--backend", choices=["local_json", "graphiti"], default="local_json")
1117
+ restore_archive.add_argument("--data-path")
1118
+ restore_archive.add_argument("--input", required=True)
1119
+ restore_archive.add_argument("--confirm", default="")
1120
+ restore_archive.set_defaults(func=_cmd_restore_archive)
1121
+
1122
+ restore = sub.add_parser("restore")
1123
+ restore.add_argument("--backend", choices=["local_json"], default="local_json")
1124
+ restore.add_argument("--data-path")
1125
+ restore.add_argument("--input", required=True)
1126
+ restore.set_defaults(func=_cmd_restore)
1127
+
1128
+ reset = sub.add_parser("reset")
1129
+ reset.add_argument("--backend", choices=["inmemory", "local_json", "graphiti"])
1130
+ reset.add_argument("--data-path")
1131
+ reset.add_argument("--confirm", default="")
1132
+ reset.set_defaults(func=_cmd_reset)
1133
+
1134
+ redact = sub.add_parser("redact")
1135
+ redact.add_argument("text")
1136
+ redact.set_defaults(func=_cmd_redact)
1137
+
1138
+ install_openclaw = sub.add_parser("install-openclaw")
1139
+ install_openclaw.add_argument("--home", default="~/.openclaw")
1140
+ install_openclaw.add_argument(
1141
+ "--command",
1142
+ help=(
1143
+ "Command to run. Defaults to the active Python executable and "
1144
+ "`-m people_network_memory.cli start`."
1145
+ ),
1146
+ )
1147
+ install_openclaw.add_argument(
1148
+ "--command-arg",
1149
+ action="append",
1150
+ help="Override MCP command args. Repeat for each arg.",
1151
+ )
1152
+ install_openclaw.add_argument(
1153
+ "--backend", choices=["local_json", "inmemory", "graphiti"], default="local_json"
1154
+ )
1155
+ install_openclaw.add_argument("--ingestion-extractor", choices=["off", "llm"], default="llm")
1156
+ install_openclaw.add_argument("--identity-advisor", choices=["off", "llm"], default="llm")
1157
+ install_openclaw.add_argument(
1158
+ "--managed-bootstrap",
1159
+ action="store_true",
1160
+ help=(
1161
+ "Install a self-healing MCP command that creates/updates the local "
1162
+ "venv before launching the server."
1163
+ ),
1164
+ )
1165
+ install_openclaw.add_argument(
1166
+ "--auto-update",
1167
+ action="store_true",
1168
+ help="With --managed-bootstrap, run git pull --ff-only before dependency checks.",
1169
+ )
1170
+ install_openclaw.add_argument("--dry-run", action="store_true")
1171
+ install_openclaw.set_defaults(func=_cmd_install_openclaw)
1172
+
1173
+ smoke_openclaw = sub.add_parser("smoke-openclaw")
1174
+ smoke_openclaw.add_argument("--home")
1175
+ smoke_openclaw.add_argument(
1176
+ "--backend", choices=["local_json", "inmemory", "graphiti"], default="local_json"
1177
+ )
1178
+ smoke_openclaw.add_argument("--no-install", action="store_true")
1179
+ smoke_openclaw.set_defaults(func=_cmd_smoke_openclaw)
1180
+
1181
+ eval_harness = sub.add_parser("eval-harness-integration")
1182
+ eval_harness.add_argument("--output")
1183
+ eval_harness.set_defaults(func=_cmd_eval_harness_integration)
1184
+
1185
+ eval_extractor = sub.add_parser("eval-extractor")
1186
+ eval_extractor.add_argument("--ingestion-extractor", choices=["llm"], default="llm")
1187
+ eval_extractor.add_argument("--max-cases", type=int)
1188
+ eval_extractor.add_argument("--min-pass-rate", type=float, default=0.8)
1189
+ eval_extractor.add_argument("--output")
1190
+ eval_extractor.set_defaults(func=_cmd_eval_extractor)
1191
+
1192
+ release_check = sub.add_parser("release-check")
1193
+ release_check.add_argument("--output")
1194
+ release_check.set_defaults(func=_cmd_release_check)
1195
+
1196
+ tool_schemas = sub.add_parser("tool-schemas")
1197
+ tool_schemas.set_defaults(func=_cmd_tool_schemas)
1198
+ return parser
1199
+
1200
+
1201
+ def main(argv: list[str] | None = None) -> int:
1202
+ parser = build_parser()
1203
+ args = parser.parse_args(argv)
1204
+ try:
1205
+ return int(args.func(args))
1206
+ except PeopleMemoryError as exc:
1207
+ print(str(exc), file=sys.stderr)
1208
+ return 2
1209
+
1210
+
1211
+ if __name__ == "__main__":
1212
+ raise SystemExit(main())