v1.4.1 bump
This commit is contained in:
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -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 — env → config-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.
Binary file not shown.
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 {
|
||||||
|
"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 {
|
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,
|
"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.
|
||||||
|
|||||||
Reference in New Issue
Block a user