@waynesutton/agent-memory 0.0.1 → 0.0.2-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1 +1,1013 @@
1
- # agent-memory
1
+ # @waynesutton/agent-memory (Alpha)
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@waynesutton/agent-memory.svg)](https://www.npmjs.com/package/@waynesutton/agent-memory)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@waynesutton/agent-memory.svg)](https://www.npmjs.com/package/@waynesutton/agent-memory)
5
+ [![GitHub](https://img.shields.io/github/stars/waynesutton/agent-memory?style=social)](https://github.com/waynesutton/agent-memory)
6
+ [![License](https://img.shields.io/npm/l/@waynesutton/agent-memory.svg)](https://github.com/waynesutton/agent-memory/blob/main/LICENSE)
7
+
8
+ > **Alpha** — This package is under active development. APIs may change between releases.
9
+
10
+ A Convex Component for persistent, cloud-synced agent memory. Markdown-first memory backend for AI coding agents across CLIs and IDEs — with intelligent ingest, feedback loops, memory relations, and relevance decay.
11
+
12
+ ## What It Does
13
+
14
+ AI coding agents (Claude Code, Cursor, OpenCode, Codex, Conductor, Zed, VS Code Copilot, Pi) all use local, file-based memory systems (CLAUDE.md, .cursor/rules, AGENTS.md, etc.). These are siloed to one machine with no shared backend, no cross-tool sync, and no queryable search.
15
+
16
+ `@waynesutton/agent-memory` creates a cloud-synced, markdown-first memory backend as a Convex Component. It:
17
+
18
+ - Stores structured memories in Convex with full-text search and vector/semantic search
19
+ - **Intelligently ingests** raw conversations into deduplicated memories via LLM pipeline
20
+ - Exports memories in each tool's native format (`.cursor/rules/*.mdc`, `.claude/rules/*.md`, `AGENTS.md`, etc.)
21
+ - Syncs bidirectionally between local files and the cloud
22
+ - Tracks **memory history** (full audit trail of all changes)
23
+ - Supports **feedback loops** — agents rate memories as helpful or unhelpful
24
+ - Builds **memory relations** (graph connections between related memories)
25
+ - Applies **relevance decay** — stale, low-access memories lose priority over time
26
+ - Scopes memories by **agent, session, user, and project**
27
+ - Provides a **read-only HTTP API** with bearer token auth and rate limiting
28
+ - Exposes an MCP server with 14 tools for direct agent integration
29
+ - Works standalone with any Convex app — no dependency on `@convex-dev/agent`
30
+
31
+ ## Table of Contents
32
+
33
+ - [Installation](#installation)
34
+ - [Quick Start](#quick-start)
35
+ - [Convex Component Setup](#convex-component-setup)
36
+ - [Client API](#client-api)
37
+ - [Intelligent Ingest](#intelligent-ingest)
38
+ - [Memory History](#memory-history)
39
+ - [Feedback & Scoring](#feedback--scoring)
40
+ - [Memory Relations](#memory-relations)
41
+ - [Relevance Decay](#relevance-decay)
42
+ - [CLI Usage](#cli-usage)
43
+ - [MCP Server](#mcp-server)
44
+ - [Schema](#schema)
45
+ - [Memory Types](#memory-types)
46
+ - [Tool Format Support](#tool-format-support)
47
+ - [Vector Search](#vector-search)
48
+ - [Read-Only HTTP API](#read-only-http-api)
49
+ - [Security](#security)
50
+ - [Architecture](#architecture)
51
+
52
+ ---
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ npm install @waynesutton/agent-memory
58
+ ```
59
+
60
+ Peer dependencies (must be installed in your Convex app):
61
+
62
+ ```bash
63
+ npm install convex convex-helpers
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Quick Start
69
+
70
+ ### 1. Add the component to your Convex app
71
+
72
+ ```typescript
73
+ // convex/convex.config.ts
74
+ import { defineApp } from "convex/server";
75
+ import agentMemory from "@waynesutton/agent-memory/convex.config.js";
76
+
77
+ const app = defineApp();
78
+ app.use(agentMemory);
79
+
80
+ export default app;
81
+ ```
82
+
83
+ ### 2. Create wrapper functions
84
+
85
+ ```typescript
86
+ // convex/memory.ts
87
+ import { v } from "convex/values";
88
+ import { query, mutation, action } from "./_generated/server.js";
89
+ import { components } from "./_generated/api.js";
90
+ import { AgentMemory } from "@waynesutton/agent-memory";
91
+
92
+ const memory = new AgentMemory(components.agentMemory, {
93
+ projectId: "my-project",
94
+ agentId: "my-app",
95
+ llmApiKey: process.env.OPENAI_API_KEY, // for intelligent ingest
96
+ });
97
+
98
+ // Save a memory
99
+ export const remember = mutation({
100
+ args: {
101
+ title: v.string(),
102
+ content: v.string(),
103
+ memoryType: v.union(
104
+ v.literal("instruction"),
105
+ v.literal("learning"),
106
+ v.literal("reference"),
107
+ v.literal("feedback"),
108
+ v.literal("journal"),
109
+ ),
110
+ },
111
+ returns: v.string(),
112
+ handler: async (ctx, args) => memory.remember(ctx, args),
113
+ });
114
+
115
+ // Search memories
116
+ export const recall = query({
117
+ args: { q: v.string() },
118
+ returns: v.any(),
119
+ handler: async (ctx, args) => memory.search(ctx, args.q),
120
+ });
121
+
122
+ // List all memories
123
+ export const listMemories = query({
124
+ args: {},
125
+ returns: v.any(),
126
+ handler: async (ctx) => memory.list(ctx),
127
+ });
128
+
129
+ // Intelligently ingest raw text into memories
130
+ export const ingest = action({
131
+ args: { content: v.string() },
132
+ returns: v.any(),
133
+ handler: async (ctx, args) => memory.ingest(ctx, args.content),
134
+ });
135
+
136
+ // View change history
137
+ export const memoryHistory = query({
138
+ args: { memoryId: v.string() },
139
+ returns: v.any(),
140
+ handler: async (ctx, args) => memory.history(ctx, args.memoryId),
141
+ });
142
+ ```
143
+
144
+ ### 3. Deploy and start using
145
+
146
+ ```bash
147
+ npx convex dev
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Convex Component Setup
153
+
154
+ ### Standalone (no other components)
155
+
156
+ ```typescript
157
+ // convex/convex.config.ts
158
+ import { defineApp } from "convex/server";
159
+ import agentMemory from "@waynesutton/agent-memory/convex.config.js";
160
+
161
+ const app = defineApp();
162
+ app.use(agentMemory);
163
+ export default app;
164
+ ```
165
+
166
+ ### Alongside @convex-dev/agent
167
+
168
+ Both components coexist independently with isolated tables:
169
+
170
+ ```typescript
171
+ // convex/convex.config.ts
172
+ import { defineApp } from "convex/server";
173
+ import agentMemory from "@waynesutton/agent-memory/convex.config.js";
174
+ import agent from "@convex-dev/agent/convex.config.js";
175
+
176
+ const app = defineApp();
177
+ app.use(agentMemory); // isolated tables for persistent memories
178
+ app.use(agent); // isolated tables for threads/messages
179
+ export default app;
180
+ ```
181
+
182
+ You can load memories into an Agent's system prompt:
183
+
184
+ ```typescript
185
+ // convex/myAgent.ts
186
+ import { AgentMemory } from "@waynesutton/agent-memory";
187
+ import { Agent } from "@convex-dev/agent";
188
+
189
+ const memory = new AgentMemory(components.agentMemory, {
190
+ projectId: "my-app",
191
+ });
192
+
193
+ export const chat = action({
194
+ args: { message: v.string() },
195
+ handler: async (ctx, args) => {
196
+ const bundle = await memory.getContextBundle(ctx);
197
+ const memoryContext = bundle.pinned
198
+ .map((m) => `## ${m.title}\n${m.content}`)
199
+ .join("\n\n");
200
+
201
+ const myAgent = new Agent(components.agent, {
202
+ model: "claude-sonnet-4-6",
203
+ instructions: `You are a helpful assistant.\n\nRelevant memories:\n${memoryContext}`,
204
+ });
205
+ // ... use agent as normal
206
+ },
207
+ });
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Client API
213
+
214
+ ### Constructor
215
+
216
+ ```typescript
217
+ import { AgentMemory } from "@waynesutton/agent-memory";
218
+
219
+ const memory = new AgentMemory(components.agentMemory, {
220
+ projectId: "my-project", // required: unique project identifier
221
+ defaultScope: "project", // optional: "project" | "user" | "org"
222
+ userId: "user-123", // optional: for user-scoped memories
223
+ agentId: "claude-code", // optional: agent identifier
224
+ sessionId: "session-abc", // optional: session/conversation ID
225
+ embeddingApiKey: "sk-...", // optional: enables vector search
226
+ embeddingModel: "text-embedding-3-small", // optional
227
+ llmApiKey: "sk-...", // optional: enables intelligent ingest
228
+ llmModel: "gpt-4.1-nano", // optional: LLM for fact extraction
229
+ llmBaseUrl: "https://api.openai.com/v1", // optional: custom LLM endpoint
230
+ });
231
+ ```
232
+
233
+ ### Read Operations (query context)
234
+
235
+ ```typescript
236
+ // List with rich filters
237
+ const all = await memory.list(ctx);
238
+ const byAgent = await memory.list(ctx, { agentId: "claude-code" });
239
+ const bySession = await memory.list(ctx, { sessionId: "session-123" });
240
+ const bySource = await memory.list(ctx, { source: "mcp" });
241
+ const byTags = await memory.list(ctx, { tags: ["api", "auth"] });
242
+ const recent = await memory.list(ctx, { createdAfter: Date.now() - 86400000 });
243
+
244
+ // Get a single memory
245
+ const mem = await memory.get(ctx, "jh72k...");
246
+
247
+ // Full-text search
248
+ const results = await memory.search(ctx, "API authentication");
249
+
250
+ // Progressive context bundle (feedback-boosted priority)
251
+ const bundle = await memory.getContextBundle(ctx, {
252
+ activePaths: ["src/api/routes.ts"],
253
+ });
254
+
255
+ // Export as tool-native files
256
+ const files = await memory.exportForTool(ctx, "cursor");
257
+ ```
258
+
259
+ ### Write Operations (mutation context)
260
+
261
+ ```typescript
262
+ // Create (auto-records history)
263
+ const id = await memory.remember(ctx, {
264
+ title: "api-conventions",
265
+ content: "# API Conventions\n\n- Use camelCase\n- Return JSON",
266
+ memoryType: "instruction",
267
+ tags: ["api", "style"],
268
+ paths: ["src/api/**"],
269
+ priority: 0.9,
270
+ });
271
+
272
+ // Update (auto-records history)
273
+ await memory.update(ctx, id, { content: "Updated content", priority: 1.0 });
274
+
275
+ // Archive & restore (both record history)
276
+ await memory.forget(ctx, id);
277
+ await memory.restore(ctx, id);
278
+
279
+ // Batch operations
280
+ await memory.batchArchive(ctx, ["id1", "id2", "id3"]);
281
+ await memory.batchUpdate(ctx, [
282
+ { memoryId: "id1", priority: 0.9 },
283
+ { memoryId: "id2", tags: ["updated"] },
284
+ ]);
285
+ ```
286
+
287
+ ### History & Audit Trail (query context)
288
+
289
+ ```typescript
290
+ // Get change history for a memory
291
+ const history = await memory.history(ctx, id);
292
+ // [{ event: "created", actor: "mcp", timestamp: ..., newContent: "..." }, ...]
293
+
294
+ // Get recent changes across the project
295
+ const recent = await memory.projectHistory(ctx, { limit: 20 });
296
+ ```
297
+
298
+ ### Feedback & Scoring (mutation/query context)
299
+
300
+ ```typescript
301
+ // Rate memories
302
+ await memory.addFeedback(ctx, id, "positive", { comment: "Very helpful rule" });
303
+ await memory.addFeedback(ctx, id, "negative", { comment: "Outdated information" });
304
+
305
+ // View feedback
306
+ const feedback = await memory.getFeedback(ctx, id);
307
+ ```
308
+
309
+ ### Memory Relations (mutation/query context)
310
+
311
+ ```typescript
312
+ // Create relationships
313
+ await memory.addRelation(ctx, memoryA, memoryB, "extends");
314
+ await memory.addRelation(ctx, memoryC, memoryA, "contradicts", { confidence: 0.9 });
315
+
316
+ // View relationships
317
+ const relations = await memory.getRelations(ctx, memoryA);
318
+ const contradictions = await memory.getRelations(ctx, memoryA, { relationship: "contradicts" });
319
+
320
+ // Remove a relationship
321
+ await memory.removeRelation(ctx, relationId);
322
+ ```
323
+
324
+ ### Access Tracking (mutation context)
325
+
326
+ ```typescript
327
+ // Record that memories were read (for relevance decay)
328
+ await memory.recordAccess(ctx, ["id1", "id2"]);
329
+ ```
330
+
331
+ ### Embedding Operations (action context)
332
+
333
+ ```typescript
334
+ // Single embedding
335
+ await memory.embed(ctx, id);
336
+
337
+ // Batch embed all un-embedded
338
+ const result = await memory.embedAll(ctx);
339
+
340
+ // Vector similarity search (falls back to full-text)
341
+ const results = await memory.semanticSearch(ctx, "how to handle auth errors");
342
+ ```
343
+
344
+ ---
345
+
346
+ ## Intelligent Ingest
347
+
348
+ The core "smart memory" feature. Instead of manually creating memories, feed raw text and let the LLM pipeline:
349
+
350
+ 1. **Extract** discrete facts/learnings from conversations or notes
351
+ 2. **Search** existing memories for overlap (semantic deduplication)
352
+ 3. **Decide** per-fact: ADD new, UPDATE existing, DELETE contradicted, or SKIP
353
+ 4. **Return** a structured changelog of what happened
354
+
355
+ ```typescript
356
+ const memory = new AgentMemory(components.agentMemory, {
357
+ projectId: "my-app",
358
+ llmApiKey: process.env.OPENAI_API_KEY,
359
+ });
360
+
361
+ // In an action context
362
+ const result = await memory.ingest(ctx,
363
+ `User prefers TypeScript strict mode. The API should use camelCase.
364
+ Actually, we switched from REST to GraphQL last week.
365
+ The old REST convention docs are outdated.`
366
+ );
367
+
368
+ // result.results:
369
+ // [
370
+ // { event: "added", content: "User prefers TypeScript strict mode", memoryId: "..." },
371
+ // { event: "updated", content: "API uses camelCase with GraphQL", memoryId: "...", previousContent: "..." },
372
+ // { event: "deleted", content: "REST API conventions", memoryId: "...", previousContent: "..." },
373
+ // ]
374
+ ```
375
+
376
+ ### Custom Prompts
377
+
378
+ Override the extraction and decision prompts per-project:
379
+
380
+ ```typescript
381
+ await memory.ingest(ctx, rawText, {
382
+ customExtractionPrompt: "Extract only coding conventions and preferences...",
383
+ customUpdatePrompt: "When facts conflict, always prefer the newer one...",
384
+ });
385
+ ```
386
+
387
+ Or set them in project settings for all ingest operations:
388
+
389
+ ```typescript
390
+ await ctx.runMutation(components.agentMemory.mutations.upsertProject, {
391
+ projectId: "my-app",
392
+ name: "My App",
393
+ settings: {
394
+ autoSync: false,
395
+ syncFormats: [],
396
+ factExtractionPrompt: "Your custom extraction prompt...",
397
+ updateDecisionPrompt: "Your custom update decision prompt...",
398
+ },
399
+ });
400
+ ```
401
+
402
+ ---
403
+
404
+ ## Memory History
405
+
406
+ Every create, update, archive, restore, and merge operation records a history entry. This provides a complete audit trail of how memories change over time.
407
+
408
+ ```typescript
409
+ const history = await memory.history(ctx, memoryId);
410
+ // Returns: MemoryHistoryEntry[]
411
+ // Each entry has: event, actor, timestamp, previousContent, newContent
412
+ ```
413
+
414
+ History is automatically cleaned up by a weekly cron job (entries older than 90 days are removed).
415
+
416
+ ---
417
+
418
+ ## Feedback & Scoring
419
+
420
+ Agents and users can rate memories. Feedback affects the **effective priority** used in context bundles:
421
+
422
+ - Each `positive` feedback adds up to +0.05 priority (max +0.2 boost)
423
+ - Each `negative` feedback subtracts up to -0.1 priority (max -0.5 penalty)
424
+ - `very_negative` counts as negative with stronger signal
425
+
426
+ This means good memories naturally float to the top of context bundles, while bad ones sink — without manual priority management.
427
+
428
+ ---
429
+
430
+ ## Memory Relations
431
+
432
+ Build a knowledge graph between memories:
433
+
434
+ | Relationship | Meaning |
435
+ |-------------|---------|
436
+ | `extends` | Memory B adds detail to Memory A |
437
+ | `contradicts` | Memory B conflicts with Memory A |
438
+ | `replaces` | Memory B supersedes Memory A |
439
+ | `related_to` | General association |
440
+
441
+ Relations are directional (`from` -> `to`) and queryable by direction and type.
442
+
443
+ ---
444
+
445
+ ## Relevance Decay
446
+
447
+ Memories that aren't accessed lose priority over time, preventing stale memories from dominating context windows.
448
+
449
+ **How it works:**
450
+ - A daily cron job (3 AM UTC) checks all non-pinned memories
451
+ - Memories with low access count and old `lastAccessedAt` get reduced priority
452
+ - Decay follows an exponential half-life (configurable per-project, default 30 days)
453
+ - Pinned memories (priority >= 0.8) are never decayed
454
+
455
+ **Enable per-project:**
456
+
457
+ ```typescript
458
+ await ctx.runMutation(components.agentMemory.mutations.upsertProject, {
459
+ projectId: "my-app",
460
+ name: "My App",
461
+ settings: {
462
+ autoSync: false,
463
+ syncFormats: [],
464
+ decayEnabled: true,
465
+ decayHalfLifeDays: 30, // memories lose half their priority every 30 days of no access
466
+ },
467
+ });
468
+ ```
469
+
470
+ ---
471
+
472
+ ## CLI Usage
473
+
474
+ The CLI syncs memories between local tool files and Convex.
475
+
476
+ ### Environment Variable
477
+
478
+ ```bash
479
+ export CONVEX_URL="https://your-deployment.convex.cloud"
480
+ ```
481
+
482
+ ### Commands
483
+
484
+ #### `npx agent-memory init`
485
+
486
+ Detect tools in the current directory and register the project.
487
+
488
+ ```bash
489
+ npx agent-memory init --project my-app --name "My App"
490
+ ```
491
+
492
+ Detects: Claude Code, Cursor, OpenCode, Codex, Conductor, Zed, VS Code Copilot, Pi.
493
+
494
+ #### `npx agent-memory push`
495
+
496
+ Push local memory files to Convex.
497
+
498
+ ```bash
499
+ npx agent-memory push --project my-app
500
+ npx agent-memory push --project my-app --format claude-code # specific tool only
501
+ ```
502
+
503
+ #### `npx agent-memory pull`
504
+
505
+ Pull memories from Convex to local files.
506
+
507
+ ```bash
508
+ npx agent-memory pull --project my-app --format cursor
509
+ npx agent-memory pull --project my-app --format claude-code
510
+ ```
511
+
512
+ #### `npx agent-memory list`
513
+
514
+ List all memories in the terminal.
515
+
516
+ ```bash
517
+ npx agent-memory list --project my-app
518
+ npx agent-memory list --project my-app --type instruction
519
+ ```
520
+
521
+ #### `npx agent-memory search <query>`
522
+
523
+ Search memories from the terminal.
524
+
525
+ ```bash
526
+ npx agent-memory search "API conventions" --project my-app --limit 5
527
+ ```
528
+
529
+ #### `npx agent-memory mcp`
530
+
531
+ Start the MCP server (see [MCP Server](#mcp-server) section).
532
+
533
+ ```bash
534
+ npx agent-memory mcp --project my-app
535
+ npx agent-memory mcp --project my-app --llm-api-key $OPENAI_API_KEY # enable ingest
536
+ ```
537
+
538
+ ### Hook Integration
539
+
540
+ Auto-sync on Claude Code session start/end:
541
+
542
+ ```json
543
+ // .claude/settings.json
544
+ {
545
+ "hooks": {
546
+ "SessionStart": [{
547
+ "hooks": [{ "type": "command", "command": "npx agent-memory pull --format claude-code" }]
548
+ }],
549
+ "SessionEnd": [{
550
+ "hooks": [{ "type": "command", "command": "npx agent-memory push --format claude-code" }]
551
+ }]
552
+ }
553
+ }
554
+ ```
555
+
556
+ ---
557
+
558
+ ## MCP Server
559
+
560
+ The MCP server runs as a local process, bridging AI tools to your Convex backend via stdio/JSON-RPC.
561
+
562
+ ```
563
+ ┌─────────────┐ stdio/JSON-RPC ┌──────────────────┐ ConvexHttpClient ┌─────────┐
564
+ │ Claude Code │ <────────────────> │ MCP Server │ <──────────────────> │ Convex │
565
+ │ Cursor │ │ (local process) │ │ Cloud │
566
+ │ VS Code │ │ npx agent-memory │ │ │
567
+ └─────────────┘ └──────────────────┘ └─────────┘
568
+ ```
569
+
570
+ ### Starting the Server
571
+
572
+ ```bash
573
+ npx agent-memory mcp --project my-app
574
+ ```
575
+
576
+ Options:
577
+ | Flag | Description |
578
+ |------|-------------|
579
+ | `--project <id>` | Project ID (default: "default") |
580
+ | `--read-only` | Disable write operations |
581
+ | `--disable-tools <tools>` | Comma-separated tool names to disable |
582
+ | `--embedding-api-key <key>` | Enable vector search |
583
+ | `--llm-api-key <key>` | Enable intelligent ingest |
584
+ | `--llm-model <model>` | LLM model for ingest (default: "gpt-4.1-nano") |
585
+
586
+ ### MCP Tools (14 total)
587
+
588
+ | Tool | Description |
589
+ |------|-------------|
590
+ | `memory_remember` | Save a new memory (with agent/session scoping) |
591
+ | `memory_recall` | Search memories by keyword (full-text) |
592
+ | `memory_semantic_recall` | Search memories by meaning (vector) |
593
+ | `memory_list` | List memories with filters (agent, session, source, tags) |
594
+ | `memory_context` | Get context bundle (pinned + relevant) |
595
+ | `memory_forget` | Archive a memory |
596
+ | `memory_restore` | Restore an archived memory |
597
+ | `memory_update` | Update an existing memory |
598
+ | `memory_history` | View change audit trail |
599
+ | `memory_feedback` | Rate a memory as helpful/unhelpful |
600
+ | `memory_relate` | Create relationship between memories |
601
+ | `memory_relations` | View memory graph connections |
602
+ | `memory_batch_archive` | Archive multiple memories at once |
603
+ | `memory_ingest` | Intelligently extract memories from raw text |
604
+
605
+ ### MCP Resources
606
+
607
+ | URI | Description |
608
+ |-----|-------------|
609
+ | `memory://project/{id}/pinned` | High-priority memories auto-loaded at session start |
610
+
611
+ ### Configuration in Claude Code
612
+
613
+ ```json
614
+ // .claude/settings.json
615
+ {
616
+ "mcpServers": {
617
+ "agent-memory": {
618
+ "command": "npx",
619
+ "args": [
620
+ "agent-memory", "mcp",
621
+ "--project", "my-app",
622
+ "--llm-api-key", "${env:OPENAI_API_KEY}"
623
+ ],
624
+ "env": {
625
+ "CONVEX_URL": "${env:CONVEX_URL}",
626
+ "OPENAI_API_KEY": "${env:OPENAI_API_KEY}"
627
+ }
628
+ }
629
+ }
630
+ }
631
+ ```
632
+
633
+ ### Configuration in Cursor
634
+
635
+ ```json
636
+ // .cursor/mcp.json
637
+ {
638
+ "mcpServers": {
639
+ "agent-memory": {
640
+ "command": "npx",
641
+ "args": [
642
+ "agent-memory", "mcp",
643
+ "--project", "my-app",
644
+ "--llm-api-key", "${env:OPENAI_API_KEY}"
645
+ ],
646
+ "env": {
647
+ "CONVEX_URL": "${env:CONVEX_URL}",
648
+ "OPENAI_API_KEY": "${env:OPENAI_API_KEY}"
649
+ }
650
+ }
651
+ }
652
+ }
653
+ ```
654
+
655
+ ---
656
+
657
+ ## Schema
658
+
659
+ The component creates 9 isolated tables in your Convex deployment:
660
+
661
+ ### `memories`
662
+
663
+ | Field | Type | Description |
664
+ |-------|------|-------------|
665
+ | `projectId` | `string` | Project identifier |
666
+ | `scope` | `"project" \| "user" \| "org"` | Visibility scope |
667
+ | `userId` | `string?` | Owner for user-scoped memories |
668
+ | `agentId` | `string?` | Agent that created/owns this memory |
669
+ | `sessionId` | `string?` | Session/conversation ID |
670
+ | `title` | `string` | Short title/slug |
671
+ | `content` | `string` | Markdown content |
672
+ | `memoryType` | `MemoryType` | Category (see below) |
673
+ | `tags` | `string[]` | Searchable tags |
674
+ | `paths` | `string[]?` | File glob patterns for relevance matching |
675
+ | `priority` | `number?` | 0-1 scale (>= 0.8 = pinned) |
676
+ | `source` | `string?` | Origin tool ("claude-code", "cursor", "mcp", "ingest", etc.) |
677
+ | `checksum` | `string` | FNV-1a hash for change detection |
678
+ | `archived` | `boolean` | Soft delete flag |
679
+ | `embeddingId` | `Id?` | Link to vector embedding |
680
+ | `accessCount` | `number?` | Times this memory was accessed |
681
+ | `lastAccessedAt` | `number?` | Timestamp of last access |
682
+ | `positiveCount` | `number?` | Positive feedback count |
683
+ | `negativeCount` | `number?` | Negative feedback count |
684
+
685
+ **Indexes:** `by_project`, `by_project_scope`, `by_project_title`, `by_type_priority`, `by_agent`, `by_session`, `by_source`, `by_last_accessed`
686
+ **Search indexes:** `search_content` (full-text on content), `search_title` (full-text on title)
687
+
688
+ ### `embeddings`
689
+
690
+ Vector embeddings for semantic search. Linked to memories via `memoryId`.
691
+
692
+ **Vector index:** `by_embedding` (1536 dimensions, OpenAI-compatible)
693
+
694
+ ### `projects`
695
+
696
+ Project registry with settings for sync, custom prompts, and decay configuration.
697
+
698
+ ### `syncLog`
699
+
700
+ Tracks push/pull sync events for conflict detection.
701
+
702
+ ### `memoryHistory`
703
+
704
+ Audit trail of all memory changes: created, updated, archived, restored, merged.
705
+
706
+ ### `memoryFeedback`
707
+
708
+ Quality signals from agents/users: positive, negative, very_negative with optional comments.
709
+
710
+ ### `memoryRelations`
711
+
712
+ Directional graph connections between memories with relationship types and metadata.
713
+
714
+ ### `apiKeys`
715
+
716
+ Bearer tokens for the read-only HTTP API. Stores hashed keys with per-key permissions, rate limit overrides, and expiry.
717
+
718
+ ### `rateLimitTokens`
719
+
720
+ Fixed-window token counters for HTTP API rate limiting. Cleaned up hourly by cron.
721
+
722
+ ---
723
+
724
+ ## Memory Types
725
+
726
+ | Type | Description | Maps To |
727
+ |------|-------------|---------|
728
+ | `instruction` | Rules and conventions | `.claude/rules/`, `.cursor/rules/`, `AGENTS.md` |
729
+ | `learning` | Auto-discovered patterns | Claude Code auto-memory |
730
+ | `reference` | Architecture docs, API specs | Reference documentation |
731
+ | `feedback` | Corrections, preferences | User feedback on behavior |
732
+ | `journal` | Session logs | OpenCode journal entries |
733
+
734
+ ---
735
+
736
+ ## Tool Format Support
737
+
738
+ The component reads from and writes to 8 tool formats:
739
+
740
+ | Tool | Parser Reads | Formatter Writes |
741
+ |------|-------------|-----------------|
742
+ | **Claude Code** | `.claude/rules/*.md` (YAML frontmatter) | `.claude/rules/<title>.md` |
743
+ | **Cursor** | `.cursor/rules/*.mdc` (YAML frontmatter) | `.cursor/rules/<title>.mdc` |
744
+ | **OpenCode** | `AGENTS.md` (## sections) | `AGENTS.md` or `journal/<title>.md` |
745
+ | **Codex** | `AGENTS.md`, `AGENTS.override.md` | `AGENTS.md` or `<dir>/AGENTS.md` |
746
+ | **Conductor** | `.conductor/rules/*.md` | `.conductor/rules/<title>.md` |
747
+ | **Zed** | `.zed/rules/*.md` | `.zed/rules/<title>.md` |
748
+ | **VS Code Copilot** | `.github/copilot-instructions.md`, `.copilot/rules/*.md` | `.github/copilot-instructions.md` |
749
+ | **Pi** | `.pi/rules/*.md` | `.pi/rules/<title>.md` |
750
+
751
+ ### Cross-Tool Sync Example
752
+
753
+ Push from Claude Code, pull as Cursor rules:
754
+
755
+ ```bash
756
+ # In your project directory with .claude/rules/ files
757
+ npx agent-memory push --project my-app --format claude-code
758
+
759
+ # On another machine or for another tool
760
+ npx agent-memory pull --project my-app --format cursor
761
+ # Creates .cursor/rules/*.mdc files with proper frontmatter
762
+ ```
763
+
764
+ ---
765
+
766
+ ## Vector Search
767
+
768
+ Vector search is opt-in. Everything works with full-text search by default.
769
+
770
+ ### Enabling Vector Search
771
+
772
+ Pass an OpenAI API key when configuring:
773
+
774
+ ```typescript
775
+ const memory = new AgentMemory(components.agentMemory, {
776
+ projectId: "my-app",
777
+ embeddingApiKey: process.env.OPENAI_API_KEY,
778
+ embeddingModel: "text-embedding-3-small", // default
779
+ });
780
+ ```
781
+
782
+ Or via CLI/MCP:
783
+
784
+ ```bash
785
+ npx agent-memory mcp --project my-app --embedding-api-key $OPENAI_API_KEY
786
+ ```
787
+
788
+ ### Backfilling Embeddings
789
+
790
+ ```typescript
791
+ // In an action context
792
+ const result = await memory.embedAll(ctx);
793
+ console.log(`Embedded ${result.embedded} memories, skipped ${result.skipped}`);
794
+ ```
795
+
796
+ ### Fallback Behavior
797
+
798
+ If no embedding API key is provided, `semanticSearch` automatically falls back to full-text search. No errors, no configuration changes needed.
799
+
800
+ ---
801
+
802
+ ## Read-Only HTTP API
803
+
804
+ Expose memories as REST endpoints for dashboards, CI/CD, and external integrations. The component provides a `MemoryHttpApi` class that generates `httpAction` handlers — your app mounts them on its own `httpRouter`.
805
+
806
+ ### Setup
807
+
808
+ ```typescript
809
+ // convex/http.ts
810
+ import { httpRouter } from "convex/server";
811
+ import { MemoryHttpApi } from "@waynesutton/agent-memory/http";
812
+ import { components } from "./_generated/api";
813
+
814
+ const http = httpRouter();
815
+ const memoryApi = new MemoryHttpApi(components.agentMemory, {
816
+ corsOrigins: ["https://myapp.com"], // optional, defaults to ["*"]
817
+ });
818
+ memoryApi.mount(http, "/api/memory");
819
+ export default http;
820
+ ```
821
+
822
+ ### Creating API Keys
823
+
824
+ ```typescript
825
+ // convex/memory.ts — behind your app's own auth
826
+ import { AgentMemory } from "@waynesutton/agent-memory";
827
+
828
+ const memory = new AgentMemory(components.agentMemory, { projectId: "my-app" });
829
+
830
+ export const createReadKey = mutation({
831
+ args: {},
832
+ handler: async (ctx) => {
833
+ const identity = await ctx.auth.getUserIdentity();
834
+ if (!identity) throw new Error("Not authenticated");
835
+ return await memory.createApiKey(ctx, {
836
+ name: "Dashboard key",
837
+ permissions: ["list", "search", "context"],
838
+ });
839
+ },
840
+ });
841
+ ```
842
+
843
+ ### Using the API
844
+
845
+ ```bash
846
+ # List memories
847
+ curl -H "Authorization: Bearer am_<key>" \
848
+ https://your-deployment.convex.cloud/api/memory/list
849
+
850
+ # Search
851
+ curl -H "Authorization: Bearer am_<key>" \
852
+ "https://your-deployment.convex.cloud/api/memory/search?q=API+conventions"
853
+
854
+ # Get context bundle
855
+ curl -H "Authorization: Bearer am_<key>" \
856
+ https://your-deployment.convex.cloud/api/memory/context
857
+ ```
858
+
859
+ ### Endpoints
860
+
861
+ | Path | Permission | Description |
862
+ |------|------------|-------------|
863
+ | `/list` | `list` | List memories with filters |
864
+ | `/get?id=<id>` | `get` | Get single memory |
865
+ | `/search?q=<query>` | `search` | Full-text search |
866
+ | `/context` | `context` | Progressive context bundle |
867
+ | `/export?format=<format>` | `export` | Export in tool format |
868
+ | `/history?id=<id>` | `history` | Memory audit trail |
869
+ | `/relations?id=<id>` | `relations` | Memory graph |
870
+
871
+ ### Rate Limiting
872
+
873
+ Self-contained fixed-window token bucket (no external dependency):
874
+
875
+ - **Default:** 100 requests per 60 seconds
876
+ - **Configurable:** per-key override > per-project setting > global default
877
+ - Returns `429` with `retryAfterMs` when exceeded
878
+ - Old window records cleaned up hourly by cron
879
+
880
+ ### Available Permissions
881
+
882
+ `list`, `get`, `search`, `context`, `export`, `history`, `relations`
883
+
884
+ ---
885
+
886
+ ## Security
887
+
888
+ 6 layers of protection:
889
+
890
+ | Layer | What | How |
891
+ |-------|------|-----|
892
+ | **Deployment URL** | Gate to your Convex backend | `CONVEX_URL` env var required. Each app has its own isolated deployment. |
893
+ | **Auth Token** | Authenticates the caller | Optional `CONVEX_AUTH_TOKEN` for production/team use. |
894
+ | **API Keys** | HTTP API access control | Bearer tokens with per-key permissions, expiry, and rate limits. Keys stored as hashes. |
895
+ | **Project Scope** | Isolates by project | `--project` flag. MCP server and API keys only access that project's memories. |
896
+ | **Tool Disabling** | Restrict operations | `--read-only` or `--disable-tools` flags for fine-grained control. |
897
+ | **Convex Isolation** | Runtime sandboxing | Component tables are isolated. Queries can't write. Mutations are transactional. |
898
+
899
+ ### Examples
900
+
901
+ ```bash
902
+ # Full access (default)
903
+ npx agent-memory mcp --project my-app
904
+
905
+ # Read-only (no write/delete/ingest tools)
906
+ npx agent-memory mcp --project my-app --read-only
907
+
908
+ # Disable specific tools
909
+ npx agent-memory mcp --project my-app --disable-tools memory_forget,memory_ingest
910
+ ```
911
+
912
+ ### MCP Config with Secrets
913
+
914
+ ```json
915
+ {
916
+ "mcpServers": {
917
+ "agent-memory": {
918
+ "command": "npx",
919
+ "args": ["agent-memory", "mcp", "--project", "my-app"],
920
+ "env": {
921
+ "CONVEX_URL": "${env:CONVEX_URL}",
922
+ "CONVEX_AUTH_TOKEN": "${env:CONVEX_AUTH_TOKEN}"
923
+ }
924
+ }
925
+ }
926
+ }
927
+ ```
928
+
929
+ ---
930
+
931
+ ## Architecture
932
+
933
+ ```
934
+ @waynesutton/agent-memory
935
+ ├── src/
936
+ │ ├── component/ # Convex backend (defineComponent)
937
+ │ │ ├── schema.ts # 9 tables: memories, embeddings, projects, syncLog,
938
+ │ │ │ # memoryHistory, memoryFeedback, memoryRelations,
939
+ │ │ │ # apiKeys, rateLimitTokens
940
+ │ │ ├── mutations.ts # CRUD + batch + feedback + relations + history tracking
941
+ │ │ ├── queries.ts # list, search, context bundle, history, feedback, relations
942
+ │ │ ├── actions.ts # embeddings, semantic search, intelligent ingest pipeline
943
+ │ │ ├── apiKeyMutations.ts # API key create/revoke, rate limit consume
944
+ │ │ ├── apiKeyQueries.ts # API key validation, listing
945
+ │ │ ├── crons.ts # Daily relevance decay + weekly history cleanup + hourly rate limit cleanup
946
+ │ │ ├── cronActions.ts # Internal actions called by cron jobs
947
+ │ │ ├── cronQueries.ts # Internal queries for cron job data
948
+ │ │ ├── format.ts # Memory -> tool-native file conversion
949
+ │ │ └── checksum.ts # FNV-1a content hashing
950
+ │ ├── client/
951
+ │ │ ├── index.ts # AgentMemory class (public API)
952
+ │ │ └── http.ts # MemoryHttpApi class (read-only HTTP API)
953
+ │ ├── cli/
954
+ │ │ ├── index.ts # CLI: init, push, pull, list, search, mcp
955
+ │ │ ├── sync.ts # Push/pull sync logic
956
+ │ │ └── parsers/ # 8 tool parsers (local files -> memories)
957
+ │ ├── mcp/
958
+ │ │ └── server.ts # MCP server (14 tools + resources)
959
+ │ ├── shared.ts # Shared types and validators
960
+ │ └── test.ts # Test helper for convex-test
961
+ └── example/
962
+ └── convex/ # Example Convex app
963
+ ```
964
+
965
+ ### Key Design Principles
966
+
967
+ - **Works without any API key** — full-text search, CRUD, sync, export, history, feedback, and relations all work with zero external dependencies
968
+ - **Vector search is opt-in** — pass `embeddingApiKey` to enable; falls back to full-text automatically
969
+ - **Intelligent ingest is opt-in** — pass `llmApiKey` to enable LLM-powered fact extraction and deduplication
970
+ - **Standalone** — no dependency on `@convex-dev/agent` or any other component
971
+ - **Markdown-first** — memories are markdown documents with optional YAML frontmatter
972
+ - **Checksum-based sync** — only changed content is pushed/pulled (FNV-1a hashing)
973
+ - **Progressive disclosure** — context bundles tier memories as pinned/relevant/available
974
+ - **Feedback-boosted scoring** — positive feedback raises priority; negative feedback lowers it
975
+ - **Self-maintaining** — cron jobs handle relevance decay and history cleanup automatically
976
+ - **Multi-dimensional scoping** — project + user + agent + session isolation
977
+
978
+ ### Why Convex
979
+
980
+ Built as a Convex Component, agent-memory inherits powerful guarantees that file-based memory systems (CLAUDE.md, .cursor/rules) cannot provide:
981
+
982
+ - **Real-time reactive queries** — memories update live across all connected clients. When one agent saves a memory, every other agent sees it instantly without polling or pulling.
983
+ - **ACID transactional writes** — every create, update, and archive is fully transactional. No partial saves, no corrupted state, no merge conflicts.
984
+ - **Multi-agent concurrency** — multiple agents and humans can read and write simultaneously across machines with full consistency guarantees. No locking, no race conditions.
985
+ - **Zero infrastructure** — no database to provision, no servers to manage. Convex handles storage, indexing, full-text search, and vector search out of the box.
986
+ - **Isolated component tables** — the 9 memory tables live in their own namespace (`agentMemory:`), completely isolated from your app's tables. No schema conflicts, no migrations to coordinate.
987
+
988
+ ---
989
+
990
+ ## Testing
991
+
992
+ ### Using in Your Tests
993
+
994
+ ```typescript
995
+ import { convexTest } from "convex-test";
996
+ import agentMemoryTest from "@waynesutton/agent-memory/test";
997
+
998
+ const t = convexTest();
999
+ agentMemoryTest.register(t);
1000
+ ```
1001
+
1002
+ ### Running Component Tests
1003
+
1004
+ ```bash
1005
+ npm test # run all tests
1006
+ npm run test:watch # watch mode
1007
+ ```
1008
+
1009
+ ---
1010
+
1011
+ ## License
1012
+
1013
+ Apache-2.0