v1.4.1 bump

This commit is contained in:
Jason Stedwell
2026-06-26 15:39:17 -05:00
parent 0cd8a54649
commit 9ee1530f97
15 changed files with 162 additions and 30 deletions
@@ -1,6 +1,6 @@
{
"name": "echo-memory",
"version": "1.4.0",
"version": "1.4.1",
"description": "Persistent memory via the ECHO Obsidian vault over the Obsidian Local REST API. A cross-platform, connection-pooled Python client: one-call capture/resolve/recall/link over an entity index, hybrid BM25 + graph recall, auto-linking, an offline write-ahead queue, and lock-guarded concurrency. Linter-enforced routing manifest plus /echo-load|save|recall|triage|health|sweep|reflect|doctor commands. Crosslinks notes across Claude and CoWork sessions.",
"author": {
"name": "Jason Stedwell"
@@ -12,6 +12,7 @@ python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" doctor
It prints green/red for: Python version, vault reachability, auth accepted, vault
bootstrapped (+ `schema_version`), and the **config source** for owner/endpoint/key
(per-field: env override `ECHO_OWNER`/`ECHO_BASE`/`ECHO_KEY`, then the machine-local
`~/.claude/echo-memory/config.json`). Exits non-zero if anything is red — if the config is
`~/.claude/echo-memory/config.json` — or `baked-in (plugin)` on a per-user baked build,
which is authoritative and reported as such). Exits non-zero if anything is red — if the config is
missing (or still the placeholder template), ask the operator for their echo-memory config file and install it with `echo.py config import <path>` (or `config set --owner … --endpoint … --key …`). For the full vault-invariant lint, run
`/echo-health`.
@@ -11,7 +11,9 @@ The vault belongs to a single **owner**, configured per machine (the `owner` fie
## API Configuration
The owner, endpoint, and API key are **machine-local** — the plugin ships none of them. `echo.py` (via `echo_config`) resolves each field from `~/.claude/echo-memory/config.json`, with an environment override per field (`ECHO_OWNER`, `ECHO_BASE`, `ECHO_KEY`). Never paste the key into a vault note or a doc.
The owner, endpoint, and API key are **machine-local** — the committed plugin ships none of them. `echo.py` (via `echo_config`) resolves each field from `~/.claude/echo-memory/config.json`, with an environment override per field (`ECHO_OWNER`, `ECHO_BASE`, `ECHO_KEY`). Never paste the key into a vault note or a doc.
Exception — **per-user baked builds:** a `build.py --bake-key` artifact carries the owner/endpoint/key inside the plugin. When a *complete* baked set is present it is **authoritative** — it wins over env vars and the config file and can't be shadowed (so a baked install just works, even on a machine with a stale or placeholder config). The committed source stays credential-free.
```
endpoint = <config "endpoint" / $ECHO_BASE>
@@ -34,7 +34,7 @@ At session start, GET the marker:
curl -s -o /dev/null -w "%{http_code}" -H "$AUTH" "$BASE/vault/_agent/echo-vault.md"
```
- **200** → bootstrapped. Read the marker's `schema_version`; if it is **less than** the plugin's current schema (2), run a migration pass (see *Migrations* below), otherwise proceed straight to the loading procedure in `SKILL.md`.
- **200** → bootstrapped. Read the marker's `schema_version`; if it is **less than** the plugin's current schema (4), run a migration pass (see *Migrations* below), otherwise proceed straight to the loading procedure in `SKILL.md`.
- **404** → empty/unconfigured vault. Run **Fresh bootstrap** below. (If you expected an existing vault, confirm with the operator once that the REST API is pointed at the right vault before seeding.)
---
@@ -6,7 +6,7 @@ updated: {{DATE}}
tags: [agent, system, marker]
agent_written: true
source_notes: []
schema_version: 3
schema_version: 4
bootstrapped: {{DATE}}
managed_by: echo-memory-plugin
---
@@ -16,22 +16,31 @@ committed, and is never written into a vault note. To create it, run
`python3 echo.py config init` (writes a placeholder template when absent) and edit
it, or `python3 echo.py config set --owner ... --endpoint ... --key ...`.
Per-field resolution (first hit wins):
owner env ECHO_OWNER -> config "owner" -> baked DEFAULT_OWNER
endpoint env ECHO_BASE -> config "endpoint" -> baked DEFAULT_BASE
key env ECHO_KEY -> config "key" -> baked DEFAULT_KEY
Resolution — a COMPLETE baked credential set wins unconditionally:
If the artifact carries both a baked endpoint AND key (a per-user build), those
baked values are used as-is and CANNOT be shadowed by env vars or a config file.
This is the default path: a delivered per-user plugin "just works" with no setup,
and a stale ECHO_* env var or a leftover/placeholder ~/.claude config.json can
never override or break it.
Otherwise (nothing baked — the generic published plugin), fall back per-field,
first hit wins:
owner env ECHO_OWNER -> config "owner"
endpoint env ECHO_BASE -> config "endpoint"
key env ECHO_KEY -> config "key"
The config directory honors $CLAUDE_CONFIG_DIR (defaults to ~/.claude); the config
file path itself can be overridden with $ECHO_CONFIG. Pure stdlib only.
BAKED FALLBACK (the lowest tier): the DEFAULT_* constants below are EMPTY in source
— the committed plugin ships zero credentials. `build.py --bake-key` substitutes a
specific user's owner/endpoint/key into them at package time, producing a per-user
artifact that needs no config file. This is how the plugin works in the CoWork
sandbox, where the user's ~/.claude is not bridged: the plugin itself (mounted
read-only every session) carries the values. A baked artifact is secret-bearing —
deliver it directly to that one user, NEVER commit or publish it. On a normal host
the empty defaults mean behaviour is unchanged (env/config file still win).
BAKED CREDENTIALS (the default tier): the DEFAULT_* constants below are EMPTY in
source — the committed plugin ships zero credentials. `build.py --bake-key`
substitutes a specific user's owner/endpoint/key into them at package time,
producing a per-user artifact that needs no config file. This is how the plugin
works in the CoWork sandbox, where the user's ~/.claude is not bridged: the plugin
itself (mounted read-only every session) carries the values. A baked artifact is
secret-bearing — deliver it directly to that one user, NEVER commit or publish it.
On a normal host the empty defaults mean the env/config-file path takes over.
"""
from __future__ import annotations
@@ -76,18 +85,34 @@ def _from_file() -> dict:
return data if isinstance(data, dict) else {}
def baked_complete() -> bool:
"""True when the artifact carries a usable baked endpoint AND key — i.e. a
per-user `--bake-key` build. When True, the baked values win unconditionally."""
return bool(DEFAULT_BASE and DEFAULT_KEY)
def load() -> dict:
"""Return {owner, endpoint, key} with env overriding the file. Missing -> "".
"""Return {owner, endpoint, key}. A complete baked set wins unconditionally;
otherwise env overrides the file, missing -> "".
Never raises — callers that REQUIRE a field use resolve_endpoint()/resolve_key(),
which raise a helpful error. This keeps `--help`, `config`, and `doctor` usable
even when nothing is configured yet."""
f = _from_file()
endpoint = (os.environ.get("ECHO_BASE") or f.get("endpoint") or DEFAULT_BASE or "").rstrip("/")
if baked_complete():
# Per-user artifact: baked creds are authoritative and must not be shadowed
# by a stale env var or a leftover/placeholder config file. Owner is purely
# descriptive, so it may still fall back when not baked.
return {
"owner": DEFAULT_OWNER or os.environ.get("ECHO_OWNER") or f.get("owner") or "",
"endpoint": DEFAULT_BASE.rstrip("/"),
"key": DEFAULT_KEY,
}
endpoint = (os.environ.get("ECHO_BASE") or f.get("endpoint") or "").rstrip("/")
return {
"owner": os.environ.get("ECHO_OWNER") or f.get("owner") or DEFAULT_OWNER or "",
"owner": os.environ.get("ECHO_OWNER") or f.get("owner") or "",
"endpoint": endpoint,
"key": os.environ.get("ECHO_KEY") or f.get("key") or DEFAULT_KEY or "",
"key": os.environ.get("ECHO_KEY") or f.get("key") or "",
}
@@ -133,14 +158,18 @@ def is_configured(cfg: dict | None = None) -> bool:
def source(field: str) -> str:
"""Where a field is currently coming from — for `config`/`doctor` reporting."""
baked = {"owner": DEFAULT_OWNER, "endpoint": DEFAULT_BASE, "key": DEFAULT_KEY}[field]
# A complete baked set wins unconditionally (see load()): endpoint/key always
# come from the artifact, and owner too whenever it was baked.
if baked_complete() and (field in ("endpoint", "key") or baked):
return "baked-in (plugin)"
env = {"owner": "ECHO_OWNER", "endpoint": "ECHO_BASE", "key": "ECHO_KEY"}[field]
if os.environ.get(env):
return f"{env} env"
if config_path().exists() and _from_file().get(field):
return "config file"
baked = {"owner": DEFAULT_OWNER, "endpoint": DEFAULT_BASE, "key": DEFAULT_KEY}[field]
if baked:
return "baked-in default"
return "baked-in (plugin)"
return "unset"
@@ -11,7 +11,7 @@ backfill what older vaults don't have yet:
3. **symmetrize cross-links** — for every `## Related` link A -> B, ensure the
reciprocal B -> A exists (it only adds the missing direction; it never invents
new links from body text), and
4. stamp the marker `schema_version` to the current schema (3).
4. stamp the marker `schema_version` to the current schema (4).
Dry-run by default; pass --apply to write. READ-ONLY without --apply.
Cross-platform: pure Python via echo.py.