353 Commits

Author SHA1 Message Date
Rohit Ghumare 08d781d431 chore(release): v0.9.20 — hotfix Codex Stop revert (#501)
v0.9.19 shipped #495 which chained session-end.mjs after stop.mjs on
the Codex Stop hook. Field-testing surfaced the underlying issue:
Codex fires Stop multiple times within a single conversation (once
per assistant turn), so chaining session-end marked sessions as
completed while later observations were still arriving.

#501 reverts the chain. Stop returns to summarize-only behavior. The
SessionEnd-shaped solution (a dedicated terminate event the agent
sends only once on real session end) tracks at #493.

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

1034/1034 tests pass.
v0.9.20
2026-05-18 19:20:48 +01:00
Nanami a953cadb62 Revert "fix: end Codex sessions on Stop hook (#495)" (#501)
This reverts commit abeec1d0b7.

Signed-off-by: Nanami <Rex57@users.noreply.github.com>
Co-authored-by: Nanami <Rex57@users.noreply.github.com>
2026-05-18 19:17:26 +01:00
Rohit Ghumare 29e6ebdcc0 docs(changelog): credit contributors on v0.9.19 entries 2026-05-18 14:49:19 +01:00
Rohit Ghumare bb259acedc chore(release): v0.9.19 — route around upstream iii-console installer bug
`install.iii.dev/console/main/install.sh` filters releases with
`startswith("v")` while `iii-hq/iii` tags as `iii/v0.12.0` —
slash-prefixed. Every stable release is filtered out and the script
bails with "no stable iii release found", breaking first-run console
install for every fresh agentmemory user.

Switch our prompt to pass `--next` through the installer. The next-
release lookup uses a regex on `-next.` without the buggy
`startswith` constraint, so it succeeds against the same tag set and
installs the most recent prerelease. Inline comment documents the
upstream tag-prefix bug + the revert condition for when the
installer is patched.

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

1007/1007 tests pass.
v0.9.19
2026-05-18 13:57:23 +01:00
Rohit Ghumare caa9f5223e fix(plugin): inherit AGENTMEMORY_URL/SECRET via env passthrough (#375) (#460)
The bundled plugin .mcp.json + the `agentmemory connect <agent>` wiring
both hardcoded the MCP server to localhost:3111 with no env-var seam.
Users running agentmemory remotely (k8s cluster-internal DNS, reverse
proxy with API key auth) hit a permanent /doctor warning because they
had to add a second entry with the right AGENTMEMORY_URL + SECRET, and
both entries fought for the "agentmemory" name.

Both files now use ${AGENTMEMORY_URL} / ${AGENTMEMORY_SECRET}
expansion. Host (Claude Code, Cursor, etc.) substitutes the shell
value at MCP-server launch; when vars are unset the host passes empty
string, which the standalone shim treats as missing and falls back to
http://localhost:3111. One wired entry now covers both local and
remote without duplicates.

- plugin/.mcp.json: env block added with ${VAR} expansion
- src/cli/connect/util.ts: AGENTMEMORY_MCP_BLOCK env updated; comment
  documents the empty-string fallback contract
- test/cli-connect.test.ts: regression test asserting the env block
  shape after install() — protects against future regressions where
  the literal hardcoded URL leaks back in

1008/1008 tests pass.
2026-05-18 11:22:05 +01:00
Nanami abeec1d0b7 fix: end Codex sessions on Stop hook (#495)
Signed-off-by: Nanami <Rex57@users.noreply.github.com>
Co-authored-by: Nanami <Rex57@users.noreply.github.com>
2026-05-18 11:20:40 +01:00
Rohit Ghumare 06248fd199 refactor(providers): extract OpenAI transport into shared module (#371) (#462)
* refactor(providers): extract OpenAI transport into shared module (#371)

OpenAIProvider (LLM) and OpenAIEmbeddingProvider (embedding) both
speak the OpenAI wire shape over POST /v1/chat/completions and POST
/v1/embeddings — and both need to swap to the Azure shape when the
base URL lands at *.openai.azure.com (api-key header instead of
Authorization: Bearer, api-version query param, no /v1/ prefix
because the deployment is already in the URL).

Until now only the LLM provider knew about Azure. The embedding
provider hardcoded /v1/embeddings + Bearer auth, so any user pointing
OPENAI_BASE_URL at an Azure resource saw their LLM calls route
correctly while embedding calls 404'd / 401'd.

New src/providers/_openai-shared.ts owns:

  detectAzure(baseUrl)
  buildChatUrl(baseUrl, isAzure, apiVersion)
  buildEmbeddingUrl(baseUrl, isAzure, apiVersion)
  buildAuthHeaders(apiKey, isAzure)
  normalizeBaseUrl(raw)

Both classes import these helpers. The embedding provider gains
Azure auto-detection via the same OPENAI_BASE_URL hostname check the
LLM provider already uses, plus the same OPENAI_API_VERSION knob
(default 2024-08-01-preview). Closes the gap users hit when mirroring
an Azure deployment on both surfaces.

Net delta: ~40 LOC of duplicated transport collapsed into one shared
file; both provider files are smaller and focused on their surface's
unique concerns (LLM owns max_tokens / reasoning_effort / timeout
precedence; embedding owns the MODEL_DIMENSIONS table).

Approach diverges from the issue body (single class implementing both
interfaces): the existing factory contracts at
src/providers/index.ts dispatch LLM and embedding independently
(detectProvider vs detectEmbeddingProvider), and merging into one
class would force constructors to coordinate detection across two
config surfaces. Shared transport keeps factories clean while
delivering the DRY win and the Azure-on-embeddings parity gain.

- src/providers/_openai-shared.ts: new shared transport module
- src/providers/openai.ts: import shared helpers; drop local
  detectAzure + private buildUrl/buildHeaders
- src/providers/embedding/openai.ts: import shared helpers; Azure
  auto-detection via OPENAI_BASE_URL host check, OPENAI_API_VERSION
  query param when Azure-shaped
- test/openai-shared.test.ts: 17 cases covering all four helpers
  plus end-to-end embedding Azure shape (asserts URL path + api-key
  header substitution via mocked fetch)

1023/1023 tests pass.

* fix(providers): use URL API for Azure path composition (review)

Inline review on #462 surfaced an edge-case bug in the azureUrl
helper: it tested baseUrl for an existing query string to pick the
?/& separator, then string-concatenated the route path BEFORE that
separator. If a baseUrl arrived with pre-existing query params
(corporate proxy carrying a tenant token, diagnostics endpoint,
etc.), the route path got interpolated into the query string,
producing a malformed URL like:

  baseUrl: https://proxy.example.com/openai/deployments/d?tenant=acme
  result:  https://proxy.example.com/openai/deployments/d?tenant=acme/chat/completions&api-version=2024-08-01-preview
                                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ in the query, not the path

Switch to the URL API: parse baseUrl, normalise the existing
pathname (strip trailing slashes), join the route path with a
single boundary slash, set api-version via url.searchParams.set
(handles encoding without a manual encodeURIComponent), then
toString. Existing query params are preserved.

Test header normalisation pass: the OpenAIEmbeddingProvider
Azure-detection tests captured RequestInit.headers as
Record<string,string>. buildAuthHeaders happens to return a plain
record today, but a future contributor migrating to
HeadersInit/Headers would have these tests silently pass-but-stale
because Record-style access would no longer match. Wrap the capture
in new Headers(init?.headers) so the assertions go through .get()
and tolerate any of the three valid HeadersInit shapes.

Skipped — replacing DEFAULT_AZURE_API_VERSION with /openai/v1: the
v1 Azure pattern is a recent addition and many deployments still
require the api-version query param. Switching the default would
break those deployments. Separate opt-in flag (e.g.
OPENAI_AZURE_API_STYLE=v1) is the right shape but out of scope for
this minimal-change review pass.

- src/providers/_openai-shared.ts: azureUrl rewritten on URL API
- test/openai-shared.test.ts: two new cases (query-param
  preservation, trailing-slash normalisation); embedding Azure
  tests switched to Headers API + .get() / .toBeNull()

1025/1025 tests pass.

* feat(providers): support Azure OpenAI v1 URL pattern (review)

Azure shipped a v1 GA endpoint pattern that mirrors the OpenAI wire
shape one-for-one:

  POST https://<resource>.openai.azure.com/openai/v1/chat/completions

No api-version query param, no /deployments/<dep> path segment —
deployment name is passed in the request body as `model`. Existing
OpenAI clients work as drop-in. The legacy URL pattern
(/openai/deployments/<dep>/chat/completions?api-version=<date>) is
still supported by Azure indefinitely for back-compat.

Verification of the inline review's "remove DEFAULT_AZURE_API_VERSION
and migrate all Azure calls to /openai/v1" instruction: removing it
flat would break legacy users mid-flight (their existing
OPENAI_BASE_URL=https://r.openai.azure.com/openai/deployments/d
configs would still route through the legacy path and 400 without
api-version). Hybrid auto-detect off the URL shape is the right
shape:

  - baseUrl pathname contains `/openai/deployments/` -> legacy style,
    api-version still appended (DEFAULT_AZURE_API_VERSION fallback)
  - otherwise (bare resource host, /openai prefix, /openai/v1 prefix)
    -> v1 style, no api-version, route through /openai/v1/<path>

Users migrate by stripping the /openai/deployments/<dep> suffix from
their OPENAI_BASE_URL — no env-var changes required.

azureStyleOf(baseUrl) is the new detection helper. v1AzureUrl
preserves any pre-existing query params on the base URL (corporate
proxy diagnostics tokens) but never appends api-version. The
/openai/v1 prefix is normalised so configurations like
OPENAI_BASE_URL=https://r.openai.azure.com/openai or
.../openai/v1 don't double-prefix.

- src/providers/_openai-shared.ts: azureStyleOf + v1AzureUrl;
  legacyAzureUrl (renamed from azureUrl) is unchanged; module header
  comment documents both styles and the migration path
- test/openai-shared.test.ts: 3 new cases — v1 routing on bare host,
  /openai or /openai/v1 prefix normalisation, v1 on the embedding
  surface as well; legacy assertion updated to say "Azure legacy" so
  the contrast with v1 is explicit

1028/1028 tests pass.
2026-05-18 11:20:12 +01:00
Rohit Ghumare 249d8ff032 feat(commits): link agent sessions to git commits (#498)
* feat(commits): link agent sessions to git commits

Adds a commit-link layer so memories captured during an agent session
can be traced back to the git commit they produced, and vice versa.

What ships:

- KV.commits namespace storing CommitLink records keyed by full SHA.
- Session.commitShas[] back-reference for fast forward lookup.
- POST /agentmemory/session/commit upserts a link (sha, branch, repo,
  message, author, authoredAt, files, sessionId); merges sessionIds on
  re-link and preserves linkedAt.
- GET /agentmemory/session/by-commit?sha= hydrates the link plus its
  sessions.
- GET /agentmemory/commits?branch=&repo=&limit= lists recent links,
  filtered and sorted desc by linkedAt, limit clamp 1..500 (default 100).
- src/hooks/post-commit.ts shells git rev-parse/log/diff-tree and POSTs
  to /agentmemory/session/commit. Honors AGENTMEMORY_URL,
  AGENTMEMORY_SECRET, AGENTMEMORY_CWD, AGENTMEMORY_SESSION_ID,
  AGENTMEMORY_COMMIT_SHA. Best-effort, 1500 ms timeout. Wired into
  tsdown hookEntries so it bundles to dist/hooks/ and plugin/scripts/.
- Two MCP tools: memory_commit_lookup, memory_commits.
- Four user-invocable plugin skills:
  - commit-context: traces a file/function/line via git blame plus
    memory_commit_lookup plus memory_recall.
  - commit-history: lists agent-linked commits; parses branch=/repo=/
    limit= from $ARGUMENTS.
  - handoff: resumes the most recent session for cwd; surfaces an
    unanswered user-facing question before the brief.
  - recap: summarizes the last N sessions for cwd, grouped by date;
    parses last <n> / today / this week.

Wire-up to capture from a real repo:

    ln -sf "$(realpath node_modules/agentmemory/dist/hooks/post-commit.mjs)" .git/hooks/post-commit
    chmod +x .git/hooks/post-commit

No new dependencies. Additive only.

* fix(commits): address CI + review findings on commit-link feature

- Update mcp-standalone.test.ts to expect 14 CORE_TOOLS (was 12) now that
  memory_commit_lookup and memory_commits are registered. Restores CI on
  Node 20 and 22.
- Wrap CommitLink upsert and session.commitShas mutation in withKeyedLock
  to prevent lost updates when concurrent post-commit hooks race against
  each other or against api::session::commit callers. Locks keyed by
  `commit:<sha>` and `session:<sessionId>` so unrelated writers do not
  serialize.
- Parallelize hydration of linked sessions via Promise.all in both
  api::session::by-commit and the memory_commit_lookup MCP handler;
  serial kv.get loops dragged response time for commits with many
  sessions.
- Sharpen handoff SKILL.md project-path matching: resolve relative
  $ARGUMENTS to an absolute normalized path and require directory-
  boundary equality, not raw string prefix, so sibling repos sharing a
  prefix do not collide.
- Sharpen commit-history SKILL.md HTTP fallback: build the
  /agentmemory/commits URL with URL-encoded query values so branch and
  repo values containing `?`, `&`, or `#` cannot corrupt the request.

* chore(consistency): bump documented tool and endpoint counts

Bring README.md, AGENTS.md, and the boot log in src/index.ts in line
with the live tool registry (53 MCP tools, +2 from this branch) and
the live REST registration (124 endpoints, +3 from this branch).

The Consistency test in test/consistency.test.ts derives both counts
from source and asserts the markdown matches; this commit only updates
the literal numbers, no behavior change.
2026-05-18 11:19:47 +01:00
Rohit Ghumare 4db303b6b9 perf(graph): swap BFS for Dijkstra over edge-weighted graph (#328) (#463)
* perf(graph): swap BFS for Dijkstra over edge-weighted graph (#328)

Memory graph edges carry weights from 0.1 to 1.0 (encoding relation
strength). BFS visits nodes in edge-count order regardless of weight,
so a one-hop weak edge ranked the same as a two-hop chain of strong
edges to the same node — which is the wrong semantics for the
weighted graph the retrieval surface actually queries.

Replace bfsTraversal with dijkstraTraversal:

  cost(edge) = 1 / max(edge.weight, 0.01)

Higher-weight edges cost less, so Dijkstra prefers strong-edge paths.
The clamp floor at 0.01 protects against malformed edges with
weight=0 dividing by zero (the documented weight floor is 0.1, but
imports / older snapshots may carry zeros).

Same swap tightens the perf profile:

  - Adjacency built once in O(V+E) (was: filter allEdges per visited
    node, O(V·E) overall).
  - Min-heap dequeue is O(log V) per pop (was: Array.shift() which is
    O(n) — the dominant cost on graphs above ~200 nodes per the
    contributor's benchmark linked in #328).

Min-heap is a 50-LOC inline class so the file doesn't take a new
dependency for the perf-critical inner loop.

Semantics preserved:

  - maxDepth still bounds edge-count, not accumulated cost. So a deep
    chain of strong edges still terminates at maxDepth hops.
  - One path per reachable node within depth (matches what the
    existing scoring loop consumes).
  - score = avgWeight × (1/pathLength) formula unchanged.

- src/functions/graph-retrieval.ts: dijkstraTraversal + MinHeap
- test/graph-retrieval.test.ts: 4 new cases — weight-optimal path
  selection (the core Dijkstra promise), disconnected-node safety,
  near-zero-weight clamp, maxDepth bound preserved

1011/1011 tests pass.

* fix(graph): exclude startNode from Dijkstra returned paths (review)

Inline review on #463 surfaced a pre-existing bug carried forward
through the BFS-to-Dijkstra refactor: the returned path list
included the startNode's own length-1 path, so the generic
path-scoring loop in searchByEntities ran first — pathLength=1 with
no edges → avgWeight fell to 0.5 (empty-edgeWeights fallback) →
score=0.5 → startNode obs marked visited.

The dedicated score=1.0 fallback loop for startNode obs was then
skipped by the visitedObs guard, making it dead code. So startNode
observations were silently scoring 0.5 instead of the intended 1.0.

Drop the startNode entry from pathTo before returning. The caller's
fallback loop now fires as designed.

- src/functions/graph-retrieval.ts: pathTo.delete(startNode.id)
  immediately before Array.from(pathTo.values()); inline comment
  explains why the fallback would otherwise be dead.
- test/graph-retrieval.test.ts: regression asserts obs_root carries
  score=1.0 and pathLength=0 when its node is the matched start.

1012/1012 tests pass.
2026-05-18 10:30:35 +01:00
Rohit Ghumare 9061da56d5 chore(release): v0.9.18 (#459)
Hardening + DX wave. Bundles the four follow-up issues filed during
the v0.9.17 audit (#446 timeout env unify, #447 viewer CSP tighten,
#448 fs-watcher PEM/JWT redaction, #449 mcp-standalone probe DI flake)
plus the lessons-auto-inject gap surfaced in discussion #381 / fixed
in #457.

1007/1007 tests pass.

Files bumped:
- 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)
- src/functions/export-import.ts (supportedVersions Set)
- test/export-import.test.ts (assertion)
- CHANGELOG.md (new section)
v0.9.18
2026-05-17 14:05:56 +01:00
Rohit Ghumare a557e34f39 fix(context): auto-inject lessons into mem::context payload (#457) (#458)
Lessons were generated (via mem::lesson-save / mem::reflect) and stored
in the lessons KV scope, but never folded into the auto-inject payload
the session-start hook prepends to the first turn. Agents only saw
lessons if they explicitly called memory_lesson_recall — which they
rarely did unprompted. Half the loop: lessons learned, lessons never
reused.

mem::context now reads lessons KV alongside slots + profile, filters
out tombstoned entries + cross-project leakage, ranks project-scoped
lessons above globals (1.5x boost) then by confidence, caps at 10, and
emits a "## Lessons Learned" block sized like every other context
block so the outer token-budget loop drops it cleanly when tight.

Surfaced in #381 (@rockarxiv asked where lessons get used — answer
was "nowhere, automatically"). Filed as #457, fixed here.

- src/functions/context.ts: parallel-fetch lessons with slots +
  profile, render block after profile and before sessions
- test/context-lessons.test.ts: 8 cases covering inclusion, empty
  state, project ranking, cross-project isolation, deleted-skip, top-N
  cap, confidence rendering, optional context-string append

1007/1007 tests pass.
2026-05-17 13:57:26 +01:00
Rohit Ghumare 49377db0db fix(providers): unify OPENAI_TIMEOUT_MS + AGENTMEMORY_LLM_TIMEOUT_MS (#446) (#453)
* fix(providers): unify OPENAI_TIMEOUT_MS + AGENTMEMORY_LLM_TIMEOUT_MS (#446)

v0.9.17 shipped OPENAI_TIMEOUT_MS scoped to the OpenAI LLM provider
(inline AbortController, default 60s). PR #379 then shipped
AGENTMEMORY_LLM_TIMEOUT_MS in the shared src/providers/_fetch.ts helper
used by every other raw-fetch provider (Gemini, OpenRouter, MiniMax,
OpenAI/Cohere/Voyage/OpenRouter embedding).

Two env vars, same value, different names — ops confusion.

Unify on the global name while keeping back-compat:

  1. OPENAI_TIMEOUT_MS         — OpenAI-scoped alias, takes precedence
  2. AGENTMEMORY_LLM_TIMEOUT_MS — global fall-back across providers
  3. 60 000 ms default

The OpenAI LLM provider now routes through the shared fetchWithTimeout
helper, dropping ~30 lines of duplicate AbortController + clearTimeout
plumbing. Existing users with OPENAI_TIMEOUT_MS set keep the exact
v0.9.17 behaviour; new users setting AGENTMEMORY_LLM_TIMEOUT_MS get
the OpenAI LLM path covered too.

README + .env.example now document AGENTMEMORY_LLM_TIMEOUT_MS as the
canonical name and note OPENAI_TIMEOUT_MS as the OpenAI-scoped alias.

4 new precedence tests in test/fetch-timeout.test.ts cover all four
env-var combinations.

* test(providers): strict parse for OPENAI_TIMEOUT_MS env (CodeRabbit)

CodeRabbit caught parseInt("30ms", 10) silently returning 30 in the
timeout-resolve path. Real users hitting this would think they bound
the call to 30ms when the regex would have rejected it.

parsePositiveInt now rejects anything that isn't pure digits via
/^\d+$/ (after trim). parseInt's lenience on trailing units /
underscores / signs is gone — those fall back to the 60s default
instead of masquerading as an aggressive bound.

New regression test covers "30ms", "1_000", "60s", "30abc", "-30",
"0". Whitespace padding (e.g. " 30 ") is still accepted — that's
normal env-var handling.

992/992 tests pass on the worktree.
2026-05-17 12:41:30 +01:00
Rohit Ghumare 00df540c87 fix(fs-watcher): redact PEM private-key blocks and standalone JWTs in previews (#448) (#450)
PR #332 covered KEY=value and Bearer-prefixed tokens. This extends the
filesystem watcher's preview pipeline to also collapse multi-line and inline
PEM private-key blocks (RSA / DSA / EC / OPENSSH / ENCRYPTED / generic) into
a single REDACTED line bracketed by the BEGIN/END markers, and to redact
standalone JWT-looking strings outside Bearer context. JWT redaction requires
a three-segment dot pattern AND total length >= 100 chars to avoid matching
incidental base64-shaped words.

Closes #448
2026-05-17 12:41:14 +01:00
Rohit Ghumare 92e5c885fc fix(mcp): inject livez probe to kill mcp-standalone test flake (#449) (#451)
The MCP standalone shim's REST proxy probed `:3111/agentmemory/livez` via a
hard-coded `fetch()` with a 2s AbortController timeout. In CI, vitest's mock
setup raced the in-flight probe — tests randomly failed on call counts and
response-shape assertions depending on whether the probe completed before
each test's own deadline. This file has been the source of every
"10-11 pre-existing failures unrelated to this PR" footnote for the last
five releases.

Add a `setLivezProbe(fn)` module-level seam in `src/mcp/rest-proxy.ts`
(option A in the issue). Default stays the real fetch-based probe — wire
behaviour is unchanged in production. `resetHandleForTests()` also resets
the probe so tests can't accidentally leak a stub across files.

In `test/mcp-standalone.test.ts`, install an instant `ok:false` probe in
`beforeEach` so the shim always takes the deterministic InMemoryKV fallback
path. Replace `globalThis.fetch` with a trap that throws if hit — any
regression that bypasses the seam fails loudly instead of timing out
silently and re-introducing the flake.

Validation: 10/10 consecutive runs green (~185ms each, down from 2.2s);
proxy test file unaffected (14/14); full suite 988/988 stable across 3 runs.
2026-05-17 12:41:00 +01:00
Rohit Ghumare 57c785f4db fix(viewer): self-host SVG favicon + restore tight img-src CSP (#447) (#452)
#313 added `data:` to the viewer img-src so an inline-SVG favicon
encoded as `data:image/svg+xml,...` could load. Bare `data:` allows
arbitrary data-URI image types (data:image/png;base64,..., etc),
which the viewer does not need.

Self-host the favicon at `/favicon.svg` and revert the CSP to
`img-src 'self'`. Same artwork (dark rounded tile + green "AM"),
served from a real URL with `Content-Type: image/svg+xml` and
`Cache-Control: public, max-age=3600`.

- new file: src/viewer/favicon.svg (extracted from the URL-decoded
  inline blob)
- src/auth.ts: img-src reverted to 'self'
- src/viewer/index.html: favicon link now href="/favicon.svg"
- src/viewer/server.ts: new GET /favicon.svg route, candidate-path
  loader mirrors loadViewerTemplate() so it resolves both from
  source (vitest) and from dist/ (npm run start)
- package.json: build script copies favicon.svg into dist/viewer/
  alongside index.html
- test/viewer-security.test.ts: CSP assertion now requires bare
  `img-src 'self'` and rejects any `data:` allowance; new e2e test
  hits /favicon.svg and verifies SVG payload + headers
2026-05-17 12:40:33 +01:00
Rohit Ghumare c93c715043 fix(test): stub history + removeAttribute in viewer sandbox (#445)
The viewer-session-id sandbox (added in #366) ran fine until #313
landed hash-routing in `switchTab` — `updateTabRoute` now calls
`history.replaceState`, and the aria-selected toggle calls
`removeAttribute` on the non-active tab buttons. Neither was stubbed
in the vm sandbox, so the combined main produced 2 failures:

  TypeError: b.removeAttribute is not a function
  ReferenceError: history is not defined

Both PRs (#366, #313) passed CI in isolation. The combined failure
only surfaced after merge. Stub both globals.

987/987 tests now pass on main.
2026-05-17 11:28:30 +01:00
Mersad Ajanovic 63a0742447 fix: stabilize viewer navigation and timeline layout (#313)
* fix: stabilize viewer navigation and timeline layout

* fix: address viewer review nits
2026-05-17 11:20:49 +01:00
이민재 e2f2217f59 fix: redact filesystem watcher previews before transport (#332)
* fix: redact filesystem watcher previews

* fix: redact quoted sensitive preview keys

---------

Co-authored-by: 이민재 <19909783+honor2030@users.noreply.github.com>
2026-05-17 11:19:36 +01:00
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