Skip to content

Commit 402a8a6

Browse files
committed
feat(memory): auto-sync steering index on memory changes
The memory MCP now automatically generates and maintains a steering file (team-memories-index.md) with an index of all active memories. The index is synced on: - Store load (MCP startup) - add_memory - archive_memory - remove_memory Editor detection is automatic — writes to .kiro/steering/ and/or .cursor/rules/ based on which directories exist. Bumps @arvoretech/memory-mcp to 1.3.0
1 parent 8976527 commit 402a8a6

2 files changed

Lines changed: 100 additions & 2 deletions

File tree

packages/memory/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@arvoretech/memory-mcp",
3-
"version": "1.2.0",
3+
"version": "1.3.0",
44
"description": "MCP server for team memory — persistent knowledge base with semantic search for AI-assisted development",
55
"main": "dist/index.js",
66
"type": "module",

packages/memory/src/store.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { readFile, readdir, writeFile, mkdir, rm, stat } from "node:fs/promises";
2-
import { join, basename } from "node:path";
2+
import { join, basename, resolve, dirname } from "node:path";
33
import { existsSync } from "node:fs";
44
import { createHash } from "node:crypto";
55
import * as lancedb from "@lancedb/lancedb";
@@ -15,6 +15,35 @@ import {
1515
MemoryMCPError,
1616
} from "./types.js";
1717

18+
interface SteeringTarget {
19+
dir: string;
20+
buildContent: (body: string) => string;
21+
}
22+
23+
function detectSteeringTargets(workspaceRoot: string): SteeringTarget[] {
24+
const targets: SteeringTarget[] = [];
25+
26+
const kiroSteering = join(workspaceRoot, ".kiro", "steering");
27+
if (existsSync(kiroSteering)) {
28+
targets.push({
29+
dir: kiroSteering,
30+
buildContent: (body) =>
31+
`---\ninclusion: always\nname: team-memories-index\n---\n\n${body}\n`,
32+
});
33+
}
34+
35+
const cursorRules = join(workspaceRoot, ".cursor", "rules");
36+
if (existsSync(cursorRules)) {
37+
targets.push({
38+
dir: cursorRules,
39+
buildContent: (body) =>
40+
`---\ndescription: "Team memories index — active knowledge base"\nalwaysApply: true\n---\n\n${body}\n`,
41+
});
42+
}
43+
44+
return targets;
45+
}
46+
1847
function vectorRecord(entry: MemoryEntry, vector: number[], contentHash: string): Record<string, unknown> {
1948
return {
2049
id: entry.id,
@@ -31,6 +60,7 @@ function vectorRecord(entry: MemoryEntry, vector: number[], contentHash: string)
3160

3261
export class MemoryStore {
3362
private memoriesPath: string;
63+
private workspaceRoot: string;
3464
private catalog: MemoryEntry[] = [];
3565
private embeddings: EmbeddingEngine;
3666
private db: lancedb.Connection | null = null;
@@ -40,9 +70,19 @@ export class MemoryStore {
4070

4171
constructor(memoriesPath: string, embeddingModel?: string) {
4272
this.memoriesPath = memoriesPath;
73+
this.workspaceRoot = this.resolveWorkspaceRoot(memoriesPath);
4374
this.embeddings = new EmbeddingEngine(embeddingModel);
4475
}
4576

77+
private resolveWorkspaceRoot(memoriesPath: string): string {
78+
const abs = resolve(memoriesPath);
79+
const parent = dirname(abs);
80+
if (existsSync(join(parent, ".kiro")) || existsSync(join(parent, ".cursor")) || existsSync(join(parent, ".git"))) {
81+
return parent;
82+
}
83+
return parent;
84+
}
85+
4686
async load(): Promise<void> {
4787
this.loadingPromise = this.doLoad();
4888
return this.loadingPromise;
@@ -93,6 +133,58 @@ export class MemoryStore {
93133
}
94134

95135
this.loaded = true;
136+
137+
await this.syncSteeringIndex();
138+
}
139+
140+
async syncSteeringIndex(): Promise<void> {
141+
try {
142+
const targets = detectSteeringTargets(this.workspaceRoot);
143+
if (targets.length === 0) return;
144+
145+
const active = this.catalog.filter((m) => m.status === "active");
146+
const grouped = new Map<string, MemoryEntry[]>();
147+
148+
for (const entry of active) {
149+
const list = grouped.get(entry.category) || [];
150+
list.push(entry);
151+
grouped.set(entry.category, list);
152+
}
153+
154+
const lines: string[] = [
155+
"# Team Memories Index",
156+
"",
157+
`Total: ${active.length} active memories. Use \`get_memory(id)\` to read full content.`,
158+
"",
159+
];
160+
161+
for (const category of VALID_CATEGORIES) {
162+
const entries = grouped.get(category);
163+
if (!entries || entries.length === 0) continue;
164+
165+
lines.push(`## ${category} (${entries.length})`);
166+
lines.push("");
167+
168+
for (const entry of entries) {
169+
const tags = entry.tags.length > 0 ? ` [${entry.tags.join(", ")}]` : "";
170+
lines.push(`- **${entry.title}**${tags} → \`${entry.id}\``);
171+
}
172+
173+
lines.push("");
174+
}
175+
176+
const body = lines.join("\n");
177+
178+
for (const target of targets) {
179+
const filePath = join(target.dir, "team-memories-index.md");
180+
const content = target.buildContent(body);
181+
await writeFile(filePath, content, "utf-8");
182+
}
183+
184+
console.error(`Synced steering index (${active.length} memories, ${targets.length} target(s))`);
185+
} catch (error) {
186+
console.error(`Failed to sync steering index: ${error instanceof Error ? error.message : error}`);
187+
}
96188
}
97189

98190
private async syncVectorStore(): Promise<void> {
@@ -364,6 +456,8 @@ export class MemoryStore {
364456
}
365457
}
366458

459+
await this.syncSteeringIndex();
460+
367461
return entry;
368462
}
369463

@@ -385,6 +479,8 @@ export class MemoryStore {
385479
await this.table.update({ where: `id = '${id}'`, values: { status: "archived" } });
386480
}
387481

482+
await this.syncSteeringIndex();
483+
388484
return entry;
389485
}
390486

@@ -400,6 +496,8 @@ export class MemoryStore {
400496
if (this.table) {
401497
await this.table.delete(`id = '${id}'`);
402498
}
499+
500+
await this.syncSteeringIndex();
403501
}
404502

405503
private toCatalogEntry(entry: MemoryEntry): MemoryCatalogEntry {

0 commit comments

Comments
 (0)