@cogmem/engram 0.2.0 → 0.3.0

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
@@ -19,6 +19,21 @@ bun install -g @cogmem/engram
19
19
  engram --help
20
20
  ```
21
21
 
22
+ ### Quick Setup for AI Editors
23
+
24
+ The `install` command sets up the skill file and MCP server config for your editor:
25
+
26
+ ```bash
27
+ engram install # interactive — prompts for provider + scope
28
+ engram install --provider claude --global # no prompts, installs to ~/.claude/
29
+ engram install --provider claude --project # installs to ./.claude/ in current dir
30
+ engram install --provider claude --global --dry-run # preview without writing files
31
+ ```
32
+
33
+ This installs two things:
34
+ 1. **SKILL.md** — a cognitive protocol that teaches agents how to use engram effectively
35
+ 2. **MCP config** — adds the engram server to your editor's MCP settings
36
+
22
37
  ## The Science
23
38
 
24
39
  engram is built on memory research. Every design decision traces back to how the brain operates.
@@ -158,7 +173,7 @@ engram exposes its cognitive model as an MCP (Model Context Protocol) server, so
158
173
 
159
174
  ### Setup
160
175
 
161
- Add to your MCP client configuration (e.g., Claude Code `settings.json`):
176
+ The easiest way is `engram install` (see above). To configure manually, add to your MCP client configuration:
162
177
 
163
178
  ```json
164
179
  {
package/SKILL.md ADDED
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: engram
3
+ description: Cognitive memory for AI agents — use when encoding, recalling, or managing persistent memories across sessions
4
+ ---
5
+
6
+ # Engram — Cognitive Memory Protocol
7
+
8
+ You have a biologically-inspired memory system. Use it like a brain, not a database.
9
+
10
+ ## Session Lifecycle
11
+
12
+ **Start:** Recall what you know about the current context.
13
+ ```
14
+ memory_recall → { action: "recall", cue: "<project or topic>" }
15
+ memory_manage → { action: "focus_get" }
16
+ ```
17
+
18
+ **During:** Encode insights as they emerge. Don't batch everything at the end.
19
+ ```
20
+ memory_store → { action: "encode", content: "...", type: "...", emotion: "..." }
21
+ ```
22
+
23
+ **End:** Consolidate to strengthen and link memories.
24
+ ```
25
+ memory_manage → { action: "consolidate" }
26
+ ```
27
+
28
+ ## Memory Types
29
+
30
+ | Type | Use when | Examples |
31
+ |------|----------|---------|
32
+ | `episodic` | Something *happened* — events, interactions, debugging sessions | "User reported login failing on Safari", "Deployed v2.3 with new caching" |
33
+ | `semantic` | A *fact* or *concept* — knowledge, definitions, relationships | "Auth uses JWT with 24h expiry", "The payments module depends on Stripe SDK" |
34
+ | `procedural` | A *skill* or *process* — how to do things, patterns, workflows | "To deploy: run tests → build → push to staging → verify → promote" |
35
+
36
+ **Default to `semantic`** when unsure. Procedural memories never decay — use them for durable skills.
37
+
38
+ ## Emotion Tags
39
+
40
+ Tag memories with emotional context. This affects recall priority — emotional memories surface faster.
41
+
42
+ | Emotion | When to use |
43
+ |---------|-------------|
44
+ | `joy` | Something worked well, positive outcome, breakthrough |
45
+ | `satisfaction` | Task completed successfully, clean solution |
46
+ | `curiosity` | Interesting finding, worth exploring further |
47
+ | `surprise` | Unexpected behavior, counter-intuitive result |
48
+ | `anxiety` | Risk identified, potential failure, fragile code |
49
+ | `frustration` | Recurring problem, friction, workaround needed |
50
+ | `neutral` | Routine fact, no emotional significance |
51
+
52
+ Omit emotion for routine facts. Tag frustration on pain points — it helps surface them when they recur.
53
+
54
+ ## MCP Tools Reference
55
+
56
+ ### memory_store
57
+ | Action | Required | Optional |
58
+ |--------|----------|----------|
59
+ | `encode` | `content` | `type`, `emotion`, `emotionWeight` (0-1), `context` |
60
+ | `encode_batch` | `memories[]` (1-50) | each: `type`, `emotion`, `emotionWeight`, `context` |
61
+ | `reconsolidate` | `id` | `newContext`, `currentEmotion`, `currentEmotionWeight` |
62
+
63
+ ### memory_recall
64
+ | Action | Required | Optional |
65
+ |--------|----------|----------|
66
+ | `recall` | `cue` | `limit`, `type`, `context`, `associative` (bool), `format` |
67
+ | `list` | — | `type`, `context`, `limit`, `offset`, `format` |
68
+ | `inspect` | `id` | — |
69
+ | `stats` | — | — |
70
+
71
+ ### memory_manage
72
+ | Action | Required | Optional |
73
+ |--------|----------|----------|
74
+ | `consolidate` | — | — |
75
+ | `focus_push` | `content` | `memoryRef` |
76
+ | `focus_pop` | — | — |
77
+ | `focus_get` | — | — |
78
+ | `focus_clear` | — | — |
79
+ | `recall_to_focus` | `cue` | `limit`, `type`, `context` |
80
+
81
+ ## Working Memory (Focus Buffer)
82
+
83
+ 7 slots. Use it to hold active context during complex tasks.
84
+
85
+ - **Push** key facts you'll reference repeatedly during a task
86
+ - **Recall to focus** loads top recall results into the buffer
87
+ - **Pop/clear** when switching contexts
88
+ - The buffer is LIFO — newest items pop first
89
+
90
+ **Priming pattern:** At session start, recall + focus to seed your working context:
91
+ ```
92
+ memory_manage → { action: "recall_to_focus", cue: "<current task>" }
93
+ ```
94
+
95
+ ## Key Behaviors
96
+
97
+ - **Recall strengthens memories** — each recall boosts activation (use-it-or-lose-it)
98
+ - **List does NOT strengthen** — use list for browsing without side effects
99
+ - **Procedural memories never decay** — once encoded, they persist permanently
100
+ - **Consolidation discovers associations** — run it to link related memories
101
+ - **Emotional memories resist decay** — tagged memories survive longer
102
+ - **Context scopes memories** — use `context: "project:name"` to partition
103
+
104
+ ## What to Encode
105
+
106
+ **Encode:** decisions and their rationale, architectural insights, debugging breakthroughs, user preferences, recurring patterns, project-specific knowledge, lessons learned
107
+
108
+ **Don't encode:** transient task state, information already in code/docs, obvious facts, raw data without interpretation
109
+
110
+ ## Context Convention
111
+
112
+ Use hierarchical context tags: `project:engram`, `project:acme/auth`, `topic:deployment`.
113
+ This lets you recall scoped to a project or topic without noise from other domains.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogmem/engram",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Human memory for artificial minds — a cognitive memory system modeled on neuroscience",
5
5
  "type": "module",
6
6
  "exports": {
@@ -18,7 +18,8 @@
18
18
  },
19
19
  "files": [
20
20
  "src/",
21
- "drizzle/"
21
+ "drizzle/",
22
+ "SKILL.md"
22
23
  ],
23
24
  "scripts": {
24
25
  "start": "bun run src/cli/index.ts",
@@ -34,6 +35,7 @@
34
35
  "@modelcontextprotocol/sdk": "^1.27.1",
35
36
  "citty": "^0.2.1",
36
37
  "cli-table3": "^0.6.5",
38
+ "consola": "^3.4.2",
37
39
  "dayjs": "^1.11.19",
38
40
  "drizzle-orm": "^0.45.1",
39
41
  "kleur": "^4.1.5",
@@ -0,0 +1,113 @@
1
+ import { defineCommand } from "citty";
2
+ import { readFileSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { consola } from "consola";
6
+ import { getProvider, availableProviders } from "../providers/index.ts";
7
+ import { green, dim, yellow, bold } from "../format.ts";
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const SKILL_PATH = join(__dirname, "..", "..", "..", "SKILL.md");
11
+
12
+ function loadSkillContent(): string {
13
+ return readFileSync(SKILL_PATH, "utf-8");
14
+ }
15
+
16
+ export const installCommand = defineCommand({
17
+ meta: {
18
+ name: "install",
19
+ description: "Install engram skill + MCP config for your AI editor",
20
+ },
21
+ args: {
22
+ provider: {
23
+ type: "string",
24
+ description: "Provider to install for (claude)",
25
+ alias: "p",
26
+ },
27
+ global: {
28
+ type: "boolean",
29
+ description: "Install globally (~/.claude/)",
30
+ alias: "g",
31
+ },
32
+ project: {
33
+ type: "boolean",
34
+ description: "Install to current project directory",
35
+ },
36
+ dryRun: {
37
+ type: "boolean",
38
+ description: "Show what would be installed without writing files",
39
+ alias: "n",
40
+ },
41
+ },
42
+ async run({ args }) {
43
+ const dryRun = args.dryRun ?? false;
44
+
45
+ let providerName = args.provider;
46
+ if (!providerName) {
47
+ const choices = availableProviders.map((p) => ({
48
+ label: p.available ? p.displayName : `${p.displayName} (coming soon)`,
49
+ value: p.name,
50
+ disabled: !p.available,
51
+ }));
52
+
53
+ providerName = (await consola.prompt("Select a provider", {
54
+ type: "select",
55
+ options: choices,
56
+ })) as unknown as string;
57
+
58
+ if (typeof providerName === "symbol") process.exit(0);
59
+ }
60
+
61
+ const provider = getProvider(providerName);
62
+ if (!provider) {
63
+ consola.error(`Unknown provider: ${providerName}`);
64
+ process.exit(1);
65
+ }
66
+ if (!provider.available) {
67
+ consola.error(`${provider.displayName} is not yet supported`);
68
+ process.exit(1);
69
+ }
70
+
71
+ let scope: "global" | "project" | undefined;
72
+ if (args.global) scope = "global";
73
+ else if (args.project) scope = "project";
74
+
75
+ if (!scope) {
76
+ scope = (await consola.prompt("Install scope", {
77
+ type: "select",
78
+ options: [
79
+ { label: `Global (~/.claude/)`, value: "global" },
80
+ { label: `Project (./.claude/)`, value: "project" },
81
+ ],
82
+ })) as unknown as "global" | "project";
83
+
84
+ if (typeof scope === "symbol") process.exit(0);
85
+ }
86
+
87
+ const skillContent = loadSkillContent();
88
+
89
+ if (dryRun) console.log(yellow("\n dry run — no files will be written\n"));
90
+
91
+ const result =
92
+ scope === "global"
93
+ ? await provider.installGlobal(skillContent, dryRun)
94
+ : await provider.installProject(skillContent, process.cwd(), dryRun);
95
+
96
+ if (result.status === "already_installed") {
97
+ console.log(dim(" already installed — nothing to do"));
98
+ console.log(dim(` skill: ${result.skillPath}`));
99
+ if (result.mcpConfigPath) console.log(dim(` mcp: ${result.mcpConfigPath}`));
100
+ return;
101
+ }
102
+
103
+ const prefix = dryRun ? "would install" : "installed";
104
+ const check = dryRun ? yellow("~") : green("\u2713");
105
+
106
+ console.log(` ${check} Skill ${prefix} ${dim("\u2192")} ${bold(result.skillPath)}`);
107
+ if (result.mcpConfigured && result.mcpConfigPath) {
108
+ console.log(` ${check} MCP ${prefix} ${dim("\u2192")} ${bold(result.mcpConfigPath)}`);
109
+ } else if (result.mcpConfigPath) {
110
+ console.log(dim(` - MCP already configured → ${result.mcpConfigPath}`));
111
+ }
112
+ },
113
+ });
package/src/cli/index.ts CHANGED
@@ -9,6 +9,7 @@ import { statsCommand } from "./commands/stats.ts";
9
9
  import { listCommand } from "./commands/list.ts";
10
10
  import { sleepCommand } from "./commands/sleep.ts";
11
11
  import { healthCommand } from "./commands/health.ts";
12
+ import { installCommand } from "./commands/install.ts";
12
13
 
13
14
  const main = defineCommand({
14
15
  meta: {
@@ -25,6 +26,7 @@ const main = defineCommand({
25
26
  stats: statsCommand,
26
27
  sleep: sleepCommand,
27
28
  health: healthCommand,
29
+ install: installCommand,
28
30
  },
29
31
  });
30
32
 
@@ -0,0 +1,89 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import type { ProviderInstaller } from "./types.ts";
5
+
6
+ const MCP_SERVER_CONFIG = {
7
+ command: "bunx",
8
+ args: ["-p", "@cogmem/engram", "engram-mcp"],
9
+ };
10
+
11
+ function ensureDir(dir: string) {
12
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
13
+ }
14
+
15
+ function readJsonFile(path: string): Record<string, unknown> {
16
+ if (!existsSync(path)) return {};
17
+ return JSON.parse(readFileSync(path, "utf-8"));
18
+ }
19
+
20
+ function writeJsonFile(path: string, data: Record<string, unknown>) {
21
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
22
+ }
23
+
24
+ function installSkill(skillDir: string, skillContent: string, dryRun: boolean): boolean {
25
+ const skillPath = join(skillDir, "SKILL.md");
26
+ if (existsSync(skillPath)) {
27
+ const existing = readFileSync(skillPath, "utf-8");
28
+ if (existing === skillContent) return false;
29
+ }
30
+ if (!dryRun) {
31
+ ensureDir(skillDir);
32
+ writeFileSync(skillPath, skillContent);
33
+ }
34
+ return true;
35
+ }
36
+
37
+ function configureMcp(configPath: string, dryRun: boolean): boolean {
38
+ const config = readJsonFile(configPath);
39
+ const servers = (config.mcpServers ?? {}) as Record<string, unknown>;
40
+ if (servers.engram) return false;
41
+ if (!dryRun) {
42
+ servers.engram = MCP_SERVER_CONFIG;
43
+ config.mcpServers = servers;
44
+ ensureDir(join(configPath, ".."));
45
+ writeJsonFile(configPath, config);
46
+ }
47
+ return true;
48
+ }
49
+
50
+ export const claudeProvider: ProviderInstaller = {
51
+ name: "claude",
52
+ displayName: "Claude Code",
53
+ available: true,
54
+
55
+ async installGlobal(skillContent, dryRun) {
56
+ const home = homedir();
57
+ const skillDir = join(home, ".claude", "skills", "engram");
58
+ const configPath = join(home, ".claude", "settings.json");
59
+
60
+ const skillInstalled = installSkill(skillDir, skillContent, dryRun);
61
+ const mcpInstalled = configureMcp(configPath, dryRun);
62
+
63
+ const status = !skillInstalled && !mcpInstalled ? "already_installed" : skillInstalled && mcpInstalled ? "installed" : "updated";
64
+
65
+ return {
66
+ status,
67
+ skillPath: join(skillDir, "SKILL.md"),
68
+ mcpConfigured: mcpInstalled,
69
+ mcpConfigPath: configPath,
70
+ };
71
+ },
72
+
73
+ async installProject(skillContent, projectDir, dryRun) {
74
+ const skillDir = join(projectDir, ".claude", "skills", "engram");
75
+ const configPath = join(projectDir, ".claude", "settings.local.json");
76
+
77
+ const skillInstalled = installSkill(skillDir, skillContent, dryRun);
78
+ const mcpInstalled = configureMcp(configPath, dryRun);
79
+
80
+ const status = !skillInstalled && !mcpInstalled ? "already_installed" : skillInstalled && mcpInstalled ? "installed" : "updated";
81
+
82
+ return {
83
+ status,
84
+ skillPath: join(skillDir, "SKILL.md"),
85
+ mcpConfigured: mcpInstalled,
86
+ mcpConfigPath: configPath,
87
+ };
88
+ },
89
+ };
@@ -0,0 +1,12 @@
1
+ import { claudeProvider } from "./claude.ts";
2
+ import type { ProviderInstaller } from "./types.ts";
3
+
4
+ const providers: Record<string, ProviderInstaller> = {
5
+ claude: claudeProvider,
6
+ };
7
+
8
+ export function getProvider(name: string): ProviderInstaller | undefined {
9
+ return providers[name];
10
+ }
11
+
12
+ export const availableProviders = Object.values(providers);
@@ -0,0 +1,16 @@
1
+ export type InstallStatus = "installed" | "already_installed" | "updated";
2
+
3
+ export interface InstallResult {
4
+ status: InstallStatus;
5
+ skillPath: string;
6
+ mcpConfigured: boolean;
7
+ mcpConfigPath: string | null;
8
+ }
9
+
10
+ export interface ProviderInstaller {
11
+ name: string;
12
+ displayName: string;
13
+ available: boolean;
14
+ installGlobal(skillContent: string, dryRun: boolean): Promise<InstallResult>;
15
+ installProject(skillContent: string, projectDir: string, dryRun: boolean): Promise<InstallResult>;
16
+ }
@@ -113,28 +113,30 @@ export function formSemanticAssociations(
113
113
  storage: EngramStorage,
114
114
  memory: Memory,
115
115
  now?: number,
116
+ preloadedMemories?: Memory[],
116
117
  ): Association[] {
117
118
  const currentTime = now ?? Date.now();
118
119
  const keywords = extractKeywords(memory.content);
119
120
  if (keywords.length === 0) return [];
120
121
 
121
- const allMemories = storage.getAllMemories();
122
+ const candidates = preloadedMemories ?? storage.getAllMemories();
123
+ const existing = storage.getAssociations(memory.id);
124
+ const linkedSet = new Set(
125
+ existing.flatMap((a) => [
126
+ `${a.sourceId}:${a.targetId}`,
127
+ `${a.targetId}:${a.sourceId}`,
128
+ ]),
129
+ );
122
130
  const formed: Association[] = [];
123
131
 
124
- for (const other of allMemories) {
132
+ for (const other of candidates) {
125
133
  if (other.id === memory.id) continue;
126
134
 
127
135
  const otherKeywords = extractKeywords(other.content);
128
136
  const overlap = keywords.filter((k) => otherKeywords.includes(k));
129
137
 
130
138
  if (overlap.length > 0) {
131
- const existing = storage.getAssociations(memory.id);
132
- const alreadyLinked = existing.some(
133
- (a) =>
134
- (a.sourceId === memory.id && a.targetId === other.id) ||
135
- (a.sourceId === other.id && a.targetId === memory.id),
136
- );
137
- if (alreadyLinked) continue;
139
+ if (linkedSet.has(`${memory.id}:${other.id}`)) continue;
138
140
 
139
141
  const strength = overlap.length / Math.max(keywords.length, otherKeywords.length);
140
142
  const assoc = formAssociation(
@@ -146,6 +148,8 @@ export function formSemanticAssociations(
146
148
  currentTime,
147
149
  );
148
150
  formed.push(assoc);
151
+ linkedSet.add(`${memory.id}:${other.id}`);
152
+ linkedSet.add(`${other.id}:${memory.id}`);
149
153
  }
150
154
  }
151
155
 
@@ -156,26 +160,26 @@ export function formEmotionalAssociations(
156
160
  storage: EngramStorage,
157
161
  memory: Memory,
158
162
  now?: number,
163
+ preloadedMemories?: Memory[],
159
164
  ): Association[] {
160
165
  if (memory.emotion === "neutral" || memory.emotionWeight <= 0.3) return [];
161
166
 
162
167
  const currentTime = now ?? Date.now();
163
- const allMemories = storage.getAllMemories();
168
+ const candidates = preloadedMemories ?? storage.getAllMemories();
169
+ const existing = storage.getAssociations(memory.id);
170
+ const emotionalLinks = new Set(
171
+ existing
172
+ .filter((a) => a.type === "emotional")
173
+ .flatMap((a) => [`${a.sourceId}:${a.targetId}`, `${a.targetId}:${a.sourceId}`]),
174
+ );
164
175
  const formed: Association[] = [];
165
176
  const memoryTier = AROUSAL_TIERS[memory.emotion];
166
177
 
167
- for (const other of allMemories) {
178
+ for (const other of candidates) {
168
179
  if (other.id === memory.id) continue;
169
180
  if (other.emotion === "neutral" || other.emotionWeight <= 0.3) continue;
170
181
 
171
- const existing = storage.getAssociations(memory.id);
172
- const alreadyLinked = existing.some(
173
- (a) =>
174
- a.type === "emotional" &&
175
- ((a.sourceId === memory.id && a.targetId === other.id) ||
176
- (a.sourceId === other.id && a.targetId === memory.id)),
177
- );
178
- if (alreadyLinked) continue;
182
+ if (emotionalLinks.has(`${memory.id}:${other.id}`)) continue;
179
183
 
180
184
  let strength: number;
181
185
  if (memory.emotion === other.emotion) {
@@ -190,6 +194,8 @@ export function formEmotionalAssociations(
190
194
 
191
195
  const assoc = formAssociation(storage, memory.id, other.id, "emotional", strength, currentTime);
192
196
  formed.push(assoc);
197
+ emotionalLinks.add(`${memory.id}:${other.id}`);
198
+ emotionalLinks.add(`${other.id}:${memory.id}`);
193
199
  }
194
200
 
195
201
  return formed;
@@ -69,8 +69,8 @@ export function consolidate(
69
69
  const remainingMemories = storage.getAllMemories();
70
70
  for (const memory of remainingMemories) {
71
71
  const temporalAssocs = formTemporalAssociations(storage, memory, config, currentTime);
72
- const semanticAssocs = formSemanticAssociations(storage, memory, currentTime);
73
- const emotionalAssocs = formEmotionalAssociations(storage, memory, currentTime);
72
+ const semanticAssocs = formSemanticAssociations(storage, memory, currentTime, remainingMemories);
73
+ const emotionalAssocs = formEmotionalAssociations(storage, memory, currentTime, remainingMemories);
74
74
  const causalAssocs = formCausalAssociations(storage, memory, config, currentTime);
75
75
 
76
76
  for (const assoc of [...temporalAssocs, ...semanticAssocs, ...emotionalAssocs, ...causalAssocs]) {
@@ -35,17 +35,12 @@ export function recall(
35
35
  for (const m of contextMatches) seedIds.add(m.id);
36
36
  }
37
37
 
38
- const allCandidates = options?.type
39
- ? storage.getAllMemories(options.type)
40
- : storage.getAllMemories();
41
-
42
- let filtered = allCandidates;
43
- if (options?.context) {
44
- filtered = filtered.filter((m) => m.context?.startsWith(options.context!));
45
- }
46
-
47
- const sorted = filtered.sort((a, b) => b.activation - a.activation);
48
- for (const m of sorted.slice(0, limit)) {
38
+ const topByActivation = storage.getTopMemoriesByActivation(
39
+ limit,
40
+ options?.type,
41
+ options?.context,
42
+ );
43
+ for (const m of topByActivation) {
49
44
  seedIds.add(m.id);
50
45
  }
51
46
 
@@ -153,6 +153,20 @@ export class EngramStorage {
153
153
  .all();
154
154
  }
155
155
 
156
+ getTopMemoriesByActivation(limit: number, type?: MemoryType, contextPrefix?: string): Memory[] {
157
+ const conditions = [];
158
+ if (type) conditions.push(eq(memories.type, type));
159
+ if (contextPrefix) conditions.push(sql`${memories.context} LIKE ${contextPrefix + "%"}`);
160
+
161
+ return this.db
162
+ .select()
163
+ .from(memories)
164
+ .where(conditions.length ? and(...conditions) : undefined)
165
+ .orderBy(desc(memories.activation))
166
+ .limit(limit)
167
+ .all();
168
+ }
169
+
156
170
  getMemoriesBelowActivation(threshold: number): Memory[] {
157
171
  return this.db
158
172
  .select()