@memstack/mcp 0.7.3 → 0.8.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 +57 -7
- package/dist/cli.js +491 -37
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +22 -2
- package/dist/index.js +387 -35
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
package/dist/cli.js
CHANGED
|
@@ -1,63 +1,213 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'util';
|
|
3
3
|
import { createServer as createServer$1 } from 'http';
|
|
4
|
-
import { MemStack, AnthropicLLMAdapter, OpenAILLMAdapter, OpenAIEmbeddingAdapter, RedisStorageAdapter, SQLiteStorageAdapter, PostgresStorageAdapter, DiskStorageAdapter, MarkdownStorageAdapter
|
|
5
|
-
import { readFileSync } from 'fs';
|
|
4
|
+
import { MemStack, InMemoryStorageAdapter, HarnessMemory, defaultRecallNamespaces, projectNamespace, GLOBAL_NAMESPACE, MemStackError, AnthropicLLMAdapter, OpenAILLMAdapter, OpenAIEmbeddingAdapter, RedisStorageAdapter, SQLiteStorageAdapter, PostgresStorageAdapter, DiskStorageAdapter, MarkdownStorageAdapter } from '@memstack/core';
|
|
5
|
+
import { readFileSync, realpathSync, existsSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { resolve, join, dirname } from 'path';
|
|
8
|
+
import { execFileSync } from 'child_process';
|
|
9
|
+
import { createHash } from 'crypto';
|
|
6
10
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
11
|
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
12
|
+
import { z } from 'zod';
|
|
8
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
14
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
var STORAGE_TYPES = ["memory", "markdown", "disk", "postgres", "sqlite", "redis"];
|
|
17
|
+
function memstackHome() {
|
|
18
|
+
return process.env.MEMSTACK_HOME ?? join(homedir(), ".memstack");
|
|
19
|
+
}
|
|
20
|
+
function configFilePath(home = memstackHome()) {
|
|
21
|
+
return join(home, "config.json");
|
|
22
|
+
}
|
|
23
|
+
function readConfigFile(path = configFilePath()) {
|
|
24
|
+
if (!existsSync(path)) return void 0;
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new Error(`${path} is not valid JSON: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
return validateConfigFile(parsed, path);
|
|
32
|
+
}
|
|
33
|
+
function validateConfigFile(value, path) {
|
|
34
|
+
const fail = (message) => {
|
|
35
|
+
throw new Error(`Invalid ${path}: ${message}`);
|
|
36
|
+
};
|
|
37
|
+
if (!isObject(value)) fail("expected a JSON object");
|
|
38
|
+
const config = value;
|
|
39
|
+
if (config.version !== 1) fail(`unsupported version ${JSON.stringify(config.version)}, expected 1`);
|
|
40
|
+
if (config.llm !== void 0) {
|
|
41
|
+
if (!isObject(config.llm)) fail("llm must be an object");
|
|
42
|
+
const llm = config.llm;
|
|
43
|
+
if (llm.provider !== "openai-compatible" && llm.provider !== "anthropic") {
|
|
44
|
+
fail('llm.provider must be "openai-compatible" or "anthropic"');
|
|
45
|
+
}
|
|
46
|
+
if (typeof llm.apiKey !== "string" || !llm.apiKey) fail("llm.apiKey must be a non-empty string");
|
|
47
|
+
for (const key of ["baseURL", "model"]) {
|
|
48
|
+
if (llm[key] !== void 0 && typeof llm[key] !== "string") fail(`llm.${key} must be a string`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (config.storage !== void 0) {
|
|
52
|
+
if (!isObject(config.storage)) fail("storage must be an object");
|
|
53
|
+
const storage = config.storage;
|
|
54
|
+
if (!STORAGE_TYPES.includes(storage.type)) {
|
|
55
|
+
fail(`storage.type must be one of ${STORAGE_TYPES.join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
for (const key of ["path", "url"]) {
|
|
58
|
+
if (storage[key] !== void 0 && typeof storage[key] !== "string") fail(`storage.${key} must be a string`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return config;
|
|
62
|
+
}
|
|
63
|
+
function isObject(value) {
|
|
64
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
65
|
+
}
|
|
66
|
+
var PIN_FILE = ".memstack.json";
|
|
67
|
+
var PIN_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
68
|
+
function resolveProject(cwd = process.cwd()) {
|
|
69
|
+
const dir = realpathSync(resolve(cwd));
|
|
70
|
+
const root = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
71
|
+
const base = root ?? dir;
|
|
72
|
+
const pinned = readPin(base);
|
|
73
|
+
const fallbacks = transitionalIds(dir, root);
|
|
74
|
+
if (pinned) return { id: pinned, source: "pin", key: pinned, root: base, previousIds: [] };
|
|
75
|
+
if (!root) return { ...identity("directory", dir), root: dir, previousIds: [] };
|
|
76
|
+
const shallow = git(root, ["rev-parse", "--is-shallow-repository"]) === "true";
|
|
77
|
+
const rootCommit = shallow ? null : firstCommit(root);
|
|
78
|
+
if (rootCommit) return { ...identity("root-commit", rootCommit), root, previousIds: fallbacks };
|
|
79
|
+
const remote = git(root, ["config", "--get", "remote.origin.url"]);
|
|
80
|
+
const normalized = shallow && remote ? normalizeRemote(remote) : null;
|
|
81
|
+
if (normalized) return { ...identity("remote", normalized), root, previousIds: fallbacks };
|
|
82
|
+
const gitId = identity("git", gitRepoKey(root));
|
|
83
|
+
return { ...gitId, root, previousIds: fallbacks.filter((id) => id !== gitId.id) };
|
|
84
|
+
}
|
|
85
|
+
function normalizeRemote(url) {
|
|
86
|
+
const value = url.trim();
|
|
87
|
+
if (!value) return null;
|
|
88
|
+
let host;
|
|
89
|
+
let path;
|
|
90
|
+
const scp = value.match(/^(?:[^@/]+@)?([^:/]+):(?!\/)(.+)$/);
|
|
91
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = new URL(value);
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (parsed.protocol === "file:" || !parsed.hostname) return null;
|
|
99
|
+
host = parsed.hostname;
|
|
100
|
+
path = decodeURIComponent(parsed.pathname);
|
|
101
|
+
} else if (scp) {
|
|
102
|
+
host = scp[1];
|
|
103
|
+
path = scp[2];
|
|
104
|
+
} else {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
path = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
|
|
108
|
+
if (!path) return null;
|
|
109
|
+
return `${host.toLowerCase()}/${path}`;
|
|
110
|
+
}
|
|
111
|
+
function readPin(base) {
|
|
112
|
+
const path = join(base, PIN_FILE);
|
|
113
|
+
if (!existsSync(path)) return null;
|
|
114
|
+
let project;
|
|
115
|
+
try {
|
|
116
|
+
project = JSON.parse(readFileSync(path, "utf8")).project;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new Error(`${path} is not valid JSON: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
if (project === void 0) return null;
|
|
121
|
+
if (typeof project !== "string" || !PIN_PATTERN.test(project)) {
|
|
122
|
+
throw new Error(`${path}: "project" must use letters, digits, '.', '_', or '-' and be at most 64 characters.`);
|
|
123
|
+
}
|
|
124
|
+
return project;
|
|
125
|
+
}
|
|
126
|
+
function firstCommit(root) {
|
|
127
|
+
const out = git(root, ["rev-list", "--max-parents=0", "--first-parent", "HEAD"]);
|
|
128
|
+
return out ? out.split("\n").pop().trim() : null;
|
|
129
|
+
}
|
|
130
|
+
function gitRepoKey(root) {
|
|
131
|
+
const commonDir = git(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
132
|
+
return commonDir ? dirname(realpathSync(commonDir)) : root;
|
|
133
|
+
}
|
|
134
|
+
function transitionalIds(dir, root) {
|
|
135
|
+
const ids = /* @__PURE__ */ new Set();
|
|
136
|
+
if (root) ids.add(identity("git", gitRepoKey(root)).id);
|
|
137
|
+
ids.add(identity("directory", dir).id);
|
|
138
|
+
return [...ids];
|
|
139
|
+
}
|
|
140
|
+
function identity(source, key) {
|
|
141
|
+
const id = createHash("sha256").update(`${source}:${key}`).digest("hex").slice(0, 16);
|
|
142
|
+
return { id, source, key };
|
|
143
|
+
}
|
|
144
|
+
function git(cwd, args) {
|
|
145
|
+
try {
|
|
146
|
+
const out = execFileSync("git", ["-C", cwd, ...args], {
|
|
147
|
+
encoding: "utf8",
|
|
148
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
149
|
+
timeout: 5e3
|
|
150
|
+
}).trim();
|
|
151
|
+
return out || null;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function isMissingModule(error) {
|
|
157
|
+
const code = error?.code;
|
|
158
|
+
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
|
|
159
|
+
}
|
|
160
|
+
function buildLLMAdapter(env) {
|
|
161
|
+
if (!env.ANTHROPIC_API_KEY && !env.OPENAI_API_KEY) {
|
|
13
162
|
throw new Error("At least one of OPENAI_API_KEY or ANTHROPIC_API_KEY must be set");
|
|
14
163
|
}
|
|
15
|
-
if (
|
|
16
|
-
return new AnthropicLLMAdapter({ apiKey:
|
|
164
|
+
if (env.ANTHROPIC_API_KEY) {
|
|
165
|
+
return new AnthropicLLMAdapter({ apiKey: env.ANTHROPIC_API_KEY, defaultModel: env.MEMSTACK_LLM_MODEL });
|
|
17
166
|
}
|
|
18
167
|
return new OpenAILLMAdapter({
|
|
19
|
-
apiKey:
|
|
20
|
-
baseURL:
|
|
21
|
-
defaultModel:
|
|
168
|
+
apiKey: env.OPENAI_API_KEY ?? "",
|
|
169
|
+
baseURL: env.MEMSTACK_OPENAI_BASE_URL,
|
|
170
|
+
defaultModel: env.MEMSTACK_LLM_MODEL
|
|
22
171
|
});
|
|
23
172
|
}
|
|
24
|
-
function buildEmbeddingAdapter() {
|
|
25
|
-
if (
|
|
26
|
-
if (
|
|
27
|
-
return new OpenAIEmbeddingAdapter({ apiKey:
|
|
173
|
+
function buildEmbeddingAdapter(env) {
|
|
174
|
+
if (env.MEMSTACK_OPENAI_BASE_URL) return void 0;
|
|
175
|
+
if (env.OPENAI_API_KEY) {
|
|
176
|
+
return new OpenAIEmbeddingAdapter({ apiKey: env.OPENAI_API_KEY });
|
|
28
177
|
}
|
|
29
178
|
return void 0;
|
|
30
179
|
}
|
|
31
|
-
async function buildStorageAdapter() {
|
|
32
|
-
const storage =
|
|
180
|
+
async function buildStorageAdapter(env) {
|
|
181
|
+
const storage = env.MEMSTACK_STORAGE ?? "memory";
|
|
33
182
|
switch (storage) {
|
|
34
183
|
case "memory":
|
|
35
184
|
return new InMemoryStorageAdapter();
|
|
36
185
|
case "markdown":
|
|
37
186
|
return new MarkdownStorageAdapter({
|
|
38
|
-
dir:
|
|
187
|
+
dir: env.MEMSTACK_DIR ?? "./memories"
|
|
39
188
|
});
|
|
40
189
|
case "disk":
|
|
41
190
|
return new DiskStorageAdapter({
|
|
42
|
-
storageDir:
|
|
191
|
+
storageDir: env.MEMSTACK_DIR ?? "./memstack-data"
|
|
43
192
|
});
|
|
44
193
|
case "postgres":
|
|
45
|
-
if (!
|
|
194
|
+
if (!env.DATABASE_URL) {
|
|
46
195
|
throw new Error("postgres storage requires DATABASE_URL");
|
|
47
196
|
}
|
|
48
197
|
return new PostgresStorageAdapter({
|
|
49
|
-
connectionString:
|
|
198
|
+
connectionString: env.DATABASE_URL
|
|
50
199
|
});
|
|
51
200
|
case "sqlite": {
|
|
52
201
|
try {
|
|
53
202
|
const BetterSqlite3 = await import('better-sqlite3');
|
|
54
203
|
const db = new BetterSqlite3.default(
|
|
55
|
-
|
|
204
|
+
env.SQLITE_PATH ?? "./memstack.db"
|
|
56
205
|
);
|
|
57
206
|
return new SQLiteStorageAdapter({ db });
|
|
58
|
-
} catch {
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (!isMissingModule(error)) throw error;
|
|
59
209
|
throw new Error(
|
|
60
|
-
"SQLite requires better-sqlite3. Install: npm install better-sqlite3"
|
|
210
|
+
"SQLite requires better-sqlite3. Install: npm install better-sqlite3@^11.10.0"
|
|
61
211
|
);
|
|
62
212
|
}
|
|
63
213
|
}
|
|
@@ -65,12 +215,13 @@ async function buildStorageAdapter() {
|
|
|
65
215
|
try {
|
|
66
216
|
const Redis = await import('ioredis');
|
|
67
217
|
const client = new Redis.default(
|
|
68
|
-
|
|
218
|
+
env.REDIS_URL ?? "redis://localhost:6379"
|
|
69
219
|
);
|
|
70
220
|
return new RedisStorageAdapter({ redis: client });
|
|
71
|
-
} catch {
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (!isMissingModule(error)) throw error;
|
|
72
223
|
throw new Error(
|
|
73
|
-
"Redis requires ioredis. Install: npm install ioredis"
|
|
224
|
+
"Redis requires ioredis. Install: npm install ioredis@^5.11.1"
|
|
74
225
|
);
|
|
75
226
|
}
|
|
76
227
|
}
|
|
@@ -79,11 +230,39 @@ async function buildStorageAdapter() {
|
|
|
79
230
|
}
|
|
80
231
|
}
|
|
81
232
|
async function loadConfigFromEnv() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
233
|
+
return buildConfig(process.env);
|
|
234
|
+
}
|
|
235
|
+
async function loadConfig(options = {}) {
|
|
236
|
+
return buildConfig(mergeConfigFile(readConfigFile(options.configPath), process.env));
|
|
237
|
+
}
|
|
238
|
+
function mergeConfigFile(file, env) {
|
|
239
|
+
const merged = { ...env };
|
|
240
|
+
const envHasLLM = LLM_ENV.some((key) => env[key] !== void 0);
|
|
241
|
+
if (file?.llm && !envHasLLM) {
|
|
242
|
+
if (file.llm.provider === "anthropic") merged.ANTHROPIC_API_KEY = file.llm.apiKey;
|
|
243
|
+
else {
|
|
244
|
+
merged.OPENAI_API_KEY = file.llm.apiKey;
|
|
245
|
+
merged.MEMSTACK_OPENAI_BASE_URL = file.llm.baseURL;
|
|
246
|
+
}
|
|
247
|
+
merged.MEMSTACK_LLM_MODEL = file.llm.model;
|
|
248
|
+
}
|
|
249
|
+
if (file?.storage && env.MEMSTACK_STORAGE === void 0) {
|
|
250
|
+
const { type, path, url } = file.storage;
|
|
251
|
+
merged.MEMSTACK_STORAGE = type;
|
|
252
|
+
if (type === "sqlite") merged.SQLITE_PATH = path;
|
|
253
|
+
if (type === "disk" || type === "markdown") merged.MEMSTACK_DIR = path;
|
|
254
|
+
if (type === "postgres") merged.DATABASE_URL = url;
|
|
255
|
+
if (type === "redis") merged.REDIS_URL = url;
|
|
256
|
+
}
|
|
257
|
+
return merged;
|
|
258
|
+
}
|
|
259
|
+
var LLM_ENV = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "MEMSTACK_OPENAI_BASE_URL", "MEMSTACK_LLM_MODEL"];
|
|
260
|
+
async function buildConfig(env) {
|
|
261
|
+
const llm = buildLLMAdapter(env);
|
|
262
|
+
const embedding = buildEmbeddingAdapter(env);
|
|
263
|
+
const storage = await buildStorageAdapter(env);
|
|
264
|
+
const embedOnStore = env.MEMSTACK_EMBED_ON_STORE !== "false";
|
|
265
|
+
const defaultActorId = env.MEMSTACK_ACTOR ?? "default";
|
|
87
266
|
return {
|
|
88
267
|
config: { llm, embedding, storage, defaults: { embedOnStore } },
|
|
89
268
|
defaultActorId
|
|
@@ -91,9 +270,12 @@ async function loadConfigFromEnv() {
|
|
|
91
270
|
}
|
|
92
271
|
|
|
93
272
|
// src/config.ts
|
|
94
|
-
async function
|
|
273
|
+
async function loadConfig2() {
|
|
95
274
|
return loadConfigFromEnv();
|
|
96
275
|
}
|
|
276
|
+
async function loadHarnessConfig() {
|
|
277
|
+
return loadConfig();
|
|
278
|
+
}
|
|
97
279
|
var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
98
280
|
var TOOLS = [
|
|
99
281
|
{
|
|
@@ -106,7 +288,7 @@ var TOOLS = [
|
|
|
106
288
|
actorId: { type: "string", description: "Actor ID. Defaults to the current session actor." },
|
|
107
289
|
importance: { type: "number", description: "Importance score (0.0-1.0). Auto-scored if omitted." },
|
|
108
290
|
tags: { type: "array", items: { type: "string" }, description: "Tags for categorization. Auto-extracted if omitted." },
|
|
109
|
-
memoryType: { type: "string", description: "Memory type (interaction, summary, observation, fact, reflection). Default: interaction." },
|
|
291
|
+
memoryType: { type: "string", description: "Memory type (interaction, summary, observation, fact, reflection, preference, decision, instruction). Default: interaction." },
|
|
110
292
|
metadata: { type: "object", description: "Additional metadata key-value pairs" }
|
|
111
293
|
},
|
|
112
294
|
required: ["content"]
|
|
@@ -122,7 +304,7 @@ var TOOLS = [
|
|
|
122
304
|
actorId: { type: "string", description: "Actor ID. Defaults to the current session actor." },
|
|
123
305
|
importance: { type: "number", description: "Importance score 0.0-1.0. Default: 0.5." },
|
|
124
306
|
tags: { type: "array", items: { type: "string" }, description: "Tags for categorization" },
|
|
125
|
-
memoryType: { type: "string", description: "Memory type (interaction, summary, observation, fact, reflection). Default: interaction." },
|
|
307
|
+
memoryType: { type: "string", description: "Memory type (interaction, summary, observation, fact, reflection, preference, decision, instruction). Default: interaction." },
|
|
126
308
|
metadata: { type: "object", description: "Additional metadata key-value pairs" }
|
|
127
309
|
},
|
|
128
310
|
required: ["content"]
|
|
@@ -144,7 +326,7 @@ var TOOLS = [
|
|
|
144
326
|
actorId: { type: "string", description: "Actor ID. Defaults to the current session actor." },
|
|
145
327
|
importance: { type: "number", description: "Importance score 0.0-1.0. Default: 0.5." },
|
|
146
328
|
tags: { type: "array", items: { type: "string" }, description: "Tags for categorization" },
|
|
147
|
-
memoryType: { type: "string", description: "Memory type (interaction, summary, observation, fact, reflection). Default: interaction." },
|
|
329
|
+
memoryType: { type: "string", description: "Memory type (interaction, summary, observation, fact, reflection, preference, decision, instruction). Default: interaction." },
|
|
148
330
|
metadata: { type: "object", description: "Additional metadata key-value pairs" }
|
|
149
331
|
},
|
|
150
332
|
required: ["content"]
|
|
@@ -714,21 +896,293 @@ function createServer({
|
|
|
714
896
|
});
|
|
715
897
|
return server;
|
|
716
898
|
}
|
|
899
|
+
var pkg2 = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
900
|
+
var KINDS = [
|
|
901
|
+
"fact",
|
|
902
|
+
"preference",
|
|
903
|
+
"decision",
|
|
904
|
+
"instruction",
|
|
905
|
+
"observation",
|
|
906
|
+
"interaction",
|
|
907
|
+
"summary",
|
|
908
|
+
"reflection"
|
|
909
|
+
];
|
|
910
|
+
var HARNESS_INSTRUCTIONS = [
|
|
911
|
+
"MemStack is this project's persistent memory, shared with other coding agents.",
|
|
912
|
+
"When the user asks you to remember something, call memory_store. Do not write it into README, AGENTS.md, or other project files unless the user asks for a file change.",
|
|
913
|
+
"Also call memory_store when you learn a durable fact, decision, preference, or rule.",
|
|
914
|
+
"Call memory_retrieve at the start of a task and before decisions that depend on project history, conventions, or preferences.",
|
|
915
|
+
"Never store secrets, credentials, or personal data.",
|
|
916
|
+
"",
|
|
917
|
+
'Store one self-contained statement per memory, e.g. "This project uses Hono for the API". Memories are project-scoped; use scope "global" only for preferences that apply to every project. memory_retrieve takes natural-language questions. Cite memory IDs when you rely on them, and delete memories that turn out to be wrong with memory_delete.'
|
|
918
|
+
].join("\n");
|
|
919
|
+
var StoreArgs = z.object({
|
|
920
|
+
content: z.string().trim().min(1).max(8e3).describe("One self-contained statement to remember."),
|
|
921
|
+
kind: z.enum(KINDS).default("fact").describe("What kind of memory this is."),
|
|
922
|
+
importance: z.number().min(0).max(1).optional().describe("0 to 1. Default 0.5; use 0.8+ for rules that must not be missed."),
|
|
923
|
+
tags: z.array(z.string().trim().min(1).max(40)).max(10).optional().describe("Optional topic tags. MemStack adds its own."),
|
|
924
|
+
scope: z.enum(["project", "global"]).default("project").describe('"global" only for preferences that apply to every project.')
|
|
925
|
+
}).strict();
|
|
926
|
+
var RetrieveArgs = z.object({
|
|
927
|
+
query: z.string().max(1e3).optional().describe("Natural-language question or keywords. Omit to get the most important memories."),
|
|
928
|
+
limit: z.number().int().min(1).max(25).default(10).describe("Maximum memories to return."),
|
|
929
|
+
kinds: z.array(z.enum(KINDS)).optional().describe("Only return these kinds.")
|
|
930
|
+
}).strict();
|
|
931
|
+
var IdArgs = z.object({ id: z.string().min(1).max(200) }).strict();
|
|
932
|
+
var NoArgs = z.object({}).strict();
|
|
933
|
+
var TOOLS2 = [
|
|
934
|
+
{
|
|
935
|
+
name: "memory_store",
|
|
936
|
+
description: "Save to project memory. Use this whenever the user asks you to remember something, instead of editing project files, and for durable facts, decisions, preferences, or rules. Later sessions and other agents can recall it.",
|
|
937
|
+
schema: StoreArgs
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
name: "memory_retrieve",
|
|
941
|
+
description: "Recall memories for this project (and global preferences) relevant to a question. Fast and local; call it freely.",
|
|
942
|
+
schema: RetrieveArgs
|
|
943
|
+
},
|
|
944
|
+
{ name: "memory_get", description: "Get one memory by ID.", schema: IdArgs },
|
|
945
|
+
{ name: "memory_delete", description: "Delete a memory that is wrong or outdated, by ID.", schema: IdArgs },
|
|
946
|
+
{ name: "memory_stats", description: "Show the current project and how many memories it has.", schema: NoArgs }
|
|
947
|
+
];
|
|
948
|
+
var RECALL_MAX_CHARS = 8e3;
|
|
949
|
+
function createHarnessServer({ memory, projectId, harness, cwd }) {
|
|
950
|
+
const server = new Server(
|
|
951
|
+
{ name: "memstack", version: pkg2.version },
|
|
952
|
+
{ capabilities: { tools: {} }, instructions: HARNESS_INSTRUCTIONS }
|
|
953
|
+
);
|
|
954
|
+
const project = projectNamespace(projectId);
|
|
955
|
+
const namespaces = defaultRecallNamespaces(projectId);
|
|
956
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
957
|
+
tools: TOOLS2.map(({ name, description, schema }) => ({
|
|
958
|
+
name,
|
|
959
|
+
description,
|
|
960
|
+
inputSchema: z.toJSONSchema(schema, { io: "input" })
|
|
961
|
+
}))
|
|
962
|
+
}));
|
|
963
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
964
|
+
const tool = TOOLS2.find((t) => t.name === request.params.name);
|
|
965
|
+
if (!tool) return failure(`Unknown tool: ${request.params.name}`);
|
|
966
|
+
const parsed = tool.schema.safeParse(request.params.arguments ?? {});
|
|
967
|
+
if (!parsed.success) {
|
|
968
|
+
return failure(`Invalid arguments for ${tool.name}: ${z.prettifyError(parsed.error)}`);
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
switch (tool.name) {
|
|
972
|
+
case "memory_store": {
|
|
973
|
+
const args = parsed.data;
|
|
974
|
+
const stored = await memory.remember({
|
|
975
|
+
namespace: args.scope === "global" ? GLOBAL_NAMESPACE : project,
|
|
976
|
+
content: args.content,
|
|
977
|
+
kind: args.kind,
|
|
978
|
+
importance: args.importance,
|
|
979
|
+
tags: args.tags,
|
|
980
|
+
source: {
|
|
981
|
+
harness: harness ?? server.getClientVersion()?.name ?? "unknown",
|
|
982
|
+
project: projectId,
|
|
983
|
+
cwd
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
return text(`Stored ${stored.id} (${args.scope}, ${stored.memoryType}). Tags: ${stored.tags.join(", ") || "none"}.`);
|
|
987
|
+
}
|
|
988
|
+
case "memory_retrieve": {
|
|
989
|
+
const args = parsed.data;
|
|
990
|
+
const { hits, fallback } = await memory.recall({
|
|
991
|
+
namespaces,
|
|
992
|
+
query: args.query,
|
|
993
|
+
kinds: args.kinds,
|
|
994
|
+
limit: args.limit,
|
|
995
|
+
maxChars: RECALL_MAX_CHARS
|
|
996
|
+
});
|
|
997
|
+
if (hits.length === 0) return text("No memories yet for this project.");
|
|
998
|
+
const header = fallback && args.query ? `Nothing matched "${args.query}". The most important memories instead:` : `${hits.length} ${hits.length === 1 ? "memory" : "memories"}:`;
|
|
999
|
+
return text([header, ...hits.map(({ memory: m }) => formatMemory(m, projectId))].join("\n"));
|
|
1000
|
+
}
|
|
1001
|
+
case "memory_get": {
|
|
1002
|
+
const { id } = parsed.data;
|
|
1003
|
+
const found = await memory.get(id, namespaces);
|
|
1004
|
+
return found ? text(formatMemory(found, projectId, true)) : failure(`Memory not found in this project: ${id}`);
|
|
1005
|
+
}
|
|
1006
|
+
case "memory_delete": {
|
|
1007
|
+
const { id } = parsed.data;
|
|
1008
|
+
await memory.forget(id, namespaces);
|
|
1009
|
+
return text(`Deleted ${id}.`);
|
|
1010
|
+
}
|
|
1011
|
+
case "memory_stats": {
|
|
1012
|
+
const counts = await memory.stats(namespaces);
|
|
1013
|
+
return text(
|
|
1014
|
+
`Project ${projectId}${cwd ? ` (${cwd})` : ""}: ${counts[project] ?? 0} memories. Global: ${counts[GLOBAL_NAMESPACE] ?? 0}.`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
if (error instanceof MemStackError && error.code === "NOT_FOUND") {
|
|
1020
|
+
return failure(`Memory not found in this project: ${parsed.data.id ?? ""}`);
|
|
1021
|
+
}
|
|
1022
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
return server;
|
|
1026
|
+
}
|
|
1027
|
+
function formatMemory(m, projectId, detailed = false) {
|
|
1028
|
+
const scope = m.actorId === GLOBAL_NAMESPACE ? "global" : "project";
|
|
1029
|
+
const source = m.metadata?.source?.harness;
|
|
1030
|
+
const meta = [m.memoryType, scope, source && `from ${source}`, m.createdAt.toISOString().slice(0, 10)].filter(Boolean);
|
|
1031
|
+
const line = `- [${m.id}] ${m.content} (${meta.join(", ")})`;
|
|
1032
|
+
if (!detailed) return line;
|
|
1033
|
+
return `${line}
|
|
1034
|
+
importance: ${m.importance}
|
|
1035
|
+
tags: ${m.tags.join(", ") || "none"}
|
|
1036
|
+
project: ${scope === "project" ? projectId : "all"}`;
|
|
1037
|
+
}
|
|
1038
|
+
function text(message) {
|
|
1039
|
+
return { content: [{ type: "text", text: message }] };
|
|
1040
|
+
}
|
|
1041
|
+
function failure(message) {
|
|
1042
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
1043
|
+
}
|
|
1044
|
+
var HOOK_MAX_CHARS = 6e3;
|
|
1045
|
+
var HOOK_LIMIT = 15;
|
|
1046
|
+
async function sessionStartContext(input, env = process.env) {
|
|
1047
|
+
const cwd = env.CLAUDE_PROJECT_DIR ?? input.cwd ?? process.cwd();
|
|
1048
|
+
const project = resolveProject(cwd);
|
|
1049
|
+
const { config } = await loadConfig();
|
|
1050
|
+
const storage = config.storage ?? new InMemoryStorageAdapter();
|
|
1051
|
+
try {
|
|
1052
|
+
const memory = new HarnessMemory({ storage, llm: config.llm, autoTags: false });
|
|
1053
|
+
await memory.adoptProjects(project.previousIds, project.id);
|
|
1054
|
+
const { hits } = await memory.recall({
|
|
1055
|
+
namespaces: defaultRecallNamespaces(project.id),
|
|
1056
|
+
limit: HOOK_LIMIT,
|
|
1057
|
+
maxChars: HOOK_MAX_CHARS
|
|
1058
|
+
});
|
|
1059
|
+
if (hits.length === 0) return "";
|
|
1060
|
+
return [
|
|
1061
|
+
`MemStack project memory (project ${project.id}), most important first. Follow these unless the user says otherwise; use memory_retrieve for more and memory_store to save new facts.`,
|
|
1062
|
+
...hits.map(({ memory: m }) => formatLine(m))
|
|
1063
|
+
].join("\n");
|
|
1064
|
+
} finally {
|
|
1065
|
+
await storage.close();
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async function runSessionStartHook() {
|
|
1069
|
+
try {
|
|
1070
|
+
const input = parseInput(await readStdin());
|
|
1071
|
+
const context = await sessionStartContext(input);
|
|
1072
|
+
if (context) process.stdout.write(`${context}
|
|
1073
|
+
`);
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
process.stderr.write(`memstack hook: ${error instanceof Error ? error.message : String(error)}
|
|
1076
|
+
`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function formatLine(m) {
|
|
1080
|
+
const scope = m.actorId === GLOBAL_NAMESPACE ? "global" : "project";
|
|
1081
|
+
return `- [${m.id}] ${m.content} (${m.memoryType}, ${scope})`;
|
|
1082
|
+
}
|
|
1083
|
+
function parseInput(raw) {
|
|
1084
|
+
if (!raw.trim()) return {};
|
|
1085
|
+
try {
|
|
1086
|
+
const parsed = JSON.parse(raw);
|
|
1087
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1088
|
+
} catch {
|
|
1089
|
+
return {};
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function readStdin(timeoutMs = 2e3) {
|
|
1093
|
+
if (process.stdin.isTTY) return Promise.resolve("");
|
|
1094
|
+
return new Promise((resolve2) => {
|
|
1095
|
+
let data = "";
|
|
1096
|
+
const timer = setTimeout(() => resolve2(data), timeoutMs);
|
|
1097
|
+
process.stdin.setEncoding("utf8");
|
|
1098
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
1099
|
+
process.stdin.on("end", () => {
|
|
1100
|
+
clearTimeout(timer);
|
|
1101
|
+
resolve2(data);
|
|
1102
|
+
});
|
|
1103
|
+
process.stdin.on("error", () => {
|
|
1104
|
+
clearTimeout(timer);
|
|
1105
|
+
resolve2(data);
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
717
1109
|
async function main() {
|
|
1110
|
+
if (process.argv[2] === "hook") {
|
|
1111
|
+
if (process.argv[3] !== "session-start") {
|
|
1112
|
+
console.error(`Unknown hook "${process.argv[3] ?? ""}". Supported: session-start`);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
await runSessionStartHook();
|
|
1116
|
+
process.exit(0);
|
|
1117
|
+
}
|
|
718
1118
|
const { values } = parseArgs({
|
|
719
1119
|
options: {
|
|
720
1120
|
http: { type: "boolean", default: false },
|
|
721
|
-
port: { type: "string", default: "3939" }
|
|
1121
|
+
port: { type: "string", default: "3939" },
|
|
1122
|
+
profile: { type: "string", default: "default" },
|
|
1123
|
+
harness: { type: "string" }
|
|
722
1124
|
}
|
|
723
1125
|
});
|
|
724
|
-
|
|
1126
|
+
if (values.profile === "harness") {
|
|
1127
|
+
if (values.http) throw new Error("--profile harness supports stdio only: the project comes from the harness's working directory");
|
|
1128
|
+
await runHarness(values.harness);
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (values.profile !== "default") throw new Error(`Unknown --profile "${values.profile}". Use "default" or "harness".`);
|
|
1132
|
+
const { config, defaultActorId } = await loadConfig2();
|
|
725
1133
|
if (values.http) {
|
|
726
1134
|
await runHttp({ config, defaultActorId, port: Number(values.port) });
|
|
727
1135
|
return;
|
|
728
1136
|
}
|
|
729
|
-
const
|
|
1137
|
+
const ms = new MemStack(config);
|
|
1138
|
+
const server = createServer({ config, defaultActorId, ms });
|
|
1139
|
+
const transport = new StdioServerTransport();
|
|
1140
|
+
await server.connect(transport);
|
|
1141
|
+
exitWhenStdinCloses(transport, () => ms.close());
|
|
1142
|
+
}
|
|
1143
|
+
async function runHarness(harness) {
|
|
1144
|
+
const cwd = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
1145
|
+
const project = resolveProject(cwd);
|
|
1146
|
+
const { config } = await loadHarnessConfig();
|
|
1147
|
+
const storage = config.storage ?? new InMemoryStorageAdapter();
|
|
1148
|
+
if (storage instanceof InMemoryStorageAdapter) {
|
|
1149
|
+
console.error("memstack-mcp: storage is in-memory, so memories are lost when this session ends. Run `memstack init` to choose a store.");
|
|
1150
|
+
}
|
|
1151
|
+
const memory = new HarnessMemory({
|
|
1152
|
+
storage,
|
|
1153
|
+
llm: config.llm,
|
|
1154
|
+
onError: (error, context) => console.error(`memstack-mcp: ${context}: ${error.message}`)
|
|
1155
|
+
});
|
|
1156
|
+
const adopted = await memory.adoptProjects(project.previousIds, project.id);
|
|
1157
|
+
if (adopted > 0) console.error(`memstack-mcp: moved ${adopted} memories from this project's previous ID to ${project.id}.`);
|
|
1158
|
+
const server = createHarnessServer({ memory, projectId: project.id, harness, cwd: project.root });
|
|
730
1159
|
const transport = new StdioServerTransport();
|
|
731
1160
|
await server.connect(transport);
|
|
1161
|
+
exitWhenStdinCloses(transport, () => storage.close());
|
|
1162
|
+
}
|
|
1163
|
+
function exitWhenStdinCloses(transport, close) {
|
|
1164
|
+
let pending = 0;
|
|
1165
|
+
let ended = false;
|
|
1166
|
+
const finish = () => {
|
|
1167
|
+
if (ended && pending === 0) void close().finally(() => process.exit(0));
|
|
1168
|
+
};
|
|
1169
|
+
const onmessage = transport.onmessage;
|
|
1170
|
+
transport.onmessage = (message) => {
|
|
1171
|
+
if ("method" in message && "id" in message) pending++;
|
|
1172
|
+
onmessage?.(message);
|
|
1173
|
+
};
|
|
1174
|
+
const send = transport.send.bind(transport);
|
|
1175
|
+
transport.send = async (message) => {
|
|
1176
|
+
await send(message);
|
|
1177
|
+
if (!("method" in message) && "id" in message) {
|
|
1178
|
+
pending--;
|
|
1179
|
+
finish();
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
process.stdin.once("end", () => {
|
|
1183
|
+
ended = true;
|
|
1184
|
+
finish();
|
|
1185
|
+
});
|
|
732
1186
|
}
|
|
733
1187
|
async function runHttp({ config, defaultActorId, port }) {
|
|
734
1188
|
const ms = new MemStack(config);
|