Commit Graph

485 Commits

Author SHA1 Message Date
CauchYoung e9f447c80e fix: recover stale sessions before eviction (#387)
* fix: recover stale sessions before eviction

Constraint: preserve recoverable session insights during stale cleanup
Confidence: high
Scope-risk: narrow
Tested: npm test -- test/evict.test.ts --reporter=dot; npx tsdown; git diff --check
Tested: npm test -- --reporter=dot (fails on existing Windows path-sensitive compress-file and obsidian-export tests)
Signed-off-by: laplace young <yangqk12@whu.edu.cn>

* fix: refine stale session recovery guard

Constraint: preserve raw-only stale sessions until a compression recovery path exists
Confidence: high
Scope-risk: narrow
Tested: npm test -- test/evict.test.ts --reporter=dot; npx tsdown; git diff --check
Signed-off-by: laplace young <yangqk12@whu.edu.cn>

---------

Signed-off-by: laplace young <yangqk12@whu.edu.cn>
Co-authored-by: laplace young <yangqk12@whu.edu.cn>
2026-05-17 11:15:06 +01:00
이민재 0ee3e6510e fix(viewer): handle sessions missing ids (#366)
* fix(viewer): handle sessions missing ids

* fix(viewer): address session id review feedback

---------

Co-authored-by: honor2030 <19909783+honor2030@users.noreply.github.com>
2026-05-17 11:08:19 +01:00
Satyam Pratibhan 97ae69a3e4 fix: add AbortController timeout to all raw-fetch providers (#379)
* fix: add AbortController timeout to all raw-fetch providers (closes #373)

Signed-off-by: Satyam Pratibhan <142714564+SATYAM-PRATIBHAN@users.noreply.github.com.>

* fix: validate timeout ms and compose upstream AbortSignal in fetchWithTimeout

Signed-off-by: Satyam Pratibhan <142714564+SATYAM-PRATIBHAN@users.noreply.github.com.>

---------

Signed-off-by: Satyam Pratibhan <142714564+SATYAM-PRATIBHAN@users.noreply.github.com.>
2026-05-17 10:52:13 +01:00
Erwan Leboucher 1c8713f978 fix(hermes): is_available() must not do network calls (#377) 2026-05-17 10:29:56 +01:00
Virgile 57ffe91302 fix(hermes): use correct API keys tool_input/tool_output in sync_turn (#384)
The Hermes plugin's sync_turn() method sends conversation observations
to the agentmemory /observe endpoint using keys 'input' and 'output',
but the server's mem::observe handler extracts data using 'tool_input'
and 'tool_output' (see src/functions/observe.ts).

This key mismatch means raw.toolInput and raw.toolOutput are always
undefined. When auto-compress is enabled, the LLM receives a nearly
empty compression prompt (only timestamp + hookType + toolName), which
produces generic, useless observations like:

  title: "Agent initiated conversation"
  narrative: "No specific content was provided"
  importance: 1

With the corrected keys, DeepSeek/Vision models receive the actual
conversation content and produce meaningful, structured observations:

  title: "Verified fix for agentmemory plugin key naming"
  importance: 6
  concepts: ["agentmemory plugin", "tool_input/tool_output key renaming"]
  facts: ["The fix replaces 'input'/'output' keys with..."]
2026-05-17 10:29:50 +01:00
Erwan Leboucher 60a650d8c9 fix(cli): authenticate apiFetch when AGENTMEMORY_SECRET is set (#380) 2026-05-17 10:29:45 +01:00
healdigital d271fb41cb fix(coolify): use environment map syntax for Coolify v4 parser (#424)
Coolify v4's PHP-based docker-compose validator rejects the shorthand
environment list syntax ('- VAR' without =) with:

    Error: non-string key in services.agentmemory.environment: 0

Switching to the map form with explicit variable substitution preserves
the original 'inherit from build env' behavior — Coolify still resolves
SERVICE_FQDN_AGENTMEMORY_3111 from the magic variable mechanism — while
satisfying the stricter parser.

Verified with:
  SERVICE_FQDN_AGENTMEMORY_3111=https://example.com \
    docker compose -f deploy/coolify/docker-compose.yml \
                   --project-directory deploy/coolify config

Tested live on a Coolify v4.0.0-beta.397 instance: deploy succeeds and
container reaches running:healthy on first try with this change.
2026-05-17 10:15:16 +01:00
@aaronjmars 3e7b61efd0 fix(security): reject non-loopback Host headers in viewer server (DNS rebinding) (#294)
The viewer (default :3113) binds to 127.0.0.1 and proxies every request to
the local REST API with the AGENTMEMORY_SECRET attached as Bearer auth.
The Origin allowlist defends against a plain cross-origin attacker page,
but it does not defend against DNS rebinding: a victim visiting
attacker.com whose authoritative DNS short-TTL-rebinds to 127.0.0.1 ends
up issuing same-origin fetches to the viewer (origin and target both
http://attacker.com:3113 from the browser's perspective), no preflight
fires, and the proxy adds the secret server-side. The attacker page then
reads every memory the user has stored.

Fix: derive a Host-header allowlist from VIEWER_ALLOWED_ORIGINS plus the
viewer's actual listen port (resolved post-listen so port=0 unit tests
still work) and reject any other Host with 403 before the route logic
runs. An explicit VIEWER_ALLOWED_HOSTS env var is honoured for reverse-
proxy setups. Tests cover the predicate plus an end-to-end http.request
that confirms an attacker-controlled Host header is rejected before the
HTML / proxy paths.

Detected by Aeon + manual review.
Severity: high
CWE-350 (Reliance on Reverse DNS Resolution for a Security-Critical Action)

Co-authored-by: aeonframework <aeon@aaronjmars.com>
2026-05-17 10:15:09 +01:00
Rohit Ghumare 3a3f866ab5 chore(release): v0.9.17 — OpenAI provider + telemetry id + Compare polish (#432)
Patch bump per the established rule: additive surface only. OpenAI
provider is a new optional surface that activates only when
OPENAI_API_KEY is set, gated by OPENAI_API_KEY_FOR_LLM. Telemetry
project_name pin is pure observability metadata. Compare polish is
docs/website only.

PRs included since v0.9.16:
  #307 — OpenAI-compatible LLM provider (universal adapter for
         OpenAI, Azure OpenAI auto-detected by hostname, DeepSeek,
         SiliconFlow, vLLM, LM Studio, Ollama). Plus the maintainer-
         pushed Azure detection + fetch timeout + README scope hint
         follow-ups. Closes #185, #232, #312, supersedes #240.
  #426 — pin worker telemetry project_name
  #427 — Compare section polish (title + native plugins cell + grid)

Files bumped (9):
  package.json, packages/mcp/package.json, plugin/.claude-plugin/plugin.json,
  plugin/.codex-plugin/plugin.json, src/version.ts, src/types.ts,
  src/functions/export-import.ts, test/export-import.test.ts,
  CHANGELOG.md
v0.9.17
2026-05-16 20:36:24 +01:00
VantHoff 3166780d2c feat: add OpenAI-compatible LLM provider (#307)
* feat: add OpenAI-compatible LLM provider

- Add OpenAIProvider using raw fetch (no SDK dependency)
- Supports any /v1/chat/completions endpoint: OpenAI, DeepSeek,
  SiliconFlow, Azure OpenAI, vLLM, LM Studio, Ollama
- Auto-detects OPENAI_API_KEY with OPENAI_API_KEY_FOR_LLM opt-out
- Add OPENAI_REASONING_EFFORT passthrough for thinking models
  (e.g. Ollama Cloud kimi-k2.6) to ensure content is populated
- Update README with OpenAI provider table, env vars, and reasoning config

* fix(config): honor OPENAI_API_KEY_FOR_LLM=false in detectLlmProviderKind

`detectProvider()` correctly gates OpenAI auto-detection on
OPENAI_API_KEY_FOR_LLM !== "false", but `detectLlmProviderKind()` did
not — so users who set OPENAI_API_KEY only for embeddings (via the
existing OPENAI_BASE_URL + OPENAI_EMBEDDING_MODEL flow from #186)
would see /agentmemory/config/flags report `provider: llm` even
though detectProvider() routed them to the noop provider.

Also clarify in the README that OPENAI_REASONING_EFFORT is honored
only by reasoning models (o1, o3, gpt-*-reasoning) and providers
that mirror that schema (Ollama Cloud thinking models). Standard
chat models reject the field with 400.

Verified:
- OPENAI_API_KEY=sk-... + OPENAI_API_KEY_FOR_LLM=false now returns
  "noop" from detectLlmProviderKind (was "llm" before the fix).
- OPENAI_API_KEY=sk-... alone still returns "llm" (intended default).
- npm run build clean.

Note: 10 pre-existing test failures on test/mcp-standalone.test.ts
are a stale-branch artefact — this branch is 10 commits behind main
and is missing the MCP shim fixes that landed via #311 / #327.
Recommend rebasing on main (or "Update branch" via the GitHub UI)
before merge.

* feat(providers): openai — fetch timeout, Azure auto-detect, OPENAI_API_KEY scope hint

Three follow-ups against the v2 PR so reviewers can see what's left
before merge:

1. Outbound fetch timeout. The other raw-fetch providers
   (anthropic / gemini / openrouter / minimax) all lack one too —
   that's a same-pattern repo-wide concern tracked as a follow-up
   issue. This PR fixes the bound on the new surface only:
   AbortController + setTimeout, default 60s, overridable via
   OPENAI_TIMEOUT_MS. Abort messages explain how to raise the bound.

2. Azure OpenAI auto-detection. Azure is in the README's supported
   list but the code only emitted the standard OpenAI shape. Now
   detects `.openai.azure.com` hostnames at construction time and:
     - swaps `Authorization: Bearer` for `api-key: <KEY>`
     - drops the `/v1` path prefix (deployment is baked into base URL)
     - appends `api-version=<version>` query param (env-overridable)
   Default api-version is `2024-08-01-preview` per Azure docs.
   Existing users of the standard OpenAI shape are unaffected — the
   detection is purely hostname-driven.

3. README clarity on `OPENAI_API_KEY` shared use. Embeddings (#186)
   and this PR's LLM both auto-activate from the same key, which
   surprises users who only meant one or the other. Added an
   explicit callout above the LLM section pointing at the
   `OPENAI_API_KEY_FOR_LLM=false` opt-out. Also documents the new
   timeout + Azure base-URL shape inline.

Build clean. Tests 954/954 (10 pre-existing mcp-standalone failures
on main are not in this branch's snapshot).

---------

Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
2026-05-16 18:40:00 +01:00
Rohit Ghumare 6f228bd8c0 fix(website): compare section polish (#427)
User caught the section looking bad on the live merge:

1. Title "AGENTMEMORY VS. THE FIELD." was redundant (eyebrow already
   reads VS.) and wrapped ugly with FIELD. dangling on its own line.
   Shortened to "VS. THE FIELD." Eyebrow + title now read together
   as "VS. · VS. THE FIELD." which is still on-brand and fits one
   line at every viewport above 480px.

2. Added `text-wrap: balance` to .section-title globally so titles
   that DO need to wrap break at a balanced point instead of leaving
   orphan words. Affects every section's heading; net win — already
   verified other titles (Features TWELVE THINGS, CommandCenter TWO
   UIs, Agents SIX FIRST-PARTY, etc) render correctly.

3. Comparison NATIVE PLUGINS cell value was "6 (Claude/Codex/
   OpenClaw/Hermes/pi/OpenHuman)" — that's 49 chars stuffed into a
   ~180px grid column, forcing multi-line wrap that looked cramped.
   Cell now reads just "6"; the agent names are already visible in
   the dedicated Agents grid two sections above.

4. Row grid: bumped agentmemory column from 1fr → 1.3fr at the
   expense of the label column (1.6fr → 1.4fr) so the longer cell
   values (95.2%, YES (APACHE-2.0), 2 (Qdrant, Neo4j)) have
   breathing room. Added `word-break: break-word` as a safety net
   for future entries. Padded the row 24px horizontally so cells
   don't kiss the section edge.

Build clean.
2026-05-16 10:43:30 +01:00
Rohit Ghumare f1fbea3c08 feat(website): hero rebuild — npx CTA + inline validation + SSR star count (#425)
Five user-reported issues against agent-memory.dev v0.9.16:

1. AGENT/MEMORY title was clamp(54px, 12vw, 160px) — eating ~half the
   viewport on most desktop widths and forcing FeaturedIn below the
   fold. Reduced to clamp(44px, 9vw, 124px). Mobile-stacked variant
   now clamp(40px, 14vw, 72px) — cleaner.

2. Primary CTA was a generic "START IN 30 SECONDS" button that
   anchored to #install. Replaced with a copyable terminal command —
   `$ npx @agentmemory/agentmemory` rendered as a gold-bordered cell
   with "CLICK TO COPY" affordance. Copy → clipboard, transient
   "COPIED" state. Matches what users actually type.

3. FeaturedIn was a standalone section below the fold. Now inlined
   inside the hero (compact mode — no border, no own padding,
   transparent background) so AlphaSignal / Agentic AI Foundation /
   Trendshift validation lands above the fold.

4. Star count was missing from the hero CTA button. The component
   only fetched client-side from unauthenticated github.com/api which
   is rate-limited to ~60 req/hr/IP — first paint had no count, often
   forever on mobile (which is what the user screenshot showed).
   Hero is now an async server component; calls lib/github.ts
   fetchRepoStats (same path Nav already uses), passes the SSR'd
   count into GitHubStarButton via new optional initialStars prop.
   Client-side refetch still fires in useEffect as a best-effort
   refresh.

5. Mobile layout was unverified. Tightened all clamp() values,
   added explicit @media (max-width: 760px) breakpoint that drops
   padding, stacks the title, and reduces the chip size. The new
   HeroNpxCommand has its own mobile breakpoint at 640px that hides
   the "CLICK TO COPY" hint and stretches to full width.

CTA stacks vertically now: copyable command (primary, gold border)
on row 1, then "SEE IT MOVE" + STAR-with-count on row 2. FeaturedIn
strip sits below row 2 as part of the above-the-fold validation.

Build clean. No new deps.
2026-05-16 10:09:14 +01:00
Rohit Ghumare fae26ca049 chore(telemetry): pin worker project_name in registerWorker options (#426)
iii-sdk's InitOptions.telemetry.project_name field falls back to
auto-detection when omitted — cwd / package.json basename / hostname,
in that order — which produces inconsistent identifiers per host
(`agentmemory`, `node`, `npm`, occasionally the user's home dir
basename when run via npx). Setting it explicitly to "agentmemory"
gives every install the same stable project identifier in the
engine's metrics + traces output, so downstream attribution and
grouping read cleanly.

Also pins `language: "node"` and `framework: "iii-sdk"` for the same
reason — auto-detection of those two has the same divergence problem
across install paths (npm global vs npx vs source build).

No behavior change for users. No new deps. iii-sdk 0.11.6 exposes
these fields under InitOptions.telemetry — see
node_modules/iii-sdk/dist/index.d.mts:32-37 (TelemetryOptions).
2026-05-16 10:08:55 +01:00
이민재 705321be34 docs: refresh current project stats (#330)
* docs: refresh current project stats

* docs: update REST endpoint badge count

* docs: refresh stats for v0.9.16

---------

Co-authored-by: 이민재 <19909783+honor2030@users.noreply.github.com>
2026-05-16 10:00:00 +01:00
이민재 b48176cacd fix(mcp): use DELETE for governance memory deletion proxy (#423)
Co-authored-by: 이민재 <19909783+honor2030@users.noreply.github.com>
2026-05-16 09:59:08 +01:00
Rohit Ghumare b7e1240d06 chore(release): v0.9.16 — DevEx polish + website refresh (#417)
Patch bump per the established rule: additive surface only, no breaks
to MemoryProvider trait, exported types, or default behaviour. New
top-level subcommands (`--reset` already shipped 0.9.15, no new
commands here) are opt-in.

PRs included since v0.9.15:
  #408 — onboarding wires selected agents inline + memory-share callout
  #409 — clarify MCP is opt-in (REST primary)
  #410 — 5-port ready panel, iii console install, global-install prompt
  #411 — splash banner rerender + README install hoist + npx caveat
  #415 — agent-memory.dev refresh (FeaturedIn bar + Agents/Compare/
         CommandCenter/Hero updates)

Files bumped (9):
  package.json, packages/mcp/package.json, plugin/.claude-plugin/plugin.json,
  plugin/.codex-plugin/plugin.json, src/version.ts, src/types.ts,
  src/functions/export-import.ts, test/export-import.test.ts,
  CHANGELOG.md
v0.9.16
2026-05-15 19:58:24 +01:00
Rohit Ghumare 9ba346789b docs(website): refresh agent-memory.dev for v0.9.15+ (#415)
* docs(website): refresh agent-memory.dev for v0.9.15+ DevEx surface

Drift accumulated across the website from v0.9.0 through v0.9.15.
Each section caught up:

CommandCenter:
- "iii CONSOLE · OPTIONAL" → "iii CONSOLE · FIRST-CLASS". User
  pushback: the console gives engine-level visibility (workers,
  functions, queues, traces) and is now installed inline by the
  agentmemory CLI on first run from v0.9.16. Not optional.
- Bullet "49 HTTP TRIGGERS" → "107 HTTP ENDPOINTS" to match
  generated-meta.json restEndpoints count. Dropped the stale "33
  REGISTERED FUNCTIONS" subhead.
- Section lede now mentions both UIs are first-class + inline-
  installed.

Compare:
- MCP TOOLS our column 44 → 51 (matches generated-meta.json).
- New REST ENDPOINTS row (107) — competitors don't ship a REST
  shape per their docs, so dashes for mem0/letta/cognee.
- New NATIVE PLUGINS row listing the 6 first-party agents +
  Cursor/Gemini CLI.

Agents:
- FEATURED expanded from 4 to 6 (added pi + OpenHuman). Codex CLI
  pitch updated to "6 hooks + MCP · native plugin" reflecting the
  hooks addition in PR #383.
- Section title "FOUR FIRST-PARTY" → "SIX FIRST-PARTY".
- Section lede now mentions `agentmemory connect <agent>`
  (shipped in v0.9.15 PR #402).

Hero:
- "START IN 60 SECONDS" CTA → "START IN 30 SECONDS". Cold install
  + engine spawn measured 8-12s on v0.9.15 with the native binary
  path; 60s was the v0.9.0-era Docker-first claim.

generated-meta.json regenerated by the build (auto-derive rule).
Website build clean.

* chore(website): refresh generated-meta timestamp

* feat(website): AS FEATURED IN bar — AlphaSignal, AAIF, Trendshift

Wedges between Hero and Stats. Three text-cells in a 3-col row (1-col
on mobile), each links to the source article. Matches the existing
brutalist mono aesthetic — no logo files, no marquee animation, no
JS. Border + small ↗ glyph on each card; gold border on hover.

Sources chosen for brand authority + independence:
- AlphaSignal (180K technical subscribers) →
  alphasignalai.substack.com/p/how-agentmemory-works-and-how-to
- Agentic AI Foundation (Linux Foundation backed) → aaif.io
- Trendshift "NEW 2026 · Position #19" → trendshift.io/repositories/25123

Static, server-rendered, no client bundle impact.

* feat(website): real logos in AS FEATURED IN bar

Replaced text-only treatment with actual brand marks:

- AlphaSignal — github.com/Alpha-Signal org avatar (verified)
  https://avatars.githubusercontent.com/u/64016073?s=200&v=4
- Agentic AI Foundation — aaif.io's own favicon PNG (verified by
  fetching their site)
- Trendshift — their official badge endpoint
  trendshift.io/api/badge/repositories/25123 — bakes the repo's live
  star + position count into the image, so the cell tracks the
  Trendshift state automatically

Layout split: AlphaSignal + AAIF render logo-left / text-right with
↗ arrow. Trendshift renders centered with the full badge image (it's
already a wide brand mark with the rank + stars baked in, so making
it text-bound looks cramped).

next.config.ts adds the three new image hosts to remotePatterns
(aaif.io, trendshift.io, raw.githubusercontent.com). Existing
unoptimized={true} per image so we don't proxy through Vercel's
optimizer.

Build clean. No new deps.

* feat(website): use real AAIF wordmark logo + center 3-card grid

User pushed the actual Agentic AI Foundation wordmark — 838x203 PNG
on transparent background, black artwork. Dropped the favicon and
self-hosted the wordmark at /featured/aaif-logo.png so the brand
reads cleanly on the dark cell.

Rendered as a badge-style cell (centered, wide-form) like Trendshift,
with a `filter: invert(1) brightness(1.05)` so the black artwork
inverts to white-on-dark and matches the site palette without
shipping a second asset.

Grid template balanced: 1fr / 1.3fr / 1fr — gives AAIF the extra
horizontal room its wordmark needs while keeping AlphaSignal and
Trendshift consistent on either side. Max width bumped to 1000px.

`badgeImg` got `object-fit: contain` and an explicit `max-height` so
the AAIF + Trendshift badges sit at the same vertical baseline.

* fix(website): coderabbit PR #415 — meta drift + a11y + dead CSS

Five findings, all valid against the branch:

1. CommandCenter line 48 hardcoded "107 HTTP ENDPOINTS" but
   website/lib/generated-meta.json reports restEndpoints: 121.
   Bumped to 121. The meta file is the source of truth (auto-derived
   from src/triggers/api.ts at build via scripts/derive-meta.js).

2. Compare.tsx REST ENDPOINTS row same drift: "107" → "121".

3. Compare.tsx NATIVE PLUGINS row claimed "6" in the label but the
   parenthesized list had 8 names (Cursor + Gemini included). Per
   README: those two are MCP-server tier, not native plugin. List
   trimmed to the actual 6 native-plugin agents:
   Claude/Codex/OpenClaw/Hermes/pi/OpenHuman.

4. FeaturedIn.module.css .cell had :hover only — keyboard users
   tabbing through saw no focus indicator (WCAG 2.4.7). Added
   :focus-visible with gold border + outline.

5. .cellBadge declared `grid-template-columns: none` while using
   display: flex. Dead property removed.
2026-05-15 19:23:45 +01:00
Rohit Ghumare 7530339b4c feat(cli): 5-port ready panel + iii console install + global-install prompt + version-string fix (#410)
* feat(cli): 5-port ready panel, console install, global-install prompt

The boot/ready surface now reflects what's actually running and what's
on the menu instead of eliding three services and offering one passive
hint.

1. Five-port ready panel. printReadyHint switches from a single-line
   "Memory ready on :3111 · viewer on ..." to a clack p.note panel
   that lists REST API, Viewer, Streams, Engine bridge, and iii
   console. Each port reads from its env var (III_REST_PORT,
   III_VIEWER_PORT, III_STREAM_PORT / III_STREAMS_PORT,
   III_ENGINE_PORT / III_ENGINE_URL) with sane defaults; nothing
   is hard-coded.

2. iii console install probe + auto-install. New ensureIiiConsole
   detects the binary via whichBinary("iii-console") or
   ~/.local/bin/iii-console. If absent on a TTY (not CI), prompt
   the user; on yes shell out to "curl | sh"; on no persist
   skipConsoleInstall: true. We do NOT probe the console's HTTP
   port: its default 3113 collides with our viewer, so binary
   presence is the right signal at boot time.

3. Global-install prompt replaces maybeEmitNpxHint. Users kept
   ignoring the passive npx tip and then typing "agentmemory stop"
   in a new shell to a "command not found". Now on first npx run
   (TTY, not CI, not already skipped), we ask once and run
   "npm install -g @agentmemory/agentmemory@<VERSION>" on yes. On
   no, we persist skipGlobalInstall: true so we never re-prompt.

4. Version-string fix. The engine-version-mismatch warning quoted
   "agentmemory v0.9.14+ pins ..." forever. Switched to the live
   VERSION constant so the message stays honest across releases.

Preferences schema gains skipGlobalInstall and skipConsoleInstall
(both default false); schemaVersion stays at 1 because the reader
already merges over defaults.

* fix(cli): coderabbit PR #410 — ready panel host detection + runnable hints

Two findings, both valid against the just-shipped feat/cli-boot-panel
code:

1. The console line printed `(run: iii-console -p <port>)` regardless
   of where the binary actually lived. We just detected the binary
   path two lines earlier — print that as the executable hint
   (`consoleState.binPath -p <port>`) so the suggestion is runnable
   without assuming `iii-console` is on PATH.

2. REST / Streams / Engine URLs hardcoded `localhost`, so a deploy
   that binds `III_ENGINE_URL=ws://remote-host:49134` would print
   misleading addresses. Added `getEngineHost()` which reads
   III_ENGINE_URL or AGENTMEMORY_URL and falls back to localhost.
   REST now uses getBaseUrl() (already honors AGENTMEMORY_URL),
   Streams + Engine read the new host helper. Viewer line untouched
   — getViewerUrl() already handles this correctly.

3. Bonus runnability fix: the "Try: agentmemory demo" suggestion
   assumed the bare command was on PATH. Now picks `npx
   @agentmemory/agentmemory demo` when invoked via npx, falls back
   to the bare form for global installs.

Build clean, 954/954 tests pass.
2026-05-15 18:03:36 +01:00
Rohit Ghumare 9344cfd502 fix(cli): rerender splash banner + README npx caveat (#411)
* fix(cli): rerender splash banner with clean figlet output + README npx caveat

Two small fixes from local v0.9.15 testing:

1. The hand-drawn ASCII in `src/cli/splash.ts` was rendering "agentmemory"
   with broken middle glyphs — `_ _` instead of `_ __` around the 'n'
   and merged 'tm/me' segments. Replaced with verified `figlet
   agentmemory` standard-font output (70 cols, 6 rows, regenerable).
   Compact + minimal banners untouched.

2. Added a "Recommended: install globally" section to README's Quick
   Start. Explains the npx per-version cache gotcha (running
   `npx ...@0.9.14` then `npx @agentmemory/agentmemory` may serve the
   stale cached version) and the three workarounds:
     - `npm install -g @agentmemory/agentmemory` (recommended)
     - `npx -y @agentmemory/agentmemory@latest` (forces fresh fetch)
     - `rm -rf ~/.npm/_npx && npx ...` (one-shot cache clear)
   Notes that v0.9.16 ships an interactive global-install prompt
   inline on first npx run.

Build clean (44.46 kB cli.mjs). All 954 tests pass.

* docs: hoist install section to top of README + update website Install card

README: install was buried at line 306 below benchmarks, comparison
tables, and the agents grid. Moved a 4-line install block right under
the nav anchors at the top, with the bare `agentmemory` command from
the global install path leading. Kept the deeper Quick Start section
below — this is the "front door" version.

website (Install.tsx): the three-card install flow now leads with
`npm install -g @agentmemory/agentmemory` (step 1), then bare
`agentmemory` + `agentmemory demo`. Section title changed from "THREE
COMMANDS. ANY AGENT." to "ONE INSTALL. ANY AGENT." and the lede notes
the npx fallback for users who want zero-install.

website/lib/generated-meta.json refreshed by the build (auto-derived
from package.json + walked test count; pre-existing feedback rule).

Build clean (website + main both pass).

* docs: address coderabbit PR #411 review

README: annotate `rm -rf ~/.npm/_npx` as POSIX-only, point Windows
users at `Remove-Item ...\npm-cache\_npx`. Mark `npx -y @...@latest`
as the cross-platform option. Applies to both install-section and
quick-start blocks (lines 87, 363).

website Install.tsx: hint on step 1 now says "STEPS 2/3 NEED THIS"
so users who skip can't get stuck on `agentmemory` not found. Added
a fourth card "PREFER ZERO-INSTALL? USE NPX" with the bare npx
command and a pointer to the README caveat — covers the skip path
without making step 1 ambiguous.

Skipped (verified-false): coderabbit flagged the splash banner's
"70 columns wide" comment as wrong (claimed rows 3+5 are 76/77). They
counted source-string chars including the `\\` escapes; runtime is 70
on all 6 rows (node length check), which is what alignment cares
about. Comment is accurate, no change needed.
2026-05-15 18:02:33 +01:00
Rohit Ghumare 48070ad096 feat(cli): onboarding wires selected agents inline + memory-share callout (#408)
After the agents multi-select, show a callout explaining that all picked agents share the same memory at :3111 (Claude Code saves visible to Codex + Cursor instantly).

After provider selection + .env write, ask whether to run agentmemory connect <agent> for each selected agent right now. On yes, dispatch through the existing connect adapter (same code path as agentmemory connect <agent>) and summarize wired vs manual vs failed. On no/cancel, print the exact commands to run later.

Expose runAdapter from src/cli/connect/index.ts so onboarding reuses the same install path the CLI uses today.
2026-05-15 17:45:37 +01:00
Rohit Ghumare 6e3c052943 feat(cli): clarify MCP is opt-in — REST primary, MCP for MCP-only clients (#409) 2026-05-15 17:45:13 +01:00
Rohit Ghumare c3a613aed8 chore(release): v0.9.15 — DevEx overhaul (splash, connect, doctor v2, remove, silent killers) (#407)
Patch bump per the established rule: all changes additive, no breaks
to MemoryProvider trait or exported types or default behaviour. New
top-level subcommands (connect, remove, --reset, --force) are opt-in.

PRs included since v0.9.14:
  #405 — silent killers: viewer port auto-bump, engine version-match
         warning, stop --force, adopt-on-attach, npx PATH hint
  #402 — agentmemory connect — automate native-plugin install for 8
         agents (claude-code, codex, cursor, gemini-cli, openclaw
         end-to-end; hermes/pi/openhuman stubbed)
  #406 — interactive doctor v2 + agentmemory remove (destruction plan
         + two-confirmation flow)
  #403 — splash banner + agent grid + provider picker + smart-defaults
         preferences + bootLog shim (30+ lines of log spam → 10)

Files bumped (9):
  package.json, packages/mcp/package.json, plugin/.claude-plugin/plugin.json,
  plugin/.codex-plugin/plugin.json, src/version.ts, src/types.ts,
  src/functions/export-import.ts, test/export-import.test.ts,
  CHANGELOG.md
v0.9.15
2026-05-15 16:43:06 +01:00
Rohit Ghumare 659d0c3e1a feat(cli): splash banner, agent grid, provider picker, smart defaults (#403)
Replaces the 25-line `[agentmemory] X` log spam on first invocation
with a SkillKit-grade onboarding flow:

* `src/cli/splash.ts` — terminal-width-aware ASCII banner with three
  tiers (>=120 / 80-119 / <80 cols), brand-accent orange via ANSI
  256-colour, NO_COLOR-aware, no new deps.
* `src/cli/preferences.ts` — JSON-backed prefs at
  `~/.agentmemory/preferences.json`, atomic write via tmp+fsync+rename,
  graceful defaults on corrupt/missing file, `isFirstRun()` /
  `resetPrefs()` helpers. 7 unit tests added.
* `src/cli/onboarding.ts` — first-run multi-select for agents (native
  plugin + MCP-server rows from the README), single-select for
  provider (anthropic / openai / gemini / openrouter / minimax / skip
  -> BM25-only), seeds `~/.agentmemory/.env`, prints the next-step
  hint.
* `src/logger.ts` — adds `bootLog` / `setBootVerbose`. The 18 startup
  `console.log` calls in `src/index.ts` route through this shim;
  default is muted, `--verbose` (or AGENTMEMORY_VERBOSE=1) restores
  the old chatty output. Warnings, errors, and shutdown logs stay
  loud.
* `src/cli.ts` — splash before clack intro, run onboarding when
  `isFirstRun()` or `--reset`, propagate verbosity to the worker
  logger, wait for `/agentmemory/livez` after `import("./index.js")`
  and print a single-line ready hint. Adds `--reset` flag.

First-run output drops from 30+ lines to ~10. Subsequent runs are a
single ready line. `--verbose` restores the old verbose surface for
debugging. Build passes; the 10 pre-existing failures in
`test/mcp-standalone.test.ts` are unchanged (903 -> 910, all 7 new
passes are `test/preferences.test.ts`).
2026-05-15 16:38:09 +01:00
Rohit Ghumare 29e8865286 feat(cli): interactive doctor v2 + agentmemory remove (#406)
* feat(cli): interactive doctor v2 with inline fixes

Replace passive doctor with an interactive [F]ix/[S]kip/[?]more/[Q]uit
prompt per diagnostic. Each check now ships a one-line problem, cause,
fix-preview, and inline apply() — and we re-check only the affected
diagnostic after a fix, not the whole suite.

New flags:
  doctor --all        apply every fix without prompting (CI)
  doctor --dry-run    show what each fix would do, run nothing

Catalog (src/cli/doctor-diagnostics.ts):
- env-missing                     → runInit()
- no-llm-provider-key             → open .env in $EDITOR
- engine-version-mismatch         → runIiiInstaller + restart
- viewer-unreachable              → stop + restart engine
- stale-pidfile                   → clear pid + state, restart
- env-placeholder-keys            → open .env in $EDITOR
- iii-on-path-not-local-bin       → manual (suggest installer)

The catalog is exported as a pure data structure so tests can assert
each entry has check/fix/message without booting clack.

Passive server checks (graph populated, Claude Code hooks, flags) still
run first — they need a live engine and have no one-shot inline fix.

* feat(cli): add agentmemory remove for clean uninstall

New command tears down everything agentmemory installs:
- ~/.agentmemory/iii.pid + engine-state.json (after stopping engine)
- ~/.agentmemory/.env (asks separately — holds API keys)
- ~/.agentmemory/preferences.json
- ~/.agentmemory/backups/ (connect-PR manifest + backups)
- ~/.local/bin/iii (only when iii --version matches our pin)
- Any agent symlinks from the connect-manifest
- ~/.agentmemory/data/ (asks separately, default keep)

Surface:
  agentmemory remove              # interactive, double-confirms
  agentmemory remove --force      # skip generic confirms
  agentmemory remove --keep-data  # remove binaries + symlinks only

Plan building lives in src/cli/remove-plan.ts as a pure fs-inspecting
function so tests sandbox a fake $HOME under tmpdir() and assert on
plan shape without touching the real home directory.

Always-ask items (.env, memory data, mismatched-version local-bin iii)
still get per-item confirmation even with --force.
2026-05-15 16:20:24 +01:00
Rohit Ghumare b36a6d7c73 feat(cli): add agentmemory connect to automate native-plugin install for 8 agents (#402)
agentmemory connect <agent> wires agentmemory into each supported coding agent: detect install, back up existing config to ~/.agentmemory/backups/, merge agentmemory MCP entry, verify, idempotent on re-run.

Working: claude-code, codex, cursor, gemini-cli, openclaw. Stubbed with manual-install hints: hermes (YAML), pi (TS extension copy), openhuman (no integrations folder). Windows prints manual-install message and exits.

Argv: agentmemory connect [agent] [--all] [--dry-run] [--force]
2026-05-15 16:15:49 +01:00
Rohit Ghumare 2250d8677d fix(cli): silent killers — viewer port auto-bump, engine version-match warning, adopt-on-attach, stop --force, npx PATH hint (#405)
* fix(cli): auto-bump viewer port when 3113 is taken

The viewer used to log 'Viewer port 3113 already in use, skipping viewer.'
and silently fail. On every fresh terminal you'd lose the viewer for the
rest of the session unless you noticed the warning buried in stdout.

Retry the next 10 ports (3113 -> 3122) before giving up. On fallback,
log: 'Viewer started on http://localhost:3114 (fallback from 3113)'.

* fix(cli): warn when iii-engine on PATH is not the pinned version

agentmemory pins iii-engine to v0.11.2 (see IIPINNED_VERSION in
src/cli.ts) but anyone with a different iii on PATH gets silent
behavioral drift -- EPIPE reconnect loops, empty search after save --
because we never tell them their engine doesn't match.

Add warnIfEngineVersionMismatch() and fire it from both:
  - startIiiBin() before we spawn (covers fresh starts)
  - main() early-return when isEngineRunning() returns true (covers
    attach-to-existing)

The warn tells the operator how to silence (AGENTMEMORY_III_VERSION)
and how to downgrade (curl | tar).

* fix(cli): add stop --force to bypass Docker-heuristic guard

runStop() correctly refuses to signal host PIDs when state file is
missing and a docker-compose.yml is discoverable -- the engine might
have been started by another shell via Docker compose, and SIGTERMing
the lsof match would kill an unrelated host process.

But when the engine actually WAS started natively (e.g. by a stale
agentmemory v0.9.13 with no state-writing) and the state file is gone,
the guard becomes a foot-gun: stop refuses, the engine stays up, and
the operator has no escape hatch short of `lsof | kill`.

Add --force. Documented in --help. The non-force path remains the
default and still refuses.

* fix(cli): adopt running engine on attach (write pidfile + state)

When main() finds isEngineRunning() already true (e.g. a previous
shell started it), we early-return without writing iii.pid or
engine-state.json. Later `agentmemory stop` then hits the
Docker-heuristic guard (state missing + compose discoverable) and
refuses to act -- correct in principle, foot-gun in practice.

Adopt on attach: write iii.pid from lsof-on-port and engine-state.json
as kind=native attached=true, but only if neither already exists.
Idempotent -- safe to re-run.

Log: 'Attached to existing iii-engine (pid 12345)'.

* fix(cli): hint that global install drops the `npx` prefix

After the engine is ready, if invoked via npx (detected via
npm_lifecycle_event / process.argv[1] containing _npx /
npm_config_user_agent starting with npm/), emit one line:

  Tip: install globally for the bare `agentmemory` command:
    npm install -g @agentmemory/agentmemory

Suppressed by ~/.agentmemory/preferences.json with skipNpxHint:true
(a sibling PR will write that file when the user opts out).
2026-05-15 16:14:15 +01:00
Rohit Ghumare 372c6a6bbb chore(release): v0.9.14 — CLI installer first + agentmemory stop + Docker-aware teardown (#401)
Patch bump. No breaking changes to the public API or the existing CLI
subcommands. CLI bootstrap flow changes are additive; existing Docker
users keep working via the new AGENTMEMORY_USE_DOCKER=1 opt-in.

Files bumped (9):
  - package.json
  - packages/mcp/package.json
  - plugin/.claude-plugin/plugin.json
  - plugin/.codex-plugin/plugin.json
  - src/version.ts
  - src/types.ts (ExportData.version union — also adds 0.9.13
    which was missed in the previous release)
  - src/functions/export-import.ts (supportedVersions Set)
  - test/export-import.test.ts
  - CHANGELOG.md

PRs included since v0.9.13:
  #396 — feat(cli): install iii-engine binary first, Docker opt-in;
         agentmemory stop with Docker-aware teardown; LISTEN-only
         lsof filter so the CLI no longer signals its own parent.
  #397 — docs(readme): move OpenHuman after pi in agents grid.
v0.9.14
2026-05-15 15:11:47 +01:00
Rohit Ghumare 9faf73760c feat(cli): install iii-engine binary first, Docker opt-in (#396)
* feat(cli): install iii-engine binary first, Docker opt-in

Reorder startEngine() fallback so the native iii binary is the happy
path. Docker compose becomes opt-in via AGENTMEMORY_USE_DOCKER=1, an
explicit prompt choice, or CI mode falls back to it only when
auto-install fails. Fixes a class of "engine started but REST never
responded" failures observed on Rancher Desktop, where docker compose
returns 0 on a degraded pull and we wait 15s for /livez against
nothing.

New tier order:
  1. iii on PATH
  2. ~/.local/bin/iii / fallback paths
  3. clack select (install / docker / manual) -- auto-install in CI
  4. Docker compose only via explicit opt-in
  5. fail-loud with install instructions

Extracts the installer logic out of runUpgrade into runIiiInstaller()
so first-run and upgrade share the same pinned-v0.11.2 curl+tar path.

Adds an `agentmemory stop` command:
  - writes ~/.agentmemory/iii.pid from spawnEngineBackground
  - SIGTERM with 3s wait, escalates to SIGKILL (iii ignores SIGTERM)
  - lsof :3111 fallback when pidfile is stale or missing
  - clears pidfile on success

Engine persistence is unchanged: `detached: true` + `child.unref()`
means memories keep flowing after npx exits. A second `npx` short-
circuits at isEngineRunning() before re-prompting.

Tightens installInstructions() copy: drops the AGENTMEMORY_III_VERSION
rationale tail (now in --help) and reorders so the curl path leads,
Docker is path B with the opt-in env var.

Help: documents stop, AGENTMEMORY_USE_DOCKER, AGENTMEMORY_III_VERSION.

* fix(cli): make agentmemory stop Docker-aware and crash-safe

Two issues caught in PR #396 review:

1. Docker-started engine would let runStop kill host proxies.

   `findEnginePidsByPort` returned every PID holding :3111. When
   the engine was started via `docker compose up -d`, the listening
   socket lives inside the container; on the host, lsof reports the
   Docker socket proxy (`com.docker.backend`, vmnetd, etc.). The
   stop flow would happily SIGTERM/SIGKILL those, taking down
   Docker Desktop networking for every other container the user
   has running.

   Adds an EngineState record at ~/.agentmemory/engine-state.json
   written by startEngine at spawn time (kind=native|docker,
   configPath or composeFile). runStop checks state first:
     - kind=docker: run `docker compose -f <file> down`, never
       signal host PIDs. Verify the docker binary is still on PATH
       and the compose file still exists; otherwise print the exact
       command the user should run.
     - kind=native: existing pidfile + lsof flow.
     - state missing + compose file discoverable + port held + no
       pidfile: refuse to signal blind, point at `docker compose
       down`. Catches engines started by a different shell or older
       agentmemory build that didn't write state.

2. runStop cleared the pidfile when isEngineRunning() returned
   false, even if a stale process was still bound to the port.

   The HTTP probe can fail while the engine is hung, paused
   (SIGSTOP), or in a half-closed state. Clearing the pidfile in
   that case made the next run silently start a second engine on a
   conflicting port. Now we keep the pidfile and surface the live
   PIDs with `ps -p` + `lsof` hints so the user can investigate
   before manual cleanup.

3. Bonus fix: `lsof -i :3111 -t` also returns CLIENT PIDs, not just
   the LISTEN socket owner. The CLI's own keep-alive fetch in
   isEngineRunning() showed up as a client, so signalAndWait would
   SIGKILL its own parent — exit code 137, files never cleaned up.
   Restrict to `-sTCP:LISTEN` and filter out `process.pid` as a
   belt-and-braces guard.

Verified with four scenarios on a real iii engine:
  - native + pidfile + state: SIGTERM → SIGKILL → exit 0, state cleared
  - docker state pointing at missing compose: refusal, state preserved,
    engine untouched
  - paused engine (kill -STOP): preserves pidfile, surfaces PIDs
  - lsof self-pid filter: CLI no longer kills itself

Build: dist/cli.mjs 44.46 kB. Tests: 892/903 (11 pre-existing
mcp-standalone failures unrelated to CLI changes).
2026-05-15 14:59:16 +01:00
Rohit Ghumare 471cc21492 docs(readme): move OpenHuman after pi in agents grid (#397)
Reorder row 1 of the works-with-every-agent table so OpenHuman sits in
the sixth slot (after pi) instead of the third. Keeps the
native-plugin agents grouped left, leaves OpenHuman next to pi so the
native-Memory-trait callout reads cleanly alongside the other
native-integration agents.
2026-05-15 14:17:24 +01:00
Rahul ff3e024d7d docs(readme): clarify MCP shim vs full server tool count (#233 gap 1) (#391)
The README implied the `@agentmemory/mcp` shim exposes "all 51 tools when
AGENTMEMORY_TOOLS=all".  In reality:

- `AGENTMEMORY_TOOLS` is a server-side flag (read by iii-engine), not by
  the shim — setting it in the shim's MCP `env` block does nothing.
- The shim has two modes: proxy (when `AGENTMEMORY_URL` reaches a running
  agentmemory server, all 51 tools are forwarded) and local fallback
  (no server reachable → 7 hardcoded tools from
  `src/mcp/standalone.ts`'s `IMPLEMENTED_TOOLS`).

This caused users (Cursor / OpenCode / Gemini CLI) following the README
to assume they had misconfigured something when they only saw 7 tools.

Two changes:
- Codex plugin bullet: replace the misleading `AGENTMEMORY_TOOLS=all`
  qualifier with an accurate proxy/local mode description.
- MCP Server section header: add a callout that explicitly enumerates the
  7-tool local fallback set and tells users how to unlock the full 51.

Addresses gap #1 in #233.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:04:35 +01:00
Rohit Ghumare eb5f7e6895 chore(deps): bundle 10 Dependabot bumps (claude-agent-sdk 0.3, dotenv 17, vitest 4, ts 6, types/node 25, react 19.2.6, checkout/setup-node v6) (#390)
* chore(deps): bundle 10 Dependabot bumps + README agents grid refresh

Closes the open Dependabot wave in one PR.

Root npm bumps:
- @anthropic-ai/claude-agent-sdk ^0.2.56 → ^0.3.142 (closes #389 group)
- dotenv ^16.4.7 → ^17.4.2 (closes #356)
- @types/node ^22 → ^25.7.0 (closes #357)
- typescript ^5.7 → ^6.0.3 (closes #358)
- vitest ^3 → ^4.1.6 (closes #359)

Website npm bumps:
- @types/node 22.10.2 → ^25.7.0 (closes #354)
- typescript 5.7.2 → ^6.0.3 (closes #353)
- react/react-dom ^19.2.5 → ^19.2.6 (closes #352 group)

github-actions bumps:
- actions/checkout v4 → v6 (closes #351)
- actions/setup-node v4 → v6 (closes #350)

Verified:
- npm run build clean against new TypeScript 6 + vitest 4 stack
- npm test 903 / 903 pass
- website npm run build clean (next 16.2.6 + react 19.2.6 + ts 6 stack
  generates 5 static pages, finalises optimisation)
- No source changes were needed for any breaking-version bump — the
  agent-sdk 0.3.x removal of unstable_v2_createSession etc. is not on
  any path agentmemory exercises (we use query() + the existing
  AgentSDKProvider shape).

Plus README refresh on the Works-with-every-agent grid:
- Added OpenHuman entry (tinyhumansai/openhuman) pointing at the
  recently-landed Memory trait backend (PR tinyhumansai/openhuman#1743)
- Added pi as a native-plugin entry (we already ship
  integrations/pi/security.ts and the plugin file in-tree)
- Reordered the grid so native-plugin agents sit in row 1 and
  MCP-only / REST-only agents sit in row 2
- Updated sub-text on Claude Code / Codex CLI / OpenClaw / Hermes / pi
  to consistently say "native plugin" rather than mixing "plugin",
  "skills", "hooks". Counter on assets/tags/section-agents.svg moved
  from "16 integrations" to "15 integrations" because we dropped the
  meta "Any agent" + "Claude SDK" cells that double-counted REST and
  the AgentSDKProvider path already represented by Claude Code.

* docs(readme): swap pi logo to first-party SVG

Drops the generic GitHub-org avatar for the brand-mark SVG. Lives in
assets/agents/ so the README + future grid renders reach it directly.
2026-05-15 11:03:36 +01:00
Rohit Ghumare 8b98432853 chore(release): v0.9.13 — env-example + init + CJK + load bench + deploy templates + Gemini GA bumps (#383)
* chore(release): v0.9.13 — env-example discovery + CJK tokenizer + load
harness + deploy templates + Gemini GA bumps + 14 advisories closed

Six PRs landed since v0.9.12:

- #372 .env.example discovery (this commit) — repo-root template + `init`
  CLI command + CI sync-checker
- #362 CJK BM25 tokenizer (`@node-rs/jieba` + tiny-segmenter + Hangul)
- #363 `benchmark/load-100k.ts` harness with p50/p90/p99 + per-release
  results dir
- #361 one-click deploy templates for fly.io / Railway / Render / Coolify
  (multi-stage Dockerfile, `iiidev/iii` base, `gosu` privilege drop,
  first-boot HMAC, verified end-to-end on fly.io)
- #364 Python ecosystem via `iii-sdk` example (replaces closed PR #360)
- #370 Gemini GA bumps (LLM default → gemini-2.5-flash, embedding →
  gemini-embedding-001 + L2-norm + 768 dims)

Plus 14 open Dependabot advisories closed in PR #348 via Next.js → 16.2.6
and PostCSS → 8.5.10 overrides.

Bumped:

- src/version.ts: VERSION 0.9.12 → 0.9.13
- package.json: 0.9.12 → 0.9.13, files += ".env.example", build script
  copies .env.example into dist/
- packages/mcp/package.json: 0.9.12 → 0.9.13 (lockstep with main)
- plugin/.claude-plugin/plugin.json, plugin/.codex-plugin/plugin.json:
  0.9.12 → 0.9.13
- src/types.ts: ExportData.version union extended with "0.9.13"
- src/functions/export-import.ts: supportedVersions Set extended
- test/export-import.test.ts: expected version updated

New surface:

- .env.example at repo root — every env var read by src/ documented in
  one place, grouped by surface (LLM, embedding, auth, search tuning,
  behaviour flags, CLI runtime, ports, iii engine pin, Claude Code
  bridge, Obsidian export). Every line commented out by default so the
  file is a template.
- agentmemory init — copies bundled .env.example to ~/.agentmemory/.env
  if absent, refuses to overwrite, prints a diff command. Wired into
  CLI dispatch + help block.
- scripts/check-env-example.mjs — walks src/ for env-read patterns,
  fails CI on drift in either direction. Plugged into ci.yml after
  npm test. Initial bootstrap: 60 keys in sync.

Verified: npm test 903/903, npm run build clean, init smoke pass
(creates ~/.agentmemory/.env on first run, refuses overwrite on second).

* fix(init): atomic copy via COPYFILE_EXCL; address CodeRabbit review

Two valid findings from the CodeRabbit pass on PR #383.

1. `runInit` race between existsSync(target) + copyFile(template, target).
   A parallel `agentmemory init` (or any other process touching
   ~/.agentmemory/.env between the two calls) would silently overwrite
   the config the operator just wrote. Switch to a single atomic
   `copyFile(template, target, fsConstants.COPYFILE_EXCL)` and treat
   the EEXIST error as the "already configured" signal — same warning
   + diff hint as before, but the check + copy now happen in one
   syscall so they cannot race. Other failure paths still surface as
   process exit 1.

2. Comment on `scripts/check-env-example.mjs::walk` claimed it matched
   ".ts / .mts / .mjs" but the regex also matched ".js". Rewrote the
   comment to match the regex (".ts / .mts / .mjs / .js"). Same comment
   pass: noted that test/ never enters because the walk is rooted at
   src/, not because of an explicit skip.

Skipped findings:

- WHAT-style comment on `findEnvExample` — kept a one-liner explaining
  the package-vs-source priority since both paths are real; reduced
  the block from 4 lines to 2 instead of removing it entirely.
- "Add trailing newline to .env.example" — file already ends with `\n`
  (verified `tail -c 5` shows `tion\n`).

Verified locally:
- `npm run build` clean.
- `npm test` 903 / 903 pass.
- First `agentmemory init` against a clean HOME creates the file.
- Second init against the same HOME hits EEXIST and prints the
  "leaving it untouched" warning + diff hint without overwriting.
- `node scripts/check-env-example.mjs` — in sync (60 keys).
v0.9.13
2026-05-15 10:11:37 +01:00
Rohit Ghumare 4b354b7bd8 chore(providers): bump Gemini defaults to current GA models (#370)
* chore(providers): bump Gemini defaults to current GA models

Bundles two upstream PRs into one chore — both are blocking real users
today and both are simple default-string bumps with no API contract
change.

LLM default (was PR #368, @yut304)
- `gemini-2.0-flash` is deprecated in Google's Gemini API and returns
  429 rate-limit errors under load. Replace the default with
  `gemini-flash-latest`. Users on a pinned `GEMINI_MODEL` in
  `~/.agentmemory/.env` are unaffected.

Embedding default (was PR #246, @AmmarSaleh50)
- `text-embedding-004` is deprecated (shutdown Jan 14 2026). Replace
  with `gemini-embedding-001` (GA): 100+ languages, MRL dims
  (768 / 1536 / 3072), 2048-token input.
- URL path changes from `:batchEmbedContent` to `:batchEmbedContents`
  (plural — the new model's batch endpoint).
- Each request now sends `outputDimensionality: 768` so the returned
  vectors match the existing index dim guard from #248 — no
  reindex needed.
- L2-normalize each returned vector before pushing to the result
  array. `gemini-embedding-001` does not normalize by default,
  unlike `text-embedding-004`. Without this the cosine-similarity
  math elsewhere in the search pipeline (which assumes unit-length
  vectors) collapses.

Verified
- `npm test` clean: 903 / 903.
- `npm run build` clean.

Closes #368, closes #246.

* fix(gemini): pin LLM default to gemini-2.5-flash + warn-once on zero-norm

Addresses CodeRabbit findings on PR #370.

1. Pin Gemini LLM default to gemini-2.5-flash.

   `gemini-flash-latest` is a moving alias that points to whatever
   Google promotes next. Production behaviour should be deterministic
   from a release perspective — users who upgrade agentmemory should
   not also get a Gemini model rotation in the same step. Switch the
   default to the current stable GA model `gemini-2.5-flash`.
   Users who want the moving alias keep getting it via
   `GEMINI_MODEL=gemini-flash-latest` in `~/.agentmemory/.env`.

2. Warn-once on zero-norm embedding in l2Normalize.

   `gemini-embedding-001` can return a zero-norm vector for
   degenerate input. The previous code silently returned the zero
   vector — downstream cosine-similarity math then divides by zero
   and the call site sees `NaN` scores with no signal as to why.

   Emit a one-time stderr warning naming the model + vector length
   so operators can correlate index quality dips with upstream
   embedding regressions. Behaviour otherwise unchanged: return the
   zero vector and let BM25 carry the search signal.

   Throwing was the other option — rejected because a single bad
   embedding in a 100-item batch would abort the whole batch and
   surface as an indexing pipeline halt. Soft-fail + warn matches
   the rest of the embedding provider error handling.

Skipped finding:

- `outputDimensionality` → `output_dimensionality` snake_case rename.
  CodeRabbit asserts the REST API expects snake_case. The Gemini
  REST API actually uses camelCase on the wire — confirmed against
  ai.google.dev/api/embeddings (field labelled
  `outputDimensionality` in the REST schema; the Python SDK alone
  uses snake_case and translates internally). Current code is
  correct as-shipped; the snake_case rename would silently break
  the dim override.

Verified: 903 / 903 tests pass; build clean.
2026-05-14 13:35:02 +01:00
Rohit Ghumare a9c3a59da8 feat(deploy): fly.io / Railway / Render one-click templates (#361)
* feat(deploy): add fly.io / Railway / Render one-click templates

Closes #343.

Lets operators stand up agentmemory on managed infrastructure without
rolling their own Docker host. Each template extends the published
rohitghumare64/agentmemory:latest image, mounts persistent storage at
/data, generates the HMAC secret on first boot via openssl rand into
/data/.hmac (chmod 600, printed once to stdout), and ships
AGENTMEMORY_REQUIRE_HTTPS=1 by default so the v0.9.12 plaintext-bearer
guard refuses to leak tokens over non-loopback HTTP if a TLS upstream
gets misconfigured.

Only port 3111 (REST API) is exposed publicly. The viewer on 3113
stays bound to localhost inside the container; every README documents
the SSH-tunnel pattern. The main README gains a Deploy section with
three Deploy-to-platform shield buttons linking to the platform's
URL-encoded one-click flow, plus the deploy/ subtree with per-platform
docs covering HMAC capture, rotation, /data backup, viewer access,
cost floor, and known caveats.

* fix(deploy): run as non-root, placeholder app names, drop invalid Render badge

Addresses CodeRabbit findings on PR #361.

Dockerfiles (fly/railway/render): drop `USER root` + RUN chmod — those
left the container running as root after build. Replaced with
`COPY --chmod=0755 --chown=65532:65532` so the entrypoint script lands
with the correct permissions and owner without ever switching off the
distroless image's default nonroot user. Added an explicit
`USER 65532:65532` directive after COPY to make the runtime user
intent-visible to reviewers.

fly/README.md: every command referenced the hardcoded app name
`agentmemory`. Since the global `agentmemory` slug on Fly is almost
certainly taken, the first `fly launch --name agentmemory` fails and
the rest of the volume/log commands point at a non-existent app. Walk
users through setting `APP="agentmemory-$(whoami)"` once at the top and
reference `$APP` everywhere (launch / volumes / logs / proxy / ssh /
backup) — and derive the volume name (`agentmemory_data` style) from
`$APP` so it stays consistent.

railway/README.md: the previous `railway ssh --service agentmemory --
-L 3113:localhost:3113` snippet was invalid — Railway's CLI ssh does
not support port forwarding. Replaced with a quick in-container curl
check plus two valid browser paths: Railway TCP Proxy (the recommended
option, dashboard → Networking → TCP Proxy on container port 3113) and
an in-container sshd over a TCP Proxy if true SSH tunneling is needed.

README.md: removed the "Deploy to Render" badge. Render's deploy URL
requires `render.yaml` at the repository root, which we keep clean.
The `deploy/render/` blueprint stays in-tree for users who set up the
Render Blueprint flow manually — the deploy section now points readers
there explicitly.

Skipped: the entrypoint.sh SECRET-shape regex check. `openssl rand
-hex 32` under `set -eu` already exits non-zero on any failure path
and always emits exactly 64 hex characters when it succeeds — a
post-hoc regex would only fire on a kernel-level surprise we can't
recover from anyway.

* fix(deploy): self-contained Dockerfiles (no phantom base image)

Templates previously did `FROM rohitghumare64/agentmemory:latest`, an
image that does not exist on Docker Hub. agentmemory ships via npm
(`@agentmemory/agentmemory`) and runs against the `iii` engine binary
fetched from the `iii-hq/iii` GitHub release. There is no first-party
agentmemory Docker image yet.

Rewrote all three Dockerfiles to be self-contained against
`node:22-slim`:

- `apt-get` curl / openssl / ca-certificates / tini for the runtime
  prereqs (openssl for the first-boot HMAC, tini for clean PID 1).
- Download the iii binary from
  `github.com/iii-hq/iii/releases/download/iii/v${III_VERSION}/...`
  matching the build host's arch (uname -m).
- `npm install -g @agentmemory/agentmemory@${AGENTMEMORY_VERSION}` with
  `--omit=optional` so the CJK native deps (added in PR #362) don't
  fail the build on platforms without musl/glibc prebuilds.
- `mkdir -p /data && chown node:node /data` so the existing
  first-boot entrypoint can write `/data/.hmac` without root.
- `USER node:node` (UID 1000, the standard non-root user in the node
  image) instead of the made-up `65532:65532` that assumed a
  distroless base.

Both `AGENTMEMORY_VERSION` and `III_VERSION` are `ARG`s so platform
operators can pin a specific release through their dashboard's
build-args UI without editing the Dockerfile.

README updates:

- Top-level `README.md` and `deploy/README.md` no longer claim users
  pull a pre-published image; the wording now matches what each
  template actually does (build from source on the platform's builder).
- Per-platform "Known caveats" sections drop the
  `rohitghumare64/agentmemory:latest` line and replace it with notes
  about build-time, cache reuse, and how to pin via build args.
- `deploy/render/README.md` drops the obsolete `&imgURL=...` Deploy
  Hook query-string trick and replaces it with
  `AGENTMEMORY_VERSION` build-arg guidance.
- `deploy/README.md` switches "Integration clients (Hermes, OpenClaw,
  pi)" → "Integration plugins" to match the scrub convention from the
  earlier PR-body edit.

* feat(deploy): add Coolify self-hosted template

Adds deploy/coolify/ alongside the existing fly / Railway / Render
templates. Coolify (https://coolify.io/self-hosted) is the
self-hosted-on-your-own-VPS option for operators who don't want their
memories on a third-party managed plane.

Same self-contained Dockerfile pattern as the other three (node:22-slim
+ iii binary + npm package, USER node, build-args for version pinning)
plus a docker-compose.yml that Coolify's "Docker Compose" build pack
consumes directly. Compose layer adds a HEALTHCHECK, a named volume
for /data, and json-file log rotation matching the rest of the deploy
surface.

README walks the operator through:

- Coolify dashboard flow (New Application -> Public Repository ->
  Docker Compose build pack -> Base Directory: deploy/coolify).
- Capturing the first-boot HMAC secret from the Logs tab.
- Viewer access via SSH tunnel from the Coolify host (port 3113 stays
  internal by default) and the optional second-domain + basic-auth
  pattern for sharing it.
- HMAC rotation, /data backup paths (Restic / Borg / rsync / Coolify
  Backups), and VPS cost-floor pointers (Hetzner CX22, DO Basic
  Droplet, Vultr).

Top-level deploy/README.md and README.md updated to list the new
template and explain when to pick it (already running a VPS, want a
self-hosted control plane, don't want a third-party host holding your
memories).

* fix(deploy): COPY iii from iiidev/iii image, drop tarball download

Replaces the curl + tar + chmod sequence in all four Dockerfiles
(fly / railway / render / coolify) with a single multi-stage
COPY --from=iiidev/iii:${III_VERSION} /app/iii /usr/local/bin/iii.

Why:

- Drops the supply-chain risk CodeRabbit flagged on PR #361
  (curl-and-extract without checksum verification). iiidev/iii is the
  same image agentmemory's docker-compose.yml already pulls — using
  Docker Hub's content-addressed manifest as the integrity boundary
  is the same trust we already extend on the local Compose path.
- Drops the arch case statement (BuildKit picks the right manifest
  entry automatically from the multi-arch tag).
- Drops 3 of the 4 apt-get packages from the bookkeeping side; curl
  stays for the eventual HEALTHCHECK probe.

The iii binary lives at /app/iii inside the distroless iiidev/iii
image (per github.com/iii-hq/iii engine/Dockerfile), and COPY into
node:22-slim lands it at /usr/local/bin/iii with 755 permissions so
any USER can exec it.

ARG III_VERSION is now declared before the first FROM so it can be
interpolated into the iii-image stage tag. ARG AGENTMEMORY_VERSION
stays in the final stage where the npm install consumes it.

* fix(deploy): rewire iii config + /data perms + platform port binding

Audit against fly.io / Railway / Render / Coolify docs and the
agentmemory CLI source surfaced four bugs that would prevent every
template from actually serving traffic on a managed host. This commit
addresses all four together because they share the same fix surface
(entrypoint + Dockerfile + platform config) and shipping them
independently would leave intermediate states broken.

1. iii config bound 127.0.0.1 + used relative ./data paths.

   The npm-bundled @agentmemory/agentmemory/dist/iii-config.yaml hard-
   codes `host: 127.0.0.1` on iii-http and `file_path: ./data/...` on
   iii-state / iii-stream. agentmemory CLI's findIiiConfig() returns
   the first existing candidate from a 3-item list with the npm bundle
   at position 0, so a sibling iii-config.yaml in cwd does not
   override. On any managed platform the result is the REST API
   binding loopback (router can't reach) and state writing to a
   directory that's not the persistent mount.

   Fix: the entrypoint overwrites the bundled file at boot with a
   container-tuned config (host 0.0.0.0, /data/... absolute paths,
   no iii-exec because the CLI orchestrates the worker over WS).
   This needs the entrypoint to run as root, hence the Dockerfile
   change below.

2. /data volume permissions root:root vs USER node.

   Every managed platform mounts persistent volumes root-owned 0755.
   The earlier Dockerfile ran `RUN mkdir -p /data && chown -R node`
   at build time, but the volume overlay at runtime replaces /data
   with the platform's empty root-owned mountpoint, leaving the
   `node` USER unable to write state_store.db / .hmac.

   Fix: stay USER root through the image, add `gosu` to apt, do the
   chown in entrypoint.sh against the *runtime* /data, then drop to
   the unprivileged `node` user via `exec gosu node:node agentmemory`
   before exec'ing the CLI. Symmetric to the iii-init busybox sidecar
   pattern in the repo-root docker-compose.yml.

3. Render's PORT injection broke the published port.

   Render auto-sets PORT (default 10000) and routes the public proxy
   to that port; the container then must bind 0.0.0.0:$PORT. Our
   container always binds 3111 (Dockerfile EXPOSE + iii-http config),
   so Render's proxy was forwarding to 10000 and getting connection
   refused.

   Fix: render.yaml now sets envVars PORT=3111 to override Render's
   default, plus exposes AGENTMEMORY_VERSION / III_VERSION as envVars
   (Render translates envVars into docker build args automatically).
   Also drops the AGENTMEMORY_REQUIRE_HTTPS / AGENTMEMORY_HMAC_FILE /
   AGENTMEMORY_DATA_DIR entries — server-side code never reads any of
   them; entrypoint reads HMAC_FILE / DATA_DIR via ${VAR:-default}
   so defaults already apply.

4. Coolify compose `ports:` bypassed Traefik.

   Per Coolify docs, `ports: ["3111:3111"]` binds the port directly on
   the host outside of the proxy network. The deploy would have been
   reachable on `http://<host>:3111` with no TLS termination and no
   domain routing.

   Fix: replaced `ports:` with `expose:` so the port is only
   reachable on the internal proxy network. Operator now sets the
   service domain in the Coolify UI as `https://<fqdn>:3111` (the
   `:3111` is Coolify's proxy hint, not a public port). Also added
   `SERVICE_FQDN_AGENTMEMORY_3111` to the environment so the
   container can know its public URL via Coolify's magic-env
   convention.

Other minor fixes:

- railway.json adds `requiredMountPath: /data` so Railway fails fast
  if the operator forgets to attach a volume.
- fly.toml drops the dead-letter [env] block (server code does not
  read AGENTMEMORY_REQUIRE_HTTPS / AGENTMEMORY_HMAC_FILE /
  AGENTMEMORY_DATA_DIR; entrypoint defaults handle the latter two).
- All 4 entrypoints unified — same script, same heredoc, copy with
  `cp -p` at build time.
- READMEs updated to reflect the new bound port story, the manual
  Render Blueprint flow (Render's deploy-button auto-detection only
  scans repo-root render.yaml), the Coolify proxy / domain pattern,
  and the gosu privilege drop.

Best-practice audit results (fly.toml, railway.json, render.yaml,
docker-compose.yml) verified against current platform docs — fly.toml
schema is clean, railway.json gets the requiredMountPath addition,
render.yaml + coolify compose get the rewires above.

* fix(deploy): pin iii-sdk to 0.11.2 via npm overrides

agentmemory@0.9.12 declares `iii-sdk: ^0.11.2`, which `npm install`
caret-resolves to **0.11.6** as of writing. That bumps the worker
SDK ahead of the engine the agentmemory repo pins (iii v0.11.2).
The two versions are wire-compatible in the calls agentmemory uses
today, but the policy intent in the repo is single-version lockstep
until the v0.11.6 sandbox-everything model is wired through — running
SDK 0.11.6 against engine 0.11.2 in production silently re-introduces
the very drift the AGENTMEMORY_III_VERSION pin exists to prevent.

`npm install -g` ignores the `overrides` field, so we cannot fix this
with the existing global install. Switch each Dockerfile to a local
install under /opt/agentmemory with a one-line package.json carrying:

    overrides: { iii-sdk: 0.11.2 }

then symlink the produced `node_modules/.bin/agentmemory` into
`/usr/local/bin/agentmemory` so the entrypoint invocation stays
identical. Verified locally that this resolves
`node_modules/iii-sdk/package.json` to version 0.11.2 (was 0.11.6
without the override).

Side effects:
- Entrypoint III_CONFIG path moves from
  `/usr/local/lib/node_modules/@agentmemory/agentmemory/dist/iii-config.yaml`
  (global) to
  `/opt/agentmemory/node_modules/@agentmemory/agentmemory/dist/iii-config.yaml`
  (local). agentmemory CLI's findIiiConfig() resolves __dirname
  through the symlink to the local install so candidate-0 lookup
  still hits the file we overwrite.
- New ARG `III_SDK_VERSION=0.11.2` declared in every Dockerfile,
  surfaced as a `build.args` field in coolify's compose and as an
  envVar in render.yaml so operators can pin a different SDK version
  if they manually migrate to engine 0.11.6+ down the line.
- AGENTMEMORY_III_VERSION env now baked into the image (matches the
  engine pin) so the CLI's iii-binary download fallback path also
  resolves to the right version if PATH lookup ever misses.

* fix(deploy): set TINI_SUBREAPER=1 so tini reaps zombies on Fly

End-to-end deploy to fly.io surfaced the warning:

    [WARN  tini (645)] Tini is not running as PID 1 and isn't
    registered as a child subreaper. Zombie processes will not be
    re-parented to Tini, so zombie reaping won't work.

Fly's init system holds PID 1, so tini lands as a child process and
the agentmemory CLI (which spawns iii detached) ends up with no
process reaping its eventual zombies. Setting TINI_SUBREAPER=1
triggers prctl(PR_SET_CHILD_SUBREAPER, 1) so tini still reaps
descendant zombies even when it isn't PID 1. Same fix applies to
Railway and Render (which also wrap our entrypoint under their own
init) and to Coolify when the operator runs the container under any
PID-1-grabbing supervisor like systemd-nspawn.

Verified end-to-end against fly.io:

- App agentmemory-ghumare64 deployed via remote builder
- 114 MB image (iii engine + node:22-slim + npm packages)
- 1 GB volume in iad provisioned and mounted
- First-boot HMAC generated and persisted to /data/.hmac
- iii-engine + agentmemory worker came up clean
- Worker registered with iii engine over ws://localhost:49134
- /agentmemory/livez returned 200 in 220 ms
- Bearer auth gating verified: 401 without secret, 200/201 with it
- POST /agentmemory/observe ingested a synthetic observation
- POST /agentmemory/smart-search returned a valid response envelope

Only outstanding log noise after this commit is a Node punycode
deprecation warning from a transitive dep — not actionable from this
side.

* fix(deploy): backfill TINI_SUBREAPER=1 on railway + render Dockerfiles

Previous commit missed these two — same reasoning applies (Railway and
Render both wrap our entrypoint under their own platform init, so tini
lands as a non-PID-1 child and zombie reaping needs the prctl
subreaper bit set).

* fix(deploy): apply live-fly-deploy findings across all templates

Real end-to-end deploy on fly.io surfaced four refinements that
generalise across the other three templates. This commit ports them.

1. Health-check grace_period: 10 s -> 30 s.

   Measured cold start on a fresh fly.io machine in `iad`:

       machine image prepared :  5.1 s
       volume mount + format  :  2.5 s
       firecracker boot       :  1.0 s
       entrypoint + chown     :  0.5 s
       iii-engine ready       :  3.0 s
       agentmemory worker reg :  2.0 s
       --------------------------------
       healthcheck passes     : ~9-10 s

   `grace_period = "10s"` (fly.toml) and `start_period: 20s` (coolify
   compose + Dockerfile HEALTHCHECK) were both within the noise of the
   measured time. Bumped both to 30 s — gives a 3x safety margin
   without affecting normal-operation health-check cadence.

   Railway and Render manage their own initial-deploy grace windows
   server-side, so railway.json's `healthcheckTimeout: 30` (per-attempt)
   and render.yaml's `healthCheckPath` stay as-is.

2. Documented LLM + embedding provider env-var menu in deploy/README.md.

   Live deploy logs surface a clear warning:

       [agentmemory] No LLM provider key found
       (ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY,
        MINIMAX_API_KEY). LLM-backed compression and summarization are
       DISABLED -- using no-op provider.

   The deploy READMEs never told operators how to opt in. Added a
   table covering ANTHROPIC / GEMINI / OPENROUTER for LLM,
   OPENAI / VOYAGE for embeddings, plus AGENTMEMORY_AUTO_COMPRESS and
   AGENTMEMORY_INJECT_CONTEXT for the corresponding behaviour flags.
   Each platform's variable-injection surface (flyctl secrets,
   dashboard Environment tab) is named in the same table.

3. Captured cold-start budget in deploy/README.md.

   Same step-by-step measured timing above is now in the shared
   README so operators have a concrete expectation rather than
   guessing why first-byte after `fly deploy` takes ten seconds. Notes
   that every template's start window is sized for 3x of this.

4. fly README: documented shared-IPv4 + dedicated-IPv6 default.

   Fly assigns one of each by default at no charge. Legacy clients
   without SNI that need a dedicated IPv4 can buy one for $2/month
   via `fly ips allocate-v4`. Added the build-time stats observed in
   the real deploy (~30 s first build, ~10 s cached rebuild, 114 MB
   image) to the Known Caveats section so operators sizing
   build-minute budgets have real numbers.

No code paths changed -- only TOML / YAML / Markdown surface tweaks
and one HEALTHCHECK `start_period` bump in the coolify Dockerfile.
2026-05-14 11:22:58 +01:00
Rohit Ghumare 8c3418cc61 feat(search): CJK tokenizer for BM25 (#362)
* feat(search): CJK tokenizer for BM25 (closes #344)

BM25 previously tokenized Chinese, Japanese, and Korean memories as a
single sentence-long token because CJK scripts don't put spaces between
words, leaving the keyword leg of hybrid search as dead weight for CJK
users. Greek, Cyrillic, accented Latin already work since v0.9.12 (#327)
because they split cleanly on whitespace.

Detect CJK by Unicode block, then route per-run:
- Han: @node-rs/jieba (native, no model download), Jieba.cut(text, hmm)
- Kana (with mixed kanji): tiny-segmenter (pure JS, ~25KB)
- Hangul: rule-based, each [가-힯]+ run is one token

Both segmenters declared in optionalDependencies so the bundle stays
small for users who don't need them. If the dep isn't installed the
segmenter soft-falls to whole-run tokenization (the pre-fix behavior)
and prints a one-time stderr hint per language. Non-CJK input goes
through the existing tokenize path unchanged — zero behavior change for
the Latin/Greek/Cyrillic/Hebrew/Arabic paths.

New fixtures in test/search-index.test.ts cover Chinese (项目记忆存储),
Japanese (プロジェクト記憶), and Korean (프로젝트 메모리 저장소). Existing
Greek + ASCII tests stay green. Suite grew 899 -> 902 tests.

* fix(cjk): preserve source order across mixed CJK + non-CJK runs

The previous segmentCjk walked the input three times — once for Han/kana
runs, once for Hangul runs, once for non-CJK pieces — and concatenated
the results. For a mixed string like "abc 메모리 def 项目 ghi" that
emitted ["메모리","项目","abc","def","ghi"], dropping the original
positional information BM25 phrase queries and any downstream consumer
keyed on token order rely on.

Replace with a single left-to-right pass over CJK_RUN_RE: emit any
intervening non-CJK substring (trimmed), then route the matched CJK run
to segmentHangul / segmentKana / segmentHan based on which script it
contains, and finally emit any trailing non-CJK piece.

Added a regression test asserting source order across four mixed-script
strings. Drops the now-unused HAN_KANA_RUN_RE / HANGUL_RUN_RE regex
constants.
2026-05-13 22:20:24 +01:00
Rohit Ghumare f365d38c75 docs(python): iii-sdk example replaces standalone REST client (#364)
Closes #342.

agentmemory functions register as mem::remember, mem::observe,
mem::context, mem::smart-search, mem::forget via sdk.registerFunction.
iii-sdk already ships in Python, Rust, and Node — any of them can call
those functions directly over the iii WebSocket transport. A standalone
Python REST client would duplicate the transport layer and fragment the
cross-language story.

This patch adds:
- examples/python/quickstart.py — minimal save + smart-search loop.
- examples/python/observe_and_recall.py — observation ingest + context
  rendering at a token budget.
- examples/python/README.md — function map + install + usage.
- README "Programmatic access (Python / Rust / Node)" section pointing
  at the example and the three SDK install commands.

REST on :3111 is unchanged for hosts without an iii runtime.
2026-05-13 21:08:31 +01:00
Rohit Ghumare d1517465ca feat(bench): load-100k.ts harness with p50/p90/p99 output (#363)
Adds a reproducible, dependency-free load harness so we can answer
"what's p99 at 100k memories under concurrency 100?" with a number
instead of a shrug.

The harness seeds N synthetic memories against a local agentmemory
daemon (defaults to http://localhost:3111, optional autostart via
AGENTMEMORY_BENCH_AUTOSTART=1), then drives a matrix of
(N, concurrency, endpoint) cells with hand-rolled Promise.allSettled
batches. Per-request latency is collected via performance.now() and
summarized as nearest-rank p50 / p90 / p99 plus min / max / errors
and wall-clock throughput. Results are written to
benchmark/results/load-100k-<short-git-sha>.json with a
schema_version field so future format changes don't silently break
consumers.

Defaults match issue #346: N in {1000, 10000, 100000} x C in
{1, 10, 100} x three endpoints (POST /agentmemory/remember,
POST /agentmemory/smart-search, GET /agentmemory/memories?latest=true).
Each cell issues BENCH_OPS=200 requests by default — enough samples
for stable p99 without dragging a 100k-seed run past tens of minutes.

Content is generated by a small noun/verb/concept vocabulary fed by a
mulberry32(BENCH_SEED) PRNG so re-running the harness against the
same daemon build yields the same seed corpus. Reproducibility, not
realism, is the point — latency variance comes from the daemon, not
JSON payload jitter.

Files:
- benchmark/load-100k.ts: main harness
- benchmark/lib/percentiles.ts: zero-dep pXX helper, nearest-rank
- benchmark/README.md: how to run, what gets measured, where results
  land, and why p99 is the number you want for capacity planning
- benchmark/results/load-100k-96c0ed0.json: example result from a
  small-N (N=1000, C=10) verification run against a fresh daemon
- package.json: wires `npm run bench:load`
- CHANGELOG.md: Unreleased entry + a Performance section placeholder
  describing where per-release numbers should land going forward

Verified locally at BENCH_N=1000 BENCH_C=10 BENCH_OPS=200 — three
cells, zero errors, JSON written. Full 100k matrix is intentionally
deferred to CI/release time. Closes #346.
2026-05-13 21:06:43 +01:00
Rohit Ghumare 96c0ed017d chore(security): add dependabot config + close 14 open advisories (#348)
- Add .github/dependabot.yml covering npm (root, website, 3 integrations) +
  github-actions. Weekly cadence, grouped minor/patch, scoped commit prefixes.
- Bump website next ^16.2.4 → ^16.2.6: closes 13 open advisories
  (middleware/proxy bypass, SSRF, DoS, CSP-nonce XSS, image-opt DoS, cache
  poisoning, beforeInteractive XSS, segment-prefetch routes).
- Pin website postcss → ^8.5.10 via overrides: closes XSS via unescaped
  </style> in CSS stringify output (resolves to 8.5.14).

Verified:
- `npm audit --omit=dev` → 0 vulnerabilities.
- `npm run build` clean on Next 16.2.6 (Turbopack).
2026-05-13 20:27:44 +01:00
Rohit Ghumare 87fae50b79 chore(release): v0.9.12 — BM25 unicode + vector live-write + viewer hardening (#337)
Three landed PRs since v0.9.11:
  - #327 (#295) — BM25 tokenizer now accepts non-ASCII (Greek,
    accented Latin, Hebrew, Arabic, Cyrillic), VectorIndex.add now
    actually called at runtime via vectorIndexAddGuarded helper with
    dim guard + input clip, migrateVectorIndex for dim migrations.
  - #326 (#277) — RetentionScore type no longer declares source
    twice; JSDoc back-compat note no longer shadowed.
  - #335 (#323) — viewer drops Google Fonts <link> (CSP-blocked),
    loadDashboard now surfaces load errors inline instead of
    sticking on "Loading dashboard…".

Bumping 0.9.11 -> 0.9.12 across the 9 standard files:
- package.json
- packages/mcp/package.json
- plugin/.claude-plugin/plugin.json
- plugin/.codex-plugin/plugin.json
- src/version.ts
- src/types.ts (ExportData.version literal)
- src/functions/export-import.ts (supportedVersions)
- test/export-import.test.ts (round-trip expectation)
- CHANGELOG.md (new 0.9.12 entry)

886 / 886 tests pass. Build clean.
v0.9.12
2026-05-13 16:17:01 +01:00
Matt Van Horn 1a706ca2d6 fix(integrations): warn when bearer auth crosses plaintext HTTP to non-loopback (#315)
* fix(integrations): warn when bearer auth crosses plaintext HTTP to non-loopback (#275)

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>

* test(integrations/315): edge cases for IPv6 loopback + LAN IPs + lookalike hostnames + docs

@mvanhorn's PR #315 lands the right plaintext-bearer-auth warn-once
pattern across three runtimes (hermes/python, openclaw/mjs,
pi/typescript). Reviewed cleanly. Adding test cases + docs before
merge so the contract is locked:

Test additions (test/integration-plaintext-http.test.ts pi suite,
+4 cases, 13 total in suite):

- IPv6 loopback (http://[::1]:3111) — URL parser strips brackets,
  hostname comes back as `::1`, the LOOPBACK_HOSTS set already covers
  this. Test pins the parsing semantic so a future refactor can't
  silently break it.

- Private LAN IPs (RFC1918 ranges 192.168.x.y, 10.x.x.x) — NOT
  loopback, guard MUST warn. RFC1918 was a deliberate design choice
  (loopback set stays minimal: localhost / 127.0.0.1 / ::1). LAN
  deployment behind a tunnel still leaks the bearer over the wire.

- No-secret short-circuit (guard with secret="" or undefined) — guard
  never fires when a bearer wouldn't actually be sent. Mirrors the
  usesPlaintextBearerAuth() early-return.

- Lookalike hostname (http://localhost.evil.com:3111) — `parsed.hostname`
  returns the full `localhost.evil.com` string, which is NOT in
  LOOPBACK_HOSTS. Guard warns. This pins the exact-match (vs prefix)
  loopback semantic.

Docs additions:

- integrations/pi/README.md and integrations/hermes/README.md now
  document AGENTMEMORY_REQUIRE_HTTPS in the env-vars table — same row
  shape as AGENTMEMORY_SECRET, names the env value (=1), describes the
  warn-vs-throw semantic, and lists the loopback hostnames the guard
  recognises.

No code changes to the guard itself — it's already correct. Pre-merge
hardening only.

899 / 899 tests pass on rebased branch. Build clean.

---------

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
2026-05-13 15:59:50 +01:00
Rohit Ghumare 24f6c5fb24 fix(search): BM25 unicode + vector index live-write — improved (closes #295, supersedes #296) (#327)
* fix #2: BM25 tokenizer now preserves Cyrillic/Unicode via \p{L}\p{N} regex

Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com>

* fix #1: add getVectorIndex/setVectorIndex + vector embedding in rebuildIndex

Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com>

* fix #1+#3: vectorIndex.add in remember/observe/compress + setVectorIndex in index.ts

Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com>

* fix: add migration script, Cyrillic BM25 tests, vector-populate tests, rebuildIndex vector test

Adds migrateVectorIndex utility for re-embedding when embedding provider dimensions change. Adds comprehensive test coverage for Cyrillic tokenization, vector index population on remember/rebuildIndex, and migration edge cases.

Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com>

* chore: remove internal research spec from PR branch

Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com>

* refactor(search): centralize vector-index writes via vectorIndexAddGuarded

Pre-merge hardening on top of @nik1t7n's PR #296.

Original PR had vectorIndex.add() inlined at four sites (remember,
observe, compress, rebuildIndex) with the same try/catch shape repeated
each time. That open-codes three sharp edges per site:

1. Missing dimension guard at the write boundary. Per #248, persistence
   load already refuses to start when persisted vectors mismatch the
   active provider's dimensions — but the live-write path had no
   symmetric check. A mis-configured embedding provider returning a
   different-length Float32Array would silently corrupt the in-memory
   index until next restart cleared it.

2. No input clipping. Memory.content can be arbitrarily large (the
   MemorySlot path even allows 20KB content per slot). Most embedding
   providers cap input around 8k tokens and 400 on overflow. A single
   oversize memory could throw at the provider and lose the vector
   entry; worse, on local-Ollama-style providers it can cost real
   inference time.

3. Inconsistent error logging (rebuildIndex had `catch {}` swallowing
   the error entirely, the other three sites logged via logger.warn
   with slightly different shape).

Consolidates into a single exported `vectorIndexAddGuarded()` in
src/functions/search.ts:

  - Reads vectorIndex + currentEmbeddingProvider from module-level
    state (same singletons the PR added).
  - Clips embed input to 16k chars (EMBED_MAX_CHARS) — well under
    every provider's documented limit, leaves headroom for
    chars-per-token variance across languages.
  - Validates embedding.length === provider.dimensions before
    calling vi.add(); logs structured warning and skips on mismatch.
  - Single logger.warn on embed failure with provider name + error
    message + indexed-item kind ("memory" | "observation" |
    "synthetic") + id.
  - Returns boolean (true on success) so callers can branch if they
    want, but every existing call site fire-and-forgets — the contract
    is "soft-fail: index entry skipped, upstream save still succeeds".

Rewrites the four call sites (remember.ts, observe.ts synthetic
branch, compress.ts post-LLM, search.ts rebuildIndex memories + obs
loops) to call vectorIndexAddGuarded() instead of the inline try/catch.

Side effect: cleans up the lingering `catch {}` in rebuildIndex that
was swallowing rebuild-time embed failures silently — every failure
now surfaces in logs at a consistent shape.

Tests:
- test/multimodal.test.ts mocked `../src/functions/search.js` and
  exposed only getSearchIndex; needed vectorIndexAddGuarded added to
  the mock surface or test fails with "No vectorIndexAddGuarded export
  is defined on the search.js mock". Added the export to the mock.
- All other tests (search-index Cyrillic, search rebuildIndex vector,
  vector-index-populate, migrate-vector-index) green on the helper
  refactor — no behavioural change beyond the three guards above.

886 / 886 tests pass. Build clean.

Out-of-scope (deferred to follow-ups):
- Blocking await on every save. The embed call is on the request hot
  path. For local Ollama / on-device providers this is fine; for
  network providers it adds 200ms-2s latency per remember/observe.
  Should be async / fire-and-forget with a write-behind queue —
  separate perf PR after this lands.
- Periodic persistence flush of the vector index. Today the index is
  serialized at process exit (per IndexPersistence). Runtime adds
  between flushes survive in memory but die on crash. Acceptable for
  this PR; future PR can add a debounced flush.
- Batching in rebuildIndex. Today the rebuild path embeds one doc at
  a time. embedBatch() exists on every provider and would be ~5-10x
  faster for cold restarts on large corpora. Defer.

* fix(search): tighter migrate guard + rebuild vector clear + test hygiene

Five reviewer findings applied. Five skipped with reason:

APPLIED:

1. migrate-vector-index.ts filter now requires m.content (and rejects
   empty/whitespace). Without this, undefined m.content would concat
   to "title undefined" and poison the embed input. Memory.content is
   declared string but #277-era data shapes had cases where it could
   be missing — defensive filter.

2. migrate-vector-index.ts success expression now derives from the
   failed counter (success: failed === 0). Previously hardcoded true
   so callers couldn't rely on the boolean alone.

3. search.ts rebuildIndex now calls vectorIndex?.clear() before the
   repopulation loops. Symmetric to the BM25 idx.clear() that was
   already there. Without this, memories deleted between runs leave
   orphan embeddings in the vector store after every rebuild.

4. test/vector-index-dimensions.test.ts assertion tightened from
   toBeGreaterThan(0) to toBe(1) for both totalProcessed and
   vectorSize. The seeded fixture has exactly one observation; loose
   assertion would hide regressions where the migration produced
   extra/duplicate vectors.

5. test/vector-index-populate.test.ts switched to beforeEach/afterEach
   for the setVectorIndex / setEmbeddingProvider lifecycle. Manual
   cleanup in each test body was easy to drift on additions.

PLUS:

6. compress.ts BM25 add now wrapped in try/catch matching the
   remember.ts pattern for parity. Helps cross-file symmetry even
   though SearchIndex.add doesn't throw on valid input.

SKIPPED (with reason):

- Wrapping vectorIndexAddGuarded calls in additional try/catch at
  remember.ts / observe.ts / compress.ts: the helper has internal
  try/catch (src/functions/search.ts:67-89). It NEVER throws. Adding
  outer try/catch is dead code. The reviewer missed the helper's
  contract.
- vector-index-dimensions.test.ts mockKV.delete -> real deletion: the
  tests in that file don't exercise deletion semantics, and
  migrateVectorIndex itself never calls kv.delete. Touching it would
  add noise without effect on any assertion.
- recordAudit + Promise.allSettled in remember.ts: out of scope.
  recordAudit is already called earlier in the function. Parallelizing
  cascade-update with vector-add changes failure semantics (cascade-
  update is currently fire-and-forget) — separate perf PR if wanted.
- validateDimensions(N) assertion in vector-index-dimensions.test.ts:
  vectorSize is the COUNT of stored vectors, not their dimensions.
  Adding a validateDimensions check on the result would require
  exposing the new index. Out of scope for assertion tightening.

886 / 886 tests pass. Build clean.

* fix(migrate): per-session isolation + dim guard + drop unused oldIndex param

Four reviewer findings applied. One skipped with reason:

APPLIED:

1. Per-session isolation in migrateVectorIndex observations phase. The
   previous shape wrapped the entire sessions for-loop in one try/catch
   — a single bad session (kv.list throws, embedBatch rejects, etc.)
   aborted every subsequent session and silently truncated the
   migration. The loop now has per-session try/catch; failures
   increment `failed`, append the session id to a new `failedSessions`
   array on the result, and the loop continues. The kv.list<sessions>
   call itself is now guarded separately — if THAT fails the whole
   migration aborts (no sessions to iterate), but with `failed`
   recorded and `failedSessions` empty so the caller can distinguish.

2. Drop `oldIndex` parameter from migrateVectorIndex signature. It was
   dead — no read path inside the function. Three test call sites
   updated to match the new signature
   migrateVectorIndex(kv, newProvider). Confused the API contract; if
   we ever DO need a previous-index argument it can come back with a
   real use case.

3. Dim guard on every embedding result inside migrateVectorIndex, in
   both the memories phase and the per-session observations phase.
   Extracted into a local isValidEmbedding() helper that mirrors the
   shape of search.ts::vectorIndexAddGuarded's guard: log structured
   warn + skip on mismatch. Without this, a misconfigured provider
   returning the wrong-length Float32Array would corrupt the rebuilt
   index, defeating the #248 persistence-load guard.

4. Rename `vi` -> `vectorIndex` in test/vector-index-populate.test.ts
   so the let binding doesn't shadow the vitest `vi` import. Practical
   impact: future tests in that file can use `vi.fn()` / `vi.spyOn()`
   without renaming this binding first. Cosmetic but cheap.

SKIPPED (with reason):

- Use embedBatch in rebuildIndex (search.ts): perf optimization, not
  correctness. Already called out as deferred in the previous review's
  commit message. Cold-restart speed matters but the fix here is
  bigger refactor (collect-into-batches with metadata, validate each,
  add only successful) and worth its own PR. Will pick up when we
  also do the write-behind queue for live-write batching.

Result envelope:
  Added `failedSessions: string[]` to MigrateVectorIndexResult so the
  caller can decide whether to retry per-session or just abort.
  Existing fields (success, totalProcessed, failed, vectorSize)
  unchanged in shape.

886 / 886 tests pass. Build clean.

* fix(migrate): attribute batch-level failures to correct count + distinguish list-fail

Two new reviewer findings on migrate-vector-index. Six earlier findings
re-flagged in the same review block had already been addressed in prior
commits on this branch (verified inline):

  - compress.ts BM25 try/catch (commit d2098a5)
  - migrate filter requires m.content (commit d2098a5)
  - migrate success: failed === 0 (commit d2098a5)
  - rebuildIndex vector clear (commit d2098a5)
  - observe.ts / remember.ts outer try/catch — SKIPPED with reason
    (vectorIndexAddGuarded has internal try/catch, NEVER throws)
  - mockKV.delete no-op — SKIPPED with reason (no test exercises it)

NEW FINDINGS APPLIED:

1. Catch block in memories phase counted +1 for the entire batch
   regardless of batch size. If kv.list returned 250 memories and
   embedBatch threw, `failed` reflected 1 missed embedding when
   reality is 250.

   textMems is now declared outside the try so the catch can read its
   length. If kv.list itself threw before textMems was populated
   (length 0), we fall back to +1 (something failed but the batch
   size is unknown). If embedBatch threw, `failed += textMems.length`
   so the counter is honest.

2. kv.list<sessions> failure path returned `failed: 1,
   failedSessions: []`. Indistinguishable from "0 sessions, all OK"
   from the caller's perspective — they can't tell whether the
   enumeration itself blew up or simply found no sessions.

   The path now pushes a marker string "<sessions-list-failed>" into
   failedSessions before returning. Marker keeps the existing schema
   (failedSessions: string[]) — no boolean flag added — and gives
   callers a sentinel they can grep for. Per-session entries continue
   to use real session ids, so the marker is unambiguous.

886 / 886 tests pass. Build clean.

* test: swap Cyrillic non-ASCII tokenizer fixtures for Greek

The two regression tests added in this branch for the BM25 non-ASCII
tokenizer fix used Russian-language fixtures
("Проверка памяти", "Тестируем поиск по кириллице", etc). Switching
to Greek-language equivalents ("Προβολή μνήμης",
"Δοκιμάζουμε αναζήτηση σε ελληνικά", etc).

Reasoning: the test fixtures need to exercise the same code path —
non-ASCII Unicode letters that the old ASCII-only \w regex stripped.
Greek code points (U+0370..U+03FF) hit \p{L} identically to Cyrillic
(U+0400..U+04FF), so the regression coverage is preserved. Greek is
also less likely to be misread as politically loaded content in a
public repo.

Test ids renamed from obs_cyrillic to obs_greek; test names from
"indexes and finds Cyrillic text" to "indexes and finds non-ASCII
(Greek) text" (and the mixed-script variant likewise). Branch name
stays as-is to keep PR continuity but the shipped test fixtures are
language-neutral going forward.

11/11 search-index tests pass with the swap. Full suite 886/886.

---------

Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com>
Co-authored-by: Nikita Nosov <20nik.nosov21@gmail.com>
2026-05-13 15:27:43 +01:00
Rohit Ghumare 117f49a52a fix(viewer): drop Google Fonts link + surface dashboard load errors (closes #323) (#335)
@hem57 reported the viewer dashboard tab stuck on "Loading
dashboard..." on Windows under v0.9.11 with CSP violations visible in
the browser console.

Two narrow fixes that together close the bug premise:

1. Drop the <link rel="stylesheet" href="https://fonts.googleapis.com/
   css2?..."> tag.

   The viewer ships a strict CSP (src/auth.ts:16-30):
     default-src 'none'
     style-src 'unsafe-inline'
     font-src 'self'

   Note that style-src does NOT include the Google Fonts origin and
   font-src does NOT include fonts.gstatic.com. Per CSP semantics
   style-src does not fall back to default-src, so the external
   stylesheet was blocked outright. On every viewer load the browser
   logged a CSP violation for the <link>, plus more for each blocked
   font file. On Windows Edge this produced the screenshot @hem57
   shared.

   The viewer's font stack already declares system fallbacks on every
   --font-* CSS variable (src/viewer/index.html:39-42):
     --font-display: 'Playfair Display', Georgia, ...
     --font-body:    'Lora', Georgia, serif;
     --font-ui:      'Inter', -apple-system, sans-serif;
     --font-mono:    'JetBrains Mono', 'SF Mono', ...

   Dropping the <link> falls back to the system stack with no visual
   loss on platforms that ship Georgia / SF Mono / system serifs (most
   macOS, Windows, Linux distros).

2. Wrap loadDashboard() body in try/catch that surfaces failures in
   the dashboard panel.

   Before this fix, any uncaught error in the Promise.all of apiGet
   calls (or in renderDashboard) left the dashboard stuck on the
   "Loading dashboard..." text forever, with no indication of what
   went wrong. apiGet() already swallows network/HTTP errors and
   returns null, so most errors are caught upstream — but renderDashboard
   can still throw on shape surprises (CSP-blocked event handler
   binding, undefined object access, etc.).

   The new catch renders an inline error block with the error message
   and a pointer to the browser console for full detail. Future
   reports of "stuck Loading" will at least come with a concrete error
   message instead of just a screenshot of the placeholder.

Out-of-scope (deferred to follow-up):

- CSP script-src-attr 'none' blocks inline event handlers in
  innerHTML-injected dynamic markup (oninput= / onchange= at lines
  2706, 2765, 2766, 2852). Browser logs a CSP violation per element
  but the handlers simply don't bind — the dashboard still loads.
  Eliminating these requires moving from inline strings to delegated
  event listeners, ~50 lines of refactor across four search/filter
  controls. Separate PR.

- Self-hosting the fonts (instead of dropping them) would preserve
  the design intent but adds 200-400KB of font files to the dist.
  Acceptable tradeoff for now: fonts go, system stack is fine.

Test plan:
- npm test — 877/877 pass.
- npm run build — tsdown clean, dist/viewer/index.html regenerated.
- Visual sanity check pending: open localhost:3113 in Chrome dev
  tools, confirm no CSP violations from font-related requests; force
  a 502 from one apiGet endpoint and confirm dashboard shows the
  error message instead of sticking.
2026-05-13 14:38:09 +01:00
Rohit Ghumare cba234d950 fix(types): remove duplicate source field in RetentionScore (closes #277) (#326)
cl0ckt0wer reported in #277 that RetentionScore declared source twice
(src/types.ts:835 and :842), both as ?"episodic" | "semantic". TS
silently accepts duplicate property declarations when types are
identical; runtime treats the second as the canonical declaration,
which means the documented JSDoc on the FIRST one (referring to #124,
pre-0.8.10 rows, scope probing semantics) was effectively dead code
to anyone reading the type definition.

Verified across src/ that no code path WRITES to source twice: grep
returned zero callers setting source on RetentionScore, so removing
the duplicate is a no-op at runtime. The kept copy retains the
#124-referencing JSDoc so callers still see the back-compat note.

Test impact: none (type-only). Full suite 877/877 unchanged.
2026-05-13 12:35:51 +01:00
Rohit Ghumare 25dddc4379 chore(release): v0.9.11 — Codex plugin platform + OpenClaw slot fix + website star button (#317)
Three landed PRs since v0.9.10:
- #311 — Codex plugin support (manifest + marketplace + Codex-shaped
  hooks subset, sharing the same scripts as the Claude Code plugin
  via CLAUDE_PLUGIN_ROOT OOTB-compat injection on Codex's hook
  engine)
- #310 — OpenClaw plugins.slots.memory now actually claimed via
  api.registerMemoryCapability({ promptBuilder }); older builds
  without the capability API still load via the existing hook-only
  path
- #316 — Star on GitHub button in the website hero CTA row, live
  stargazer count via api.github.com, 30-minute localStorage cache,
  graceful degrade on API failure

Bumping 0.9.10 -> 0.9.11 across the 8 standard files:
- package.json
- packages/mcp/package.json
- plugin/.claude-plugin/plugin.json
- plugin/.codex-plugin/plugin.json
- src/version.ts
- src/types.ts (ExportData.version literal)
- src/functions/export-import.ts (supportedVersions)
- test/export-import.test.ts (round-trip expectation)
- CHANGELOG.md (new 0.9.11 entry)

877 / 877 tests pass. Build clean.
v0.9.11
2026-05-13 00:21:27 +01:00
Rohit Ghumare 3f661d2e42 feat(website): add Star on GitHub button to hero CTA row (#316)
Hero only had START IN 60 SECONDS + SEE IT MOVE. Missing the obvious
CTA for visitors who landed via X/Reddit and want to bookmark before
installing.

Adds GitHubStarButton component next to the existing ghost button:

- ghost-style button matching the existing visual register
  (no new design surface introduced)
- star icon (inline SVG, currentColor) + STAR label + live
  star count
- count fetches once on mount from api.github.com/repos/<repo>
  (unauthenticated, 60 req/h limit per IP — fine for a marketing page)
- 30-minute localStorage cache keyed by repo so repeat visits don't
  re-hit the API
- graceful degradation: if fetch fails (offline, rate-limited,
  blocked), button still renders without the count

Only client-side ("use client") because we want the live count.
No deps added. Build clean (Next 16 static export).
2026-05-13 00:13:39 +01:00
Rohit Ghumare 48b11e8351 fix(openclaw): claim plugins.slots.memory via registerMemoryCapability (#310)
* fix(openclaw): claim plugins.slots.memory via registerMemoryCapability

The OpenClaw integration declared kind: "memory" in its manifest and
wired before_agent_start / agent_end hooks, but never called
api.registerMemoryCapability(...). On OpenClaw 2026.4.9+ that
left plugins.slots.memory = "agentmemory" reporting unavailable
even though the hooks worked end-to-end and the REST API was
healthy.

Per OpenClaw's plugin SDK declarations
(plugin-sdk/src/plugins/types.d.ts), api.registerMemoryCapability
takes a MemoryPluginCapability and is the canonical way to occupy
the memory slot. All capability fields are optional. The
promptBuilder field has signature:
  (params: { availableTools: Set<string>; citationsMode? }) => string[]

This commit:

- Calls api.registerMemoryCapability({ promptBuilder }) at register
  time when the host exposes it. promptBuilder returns three lines
  describing agentmemory as the active memory provider, including
  the configured base_url so the agent knows where recall is
  coming from.
- Guards on `typeof api.registerMemoryCapability === "function"`
  so older OpenClaw builds (pre-capability API) still load via
  the existing hook-only path. Net behaviour on older hosts is
  unchanged.
- Does NOT register a runtime adapter. OpenClaw's current
  MemoryRuntimeBackendConfig type is exactly { backend: "builtin" }
  or { backend: "qmd"; qmd?: { command?: string } }; both are
  in-process backends and don't fit an external REST service.
  The hook-driven recall + capture flow is the working
  integration path; runtime registration is documented as a
  follow-up gated on an upstream OpenClaw change to add an
  "external" backend variant.

README troubleshooting block adds a specific entry for the
"unavailable slot" symptom, pointing at the registerMemoryCapability
fix. The README also documents the runtime-adapter scope decision
so users running deep host integrations aren't surprised.

3 new tests in test/openclaw-plugin.test.ts assert:
1. registerMemoryCapability is called with a promptBuilder when
   the host supports it.
2. Plugin still registers hooks and does not throw on older hosts
   that don't expose registerMemoryCapability.
3. promptBuilder respects the configured base_url.

871 / 871 tests pass.

Verified against the openclaw npm package surface:
- api.registerMemoryCapability is declared at
  plugin-sdk/src/plugins/types.d.ts:2028
- MemoryPluginCapability is declared at
  plugin-sdk/src/plugins/memory-state.d.ts
- promptBuilder signature matches MemoryPromptSectionBuilder
- No import from openclaw/extensions/* needed (and that path is
  not in the package's exports map anyway)

This is a fresh implementation. The closed PR #302 attempted the
same end goal but imported memoryRuntime from a non-existent
path (openclaw/extensions/memory-core/runtime-api) and would have
failed with ERR_PACKAGE_PATH_NOT_EXPORTED at load time.

* fix(openclaw): accept params arg in promptBuilder to match MemoryPromptSectionBuilder

Reviewer flagged that promptBuilder was declared as () => [...] which
doesn't match the MemoryPluginCapability contract verified from
openclaw@2026.5.7 (plugin-sdk/src/plugins/memory-state.d.ts):

  type MemoryPromptSectionBuilder = (params: {
    availableTools: Set<string>;
    citationsMode?: MemoryCitationsMode;
  }) => string[];

JavaScript would tolerate the zero-arg form at runtime (extra args
are ignored), but the signature was misleading and would fail a
TypeScript port. Updated to (_params) => [...] with a comment
explaining we accept-but-ignore. Underscore-prefix conveys
intentional non-use to lint/readers.

Validation: 3/3 openclaw tests still pass (they invoke
promptBuilder({ availableTools: new Set() }) — both old and new
signatures accept that call).
2026-05-12 23:39:24 +01:00
Rohit Ghumare c21410e287 feat(plugin): ship Codex plugin manifest + marketplace (#311)
* feat(plugin): ship Codex plugin manifest + marketplace (#codex-plugins)

OpenAI Codex shipped a plugin platform
(developers.openai.com/codex/plugins) with the same shape as Anthropic
Claude Code plugins: .codex-plugin/plugin.json manifest, optional
.mcp.json, hooks/hooks.json, and a skills/ directory.

Verified against openai/codex source
(codex-rs/hooks/src/engine/discovery.rs): Codex's hook engine
explicitly injects CLAUDE_PLUGIN_ROOT into hook subprocesses for
OOTB compat with existing Claude Code plugins. The wire-format
input schemas
(codex-rs/hooks/schema/generated/session-start.command.input.schema.json
and siblings) use the same field names as Claude Code
(session_id, cwd, hook_event_name, source, transcript_path, model,
permission_mode).

Net result: our existing plugin/ directory already works as a Codex
plugin with two small additions:

1. plugin/.codex-plugin/plugin.json — Codex manifest pointing at the
   shared ./.mcp.json, ./skills/, and a Codex-specific ./hooks/
   hooks.codex.json. Name (kebab-case), version, description match
   the Claude Code manifest.

2. plugin/hooks/hooks.codex.json — Codex-compatible hook subset.
   Drops SubagentStart, SubagentStop, SessionEnd, Notification,
   TaskCompleted, PostToolUseFailure (Claude-Code-only). Keeps
   SessionStart, UserPromptSubmit, PreToolUse, PostToolUse,
   PreCompact, Stop. Adds Codex-specific `statusMessage` decorations
   on the two hooks the user sees most (SessionStart + UserPromptSubmit).
   The script commands themselves still reference ${CLAUDE_PLUGIN_ROOT}
   because Codex's engine injects it.

3. .codex-plugin/marketplace.json at repo root — git-subdir source
   pointing at ./plugin so `codex plugin marketplace add rohitg00/
   agentmemory` works the same way `claude plugin marketplace add`
   does. Mirrors the existing .claude-plugin/marketplace.json shape
   but in Codex's marketplace schema (name + interface.displayName
   + plugins[].source.{source, url, path, ref} + plugins[].policy).

README updates:
- Codex CLI tile in the Works-with-every-agent grid now reads
  "6 hooks + MCP + skills" instead of "MCP server" to surface the
  upgraded surface.
- "Other agents" install table now distinguishes "Codex CLI (MCP
  only)" (existing codex mcp add path) from "Codex CLI (full
  plugin)" (new marketplace install).
- New Codex section in the "paste this prompt" block with the
  two-command install (server + marketplace add + plugin install),
  the list of registered surfaces, and the env-var-injection note
  citing the Codex source line.

6 new tests in test/codex-plugin.test.ts cover:
1. .codex-plugin/plugin.json present with kebab-case name and
   required references.
2. Manifest version matches main package.json (so future bumps
   don't drift the Codex side).
3. Every path referenced in the manifest resolves to a real file or
   directory on disk.
4. hooks.codex.json contains only events Codex supports — any
   future addition has to be added to a Codex allowlist or the
   test fails loudly.
5. Every ${CLAUDE_PLUGIN_ROOT}/scripts/* command references an
   existing script file (catches typos before users hit them).
6. .codex-plugin/marketplace.json declares git-subdir source with
   path: "./plugin" pointing at this repo.

874 / 874 tests pass.

* fix(plugin/codex): align manifest description + assert PreCompact hook

Two reviewer findings addressed:

1. plugin/.codex-plugin/plugin.json's description claimed "12 hooks"
   but Codex only registers 6 (the Codex hook input schemas don't
   define SubagentStart, SubagentStop, SessionEnd, Notification,
   TaskCompleted, PostToolUseFailure). Updated the description to
   "6 hooks, 51 MCP tools, 4 skills, real-time viewer." so the
   marketplace listing matches the registered surface.

2. test/codex-plugin.test.ts asserted five lifecycle events
   (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop)
   but omitted PreCompact, leaving a regression hole: dropping
   PreCompact from hooks.codex.json would still pass the test
   because the allowlist loop only blocks UNKNOWN events. Added
   expect(events).toContain("PreCompact") so the suite enforces
   the full six-hook contract.

Skipped (with reason):

3. marketplace.json source.ref currently "main" — reviewer asked
   for a fixed tag/SHA. Skipping because pinning to a tag forces
   updating marketplace.json on every release; "main" matches our
   convention where releases tag from main HEAD and pre-release
   work is on feature branches. Users wanting an immutable cut
   can pass --ref vX.Y.Z to `codex plugin marketplace add` per
   Codex docs.

Validation: 6/6 codex tests pass; 874/874 full suite unchanged.
2026-05-12 23:39:10 +01:00
Rohit Ghumare 292e9f6af1 docs(readme): add Trendshift badge + star history graph + tagline fix (#305)
* docs(readme): add Trendshift badge + star history graph + tagline fix

Three small README polishes flagged on the live README screenshot:

1. Add the Trendshift badge (repo #25123) directly under the tagline
   so the trending signal sits at eye level with the banner.
2. Add the star-history.com graph under the badge so the growth
   curve is visible without scrolling — uses <picture> with
   dark/light variants matching the rest of the README's
   prefers-color-scheme convention.
3. Tagline missed two first-party integrations that already ship
   plugin folders in this repo (integrations/hermes,
   integrations/openclaw); added Hermes + OpenClaw to the agent
   list so the supported-clients sentence matches what the
   integration index actually advertises.
4. Reflow the Karpathy-gist sentence onto a single italic line —
   the previous block-strong + <br/> shape wrapped awkwardly on
   narrow viewports.

Not in this PR (deferred): the stat-tag row redesign flagged on
the same screenshot. That needs new SVG assets in assets/tags/
and is worth doing as a separate visual pass rather than a quick
edit.

* Update README.md

* docs(readme): simplify star history to plain img link

GitHub's image proxy doesn't reliably render <picture> with external
SVG sources from api.star-history.com — the entire block came back
empty in rendered README. Switched to the standard plain img inside
an anchor pattern that every other widely-used repo (k8s, react,
deno, etc) uses for star-history embeds. Single URL, no
prefers-color-scheme variants, single fetch path for the camo
proxy.

* docs(readme): switch star history to markdown form (&amp; escaping bug)

HTML form had unescaped `&type=Date` in src attribute. Browser/GitHub
parsed `&type` as an HTML entity attempt, so the URL camo fetched was
truncated to `?repos=rohitg00/agentmemory` — invalid endpoint, broken
image. Markdown form auto-escapes ampersands, matching every other
repo's star-history embed.

* docs(readme): use canonical star-history chart endpoint with theme variants

User-provided exact snippet. Switches:
- /svg -> /chart endpoint (canonical star-history.com API)
- type=Date -> type=date (lowercase, matches docs)
- adds legend=top-left
- <picture> with prefers-color-scheme dark/light sources
- outer link uses query-param form (?repos=...) not fragment (#repos=...)

Matches the pattern star-history.com's own embed-generator hands out.
v0.9.10
2026-05-12 16:24:59 +01:00
Rohit Ghumare 5ea01c162c fix(deploy): distroless volume perms + viewer proxy + budget loop (#299, #301) (#304)
@flamerged reported three issues on a real production deployment:

1. (#301) v0.9.7's working-directory fix moved iii-config paths from
   ./data/... to /data/... so the named volume mount is actually
   reached. But iiidev/iii is distroless and runs as UID 65532, while
   `docker volume create` initializes the named volume mountpoint as
   root:root mode 755. Engine writes fail Permission denied (os error
   13), the API silently buffers in RAM, every API call returns
   success, and state evaporates on every container restart — exactly
   what 0.9.7 set out to fix.

   Fix: ship a one-shot iii-init service in docker-compose.yml
   (busybox:1.36, ~4MB, exits in <100ms) that chowns /data to
   65532:65532. iii-engine now has user: "65532:65532" and
   depends_on.iii-init.condition: service_completed_successfully.
   Verified live: pre-fix volume stayed 4.0K after API writes; post-
   fix volume grows to 44K with state_store.db/mem%3A*.bin files
   written through the named volume.

2. (#299) src/viewer/index.html ports detection hardcoded ':3113' as
   the fallback when window.location.port is empty (page served on
   80/443 behind a reverse proxy). Every browser-side /agentmemory/*
   fetch went to <host>:3113, which is typically loopback-only on the
   self-hosted shape — the dashboard rendered cleanly but every
   panel showed the empty "first run" state.

   Fix: when neither ?port=N nor window.location.port is set, use
   window.location.origin as the REST base and window.location.host
   for the WebSocket URL — same-origin path works for both REST and
   live updates. Explicit ?port=N / non-default window.location.port
   paths unchanged.

3. (bundled) mem::context budget loop used `break` on first oversized
   block. With #288's new pinned-slot injection sorting first via
   recency: Date.now(), one fat pinned slot could starve every
   smaller block downstream that would have fit. Switched to
   `continue` so smaller blocks still slip into remaining budget.
   Total tokens still bounded by tokenBudget; only composition under
   contention changes.

Bumping 0.9.9 -> 0.9.10 across the 8 standard files (package.json,
packages/mcp/package.json, plugin/.claude-plugin/plugin.json,
src/version.ts, src/types.ts ExportData literal,
src/functions/export-import.ts supportedVersions, the export
round-trip test expectation, and CHANGELOG.md).

868 / 868 tests pass. Build clean. Volume + viewer fixes verified
end-to-end live.
2026-05-12 16:19:13 +01:00
Rohit Ghumare 03fb42db39 chore(release): v0.9.9 — pinned slot injection + MiniMax env loader (#290)
Two field-reported fixes landed since v0.9.8:

- #288 (closes #286) — wires pinned memory slots into mem::context so
  agentmemory pinned-slot content actually reaches SessionStart. The
  renderPinnedContext / listPinnedSlots helpers from #182 had zero
  callers; this lands the wiring behind isSlotsEnabled().
- #289 (closes #285) — MiniMax provider now reads MINIMAX_BASE_URL via
  the shared getEnvVar() loader (so ~/.agentmemory/.env values get
  picked up), and the default endpoint is bumped from the stale
  api.minimaxi.com to api.minimax.io/anthropic per MiniMax's current
  Anthropic-compatible docs.

Bumping 0.9.8 -> 0.9.9 across the 8 standard files (package.json,
packages/mcp/package.json, plugin/.claude-plugin/plugin.json,
src/version.ts, src/types.ts ExportData literal,
src/functions/export-import.ts supportedVersions, the export
round-trip test expectation, and CHANGELOG.md).

868 / 868 tests pass.
v0.9.9
2026-05-11 19:38:20 +01:00