From 8f1d96c0a5b028be330c50fdfe2fbbeb7173c9bd Mon Sep 17 00:00:00 2001 From: Ori Nachum Date: Tue, 23 Jun 2026 09:29:52 +0300 Subject: [PATCH] eidetic-memory: ### Added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **Vendored the `remember` + `recall` memory skills from eidetic-cli** (cite-don't-import) — the write/read halves of eidetic's shared `~/.eidetic/memory` surface, so this agent (Claude and its colleague backend) can persist facts across sessions and recall them later, sharing one store. `remember` drives `eidetic remember` (idempotent upsert of one JSON record or an NDJSON batch on stdin, dedup by id + content hash); `recall` drives `eidetic recall` with four search modes — exact / approximate / keyword / hybrid — each hit carrying text, full provenance metadata, a relevance score, and a freshness signal. The `.sh` wrappers are byte-verbatim from eidetic-cli (their first-party origin); each `SKILL.md` is localized only in the illustrative `--scope ` examples (Provenance keeps "First-party to eidetic-cli"). Both default to this agent's PRIVATE scope, reading the suffix from `culture.yaml`. Runtime dep: the `eidetic` CLI on PATH (else a local eidetic-cli checkout with `uv`). Propagated by rollout-cli's `eidetic-memory` recipe. --- .claude/skills/recall/SKILL.md | 181 ++++++++++++++++++++ .claude/skills/recall/scripts/recall.sh | 141 +++++++++++++++ .claude/skills/remember/SKILL.md | 118 +++++++++++++ .claude/skills/remember/scripts/remember.sh | 138 +++++++++++++++ CHANGELOG.md | 20 +++ pyproject.toml | 2 +- 6 files changed, 599 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/recall/SKILL.md create mode 100755 .claude/skills/recall/scripts/recall.sh create mode 100644 .claude/skills/remember/SKILL.md create mode 100755 .claude/skills/remember/scripts/remember.sh diff --git a/.claude/skills/recall/SKILL.md b/.claude/skills/recall/SKILL.md new file mode 100644 index 0000000..04414fb --- /dev/null +++ b/.claude/skills/recall/SKILL.md @@ -0,0 +1,181 @@ +--- +name: recall +type: command +description: > + Search the shared eidetic memory store and get back ranked, provenanced + records. Drives `eidetic recall` with four search modes — exact (verbatim + substring), approximate (vector/semantic), keyword (BM25 lexical), and hybrid + (a weighted blend of vector+keyword, the default) — each hit carrying its + text, full metadata, a relevance `score`, and a freshness `signal`. Recall + passively reinforces matched records (bumps last_recall + recall_count). + Shadowed and archived records are excluded by default; use + --include-shadowed / --include-archived to retrieve them. The store lives at + ~/.eidetic/memory (a home-dir path outside any git worktree); the wrapper + defaults queries to this agent's PERSONAL, PRIVATE scope (`--scope tensor-cli + --visibility private`, suffix read from culture.yaml) — matching where + /remember writes — so a no-flag recall returns this agent's own private records + plus the shared public pool, and Claude and the colleague backend recall each + other's memories because both resolve the same suffix via this skill. Use + when the user says "recall", "what do we know about X", "search memory", + "have we seen X before", "look it up in memory", "eidetic recall", or before + answering from scratch when prior context may already be stored. Pairs with + the sibling /remember skill. +--- + +# recall — search the shared eidetic memory + +`recall` drives **`eidetic recall`**: given a query, it returns the top-k stored +records ranked by relevance, each with its `text`, full `metadata` (provenance), +a numeric `score`, and a freshness `signal`. It is the read half of the memory +surface; the write half is the sibling **/remember** skill. + +The point of a *shared* store is that memory is a **team faculty**, not a +per-agent silo: a record Claude wrote is recallable by the colleague backend +(and vice versa), because both resolve the same `~/.eidetic/memory` path. + +## How to run + +```bash +bash .claude/skills/recall/scripts/recall.sh "" [flags...] +``` + +The wrapper resolves the CLI portably (installed `eidetic` on `PATH`, else +`uv run eidetic` from the checkout) and forwards every flag verbatim, so it is +exactly `eidetic recall …`. Run it from anywhere; the store is the same. + +## Search modes (`--mode`, default `hybrid`) + +| Mode | What it matches | Needs embed server? | +|------|-----------------|---------------------| +| `exact` | case-insensitive verbatim substring (`--case-sensitive` to tighten) | no — offline-safe | +| `approximate` | vector cosine / semantic similarity | yes (falls back offline) | +| `keyword` | BM25 lexical; only records sharing a query term | no — offline-safe | +| `hybrid` | `alpha*approximate + (1-alpha)*keyword` (`--alpha`, default 0.5) | uses it when up | + +`hybrid` is the default because the two signals cover each other's blind spots: +vector catches paraphrases, keyword catches exact ids/quotes. When the embed +server is unreachable, `hybrid` collapses to keyword-only (it never fuses +meaningless offline-fallback cosine). + +## Output fields + +Each hit in `--json` output includes: + +| Field | Notes | +|-------|-------| +| `id` | stable record identity | +| `text` | the stored chunk | +| `type` | record type | +| `metadata` | full provenance, round-tripped verbatim from ingest | +| `score` | relevance score from the chosen search mode (freshness-blended) | +| `signal` | freshness strength in [0, 1]; computed at recall time from age, recall frequency, and staleness | +| `created` | ISO-8601 ingest date (may be DATE_UNKNOWN for legacy records) | +| `last_recall` | ISO-8601 timestamp of the most recent recall hit (null if never recalled) | +| `recall_count` | number of times this record has been recalled (passive reinforcement counter) | +| `lifecycle` | `active`, `shadowed`, or `archived` | +| `links` | list of related-memory ids | + +## Freshness signal + +Every `recall` hit carries a `signal` field (float in `[0, 1]`). The signal +blends **multiplicatively** into the lexical/vector score so recently-created +and frequently-recalled records surface ahead of stale ones. The formula: + +``` +access_bonus = min(0.5, recall_count * 0.05) +age_factor = 1 / (1 + days_since_creation * 0.01) +staleness = days_since_last_recall * 0.01 +signal = clamp((0.5 - staleness + access_bonus) * age_factor, 0, 1) +blended_score = score * (1 + 0.25 * (signal - 0.5)) +``` + +Records with no temporal data (legacy, undated) are an exact no-op — the blend +is skipped for them so pre-existing fixture scores are unchanged. + +Each `recall` call is also **passive reinforcement**: it bumps `last_recall` and +`recall_count` on every matched record, so frequently-recalled memories organically +gain signal strength over time. + +## Lifecycle flags + +By default, `recall` returns only `active` records. Use these flags to retrieve +non-active records: + +- `--include-shadowed` — include records whose `lifecycle == "shadowed"` (records + superseded within their scope by a newer record). Shadowed records are preserved + and still searchable; they are just hidden from the default result set. +- `--include-archived` — include records whose `lifecycle == "archived"` (records + older than ~1 year or below the signal threshold). Archived records are fully + preserved; the flag makes them retrievable again. + +Both flags can be combined. Neither affects ranking — shadowed/archived records +compete on score/signal just like active ones when included. + +## Common flags (forwarded to `eidetic recall`) + +- `--mode exact|approximate|keyword|hybrid` — default `hybrid`. +- `--top-k N` — max results (default 5). +- `--alpha F` — hybrid blend weight in `[0,1]` (default 0.5). +- `--case-sensitive` — for `--mode exact`. +- `--filter KEY=VALUE` — metadata facet filter (repeatable): e.g. `--filter source=docs`. +- `--scope NAME` / `--visibility public|private` — scope isolation (no private + leak). **The wrapper defaults this to the agent's PERSONAL, PRIVATE scope** + (`--scope tensor-cli --visibility private`, suffix read from `culture.yaml`), + matching where `/remember` writes — so a no-flag recall returns this agent's + own private records **plus** the shared public pool, while those private records + stay invisible to a `default`/other-scope recall. Pass `--scope`/`--visibility` + to query elsewhere; a wheel install with no `culture.yaml` falls back to the + CLI default `default`/`public`. +- `--backend files|mongo|neo4j` — default `files` (the shared home-dir store). +- `--include-shadowed` — include shadowed records in results (excluded by default). +- `--include-archived` — include archived records in results (excluded by default). +- `--json` — structured list to stdout (use this when an agent parses the result). + +## Examples + +```bash +# Default hybrid recall, JSON for an agent to parse: +bash .claude/skills/recall/scripts/recall.sh "jetson nano power draw" --json + +# Find the exact message that mentions a phrase: +bash .claude/skills/recall/scripts/recall.sh "Orin Nano" --mode exact + +# Keyword search, offline-safe, narrowed to a source: +bash .claude/skills/recall/scripts/recall.sh "thermal throttle" --mode keyword \ + --filter source=discord --top-k 10 + +# Retrieve a record that was recently shadowed (its superseding record is now active): +bash .claude/skills/recall/scripts/recall.sh "old topic" --include-shadowed --json + +# Retrieve all records including archived (to audit stale memories): +bash .claude/skills/recall/scripts/recall.sh "power" --include-archived --include-shadowed --json +``` + +## Notes + +- **Provenance is mandatory** on every hit — recall is for *cited* answers. +- The embed endpoint defaults to the local model-gear embed gear + (`http://localhost:8002/v1`, model `Qwen/Qwen3-Embedding-0.6B`); override with + `EIDETIC_EMBED_URL` / `EIDETIC_EMBED_MODEL`. `exact`/`keyword` ignore it. +- **Use the wrapper, not a bare `eidetic`.** The console script may not be on + `PATH` (in a dev checkout it isn't) — the wrapper resolves it for you (`PATH` + first, else `uv run eidetic`). For the docs, run `eidetic explain recall` if + installed, otherwise `uv run --project eidetic explain + recall`. (`explain` is an **`eidetic`** verb — a sibling tool like `devex` + won't know it.) +- **Reading scores:** `exact`, `keyword`, and `hybrid` drop non-matching records + (hybrid drops any record with a `0.0` blended score), so their hits are real + matches. `approximate` keeps every candidate ranked by raw cosine, so it can + return low/near-zero scores when the store is small — lower `--top-k` to trim. + A `--min-score` threshold is a tracked follow-up. +- **Sharing scope = one OS user.** The default store is `~/.eidetic/memory`, so + every agent/process running as the *same* OS user shares it (that is the point — + Claude + colleague). It is not isolated between OS users by anything but file + permissions; keep genuinely private data in a `--visibility private` scope and + treat the host as the trust boundary. + +## Provenance + +First-party to **eidetic-cli** — eidetic owns its memory surface. Cite, don't +import: downstream repos copy this skill, they don't symlink it. See +[`docs/skill-sources.md`](../../../docs/skill-sources.md). diff --git a/.claude/skills/recall/scripts/recall.sh b/.claude/skills/recall/scripts/recall.sh new file mode 100755 index 0000000..9fe9741 --- /dev/null +++ b/.claude/skills/recall/scripts/recall.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# recall.sh — search the shared eidetic memory store (the /recall skill). +# +# Thin, portable wrapper around `eidetic recall`. It resolves the CLI, points +# the embedding modes at the local model-gear embed gear (overridable), and +# forwards every flag verbatim — so `recall.sh "" --mode hybrid --json` +# is exactly `eidetic recall "" --mode hybrid --json`. +# +# The store is the files backend at ~/.eidetic/memory by default — a home-dir +# path OUTSIDE any git worktree, so Claude and the colleague backend (which runs +# in throwaway worktrees) read the SAME memories. Set EIDETIC_DATA_DIR to opt out +# of sharing; set EIDETIC_MONGO_URI / NEO4J_URI + --backend for a server store. + +set -euo pipefail + +# ── resolve the eidetic CLI (installed tool first, then dev checkout) ──────── +EIDETIC=() +resolve_eidetic() { + if command -v eidetic >/dev/null 2>&1; then + EIDETIC=(eidetic) # installed console script — the normal case + return 0 + fi + # Dev fallback: inside the eidetic-cli checkout, run via uv. + local dir + dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + while [ -n "$dir" ] && [ "$dir" != "/" ]; do + if [ -f "$dir/pyproject.toml" ] \ + && grep -q '^name = "eidetic-cli"' "$dir/pyproject.toml" 2>/dev/null; then + if command -v uv >/dev/null 2>&1; then + EIDETIC=(uv run --project "$dir" eidetic) + return 0 + fi + break + fi + dir=$(dirname "$dir") + done + cat >&2 <<'EOF' +error: eidetic CLI not found. +hint: install it with `uv tool install eidetic-cli` (or `pipx install eidetic-cli`), + or run from inside the eidetic-cli checkout with `uv` available. + The console script is `eidetic` (dist name: eidetic-cli). +EOF + return 1 +} + +usage() { + cat <<'EOF' +recall.sh — search the shared eidetic memory store (the /recall skill). + +Usage: + recall.sh "" [--mode exact|approximate|keyword|hybrid] [--top-k N] \ + [--alpha F] [--case-sensitive] [--filter KEY=VALUE]... \ + [--backend files|mongo|neo4j] [--scope NAME] [--visibility public|private] \ + [--json] + +Modes (default: hybrid): + exact case-insensitive verbatim substring (--case-sensitive to tighten); offline-safe + approximate vector cosine / semantic similarity (uses the embed server) + keyword BM25 lexical; only records sharing a query term; offline-safe + hybrid alpha*approximate + (1-alpha)*keyword (--alpha, default 0.5); + degrades to keyword-only when the embed server is offline + +Every flag is forwarded verbatim to `eidetic recall`. See `eidetic explain recall`. +EOF +} + +case "${1:-}" in + -h | --help | help | "") + usage + exit 0 + ;; +esac + +resolve_eidetic || exit 2 + +# ── default to this agent's PERSONAL, PRIVATE scope (culture.yaml `suffix`) ── +# Query this agent's OWN personal scope by default, matching where /remember +# writes, instead of the global `default` scope shared by every project on this +# host. We read the `suffix` from the nearest culture.yaml (walking up from this +# script), so the scope follows the repo identity rather than being hard-coded — +# a downstream cite-don't-import copy adapts to its own suffix, and the colleague +# backend (running in a worktree of this same repo) resolves the same suffix, +# keeping the Claude↔colleague shared-memory story intact. +# +# The personal scope is PRIVATE by default to match /remember: in eidetic's model +# a private record is served only to a recall in the SAME scope (`can_serve`), so +# querying with --scope --visibility private is what retrieves those +# isolated records (a public/default recall can't see them). Scope and visibility +# are paired — the private default applies only when we inject the resolved scope, +# and only if the caller didn't pass --visibility (so an explicit +# `--visibility public` still wins). An explicit --scope on the command line takes +# over steering entirely; a wheel install with no culture.yaml falls back to the +# plain CLI default (`default`/`public`). +resolve_scope() { + local dir suffix="" + dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + while [ -n "$dir" ] && [ "$dir" != "/" ]; do + if [ -f "$dir/culture.yaml" ]; then + # Capture only the first non-space token after `suffix:` (so an + # inline `# comment` or trailing space can't bleed into the scope), + # then strip surrounding quotes only — matching the canonical parser + # in .claude/skills/cicd/scripts/_resolve-nick.sh. + suffix=$(sed -n \ + 's/^[[:space:]]*-\{0,1\}[[:space:]]*suffix:[[:space:]]*\([^[:space:]]*\).*/\1/p' \ + "$dir/culture.yaml" | head -n1 | tr -d "\"'") + break + fi + dir=$(dirname "$dir") + done + printf '%s' "$suffix" +} + +has_flag() { + local needle=$1 + shift + local a + for a in "$@"; do + case "$a" in + "$needle" | "$needle"=*) return 0 ;; + esac + done + return 1 +} + +SCOPE_ARGS=() +if ! has_flag --scope "$@"; then + EIDETIC_SCOPE=$(resolve_scope) + if [ -n "$EIDETIC_SCOPE" ]; then + SCOPE_ARGS+=(--scope "$EIDETIC_SCOPE") + has_flag --visibility "$@" || SCOPE_ARGS+=(--visibility private) + fi +fi + +# Default the embedding endpoint to the local model-gear embed gear. eidetic +# falls back to a deterministic offline embedding if it's unreachable, so this +# is safe even when the gear is down. Override by exporting these yourself. +: "${EIDETIC_EMBED_URL:=http://localhost:8002/v1}" +: "${EIDETIC_EMBED_MODEL:=Qwen/Qwen3-Embedding-0.6B}" +export EIDETIC_EMBED_URL EIDETIC_EMBED_MODEL + +exec "${EIDETIC[@]}" recall "${SCOPE_ARGS[@]}" "$@" diff --git a/.claude/skills/remember/SKILL.md b/.claude/skills/remember/SKILL.md new file mode 100644 index 0000000..73feae5 --- /dev/null +++ b/.claude/skills/remember/SKILL.md @@ -0,0 +1,118 @@ +--- +name: remember +type: command +description: > + Ingest records into the shared eidetic memory store so they can be recalled + later. Drives `eidetic remember`: accepts one record as a JSON object, or a + batch as NDJSON on stdin for bulk ingest. Upsert is idempotent by id (and + dedups by content hash) — re-remembering updates in place, never duplicates. + Stamps a `created` date on every record at ingest time. Accepts `supersedes` + (id of the record this one replaces, for within-scope shadowing via `sweep`) + and `links` (list of related-memory ids). The store lives at + ~/.eidetic/memory (a home-dir path outside any git worktree), and the wrapper + defaults records to this agent's PERSONAL, PRIVATE scope (`--scope tensor-cli + --visibility private`, suffix read from culture.yaml) so they don't leak to a + default/other-scope recall — Claude and the colleague backend still share them + because both resolve the same suffix via this skill. Pass `--visibility public` + to contribute to the shared public pool instead. Use when the user says + "remember this", "store this", "save to memory", "index these", "eidetic + remember", or when something learned this session should outlive it. Pairs with + the sibling /recall skill. +--- + +# remember — write to the shared eidetic memory + +`remember` drives **`eidetic remember`**, the write half of the memory surface +(the read half is the sibling **/recall** skill). Records you store here are +recallable later by *any* agent on this machine — Claude or the colleague +backend — because the default store is one shared `~/.eidetic/memory` path. + +## How to run + +```bash +# One record (JSON object as the argument): +bash .claude/skills/remember/scripts/remember.sh \ + '{"id":"d1","text":"Orin Nano draws 7-15W","type":"docs","metadata":{"source":"docs","permalink":"https://..."}}' --json + +# Batch (NDJSON on stdin, one record per line) — for bulk re-index: +cat records.ndjson | bash .claude/skills/remember/scripts/remember.sh --json + +# Record that supersedes an older one (same scope required for sweep to shadow): +bash .claude/skills/remember/scripts/remember.sh \ + '{"id":"r2","text":"Updated Orin Nano draw: 10-20W","type":"note","supersedes":"r1","links":["r3"]}' --json +``` + +The wrapper resolves the CLI portably (installed `eidetic` on `PATH`, else +`uv run eidetic` from the checkout) and forwards every flag verbatim. + +## Record shape + +| Field | Required? | Notes | +|-------|-----------|-------| +| `id` | yes | stable identity; the upsert key | +| `text` | yes | the chunk being remembered | +| `type` | yes | e.g. `note`, `docs`, `discord`, a research object type | +| `hash` | optional | content hash for dedup; derived from `text` when omitted | +| `metadata` | recommended | provenance + facets; **round-trips verbatim** on recall | +| `created` | auto-stamped | ISO-8601 UTC date; stamped at ingest if absent; drives freshness signal age-decay | +| `supersedes` | optional | id of an earlier same-scope record this one replaces; `sweep` auto-shadows the target | +| `links` | optional | list of related-memory ids; persisted for future corroboration scoring | + +`score` and `signal` are recall-only and are ignored on ingest. **Mind the +scope:** the default personal scope is **private** (`--scope tensor-cli +--visibility private`), so personal/role-gated notes stay isolated to this +agent's recall and are safe to store. Only when you deliberately write to a +**public** scope (`--visibility public`) does the record enter the shared pool +visible to every scope — keep public-scope records to public data only. + +## Idempotency + +Re-submitting a record with the same `id` overwrites the previous value; a record +with a matching content `hash` is de-duplicated. So re-running an ingest (e.g. a +periodic re-scan) is safe and will not create duplicates. + +## Lifecycle — supersedes and sweep + +Setting `supersedes` on a record declares that this record replaces an earlier one +**within the same scope**. The actual lifecycle transition (marking the older record +as `shadowed`) is applied by `eidetic sweep`, not by `remember` itself. Cross-scope +`supersedes` links are recorded but never auto-shadow (preserving the +public/private no-leak invariant). + +To apply pending transitions after ingesting superseding records: + +```bash +eidetic sweep --dry-run # preview what would change +eidetic sweep # apply transitions +``` + +## Flags (forwarded to `eidetic remember`) + +- `--json` — structured result (`{"upserted": N, "ids": [...]}`) to stdout. +- `--scope NAME` / `--visibility public|private` — record scope. **The wrapper + defaults this to the agent's PERSONAL, PRIVATE scope** — `--scope + --visibility private`, where `` is read from the nearest `culture.yaml` + (here, `tensor-cli`). Private records are served only to a recall in the same + scope, so they don't leak to a `default`/other-scope query. Pass `--scope` to + steer to a different scope (which then uses the plain CLI default visibility), + or `--visibility public` to keep the personal scope but make it shared. A wheel + install with no `culture.yaml` falls back to the CLI default `default`/`public`. +- `--backend files|mongo|neo4j` — default `files` (the shared home-dir store); + use `mongo`/`neo4j` (with `EIDETIC_MONGO_URI` / `NEO4J_URI`) for a server store. + +## Notes + +- The embed endpoint defaults to the local model-gear embed gear + (`http://localhost:8002/v1`); override with `EIDETIC_EMBED_URL` / + `EIDETIC_EMBED_MODEL`. Ingest still works offline (embeddings are recomputed at + recall time). +- **Use the wrapper, not a bare `eidetic`.** The console script may not be on + `PATH` (in a dev checkout it isn't); the wrapper resolves it (`PATH` first, else + `uv run eidetic`). For the docs, run `eidetic explain remember` if installed, + otherwise `uv run --project eidetic explain remember`. + +## Provenance + +First-party to **eidetic-cli** — eidetic owns its memory surface. Cite, don't +import: downstream repos copy this skill, they don't symlink it. See +[`docs/skill-sources.md`](../../../docs/skill-sources.md). diff --git a/.claude/skills/remember/scripts/remember.sh b/.claude/skills/remember/scripts/remember.sh new file mode 100755 index 0000000..164736e --- /dev/null +++ b/.claude/skills/remember/scripts/remember.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# remember.sh — ingest records into the shared eidetic memory store (the /remember skill). +# +# Thin, portable wrapper around `eidetic remember`. It resolves the CLI, points +# the embedding endpoint at the local model-gear embed gear (overridable), and +# forwards every argument verbatim. Accepts ONE record as a JSON object argument, +# or a BATCH as NDJSON on stdin (one JSON object per line) for bulk ingest. +# +# remember.sh '{"id":"d1","text":"...","type":"docs","metadata":{...}}' --json +# cat records.ndjson | remember.sh --json +# +# Upsert is idempotent by id (and dedups by content hash): re-remembering the +# same record updates it in place, never duplicates. +# +# The store is the files backend at ~/.eidetic/memory by default — a home-dir +# path OUTSIDE any git worktree, so a record Claude remembers is recallable by +# the colleague backend (which runs in throwaway worktrees), and vice versa. +# Set EIDETIC_DATA_DIR to opt out of sharing; use --backend mongo|neo4j (with +# EIDETIC_MONGO_URI / NEO4J_URI) for a server-backed shared store. + +set -euo pipefail + +# ── resolve the eidetic CLI (installed tool first, then dev checkout) ──────── +EIDETIC=() +resolve_eidetic() { + if command -v eidetic >/dev/null 2>&1; then + EIDETIC=(eidetic) + return 0 + fi + local dir + dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + while [ -n "$dir" ] && [ "$dir" != "/" ]; do + if [ -f "$dir/pyproject.toml" ] \ + && grep -q '^name = "eidetic-cli"' "$dir/pyproject.toml" 2>/dev/null; then + if command -v uv >/dev/null 2>&1; then + EIDETIC=(uv run --project "$dir" eidetic) + return 0 + fi + break + fi + dir=$(dirname "$dir") + done + cat >&2 <<'EOF' +error: eidetic CLI not found. +hint: install it with `uv tool install eidetic-cli` (or `pipx install eidetic-cli`), + or run from inside the eidetic-cli checkout with `uv` available. + The console script is `eidetic` (dist name: eidetic-cli). +EOF + return 1 +} + +usage() { + cat <<'EOF' +remember.sh — ingest records into the shared eidetic memory store (the /remember skill). + +Usage: + remember.sh '' [--json] [--backend files|mongo|neo4j] \ + [--scope NAME] [--visibility public|private] + cat records.ndjson | remember.sh [--json] ... + +A record needs `id`, `text`, and `type`; `hash` and `metadata` are recommended +(hash is derived from text when omitted). Upsert is idempotent by id. +Public data only. Every flag is forwarded verbatim to `eidetic remember`. +See `eidetic explain remember`. +EOF +} + +case "${1:-}" in + -h | --help | help) + usage + exit 0 + ;; +esac + +resolve_eidetic || exit 2 + +# ── default to this agent's PERSONAL, PRIVATE scope (culture.yaml `suffix`) ── +# A record this agent remembers should land in its OWN personal scope, not the +# global `default` scope shared by every project on this host. We read the +# `suffix` from the nearest culture.yaml (walking up from this script), so the +# scope follows the repo identity rather than being hard-coded — a downstream +# cite-don't-import copy adapts to its own suffix, and the colleague backend +# (running in a worktree of this same repo) resolves the same suffix, keeping +# the Claude↔colleague shared-memory story intact. +# +# The personal scope is PRIVATE by default: in eidetic's model only a private +# record is isolated to its scope (`can_serve`), so private is what actually +# keeps these records from leaking to a default/other-scope recall. Scope and +# visibility are paired — the private default applies only when we inject the +# resolved scope, and only if the caller didn't pass --visibility (so an +# explicit `--visibility public` still wins). An explicit --scope on the command +# line takes over steering entirely; a wheel install with no culture.yaml falls +# back to the plain CLI default (`default`/`public`). +resolve_scope() { + local dir suffix="" + dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + while [ -n "$dir" ] && [ "$dir" != "/" ]; do + if [ -f "$dir/culture.yaml" ]; then + # Capture only the first non-space token after `suffix:` (so an + # inline `# comment` or trailing space can't bleed into the scope), + # then strip surrounding quotes only — matching the canonical parser + # in .claude/skills/cicd/scripts/_resolve-nick.sh. + suffix=$(sed -n \ + 's/^[[:space:]]*-\{0,1\}[[:space:]]*suffix:[[:space:]]*\([^[:space:]]*\).*/\1/p' \ + "$dir/culture.yaml" | head -n1 | tr -d "\"'") + break + fi + dir=$(dirname "$dir") + done + printf '%s' "$suffix" +} + +has_flag() { + local needle=$1 + shift + local a + for a in "$@"; do + case "$a" in + "$needle" | "$needle"=*) return 0 ;; + esac + done + return 1 +} + +SCOPE_ARGS=() +if ! has_flag --scope "$@"; then + EIDETIC_SCOPE=$(resolve_scope) + if [ -n "$EIDETIC_SCOPE" ]; then + SCOPE_ARGS+=(--scope "$EIDETIC_SCOPE") + has_flag --visibility "$@" || SCOPE_ARGS+=(--visibility private) + fi +fi + +: "${EIDETIC_EMBED_URL:=http://localhost:8002/v1}" +: "${EIDETIC_EMBED_MODEL:=Qwen/Qwen3-Embedding-0.6B}" +export EIDETIC_EMBED_URL EIDETIC_EMBED_MODEL + +exec "${EIDETIC[@]}" remember "${SCOPE_ARGS[@]}" "$@" diff --git a/CHANGELOG.md b/CHANGELOG.md index b0f391d..efd9cf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.0] - 2026-06-23 + +### Added + +- **Vendored the `remember` + `recall` memory skills from eidetic-cli** + (cite-don't-import) — the write/read halves of eidetic's shared + `~/.eidetic/memory` surface, so this agent (Claude and its colleague backend) + can persist facts across sessions and recall them later, sharing one store. + `remember` drives `eidetic remember` (idempotent upsert of one JSON record or + an NDJSON batch on stdin, dedup by id + content hash); `recall` drives + `eidetic recall` with four search modes — exact / approximate / keyword / + hybrid — each hit carrying text, full provenance metadata, a relevance score, + and a freshness signal. The `.sh` wrappers are byte-verbatim from eidetic-cli + (their first-party origin); each `SKILL.md` is localized only in the + illustrative `--scope ` examples (Provenance keeps "First-party to + eidetic-cli"). Both default to this agent's PRIVATE scope, reading the suffix + from `culture.yaml`. Runtime dep: the `eidetic` CLI on PATH (else a local + eidetic-cli checkout with `uv`). Propagated by rollout-cli's `eidetic-memory` + recipe. + ## [0.2.1] - 2026-06-13 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 2eb0648..a8b7a84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "tensor-cli" -version = "0.2.1" +version = "0.3.0" description = "Agent/CLI for tensor operations and ML tensor manipulation" readme = "README.md" license = "MIT"