1. capture-update keeps the whole body (was truncating to first line) 2. frontmatter completeness: kind-default status + kind-seeded tags at capture, fm create-or-replace, incomplete-frontmatter lint, sweep backfill (one data source: KIND_STATUS/KIND_REQUIRED_FM) 3. pre-write duplicate gate (exit 76, --merge-into/--force) 4. recall corpus + ranking: sessions/journal indexed (down-weighted), BM25 x freshness x status fusion, recall --json, index schema 2 5. session hooks: SessionStart auto-load, Stop reflection nudge 6. one-tap triage verb with processing-log audit; --json on read verbs +22 mock end-to-end tests; all suites green. Docs current (README, CHANGELOG, SKILL.md, commands, references, MAINTENANCE, eval README). Drops tracked .DS_Store (gitignored); retires README-gretchen.md (moved to gitignored dist/); adds the field report that drove item 2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
10 KiB
ECHO — Obsidian Local REST API Reference
Server: the configured endpoint — the endpoint from ~/.claude/echo-memory/config.json (overridable with ECHO_BASE), a reverse proxy → backend Obsidian Local REST API. Examples below use $ECHO_BASE (or the neutral placeholder https://obsidian.example.com).
Auth header: Authorization: Bearer $ECHO_KEY — export ECHO_KEY=<token> before running these recipes, or let echo.py resolve it automatically (per-field, first wins: env override ECHO_KEY → the key in ~/.claude/echo-memory/config.json, resolved via the echo_config module). The literal key is never stored in docs.
A configured endpoint should present a valid TLS certificate — -k is not required. Paths address the vault at its root.
Prefer
scripts/echo.pyover the raw recipes below. It wraps every verb with auth, status checking, retry, idempotent append, and frontmatter patches, and runs on any platform with a Python interpreter. Thecurlrecipes here are the underlying mechanics and a *nix/bash fallback (they use heredocs and--data-binary); on Windows, preferecho.pyor an equivalent. If you callcurldirectly, check the HTTP status — add-o /dev/null -w "%{http_code}"and branch on it. APATCHto a non-existent heading returns400 invalid-target(errorCode 40080) and the write is silently lost; a barecurlthat ignores status will report success anyway.GETreturns404for a missing file. Treat any>= 400as a failed operation, surface it, and do not continue as if it succeeded.
Client performance (echo.py) — pooling & concurrency
echo.request() keeps one persistent keep-alive connection per thread and reuses it across calls, instead of opening a fresh TCP+TLS connection per request. Against a remote HTTPS endpoint the repeated handshake was the dominant cost, so a full-vault pass used to make hundreds of serial handshakes and exceed the agent/tool timeout (which kills the run mid-flight). A stale pooled connection reconnects transparently; the (status, body) contract is unchanged (status 0 still means transport failure, so the offline write-queue still triggers).
For bulk reads, echo.read_many(paths) fans GETs across a thread pool and returns {path: text_or_None} — resilient (one unreadable file becomes None rather than aborting the batch). sweep.py and vault_lint.py prefetch the whole vault once with it and share that single cache across every pass, so no note is fetched more than once. Net effect: a several-minute full-vault sweep becomes sub-second on a few-hundred-note vault.
Tuning knobs (env overrides):
ECHO_TIMEOUT(default30) — per-request socket timeout in seconds. With concurrency, one slow file only blocks its own worker, not the run.ECHO_WORKERS(default8) —read_manythread-pool size. Raise it for very large vaults; lower it to be gentler on the backend.
For a vault large enough that even the concurrent pass approaches the tool timeout, run the script with the harness's background-execution option rather than blocking the turn.
Reading Files
# Read any file by vault path
curl -s \
-H "Authorization: Bearer $ECHO_KEY" \
"$ECHO_BASE/vault/_agent/context/current-context.md"
Returns raw file content (text/markdown). On 404, the file does not exist.
# Read a specific heading's content only
curl -s \
-H "Authorization: Bearer $ECHO_KEY" \
"$ECHO_BASE/vault/_agent/memory/semantic/operator-preferences.md/heading/Operator"
Nested headings: separate levels with :: (URL-encode spaces as %20):
/vault/path/to/note.md/heading/Work%3A%3AMeetings
Listing Directories
# List contents of a directory (trailing slash required)
curl -s \
-H "Authorization: Bearer $ECHO_KEY" \
"$ECHO_BASE/vault/_agent/sessions/"
Returns JSON: { "files": [...], "folders": [...] }.
Appending Content (POST)
POST appends to the end of an existing file. Creates the file if it doesn't exist.
cat > /tmp/obs_entry.md << 'OBSEOF'
- 2026-06-05: your entry here
OBSEOF
curl -s -X POST \
-H "Authorization: Bearer $ECHO_KEY" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_entry.md \
"$ECHO_BASE/vault/inbox/captures/inbox.md"
Creating or Overwriting Files (PUT)
PUT creates a new file or fully overwrites an existing one. Intermediate directories are created automatically.
cat > /tmp/obs_file.md << 'OBSEOF'
---
type: session-log
status: complete
created: 2026-06-05
updated: 2026-06-05
tags: [agent, session]
agent_written: true
source_notes: []
---
# content here
## Related
- [[projects/active/some-project]]
OBSEOF
curl -s -X PUT \
-H "Authorization: Bearer $ECHO_KEY" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_file.md \
"$ECHO_BASE/vault/_agent/sessions/2026-06-05-1430-my-session.md"
Patching a Specific Section (PATCH)
PATCH edits inside a file without rewriting it. Target and operation are set via headers.
Append under a heading
Heading targets must be the FULL heading path, ::-delimited from the top-level heading. A bare subheading name returns 400 invalid-target (errorCode 40080). Example: ## Fact / Pattern nested under # Operator Preferences → Target: Operator Preferences::Fact / Pattern. Percent-encode non-ASCII characters (e.g. H%C3%A9llo); spaces are fine.
cat > /tmp/obs_patch.md << 'OBSEOF'
The operator prefers concise status updates — lead with the decision.
OBSEOF
curl -s -X PATCH \
-H "Authorization: Bearer $ECHO_KEY" \
-H "Operation: append" \
-H "Target-Type: heading" \
-H "Target: Operator Preferences::Fact / Pattern" \
-H "Content-Type: text/markdown" \
--data-binary @/tmp/obs_patch.md \
"$ECHO_BASE/vault/_agent/memory/semantic/operator-preferences.md"
Discover heading / block / frontmatter targets
When unsure of the exact heading path, GET the note with the document-map Accept header:
curl -s \
-H "Authorization: Bearer $ECHO_KEY" \
-H "Accept: application/vnd.olrapi.document-map+json" \
"$ECHO_BASE/vault/_agent/memory/semantic/operator-preferences.md"
Returns { "headings": [...], "blocks": [...], "frontmatterFields": [...] }. Copy the heading string verbatim into Target.
Replace a heading's content entirely
Same call with Operation: replace — e.g. to refresh a project's Project Name::Status.
Prepend under a heading
Same call with Operation: prepend.
Patch a frontmatter field
A
PATCHTarget-Type: frontmatterreplace on a key the note does not have returns400 invalid-target— the raw API cannot create a key.echo.py fm(v1.5+) handles this: on that 400 it surgically inserts the onefield: valueline into the frontmatter block and re-PUTs (verified), leaving every other line untouched. Raw-curl users must GET-edit-PUT themselves — carefully (a naive rewrite is how frontmatter gets clobbered).
curl -s -X PATCH \
-H "Authorization: Bearer $ECHO_KEY" \
-H "Operation: replace" \
-H "Target-Type: frontmatter" \
-H "Target: updated" \
-H "Content-Type: application/json" \
--data '"2026-06-05"' \
"$ECHO_BASE/vault/projects/active/vault-foundation.md"
Searching
curl -s -X POST \
-H "Authorization: Bearer $ECHO_KEY" \
"$ECHO_BASE/search/simple/?query=weekly+review"
Returns an array of { filename, score, matches: [{ context, match }] }.
Deleting Files
curl -s -X DELETE \
-H "Authorization: Bearer $ECHO_KEY" \
"$ECHO_BASE/vault/inbox/imports/old-note.md"
Only on explicit operator request. Deletion is destructive.
URL-Encoding Notes
- Path separators (
/) in the vault path are literal — do not encode them. - Spaces in filenames or heading targets in a URL: use
%20. - Nested heading levels in a URL path: use
%3A%3Afor::. - Heading text in the
Target:header: the full heading path joined by::(e.g.Operator Preferences::Fact / Pattern), no#; spaces are fine; percent-encode non-ASCII.
Memory Routing Map
| Situation | Vault path | Method |
|---|---|---|
| Quick capture / unsorted | inbox/captures/inbox.md (date-prefixed line) |
POST |
| Raw imported material | inbox/imports/ |
PUT |
| Operator preference / durable fact | _agent/memory/semantic/operator-preferences.md |
PATCH |
| Other durable facts, patterns | _agent/memory/semantic/<slug>.md |
PUT |
| Event record (what happened) | _agent/memory/episodic/<slug>.md |
PUT |
| Short-lived, time-boxed state | _agent/memory/working/<slug>.md |
PUT |
| Task-scoped context / focus | _agent/context/current-context.md |
PATCH / PUT |
| Working-session log | _agent/sessions/YYYY-MM-DD-HHMM-<slug>.md |
PUT |
| Long-running project state | projects/<lifecycle>/<slug>.md (lifecycle: incubating → active → on-hold/archived; folder and status: MUST agree) |
PUT + PATCH |
| Ongoing area of responsibility (standing domain, no end state) | areas/<domain>/<slug>.md (<domain>: business/personal/learning/systems) |
PUT |
| Non-obvious decision (ADR) | decisions/by-date/YYYY-MM-DD-<slug>.md (mirror only into an existing project note's ## Key Decisions; otherwise skip) |
PUT |
| Person context | resources/people/<name>.md |
PUT / PATCH |
| Company / organization context | resources/companies/<slug>.md |
PUT / PATCH |
| Concept / reference note | resources/concepts/ or resources/references/ |
PUT |
| Meeting notes / call recap | resources/meetings/YYYY-MM-DD-<slug>.md |
PUT |
| Skill / plugin capability entry (catalog, not build work) | _agent/skills/active/<slug>.md (→ archived/ when retired) |
PUT |
| Daily activity / Agent Log | journal/daily/YYYY-MM-DD.md |
POST / PATCH |
| Journal rollup | journal/{weekly/YYYY-Www,monthly/YYYY-MM,quarterly/YYYY-Qn,annual/YYYY}.md (weekly = opt-in on first session of a new ISO week; monthly = offered with Vault Health; quarterly/annual = manual) |
PUT |
| Vault-health audit (agent self-maintenance) | _agent/health/YYYY-MM-vault-health.md (monthly; NOT a journal entry) |
PUT |
| Session-end orientation pointer | _agent/heartbeat/last-session.md (one line, overwritten each session end) |
PUT |
| Bootstrap marker (plugin-owned) | _agent/echo-vault.md (schema_version, bootstrap date) — the "is this vault set up?" probe |
GET / PUT |
Slug rules: kebab-case, ASCII, ~40 chars max. Every file carries canonical frontmatter (see vault-layout.md).