diff --git a/.DS_Store b/.DS_Store index 01e09b6..6027d57 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 353f78f..a3e764a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 1.4.2 + +### Fixed — CoWork sandbox plugin-path resolution + +In a remote CoWork sandbox, `${CLAUDE_PLUGIN_ROOT}` can resolve to a host path the +sandbox can't reach (e.g. `/var/folders/…`), while the plugin is actually mounted under +`…/mnt/.remote-plugins/…`. Every `python3 "${CLAUDE_PLUGIN_ROOT}/…"` invocation then +failed until the agent manually located the real path. (Root cause is the harness env +var, not the plugin — the scripts always used the variable correctly, never a hardcoded +path — but the plugin now self-heals.) + +- SKILL.md and all eight slash commands resolve the scripts path with a fallback: + prefer `${CLAUDE_PLUGIN_ROOT}`, else locate the copy under + `/sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/`. Reused via + `$ECHO`/`$LINT`/`$SWEEP`/`$SDIR`; on a normal host the primary path always wins, so + behaviour is unchanged. +- `echo-health` / `echo-sweep` `allowed-tools` broadened to match the resolved + (variable-dir) invocation and the `ls`/`dirname` resolver steps. + ## 1.4.1 ### Fixed — baked credentials now win unconditionally diff --git a/README-gretchen.md b/README-gretchen.md index 4ea72d9..8b2e039 100644 --- a/README-gretchen.md +++ b/README-gretchen.md @@ -1,6 +1,6 @@ # ECHO Memory — install guide (Gretchen) -This is your personal build of the ECHO Memory plugin: **`echo-memory-1.4.1-gretchen.plugin`**. +This is your personal build of the ECHO Memory plugin: **`echo-memory-1.4.2-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. @@ -14,7 +14,7 @@ config file or to paste a key anywhere. Just install it and go. ### In CoWork (recommended) 1. Open the plugins panel. -2. Upload / add the file **`echo-memory-1.4.1-gretchen.plugin`**. +2. Upload / add the file **`echo-memory-1.4.2-gretchen.plugin`**. 3. Enable **echo-memory** if it isn't on automatically. 4. Start a new session — the skill loads on its own. diff --git a/README.md b/README.md index f850772..be0a040 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# echo-memory — v1.4.1 +# echo-memory — v1.4.2 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`. @@ -380,6 +380,7 @@ If the API returns a connection error, timeout, or `502` (usually Obsidian / the | Version | Highlights | |---------|-----------| +| **1.4.2** | **CoWork sandbox path resilience.** In a remote CoWork sandbox `${CLAUDE_PLUGIN_ROOT}` can point at a host path the sandbox can't reach (`/var/folders/…`) while the plugin is mounted under `…/mnt/.remote-plugins/…`, so script invocations failed until the agent found the real path by hand. SKILL.md and all eight slash commands now resolve the scripts dir with a fallback — prefer `${CLAUDE_PLUGIN_ROOT}`, else locate the mounted copy under `/sessions/*/mnt/.remote-plugins/*/…` — reused via `$ECHO`/`$LINT`/`$SWEEP`/`$SDIR`. On a normal host the primary path always wins (no behaviour change). `echo-health`/`echo-sweep` `allowed-tools` broadened to match the resolved invocation. (The scripts never hardcoded a path — root cause is the harness env var — but the plugin now self-heals.) | | **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 `""` 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 [--label ]` 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 `** 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. | diff --git a/echo-memory-1.4.2.plugin b/echo-memory-1.4.2.plugin new file mode 100644 index 0000000..51e1908 Binary files /dev/null and b/echo-memory-1.4.2.plugin differ diff --git a/echo-memory.plugin b/echo-memory.plugin index 43d0f10..51e1908 100644 Binary files a/echo-memory.plugin and b/echo-memory.plugin differ diff --git a/echo-memory.plugin.src/.DS_Store b/echo-memory.plugin.src/.DS_Store index 8a58d05..73d9fae 100644 Binary files a/echo-memory.plugin.src/.DS_Store and b/echo-memory.plugin.src/.DS_Store differ diff --git a/echo-memory.plugin.src/.claude-plugin/plugin.json b/echo-memory.plugin.src/.claude-plugin/plugin.json index 54801c6..eb617ec 100644 --- a/echo-memory.plugin.src/.claude-plugin/plugin.json +++ b/echo-memory.plugin.src/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "echo-memory", - "version": "1.4.1", + "version": "1.4.2", "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" diff --git a/echo-memory.plugin.src/commands/echo-doctor.md b/echo-memory.plugin.src/commands/echo-doctor.md index 9864f76..54dfd01 100644 --- a/echo-memory.plugin.src/commands/echo-doctor.md +++ b/echo-memory.plugin.src/commands/echo-doctor.md @@ -5,7 +5,9 @@ description: Check ECHO readiness — Python, vault reachability, auth, bootstra Use the **echo-memory** skill to run a one-call readiness check before relying on memory. ```bash -python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" doctor +ECHO="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" +[ -f "$ECHO" ] || ECHO=$(ls /sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/echo.py 2>/dev/null | head -1) # CoWork sandbox fallback +python3 "$ECHO" doctor # Windows: use `python` or `py -3` if `python3` is not on PATH. ``` diff --git a/echo-memory.plugin.src/commands/echo-health.md b/echo-memory.plugin.src/commands/echo-health.md index b53ab00..e6a6920 100644 --- a/echo-memory.plugin.src/commands/echo-health.md +++ b/echo-memory.plugin.src/commands/echo-health.md @@ -1,12 +1,14 @@ --- description: Run the ECHO vault-health linter and summarize any invariant violations -allowed-tools: Bash(python3 */echo-memory/scripts/vault_lint.py*), Bash(python */echo-memory/scripts/vault_lint.py*) +allowed-tools: Bash(python3 *vault_lint.py*), Bash(python *vault_lint.py*), Bash(ls /sessions/*), Bash(dirname *) --- Run the bundled, read-only vault linter and report findings. Pass the conversation's current date (`ECHO_TODAY`) so stale/aging math uses the same clock the agent writes with: ```bash -ECHO_TODAY= python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/vault_lint.py" +SDIR="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts" +[ -d "$SDIR" ] || SDIR=$(dirname "$(ls /sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/vault_lint.py 2>/dev/null | head -1)") # CoWork sandbox fallback +ECHO_TODAY= python3 "$SDIR/vault_lint.py" # Windows: use `python` or `py -3` if `python3` is not on PATH. ``` diff --git a/echo-memory.plugin.src/commands/echo-load.md b/echo-memory.plugin.src/commands/echo-load.md index 15926c8..e385057 100644 --- a/echo-memory.plugin.src/commands/echo-load.md +++ b/echo-memory.plugin.src/commands/echo-load.md @@ -5,7 +5,9 @@ description: Load ECHO memory — cold-start context read (profile, scope, lates Use the **echo-memory** skill to load memory now. Run the cold-start **Loading procedure** from `SKILL.md`: the six orientation reads (marker, operator-preferences, current-context, heartbeat, today's daily note, inbox) in **one call**, then do the load-time **reconcile** (inbox-depth + scope-drift) and surface it in a single line. ```bash -python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" load +ECHO="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" +[ -f "$ECHO" ] || ECHO=$(ls /sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/echo.py 2>/dev/null | head -1) # CoWork sandbox fallback +python3 "$ECHO" load # Windows: use `python` or `py -3` if `python3` is not on PATH. ``` diff --git a/echo-memory.plugin.src/commands/echo-recall.md b/echo-memory.plugin.src/commands/echo-recall.md index e808e28..3f93505 100644 --- a/echo-memory.plugin.src/commands/echo-recall.md +++ b/echo-memory.plugin.src/commands/echo-recall.md @@ -10,7 +10,9 @@ Use the **echo-memory** skill to recall context for: Run the one-call recall — it resolves the term against the entity index, searches, and expands one hop along `## Related` links and `source_notes`, so you get the connected web (linked decisions, people, prior sessions), not an isolated note: ```bash -python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" recall "$ARGUMENTS" +ECHO="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" +[ -f "$ECHO" ] || ECHO=$(ls /sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/echo.py 2>/dev/null | head -1) # CoWork sandbox fallback +python3 "$ECHO" recall "$ARGUMENTS" # Windows: use `python` or `py -3` if `python3` is not on PATH. ``` diff --git a/echo-memory.plugin.src/commands/echo-reflect.md b/echo-memory.plugin.src/commands/echo-reflect.md index ba9845e..daef919 100644 --- a/echo-memory.plugin.src/commands/echo-reflect.md +++ b/echo-memory.plugin.src/commands/echo-reflect.md @@ -20,6 +20,7 @@ things worth remembering across sessions, then propose them — don't write blin ```bash ECHO="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" # python3 (Windows: python / py -3) +[ -f "$ECHO" ] || ECHO=$(ls /sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/echo.py 2>/dev/null | head -1) # CoWork sandbox fallback python3 "$ECHO" reflect proposals.json # dry-run: validate + classify + preview python3 "$ECHO" reflect proposals.json --apply # write — routes each via capture (indexed, linked, logged) ``` diff --git a/echo-memory.plugin.src/commands/echo-save.md b/echo-memory.plugin.src/commands/echo-save.md index 804b41c..5b4e10c 100644 --- a/echo-memory.plugin.src/commands/echo-save.md +++ b/echo-memory.plugin.src/commands/echo-save.md @@ -11,6 +11,7 @@ Prefer the one-call **`capture`** — it routes (via the entity index), derives ```bash ECHO="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" # run with: python3 "$ECHO" ... (Windows: python / py -3) +[ -f "$ECHO" ] || ECHO=$(ls /sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/echo.py 2>/dev/null | head -1) # CoWork sandbox fallback python3 "$ECHO" capture "" <bodyfile> --kind <kind> [--aliases a,b] [--source p1,p2] python3 "$ECHO" capture "<title>" --inbox # unknown home -> idempotent inbox line ``` diff --git a/echo-memory.plugin.src/commands/echo-sweep.md b/echo-memory.plugin.src/commands/echo-sweep.md index 57fd0c0..8e49c51 100644 --- a/echo-memory.plugin.src/commands/echo-sweep.md +++ b/echo-memory.plugin.src/commands/echo-sweep.md @@ -1,13 +1,15 @@ --- description: Bring the ECHO vault up to the current feature spec (entity index + cross-links) -allowed-tools: Bash(python3 */echo-memory/scripts/sweep.py*), Bash(python */echo-memory/scripts/sweep.py*) +allowed-tools: Bash(python3 *sweep.py*), Bash(python *sweep.py*), Bash(ls /sessions/*), Bash(dirname *) --- Use the **echo-memory** skill to bring the vault up to the current spec. Run the sweep **dry-run first**, show the operator the plan, and only `--apply` on their go-ahead: ```bash -python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/sweep.py" # plan (read-only) -python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/sweep.py" --apply # write +SDIR="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts" +[ -d "$SDIR" ] || SDIR=$(dirname "$(ls /sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/sweep.py 2>/dev/null | head -1)") # CoWork sandbox fallback +python3 "$SDIR/sweep.py" # plan (read-only) +python3 "$SDIR/sweep.py" --apply # write # Windows: use `python` or `py -3` if `python3` is not on PATH. ``` diff --git a/echo-memory.plugin.src/commands/echo-triage.md b/echo-memory.plugin.src/commands/echo-triage.md index 666429a..7324602 100644 --- a/echo-memory.plugin.src/commands/echo-triage.md +++ b/echo-memory.plugin.src/commands/echo-triage.md @@ -5,7 +5,9 @@ description: Triage the ECHO inbox — route aging captures to their canonical h Use the **echo-memory** skill to run **Inbox Triage**. GET `inbox/captures/inbox.md`, list captures older than ~7 days that were never routed, and offer to route them. ```bash -python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" get inbox/captures/inbox.md +ECHO="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" +[ -f "$ECHO" ] || ECHO=$(ls /sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/echo.py 2>/dev/null | head -1) # CoWork sandbox fallback +python3 "$ECHO" get inbox/captures/inbox.md # Windows: use `python` or `py -3` if `python3` is not on PATH. ``` diff --git a/echo-memory.plugin.src/skills/.DS_Store b/echo-memory.plugin.src/skills/.DS_Store index 509b4fa..9d221e9 100644 Binary files a/echo-memory.plugin.src/skills/.DS_Store and b/echo-memory.plugin.src/skills/.DS_Store differ diff --git a/echo-memory.plugin.src/skills/echo-memory/SKILL.md b/echo-memory.plugin.src/skills/echo-memory/SKILL.md index 6132f77..dfebbfd 100644 --- a/echo-memory.plugin.src/skills/echo-memory/SKILL.md +++ b/echo-memory.plugin.src/skills/echo-memory/SKILL.md @@ -56,10 +56,13 @@ Executable logic ships under `scripts/` — **pure Python**, so the whole toolch All paths below are under `${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/`. **Invoke with `python3`** (on Windows where that isn't on PATH, use `python` or `py -3`). +**Plugin-root resolution (CoWork sandbox).** Prefer `${CLAUDE_PLUGIN_ROOT}`. In a remote CoWork sandbox that variable can point at a host path the sandbox can't reach (e.g. `/var/folders/…`); the plugin is actually mounted under `…/mnt/.remote-plugins/…`. So resolve the scripts dir once with a fallback and reuse it — the `$ECHO`/`$LINT`/`$SWEEP` snippets below already do this. The same fallback applies to `vault_lint.py`, `sweep.py`, `bootstrap.py`, and `migrate.py`. + **`scripts/echo.py` — use this for every read/write.** It centralizes auth, **HTTP-status checking** (a failed write exits non-zero instead of looking like success), one bounded retry on 5xx/connection errors, whole-line idempotent append, correct `::` heading targets, frontmatter patches, a read-back-confirmed advisory lock, and a one-call cold-start `load`. The raw `curl` recipes in `references/api-reference.md` are the underlying mechanics / *nix fallback — reach for them only if `echo.py` is unavailable, and if you do, **check the HTTP status yourself** (the PATCH-heading `400 invalid-target` failure silently loses writes otherwise). ```bash ECHO="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/echo.py" # call as: python3 "$ECHO" <cmd> ... +[ -f "$ECHO" ] || ECHO=$(ls /sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/echo.py 2>/dev/null | head -1) # CoWork sandbox fallback python3 "$ECHO" load # cold-start: the 6 orientation reads in one call python3 "$ECHO" get <path> # 404 -> exit 44 python3 "$ECHO" ls <dir> ; python3 "$ECHO" map <path> # listing / document-map @@ -292,7 +295,9 @@ On the first substantive session of a calendar month, run a quick health pass an Run the bundled linter first — it mechanically checks the invariants below so you don't eyeball them. **Pass `ECHO_TODAY` = the conversation's `currentDate`** so stale/aging math uses the same clock you write with (not the runner's machine date): ```bash -ECHO_TODAY=<currentDate> python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/vault_lint.py" +LINT="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/vault_lint.py" +[ -f "$LINT" ] || LINT=$(ls /sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/vault_lint.py 2>/dev/null | head -1) # CoWork sandbox fallback +ECHO_TODAY=<currentDate> python3 "$LINT" ``` Exit codes: `0` clean · `1` violations · `2` unreachable · `3` not bootstrapped. Checks (the linter asserts each and prints violations): @@ -312,8 +317,10 @@ Exit codes: `0` clean · `1` violations · `2` unreachable · `3` not bootstrapp After a plugin upgrade, run the sweep once to back-fill what older vaults lack — it (1) creates `_agent/index/`, (2) **rebuilds the entity index** from the notes already present, (3) **symmetrizes cross-links** (for every `## Related` A→B, adds the missing B→A; it never invents new links from body text), and (4) stamps the marker `schema_version` to current. Dry-run by default: ```bash -python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/sweep.py" # plan (read-only) -python3 "${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/sweep.py" --apply # write +SWEEP="${CLAUDE_PLUGIN_ROOT}/skills/echo-memory/scripts/sweep.py" +[ -f "$SWEEP" ] || SWEEP=$(ls /sessions/*/mnt/.remote-plugins/*/skills/echo-memory/scripts/sweep.py 2>/dev/null | head -1) # CoWork sandbox fallback +python3 "$SWEEP" # plan (read-only) +python3 "$SWEEP" --apply # write ``` Run it after `migrate.py` (which handles structural/schema changes) — or any time `/echo-health` reports index drift. It is idempotent; re-running is safe.