350 Commits

Author SHA1 Message Date
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
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