brainpalace-cli 26.7.2__tar.gz → 26.7.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/PKG-INFO +2 -2
  2. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/cli.py +8 -0
  3. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/client/api_client.py +252 -0
  4. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/__init__.py +6 -0
  5. brainpalace_cli-26.7.4/brainpalace_cli/commands/entities.py +231 -0
  6. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/folders.py +41 -0
  7. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/hook.py +17 -0
  8. brainpalace_cli-26.7.4/brainpalace_cli/commands/ingest.py +372 -0
  9. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/init.py +244 -48
  10. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/query.py +192 -0
  11. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/records.py +42 -0
  12. brainpalace_cli-26.7.4/brainpalace_cli/commands/references.py +163 -0
  13. brainpalace_cli-26.7.4/brainpalace_cli/commands/rules.py +164 -0
  14. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/status.py +55 -1
  15. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/verify_docs.py +23 -2
  16. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/config_fields.py +14 -0
  17. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/data/ai_guidance.md +51 -13
  18. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/checkers/cli_commands.py +2 -2
  19. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/generator.py +12 -6
  20. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/serializer.py +16 -0
  21. brainpalace_cli-26.7.4/brainpalace_cli/merge.py +34 -0
  22. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/pyproject.toml +2 -2
  23. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/README.md +0 -0
  24. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/__init__.py +0 -0
  25. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/__main__.py +0 -0
  26. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/ai_guidance.py +0 -0
  27. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/client/__init__.py +0 -0
  28. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/client/errors.py +0 -0
  29. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/_dashboard_url.py +0 -0
  30. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/ai_guide.py +0 -0
  31. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/backfill.py +0 -0
  32. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/bm25_project.py +0 -0
  33. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/cache.py +0 -0
  34. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/config.py +0 -0
  35. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/context.py +0 -0
  36. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/dashboard.py +0 -0
  37. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/doctor.py +0 -0
  38. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/estimate_util.py +0 -0
  39. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/extraction.py +0 -0
  40. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/extraction_drain.py +0 -0
  41. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/graph.py +0 -0
  42. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/index.py +0 -0
  43. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/init_plan.py +0 -0
  44. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/inject.py +0 -0
  45. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/install_agent.py +0 -0
  46. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/jobs.py +0 -0
  47. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/list_cmd.py +0 -0
  48. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/lsp.py +0 -0
  49. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/mcp.py +0 -0
  50. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/memories.py +0 -0
  51. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/plugin_detect.py +0 -0
  52. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/read_only.py +0 -0
  53. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/reap.py +0 -0
  54. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/recall.py +0 -0
  55. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/remember.py +0 -0
  56. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/reset.py +0 -0
  57. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/session_hooks.py +0 -0
  58. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/sessions.py +0 -0
  59. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/start.py +0 -0
  60. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/stop.py +0 -0
  61. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/sync_docs.py +0 -0
  62. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/types.py +0 -0
  63. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/uninstall.py +0 -0
  64. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/update.py +0 -0
  65. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/commands/whoami.py +0 -0
  66. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/config.py +0 -0
  67. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/config_migrate.py +0 -0
  68. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/config_parity.py +0 -0
  69. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/config_resolve.py +0 -0
  70. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/config_review.py +0 -0
  71. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/config_schema.py +0 -0
  72. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/data/__init__.py +0 -0
  73. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/data/hooks/__init__.py +0 -0
  74. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/data/hooks/sessionstart-hook.sh +0 -0
  75. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/diagnostics.py +0 -0
  76. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/discovery.py +0 -0
  77. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/__init__.py +0 -0
  78. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/allowlist.py +0 -0
  79. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/checkers/__init__.py +0 -0
  80. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/checkers/base.py +0 -0
  81. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/checkers/config.py +0 -0
  82. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/checkers/endpoints.py +0 -0
  83. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/checkers/mcp.py +0 -0
  84. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/checkers/mode_parity_checker.py +0 -0
  85. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/checkers/modes.py +0 -0
  86. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/checkers/plugin_docs.py +0 -0
  87. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/checkers/provider_tables.py +0 -0
  88. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/checkers/skills.py +0 -0
  89. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/contract_parity.py +0 -0
  90. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/facts.py +0 -0
  91. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/introspect.py +0 -0
  92. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/markers.py +0 -0
  93. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/mode_meta.py +0 -0
  94. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/mode_parity.py +0 -0
  95. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/orchestrator.py +0 -0
  96. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/referential.py +0 -0
  97. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/doc_sync/triggers.py +0 -0
  98. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/known_projects.py +0 -0
  99. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/lsp_install.py +0 -0
  100. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/mcp_server/__init__.py +0 -0
  101. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/mcp_server/lifecycle.py +0 -0
  102. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/mcp_server/schemas.py +0 -0
  103. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/mcp_server/server.py +0 -0
  104. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/mcp_server/tools.py +0 -0
  105. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/migration.py +0 -0
  106. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/optional_deps.py +0 -0
  107. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/prompt_render.py +0 -0
  108. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/providers.py +0 -0
  109. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/runtime/__init__.py +0 -0
  110. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/runtime/claude_converter.py +0 -0
  111. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/runtime/codex_converter.py +0 -0
  112. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/runtime/converter_base.py +0 -0
  113. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/runtime/gemini_converter.py +0 -0
  114. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/runtime/opencode_converter.py +0 -0
  115. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/runtime/parser.py +0 -0
  116. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/runtime/skill_runtime_converter.py +0 -0
  117. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/runtime/tool_maps.py +0 -0
  118. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/runtime/types.py +0 -0
  119. {brainpalace_cli-26.7.2 → brainpalace_cli-26.7.4}/brainpalace_cli/xdg_paths.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brainpalace-cli
3
- Version: 26.7.2
4
- Summary: BrainPalace CLI - Command-line interface for managing AI agent memory and knowledge retrieval
3
+ Version: 26.7.4
4
+ Summary: BrainPalace CLI - Command-line interface for the typed-memory & retrieval engine for AI agents (code & docs today, more domains coming)
5
5
  License: MIT
6
6
  Keywords: brainpalace,rag,cli,ai-memory,llm-memory,semantic-search,ai-agent,claude-code,agent-memory
7
7
  Author: bxw91
@@ -22,6 +22,7 @@ from .commands import (
22
22
  folders_group,
23
23
  hook_group,
24
24
  index_command,
25
+ ingest_command,
25
26
  init_command,
26
27
  inject_command,
27
28
  install_agent_command,
@@ -35,8 +36,10 @@ from .commands import (
35
36
  query_command,
36
37
  recall_command,
37
38
  records_group,
39
+ references_group,
38
40
  remember_command,
39
41
  reset_command,
42
+ rules_group,
40
43
  session_path_command,
41
44
  start_command,
42
45
  status_command,
@@ -49,6 +52,7 @@ from .commands import (
49
52
  verify_docs_command,
50
53
  whoami_command,
51
54
  )
55
+ from .commands.entities import entities_group
52
56
  from .commands.graph import graph_group
53
57
 
54
58
 
@@ -137,6 +141,7 @@ cli.add_command(context_command, name="context")
137
141
  cli.add_command(submit_session_command, name="submit-session")
138
142
  cli.add_command(session_path_command, name="session-path")
139
143
  cli.add_command(index_command, name="index")
144
+ cli.add_command(ingest_command, name="ingest")
140
145
  cli.add_command(inject_command, name="inject")
141
146
  cli.add_command(jobs_command, name="jobs")
142
147
  cli.add_command(reset_command, name="reset")
@@ -152,8 +157,11 @@ cli.add_command(install_agent_command, name="install-agent")
152
157
  cli.add_command(install_session_hooks_command, name="install-session-hooks")
153
158
  cli.add_command(backfill_command, name="backfill-sessions")
154
159
  cli.add_command(records_group, name="records")
160
+ cli.add_command(references_group, name="references")
161
+ cli.add_command(rules_group, name="rules")
155
162
  cli.add_command(extraction_group, name="extraction")
156
163
  cli.add_command(graph_group, name="graph")
164
+ cli.add_command(entities_group, name="entities")
157
165
 
158
166
  # Register doc-sync maintenance commands (hidden; deterministic, never call an LLM)
159
167
  cli.add_command(sync_docs_command, name="sync-docs")
@@ -58,6 +58,9 @@ class IndexingStatus:
58
58
  migration: dict[str, Any] | None = None
59
59
  graph_index: dict[str, Any] | None = None
60
60
  features: dict[str, Any] | None = None
61
+ #: Programmatic text-ingest chunk count block ({"chunks": int}); separate
62
+ #: from folder-manifest-derived total_documents (spec Item 3).
63
+ text_ingest: dict[str, Any] | None = None
61
64
  #: Index-drift warnings (embedding provider/model or storage backend changed
62
65
  #: away from what the existing index was built with). Empty when consistent.
63
66
  index_warnings: list[str] = field(default_factory=list)
@@ -287,6 +290,7 @@ class DocServeClient:
287
290
  embedding_cache=data.get("embedding_cache"),
288
291
  graph_index=data.get("graph_index"),
289
292
  features=data.get("features"),
293
+ text_ingest=data.get("text_ingest"),
290
294
  index_warnings=data.get("index_warnings") or [],
291
295
  )
292
296
 
@@ -300,8 +304,11 @@ class DocServeClient:
300
304
  source_types: list[str] | None = None,
301
305
  languages: list[str] | None = None,
302
306
  file_paths: list[str] | None = None,
307
+ domains: list[str] | None = None,
308
+ metadata_filter: dict[str, str] | None = None,
303
309
  time_decay: bool = True,
304
310
  language: str | None = None,
311
+ include_sensitive: bool = False,
305
312
  ) -> QueryResponse:
306
313
  """
307
314
  Query indexed documents.
@@ -315,8 +322,15 @@ class DocServeClient:
315
322
  source_types: Filter by source types (doc, code, test).
316
323
  languages: Filter by programming languages.
317
324
  file_paths: Filter by file path patterns.
325
+ domains: Filter to chunks ingested under one of these domains
326
+ (reserved `domain` metadata key; OR across values).
327
+ metadata_filter: Filter to chunks whose metadata exact-matches
328
+ every key/value pair (AND across keys).
318
329
  language: BM25 query language override (ISO 639-1). Defaults to
319
330
  the project bm25.language setting when None.
331
+ include_sensitive: Reveal rows marked sensitive (interactive CLI
332
+ only). Omitted from the request when False, so MCP/dashboard
333
+ callers — which never pass it — stay at default-deny.
320
334
 
321
335
  Returns:
322
336
  QueryResponse with matching results.
@@ -330,6 +344,8 @@ class DocServeClient:
330
344
  }
331
345
  if not time_decay:
332
346
  request_data["time_decay"] = False
347
+ if include_sensitive:
348
+ request_data["include_sensitive"] = True
333
349
  # Truthy (not ``is not None``) so an empty list means "no filter" — the
334
350
  # MCP QueryInput now defaults these to ``[]`` (avoids a nullable-union
335
351
  # JSON schema that some LLM clients mishandle). Empty == omit == default.
@@ -339,6 +355,10 @@ class DocServeClient:
339
355
  request_data["languages"] = languages
340
356
  if file_paths:
341
357
  request_data["file_paths"] = file_paths
358
+ if domains:
359
+ request_data["domains"] = domains
360
+ if metadata_filter:
361
+ request_data["metadata_filter"] = metadata_filter
342
362
  if language is not None:
343
363
  request_data["language"] = language
344
364
 
@@ -449,6 +469,9 @@ class DocServeClient:
449
469
  watch_mode: str | None = None,
450
470
  watch_debounce_seconds: int | None = None,
451
471
  rebuild_graph: bool = False,
472
+ domain: str | None = None,
473
+ authority: str | None = None,
474
+ force_authority: bool = False,
452
475
  ) -> IndexResponse:
453
476
  """
454
477
  Enqueue an indexing job for documents and optionally code from a folder.
@@ -474,6 +497,11 @@ class DocServeClient:
474
497
  watch_debounce_seconds: Per-folder debounce in seconds.
475
498
  rebuild_graph: Rebuild the graph index from existing chunks only
476
499
  (no embedding); returns a completed response, not a queued job.
500
+ domain: Optional user-facing domain label for the folder (6.5).
501
+ authority: Binary provenance trust level: 'authoritative' or
502
+ 'reference' (6.5). None lets the server resolve the default.
503
+ force_authority: Allow an external folder to be registered as
504
+ 'authoritative' (6.5).
477
505
 
478
506
  Returns:
479
507
  IndexResponse with job ID and queue status.
@@ -490,7 +518,12 @@ class DocServeClient:
490
518
  "exclude_patterns": exclude_patterns,
491
519
  "force": force,
492
520
  "force_budget": force_budget,
521
+ "force_authority": force_authority,
493
522
  }
523
+ if domain is not None:
524
+ body["domain"] = domain
525
+ if authority is not None:
526
+ body["authority"] = authority
494
527
  if include_types is not None:
495
528
  body["include_types"] = include_types
496
529
  if injector_script is not None:
@@ -521,6 +554,225 @@ class DocServeClient:
521
554
  message=data.get("message"),
522
555
  )
523
556
 
557
+ def ingest_text(
558
+ self,
559
+ *,
560
+ items: list[dict[str, Any]],
561
+ sensitivity: str = "normal",
562
+ language: str | None = None,
563
+ ) -> dict[str, Any]:
564
+ """
565
+ Ingest free text with caller-supplied provenance (spec Item 3).
566
+
567
+ Args:
568
+ items: List of {text, metadata, domain, source, source_id} dicts.
569
+ sensitivity: Sensitivity mark applied to all items in this call.
570
+ language: Optional BM25 language override.
571
+
572
+ Returns:
573
+ Response dict with chunks_new, chunks_kept, chunks_deleted,
574
+ chunk_ids, source_ids.
575
+ """
576
+ body: dict[str, Any] = {"items": items, "sensitivity": sensitivity}
577
+ if language:
578
+ body["language"] = language
579
+ return self._request("POST", "/ingest/text", json=body)
580
+
581
+ def ingest_records(
582
+ self,
583
+ *,
584
+ items: list[dict[str, Any]],
585
+ sensitivity: str = "normal",
586
+ ) -> dict[str, Any]:
587
+ """Ingest caller-asserted typed records (POST /ingest/records).
588
+
589
+ Args:
590
+ items: List of {subject, metric, value, unit?, ts?, domain, source,
591
+ source_id, confidence?, properties?} dicts.
592
+ sensitivity: Sensitivity mark applied to all items in this call.
593
+
594
+ Returns:
595
+ Response dict with the number of records persisted.
596
+ """
597
+ return self._request(
598
+ "POST",
599
+ "/ingest/records",
600
+ json={"items": items, "sensitivity": sensitivity},
601
+ )
602
+
603
+ def ingest_references(
604
+ self,
605
+ *,
606
+ items: list[dict[str, Any]],
607
+ sensitivity: str = "normal",
608
+ ) -> dict[str, Any]:
609
+ """Ingest lazy-tier references (POST /ingest/references).
610
+
611
+ Args:
612
+ items: List of {pointer, summary, domain, source, source_id} dicts.
613
+ sensitivity: Sensitivity mark applied to all items in this call.
614
+
615
+ Returns:
616
+ Response dict with the number of references persisted.
617
+ """
618
+ return self._request(
619
+ "POST",
620
+ "/ingest/references",
621
+ json={"items": items, "sensitivity": sensitivity},
622
+ )
623
+
624
+ def references_list(self, domain: str | None = None) -> dict[str, Any]:
625
+ """List references, optionally filtered by domain (GET /references)."""
626
+ params = {"domain": domain} if domain else None
627
+ return self._request("GET", "/references", params=params)
628
+
629
+ def references_search(
630
+ self,
631
+ *,
632
+ query: str,
633
+ top_k: int = 5,
634
+ domain: str | None = None,
635
+ include_sensitive: bool = False,
636
+ ) -> dict[str, Any]:
637
+ """Semantic search over reference summaries (POST /references/search)."""
638
+ body: dict[str, Any] = {
639
+ "query": query,
640
+ "top_k": top_k,
641
+ "include_sensitive": include_sensitive,
642
+ }
643
+ if domain:
644
+ body["domain"] = domain
645
+ return self._request("POST", "/references/search", json=body)
646
+
647
+ def references_embed_missing(self) -> dict[str, Any]:
648
+ """Backfill embeddings for references lacking one
649
+ (POST /references/embed-missing)."""
650
+ return self._request("POST", "/references/embed-missing")
651
+
652
+ def ingest_sources(
653
+ self,
654
+ *,
655
+ domain: str | None = None,
656
+ source: str | None = None,
657
+ include_sensitive: bool = False,
658
+ ) -> dict[str, Any]:
659
+ """List distinct ingested source_ids (GET /ingest/sources).
660
+
661
+ Args:
662
+ domain: Optional domain filter.
663
+ source: Optional raw-source filter.
664
+ include_sensitive: Reveal sources whose chunks are marked sensitive.
665
+
666
+ Returns:
667
+ Response dict with `sources` (each: source_id, domain, source,
668
+ chunk_count, ingested_at) and `total`.
669
+ """
670
+ params: dict[str, Any] = {}
671
+ if domain:
672
+ params["domain"] = domain
673
+ if source:
674
+ params["source"] = source
675
+ if include_sensitive:
676
+ params["include_sensitive"] = True
677
+ return self._request("GET", "/ingest/sources", params=params or None)
678
+
679
+ def ingest_show(
680
+ self,
681
+ source_id: str,
682
+ *,
683
+ offset: int = 0,
684
+ limit: int = 50,
685
+ include_sensitive: bool = False,
686
+ ) -> dict[str, Any]:
687
+ """Show one source_id's ingested chunks, paginated
688
+ (GET /ingest/text/{source_id}).
689
+
690
+ Args:
691
+ source_id: Caller-supplied source id used at ingest time.
692
+ offset: Pagination offset.
693
+ limit: Page size.
694
+ include_sensitive: Reveal chunks marked sensitive.
695
+
696
+ Returns:
697
+ Response dict with source_id, total, offset, limit, and chunks
698
+ (each: chunk_id, text, metadata).
699
+ """
700
+ params: dict[str, Any] = {"offset": offset, "limit": limit}
701
+ if include_sensitive:
702
+ params["include_sensitive"] = True
703
+ return self._request("GET", f"/ingest/text/{source_id}", params=params)
704
+
705
+ def ingest_delete(self, source_id: str) -> dict[str, Any]:
706
+ """
707
+ Delete all ingested chunks for a source_id (un-ingest).
708
+
709
+ Args:
710
+ source_id: Caller-supplied source id used at ingest time.
711
+
712
+ Returns:
713
+ Response dict with chunks_deleted.
714
+ """
715
+ return self._request("DELETE", f"/ingest/text/{source_id}")
716
+
717
+ def ingest_forget(self, source_id: str) -> dict[str, Any]:
718
+ """
719
+ Full forget: delete a source_id across all three ingest tiers
720
+ (DELETE /ingest/source/{source_id}).
721
+
722
+ Args:
723
+ source_id: Caller-supplied source id used at ingest time.
724
+
725
+ Returns:
726
+ Response dict with chunks_deleted, records_deleted,
727
+ references_deleted.
728
+ """
729
+ return self._request("DELETE", f"/ingest/source/{source_id}")
730
+
731
+ # --- identity (G5): person / alias / link ---------------------------
732
+
733
+ def entities_person(self, person: dict[str, Any]) -> dict[str, Any]:
734
+ """Upsert a person (POST /entities/person)."""
735
+ return self._request("POST", "/entities/person", json=person)
736
+
737
+ def entities_alias(self, alias: dict[str, Any]) -> dict[str, Any]:
738
+ """Bind a surface to a person (POST /entities/alias)."""
739
+ return self._request("POST", "/entities/alias", json=alias)
740
+
741
+ def entities_link(self, link: dict[str, Any]) -> dict[str, Any]:
742
+ """Attach a ref to a person or record it unresolved
743
+ (POST /entities/link)."""
744
+ return self._request("POST", "/entities/link", json=link)
745
+
746
+ def entities_resolve(
747
+ self,
748
+ *,
749
+ surface: str,
750
+ scope: str | None = None,
751
+ at: str | None = None,
752
+ ref: str | None = None,
753
+ session_id: str | None = None,
754
+ ) -> dict[str, Any]:
755
+ """Ranked candidates + evidence (GET /entities/resolve). Never picks."""
756
+ params: dict[str, Any] = {"surface": surface}
757
+ if scope is not None:
758
+ params["scope"] = scope
759
+ if at is not None:
760
+ params["at"] = at
761
+ if ref is not None:
762
+ params["ref"] = ref
763
+ if session_id is not None:
764
+ params["session_id"] = session_id
765
+ return self._request("GET", "/entities/resolve", params=params)
766
+
767
+ def entities_unresolved(self) -> dict[str, Any]:
768
+ """The unresolved-link bucket (GET /entities/unresolved)."""
769
+ return self._request("GET", "/entities/unresolved")
770
+
771
+ def entities_backfill(self) -> dict[str, Any]:
772
+ """Re-score unresolved links against current aliases
773
+ (POST /entities/backfill)."""
774
+ return self._request("POST", "/entities/backfill")
775
+
524
776
  def estimate_index(
525
777
  self,
526
778
  folder_path: str,
@@ -11,6 +11,7 @@ from .extraction import extraction_group
11
11
  from .folders import folders_group
12
12
  from .hook import hook_group
13
13
  from .index import index_command
14
+ from .ingest import ingest_command
14
15
  from .init import init_command
15
16
  from .inject import inject_command
16
17
  from .install_agent import install_agent_command
@@ -23,8 +24,10 @@ from .plugin_detect import plugin_group
23
24
  from .query import query_command
24
25
  from .recall import recall_command
25
26
  from .records import records_group
27
+ from .references import references_group
26
28
  from .remember import remember_command
27
29
  from .reset import reset_command
30
+ from .rules import rules_group
28
31
  from .session_hooks import install_session_hooks_command
29
32
  from .sessions import session_path_command, submit_session_command
30
33
  from .start import start_command
@@ -47,6 +50,7 @@ __all__ = [
47
50
  "doctor_command",
48
51
  "folders_group",
49
52
  "index_command",
53
+ "ingest_command",
50
54
  "inject_command",
51
55
  "init_command",
52
56
  "install_agent_command",
@@ -61,9 +65,11 @@ __all__ = [
61
65
  "hook_group",
62
66
  "query_command",
63
67
  "records_group",
68
+ "references_group",
64
69
  "recall_command",
65
70
  "remember_command",
66
71
  "reset_command",
72
+ "rules_group",
67
73
  "session_path_command",
68
74
  "start_command",
69
75
  "submit_session_command",
@@ -0,0 +1,231 @@
1
+ """Identity commands (G5): person / alias / link over the engine's identity
2
+ store. The engine stores and RANKS candidates; it never picks a winner — the
3
+ consumer app decides (D7).
4
+
5
+ ``--json`` contract (mirrors ``ingest``): on success the result dict is printed;
6
+ on failure a ``{"error": ...}`` object is printed with a non-zero exit and NO
7
+ ``results`` key."""
8
+
9
+ import json as _json
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+
13
+ import click
14
+ from rich.console import Console
15
+
16
+ from ..client import (
17
+ ConnectionError,
18
+ DocServeClient,
19
+ ServerError,
20
+ exit_on_connection_error,
21
+ )
22
+ from ..config import get_server_url
23
+
24
+ console = Console()
25
+
26
+
27
+ def _emit(result: dict[str, Any], json_output: bool) -> None:
28
+ if json_output:
29
+ click.echo(_json.dumps(result, indent=2))
30
+ else:
31
+ console.print(result)
32
+
33
+
34
+ def _run(
35
+ resolved_url: str,
36
+ json_output: bool,
37
+ fn: Callable[[DocServeClient], dict[str, Any]],
38
+ ) -> None:
39
+ """Execute ``fn(client)`` with the shared connection/error handling and
40
+ the ``--json`` failure contract."""
41
+ try:
42
+ with DocServeClient(base_url=resolved_url) as client:
43
+ result = fn(client)
44
+ _emit(result, json_output)
45
+ except ConnectionError as e:
46
+ exit_on_connection_error(e, base_url=resolved_url, json_output=json_output)
47
+ except ServerError as e:
48
+ if json_output:
49
+ click.echo(_json.dumps({"error": str(e), "detail": e.detail}))
50
+ else:
51
+ console.print(f"[red]Server Error ({e.status_code}):[/] {e.detail}")
52
+ raise SystemExit(1) from e
53
+
54
+
55
+ _URL = click.option(
56
+ "--url",
57
+ envvar="BRAINPALACE_URL",
58
+ default=None,
59
+ help="BrainPalace server URL (default: from config or http://127.0.0.1:8000)",
60
+ )
61
+ _JSON = click.option("--json", "json_output", is_flag=True, help="Output as JSON")
62
+
63
+
64
+ @click.group("entities")
65
+ def entities_group() -> None:
66
+ """Manage identity: person / alias / link (who someone is)."""
67
+
68
+
69
+ @entities_group.command("person")
70
+ @click.option("--id", "person_id", default=None, help="Existing person id (upsert).")
71
+ @click.option("--name", default=None, help="Display name (omit for an unknown person).")
72
+ @click.option("--kind", default="person", show_default=True, help="person|place|org.")
73
+ @click.option("--domain", required=True, help="Domain the person belongs to.")
74
+ @click.option("--sensitivity", default="normal", show_default=True)
75
+ @_URL
76
+ @_JSON
77
+ def person_cmd(
78
+ person_id: str | None,
79
+ name: str | None,
80
+ kind: str,
81
+ domain: str,
82
+ sensitivity: str,
83
+ url: str | None,
84
+ json_output: bool,
85
+ ) -> None:
86
+ """Upsert a person (naming an unknown one is an update in place)."""
87
+ resolved_url = url or get_server_url()
88
+ body = {"kind": kind, "domain": domain, "sensitivity": sensitivity}
89
+ if person_id:
90
+ body["id"] = person_id
91
+ if name is not None:
92
+ body["name"] = name
93
+ _run(resolved_url, json_output, lambda c: c.entities_person(body))
94
+
95
+
96
+ @entities_group.command("alias")
97
+ @click.option("--surface", required=True, help='Surface string, e.g. "Mama".')
98
+ @click.option(
99
+ "--person-id", "person_id", required=True, help="Person the surface names."
100
+ )
101
+ @click.option("--scope", default=None, help="Speaker person id; omit for global.")
102
+ @click.option("--valid-from", "valid_from", default=None)
103
+ @click.option("--valid-to", "valid_to", default=None)
104
+ @_URL
105
+ @_JSON
106
+ def alias_cmd(
107
+ surface: str,
108
+ person_id: str,
109
+ scope: str | None,
110
+ valid_from: str | None,
111
+ valid_to: str | None,
112
+ url: str | None,
113
+ json_output: bool,
114
+ ) -> None:
115
+ """Bind a surface to a person (scoped + time-bounded)."""
116
+ resolved_url = url or get_server_url()
117
+ body = {"surface": surface, "person_id": person_id}
118
+ if scope is not None:
119
+ body["scope"] = scope
120
+ if valid_from is not None:
121
+ body["valid_from"] = valid_from
122
+ if valid_to is not None:
123
+ body["valid_to"] = valid_to
124
+ _run(resolved_url, json_output, lambda c: c.entities_alias(body))
125
+
126
+
127
+ @entities_group.command("link")
128
+ @click.option(
129
+ "--ref", required=True, help="chunk address, session id, or external key."
130
+ )
131
+ @click.option(
132
+ "--ref-kind", "ref_kind", required=True, help="chunk|span|session|external."
133
+ )
134
+ @click.option("--role", required=True, help="speaker|mentioned|participant.")
135
+ @click.option(
136
+ "--method", required=True, help="user_asserted|call_log|llm_inferred|alias_match."
137
+ )
138
+ @click.option("--at", required=True, help="Mention timestamp (ISO-8601).")
139
+ @click.option(
140
+ "--person-id", "person_id", default=None, help="Resolved person; omit = unresolved."
141
+ )
142
+ @click.option("--span-start", "span_start", type=int, default=None)
143
+ @click.option("--span-end", "span_end", type=int, default=None)
144
+ @click.option("--surface", default=None, help="Surface being resolved (for backfill).")
145
+ @click.option("--scope", default=None, help="Scope of that surface (for backfill).")
146
+ @click.option("--confidence", type=float, default=None)
147
+ @_URL
148
+ @_JSON
149
+ def link_cmd(
150
+ ref: str,
151
+ ref_kind: str,
152
+ role: str,
153
+ method: str,
154
+ at: str,
155
+ person_id: str | None,
156
+ span_start: int | None,
157
+ span_end: int | None,
158
+ surface: str | None,
159
+ scope: str | None,
160
+ confidence: float | None,
161
+ url: str | None,
162
+ json_output: bool,
163
+ ) -> None:
164
+ """Attach a ref to a person, or record it unresolved."""
165
+ resolved_url = url or get_server_url()
166
+ body: dict[str, Any] = {
167
+ "ref": ref,
168
+ "ref_kind": ref_kind,
169
+ "role": role,
170
+ "method": method,
171
+ "at": at,
172
+ }
173
+ if person_id is not None:
174
+ body["person_id"] = person_id
175
+ if span_start is not None:
176
+ body["span_start"] = span_start
177
+ if span_end is not None:
178
+ body["span_end"] = span_end
179
+ if surface is not None:
180
+ body["surface"] = surface
181
+ if scope is not None:
182
+ body["scope"] = scope
183
+ if confidence is not None:
184
+ body["confidence"] = confidence
185
+ _run(resolved_url, json_output, lambda c: c.entities_link(body))
186
+
187
+
188
+ @entities_group.command("resolve")
189
+ @click.option("--surface", required=True, help="Surface string to resolve.")
190
+ @click.option("--scope", default=None, help="Speaker person id; omit for global.")
191
+ @click.option("--at", default=None, help="Resolution timestamp (defaults to now).")
192
+ @click.option("--ref", default=None, help="Chunk/session ref for co-occurrence.")
193
+ @click.option("--session-id", "session_id", default=None, help="Session for recency.")
194
+ @_URL
195
+ @_JSON
196
+ def resolve_cmd(
197
+ surface: str,
198
+ scope: str | None,
199
+ at: str | None,
200
+ ref: str | None,
201
+ session_id: str | None,
202
+ url: str | None,
203
+ json_output: bool,
204
+ ) -> None:
205
+ """Ranked candidates + evidence. Never picks a winner."""
206
+ resolved_url = url or get_server_url()
207
+ _run(
208
+ resolved_url,
209
+ json_output,
210
+ lambda c: c.entities_resolve(
211
+ surface=surface, scope=scope, at=at, ref=ref, session_id=session_id
212
+ ),
213
+ )
214
+
215
+
216
+ @entities_group.command("unresolved")
217
+ @_URL
218
+ @_JSON
219
+ def unresolved_cmd(url: str | None, json_output: bool) -> None:
220
+ """List unresolved links (the bucket the app must decide or ask about)."""
221
+ resolved_url = url or get_server_url()
222
+ _run(resolved_url, json_output, lambda c: c.entities_unresolved())
223
+
224
+
225
+ @entities_group.command("backfill")
226
+ @_URL
227
+ @_JSON
228
+ def backfill_cmd(url: str | None, json_output: bool) -> None:
229
+ """Re-score unresolved links against the current aliases."""
230
+ resolved_url = url or get_server_url()
231
+ _run(resolved_url, json_output, lambda c: c.entities_backfill())