memleaf 0.2.35__tar.gz → 0.2.37__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.
- {memleaf-0.2.35 → memleaf-0.2.37}/CHANGELOG.md +13 -0
- {memleaf-0.2.35/src/memleaf.egg-info → memleaf-0.2.37}/PKG-INFO +2 -2
- {memleaf-0.2.35 → memleaf-0.2.37}/README.en.md +1 -1
- {memleaf-0.2.35 → memleaf-0.2.37}/README.md +1 -1
- {memleaf-0.2.35 → memleaf-0.2.37}/pyproject.toml +4 -1
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/hermes_provider/__init__.py +7 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/hermes_runtime.py +35 -1
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/installer.py +64 -5
- {memleaf-0.2.35 → memleaf-0.2.37/src/memleaf.egg-info}/PKG-INFO +2 -2
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf.egg-info/SOURCES.txt +3 -1
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf.egg-info/entry_points.txt +3 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_hermes_runtime_install.py +35 -3
- memleaf-0.2.37/tests/test_hermes_windows_subprocess.py +43 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_pypi_install.py +5 -1
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_c3_packaging.py +4 -0
- memleaf-0.2.37/tests/test_windows_public_mcp_launcher.py +75 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/LICENSE +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/MANIFEST.in +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/docs/capture-budget-design.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/docs/config-migrations.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/docs/core-refactor.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/docs/evidence-retention.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/docs/gate-evidence-boundary.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/docs/general-processing.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/docs/hermes-mcp-runtime.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/docs/performance.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/docs/processing-quality-acceptance.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/docs/v0.2.26-processing-status.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/examples/README.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/examples/basic_usage.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/examples/live_core_lifecycle_acceptance.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/examples/live_processing_acceptance.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/install.ps1 +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/install.sh +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/setup.cfg +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/admission.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/config.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/create_coordinator.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/evidence_budget.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/evidence_policy.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/hermes_provider/evidence_budget.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/index.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/inspection.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/memory_commit.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/memory_planner.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/model_execution.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/models.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/parallel_model.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/planning_context.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/process_common.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/process_jobs.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/process_journal.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/process_owner.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/processing.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/provenance.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/recording_policy.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/retention.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/service.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/source_policy.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/state_layout.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/target_reconciliation.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/turn_audit.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/turn_plan.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/update_coordinator.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/update_review.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/__init__.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/semantic_fixtures.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_admission_noise.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_automatic_duplicate_noop_collision.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_candidate_polarity.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_config_migrations_v028.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_conversation_only.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_cross_turn_dedupe_regressions.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_due_date_grounding.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_due_date_grounding_retry.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_email_actionable_coverage.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_evidence_budget.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_evidence_retention_policy.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_external_source_dates.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_extraction_quality_regressions.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_gate_capacity.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_gate_schema_repair.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_general_evidence_admission.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_general_tool_provenance.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_global_todo_acceptance.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_global_todo_query_no_write.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_global_todo_retrieval.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_hermes_native_registration.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_hermes_provider.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_hermes_transport_evidence.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_host_events.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_inspection_state_v028.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_install.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_long_run_hygiene.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_maintenance_v2.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_model_owned_fields.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_new_scope_source_grounding.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_partial_retry_idempotency.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_phase2_model_decisions.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_process_jobs.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_process_owner_locking.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_processing_contract_v026.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_processing_observability_concurrency.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_read_only_deferred_isolation.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_review_source_context.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_revision_digest.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_shared_memory_refactor.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_source_neutral_todos_v028.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_b1.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_b2a.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_c1_mcp.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_state_layout_v028.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_summary_date_grounding_integration.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_target_reconciliation.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_target_reconciliation_integration.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_todo_state_recovery.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_update_review.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_update_target_recovery.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_v023_scope_correction.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_v2_search_gate_acceptance.py +0 -0
- {memleaf-0.2.35 → memleaf-0.2.37}/tests/test_whole_unit_bindings.py +0 -0
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.37 — 2026-09-09
|
|
6
|
+
|
|
7
|
+
- Add a dedicated `memleaf-mcpw` GUI entry point for the Hermes public MCP on Windows. The GUI-subsystem launcher does not allocate a console window even when an older Hermes/MCP SDK starts it without `CREATE_NO_WINDOW`; it enters the same `memleaf.mcp_server:main` implementation and keeps the same stdio JSON-RPC protocol.
|
|
8
|
+
- Keep the Hermes MemoryProvider private MCP on `memleaf-mcp.exe` with the v0.2.36 `CREATE_NO_WINDOW` protection, so both Windows launch paths are covered without changing macOS/Linux behavior or the Markdown Vault architecture.
|
|
9
|
+
- Treat `memleaf-mcp.exe` and its sibling `memleaf-mcpw.exe` as the same installed memleaf runtime for preflight/runtime policy while still requiring the GUI launcher for the public Hermes MCP. Existing v0.2.36 direct entries in the same runtime migrate automatically; different-runtime and different-Vault protections remain fail closed.
|
|
10
|
+
- Add Windows acceptance that inspects the installed launcher PE Subsystem, starts `memleaf-mcpw.exe` with `creationflags=0`, performs real MCP initialize and tools/list over redirected stdio, confirms all 13 tools, and verifies clean EOF shutdown.
|
|
11
|
+
|
|
12
|
+
## 0.2.36 — 2026-09-08
|
|
13
|
+
|
|
14
|
+
- Prevent the Hermes MemoryProvider from opening a visible console/Windows Terminal window whenever it starts its private `memleaf-mcp` stdio child on Windows. Provider-owned MCP launches now use `subprocess.CREATE_NO_WINDOW`; stdin/stdout pipes, stderr suppression, timeout handling, process reuse and shutdown semantics are unchanged.
|
|
15
|
+
- Add deterministic regression coverage for the Windows creation flag, the non-Windows zero-flag path, and the exact `Popen` stdio contract.
|
|
16
|
+
- Correct the README release banner that was still displaying 0.2.34 after the 0.2.35 publication.
|
|
17
|
+
|
|
5
18
|
## 0.2.35 — 2026-09-08
|
|
6
19
|
|
|
7
20
|
- Add safe structural model-call telemetry across successful, deferred, and failed processing: call/retry/failure counts, model-request and stage wall-clock timing, input/output lengths, and maximum in-flight concurrency are retained without prompts, responses, memory bodies, credentials, provider secrets, URLs, or raw exception text.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.37
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
25
25
|
|
|
26
|
-
> **版本:0.2.
|
|
26
|
+
> **版本:0.2.37。**
|
|
27
27
|
> 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[中文](README.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
6
6
|
|
|
7
|
-
> **Version: 0.2.
|
|
7
|
+
> **Version: 0.2.37.**
|
|
8
8
|
> Automatic extraction now uses only the current turn's visible user input and final assistant reply. Raw tool output, attachments, web/file/terminal payloads and legacy tool-evidence bodies are not new source evidence; existing or retrieved memory remains comparison context rather than source authority. Background processing is persisted as a local job and can be checked through the read-only `process_status` MCP tool; failed work remains retryable and fail closed. The release also tightens source/date grounding, target reconciliation, duplicate/no-op handling and semantic review before writes. Markdown remains the sole source of truth with no SQLite runtime dependency. Acceptance covers deterministic regression suites and synthetic inputs; it does not claim real-mail or customer-business acceptance.
|
|
9
9
|
> **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
6
6
|
|
|
7
|
-
> **版本:0.2.
|
|
7
|
+
> **版本:0.2.37。**
|
|
8
8
|
> 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
|
|
9
9
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "memleaf"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.37"
|
|
8
8
|
description = "A local-first Markdown memory core for AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -27,6 +27,9 @@ dependencies = []
|
|
|
27
27
|
memleaf-mcp = "memleaf.mcp_server:main"
|
|
28
28
|
memleaf = "memleaf.cli:main"
|
|
29
29
|
|
|
30
|
+
[project.gui-scripts]
|
|
31
|
+
memleaf-mcpw = "memleaf.mcp_server:main"
|
|
32
|
+
|
|
30
33
|
[tool.setuptools.packages.find]
|
|
31
34
|
where = ["src"]
|
|
32
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.37"
|
|
4
4
|
|
|
5
5
|
from .config import DEFAULT_CONFIG, default_config, load_config, save_config
|
|
6
6
|
from .frontmatter import FrontmatterError, dump_frontmatter, dump_yaml, load_yaml, parse_frontmatter
|
|
@@ -1025,6 +1025,12 @@ def _write_json(path: Path, value: Mapping[str, Any]) -> None:
|
|
|
1025
1025
|
os.replace(temporary, path)
|
|
1026
1026
|
|
|
1027
1027
|
|
|
1028
|
+
def _mcp_creationflags() -> int:
|
|
1029
|
+
"""Hide the provider-owned MCP child console on Windows only."""
|
|
1030
|
+
|
|
1031
|
+
return subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
|
1032
|
+
|
|
1033
|
+
|
|
1028
1034
|
class _MCPClient:
|
|
1029
1035
|
"""Small synchronous JSON-RPC client for memleaf's stdio MCP server."""
|
|
1030
1036
|
|
|
@@ -1105,6 +1111,7 @@ class _MCPClient:
|
|
|
1105
1111
|
encoding="utf-8",
|
|
1106
1112
|
errors="strict",
|
|
1107
1113
|
bufsize=1,
|
|
1114
|
+
creationflags=_mcp_creationflags(),
|
|
1108
1115
|
)
|
|
1109
1116
|
self._start_stdout_reader_locked(self._process)
|
|
1110
1117
|
initialize_result = self._request_locked(
|
|
@@ -18,7 +18,7 @@ from typing import Any, Mapping, Sequence
|
|
|
18
18
|
from .adapters.hermes import _parse_json_or_yaml_mcp
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
_MEMLEAF_COMMAND_NAMES = frozenset({"memleaf-mcp", "memleaf-mcp.exe"})
|
|
21
|
+
_MEMLEAF_COMMAND_NAMES = frozenset({"memleaf-mcp", "memleaf-mcp.exe", "memleaf-mcpw", "memleaf-mcpw.exe"})
|
|
22
22
|
_TRUE_VALUES = frozenset({"true", "1", "yes", "on"})
|
|
23
23
|
_FALSE_VALUES = frozenset({"false", "0", "no", "off"})
|
|
24
24
|
|
|
@@ -111,6 +111,27 @@ def is_absolute_memleaf_command(
|
|
|
111
111
|
return Path(text).is_absolute()
|
|
112
112
|
|
|
113
113
|
|
|
114
|
+
def memleaf_commands_same_runtime(
|
|
115
|
+
left: Path | str,
|
|
116
|
+
right: Path | str,
|
|
117
|
+
*,
|
|
118
|
+
platform: str | None = None,
|
|
119
|
+
) -> bool:
|
|
120
|
+
"""Return whether two absolute memleaf launchers belong to one runtime."""
|
|
121
|
+
|
|
122
|
+
if host_paths_equivalent(left, right, platform=platform):
|
|
123
|
+
return True
|
|
124
|
+
if _platform_name(platform) != "nt":
|
|
125
|
+
return False
|
|
126
|
+
left_text = _path_text(left).replace("/", "\\")
|
|
127
|
+
right_text = _path_text(right).replace("/", "\\")
|
|
128
|
+
if not is_absolute_memleaf_command(left_text, platform="nt"):
|
|
129
|
+
return False
|
|
130
|
+
if not is_absolute_memleaf_command(right_text, platform="nt"):
|
|
131
|
+
return False
|
|
132
|
+
return ntpath.normcase(ntpath.dirname(left_text)) == ntpath.normcase(ntpath.dirname(right_text))
|
|
133
|
+
|
|
134
|
+
|
|
114
135
|
def _is_bare_memleaf_command(command: str, *, platform: str | None = None) -> bool:
|
|
115
136
|
text = command.strip()
|
|
116
137
|
return (
|
|
@@ -172,6 +193,7 @@ def inspect_hermes_mcp(
|
|
|
172
193
|
expected_command: Path | str,
|
|
173
194
|
*,
|
|
174
195
|
platform: str | None = None,
|
|
196
|
+
allow_same_runtime: bool = False,
|
|
175
197
|
) -> HermesMcpInspection:
|
|
176
198
|
"""Inspect ``mcp_servers.memleaf`` without executing configured commands.
|
|
177
199
|
|
|
@@ -313,6 +335,17 @@ def inspect_hermes_mcp(
|
|
|
313
335
|
configured_command=command,
|
|
314
336
|
configured_vault=configured_vault,
|
|
315
337
|
)
|
|
338
|
+
if allow_same_runtime and memleaf_commands_same_runtime(
|
|
339
|
+
command, expected_command, platform=platform
|
|
340
|
+
):
|
|
341
|
+
return _result(
|
|
342
|
+
"correct",
|
|
343
|
+
"the existing memleaf MCP entry uses a sibling launcher from this runtime and Vault",
|
|
344
|
+
config_path=path,
|
|
345
|
+
expected_command=expected_command,
|
|
346
|
+
configured_command=command,
|
|
347
|
+
configured_vault=configured_vault,
|
|
348
|
+
)
|
|
316
349
|
if _is_bare_memleaf_command(command, platform=platform):
|
|
317
350
|
return _result(
|
|
318
351
|
"legacy",
|
|
@@ -346,4 +379,5 @@ __all__ = [
|
|
|
346
379
|
"host_paths_equivalent",
|
|
347
380
|
"inspect_hermes_mcp",
|
|
348
381
|
"is_absolute_memleaf_command",
|
|
382
|
+
"memleaf_commands_same_runtime",
|
|
349
383
|
]
|
|
@@ -38,6 +38,7 @@ from .hermes_runtime import (
|
|
|
38
38
|
HermesMcpInspection,
|
|
39
39
|
inspect_hermes_mcp,
|
|
40
40
|
is_absolute_memleaf_command,
|
|
41
|
+
memleaf_commands_same_runtime,
|
|
41
42
|
)
|
|
42
43
|
from .locking import atomic_write_json
|
|
43
44
|
from .native_registration import ensure_hermes_native_sources
|
|
@@ -226,6 +227,33 @@ def _memleaf_mcp_command() -> Path:
|
|
|
226
227
|
raise RuntimeError("memleaf-mcp console entry point was not found after package installation")
|
|
227
228
|
|
|
228
229
|
|
|
230
|
+
def _provider_mcp_command(runtime_command: Path | str) -> Path:
|
|
231
|
+
"""Return the console entry point used by the copied MemoryProvider."""
|
|
232
|
+
|
|
233
|
+
runtime = Path(runtime_command).expanduser().resolve()
|
|
234
|
+
if os.name != "nt":
|
|
235
|
+
return runtime
|
|
236
|
+
if runtime.name.casefold() == "memleaf-mcp.exe":
|
|
237
|
+
return runtime
|
|
238
|
+
if runtime.name.casefold() == "memleaf-mcpw.exe":
|
|
239
|
+
console = runtime.with_name("memleaf-mcp.exe")
|
|
240
|
+
if console.is_file():
|
|
241
|
+
return console.resolve()
|
|
242
|
+
raise RuntimeError("the selected memleaf runtime has no memleaf-mcp.exe console entry point")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _hermes_public_mcp_command(runtime_command: Path | str) -> Path:
|
|
246
|
+
"""Return the no-console public MCP launcher for the selected runtime."""
|
|
247
|
+
|
|
248
|
+
provider_command = _provider_mcp_command(runtime_command)
|
|
249
|
+
if os.name != "nt":
|
|
250
|
+
return provider_command
|
|
251
|
+
gui = provider_command.with_name("memleaf-mcpw.exe")
|
|
252
|
+
if not gui.is_file():
|
|
253
|
+
raise RuntimeError("the selected memleaf runtime has no memleaf-mcpw.exe GUI entry point")
|
|
254
|
+
return gui.resolve()
|
|
255
|
+
|
|
256
|
+
|
|
229
257
|
def _copy_provider(hermes_home: Path) -> Path:
|
|
230
258
|
plugins = hermes_home / "plugins"
|
|
231
259
|
target = plugins / "memleaf"
|
|
@@ -481,6 +509,15 @@ def _configure_hermes_mcp_entry(
|
|
|
481
509
|
command=[detection.executable, "mcp", "list"],
|
|
482
510
|
)
|
|
483
511
|
allowed = inspection.status in {"absent", "legacy"}
|
|
512
|
+
same_runtime_launcher_migration = bool(
|
|
513
|
+
inspection.status == "runtime_conflict"
|
|
514
|
+
and inspection.configured_command
|
|
515
|
+
and memleaf_commands_same_runtime(
|
|
516
|
+
inspection.configured_command, command, platform=platform
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
if same_runtime_launcher_migration:
|
|
520
|
+
allowed = True
|
|
484
521
|
if inspection.status == "runtime_conflict" and allow_runtime_migration:
|
|
485
522
|
allowed = True
|
|
486
523
|
if not allowed:
|
|
@@ -779,6 +816,7 @@ def install_hermes(
|
|
|
779
816
|
selected_vault,
|
|
780
817
|
current_command,
|
|
781
818
|
platform=platform,
|
|
819
|
+
allow_same_runtime=True,
|
|
782
820
|
)
|
|
783
821
|
selected_command, runtime_details = _choose_hermes_mcp_command(
|
|
784
822
|
preflight,
|
|
@@ -806,6 +844,26 @@ def install_hermes(
|
|
|
806
844
|
user_action=action,
|
|
807
845
|
)
|
|
808
846
|
|
|
847
|
+
try:
|
|
848
|
+
provider_command = _provider_mcp_command(selected_command)
|
|
849
|
+
public_command = _hermes_public_mcp_command(selected_command)
|
|
850
|
+
except RuntimeError as error:
|
|
851
|
+
return _failure_result(
|
|
852
|
+
stage="runtime_launcher",
|
|
853
|
+
reason=str(error),
|
|
854
|
+
core_version=core_version,
|
|
855
|
+
vault=selected_vault,
|
|
856
|
+
vault_source=vault_source,
|
|
857
|
+
mcp_runtime=runtime_details,
|
|
858
|
+
)
|
|
859
|
+
runtime_details = dict(runtime_details)
|
|
860
|
+
runtime_details.update(
|
|
861
|
+
{
|
|
862
|
+
"provider_command": str(provider_command),
|
|
863
|
+
"public_command": str(public_command),
|
|
864
|
+
}
|
|
865
|
+
)
|
|
866
|
+
|
|
809
867
|
vault = Vault.initialize(selected_vault)
|
|
810
868
|
model = _prepare_model_route(
|
|
811
869
|
vault.root,
|
|
@@ -872,7 +930,7 @@ def install_hermes(
|
|
|
872
930
|
adapter,
|
|
873
931
|
detection,
|
|
874
932
|
vault.root,
|
|
875
|
-
|
|
933
|
+
str(public_command),
|
|
876
934
|
allow_runtime_migration=(
|
|
877
935
|
mcp_runtime == "current" and preflight.status == "runtime_conflict"
|
|
878
936
|
),
|
|
@@ -888,7 +946,7 @@ def install_hermes(
|
|
|
888
946
|
),
|
|
889
947
|
recovery_commands=_mcp_recovery_commands(
|
|
890
948
|
detection.executable,
|
|
891
|
-
|
|
949
|
+
str(public_command),
|
|
892
950
|
vault.root,
|
|
893
951
|
),
|
|
894
952
|
)
|
|
@@ -899,7 +957,7 @@ def install_hermes(
|
|
|
899
957
|
mcp=configured.to_dict(),
|
|
900
958
|
recovery_commands=_mcp_recovery_commands(
|
|
901
959
|
detection.executable,
|
|
902
|
-
|
|
960
|
+
str(public_command),
|
|
903
961
|
vault.root,
|
|
904
962
|
),
|
|
905
963
|
)
|
|
@@ -933,7 +991,7 @@ def install_hermes(
|
|
|
933
991
|
),
|
|
934
992
|
)
|
|
935
993
|
try:
|
|
936
|
-
_write_provider_config(provider_config,
|
|
994
|
+
_write_provider_config(provider_config, provider_command, vault.root)
|
|
937
995
|
except Exception as error:
|
|
938
996
|
raise _HermesInstallFailure(
|
|
939
997
|
"provider_config",
|
|
@@ -1015,7 +1073,8 @@ def install_hermes(
|
|
|
1015
1073
|
"vault": str(vault.root),
|
|
1016
1074
|
"vault_source": vault_source,
|
|
1017
1075
|
"provider": str(provider_path),
|
|
1018
|
-
"mcp_command":
|
|
1076
|
+
"mcp_command": str(public_command),
|
|
1077
|
+
"provider_mcp_command": str(provider_command),
|
|
1019
1078
|
"mcp_runtime": runtime_details,
|
|
1020
1079
|
"model": model,
|
|
1021
1080
|
"native_sources": native_registration,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.37
|
|
4
4
|
Summary: A local-first Markdown memory core for AI agents
|
|
5
5
|
Author: memleaf contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
25
25
|
|
|
26
|
-
> **版本:0.2.
|
|
26
|
+
> **版本:0.2.37。**
|
|
27
27
|
> 记忆只从用户与 Agent 的可见对话提炼。Agent 已在回复中整理的事实、项目进展和明确待办可作为来源;邮件、附件、网页和终端等工具原文不进入记忆提炼。模型负责保留原话中的不确定性,现有记忆仅用于比较、去重和更新。Markdown 仍是唯一事实源。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
@@ -131,6 +131,7 @@ tests/test_hermes_provider.py
|
|
|
131
131
|
tests/test_hermes_runtime_install.py
|
|
132
132
|
tests/test_hermes_stdio_transport.py
|
|
133
133
|
tests/test_hermes_transport_evidence.py
|
|
134
|
+
tests/test_hermes_windows_subprocess.py
|
|
134
135
|
tests/test_host_events.py
|
|
135
136
|
tests/test_host_runtime_contract.py
|
|
136
137
|
tests/test_inspection_state_v028.py
|
|
@@ -183,4 +184,5 @@ tests/test_v2_host_flow.py
|
|
|
183
184
|
tests/test_v2_mcp_flow.py
|
|
184
185
|
tests/test_v2_nomatch_semantics.py
|
|
185
186
|
tests/test_v2_search_gate_acceptance.py
|
|
186
|
-
tests/test_whole_unit_bindings.py
|
|
187
|
+
tests/test_whole_unit_bindings.py
|
|
188
|
+
tests/test_windows_public_mcp_launcher.py
|
|
@@ -96,7 +96,7 @@ class HermesRuntimeInspectionTests(unittest.TestCase):
|
|
|
96
96
|
root = Path(temporary)
|
|
97
97
|
config = root / "config.yaml"
|
|
98
98
|
vault = root / "vault"
|
|
99
|
-
current = root / "managed" / "bin" / "memleaf-mcp"
|
|
99
|
+
current = root / "managed" / "bin" / ("memleaf-mcp.exe" if os.name == "nt" else "memleaf-mcp")
|
|
100
100
|
existing = root / "source" / ".venv" / "bin" / "memleaf-mcp"
|
|
101
101
|
vault.mkdir()
|
|
102
102
|
current.parent.mkdir(parents=True)
|
|
@@ -148,6 +148,36 @@ class HermesRuntimeInspectionTests(unittest.TestCase):
|
|
|
148
148
|
|
|
149
149
|
self.assertEqual("correct", inspection.status)
|
|
150
150
|
|
|
151
|
+
def test_windows_gui_sibling_is_same_runtime_only_when_allowed(self) -> None:
|
|
152
|
+
with tempfile.TemporaryDirectory(prefix="memleaf-runtime-windows-gui-") as temporary:
|
|
153
|
+
config = Path(temporary) / "config.yaml"
|
|
154
|
+
vault = r"F:\Memleaf\Vault"
|
|
155
|
+
config.write_text(
|
|
156
|
+
json.dumps(
|
|
157
|
+
{
|
|
158
|
+
"mcp_servers": {
|
|
159
|
+
"memleaf": {
|
|
160
|
+
"command": r"F:\Runtime\Scripts\memleaf-mcpw.exe",
|
|
161
|
+
"args": ["--vault", vault],
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
),
|
|
166
|
+
encoding="utf-8",
|
|
167
|
+
)
|
|
168
|
+
strict = inspect_hermes_mcp(
|
|
169
|
+
config, vault, r"F:\Runtime\Scripts\memleaf-mcp.exe", platform="nt"
|
|
170
|
+
)
|
|
171
|
+
runtime_equivalent = inspect_hermes_mcp(
|
|
172
|
+
config,
|
|
173
|
+
vault,
|
|
174
|
+
r"F:\Runtime\Scripts\memleaf-mcp.exe",
|
|
175
|
+
platform="nt",
|
|
176
|
+
allow_same_runtime=True,
|
|
177
|
+
)
|
|
178
|
+
self.assertEqual("runtime_conflict", strict.status)
|
|
179
|
+
self.assertEqual("correct", runtime_equivalent.status)
|
|
180
|
+
|
|
151
181
|
def test_other_vault_remains_a_hard_conflict(self) -> None:
|
|
152
182
|
with tempfile.TemporaryDirectory(prefix="memleaf-runtime-vault-") as temporary:
|
|
153
183
|
root = Path(temporary)
|
|
@@ -455,7 +485,7 @@ class HermesInstallerTransactionTests(unittest.TestCase):
|
|
|
455
485
|
hermes_home = root / "hermes"
|
|
456
486
|
config = hermes_home / "config.yaml"
|
|
457
487
|
vault = root / "vault"
|
|
458
|
-
current = root / "managed" / "bin" / "memleaf-mcp"
|
|
488
|
+
current = root / "managed" / "bin" / ("memleaf-mcp.exe" if os.name == "nt" else "memleaf-mcp")
|
|
459
489
|
existing = root / "source" / ".venv" / "bin" / "memleaf-mcp"
|
|
460
490
|
config.parent.mkdir(parents=True)
|
|
461
491
|
vault.mkdir()
|
|
@@ -513,13 +543,15 @@ class HermesInstallerTransactionTests(unittest.TestCase):
|
|
|
513
543
|
hermes_home = root / "hermes"
|
|
514
544
|
config = hermes_home / "config.yaml"
|
|
515
545
|
vault = root / "vault"
|
|
516
|
-
current = root / "managed" / "bin" / "memleaf-mcp"
|
|
546
|
+
current = root / "managed" / "bin" / ("memleaf-mcp.exe" if os.name == "nt" else "memleaf-mcp")
|
|
517
547
|
existing = root / "source" / ".venv" / "bin" / "memleaf-mcp"
|
|
518
548
|
config.parent.mkdir(parents=True)
|
|
519
549
|
vault.mkdir()
|
|
520
550
|
current.parent.mkdir(parents=True)
|
|
521
551
|
existing.parent.mkdir(parents=True)
|
|
522
552
|
current.write_text("", encoding="utf-8")
|
|
553
|
+
if os.name == "nt":
|
|
554
|
+
current.with_name("memleaf-mcpw.exe").write_text("", encoding="utf-8")
|
|
523
555
|
existing.write_text("", encoding="utf-8")
|
|
524
556
|
original = json.dumps(
|
|
525
557
|
{
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
from tests.test_hermes_provider import provider_module
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HermesWindowsSubprocessTests(unittest.TestCase):
|
|
10
|
+
def test_windows_creationflags_use_create_no_window(self):
|
|
11
|
+
flag = 0x08000000
|
|
12
|
+
with (
|
|
13
|
+
patch.object(provider_module.os, "name", "nt"),
|
|
14
|
+
patch.object(provider_module.subprocess, "CREATE_NO_WINDOW", flag, create=True),
|
|
15
|
+
):
|
|
16
|
+
self.assertEqual(provider_module._mcp_creationflags(), flag)
|
|
17
|
+
|
|
18
|
+
def test_non_windows_creationflags_are_zero(self):
|
|
19
|
+
with patch.object(provider_module.os, "name", "posix"):
|
|
20
|
+
self.assertEqual(provider_module._mcp_creationflags(), 0)
|
|
21
|
+
|
|
22
|
+
def test_mcp_popen_receives_creationflags(self):
|
|
23
|
+
client = provider_module._MCPClient("memleaf-mcp", ".", 5.0)
|
|
24
|
+
process = Mock()
|
|
25
|
+
flag = 0x08000000
|
|
26
|
+
with (
|
|
27
|
+
patch.object(provider_module, "_mcp_creationflags", return_value=flag),
|
|
28
|
+
patch.object(provider_module.subprocess, "Popen", return_value=process) as popen,
|
|
29
|
+
patch.object(client, "_resolve_command", return_value="memleaf-mcp"),
|
|
30
|
+
patch.object(client, "_start_stdout_reader_locked"),
|
|
31
|
+
patch.object(client, "_request_locked", return_value={}),
|
|
32
|
+
patch.object(client, "_send_locked"),
|
|
33
|
+
):
|
|
34
|
+
client._start_locked()
|
|
35
|
+
|
|
36
|
+
self.assertEqual(popen.call_args.kwargs.get("creationflags"), flag)
|
|
37
|
+
self.assertIs(popen.call_args.kwargs.get("stdin"), provider_module.subprocess.PIPE)
|
|
38
|
+
self.assertIs(popen.call_args.kwargs.get("stdout"), provider_module.subprocess.PIPE)
|
|
39
|
+
self.assertIs(popen.call_args.kwargs.get("stderr"), provider_module.subprocess.DEVNULL)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
unittest.main()
|
|
@@ -53,6 +53,10 @@ class PyPIInstallTests(unittest.TestCase):
|
|
|
53
53
|
"name: memleaf\nversion: 0.2.9\n",
|
|
54
54
|
encoding="utf-8",
|
|
55
55
|
)
|
|
56
|
+
runtime_command = root / ("memleaf-mcp.exe" if os.name == "nt" else "memleaf-mcp")
|
|
57
|
+
runtime_command.write_text("", encoding="utf-8")
|
|
58
|
+
if os.name == "nt":
|
|
59
|
+
runtime_command.with_name("memleaf-mcpw.exe").write_text("", encoding="utf-8")
|
|
56
60
|
detection = SimpleNamespace(
|
|
57
61
|
detected=True,
|
|
58
62
|
confidence="high",
|
|
@@ -76,7 +80,7 @@ class PyPIInstallTests(unittest.TestCase):
|
|
|
76
80
|
to_dict=lambda: {"status": "configured", "reason": "configured"},
|
|
77
81
|
)
|
|
78
82
|
|
|
79
|
-
with mock.patch("memleaf.installer._home_from_environment", return_value=home), mock.patch("memleaf.installer._hermes_home", return_value=hermes_home), mock.patch("memleaf.installer._select_vault_path", return_value=(vault_path, "default")), mock.patch("memleaf.installer.Vault.initialize", return_value=initialized), mock.patch("memleaf.installer._prepare_model_route", return_value=model), mock.patch("memleaf.installer.HermesAdapter", return_value=adapter), mock.patch("memleaf.installer._memleaf_mcp_command", return_value=
|
|
83
|
+
with mock.patch("memleaf.installer._home_from_environment", return_value=home), mock.patch("memleaf.installer._hermes_home", return_value=hermes_home), mock.patch("memleaf.installer._select_vault_path", return_value=(vault_path, "default")), mock.patch("memleaf.installer.Vault.initialize", return_value=initialized), mock.patch("memleaf.installer._prepare_model_route", return_value=model), mock.patch("memleaf.installer.HermesAdapter", return_value=adapter), mock.patch("memleaf.installer._memleaf_mcp_command", return_value=runtime_command), mock.patch("memleaf.installer._configure_hermes_mcp_entry", return_value=configured), mock.patch("memleaf.installer._copy_provider", return_value=provider_path), mock.patch("memleaf.installer._write_provider_config") as write_config:
|
|
80
84
|
result = install_hermes()
|
|
81
85
|
|
|
82
86
|
self.assertEqual("failure", result["status"])
|
|
@@ -45,6 +45,10 @@ class StageC3PackagingTests(unittest.TestCase):
|
|
|
45
45
|
"memleaf-mcp": "memleaf.mcp_server:main",
|
|
46
46
|
},
|
|
47
47
|
)
|
|
48
|
+
self.assertEqual(
|
|
49
|
+
self.project["gui-scripts"],
|
|
50
|
+
{"memleaf-mcpw": "memleaf.mcp_server:main"},
|
|
51
|
+
)
|
|
48
52
|
package_data = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))["tool"]["setuptools"]["package-data"]
|
|
49
53
|
self.assertIn("plugin.yaml", package_data["memleaf.hermes_provider"])
|
|
50
54
|
self.assertIn("README.md", package_data["memleaf.hermes_provider"])
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
import struct
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
import unittest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WindowsPublicMcpLauncherTests(unittest.TestCase):
|
|
14
|
+
@unittest.skipUnless(os.name == "nt", "Windows GUI launcher acceptance")
|
|
15
|
+
def test_gui_launcher_is_windows_gui_subsystem(self) -> None:
|
|
16
|
+
launcher = shutil.which("memleaf-mcpw")
|
|
17
|
+
self.assertIsNotNone(launcher)
|
|
18
|
+
path = Path(str(launcher))
|
|
19
|
+
with path.open("rb") as stream:
|
|
20
|
+
header = stream.read(64)
|
|
21
|
+
self.assertGreaterEqual(len(header), 64)
|
|
22
|
+
self.assertEqual(header[:2], b"MZ")
|
|
23
|
+
pe_offset = struct.unpack_from("<I", header, 0x3C)[0]
|
|
24
|
+
stream.seek(pe_offset)
|
|
25
|
+
self.assertEqual(stream.read(4), b"PE\x00\x00")
|
|
26
|
+
stream.seek(20, 1)
|
|
27
|
+
optional_start = stream.tell()
|
|
28
|
+
stream.seek(optional_start + 68)
|
|
29
|
+
subsystem = struct.unpack("<H", stream.read(2))[0]
|
|
30
|
+
self.assertEqual(2, subsystem, "memleaf-mcpw.exe must use IMAGE_SUBSYSTEM_WINDOWS_GUI")
|
|
31
|
+
|
|
32
|
+
@unittest.skipUnless(os.name == "nt", "Windows GUI launcher acceptance")
|
|
33
|
+
def test_gui_launcher_stdio_mcp_round_trip_without_creation_flags(self) -> None:
|
|
34
|
+
launcher = shutil.which("memleaf-mcpw")
|
|
35
|
+
self.assertIsNotNone(launcher)
|
|
36
|
+
with tempfile.TemporaryDirectory(prefix="memleaf-mcpw-vault-") as temporary:
|
|
37
|
+
vault = Path(temporary) / "vault"
|
|
38
|
+
requests = [
|
|
39
|
+
{
|
|
40
|
+
"jsonrpc": "2.0",
|
|
41
|
+
"id": 1,
|
|
42
|
+
"method": "initialize",
|
|
43
|
+
"params": {
|
|
44
|
+
"protocolVersion": "2024-11-05",
|
|
45
|
+
"capabilities": {},
|
|
46
|
+
"clientInfo": {"name": "memleaf-windows-test", "version": "1"},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
|
|
50
|
+
]
|
|
51
|
+
payload = "".join(json.dumps(item) + "\n" for item in requests)
|
|
52
|
+
process = subprocess.Popen(
|
|
53
|
+
[str(launcher), "--vault", str(vault)],
|
|
54
|
+
stdin=subprocess.PIPE,
|
|
55
|
+
stdout=subprocess.PIPE,
|
|
56
|
+
stderr=subprocess.PIPE,
|
|
57
|
+
text=True,
|
|
58
|
+
encoding="utf-8",
|
|
59
|
+
errors="strict",
|
|
60
|
+
creationflags=0,
|
|
61
|
+
)
|
|
62
|
+
stdout, stderr = process.communicate(payload, timeout=20)
|
|
63
|
+
self.assertEqual(0, process.returncode, stderr)
|
|
64
|
+
responses = [json.loads(line) for line in stdout.splitlines() if line.strip()]
|
|
65
|
+
by_id = {item.get("id"): item for item in responses}
|
|
66
|
+
self.assertIn(1, by_id)
|
|
67
|
+
self.assertIn(2, by_id)
|
|
68
|
+
self.assertIn("result", by_id[1])
|
|
69
|
+
tools = by_id[2]["result"]["tools"]
|
|
70
|
+
self.assertEqual(13, len(tools))
|
|
71
|
+
self.assertEqual(13, len({tool["name"] for tool in tools}))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|