New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@contextq/memory-tool

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@contextq/memory-tool

Anthropic memory tool (memory_20250818) backend adapter that stores Claude's memory files as ContextQ contexts

latest
Source
npmnpm
Version
0.1.0
Version published
Weekly downloads
7
-36.36%
Maintainers
1
Weekly downloads
 
Created
Source

@contextq/memory-tool

A backend adapter for Anthropic's memory tool (memory_20250818) that stores every memory file as a ContextQ context, wired up entirely through ContextQ's existing /api/contexts* endpoints -- no schema changes.

Background: docs/product/claude-surfaces-positioning.md (Q1) is the feasibility study this package implements; this README covers install/use. Product framing (why an adapter, not a competing surface): the same doc's "Q3 -- The one-page positioning statement".

Install

npm install @contextq/memory-tool @anthropic-ai/sdk

@anthropic-ai/sdk is a peer dependency -- bring your own version (tested against ^0.120.0, the version current when this package was built).

Quick start

import Anthropic from '@anthropic-ai/sdk';
import { contextQMemoryTool } from '@contextq/memory-tool';

const client = new Anthropic(); // reads ANTHROPIC_API_KEY
const memory = contextQMemoryTool({
  apiKey: process.env.CONTEXT_API_KEY!, // sk_live_<40 hex>
  baseUrl: process.env.CONTEXT_API_URL, // default http://localhost:38200
});

const runner = client.beta.messages.toolRunner({
  model: 'claude-opus-5',
  max_tokens: 16000,
  tools: [memory],
  messages: [
    { role: 'user', content: 'Remember that I prefer TypeScript, under /memories/acme/.' },
  ],
});

for await (const message of runner) {
  console.log(message);
}

contextQMemoryTool(options) is a thin factory: new ContextQMemoryTool(options) handed to Anthropic's betaMemoryTool() helper. Use ContextQMemoryTool directly if you want the MemoryToolHandlers-shaped object without the BetaRunnableTool wrapper (e.g. to compose your own tool-loop).

Options

OptionDefaultNotes
apiKeyprocess.env.CONTEXT_API_KEYContextQ API key, sk_live_<40 hex>. Required (one of option or env).
baseUrlprocess.env.CONTEXT_API_URL || http://localhost:38200ContextQ API base URL.
contextType'reference'The ContextQ type every memory-tool row gets. The memory tool has no type taxonomy of its own.
tag'mem-tool'Reserved tag stamped on every row this adapter creates; also used to scope directory/prefix listings so this adapter never lists a caller's unrelated contexts.
maxRetries, timeoutMs, fetchImplsee HttpClientOptionsPassed straight through to the internal HTTP client.

Path model

/memories                          -> lists workspaces
/memories/<workspace>               -> lists that workspace's mem-tool-tagged contexts
/memories/<workspace>/a/b/c.md      -> a single context

The first path segment after /memories/ is the ContextQ workspace slug (lowercased). Everything after it is joined into ContextQ's external_id for that context, namespaced as mem-tool:<workspace>/<rest> -- the workspace is included in the key (unlike the bare "remaining path" sketch in the feasibility doc) because contexts.external_id is unique per tenant, not per (tenant, workspace); two workspaces both wanting a file named notes.md would otherwise collide.

Command -> endpoint mapping

CommandEndpoint(s)Atomic?
view (file)GET /api/contexts/by-external/:externalIdyes
view (directory / /memories root)GET /api/contexts?workspace=&tag=mem-tool / GET /api/workspacesyes (read-only)
createPOST /api/contexts/bulk-upsert (idempotent upsert on externalId)yes
delete (single file)GET .../by-external/:id then DELETE /api/contexts/:idyes
delete (workspace root / prefix)list + loop DELETE /api/contexts/:idno -- see below
str_replaceGET .../by-external/:id -> mutate string in-process -> PUT /api/contexts/:idno -- see below
insertsame as str_replaceno -- see below
renameGET/POST bulk-upsert (create at new path) then DELETE (old path)no -- see below

Non-atomic commands, and why

str_replace and insert fetch the whole context, mutate the string in this process, then PUT the whole content back. ContextQ has no line-indexed edit primitive and no server-side conflict detection -- a concurrent writer on the same context can race with this adapter and one write will silently clobber the other.

delete on a workspace root or a directory-shaped prefix loops individual DELETE calls (ContextQ has no bulk-delete-by-path-prefix endpoint). A failure partway through a multi-file delete leaves the remaining files undeleted, and the thrown error surfaces that rather than swallowing it.

rename is best-effort create-then-delete: it creates a new context at the destination path, then deletes the source. If the create step fails, nothing changes (the source is untouched -- a safe failure). If the create succeeds but the delete step then fails, both rows persist and ContextQMemoryTool.rename() throws an error that says so explicitly, naming both paths and the source row's id, rather than hiding the duplication. There is no server-side transaction spanning both calls.

This matches docs/product/claude-surfaces-positioning.md Q1's verdict: "buildable thin, with named gaps" -- three commands (view-whole-file, create, delete-single) map cleanly onto existing endpoints; the other three degrade to adapter-side string surgery with no atomicity guarantee.

Directory listings are not a true nested tree

The reference view command on a real filesystem lists up to two directory levels deep, with true directory objects and byte sizes. ContextQ has no directory object -- view on /memories/<workspace> or a sub-path lists every mem-tool-tagged context whose stored path starts with that prefix, flat, at its full relative path (not a nested tree), and the "size" column shows each row's updatedAt timestamp rather than a byte count (a ContextSummary never carries content, so a real byte size would need an extra fetch per row). Pagination caps at one page of 500 rows.

Context-editing eviction warning

Anthropic's context-editing feature (clear_tool_uses_20250919) can clear old tool results -- including this memory tool's own results -- from a long conversation once a token threshold fires. There is no automatic exemption for the memory tool; protection requires the calling application to explicitly set:

context_management: {
  edits: [{
    type: 'clear_tool_uses_20250919',
    trigger: { type: 'input_tokens', value: 30_000 },
    exclude_tools: ['memory'], // <- must be set by the caller
  }],
}

If a tool named memory is not in exclude_tools, its results (and therefore whatever the model most recently read back from ContextQ) can be pruned from context like any other stale tool result. See docs/product/claude-surfaces-positioning.md Q2 for the full writeup, including why ContextQ's own brain-state injection has the same exposure today and how registering as the literal memory tool (this package's purpose) is the one path that benefits automatically from callers who follow Anthropic's own documented exclude_tools: ["memory"] pattern.

Anthropic SDK shape

This package implements MemoryToolHandlers (@anthropic-ai/sdk/helpers/beta/memory) -- the same interface Anthropic's own reference implementation, BetaLocalFilesystemMemoryTool, implements. The TypeScript SDK does not export a class literally named BetaAbstractMemoryTool to subclass; that shape exists in Anthropic's Python SDK. TypeScript's idiomatic equivalent -- confirmed against the live anthropic-sdk-typescript source, not assumed -- is exactly this: implement MemoryToolHandlers and hand the instance to betaMemoryTool(), which returns a BetaRunnableTool usable with client.beta.messages.toolRunner(...).

Development

npm install
npm test          # unit tests against a local fake HTTP server (node:http) -- no real ContextQ needed
npx tsc --noEmit   # typecheck (src + scripts)
npm run build      # emit dist/ (src only, tests excluded)

Live e2e

scripts/e2e-live.ts runs a real Claude Messages API conversation against a real local ContextQ instance: Claude saves a fact via memory.create, a second, separate turn asks it to recall the fact via memory.view, and the script independently verifies via GET /api/contexts that the value actually landed in ContextQ (not just that Claude said it remembered).

Requires:

  • ANTHROPIC_API_KEY in the environment, valid for api.anthropic.com directly. If ANTHROPIC_BASE_URL is also set (e.g. by a repo-wide .env.local pointing dev tooling at a gateway), the SDK honours it, and a gateway that needs its own separate credential answers a bare 401 Unauthorized that looks exactly like a bad key. The script logs the resolved host (anthropic base url: ...) on every run so this is never silent -- unset it for a direct run: env -u ANTHROPIC_BASE_URL.
  • The local ContextQ dev stack already running and reachable (default http://localhost:38200).
  • Docker access to the shared-context-postgres container (used to create a throwaway tenant + owner user and mint an API key for them by direct INSERT -- see below).
( set -a; source path/to/.env.local; set +a
  env -u ANTHROPIC_BASE_URL CONTEXT_API_URL=http://localhost:38200 npm run e2e:live )

Why a throwaway tenant, not the shared local test tenant

Earlier versions of this script logged in as the repo's shared test@local.dev fixture user. That tenant already holds 501+ contexts against PLANS.free.quotas.contexts = 100 (src/billing/plans.ts), so create reliably 402s with a quota error there -- not an adapter bug, just no headroom on shared dev state other sessions have piled onto. The script now creates a fully throwaway tenant + owner user by direct INSERT (the same sanctioned local-dev pattern used for the API key -- see the script's doc comments), mints one API key scoped read+write+admin for that tenant (the admin scope lets the same key drive workspace create/archive/delete too, so no separate JWT/cookie session is needed anywhere in the script), and runs the whole conversation against a tenant that starts at 0 contexts. The login-as-test@local.dev path has been removed entirely.

Cleanup runs in the finally block regardless of pass/fail, in order: delete the created context(s) and the workspace via the API, then the API key row, then the user row, then -- conditionally -- the tenant row. The tenant delete is conditional: activity_logs' tamper-evident hash chain is global (not per-tenant), so a tenant that already produced a chained audit row (prev_hash IS NOT NULL) during the run cannot be hard-deleted without forking that chain for every row inserted after it. The script checks for this (mirroring the guard in src/test-helpers/tenant-cleanup.ts) and, when it applies, intentionally leaves the tenant row in place and prints that it did so -- a small, always uniquely-named, low-cardinality leaked row is the accepted trade-off, the same one 50+ of this repo's own test files already make.

If ANTHROPIC_API_KEY is unset, the script prints a message and exits 0 rather than fabricating a result.

What "publishable" means here

This package is built and tested to be published, but publishing itself (npm publish) is not this package's job -- see the repo's task tracker (T666) for who runs it.

Keywords

contextq

FAQs

Package last updated on 15 Sep 2026

Related posts