be3fd36ebf
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>
238 lines
10 KiB
Markdown
238 lines
10 KiB
Markdown
# 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.py` over 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. The `curl` recipes here are the underlying mechanics and a **\*nix/bash fallback** (they use heredocs and `--data-binary`); on Windows, prefer `echo.py` or an equivalent. **If you call `curl` directly, check the HTTP status** — add `-o /dev/null -w "%{http_code}"` and branch on it. A `PATCH` to a non-existent heading returns `400 invalid-target` (errorCode 40080) and the write is *silently lost*; a bare `curl` that ignores status will report success anyway. `GET` returns `404` for a missing file. Treat any `>= 400` as 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` (default `30`) — per-request socket timeout in seconds. With concurrency, one slow file only blocks its own worker, not the run.
|
|
- `ECHO_WORKERS` (default `8`) — `read_many` thread-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
|
|
|
|
```bash
|
|
# 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.
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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.
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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 `PATCH` `Target-Type: frontmatter` **replace** on a key the note does not have returns
|
|
> `400 invalid-target` — the raw API cannot create a key. `echo.py fm` (v1.5+) handles this:
|
|
> on that 400 it surgically inserts the one `field: value` line 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).
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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%3A` for `::`.
|
|
- 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`).
|