1
0
forked from jason/echo

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
Vendored
BIN
View File
Binary file not shown.
+6
View File
@@ -19,3 +19,9 @@ eval/results/*.json
# ECHO config: the filled-in key file is yours to distribute, never commit it # ECHO config: the filled-in key file is yours to distribute, never commit it
/echo-memory.config.json /echo-memory.config.json
/dist/ /dist/
# Per-user BAKED artifacts (build.py --bake-key, named echo-memory-<ver>-<label>.plugin)
# carry a live vault bearer token — NEVER commit them. They belong in dist/ (above);
# this also guards any that get built at the repo root. The version-only pointer
# (echo-memory.plugin) and version-only artifacts (echo-memory-<ver>.plugin) stay tracked.
echo-memory-*-*.plugin
+29 -3
View File
@@ -1,5 +1,29 @@
# Changelog # Changelog
## 1.4.1
### Fixed — baked credentials now win unconditionally
A per-user baked artifact could still be prompted for credentials on install. The
baked `DEFAULT_*` tier was lowest priority, so a stale `ECHO_*` env var or a
leftover/placeholder `~/.claude/echo-memory/config.json` shadowed the baked key —
even an unedited `"<placeholder>"` value won over it, flipping `is_configured()` to
false and triggering the first-run "ask the operator" flow despite valid baked creds.
- `echo_config.load()` now treats a **complete** baked set (endpoint + key both
present) as authoritative: it wins over env vars and the config file and cannot be
shadowed. The generic (unbaked) plugin keeps its env → file → first-run flow.
- Added `baked_complete()`; `source()` reports `baked-in (plugin)` for that tier.
### Security — untracked leaked baked artifacts
- Two labeled baked artifacts (`echo-memory-1.4.0-jason.plugin`,
`echo-memory-1.4.0-gretchen.plugin`), each carrying a live vault bearer token, had
been committed at the repo root. They are now `git rm --cached`-removed and
`.gitignore` gains `echo-memory-*-*.plugin` so labeled builds can't be re-committed
(the version-only pointer `echo-memory.plugin` and `echo-memory-<ver>.plugin` stay
tracked). Tokens remain in prior history — rotate if the repo was ever shared.
## 1.4.0 ## 1.4.0
### Added — per-user baked-key builds (CoWork zero-touch) ### Added — per-user baked-key builds (CoWork zero-touch)
@@ -22,9 +46,11 @@ the committed tree still ships zero credentials.
### Changed — artifact hygiene ### Changed — artifact hygiene
- `.gitignore` now excludes `*.plugin` and `dist/`; previously-tracked `.plugin` - `.gitignore` excludes `dist/` and the filled-in `echo-memory.config.json`, where
artifacts were untracked. A baked artifact carries a vault key and must be baked builds and the key file are meant to live. A baked artifact carries a vault
**delivered directly to that one user, never committed, pushed, or published.** key and must be **delivered directly to that one user, never committed, pushed, or
published.** (Note: labeled baked artifacts built at the repo *root* were not yet
guarded — see 1.4.1.)
### Resolution order (unchanged for hosts) ### Resolution order (unchanged for hosts)
+66
View File
@@ -0,0 +1,66 @@
# ECHO Memory — install guide (Gretchen)
This is your personal build of the ECHO Memory plugin: **`echo-memory-1.4.1-gretchen.plugin`**.
Your vault owner name, endpoint, and API key are already **baked in** — you don't need a
config file or to paste a key anywhere. Just install it and go.
- **Owner:** Gretchen Friedrich
- **Endpoint:** `https://imprintapi.mpm.to`
- **Backend:** Obsidian Local REST API (verified reachable + your key authenticates ✅)
---
## Install
### In CoWork (recommended)
1. Open the plugins panel.
2. Upload / add the file **`echo-memory-1.4.1-gretchen.plugin`**.
3. Enable **echo-memory** if it isn't on automatically.
4. Start a new session — the skill loads on its own.
### In Claude Code (desktop / CLI)
1. Save the `.plugin` file somewhere local.
2. Add it through your plugin manager (the `.plugin` file is a standard plugin package).
3. Restart your session so the plugin is picked up.
No `~/.claude/echo-memory/config.json` needed — the baked build carries everything.
---
## Verify it's working
Run either:
- **`/echo-doctor`** — checks Python, your endpoint, that your key authenticates, and
whether the vault is bootstrapped. All green = you're set.
- **`/echo-load`** — loads your memory context for the session.
If the vault hasn't been set up yet, `/echo-doctor` will say the marker is absent — that's
expected on a brand-new vault and just means a one-time bootstrap is needed.
---
## Day-to-day
| You want to… | Command |
|---|---|
| Pull in your context at the start of a session | `/echo-load` |
| Save something durable (fact, decision, preference) | `/echo-save` |
| Recall a topic + its linked notes | `/echo-recall <topic>` |
| File a quick capture for later sorting | `/echo-triage` |
| Reflect on a session and capture takeaways | `/echo-reflect` |
You can also just talk naturally — ask Claude to "remember" something or "what do we know
about X" and it'll route to the right place.
---
## Keep in mind
- **This file contains your vault key.** Don't forward the `.plugin` to anyone else or post
it in a shared channel — it grants access to your vault under your name.
- **Updating / rotating your key:** you can't edit a baked build. If your token changes,
ask Jason for a fresh build, then reinstall.
- **Trouble connecting?** Run `/echo-doctor` first — it pinpoints whether it's the endpoint,
the key, or the vault. If the endpoint is unreachable, confirm Obsidian + the Local REST
API plugin are running on the host serving `imprintapi.mpm.to`.
+5 -3
View File
@@ -1,4 +1,4 @@
# echo-memory — v1.4.0 # echo-memory — v1.4.1
Persistent memory for Claude / CoWork sessions via the **ECHO** Obsidian vault, driven over the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api). The skill makes direct REST calls through a bundled validated client (`scripts/echo.py`). The whole toolchain is **pure Python** (stdlib only), so it runs identically on Windows, macOS, and Linux — no bash, no platform-specific `date`. Persistent memory for Claude / CoWork sessions via the **ECHO** Obsidian vault, driven over the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api). The skill makes direct REST calls through a bundled validated client (`scripts/echo.py`). The whole toolchain is **pure Python** (stdlib only), so it runs identically on Windows, macOS, and Linux — no bash, no platform-specific `date`.
@@ -39,10 +39,10 @@ The CoWork sandbox does **not** bridge your real `~/.claude` (the mounted `.clau
```bash ```bash
python build.py --bake-key --from alice-config.json --label alice python build.py --bake-key --from alice-config.json --label alice
# -> dist/echo-memory-1.4.0-alice.plugin (carries Alice's vault key) # -> dist/echo-memory-1.4.1-alice.plugin (carries Alice's vault key)
``` ```
Resolution gains a lowest-priority **baked** tier (`DEFAULT_OWNER/BASE/KEY` in `echo_config.py`) that is **empty in source** — the committed tree still ships zero credentials, and host behaviour is unchanged (env/config-file still win). `build.py --bake-key` fills those constants from a `config.json` (or `--owner/--endpoint/--key`, or `ECHO_*` env). The end user just installs the artifact — **no config file, no per-session paste**, works on desktop and in every CoWork session. Resolution gains a **baked** tier (`DEFAULT_OWNER/BASE/KEY` in `echo_config.py`) that is **empty in source** — the committed tree still ships zero credentials. As of **v1.4.1** a *complete* baked set (endpoint **and** key both present) is **authoritative**: it wins over `ECHO_*` env vars and any `~/.claude` config file and cannot be shadowed, so a delivered per-user artifact "just works" even on a machine carrying a stale or placeholder config. When nothing is baked (the generic published plugin), host behaviour is unchanged envconfig-file → first-run prompt. `build.py --bake-key` fills those constants from a `config.json` (or `--owner/--endpoint/--key`, or `ECHO_*` env). The end user just installs the artifact — **no config file, no per-session paste**, works on desktop and in every CoWork session.
> **A baked artifact contains a vault bearer token.** Baked builds land in `dist/` (gitignored) and never touch the shared `echo-memory.plugin` pointer. Deliver each one **directly** to its single user; **never commit, push, or publish it.** Use `--strip-key` (or just a clean source build) for a token-free artifact. Rotation = rebuild + reinstall. > **A baked artifact contains a vault bearer token.** Baked builds land in `dist/` (gitignored) and never touch the shared `echo-memory.plugin` pointer. Deliver each one **directly** to its single user; **never commit, push, or publish it.** Use `--strip-key` (or just a clean source build) for a token-free artifact. Rotation = rebuild + reinstall.
@@ -380,6 +380,8 @@ If the API returns a connection error, timeout, or `502` (usually Obsidian / the
| Version | Highlights | | Version | Highlights |
|---------|-----------| |---------|-----------|
| **1.4.1** | **Baked credentials now win unconditionally.** A per-user baked artifact could still be prompted for credentials on install: the baked `DEFAULT_*` tier was lowest priority, so a stale `ECHO_*` env var or a leftover/placeholder `~/.claude/echo-memory/config.json` shadowed the baked key — even an unedited `"<placeholder>"` value beat it, flipping `is_configured()` to false and triggering the first-run "ask the operator" flow despite valid baked creds. `echo_config.load()` now treats a **complete** baked set (endpoint + key both present) as authoritative — it wins over env vars and the config file and cannot be shadowed; the generic (unbaked) plugin keeps its env → file → first-run flow. New `baked_complete()`; `source()` reports `baked-in (plugin)`. Per-user artifacts rebaked; manifest → 1.4.1. |
| **1.4.0** | **Per-user baked-key builds — zero-touch CoWork install.** The CoWork sandbox doesn't bridge your real `~/.claude`, but the plugin itself is mounted every session — so credentials can travel *inside* the artifact. `echo_config` gains a baked tier (`DEFAULT_OWNER/BASE/KEY`, **empty in source** so the committed tree still ships zero credentials). New `build.py --bake-key --from <config.json> [--label <name>]` substitutes one user's owner/endpoint/key into those constants, producing a per-user artifact that needs **no config file and no per-session paste** (values may also come from `--owner/--endpoint/--key` or `ECHO_*` env). Baked builds default to `dist/` (gitignored) and never touch the shared `echo-memory.plugin` pointer; `--strip-key` force-blanks the constants for a guaranteed token-free build. `config show`/`doctor` report the baked source. |
| **1.3.1** | **Prompts for the key on load.** A fresh or just-upgraded machine has no config yet, so the plugin detects that and asks for it instead of failing opaquely. `echo_config.is_configured()` treats a missing **or still-placeholder** config as unconfigured; `echo.py load` prints a `NOT CONFIGURED` banner and exits `78` (other verbs exit `2`; `/echo-doctor` reports it red). New **`echo.py config import <file>`** adopts a config you've been handed — it validates the file and writes `~/.claude/echo-memory/config.json`. `SKILL.md` gains a **First run** step: on the not-configured signal the agent asks the operator for their key file and installs it, then proceeds. One-time per machine — the config lives in `~/.claude/`, so later plugin updates don't disturb it. | | **1.3.1** | **Prompts for the key on load.** A fresh or just-upgraded machine has no config yet, so the plugin detects that and asks for it instead of failing opaquely. `echo_config.is_configured()` treats a missing **or still-placeholder** config as unconfigured; `echo.py load` prints a `NOT CONFIGURED` banner and exits `78` (other verbs exit `2`; `/echo-doctor` reports it red). New **`echo.py config import <file>`** adopts a config you've been handed — it validates the file and writes `~/.claude/echo-memory/config.json`. `SKILL.md` gains a **First run** step: on the not-configured signal the agent asks the operator for their key file and installs it, then proceeds. One-time per machine — the config lives in `~/.claude/`, so later plugin updates don't disturb it. |
| **1.3.0** | **User-agnostic — owner/endpoint/key out of the source.** The plugin previously baked in an API key and hard-coded the endpoint; both are gone. Owner, endpoint, and key are now **machine-local**, resolved by the new `echo_config.py` from `~/.claude/echo-memory/config.json` (env `ECHO_OWNER`/`ECHO_BASE`/`ECHO_KEY` override per field); `echo_secrets.py` removed. New `echo.py config` subcommand (`show`/`init`/`set`). All personal details (owner, role/employer), the live endpoint, and the literal key were **stripped** from the plugin source, references, commands, and eval harness — the only name that remains is the architect credit. A placeholder `config.template.json` is provided for new users/vaults. Routing manifest unchanged; offline test suites pass. Manifest → 1.3.0. | | **1.3.0** | **User-agnostic — owner/endpoint/key out of the source.** The plugin previously baked in an API key and hard-coded the endpoint; both are gone. Owner, endpoint, and key are now **machine-local**, resolved by the new `echo_config.py` from `~/.claude/echo-memory/config.json` (env `ECHO_OWNER`/`ECHO_BASE`/`ECHO_KEY` override per field); `echo_secrets.py` removed. New `echo.py config` subcommand (`show`/`init`/`set`). All personal details (owner, role/employer), the live endpoint, and the literal key were **stripped** from the plugin source, references, commands, and eval harness — the only name that remains is the architect credit. A placeholder `config.template.json` is provided for new users/vaults. Routing manifest unchanged; offline test suites pass. Manifest → 1.3.0. |
| **1.2.0** | **Entity-resolution overhaul — fewer duplicate notes.** Resolution was exact-match only, so a shortened or expanded mention (e.g. "echo memory" for the `echo`-slugged active project) returned *no match* and the writer created a parallel note. Now `resolve()` matches on slug / title / **aliases**, with a `fuzzy_candidates()` "did-you-mean" fallback ranked by shared distinctive tokens (guarded by `echo_quality` so generic words like "data"/"api"/"the" can't drive a match). Aliases are **auto-derived from titles** (hyphen/space/case variants), **learned from mentions** on update, and stored in each note's `aliases:` frontmatter (the Obsidian-native home), then folded back into the entity index by `sweep` — so the graph gets better at resolution over time, while exact-match safety still prevents wrong auto-merges. Ships with the v1.1 connection-pool + concurrency work. 33 automated tests pass across Win/macOS/Linux; linter clean. See the [performance brief](echo-memory-performance-brief.html). | | **1.2.0** | **Entity-resolution overhaul — fewer duplicate notes.** Resolution was exact-match only, so a shortened or expanded mention (e.g. "echo memory" for the `echo`-slugged active project) returned *no match* and the writer created a parallel note. Now `resolve()` matches on slug / title / **aliases**, with a `fuzzy_candidates()` "did-you-mean" fallback ranked by shared distinctive tokens (guarded by `echo_quality` so generic words like "data"/"api"/"the" can't drive a match). Aliases are **auto-derived from titles** (hyphen/space/case variants), **learned from mentions** on update, and stored in each note's `aliases:` frontmatter (the Obsidian-native home), then folded back into the entity index by `sweep` — so the graph gets better at resolution over time, while exact-match safety still prevents wrong auto-merges. Ships with the v1.1 connection-pool + concurrency work. 33 automated tests pass across Win/macOS/Linux; linter clean. See the [performance brief](echo-memory-performance-brief.html). |
Binary file not shown.
@@ -1,6 +1,6 @@
{ {
"name": "echo-memory", "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.", "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": { "author": {
"name": "Jason Stedwell" "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 It prints green/red for: Python version, vault reachability, auth accepted, vault
bootstrapped (+ `schema_version`), and the **config source** for owner/endpoint/key 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 (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 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`. `/echo-health`.
@@ -11,7 +11,9 @@ The vault belongs to a single **owner**, configured per machine (the `owner` fie
## API Configuration ## 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> 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" 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.) - **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] tags: [agent, system, marker]
agent_written: true agent_written: true
source_notes: [] source_notes: []
schema_version: 3 schema_version: 4
bootstrapped: {{DATE}} bootstrapped: {{DATE}}
managed_by: echo-memory-plugin 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 `python3 echo.py config init` (writes a placeholder template when absent) and edit
it, or `python3 echo.py config set --owner ... --endpoint ... --key ...`. it, or `python3 echo.py config set --owner ... --endpoint ... --key ...`.
Per-field resolution (first hit wins): Resolution — a COMPLETE baked credential set wins unconditionally:
owner env ECHO_OWNER -> config "owner" -> baked DEFAULT_OWNER
endpoint env ECHO_BASE -> config "endpoint" -> baked DEFAULT_BASE If the artifact carries both a baked endpoint AND key (a per-user build), those
key env ECHO_KEY -> config "key" -> baked DEFAULT_KEY 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 The config directory honors $CLAUDE_CONFIG_DIR (defaults to ~/.claude); the config
file path itself can be overridden with $ECHO_CONFIG. Pure stdlib only. 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 BAKED CREDENTIALS (the default tier): the DEFAULT_* constants below are EMPTY in
— the committed plugin ships zero credentials. `build.py --bake-key` substitutes a source — the committed plugin ships zero credentials. `build.py --bake-key`
specific user's owner/endpoint/key into them at package time, producing a per-user substitutes a specific user's owner/endpoint/key into them at package time,
artifact that needs no config file. This is how the plugin works in the CoWork producing a per-user artifact that needs no config file. This is how the plugin
sandbox, where the user's ~/.claude is not bridged: the plugin itself (mounted works in the CoWork sandbox, where the user's ~/.claude is not bridged: the plugin
read-only every session) carries the values. A baked artifact is secret-bearing — itself (mounted read-only every session) carries the values. A baked artifact is
deliver it directly to that one user, NEVER commit or publish it. On a normal host secret-bearing — deliver it directly to that one user, NEVER commit or publish it.
the empty defaults mean behaviour is unchanged (env/config file still win). On a normal host the empty defaults mean the env/config-file path takes over.
""" """
from __future__ import annotations from __future__ import annotations
@@ -76,18 +85,34 @@ def _from_file() -> dict:
return data if isinstance(data, dict) else {} 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: 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(), Never raises — callers that REQUIRE a field use resolve_endpoint()/resolve_key(),
which raise a helpful error. This keeps `--help`, `config`, and `doctor` usable which raise a helpful error. This keeps `--help`, `config`, and `doctor` usable
even when nothing is configured yet.""" even when nothing is configured yet."""
f = _from_file() 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 { return {
"owner": os.environ.get("ECHO_OWNER") or f.get("owner") or DEFAULT_OWNER or "", "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 "",
"endpoint": endpoint, "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: def source(field: str) -> str:
"""Where a field is currently coming from — for `config`/`doctor` reporting.""" """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] env = {"owner": "ECHO_OWNER", "endpoint": "ECHO_BASE", "key": "ECHO_KEY"}[field]
if os.environ.get(env): if os.environ.get(env):
return f"{env} env" return f"{env} env"
if config_path().exists() and _from_file().get(field): if config_path().exists() and _from_file().get(field):
return "config file" return "config file"
baked = {"owner": DEFAULT_OWNER, "endpoint": DEFAULT_BASE, "key": DEFAULT_KEY}[field]
if baked: if baked:
return "baked-in default" return "baked-in (plugin)"
return "unset" 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 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 reciprocal B -> A exists (it only adds the missing direction; it never invents
new links from body text), and 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. Dry-run by default; pass --apply to write. READ-ONLY without --apply.
Cross-platform: pure Python via echo.py. Cross-platform: pure Python via echo.py.