smartmemory 1.4.0__tar.gz → 1.4.3__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 (108) hide show
  1. {smartmemory-1.4.0 → smartmemory-1.4.3}/CHANGELOG.md +53 -0
  2. {smartmemory-1.4.0 → smartmemory-1.4.3}/PKG-INFO +2 -2
  3. {smartmemory-1.4.0 → smartmemory-1.4.3}/README.md +5 -1
  4. {smartmemory-1.4.0 → smartmemory-1.4.3}/pyproject.toml +2 -2
  5. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/cli.py +184 -48
  6. smartmemory-1.4.3/smartmemory_app/cli_code.py +212 -0
  7. smartmemory-1.4.3/smartmemory_app/cli_mcp.py +197 -0
  8. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/events_server.py +101 -0
  9. smartmemory-1.4.3/smartmemory_app/launch_metrics.py +66 -0
  10. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/local_api.py +209 -17
  11. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/setup.py +20 -1
  12. smartmemory-1.4.3/smartmemory_app/skills/remember.md +22 -0
  13. smartmemory-1.4.3/smartmemory_app/static/assets/local-Clt8rYLQ.js +335 -0
  14. smartmemory-1.4.3/smartmemory_app/static/assets/local-ZCcXPKXN.css +1 -0
  15. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/static/index.html +2 -2
  16. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/storage.py +21 -2
  17. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/viewer_server.py +33 -2
  18. smartmemory-1.4.3/tests/integration/test_cli_new_subcommands.py +240 -0
  19. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_cli.py +19 -2
  20. smartmemory-1.4.3/tests/unit/test_lite_api_contract.py +369 -0
  21. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_local_api.py +33 -10
  22. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_storage.py +48 -0
  23. smartmemory-1.4.0/smartmemory_app/skills/remember.md +0 -9
  24. smartmemory-1.4.0/smartmemory_app/static/assets/local-Bmk6TID5.js +0 -334
  25. smartmemory-1.4.0/smartmemory_app/static/assets/local-CX-jTTRv.css +0 -1
  26. smartmemory-1.4.0/smartmemory_app/static/assets/local-C_ntKcqJ.js +0 -334
  27. smartmemory-1.4.0/smartmemory_app/static/assets/local-D0CVH7by.css +0 -1
  28. smartmemory-1.4.0/smartmemory_app/static/assets/local-D4I7ctUR.js +0 -334
  29. smartmemory-1.4.0/smartmemory_app/static/assets/local-DhOHKVEA.js +0 -334
  30. smartmemory-1.4.0/smartmemory_app/static/assets/local-TN7VFhW8.js +0 -334
  31. smartmemory-1.4.0/smartmemory_app/static/assets/local-YU236KFI.js +0 -334
  32. smartmemory-1.4.0/smartmemory_app/static/local.html +0 -13
  33. {smartmemory-1.4.0 → smartmemory-1.4.3}/.github/workflows/publish.yml +0 -0
  34. {smartmemory-1.4.0 → smartmemory-1.4.3}/.gitignore +0 -0
  35. {smartmemory-1.4.0 → smartmemory-1.4.3}/LICENSE +0 -0
  36. {smartmemory-1.4.0 → smartmemory-1.4.3}/LICENSE.agpl-v3 +0 -0
  37. {smartmemory-1.4.0 → smartmemory-1.4.3}/LICENSE.header +0 -0
  38. {smartmemory-1.4.0 → smartmemory-1.4.3}/plugin.json +0 -0
  39. {smartmemory-1.4.0 → smartmemory-1.4.3}/scripts/generate_seed_patterns.py +0 -0
  40. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/__init__.py +0 -0
  41. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/__main__.py +0 -0
  42. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/async_enrichment.py +0 -0
  43. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/config.py +0 -0
  44. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/daemon.py +0 -0
  45. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/ai.smartmemory.daemon.plist +0 -0
  46. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/ai.smartmemory.worker.plist +0 -0
  47. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/seed_patterns.jsonl +0 -0
  48. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/seeds/ai-model.jsonl +0 -0
  49. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/seeds/concept.jsonl +0 -0
  50. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/seeds/database.jsonl +0 -0
  51. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/seeds/framework.jsonl +0 -0
  52. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/seeds/language.jsonl +0 -0
  53. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/seeds/manifest.json +0 -0
  54. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/seeds/organization.jsonl +0 -0
  55. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/seeds/platform.jsonl +0 -0
  56. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/seeds/protocol.jsonl +0 -0
  57. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/seeds/service.jsonl +0 -0
  58. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/data/seeds/tool.jsonl +0 -0
  59. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/enrichment_queue.py +0 -0
  60. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/enrichment_worker.py +0 -0
  61. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/event_sink.py +0 -0
  62. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/hooks/distill.sh +0 -0
  63. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/hooks/learn.sh +0 -0
  64. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/hooks/observe.sh +0 -0
  65. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/hooks/orient.sh +0 -0
  66. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/hooks/persist.sh +0 -0
  67. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/hooks/recall.sh +0 -0
  68. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/lifecycle.py +0 -0
  69. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/lifecycle_api.py +0 -0
  70. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/lifecycle_config.py +0 -0
  71. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/patterns.py +0 -0
  72. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/recall_format.py +0 -0
  73. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/remote_backend.py +0 -0
  74. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/setup_tui.py +0 -0
  75. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/skills/ingest.md +0 -0
  76. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/skills/orient.md +0 -0
  77. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/skills/search.md +0 -0
  78. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/static/icons/icon-192x192.svg +0 -0
  79. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/static/icons/icon-512x512.svg +0 -0
  80. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/static/logo.svg +0 -0
  81. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/static/viewer-logo.svg +0 -0
  82. {smartmemory-1.4.0 → smartmemory-1.4.3}/smartmemory_app/sync.py +0 -0
  83. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/__init__.py +0 -0
  84. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/conftest.py +0 -0
  85. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/integration/__init__.py +0 -0
  86. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/integration/test_async_enrichment_e2e.py +0 -0
  87. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/integration/test_async_enrichment_sqlite_regression.py +0 -0
  88. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/integration/test_daemon.py +0 -0
  89. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/integration/test_entity_ruler_patterns.py +0 -0
  90. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/integration/test_hook_recall.py +0 -0
  91. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/integration/test_hook_scripts.py +0 -0
  92. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/integration/test_local_api_integration.py +0 -0
  93. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/integration/test_recall_confidence.py +0 -0
  94. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/integration/test_recall_flow.py +0 -0
  95. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/integration/test_viewer_server.py +0 -0
  96. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/__init__.py +0 -0
  97. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_async_enrichment.py +0 -0
  98. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_config.py +0 -0
  99. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_events_server.py +0 -0
  100. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_launchd.py +0 -0
  101. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_lifecycle.py +0 -0
  102. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_patterns.py +0 -0
  103. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_recall_format.py +0 -0
  104. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_remote_backend.py +0 -0
  105. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_server_tools.py +0 -0
  106. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_setup.py +0 -0
  107. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_setup_tui.py +0 -0
  108. {smartmemory-1.4.0 → smartmemory-1.4.3}/tests/unit/test_stale_markers.py +0 -0
@@ -1,5 +1,52 @@
1
1
  # Changelog — smartmemory
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ### Fixed (MCP `memory_search` broken in local mode, 2026-05-18)
6
+
7
+ - **MCP `memory_search` completely broken in local mode.** `storage.search()` only accepted `(query, top_k, filters, include_reference)`. The MCP server calls it with `memory_type`, `enable_hybrid`, `decompose_query`, `multi_hop`, `max_hops`, and `budget_ms` — any of those raised `TypeError: search() got an unexpected keyword argument`. Every `memory_search` call in local mode crashed. `storage.search` now accepts `memory_type` (forwarded as a post-filter) and `**search_kwargs` (allowlisted to `decompose_query`, `channel_weights`, `multi_hop`, `max_hops`, `budget_ms`, `semantic_hops`); unknown keys such as `enable_hybrid` are silently dropped instead of raising. Regression test added (`test_search_accepts_mcp_recall_kwargs`).
8
+
9
+ ### Fixed (DEMO-WALKTHROUGH-1, `smartmemory search` CLI, 2026-05-17)
10
+
11
+ - **`smartmemory search` no longer crashes with `AttributeError: 'str' object has no attribute 'get'`.** The daemon `/memory/search` returns the CORE-CRUD-LIST contract shape `{"items": [...]}`, but `cli.py search_cmd` iterated the dict directly — so `for r in results` yielded the key string `"items"` and `r.get("content")` threw. Now unwraps `results["items"]` (daemon) vs bare list (storage fallback), with a defensive non-dict skip. Regression test added (`test_search_cmd_daemon_items_contract`); fixed two stale `assert_called_once_with` expectations that predated the `include_reference` fallback kwarg.
12
+
13
+ ## [1.4.3] - 2026-05-17
14
+
15
+ ### Changed (lockstep relock — ships the competitive-response sprint)
16
+
17
+ - **Pinned `smartmemory-core[lite]==0.9.8`** (was `==0.9.1`, 7 versions stale despite the "move in lockstep" comment). The wrapper now actually pulls the shipped sprint core: CORE-BITEMPORAL-1 transaction-time activation, CORE-ATTENTION-FUSION-1 Phase 1, APP-VAULT-SYNC-1 Phase 0, RECALL-CITATIONS-1. Without this, `pip install smartmemory` resolved core 0.9.1 — none of the differentiator. Supersedes the never-published 1.4.2 (committed but no Release was cut; wrapper publishes only on GitHub Release / workflow_dispatch).
18
+
19
+ ### Fixed (DEMO-WALKTHROUGH-1, lite-mode graph viewer SSE, 2026-05-17)
20
+
21
+ - **Lite daemon now serves `GET /memory/progress/stream` (SSE).** `smart-memory-graph`'s `useGraphStream` migrated WS→SSE; the lite daemon only exposed the legacy `sm.v1` WebSocket on `:9015`, so the local graph viewer never connected ("Not connected to event stream", 0 nodes).
22
+ - `smartmemory_app/events_server.py`: SSE subscriber registry + `_to_progress_event()` reshaping raw sink items into ProgressEvent contract frames (`kind: graph.node|graph.edge`) + `_fanout_sse()` at the **single** existing `sink._q` drain point. The `ws://:9015` broadcast is byte-for-byte unchanged (SSE is an additional broadcast target, not a second queue consumer — preserves existing WS clients incl. the recorder). Cross-loop hand-off via `call_soon_threadsafe` (same bridge as `InProcessQueueSink.emit`).
23
+ - `smartmemory_app/local_api.py`: `GET /memory/progress/stream` `StreamingResponse` (per-connection queue, 15s keepalive, disconnect cleanup), declared before `/{memory_id}` routes.
24
+ - Verified: one ingest yields 6 `graph.node` + 10 `graph.edge` contract frames with `payload.data.memory_id`+`label`; legacy WS path unaffected.
25
+ - **Events server port now tracks the API port.** `smartmemory_app/viewer_server.py` starts the events WS on `port + 1` instead of a hardcoded `9015`. Default `9014 → 9015` is unchanged; a non-default daemon (e.g. an isolated demo on `9114`) now gets its own events server (`9115`) instead of colliding with the dev daemon's `:9015`. Enables running an isolated demo daemon alongside the launchd dev daemon.
26
+
27
+ ### Added (LAUNCH-METRICS-1, Wave 2 Stream I, 2026-05-10)
28
+
29
+ - **`smartmemory_app.launch_metrics`** module — best-effort `emit(event_type, props)` POSTing to the daemon HTTP API at `/launch/event`. Honors `SMARTMEMORY_DISABLE_LAUNCH_METRICS=1` opt-out. Failures log WARNING; never raises into a CLI command path.
30
+ - Funnel emission wired into the three Stream A entry points:
31
+ - `sm setup` / `sm init` -> `setup.complete` with `{mode: local|remote}`
32
+ - `sm code index` -> `index.start` and `index.complete` with `{repo, files, entities, edges, elapsed_s}`
33
+ - `sm mcp install <client>` -> `mcp.install` with `{client, dry_run}`
34
+ - **Daemon-side ingest** at `POST /launch/event` in `local_api.py`. Local mode appends to `launch_events.jsonl` in the data dir.
35
+
36
+ Contract: `smart-memory-docs/docs/features/LAUNCH-METRICS-1/launch-event-contract.json`.
37
+
38
+ ## [1.4.2] — 2026-05-10
39
+
40
+ ### Added (Launch Sprint Wave 1, Stream A)
41
+
42
+ - **`sm init`** — alias for `sm setup`. Same Click command registered under a second name; option/flag parity is automatic.
43
+ - **`sm code index <path>`** — wraps `SmartMemory.ingest_code()` from the core library to index Python (and optionally TypeScript) repos into the local knowledge graph. Streams structured progress to stdout (`[code:index] phase=start|done …`). Default exclusions cover `node_modules`, virtualenvs, build artifacts, and VCS dirs. Every entity is tagged with `origin = code:index` (Tier 1 user content per `smartmemory/origin_policy.py`) — set by the indexer itself, not the wrapper. Flags: `--repo`, `--language` (repeatable, `python|typescript`), `--exclude` (repeatable), `--commit-hash`.
44
+ - **`sm mcp install <client>`** — writes MCP config for `claude-code` (→ `~/.claude.json`), `cursor` (→ `~/.cursor/mcp.json`), or `codex` (→ `~/.codex/config.toml`). Pointer-only wrapper around the already-shipped `smartmemory-mcp` PyPI package — no new server logic. Flags: `--dry-run` prints the would-be config without touching disk; `--path` overrides the destination. Existing config files are merged (JSON) or section-replaced (TOML); malformed configs raise loudly rather than being clobbered.
45
+
46
+ ### Changed (CORE-EXPERTISE-1 Phase 4b, 2026-05-08)
47
+
48
+ - **README "Memory Types" listing extended** with `Constraint Memory` and `Learned Memory` entries plus a new "Expertise vs knowledge" callout that explains the knowledge / expertise cohort split, points to the canonical 1-pager at `docs.smartmemory.ai/smartmemory/concepts/expertise-vs-knowledge`, and surfaces `mem.search(query, expertise=True)` for partitioned recall. No code change.
49
+
3
50
  ## [1.1.5] — 2026-03-24
4
51
 
5
52
  ### Fixed
@@ -14,6 +61,12 @@
14
61
 
15
62
  ## [Unreleased]
16
63
 
64
+ ## [1.4.1] — 2026-05-07
65
+
66
+ ### Changed
67
+
68
+ - **Track core 0.9.1.** Pin updated from `smartmemory-core[lite]==0.9.0` to `==0.9.1`. Wrapper itself unchanged; bump pairs with the core release that lands the bounded `EventStream.read_recent(count)` API plus `read_all` deprecation (INSIGHTS-PROGRESS-MIGRATE-1 prep), CORE-EXPERTISE-1 Phase 1 (Decision schema extension), and ONTO-READER-MIGRATE-1 (`OntologyGraph` `:EntityType` → `:OntologyType` reader migration). Per `feedback_bump_wrapper_with_core` policy.
69
+
17
70
  ## [1.4.0] — 2026-05-04
18
71
 
19
72
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smartmemory
3
- Version: 1.4.0
3
+ Version: 1.4.3
4
4
  License-File: LICENSE
5
5
  License-File: LICENSE.agpl-v3
6
6
  License-File: LICENSE.header
@@ -10,7 +10,7 @@ Requires-Dist: fastapi>=0.110
10
10
  Requires-Dist: filelock>=3.12
11
11
  Requires-Dist: httpx>=0.27
12
12
  Requires-Dist: keyring>=24.0
13
- Requires-Dist: smartmemory-core[lite]==0.9.0
13
+ Requires-Dist: smartmemory-core[lite]==0.9.8
14
14
  Requires-Dist: smartmemory-mcp>=0.2.0
15
15
  Requires-Dist: textual>=8.0
16
16
  Requires-Dist: tomli-w>=1.0
@@ -162,7 +162,11 @@ SmartMemory implements a multi-layered memory architecture:
162
162
  - **Reasoning Memory**: Chain-of-thought traces capturing "why" decisions were made (System 2)
163
163
  - **Opinion Memory**: Beliefs with confidence scores, reinforced or contradicted over time
164
164
  - **Observation Memory**: Synthesized entity summaries from scattered facts
165
- - **Decision Memory**: First-class decisions with confidence tracking, provenance chains, and lifecycle management
165
+ - **Decision Memory**: First-class decisions with confidence tracking, provenance chains, and lifecycle management. Now structured: `rejected_alternatives`, `rationale`, `constraints`. Capture: `mem.add_decision(...)`.
166
+ - **Constraint Memory**: Hard rules — discovered or imposed. Capture: `mem.add_constraint(...)`.
167
+ - **Learned Memory**: Lessons learned the hard way. Capture: `mem.add_learning(...)`.
168
+
169
+ > **Expertise vs knowledge.** The five core types above (Pending/Semantic/Episodic/Procedural/Zettelkasten) plus Reasoning are best read as the **knowledge layer** — what's *true*. Decision/Constraint/Learned/Opinion/Observation form the **expertise layer** — what to *do*, and what *not* to do. The expertise layer is what makes an agent's memory useful for *acting*: captured choices, rejected alternatives, hard constraints, lessons learned. Recall partitioned by expertise type via `mem.search(query, expertise=True)`. See [Expertise vs Knowledge](https://docs.smartmemory.ai/smartmemory/concepts/expertise-vs-knowledge) for the full mapping.
166
170
 
167
171
  ### Storage Backends
168
172
 
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "smartmemory"
7
- version = "1.4.0"
7
+ version = "1.4.3"
8
8
  requires-python = ">=3.11"
9
9
  dependencies = [
10
- "smartmemory-core[lite]==0.9.0", # EXACT pin — wrapper and core versions move in lockstep
10
+ "smartmemory-core[lite]==0.9.8", # EXACT pin — wrapper and core versions move in lockstep
11
11
  "filelock>=3.12", # cross-process SQLite write locking
12
12
  "smartmemory-mcp>=0.2.0", # unified MCP server (PLAT-MCP-UNIFY-1)
13
13
  "httpx>=0.27", # remote API calls (DIST-LITE-5)
@@ -18,7 +18,11 @@ def _parse_extra_props(args: list[str]) -> dict[str, str]:
18
18
  props = {}
19
19
  i = 0
20
20
  while i < len(args):
21
- if args[i].startswith("--") and i + 1 < len(args) and not args[i + 1].startswith("--"):
21
+ if (
22
+ args[i].startswith("--")
23
+ and i + 1 < len(args)
24
+ and not args[i + 1].startswith("--")
25
+ ):
22
26
  props[args[i][2:]] = args[i + 1]
23
27
  i += 2
24
28
  else:
@@ -44,7 +48,9 @@ def _daemon_request(method: str, path: str, timeout: int = 120, **kwargs):
44
48
 
45
49
  for attempt in range(2):
46
50
  try:
47
- r = httpx.request(method, f"{_daemon_url()}{path}", timeout=timeout, **kwargs)
51
+ r = httpx.request(
52
+ method, f"{_daemon_url()}{path}", timeout=timeout, **kwargs
53
+ )
48
54
  r.raise_for_status()
49
55
  return r.json() if r.status_code != 204 else {}
50
56
  except (httpx.ConnectError, httpx.ConnectTimeout, httpx.RemoteProtocolError):
@@ -72,14 +78,29 @@ def cli() -> None:
72
78
  from smartmemory_app.setup import setup as _setup_cmd, uninstall as _uninstall_cmd # noqa: E402
73
79
 
74
80
  cli.add_command(_setup_cmd, name="setup")
81
+ # `sm init` — alias for `sm setup`. Same Click command registered under a
82
+ # second name so option/flag parity is automatic.
83
+ cli.add_command(_setup_cmd, name="init")
75
84
  cli.add_command(_uninstall_cmd, name="uninstall")
76
85
 
86
+ # `sm code …` and `sm mcp …` subgroups
87
+ from smartmemory_app.cli_code import code_group as _code_group # noqa: E402
88
+ from smartmemory_app.cli_mcp import mcp_group as _mcp_group # noqa: E402
89
+
90
+ cli.add_command(_code_group, name="code")
91
+ cli.add_command(_mcp_group, name="mcp")
92
+
77
93
 
78
94
  # ── Daemon lifecycle ────────────────────────────────────────────────────────
79
95
 
80
96
 
81
97
  @cli.command("start")
82
- @click.option("--num-workers", default=1, show_default=True, help="Number of enrichment worker processes.")
98
+ @click.option(
99
+ "--num-workers",
100
+ default=1,
101
+ show_default=True,
102
+ help="Number of enrichment worker processes.",
103
+ )
83
104
  def start_cmd(num_workers: int) -> None:
84
105
  """Start the SmartMemory daemon and enrichment workers."""
85
106
  from smartmemory_app.daemon import start_daemon, is_running
@@ -109,7 +130,12 @@ def stop_cmd() -> None:
109
130
 
110
131
 
111
132
  @cli.command("restart")
112
- @click.option("--num-workers", default=1, show_default=True, help="Number of enrichment worker processes.")
133
+ @click.option(
134
+ "--num-workers",
135
+ default=1,
136
+ show_default=True,
137
+ help="Number of enrichment worker processes.",
138
+ )
113
139
  def restart_cmd(num_workers: int) -> None:
114
140
  """Restart the SmartMemory daemon and enrichment workers."""
115
141
  from smartmemory_app.daemon import stop_daemon, start_daemon, is_running
@@ -162,7 +188,9 @@ def viewer_cmd(port: int | None) -> None:
162
188
 
163
189
 
164
190
  @cli.command("worker")
165
- @click.option("--loop", is_flag=True, help="Poll continuously instead of drain-and-exit.")
191
+ @click.option(
192
+ "--loop", is_flag=True, help="Poll continuously instead of drain-and-exit."
193
+ )
166
194
  def worker_cmd(loop: bool) -> None:
167
195
  """Run the enrichment worker (Tier 2 LLM extraction).
168
196
 
@@ -180,8 +208,15 @@ def worker_cmd(loop: bool) -> None:
180
208
  # ── Memory operations ───────────────────────────────────────────────────────
181
209
 
182
210
  _VALID_MEMORY_TYPES = {
183
- "pending", "semantic", "episodic", "procedural", "zettel",
184
- "reasoning", "opinion", "observation", "decision",
211
+ "pending",
212
+ "semantic",
213
+ "episodic",
214
+ "procedural",
215
+ "zettel",
216
+ "reasoning",
217
+ "opinion",
218
+ "observation",
219
+ "decision",
185
220
  }
186
221
 
187
222
 
@@ -194,12 +229,27 @@ def _validate_memory_type(ctx, param, value: str) -> str:
194
229
  return value
195
230
 
196
231
 
197
- @cli.command("add", context_settings=dict(
198
- ignore_unknown_options=True, allow_extra_args=True,
199
- ))
232
+ @cli.command(
233
+ "add",
234
+ context_settings=dict(
235
+ ignore_unknown_options=True,
236
+ allow_extra_args=True,
237
+ ),
238
+ )
200
239
  @click.argument("text", default="-")
201
- @click.option("--type", "memory_type", default="episodic", show_default=True, callback=_validate_memory_type)
202
- @click.option("--all", "as_whole", is_flag=True, help="Add stdin as one memory instead of line-by-line.")
240
+ @click.option(
241
+ "--type",
242
+ "memory_type",
243
+ default="episodic",
244
+ show_default=True,
245
+ callback=_validate_memory_type,
246
+ )
247
+ @click.option(
248
+ "--all",
249
+ "as_whole",
250
+ is_flag=True,
251
+ help="Add stdin as one memory instead of line-by-line.",
252
+ )
203
253
  @click.pass_context
204
254
  def add_cmd(ctx, text: str, memory_type: str, as_whole: bool) -> None:
205
255
  """Add text as a memory. Use - or pipe stdin to read from a file.
@@ -219,11 +269,17 @@ def add_cmd(ctx, text: str, memory_type: str, as_whole: bool) -> None:
219
269
 
220
270
  if text == "-":
221
271
  if sys.stdin.isatty():
222
- raise click.ClickException("No input. Pipe text or use: smartmemory add \"text\"")
272
+ raise click.ClickException(
273
+ 'No input. Pipe text or use: smartmemory add "text"'
274
+ )
223
275
  raw = sys.stdin.read()
224
276
  if not raw.strip():
225
277
  raise click.ClickException("Content cannot be empty.")
226
- chunks = [raw.strip()] if as_whole else [l.strip() for l in raw.splitlines() if l.strip()]
278
+ chunks = (
279
+ [raw.strip()]
280
+ if as_whole
281
+ else [l.strip() for l in raw.splitlines() if l.strip()]
282
+ )
227
283
  if not chunks:
228
284
  raise click.ClickException("Content cannot be empty.")
229
285
  props = _parse_extra_props(ctx.args)
@@ -261,14 +317,27 @@ def add_cmd(ctx, text: str, memory_type: str, as_whole: bool) -> None:
261
317
  @cli.command("recall")
262
318
  @click.option("--cwd", default=None, help="Current working directory for context.")
263
319
  @click.option("--top-k", default=10, show_default=True)
264
- @click.option("--query", default=None, help="Optional prompt query (UserPromptSubmit hook).")
265
- @click.option("--workspace", "workspace_id", default=None, help="Workspace ID override.")
266
- @click.option("--strict/--no-strict", default=False,
267
- help="Drop legacy items without workspace_id (kills cross-workspace leak).")
268
- @click.option("--no-snapshot", "no_snapshot", is_flag=True, default=False,
269
- help="Skip snapshot frame.")
270
- def recall_cmd(cwd: str, top_k: int, query: str, workspace_id: str,
271
- strict: bool, no_snapshot: bool) -> None:
320
+ @click.option(
321
+ "--query", default=None, help="Optional prompt query (UserPromptSubmit hook)."
322
+ )
323
+ @click.option(
324
+ "--workspace", "workspace_id", default=None, help="Workspace ID override."
325
+ )
326
+ @click.option(
327
+ "--strict/--no-strict",
328
+ default=False,
329
+ help="Drop legacy items without workspace_id (kills cross-workspace leak).",
330
+ )
331
+ @click.option(
332
+ "--no-snapshot",
333
+ "no_snapshot",
334
+ is_flag=True,
335
+ default=False,
336
+ help="Skip snapshot frame.",
337
+ )
338
+ def recall_cmd(
339
+ cwd: str, top_k: int, query: str, workspace_id: str, strict: bool, no_snapshot: bool
340
+ ) -> None:
272
341
  """Recall memories (SessionStart / UserPromptSubmit hook)."""
273
342
  params = {
274
343
  "cwd": cwd or "",
@@ -285,24 +354,42 @@ def recall_cmd(cwd: str, top_k: int, query: str, workspace_id: str,
285
354
  from smartmemory_app.storage import recall
286
355
 
287
356
  context = recall(
288
- cwd, top_k,
289
- query=query, workspace_id=workspace_id,
290
- include_snapshot=not no_snapshot, strict=strict,
357
+ cwd,
358
+ top_k,
359
+ query=query,
360
+ workspace_id=workspace_id,
361
+ include_snapshot=not no_snapshot,
362
+ strict=strict,
291
363
  )
292
364
  if context:
293
365
  click.echo(context)
294
366
 
295
367
 
296
368
  @cli.command("retag")
297
- @click.option("--content", "content_substring", required=True,
298
- help="Substring to match in item content (case-insensitive).")
299
- @click.option("--origin", "new_origin", required=True,
300
- help='New origin value, e.g. "seed:demo" (tier 4, hidden from recall).')
301
- @click.option("--dry-run", is_flag=True, default=False,
302
- help="Show items that would be retagged without modifying.")
303
- @click.option("--limit", default=100, show_default=True,
304
- help="Max items to retag in one run.")
305
- def retag_cmd(content_substring: str, new_origin: str, dry_run: bool, limit: int) -> None:
369
+ @click.option(
370
+ "--content",
371
+ "content_substring",
372
+ required=True,
373
+ help="Substring to match in item content (case-insensitive).",
374
+ )
375
+ @click.option(
376
+ "--origin",
377
+ "new_origin",
378
+ required=True,
379
+ help='New origin value, e.g. "seed:demo" (tier 4, hidden from recall).',
380
+ )
381
+ @click.option(
382
+ "--dry-run",
383
+ is_flag=True,
384
+ default=False,
385
+ help="Show items that would be retagged without modifying.",
386
+ )
387
+ @click.option(
388
+ "--limit", default=100, show_default=True, help="Max items to retag in one run."
389
+ )
390
+ def retag_cmd(
391
+ content_substring: str, new_origin: str, dry_run: bool, limit: int
392
+ ) -> None:
306
393
  """Retag items by content match (HOOK-RECALL-RELEVANCE-1 G3.C).
307
394
 
308
395
  Use to clean up legacy seed/fixture data that leaks into recall:
@@ -327,7 +414,11 @@ def retag_cmd(content_substring: str, new_origin: str, dry_run: bool, limit: int
327
414
  # Content lives in `properties.content` for SQLite/FalkorDB serialize shape;
328
415
  # fall back to top-level `content` for forward-compat.
329
416
  props = raw.get("properties") or {}
330
- content = (props.get("content") if isinstance(props, dict) else None) or raw.get("content") or ""
417
+ content = (
418
+ (props.get("content") if isinstance(props, dict) else None)
419
+ or raw.get("content")
420
+ or ""
421
+ )
331
422
  if needle not in content.lower():
332
423
  continue
333
424
  item_id = raw.get("item_id") or raw.get("id")
@@ -358,12 +449,21 @@ def retag_cmd(content_substring: str, new_origin: str, dry_run: bool, limit: int
358
449
  click.echo(f"Retagged {updated}/{len(matched)} items with origin={new_origin!r}.")
359
450
 
360
451
 
361
- @cli.command("search", context_settings=dict(
362
- ignore_unknown_options=True, allow_extra_args=True,
363
- ))
452
+ @cli.command(
453
+ "search",
454
+ context_settings=dict(
455
+ ignore_unknown_options=True,
456
+ allow_extra_args=True,
457
+ ),
458
+ )
364
459
  @click.argument("query")
365
460
  @click.option("--top-k", default=5, show_default=True)
366
- @click.option("--include-reference", is_flag=True, default=False, help="Include reference data in results")
461
+ @click.option(
462
+ "--include-reference",
463
+ is_flag=True,
464
+ default=False,
465
+ help="Include reference data in results",
466
+ )
367
467
  @click.pass_context
368
468
  def search_cmd(ctx, query: str, top_k: int, include_reference: bool) -> None:
369
469
  """Search memories by semantic similarity. Use '*' to list all.
@@ -381,13 +481,23 @@ def search_cmd(ctx, query: str, top_k: int, include_reference: bool) -> None:
381
481
  from smartmemory_app.storage import search
382
482
 
383
483
  try:
384
- results = search(query, top_k, filters=props, include_reference=include_reference)
484
+ results = search(
485
+ query, top_k, filters=props, include_reference=include_reference
486
+ )
385
487
  except NotImplementedError as e:
386
488
  raise click.ClickException(str(e))
489
+ # The daemon returns the CORE-CRUD-LIST contract shape {"items": [...]};
490
+ # the storage fallback returns a bare list. Unwrap so we always iterate
491
+ # result dicts (iterating the dict directly yielded its keys -> "items"
492
+ # string -> AttributeError: 'str' object has no attribute 'get').
493
+ if isinstance(results, dict):
494
+ results = results.get("items", [])
387
495
  if not results:
388
496
  click.echo("No results.")
389
497
  return
390
498
  for r in results:
499
+ if not isinstance(r, dict):
500
+ continue
391
501
  content = r.get("content", "")[:200]
392
502
  mem_type = r.get("memory_type", "?")
393
503
  item_id = r.get("item_id", "?")
@@ -434,6 +544,7 @@ def lifecycle_group() -> None:
434
544
  def lifecycle_orient() -> None:
435
545
  """Orient phase: recall context at session start."""
436
546
  import sys
547
+
437
548
  body = json.loads(sys.stdin.read()) if not sys.stdin.isatty() else {}
438
549
  session_id = body.get("session_id", "unknown")
439
550
  cwd = body.get("cwd")
@@ -441,7 +552,9 @@ def lifecycle_orient() -> None:
441
552
  from smartmemory_app.lifecycle import MemoryLifecycle
442
553
  from smartmemory_app.lifecycle_config import LifecycleConfig
443
554
 
444
- lc = MemoryLifecycle(session_id, LifecycleConfig.from_config(_load_lifecycle_toml()))
555
+ lc = MemoryLifecycle(
556
+ session_id, LifecycleConfig.from_config(_load_lifecycle_toml())
557
+ )
445
558
  result = lc.orient(cwd=cwd)
446
559
  if result:
447
560
  click.echo(result)
@@ -451,6 +564,7 @@ def lifecycle_orient() -> None:
451
564
  def lifecycle_recall() -> None:
452
565
  """Recall phase: inject prompt-relevant context."""
453
566
  import sys
567
+
454
568
  body = json.loads(sys.stdin.read()) if not sys.stdin.isatty() else {}
455
569
  session_id = body.get("session_id", "unknown")
456
570
  prompt = body.get("prompt", "")
@@ -458,7 +572,9 @@ def lifecycle_recall() -> None:
458
572
  from smartmemory_app.lifecycle import MemoryLifecycle
459
573
  from smartmemory_app.lifecycle_config import LifecycleConfig
460
574
 
461
- lc = MemoryLifecycle(session_id, LifecycleConfig.from_config(_load_lifecycle_toml()))
575
+ lc = MemoryLifecycle(
576
+ session_id, LifecycleConfig.from_config(_load_lifecycle_toml())
577
+ )
462
578
  result = lc.recall(prompt)
463
579
  if result:
464
580
  click.echo(result)
@@ -468,13 +584,16 @@ def lifecycle_recall() -> None:
468
584
  def lifecycle_observe() -> None:
469
585
  """Observe phase: capture tool call."""
470
586
  import sys
587
+
471
588
  body = json.loads(sys.stdin.read()) if not sys.stdin.isatty() else {}
472
589
  session_id = body.get("session_id", "unknown")
473
590
 
474
591
  from smartmemory_app.lifecycle import MemoryLifecycle
475
592
  from smartmemory_app.lifecycle_config import LifecycleConfig
476
593
 
477
- lc = MemoryLifecycle(session_id, LifecycleConfig.from_config(_load_lifecycle_toml()))
594
+ lc = MemoryLifecycle(
595
+ session_id, LifecycleConfig.from_config(_load_lifecycle_toml())
596
+ )
478
597
  lc.observe(
479
598
  tool_name=body.get("tool_name", "unknown"),
480
599
  tool_input=body.get("tool_input", {}),
@@ -486,13 +605,16 @@ def lifecycle_observe() -> None:
486
605
  def lifecycle_distill() -> None:
487
606
  """Distill phase: pair response with stored prompt."""
488
607
  import sys
608
+
489
609
  body = json.loads(sys.stdin.read()) if not sys.stdin.isatty() else {}
490
610
  session_id = body.get("session_id", "unknown")
491
611
 
492
612
  from smartmemory_app.lifecycle import MemoryLifecycle
493
613
  from smartmemory_app.lifecycle_config import LifecycleConfig
494
614
 
495
- lc = MemoryLifecycle(session_id, LifecycleConfig.from_config(_load_lifecycle_toml()))
615
+ lc = MemoryLifecycle(
616
+ session_id, LifecycleConfig.from_config(_load_lifecycle_toml())
617
+ )
496
618
  lc.distill(response=body.get("last_assistant_message", ""))
497
619
 
498
620
 
@@ -500,13 +622,16 @@ def lifecycle_distill() -> None:
500
622
  def lifecycle_learn() -> None:
501
623
  """Learn phase: capture error pattern."""
502
624
  import sys
625
+
503
626
  body = json.loads(sys.stdin.read()) if not sys.stdin.isatty() else {}
504
627
  session_id = body.get("session_id", "unknown")
505
628
 
506
629
  from smartmemory_app.lifecycle import MemoryLifecycle
507
630
  from smartmemory_app.lifecycle_config import LifecycleConfig
508
631
 
509
- lc = MemoryLifecycle(session_id, LifecycleConfig.from_config(_load_lifecycle_toml()))
632
+ lc = MemoryLifecycle(
633
+ session_id, LifecycleConfig.from_config(_load_lifecycle_toml())
634
+ )
510
635
  lc.learn(
511
636
  tool_name=body.get("tool_name", "unknown"),
512
637
  error=body.get("error", body.get("tool_response", "")),
@@ -517,13 +642,16 @@ def lifecycle_learn() -> None:
517
642
  def lifecycle_persist() -> None:
518
643
  """Persist phase: save session summary."""
519
644
  import sys
645
+
520
646
  body = json.loads(sys.stdin.read()) if not sys.stdin.isatty() else {}
521
647
  session_id = body.get("session_id", "unknown")
522
648
 
523
649
  from smartmemory_app.lifecycle import MemoryLifecycle
524
650
  from smartmemory_app.lifecycle_config import LifecycleConfig
525
651
 
526
- lc = MemoryLifecycle(session_id, LifecycleConfig.from_config(_load_lifecycle_toml()))
652
+ lc = MemoryLifecycle(
653
+ session_id, LifecycleConfig.from_config(_load_lifecycle_toml())
654
+ )
527
655
  lc.persist()
528
656
 
529
657
 
@@ -531,12 +659,15 @@ def lifecycle_persist() -> None:
531
659
  def lifecycle_status() -> None:
532
660
  """Show lifecycle configuration and session stats."""
533
661
  from smartmemory_app.lifecycle_config import LifecycleConfig
662
+
534
663
  cfg = LifecycleConfig.from_config(_load_lifecycle_toml())
535
664
  click.echo(f"Lifecycle enabled: {cfg.enabled}")
536
665
  click.echo(f"Recall strategy: {cfg.recall_strategy.value}")
537
666
  click.echo(f"Orient budget: {cfg.orient_budget} tokens")
538
667
  click.echo(f"Recall budget: {cfg.recall_budget} tokens")
539
- click.echo(f"Observe: {cfg.observe_tool_calls}, Distill: {cfg.distill_turns}, Learn: {cfg.learn_from_errors}")
668
+ click.echo(
669
+ f"Observe: {cfg.observe_tool_calls}, Distill: {cfg.distill_turns}, Learn: {cfg.learn_from_errors}"
670
+ )
540
671
 
541
672
 
542
673
  def _load_lifecycle_toml() -> dict:
@@ -544,6 +675,7 @@ def _load_lifecycle_toml() -> dict:
544
675
  try:
545
676
  import tomllib
546
677
  from smartmemory_app.config import config_path
678
+
547
679
  path = config_path()
548
680
  if path.exists():
549
681
  with open(path, "rb") as f:
@@ -684,8 +816,10 @@ def config_cmd(key: str | None, value: str | None) -> None:
684
816
 
685
817
  if key is None:
686
818
  from smartmemory_app import __version__ as wrapper_version
819
+
687
820
  try:
688
821
  from importlib.metadata import version as _pkg_version
822
+
689
823
  core_version = _pkg_version("smartmemory-core")
690
824
  except Exception:
691
825
  core_version = "?"
@@ -1183,7 +1317,9 @@ def reextract_cmd() -> None:
1183
1317
  @cli.command("server", hidden=True)
1184
1318
  def server_cmd() -> None:
1185
1319
  """Start the SmartMemory MCP server (called by MCP clients, not users)."""
1186
- click.echo("MCP server is now 'smartmemory-mcp'. It's included in your installation.")
1320
+ click.echo(
1321
+ "MCP server is now 'smartmemory-mcp'. It's included in your installation."
1322
+ )
1187
1323
  click.echo("Run: smartmemory-mcp")
1188
1324
 
1189
1325