@ccjr1120/memory-one 0.1.11 → 0.1.13
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 +23 -15
- package/dist/agent/providers.js +113 -0
- package/dist/agent/providers.js.map +1 -0
- package/dist/agent/run.js +98 -0
- package/dist/agent/run.js.map +1 -0
- package/dist/agent/types.js +2 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/api/client.js +26 -0
- package/dist/api/client.js.map +1 -0
- package/dist/app.js +36 -0
- package/dist/app.js.map +1 -0
- package/dist/integrations/codex.js +143 -0
- package/dist/integrations/codex.js.map +1 -0
- package/dist/lib/copy.js +203 -0
- package/dist/lib/copy.js.map +1 -0
- package/dist/lib/format.js +11 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/kinds.js +5 -0
- package/dist/lib/kinds.js.map +1 -0
- package/dist/lib/views.js +31 -0
- package/dist/lib/views.js.map +1 -0
- package/dist/mcp/context-cache.js +14 -0
- package/dist/mcp/context-cache.js.map +1 -0
- package/dist/mcp/server.js +80 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/routes/agent.js +268 -0
- package/dist/routes/agent.js.map +1 -0
- package/dist/routes/data.js +18 -0
- package/dist/routes/data.js.map +1 -0
- package/dist/routes/integrations.js +22 -0
- package/dist/routes/integrations.js.map +1 -0
- package/dist/routes/mcp.js +22 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/memories.js +46 -0
- package/dist/routes/memories.js.map +1 -0
- package/dist/routes/settings.js +36 -0
- package/dist/routes/settings.js.map +1 -0
- package/dist/routes/system.js +25 -0
- package/dist/routes/system.js.map +1 -0
- package/dist/server.js +2 -762
- package/dist/server.js.map +1 -1
- package/dist/storage.js +224 -7
- package/dist/storage.js.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +4 -6
- package/public/assets/{highlighted-body-KPVGNVTW-BQat7IMJ.js → highlighted-body-KPVGNVTW-jxw5D7vG.js} +1 -1
- package/public/assets/index-C3mrwXZF.css +1 -0
- package/public/assets/index-DWgbu5Rx.js +190 -0
- package/public/index.html +2 -2
- package/public/assets/index-Corxd8nB.css +0 -1
- package/public/assets/index-NA-JxEig.js +0 -190
package/dist/server.js
CHANGED
|
@@ -1,765 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fastifyStatic from "@fastify/static";
|
|
3
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import { readFileSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
9
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
11
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
12
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
13
|
-
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
-
import { z } from "zod";
|
|
15
|
-
import { MemoryStore } from "./storage.js";
|
|
16
|
-
const store = new MemoryStore();
|
|
17
|
-
const internalMcpToken = randomUUID();
|
|
18
|
-
let agentTurnQueue = Promise.resolve();
|
|
19
|
-
const json = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }] });
|
|
20
|
-
const storageScope = "user";
|
|
21
|
-
const exposeMemory = (memory) => { if (!memory || typeof memory !== "object" || "error" in memory)
|
|
22
|
-
return memory; const { project, ...rest } = memory; return { ...rest, scope: project ?? "global" }; };
|
|
23
|
-
const exposeMemories = (memories) => memories.map(exposeMemory);
|
|
24
|
-
const toStorageInput = (input) => { const { scope, ...rest } = input; return { ...rest, scope: storageScope, project: typeof scope === "string" && scope !== "global" ? scope : null }; };
|
|
25
|
-
const toStoragePatch = (patch) => { const { scope, ...rest } = patch; return "scope" in patch ? { ...rest, project: typeof scope === "string" && scope !== "global" ? scope : null } : rest; };
|
|
26
|
-
const agentSystemPrompt = "你是 Memory One 的记忆管家,首要职责是结合已读取的相关记忆直接回答用户的问题。你可以主动搜索和读取记忆来提高回答准确性,但不得因为普通对话、提问、纠正回答、顺带提到的偏好或项目细节而新增、更新或删除记忆。只有当用户明确要求‘记住/保存’某项内容、明确要求修改某条记忆,或明确要求‘忘记/删除’某条记忆时,才调用 memory_store、memory_update 或 memory_delete。执行更新或删除前先确认目标唯一,不要编造记忆。回复使用中文,简洁但可以使用 Markdown。";
|
|
27
|
-
const toolLabels = { memory_get_context: "读取相关上下文", memory_search: "搜索记忆", memory_get: "读取记忆", memory_list: "列出记忆", memory_store: "保存记忆", memory_update: "更新记忆", memory_delete: "删除记忆", memory_feedback: "记录反馈" };
|
|
28
|
-
const isMemoryOverviewRequest = (message) => /(?:有哪些|所有记忆|全部记忆|列出(?:全部)?|查看(?:全部)?|浏览全部|总结(?:下)?(?:我的)?记忆|总结我的特点|概括我的特点|我的画像|我的偏好和特点|我的记忆(?:有什么)?特点|记忆特点)/.test(message);
|
|
29
|
-
function sseEvent(type, payload, id) { return `${id === undefined ? "" : `id: ${id}\n`}event: ${type}\ndata: ${JSON.stringify(payload)}\n\n`; }
|
|
30
|
-
async function readSse(response, onEvent) {
|
|
31
|
-
if (!response.body)
|
|
32
|
-
throw new Error("provider_empty_stream");
|
|
33
|
-
const reader = response.body.getReader();
|
|
34
|
-
const decoder = new TextDecoder();
|
|
35
|
-
let buffer = "";
|
|
36
|
-
while (true) {
|
|
37
|
-
const { value, done } = await reader.read();
|
|
38
|
-
buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });
|
|
39
|
-
const chunks = buffer.split(/\r?\n\r?\n/);
|
|
40
|
-
buffer = chunks.pop() ?? "";
|
|
41
|
-
for (const chunk of chunks) {
|
|
42
|
-
const lines = chunk.split(/\r?\n/);
|
|
43
|
-
const event = lines.find((line) => line.startsWith("event:"))?.slice(6).trim() ?? "message";
|
|
44
|
-
const data = lines.filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
|
|
45
|
-
if (data && await onEvent({ event, data }) === false) {
|
|
46
|
-
await reader.cancel().catch(() => undefined);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (done)
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
async function connectMemoryMcp() {
|
|
55
|
-
const client = new Client({ name: "memory-one-agent", version: "0.1.0" });
|
|
56
|
-
const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${process.env.MEMORY_PORT ?? 8765}/mcp`), { requestInit: { headers: { "x-memory-one-internal": internalMcpToken } } });
|
|
57
|
-
await client.connect(transport);
|
|
58
|
-
return { client, transport };
|
|
59
|
-
}
|
|
60
|
-
function normalizeTools(tools) {
|
|
61
|
-
return tools.map((tool) => ({ type: "function", function: { name: tool.name, description: tool.description ?? "", parameters: tool.inputSchema } }));
|
|
62
|
-
}
|
|
63
|
-
async function openAiRound(config, messages, tools, emit) {
|
|
64
|
-
const base = (config.base_url?.trim() || "").replace(/\/$/, "");
|
|
65
|
-
const response = await fetch(`${base}/chat/completions`, { method: "POST", headers: { "content-type": "application/json", ...(config.api_key ? { authorization: `Bearer ${config.api_key}` } : {}) }, body: JSON.stringify({ model: config.model || "", messages, tools, stream: true }) });
|
|
66
|
-
if (!response.ok)
|
|
67
|
-
throw new Error(`provider_http_${response.status}: ${await response.text()}`);
|
|
68
|
-
let content = "";
|
|
69
|
-
const calls = new Map();
|
|
70
|
-
await readSse(response, ({ data }) => {
|
|
71
|
-
if (data === "[DONE]")
|
|
72
|
-
return false;
|
|
73
|
-
const chunk = JSON.parse(data);
|
|
74
|
-
const delta = chunk.choices?.[0]?.delta;
|
|
75
|
-
if (delta?.content) {
|
|
76
|
-
content += delta.content;
|
|
77
|
-
emit({ type: "delta", text: delta.content });
|
|
78
|
-
}
|
|
79
|
-
for (const call of delta?.tool_calls ?? []) {
|
|
80
|
-
const existing = calls.get(call.index) ?? { id: call.id ?? "", name: call.function?.name ?? "", arguments: "" };
|
|
81
|
-
existing.id ||= call.id ?? "";
|
|
82
|
-
existing.name ||= call.function?.name ?? "";
|
|
83
|
-
existing.arguments += call.function?.arguments ?? "";
|
|
84
|
-
calls.set(call.index, existing);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
return { content, toolCalls: [...calls.values()] };
|
|
88
|
-
}
|
|
89
|
-
function responsesInput(messages) {
|
|
90
|
-
return messages.filter((message) => message.role !== "system").flatMap((message) => {
|
|
91
|
-
if (message.role === "tool")
|
|
92
|
-
return [{ type: "function_call_output", call_id: message.tool_call_id, output: message.content }];
|
|
93
|
-
if (message.role === "assistant" && Array.isArray(message.tool_calls)) {
|
|
94
|
-
return message.tool_calls.map((call) => ({ type: "function_call", call_id: call.id, name: call.function.name, arguments: call.function.arguments }));
|
|
95
|
-
}
|
|
96
|
-
return [{ role: message.role, content: message.content }];
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
async function openAiResponsesRound(config, messages, tools, emit) {
|
|
100
|
-
const base = (config.base_url?.trim() || "").replace(/\/$/, "");
|
|
101
|
-
const system = messages.find((message) => message.role === "system")?.content;
|
|
102
|
-
const response = await fetch(`${base}/responses`, { method: "POST", headers: { "content-type": "application/json", ...(config.api_key ? { authorization: `Bearer ${config.api_key}` } : {}) }, body: JSON.stringify({ model: config.model || "", instructions: system, input: responsesInput(messages), tools: tools.map((tool) => ({ type: "function", name: tool.function.name, description: tool.function.description, parameters: tool.function.parameters })), stream: false }) });
|
|
103
|
-
if (!response.ok)
|
|
104
|
-
throw new Error(`provider_http_${response.status}: ${await response.text()}`);
|
|
105
|
-
const payload = await response.json();
|
|
106
|
-
let content = "";
|
|
107
|
-
const toolCalls = [];
|
|
108
|
-
for (const item of payload.output ?? []) {
|
|
109
|
-
if (item.type === "message") {
|
|
110
|
-
for (const part of item.content ?? [])
|
|
111
|
-
if (part.type === "output_text" && part.text)
|
|
112
|
-
content += part.text;
|
|
113
|
-
}
|
|
114
|
-
if (item.type === "function_call")
|
|
115
|
-
toolCalls.push({ id: item.call_id || item.id, name: item.name, arguments: item.arguments || "{}" });
|
|
116
|
-
}
|
|
117
|
-
if (content)
|
|
118
|
-
emit({ type: "delta", text: content });
|
|
119
|
-
return { content, toolCalls };
|
|
120
|
-
}
|
|
121
|
-
async function anthropicRound(config, messages, tools, emit) {
|
|
122
|
-
const base = (config.base_url?.trim() || "").replace(/\/$/, "");
|
|
123
|
-
const system = messages.find((message) => message.role === "system")?.content;
|
|
124
|
-
const response = await fetch(`${base}/messages`, { method: "POST", headers: { "content-type": "application/json", "anthropic-version": "2023-06-01", ...(config.api_key ? { "x-api-key": config.api_key } : {}) }, body: JSON.stringify({ model: config.model || "claude-3-5-sonnet-latest", max_tokens: 4096, system, messages: messages.filter((message) => message.role !== "system"), tools: tools.map((tool) => ({ name: tool.function.name, description: tool.function.description, input_schema: tool.function.parameters })), stream: true }) });
|
|
125
|
-
if (!response.ok)
|
|
126
|
-
throw new Error(`provider_http_${response.status}: ${await response.text()}`);
|
|
127
|
-
let content = "";
|
|
128
|
-
const calls = [];
|
|
129
|
-
let current = null;
|
|
130
|
-
await readSse(response, ({ event, data }) => {
|
|
131
|
-
if (event === "message_stop")
|
|
132
|
-
return false;
|
|
133
|
-
const item = JSON.parse(data);
|
|
134
|
-
if (event === "content_block_start" && item.content_block?.type === "tool_use") {
|
|
135
|
-
current = { id: item.content_block.id, name: item.content_block.name, arguments: "" };
|
|
136
|
-
calls.push(current);
|
|
137
|
-
}
|
|
138
|
-
if (event === "content_block_delta") {
|
|
139
|
-
if (item.delta?.type === "text_delta") {
|
|
140
|
-
content += item.delta.text;
|
|
141
|
-
emit({ type: "delta", text: item.delta.text });
|
|
142
|
-
}
|
|
143
|
-
if (item.delta?.type === "input_json_delta" && current)
|
|
144
|
-
current.arguments += item.delta.partial_json;
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
return { content, toolCalls: calls };
|
|
148
|
-
}
|
|
149
|
-
async function runAgent(request, emit) {
|
|
150
|
-
const provider = (request.provider || "").toLowerCase();
|
|
151
|
-
if (!request.message?.trim()) {
|
|
152
|
-
emit({ type: "delta", text: "告诉我你想查找、保存、修改、删除,还是总结哪些记忆?" });
|
|
153
|
-
emit({ type: "done", toolCalls: [] });
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (!["openai", "openai-compatible", "anthropic", "local"].includes(provider))
|
|
157
|
-
throw new Error("unsupported_provider");
|
|
158
|
-
if (!request.base_url?.trim())
|
|
159
|
-
throw new Error("missing_base_url");
|
|
160
|
-
if (provider !== "local" && !request.api_key)
|
|
161
|
-
throw new Error("missing_api_key");
|
|
162
|
-
const { client, transport } = await connectMemoryMcp();
|
|
163
|
-
const toolResult = await client.listTools();
|
|
164
|
-
const tools = normalizeTools(toolResult.tools);
|
|
165
|
-
const toolCalls = [];
|
|
166
|
-
const history = (request.history ?? []).slice(-12).filter((message) => message.content).map((message) => ({ role: message.role, content: message.content }));
|
|
167
|
-
const messages = [{ role: "system", content: `${agentSystemPrompt}${request.scope ? ` 默认 Scope:${request.scope}` : ""}${request.auto_context === false ? " 不需要自动读取上下文。" : " 每轮任务开始时先调用 memory_get_context。"}` }, ...history];
|
|
168
|
-
if (!history.some((message) => message.role === "user" && message.content === request.message))
|
|
169
|
-
messages.push({ role: "user", content: request.message });
|
|
170
|
-
try {
|
|
171
|
-
if (request.auto_context !== false) {
|
|
172
|
-
const overviewRequest = isMemoryOverviewRequest(request.message);
|
|
173
|
-
const context = await client.callTool({ name: "memory_get_context", arguments: { query: overviewRequest ? null : request.message, scope: request.scope || null, limit: overviewRequest ? 50 : 10 } }, CallToolResultSchema);
|
|
174
|
-
const contextText = context.content?.filter((item) => item.type === "text").map((item) => item.text ?? "").join("\n") || "{}";
|
|
175
|
-
const parsed = (() => { try {
|
|
176
|
-
return JSON.parse(contextText);
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
return contextText;
|
|
180
|
-
} })();
|
|
181
|
-
toolCalls.push({ name: "memory_get_context", label: toolLabels.memory_get_context, count: Array.isArray(parsed) ? parsed.length : parsed?.memories?.length });
|
|
182
|
-
emit({ type: "tool", tool: toolCalls.at(-1) });
|
|
183
|
-
messages[0] = { role: "system", content: `${messages[0].content}\n\n已自动读取 memory_get_context,结果如下。请基于这些结果回答,除非需要更精确检索,否则不要重复调用该工具:\n${contextText}` };
|
|
184
|
-
if (overviewRequest) {
|
|
185
|
-
const listed = await client.callTool({ name: "memory_list", arguments: { scope: request.scope || null, limit: 100 } }, CallToolResultSchema);
|
|
186
|
-
const listedText = listed.content?.filter((item) => item.type === "text").map((item) => item.text ?? "").join("\n") || "[]";
|
|
187
|
-
const listedParsed = (() => { try {
|
|
188
|
-
return JSON.parse(listedText);
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
return [];
|
|
192
|
-
} })();
|
|
193
|
-
const listCall = { name: "memory_list", label: toolLabels.memory_list, count: Array.isArray(listedParsed) ? listedParsed.length : undefined };
|
|
194
|
-
toolCalls.push(listCall);
|
|
195
|
-
emit({ type: "tool", tool: listCall });
|
|
196
|
-
messages[0] = { role: "system", content: `${messages[0].content}\n\n这是针对“有哪些记忆/总结特点”问题通过 MCP memory_list 获取的完整记忆列表。请直接基于它回答,不要声称没有上下文:\n${listedText}` };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
for (let round = 0; round < 6; round += 1) {
|
|
200
|
-
const result = provider === "anthropic" ? await anthropicRound(request, messages, tools, emit) : provider === "openai" ? await openAiResponsesRound(request, messages, tools, emit) : await openAiRound(request, messages, tools, emit);
|
|
201
|
-
if (!result.toolCalls.length) {
|
|
202
|
-
emit({ type: "done", toolCalls });
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (provider === "anthropic")
|
|
206
|
-
messages.push({ role: "assistant", content: [...(result.content ? [{ type: "text", text: result.content }] : []), ...result.toolCalls.map((call) => ({ type: "tool_use", id: call.id, name: call.name, input: JSON.parse(call.arguments || "{}") }))] });
|
|
207
|
-
else
|
|
208
|
-
messages.push({ role: "assistant", content: result.content || null, tool_calls: result.toolCalls.map((call) => ({ id: call.id, type: "function", function: { name: call.name, arguments: call.arguments } })) });
|
|
209
|
-
for (const call of result.toolCalls) {
|
|
210
|
-
const args = JSON.parse(call.arguments || "{}");
|
|
211
|
-
if (request.scope && ["memory_get_context", "memory_search", "memory_list", "memory_store"].includes(call.name) && args.scope == null)
|
|
212
|
-
args.scope = request.scope;
|
|
213
|
-
if (request.scope && call.name === "memory_update" && args.patch && typeof args.patch === "object" && args.patch.scope == null)
|
|
214
|
-
args.patch = { ...args.patch, scope: request.scope };
|
|
215
|
-
const output = await client.callTool({ name: call.name, arguments: args }, CallToolResultSchema);
|
|
216
|
-
const outputContent = output.content;
|
|
217
|
-
const text = outputContent?.filter((item) => item.type === "text").map((item) => item.text ?? "").join("\n") || JSON.stringify(output);
|
|
218
|
-
if (provider === "anthropic")
|
|
219
|
-
messages.push({ role: "user", content: [{ type: "tool_result", tool_use_id: call.id, content: text }] });
|
|
220
|
-
else
|
|
221
|
-
messages.push({ role: "tool", tool_call_id: call.id, content: text });
|
|
222
|
-
const count = (() => { try {
|
|
223
|
-
const parsed = JSON.parse(text);
|
|
224
|
-
return Array.isArray(parsed) ? parsed.length : parsed?.memories?.length ?? undefined;
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
return undefined;
|
|
228
|
-
} })();
|
|
229
|
-
const latest = { name: call.name, label: toolLabels[call.name] ?? call.name, ...(count === undefined ? {} : { count }) };
|
|
230
|
-
toolCalls.push(latest);
|
|
231
|
-
emit({ type: "tool", tool: latest });
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
throw new Error("agent_tool_loop_limit");
|
|
235
|
-
}
|
|
236
|
-
finally {
|
|
237
|
-
await transport.close().catch(() => undefined);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
const codexAgentsPath = join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "AGENTS.md");
|
|
241
|
-
const codexConfigPath = join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "config.toml");
|
|
242
|
-
const codexGuidanceStart = "<!-- memory-one:codex:start -->";
|
|
243
|
-
const codexGuidanceEnd = "<!-- memory-one:codex:end -->";
|
|
244
|
-
const codexGuidance = `${codexGuidanceStart}
|
|
245
|
-
## Memory One
|
|
246
|
-
|
|
247
|
-
Before starting any user task, call the Memory One MCP tool \`memory_get_context\` once to retrieve relevant prior experience.
|
|
248
|
-
|
|
249
|
-
- Use a concise summary of the current task as the query.
|
|
250
|
-
- When working in a project, resolve the Git repository root and pass its absolute directory path as \`scope\`. Use the same scope when storing project-specific memory; omit \`scope\` for general preferences and knowledge.
|
|
251
|
-
- Apply relevant preferences, decisions, corrections, and lessons before planning, answering, editing files, or calling task-specific tools.
|
|
252
|
-
- After the user expresses a correction, preference, decision, project convention, personal fact, or other durable information, proactively save it with \`memory_store\` (or update the matching memory with \`memory_update\`) before finishing the task. Do not save clearly transient, one-off details.
|
|
253
|
-
- Do not skip retrieval merely because the task appears self-contained.
|
|
254
|
-
${codexGuidanceEnd}`;
|
|
255
|
-
async function readCodexAgents() {
|
|
256
|
-
try {
|
|
257
|
-
return await readFile(codexAgentsPath, "utf8");
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
if (error.code === "ENOENT")
|
|
261
|
-
return "";
|
|
262
|
-
throw error;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
function removeManagedCodexGuidance(content) {
|
|
266
|
-
const start = content.indexOf(codexGuidanceStart);
|
|
267
|
-
const end = content.indexOf(codexGuidanceEnd, start + codexGuidanceStart.length);
|
|
268
|
-
if (start < 0 || end < 0)
|
|
269
|
-
return content;
|
|
270
|
-
const before = content.slice(0, start).trimEnd();
|
|
271
|
-
const after = content.slice(end + codexGuidanceEnd.length).trimStart();
|
|
272
|
-
return [before, after].filter(Boolean).join("\n\n");
|
|
273
|
-
}
|
|
274
|
-
async function getCodexIntegration() {
|
|
275
|
-
const content = await readCodexAgents();
|
|
276
|
-
const start = content.indexOf(codexGuidanceStart);
|
|
277
|
-
const end = content.indexOf(codexGuidanceEnd, start + codexGuidanceStart.length);
|
|
278
|
-
const installed = start >= 0 && end >= 0;
|
|
279
|
-
const managed = installed ? content.slice(start, end + codexGuidanceEnd.length) : "";
|
|
280
|
-
return { path: codexAgentsPath, status: installed ? managed === codexGuidance ? "configured" : "update_available" : "not_configured" };
|
|
281
|
-
}
|
|
282
|
-
async function installCodexIntegration() {
|
|
283
|
-
const content = await readCodexAgents();
|
|
284
|
-
const preserved = removeManagedCodexGuidance(content);
|
|
285
|
-
await mkdir(dirname(codexAgentsPath), { recursive: true });
|
|
286
|
-
await writeFile(codexAgentsPath, `${preserved ? `${preserved}\n\n` : ""}${codexGuidance}\n`, "utf8");
|
|
287
|
-
return getCodexIntegration();
|
|
288
|
-
}
|
|
289
|
-
async function readCodexConfig() {
|
|
290
|
-
try {
|
|
291
|
-
return await readFile(codexConfigPath, "utf8");
|
|
292
|
-
}
|
|
293
|
-
catch (error) {
|
|
294
|
-
if (error.code === "ENOENT")
|
|
295
|
-
return "";
|
|
296
|
-
throw error;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
function isMemoryOneCodexSection(name) {
|
|
300
|
-
const normalized = name.replace(/"memory-one"/g, "memory-one");
|
|
301
|
-
return normalized === "mcp_servers.memory-one" || normalized.startsWith("mcp_servers.memory-one.");
|
|
302
|
-
}
|
|
303
|
-
function getMemoryOneCodexSections(content) {
|
|
304
|
-
const sections = [];
|
|
305
|
-
let current = null;
|
|
306
|
-
for (const line of content.split("\n")) {
|
|
307
|
-
const section = line.match(/^\s*\[([^\]]+)]\s*(?:#.*)?$/);
|
|
308
|
-
if (section) {
|
|
309
|
-
if (current)
|
|
310
|
-
sections.push(current.join("\n"));
|
|
311
|
-
current = isMemoryOneCodexSection(section[1].trim()) ? [line] : null;
|
|
312
|
-
}
|
|
313
|
-
else if (current)
|
|
314
|
-
current.push(line);
|
|
315
|
-
}
|
|
316
|
-
if (current)
|
|
317
|
-
sections.push(current.join("\n"));
|
|
318
|
-
return sections.join("\n");
|
|
319
|
-
}
|
|
320
|
-
function removeMemoryOneCodexSections(content) {
|
|
321
|
-
const lines = [];
|
|
322
|
-
let removing = false;
|
|
323
|
-
for (const line of content.split("\n")) {
|
|
324
|
-
const section = line.match(/^\s*\[([^\]]+)]\s*(?:#.*)?$/);
|
|
325
|
-
if (section)
|
|
326
|
-
removing = isMemoryOneCodexSection(section[1].trim());
|
|
327
|
-
if (!removing)
|
|
328
|
-
lines.push(line);
|
|
329
|
-
}
|
|
330
|
-
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
331
|
-
}
|
|
332
|
-
function readTomlString(source, key) {
|
|
333
|
-
const match = source.match(new RegExp(`(?:^|[,{]\\s*)${key}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*"|'[^']*')`, "im"));
|
|
334
|
-
if (!match)
|
|
335
|
-
return null;
|
|
336
|
-
if (match[1].startsWith("'"))
|
|
337
|
-
return match[1].slice(1, -1);
|
|
338
|
-
try {
|
|
339
|
-
return JSON.parse(match[1]);
|
|
340
|
-
}
|
|
341
|
-
catch {
|
|
342
|
-
return null;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
async function getCodexMcpIntegration(expectedEndpoint) {
|
|
346
|
-
const content = await readCodexConfig();
|
|
347
|
-
const sections = getMemoryOneCodexSections(content);
|
|
348
|
-
const endpoint = readTomlString(sections, "url");
|
|
349
|
-
const authorization = readTomlString(sections, "Authorization");
|
|
350
|
-
const key = authorization?.match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
|
|
351
|
-
const keyRecord = key ? store.verifyMcpKey(key) : null;
|
|
352
|
-
const authRequired = store.getMcpConfig().use_bearer_key;
|
|
353
|
-
const configured = endpoint === expectedEndpoint && (authRequired ? Boolean(keyRecord) : !authorization);
|
|
354
|
-
return {
|
|
355
|
-
path: codexConfigPath,
|
|
356
|
-
detected: Boolean(content),
|
|
357
|
-
endpoint,
|
|
358
|
-
auth_required: authRequired,
|
|
359
|
-
configured_key_id: keyRecord?.id ?? null,
|
|
360
|
-
status: configured ? "configured" : sections ? "update_available" : "not_configured",
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
async function installCodexMcpIntegration(endpoint, keyId) {
|
|
364
|
-
const authRequired = store.getMcpConfig().use_bearer_key;
|
|
365
|
-
const key = authRequired && keyId ? store.getMcpKey(keyId) : null;
|
|
366
|
-
if (authRequired && (!key?.secret || key.revoked_at))
|
|
367
|
-
throw new Error("mcp_key_not_found");
|
|
368
|
-
const content = await readCodexConfig();
|
|
369
|
-
const preserved = removeMemoryOneCodexSections(content);
|
|
370
|
-
const section = `[mcp_servers.memory-one]\nurl = ${JSON.stringify(endpoint)}${authRequired ? `\nhttp_headers = { Authorization = ${JSON.stringify(`Bearer ${key.secret}`)} }` : ""}`;
|
|
371
|
-
await mkdir(dirname(codexConfigPath), { recursive: true });
|
|
372
|
-
await writeFile(codexConfigPath, `${preserved ? `${preserved}\n\n` : ""}${section}\n`, "utf8");
|
|
373
|
-
return getCodexMcpIntegration(endpoint);
|
|
374
|
-
}
|
|
375
|
-
async function trackToolCall(toolName, action) {
|
|
376
|
-
const startedAt = performance.now();
|
|
377
|
-
try {
|
|
378
|
-
const result = await action();
|
|
379
|
-
store.recordToolCall(toolName, true, Math.round((performance.now() - startedAt) * 100) / 100);
|
|
380
|
-
return result;
|
|
381
|
-
}
|
|
382
|
-
catch (error) {
|
|
383
|
-
store.recordToolCall(toolName, false, Math.round((performance.now() - startedAt) * 100) / 100);
|
|
384
|
-
throw error;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
const mcpToolNames = ["memory_store", "memory_search", "memory_get_context", "memory_get", "memory_list", "memory_update", "memory_delete", "memory_feedback"];
|
|
388
|
-
function createMcpServer(allowedTools) {
|
|
389
|
-
const mcp = new McpServer({ name: "memory-one", version: "0.1.0" }, {
|
|
390
|
-
instructions: "Memory One provides durable experience for every task. At the start of each user task, call memory_get_context before planning, answering, editing, or using task-specific tools. Query with a concise summary of the current task and apply relevant retrieved memories. After the user expresses a correction, preference, decision, project convention, personal fact, or other durable information, proactively save it with memory_store before finishing the task; use memory_update when correcting an existing memory. Do not save clearly transient, one-off details. Use memory_search for focused follow-up retrieval. Use memory_feedback after retrieved memories prove useful or unhelpful. Only use memory_delete when the user explicitly asks to forget a specific memory. Scope is optional."
|
|
391
|
-
});
|
|
392
|
-
const registerTool = (name, description, schema, handler) => {
|
|
393
|
-
if (!allowedTools || allowedTools.has(name))
|
|
394
|
-
mcp.tool(name, description, schema, handler);
|
|
395
|
-
};
|
|
396
|
-
registerTool("memory_store", "Use proactively when the user states a durable preference, decision, project convention, personal fact, or correction that can help in future tasks. Store it immediately instead of waiting for a separate request. Do not store transient chatter or one-off task details. Scope is an optional category such as a project directory, work area, or session.", {
|
|
397
|
-
content: z.string(), kind: z.string().default("fact"), scope: z.string().nullable().optional(),
|
|
398
|
-
session_id: z.string().nullable().optional(), source: z.string().nullable().optional(), occurred_at: z.string().nullable().optional(),
|
|
399
|
-
confidence: z.number().default(1), importance: z.number().default(0.5), metadata: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
400
|
-
}, async (input) => trackToolCall("memory_store", () => json(exposeMemory(store.create(toStorageInput(input))))));
|
|
401
|
-
registerTool("memory_search", "Use before answering when prior user preferences, past decisions, project conventions, or earlier facts may matter. Prefer searching over guessing, including when the answer seems obvious. Search with 5–12 concise high-signal concepts or identifiers, not the full user message; use scope as an optional category filter.", { query: z.string(), scope: z.string().nullable().optional(), limit: z.number().int().default(20) }, async ({ query, scope, limit }) => trackToolCall("memory_search", () => json(exposeMemories(store.recordRecalls(store.search(query, storageScope, scope && scope !== "global" ? scope : null, limit))))));
|
|
402
|
-
registerTool("memory_get_context", "Universal pre-task context retrieval. Call once at the beginning of every user task, before planning, answering, editing, or invoking task-specific tools. Preferences with metadata.always_include=true are always included before relevant conditional preferences and other memories. When working in a Git repository, pass the repository root's absolute directory path as scope. Project context includes memories from that directory and global memories; omitting scope retrieves global memories only. When query is provided, use 5–12 concise high-signal concepts or identifiers rather than the full user message; keep repository paths in scope.", { query: z.string().nullable().optional(), scope: z.string().nullable().optional(), limit: z.number().int().default(10) }, async ({ query, scope, limit }) => trackToolCall("memory_get_context", () => { const category = scope && scope !== "global" ? scope : null; const memories = store.context(query?.trim() || null, storageScope, category, limit); return json({ scope: scope ?? "global", strategy: category ? "project_plus_global" : "global", memories: exposeMemories(store.recordRecalls(memories)) }); }));
|
|
403
|
-
registerTool("memory_get", "Use after memory_search or memory_get_context returns a memory ID and you need the complete record before relying on or updating it.", { memory_id: z.string() }, async ({ memory_id }) => trackToolCall("memory_get", () => { const memory = store.get(memory_id); return json(exposeMemory(memory ? store.recordRecalls([memory])[0] : { error: "memory_not_found" })); }));
|
|
404
|
-
registerTool("memory_list", "Use when reviewing recent memories, auditing what has been saved, or preparing context without a specific search query. Scope is an optional category filter.", { scope: z.string().nullable().optional(), limit: z.number().int().default(50) }, async ({ scope, limit }) => trackToolCall("memory_list", () => json(exposeMemories(store.list(storageScope, scope && scope !== "global" ? scope : null, limit)))));
|
|
405
|
-
registerTool("memory_update", "Use when the user corrects, refines, or supersedes a stored memory. Fetch the record first when needed, then update only the changed fields. Scope is an optional category.", { memory_id: z.string(), patch: z.record(z.string(), z.unknown()) }, async ({ memory_id, patch }) => trackToolCall("memory_update", () => json(exposeMemory(store.update(memory_id, toStoragePatch(patch)) ?? { error: "memory_not_found" }))));
|
|
406
|
-
registerTool("memory_delete", "Use only when the user explicitly asks to forget or delete a specific memory. This is a soft delete.", { memory_id: z.string() }, async ({ memory_id }) => trackToolCall("memory_delete", () => json({ deleted: store.delete(memory_id) })));
|
|
407
|
-
registerTool("memory_feedback", "Use after applying a retrieved memory or when the user indicates that a memory was useful or not useful. Record that relevance signal so future retrieval can improve.", { memory_id: z.string(), useful: z.boolean() }, async ({ memory_id, useful }) => {
|
|
408
|
-
return trackToolCall("memory_feedback", () => {
|
|
409
|
-
const item = store.get(memory_id);
|
|
410
|
-
if (!item)
|
|
411
|
-
return json({ error: "memory_not_found" });
|
|
412
|
-
return json(exposeMemory(store.update(memory_id, { importance: Math.max(0, Math.min(1, item.importance + (useful ? 0.05 : -0.05))) })));
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
return mcp;
|
|
416
|
-
}
|
|
417
|
-
const app = Fastify({ logger: true });
|
|
418
|
-
const publicDir = join(fileURLToPath(new URL(".", import.meta.url)), "../public");
|
|
419
|
-
const packageJsonPath = join(fileURLToPath(new URL(".", import.meta.url)), "../package.json");
|
|
420
|
-
const packageVersion = JSON.parse(readFileSync(packageJsonPath, "utf8")).version;
|
|
421
|
-
app.register(fastifyStatic, { root: publicDir, prefix: "/" });
|
|
422
|
-
for (const frontendRoute of ["/", "/timeline", "/preferences", "/scopes", "/tags", "/settings", "/mcp-service"]) {
|
|
423
|
-
app.get(frontendRoute, async (_, reply) => reply.sendFile("index.html"));
|
|
424
|
-
}
|
|
425
|
-
app.get("/api/memories", async (request) => { const q = request.query; return store.list(q.scope ?? "user", q.project ?? null, Number(q.limit ?? 50)); });
|
|
426
|
-
app.get("/api/version", async () => {
|
|
427
|
-
try {
|
|
428
|
-
const response = await fetch("https://registry.npmjs.org/@ccjr1120%2Fmemory-one/latest", { signal: AbortSignal.timeout(2000), headers: { accept: "application/json" } });
|
|
429
|
-
if (!response.ok)
|
|
430
|
-
return { current: packageVersion, latest: null, updateAvailable: false };
|
|
431
|
-
const latest = String((await response.json()).version ?? "");
|
|
432
|
-
return { current: packageVersion, latest: latest || null, updateAvailable: Boolean(latest && latest !== packageVersion) };
|
|
433
|
-
}
|
|
434
|
-
catch {
|
|
435
|
-
return { current: packageVersion, latest: null, updateAvailable: false };
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
app.get("/api/search", async (request) => { const q = request.query; return store.recordRecalls(store.search(q.query, q.scope ?? "user", q.project ?? null, Number(q.limit ?? 20))); });
|
|
439
|
-
app.get("/api/memories/most-recalled", async (request) => { const q = request.query; return store.mostRecalled(q.scope ?? "user", q.project ?? null, Number(q.limit ?? 5)); });
|
|
440
|
-
app.get("/api/app-config", async () => store.getAppConfig());
|
|
441
|
-
app.put("/api/app-config", async (request, reply) => {
|
|
442
|
-
const language = request.body?.language;
|
|
443
|
-
if (language !== "zh" && language !== "en")
|
|
444
|
-
return reply.code(422).send({ detail: "language_required" });
|
|
445
|
-
return store.saveAppConfig(language);
|
|
446
|
-
});
|
|
447
|
-
app.get("/api/mcp/stats", async () => store.getToolStats());
|
|
448
|
-
app.get("/api/mcp/config", async () => store.getMcpConfig());
|
|
449
|
-
app.put("/api/mcp/config", async (request) => store.saveMcpConfig(Boolean(request.body?.use_bearer_key)));
|
|
450
|
-
app.get("/api/mcp/keys", async () => store.listMcpKeys());
|
|
451
|
-
app.post("/api/mcp/keys", async (request, reply) => {
|
|
452
|
-
const body = request.body ?? {};
|
|
453
|
-
const name = body.name?.trim() || "未命名 Key";
|
|
454
|
-
const allowedTools = [...new Set((body.allowed_tools ?? []).filter((tool) => mcpToolNames.includes(tool)))];
|
|
455
|
-
const created = store.createMcpKey(name, allowedTools);
|
|
456
|
-
return reply.code(201).send({ key: created.key, key_record: created.keyRecord });
|
|
457
|
-
});
|
|
458
|
-
app.patch("/api/mcp/keys/:id", async (request, reply) => {
|
|
459
|
-
const { id } = request.params;
|
|
460
|
-
const body = request.body ?? {};
|
|
461
|
-
const patch = {
|
|
462
|
-
...(body.name === undefined ? {} : { name: body.name.trim() || "未命名 Key" }),
|
|
463
|
-
...(body.allowed_tools === undefined ? {} : { allowed_tools: [...new Set(body.allowed_tools.filter((tool) => mcpToolNames.includes(tool)))] }),
|
|
464
|
-
};
|
|
465
|
-
const key = store.updateMcpKey(id, patch);
|
|
466
|
-
return key ? key : reply.code(404).send({ detail: "mcp_key_not_found" });
|
|
467
|
-
});
|
|
468
|
-
app.delete("/api/mcp/keys/:id", async (request, reply) => {
|
|
469
|
-
const { id } = request.params;
|
|
470
|
-
return { revoked: store.revokeMcpKey(id) };
|
|
471
|
-
});
|
|
472
|
-
function emitExecutionEvent(executionId, type, data, reply) {
|
|
473
|
-
const event = store.appendAgentExecutionEvent(executionId, type, data);
|
|
474
|
-
if (reply && !reply.raw.destroyed)
|
|
475
|
-
reply.raw.write(sseEvent(type, data, event.id));
|
|
476
|
-
return event;
|
|
477
|
-
}
|
|
478
|
-
app.get("/api/agent/config", async () => store.getAgentConfig());
|
|
479
|
-
app.put("/api/agent/config", async (request, reply) => {
|
|
480
|
-
const body = request.body ?? {};
|
|
481
|
-
return reply.send(store.saveAgentConfig(body));
|
|
482
|
-
});
|
|
483
|
-
app.get("/api/agent/messages", async () => store.listAgentMessages());
|
|
484
|
-
app.post("/api/agent/messages", async (request, reply) => {
|
|
485
|
-
const body = request.body;
|
|
486
|
-
if (!body?.id || !body.role || typeof body.content !== "string")
|
|
487
|
-
return reply.code(422).send({ detail: "message_required" });
|
|
488
|
-
return store.saveAgentMessage({ id: body.id, role: body.role, content: body.content, toolCalls: body.toolCalls });
|
|
489
|
-
});
|
|
490
|
-
app.post("/api/agent/executions", async (request, reply) => {
|
|
491
|
-
const body = request.body ?? {};
|
|
492
|
-
if (!body.message?.trim())
|
|
493
|
-
return reply.code(422).send({ detail: "message_required" });
|
|
494
|
-
const userMessageId = randomUUID();
|
|
495
|
-
const assistantMessageId = randomUUID();
|
|
496
|
-
const history = (body.history ?? []).slice(-12);
|
|
497
|
-
store.saveAgentMessage({ id: userMessageId, role: "user", content: body.message });
|
|
498
|
-
store.saveAgentMessage({ id: assistantMessageId, role: "assistant", content: "", toolCalls: [] });
|
|
499
|
-
const execution = store.createAgentExecution({ messageIds: [userMessageId, assistantMessageId] });
|
|
500
|
-
const turn = agentTurnQueue.then(async () => {
|
|
501
|
-
let content = "";
|
|
502
|
-
let toolCalls = [];
|
|
503
|
-
try {
|
|
504
|
-
await runAgent({ ...body, history }, (event) => {
|
|
505
|
-
if (event.type === "delta")
|
|
506
|
-
content += event.text;
|
|
507
|
-
if (event.type === "tool")
|
|
508
|
-
toolCalls = [...toolCalls, event.tool];
|
|
509
|
-
if (event.type === "done")
|
|
510
|
-
toolCalls = event.toolCalls;
|
|
511
|
-
store.saveAgentMessage({ id: assistantMessageId, role: "assistant", content, toolCalls });
|
|
512
|
-
});
|
|
513
|
-
store.updateAgentExecution(execution.id, { status: "completed" });
|
|
514
|
-
}
|
|
515
|
-
catch (error) {
|
|
516
|
-
const errorMessage = error instanceof Error ? error.message : "agent_request_failed";
|
|
517
|
-
store.updateAgentExecution(execution.id, { status: "failed", error: errorMessage });
|
|
518
|
-
emitExecutionEvent(execution.id, "status", { status: "failed", error: errorMessage });
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
agentTurnQueue = turn.catch(() => undefined);
|
|
522
|
-
return reply.code(202).send(store.getAgentExecution(execution.id));
|
|
523
|
-
});
|
|
524
|
-
app.get("/api/agent/executions/:executionId/events", async (request, reply) => {
|
|
525
|
-
const { executionId } = request.params;
|
|
526
|
-
if (!store.getAgentExecution(executionId))
|
|
527
|
-
return reply.code(404).send({ detail: "execution_not_found" });
|
|
528
|
-
const header = request.headers["last-event-id"];
|
|
529
|
-
const query = request.query;
|
|
530
|
-
const after = Number(header ?? query.after ?? 0) || 0;
|
|
531
|
-
reply.hijack();
|
|
532
|
-
reply.raw.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache, no-transform", connection: "keep-alive", "x-accel-buffering": "no" });
|
|
533
|
-
let closed = false;
|
|
534
|
-
request.raw.on("close", () => { closed = true; });
|
|
535
|
-
const send = () => { for (const event of store.listAgentExecutionEvents(executionId, after))
|
|
536
|
-
if (!closed)
|
|
537
|
-
reply.raw.write(sseEvent(event.type, event.data, event.id)); };
|
|
538
|
-
send();
|
|
539
|
-
const timer = setInterval(() => { if (closed) {
|
|
540
|
-
clearInterval(timer);
|
|
541
|
-
return;
|
|
542
|
-
} send(); }, 250);
|
|
543
|
-
request.raw.on("close", () => { clearInterval(timer); if (!reply.raw.destroyed)
|
|
544
|
-
reply.raw.end(); });
|
|
545
|
-
});
|
|
546
|
-
app.get("/api/agent/executions/:executionId", async (request, reply) => {
|
|
547
|
-
const { executionId } = request.params;
|
|
548
|
-
const execution = store.getAgentExecution(executionId);
|
|
549
|
-
return execution ? execution : reply.code(404).send({ detail: "execution_not_found" });
|
|
550
|
-
});
|
|
551
|
-
app.post("/api/agent/executions/:executionId/messages", async (request, reply) => {
|
|
552
|
-
const { executionId } = request.params;
|
|
553
|
-
const execution = store.getAgentExecution(executionId);
|
|
554
|
-
const body = request.body ?? {};
|
|
555
|
-
if (!execution)
|
|
556
|
-
return reply.code(404).send({ detail: "execution_not_found" });
|
|
557
|
-
if (execution.status !== "completed" && execution.status !== "failed" && execution.status !== "cancelled")
|
|
558
|
-
return reply.code(409).send({ detail: "execution_in_progress" });
|
|
559
|
-
if (!body.message?.trim())
|
|
560
|
-
return reply.code(422).send({ detail: "message_required" });
|
|
561
|
-
const userMessageId = randomUUID();
|
|
562
|
-
const assistantMessageId = randomUUID();
|
|
563
|
-
store.saveAgentMessage({ id: userMessageId, role: "user", content: body.message });
|
|
564
|
-
store.saveAgentMessage({ id: assistantMessageId, role: "assistant", content: "", toolCalls: [] });
|
|
565
|
-
store.updateAgentExecution(executionId, { status: "running", messageIds: [...execution.messageIds, userMessageId, assistantMessageId], error: null });
|
|
566
|
-
const history = [...execution.messages, { id: userMessageId, role: "user", content: body.message }].slice(-12).map((message) => ({ role: message.role, content: message.content }));
|
|
567
|
-
const turn = agentTurnQueue.then(async () => {
|
|
568
|
-
let content = "";
|
|
569
|
-
let toolCalls = [];
|
|
570
|
-
try {
|
|
571
|
-
await runAgent({ ...body, history }, (event) => { if (event.type === "delta")
|
|
572
|
-
content += event.text; if (event.type === "tool")
|
|
573
|
-
toolCalls = [...toolCalls, event.tool]; if (event.type === "done")
|
|
574
|
-
toolCalls = event.toolCalls; store.saveAgentMessage({ id: assistantMessageId, role: "assistant", content, toolCalls }); });
|
|
575
|
-
store.updateAgentExecution(executionId, { status: "completed" });
|
|
576
|
-
}
|
|
577
|
-
catch (error) {
|
|
578
|
-
store.updateAgentExecution(executionId, { status: "failed", error: error instanceof Error ? error.message : "agent_request_failed" });
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
agentTurnQueue = turn.catch(() => undefined);
|
|
582
|
-
return reply.code(202).send(store.getAgentExecution(executionId));
|
|
583
|
-
});
|
|
584
|
-
app.post("/api/agent/executions/:executionId/cancel", async (request, reply) => {
|
|
585
|
-
const { executionId } = request.params;
|
|
586
|
-
const execution = store.getAgentExecution(executionId);
|
|
587
|
-
if (!execution)
|
|
588
|
-
return reply.code(404).send({ detail: "execution_not_found" });
|
|
589
|
-
if (execution.status === "running") {
|
|
590
|
-
store.updateAgentExecution(executionId, { status: "cancelled" });
|
|
591
|
-
emitExecutionEvent(executionId, "status", { status: "cancelled" });
|
|
592
|
-
}
|
|
593
|
-
return store.getAgentExecution(executionId);
|
|
594
|
-
});
|
|
595
|
-
app.post("/api/agent/executions/:executionId/retry", async (request, reply) => {
|
|
596
|
-
const { executionId } = request.params;
|
|
597
|
-
const execution = store.getAgentExecution(executionId);
|
|
598
|
-
if (!execution)
|
|
599
|
-
return reply.code(404).send({ detail: "execution_not_found" });
|
|
600
|
-
if (execution.status === "running")
|
|
601
|
-
return reply.code(409).send({ detail: "execution_in_progress" });
|
|
602
|
-
const lastUser = [...execution.messages].reverse().find((message) => message.role === "user");
|
|
603
|
-
if (!lastUser)
|
|
604
|
-
return reply.code(422).send({ detail: "message_required" });
|
|
605
|
-
const body = request.body ?? {};
|
|
606
|
-
const messageIds = [...execution.messageIds, randomUUID(), randomUUID()];
|
|
607
|
-
const userMessageId = messageIds.at(-2);
|
|
608
|
-
const assistantMessageId = messageIds.at(-1);
|
|
609
|
-
store.saveAgentMessage({ id: userMessageId, role: "user", content: lastUser.content });
|
|
610
|
-
store.saveAgentMessage({ id: assistantMessageId, role: "assistant", content: "", toolCalls: [] });
|
|
611
|
-
store.updateAgentExecution(executionId, { status: "running", messageIds, error: null });
|
|
612
|
-
const history = [...execution.messages.filter((message) => message.id !== lastUser.id), { role: "user", content: lastUser.content }].slice(-12).map((message) => ({ role: message.role, content: message.content }));
|
|
613
|
-
const turn = agentTurnQueue.then(async () => {
|
|
614
|
-
let content = "";
|
|
615
|
-
let toolCalls = [];
|
|
616
|
-
try {
|
|
617
|
-
await runAgent({ ...body, message: lastUser.content, history }, (event) => { if (event.type === "delta")
|
|
618
|
-
content += event.text; if (event.type === "tool")
|
|
619
|
-
toolCalls = [...toolCalls, event.tool]; if (event.type === "done")
|
|
620
|
-
toolCalls = event.toolCalls; store.saveAgentMessage({ id: assistantMessageId, role: "assistant", content, toolCalls }); });
|
|
621
|
-
store.updateAgentExecution(executionId, { status: "completed" });
|
|
622
|
-
}
|
|
623
|
-
catch (error) {
|
|
624
|
-
store.updateAgentExecution(executionId, { status: "failed", error: error instanceof Error ? error.message : "agent_request_failed" });
|
|
625
|
-
}
|
|
626
|
-
});
|
|
627
|
-
agentTurnQueue = turn.catch(() => undefined);
|
|
628
|
-
return reply.code(202).send(store.getAgentExecution(executionId));
|
|
629
|
-
});
|
|
630
|
-
app.post("/api/agent/chat", async (request, reply) => {
|
|
631
|
-
try {
|
|
632
|
-
let text = "";
|
|
633
|
-
const toolCalls = [];
|
|
634
|
-
await runAgent(request.body ?? {}, (event) => { if (event.type === "delta")
|
|
635
|
-
text += event.text; if (event.type === "done")
|
|
636
|
-
toolCalls.push(...event.toolCalls); });
|
|
637
|
-
return { reply: text, toolCalls };
|
|
638
|
-
}
|
|
639
|
-
catch (error) {
|
|
640
|
-
request.log.error(error);
|
|
641
|
-
return reply.code(500).send({ detail: error instanceof Error ? error.message : "agent_request_failed" });
|
|
642
|
-
}
|
|
643
|
-
});
|
|
644
|
-
app.post("/api/agent/models", async (request, reply) => {
|
|
645
|
-
const body = request.body ?? {};
|
|
646
|
-
const provider = (body.provider || "").toLowerCase();
|
|
647
|
-
if (!["openai", "openai-compatible", "anthropic", "local"].includes(provider))
|
|
648
|
-
return reply.code(422).send({ detail: "unsupported_provider" });
|
|
649
|
-
const base = (body.base_url?.trim() || "").replace(/\/$/, "");
|
|
650
|
-
if (!base)
|
|
651
|
-
return reply.code(422).send({ detail: "base_url_required" });
|
|
652
|
-
const headers = { accept: "application/json" };
|
|
653
|
-
if (provider === "anthropic") {
|
|
654
|
-
headers["anthropic-version"] = "2023-06-01";
|
|
655
|
-
if (body.api_key?.trim())
|
|
656
|
-
headers["x-api-key"] = body.api_key.trim();
|
|
657
|
-
}
|
|
658
|
-
else if (body.api_key?.trim()) {
|
|
659
|
-
headers.authorization = `Bearer ${body.api_key.trim()}`;
|
|
660
|
-
}
|
|
661
|
-
try {
|
|
662
|
-
const response = await fetch(`${base}/models`, { headers });
|
|
663
|
-
const text = await response.text();
|
|
664
|
-
let payload = null;
|
|
665
|
-
try {
|
|
666
|
-
payload = text ? JSON.parse(text) : null;
|
|
667
|
-
}
|
|
668
|
-
catch {
|
|
669
|
-
payload = null;
|
|
670
|
-
}
|
|
671
|
-
if (!response.ok)
|
|
672
|
-
return reply.code(response.status >= 400 && response.status < 600 ? response.status : 502).send({ detail: payload?.error?.message || payload?.message || text || "model_list_failed" });
|
|
673
|
-
const entries = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : [];
|
|
674
|
-
const models = [...new Set(entries.map((item) => typeof item === "string" ? item : item?.id).filter((item) => typeof item === "string" && item.trim().length > 0))];
|
|
675
|
-
return { models };
|
|
676
|
-
}
|
|
677
|
-
catch (error) {
|
|
678
|
-
request.log.error(error);
|
|
679
|
-
return reply.code(502).send({ detail: "model_list_unreachable" });
|
|
680
|
-
}
|
|
681
|
-
});
|
|
682
|
-
app.post("/api/agent/stream", async (request, reply) => {
|
|
683
|
-
const body = (request.body ?? {});
|
|
684
|
-
const userMessageId = body.user_message_id;
|
|
685
|
-
const assistantMessageId = body.assistant_message_id;
|
|
686
|
-
if (userMessageId && body.message)
|
|
687
|
-
store.saveAgentMessage({ id: userMessageId, role: "user", content: body.message });
|
|
688
|
-
if (assistantMessageId)
|
|
689
|
-
store.saveAgentMessage({ id: assistantMessageId, role: "assistant", content: "", toolCalls: [] });
|
|
690
|
-
let assistantContent = "";
|
|
691
|
-
let assistantTools = [];
|
|
692
|
-
reply.hijack();
|
|
693
|
-
reply.raw.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache, no-transform", connection: "keep-alive", "x-accel-buffering": "no" });
|
|
694
|
-
const turn = agentTurnQueue.then(async () => {
|
|
695
|
-
const allMessages = store.listAgentMessages(1000).filter((item) => item.content);
|
|
696
|
-
const currentIndex = userMessageId ? allMessages.findIndex((item) => item.id === userMessageId) : allMessages.length - 1;
|
|
697
|
-
const storedHistory = allMessages.slice(0, Math.max(0, currentIndex)).map((item) => ({ role: item.role, content: item.content })).slice(-12);
|
|
698
|
-
await runAgent({ ...body, history: storedHistory }, (event) => {
|
|
699
|
-
if (event.type === "delta")
|
|
700
|
-
assistantContent += event.text;
|
|
701
|
-
if (event.type === "tool")
|
|
702
|
-
assistantTools = [...assistantTools, event.tool];
|
|
703
|
-
if (event.type === "done")
|
|
704
|
-
assistantTools = event.toolCalls;
|
|
705
|
-
if (assistantMessageId)
|
|
706
|
-
store.saveAgentMessage({ id: assistantMessageId, role: "assistant", content: assistantContent, toolCalls: assistantTools });
|
|
707
|
-
if (!reply.raw.destroyed)
|
|
708
|
-
reply.raw.write(sseEvent(event.type, event.type === "delta" ? { text: event.text } : event.type === "tool" ? event.tool : { toolCalls: event.toolCalls }));
|
|
709
|
-
});
|
|
710
|
-
});
|
|
711
|
-
agentTurnQueue = turn.catch(() => undefined);
|
|
712
|
-
try {
|
|
713
|
-
await turn;
|
|
714
|
-
}
|
|
715
|
-
catch (error) {
|
|
716
|
-
request.log.error(error);
|
|
717
|
-
reply.raw.write(sseEvent("error", { detail: error instanceof Error ? error.message : "agent_request_failed" }));
|
|
718
|
-
}
|
|
719
|
-
finally {
|
|
720
|
-
reply.raw.end();
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
app.get("/api/integrations/codex", async () => getCodexIntegration());
|
|
724
|
-
app.post("/api/integrations/codex/install", async () => installCodexIntegration());
|
|
725
|
-
app.get("/api/integrations/codex/mcp", async (request) => {
|
|
726
|
-
const { endpoint = "" } = request.query;
|
|
727
|
-
return getCodexMcpIntegration(endpoint);
|
|
728
|
-
});
|
|
729
|
-
app.post("/api/integrations/codex/mcp/install", async (request, reply) => {
|
|
730
|
-
const body = request.body ?? {};
|
|
731
|
-
if (!body.endpoint)
|
|
732
|
-
return reply.code(422).send({ detail: "codex_mcp_config_required" });
|
|
733
|
-
try {
|
|
734
|
-
return await installCodexMcpIntegration(body.endpoint, body.key_id);
|
|
735
|
-
}
|
|
736
|
-
catch (error) {
|
|
737
|
-
if (error instanceof Error && error.message === "mcp_key_not_found")
|
|
738
|
-
return reply.code(404).send({ detail: error.message });
|
|
739
|
-
throw error;
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
app.get("/api/memories/:id", async (request, reply) => { const { id } = request.params; const item = store.get(id); return item ? item : reply.code(404).send({ detail: "memory_not_found" }); });
|
|
743
|
-
app.post("/api/memories", async (request, reply) => { const payload = request.body; if (!payload?.content)
|
|
744
|
-
return reply.code(422).send({ detail: "content_required" }); return store.create(payload); });
|
|
745
|
-
app.patch("/api/memories/:id", async (request, reply) => { const { id } = request.params; const item = store.update(id, request.body); return item ? item : reply.code(404).send({ detail: "memory_not_found" }); });
|
|
746
|
-
app.delete("/api/memories/:id", async (request) => { const { id } = request.params; return { deleted: store.delete(id) }; });
|
|
747
|
-
app.all("/mcp", async (request, reply) => reply.redirect("/mcp/", 307));
|
|
748
|
-
app.all("/mcp/", async (request, reply) => {
|
|
749
|
-
const internal = request.headers["x-memory-one-internal"] === internalMcpToken;
|
|
750
|
-
const authRequired = store.getMcpConfig().use_bearer_key;
|
|
751
|
-
const authorization = request.headers.authorization;
|
|
752
|
-
const secret = authorization?.match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
|
|
753
|
-
const key = authRequired && secret ? store.verifyMcpKey(secret) : null;
|
|
754
|
-
if (!internal && authRequired && (!secret || !key))
|
|
755
|
-
return reply.code(401).header("www-authenticate", "Bearer").send({ error: secret ? "invalid_mcp_key" : "mcp_key_required" });
|
|
756
|
-
const allowedTools = key && key.allowed_tools.length ? new Set(key.allowed_tools) : undefined;
|
|
757
|
-
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
758
|
-
const mcp = createMcpServer(allowedTools);
|
|
759
|
-
await mcp.connect(transport);
|
|
760
|
-
await transport.handleRequest(request.raw, reply.raw, request.body);
|
|
761
|
-
reply.hijack();
|
|
762
|
-
});
|
|
1
|
+
import { createApp } from "./app.js";
|
|
763
2
|
const port = Number(process.env.MEMORY_PORT ?? 8765);
|
|
3
|
+
const { app } = createApp();
|
|
764
4
|
app.listen({ host: "127.0.0.1", port }).then(() => console.log(`Memory One listening on http://127.0.0.1:${port}`));
|
|
765
5
|
//# sourceMappingURL=server.js.map
|