@makefinks/daemon 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -9
- package/package.json +3 -2
- package/src/ai/agent-turn-runner.ts +5 -0
- package/src/ai/daemon-ai.ts +74 -24
- package/src/ai/mcp/mcp-manager.ts +348 -0
- package/src/ai/memory/memory-manager.ts +90 -2
- package/src/ai/model-config.ts +1 -1
- package/src/ai/tools/index.ts +14 -12
- package/src/ai/tools/subagents.ts +17 -13
- package/src/app/components/AppOverlays.tsx +2 -0
- package/src/components/SettingsMenu.tsx +13 -0
- package/src/components/ToolCallView.tsx +51 -12
- package/src/components/ToolsMenu.tsx +81 -10
- package/src/hooks/daemon-event-handlers.ts +9 -0
- package/src/hooks/keyboard-handlers.ts +24 -0
- package/src/hooks/use-app-context-builder.ts +2 -0
- package/src/hooks/use-app-controller.ts +5 -0
- package/src/hooks/use-app-preferences-bootstrap.ts +11 -0
- package/src/hooks/use-app-settings.ts +6 -0
- package/src/hooks/use-daemon-events.ts +4 -0
- package/src/index.tsx +3 -0
- package/src/state/app-context.tsx +2 -0
- package/src/state/daemon-events.ts +2 -0
- package/src/state/daemon-state.ts +10 -0
- package/src/types/index.ts +10 -0
- package/src/utils/config.ts +33 -0
- package/src/utils/preferences.ts +3 -0
- package/src/utils/tool-output-preview.ts +104 -0
package/README.md
CHANGED
|
@@ -11,13 +11,13 @@ but can also interact with and **control** your system through the terminal with
|
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
|
+
DAEMON requires Bun at runtime, but global installation is currently documented via npm because some Bun global setups can fail on native `sqlite3` bindings pulled in by the optional memory feature.
|
|
15
|
+
|
|
14
16
|
```bash
|
|
15
|
-
# npm (
|
|
17
|
+
# npm (recommended)
|
|
18
|
+
# Note: you may see deprecation warnings from transitive dependencies.
|
|
16
19
|
npm i -g @makefinks/daemon
|
|
17
20
|
|
|
18
|
-
# bun (recommended)
|
|
19
|
-
bun add -g @makefinks/daemon
|
|
20
|
-
|
|
21
21
|
# additional installs (macOS)
|
|
22
22
|
brew install sox # For Audio Input / Output
|
|
23
23
|
```
|
|
@@ -95,7 +95,7 @@ DAEMON can persist user-specific facts across sessions using [mem0](https://gith
|
|
|
95
95
|
| Bash Execution | Bash integration with approval scoping for potentially dangerous commands. |
|
|
96
96
|
| JS Page Rendering | Optional Playwright renderer for SPA content. |
|
|
97
97
|
|
|
98
|
-
## 📦 Install (npm
|
|
98
|
+
## 📦 Install (npm)
|
|
99
99
|
|
|
100
100
|
DAEMON is published as a CLI package. It **requires Bun** at runtime, even if you install via npm.
|
|
101
101
|
|
|
@@ -107,8 +107,6 @@ Then install DAEMON:
|
|
|
107
107
|
```bash
|
|
108
108
|
# Global npm install
|
|
109
109
|
npm i -g @makefinks/daemon
|
|
110
|
-
# or via bun
|
|
111
|
-
bun add -g @makefinks/daemon
|
|
112
110
|
|
|
113
111
|
# Then run
|
|
114
112
|
daemon
|
|
@@ -155,8 +153,6 @@ This feature is **optional** and intentionally not installed by default (browser
|
|
|
155
153
|
```bash
|
|
156
154
|
# 1) Install Playwright globally
|
|
157
155
|
npm i -g playwright
|
|
158
|
-
# or
|
|
159
|
-
bun add -g playwright
|
|
160
156
|
|
|
161
157
|
# 2) Install Chromium browser binaries
|
|
162
158
|
npx playwright install chromium
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"module": "src/index.tsx",
|
|
30
30
|
"type": "module",
|
|
31
|
-
"version": "0.
|
|
31
|
+
"version": "0.8.0",
|
|
32
32
|
"bin": {
|
|
33
33
|
"daemon": "dist/cli.js"
|
|
34
34
|
},
|
|
@@ -73,8 +73,9 @@
|
|
|
73
73
|
"typescript": "^5.9.2"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
+
"@ai-sdk/mcp": "^1.0.14",
|
|
76
77
|
"@ai-sdk/openai": "^3.0.0",
|
|
77
|
-
"@openrouter/ai-sdk-provider": "1.
|
|
78
|
+
"@openrouter/ai-sdk-provider": "^2.1.0",
|
|
78
79
|
"@opentui-ui/toast": "^0.0.3",
|
|
79
80
|
"@opentui/core": "^0.1.63",
|
|
80
81
|
"@opentui/react": "^0.1.63",
|
|
@@ -40,6 +40,7 @@ export class AgentTurnRunner {
|
|
|
40
40
|
this.abortController = new AbortController();
|
|
41
41
|
|
|
42
42
|
const isActive = () => runId === this.activeRunId && this.abortController !== null;
|
|
43
|
+
const isCurrent = () => runId === this.activeRunId;
|
|
43
44
|
|
|
44
45
|
let result: AgentTurnResult | null = null;
|
|
45
46
|
let error: Error | null = null;
|
|
@@ -93,6 +94,10 @@ export class AgentTurnRunner {
|
|
|
93
94
|
if (!isActive()) return;
|
|
94
95
|
callbacks.onStepUsage?.(usage);
|
|
95
96
|
},
|
|
97
|
+
onMemorySaved: (preview) => {
|
|
98
|
+
if (!isCurrent()) return;
|
|
99
|
+
callbacks.onMemorySaved?.(preview);
|
|
100
|
+
},
|
|
96
101
|
onComplete: (fullText, responseMessages, usage, finalText) => {
|
|
97
102
|
if (!isActive()) return;
|
|
98
103
|
result = { fullText, responseMessages, usage, finalText };
|
package/src/ai/daemon-ai.ts
CHANGED
|
@@ -21,6 +21,8 @@ import type {
|
|
|
21
21
|
ToolApprovalRequest,
|
|
22
22
|
ToolApprovalResponse,
|
|
23
23
|
TranscriptionResult,
|
|
24
|
+
MemoryToastPreview,
|
|
25
|
+
MemoryToastOperation,
|
|
24
26
|
} from "../types";
|
|
25
27
|
import { debug, toolDebug } from "../utils/debug-logger";
|
|
26
28
|
import { getOpenRouterReportedCost } from "../utils/openrouter-reported-cost";
|
|
@@ -166,7 +168,7 @@ export async function generateResponse(
|
|
|
166
168
|
|
|
167
169
|
// Include relevant memories in the system prompt if available
|
|
168
170
|
let memoryInjection: string | undefined;
|
|
169
|
-
if (isMemoryAvailable()) {
|
|
171
|
+
if (getDaemonManager().memoryEnabled && isMemoryAvailable()) {
|
|
170
172
|
const injection = await buildMemoryInjection(userMessage);
|
|
171
173
|
if (injection) {
|
|
172
174
|
memoryInjection = injection;
|
|
@@ -176,29 +178,6 @@ export async function generateResponse(
|
|
|
176
178
|
// Add the user message
|
|
177
179
|
messages.push({ role: "user" as const, content: userMessage });
|
|
178
180
|
|
|
179
|
-
const userTextForMemory = userMessage.trim();
|
|
180
|
-
if (userTextForMemory) {
|
|
181
|
-
void (async () => {
|
|
182
|
-
if (!isMemoryAvailable()) return;
|
|
183
|
-
const memoryManager = getMemoryManager();
|
|
184
|
-
await memoryManager.initialize();
|
|
185
|
-
if (!memoryManager.isAvailable) return;
|
|
186
|
-
try {
|
|
187
|
-
await memoryManager.add(
|
|
188
|
-
[{ role: "user", content: userTextForMemory }],
|
|
189
|
-
{
|
|
190
|
-
timestamp: new Date().toISOString(),
|
|
191
|
-
source: "conversation",
|
|
192
|
-
},
|
|
193
|
-
true
|
|
194
|
-
);
|
|
195
|
-
} catch (error) {
|
|
196
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
197
|
-
debug.error("memory-auto-add-failed", { message: err.message });
|
|
198
|
-
}
|
|
199
|
-
})();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
181
|
// Stream response from the agent with mode-specific system prompt
|
|
203
182
|
const agent = await createDaemonAgent(interactionMode, reasoningEffort, memoryInjection);
|
|
204
183
|
|
|
@@ -317,6 +296,11 @@ export async function generateResponse(
|
|
|
317
296
|
}
|
|
318
297
|
|
|
319
298
|
callbacks.onComplete?.(fullText, allResponseMessages, undefined, finalText);
|
|
299
|
+
|
|
300
|
+
void persistConversationMemory(userMessage, finalText ?? fullText).then((preview) => {
|
|
301
|
+
if (!preview) return;
|
|
302
|
+
callbacks.onMemorySaved?.(preview);
|
|
303
|
+
});
|
|
320
304
|
} catch (error) {
|
|
321
305
|
// Check if this was an abort - don't treat as error
|
|
322
306
|
if (abortSignal?.aborted) {
|
|
@@ -334,6 +318,72 @@ export async function generateResponse(
|
|
|
334
318
|
}
|
|
335
319
|
}
|
|
336
320
|
|
|
321
|
+
async function persistConversationMemory(
|
|
322
|
+
userMessage: string,
|
|
323
|
+
assistantMessage: string
|
|
324
|
+
): Promise<string | null> {
|
|
325
|
+
const userTextForMemory = userMessage.trim();
|
|
326
|
+
const assistantTextForMemory = assistantMessage.trim();
|
|
327
|
+
|
|
328
|
+
if (!userTextForMemory || !assistantTextForMemory) return null;
|
|
329
|
+
if (!getDaemonManager().memoryEnabled) return null;
|
|
330
|
+
if (!isMemoryAvailable()) return null;
|
|
331
|
+
|
|
332
|
+
const memoryManager = getMemoryManager();
|
|
333
|
+
await memoryManager.initialize();
|
|
334
|
+
if (!memoryManager.isAvailable) return null;
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const memoryMessages = [
|
|
338
|
+
{ role: "user", content: `<user>${userTextForMemory}</user>` },
|
|
339
|
+
{ role: "assistant", content: `<assistant>${assistantTextForMemory}</assistant>` },
|
|
340
|
+
];
|
|
341
|
+
const result = await memoryManager.add(
|
|
342
|
+
memoryMessages,
|
|
343
|
+
{
|
|
344
|
+
timestamp: new Date().toISOString(),
|
|
345
|
+
source: "conversation",
|
|
346
|
+
},
|
|
347
|
+
true
|
|
348
|
+
);
|
|
349
|
+
return buildMemoryToastPreview(result.results);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
352
|
+
debug.error("memory-auto-add-failed", { message: err.message });
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function buildMemoryToastPreview(
|
|
358
|
+
results: Array<{ memory: string; event: "ADD" | "UPDATE" | "DELETE" | "NONE" }>
|
|
359
|
+
): MemoryToastPreview | null {
|
|
360
|
+
if (results.length === 0) return null;
|
|
361
|
+
|
|
362
|
+
const saved = results.filter((entry) => entry.event === "ADD" || entry.event === "UPDATE");
|
|
363
|
+
if (saved.length === 0) return null;
|
|
364
|
+
|
|
365
|
+
const previewEntries = saved.length > 2 ? saved.slice(-2) : saved;
|
|
366
|
+
const lines = previewEntries.map((entry) => `• ${truncatePreview(entry.memory, 52)}`);
|
|
367
|
+
if (saved.length > previewEntries.length) {
|
|
368
|
+
lines.push(`• +${saved.length - previewEntries.length} more`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const hasUpdate = saved.some((entry) => entry.event === "UPDATE");
|
|
372
|
+
const operation: MemoryToastOperation = hasUpdate ? "UPDATE" : "ADD";
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
operation,
|
|
376
|
+
description: lines.join("\n"),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function truncatePreview(text: string, maxChars: number): string {
|
|
381
|
+
const trimmed = text.trim();
|
|
382
|
+
if (trimmed.length <= maxChars) return trimmed;
|
|
383
|
+
if (maxChars <= 1) return "…";
|
|
384
|
+
return `${trimmed.slice(0, maxChars - 1)}…`;
|
|
385
|
+
}
|
|
386
|
+
|
|
337
387
|
/**
|
|
338
388
|
* Generate a short descriptive title for a session based on the first user message.
|
|
339
389
|
* Uses the currently selected model.
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
|
|
4
|
+
import { type MCPClient, createMCPClient } from "@ai-sdk/mcp";
|
|
5
|
+
import type { ToolSet } from "ai";
|
|
6
|
+
|
|
7
|
+
import { type McpServerConfig, type McpTransportType, loadManualConfig } from "../../utils/config";
|
|
8
|
+
import { debug } from "../../utils/debug-logger";
|
|
9
|
+
|
|
10
|
+
export type McpServerLifecycleStatus = "idle" | "loading" | "ready" | "error";
|
|
11
|
+
|
|
12
|
+
export interface McpServerStatus {
|
|
13
|
+
id: string;
|
|
14
|
+
type: McpTransportType;
|
|
15
|
+
url: string;
|
|
16
|
+
status: McpServerLifecycleStatus;
|
|
17
|
+
toolCount: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface McpToolMeta {
|
|
22
|
+
internalName: string;
|
|
23
|
+
serverId: string;
|
|
24
|
+
originalToolName: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type McpServerResolvedConfig = {
|
|
28
|
+
id: string;
|
|
29
|
+
type: McpTransportType;
|
|
30
|
+
url: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const MAX_TOOL_NAME_LENGTH = 64;
|
|
34
|
+
|
|
35
|
+
function sanitizeNamePart(raw: string): string {
|
|
36
|
+
const cleaned = raw
|
|
37
|
+
.replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
38
|
+
.replace(/_+/g, "_")
|
|
39
|
+
.replace(/^_+/, "")
|
|
40
|
+
.replace(/_+$/, "");
|
|
41
|
+
return cleaned.length > 0 ? cleaned : "x";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function shortHash(input: string): string {
|
|
45
|
+
return createHash("sha1").update(input).digest("hex").slice(0, 8);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function deriveServerIdFromUrl(url: string): string | null {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = new URL(url);
|
|
51
|
+
const host = parsed.hostname;
|
|
52
|
+
if (!host) return null;
|
|
53
|
+
const port = parsed.port ? `_${parsed.port}` : "";
|
|
54
|
+
return `${host}${port}`;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ensureUniqueId(base: string, used: Set<string>): string {
|
|
61
|
+
let candidate = base;
|
|
62
|
+
let i = 2;
|
|
63
|
+
while (used.has(candidate)) {
|
|
64
|
+
candidate = `${base}-${i}`;
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
used.add(candidate);
|
|
68
|
+
return candidate;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildInternalToolName(serverId: string, toolName: string, used: Set<string>): string {
|
|
72
|
+
const serverPart = sanitizeNamePart(serverId);
|
|
73
|
+
const toolPart = sanitizeNamePart(toolName);
|
|
74
|
+
const base = `mcp_${serverPart}__${toolPart}`;
|
|
75
|
+
if (base.length <= MAX_TOOL_NAME_LENGTH && !used.has(base)) {
|
|
76
|
+
used.add(base);
|
|
77
|
+
return base;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const hash = shortHash(`${serverId}\0${toolName}`);
|
|
81
|
+
let left = serverPart;
|
|
82
|
+
let right = toolPart;
|
|
83
|
+
let candidate = `mcp_${left}__${right}__${hash}`;
|
|
84
|
+
if (candidate.length > MAX_TOOL_NAME_LENGTH) {
|
|
85
|
+
const maxRight = Math.max(8, MAX_TOOL_NAME_LENGTH - `mcp_${left}____${hash}`.length);
|
|
86
|
+
right = right.slice(0, maxRight);
|
|
87
|
+
candidate = `mcp_${left}__${right}__${hash}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (candidate.length > MAX_TOOL_NAME_LENGTH) {
|
|
91
|
+
candidate = candidate.slice(0, MAX_TOOL_NAME_LENGTH);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!used.has(candidate)) {
|
|
95
|
+
used.add(candidate);
|
|
96
|
+
return candidate;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let counter = 2;
|
|
100
|
+
let next = candidate;
|
|
101
|
+
while (used.has(next)) {
|
|
102
|
+
const counterHash = shortHash(`${serverId}\0${toolName}\0${counter}`);
|
|
103
|
+
next = `mcp_${left}__${right}__${counterHash}`;
|
|
104
|
+
if (next.length > MAX_TOOL_NAME_LENGTH) {
|
|
105
|
+
next = next.slice(0, MAX_TOOL_NAME_LENGTH);
|
|
106
|
+
}
|
|
107
|
+
counter++;
|
|
108
|
+
}
|
|
109
|
+
used.add(next);
|
|
110
|
+
return next;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveServerConfigs(raw: McpServerConfig[] | undefined): McpServerResolvedConfig[] {
|
|
114
|
+
if (!raw || raw.length === 0) return [];
|
|
115
|
+
const usedIds = new Set<string>();
|
|
116
|
+
const out: McpServerResolvedConfig[] = [];
|
|
117
|
+
|
|
118
|
+
for (const entry of raw) {
|
|
119
|
+
if (!entry || typeof entry !== "object") continue;
|
|
120
|
+
const type = entry.type;
|
|
121
|
+
const url = entry.url;
|
|
122
|
+
if ((type !== "http" && type !== "sse") || typeof url !== "string" || url.trim().length === 0) continue;
|
|
123
|
+
const derivedId = entry.id?.trim() ? entry.id.trim() : deriveServerIdFromUrl(url.trim());
|
|
124
|
+
const id = derivedId
|
|
125
|
+
? ensureUniqueId(derivedId, usedIds)
|
|
126
|
+
: ensureUniqueId(`server-${out.length + 1}`, usedIds);
|
|
127
|
+
out.push({ id, type, url: url.trim() });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
class McpManager extends EventEmitter {
|
|
134
|
+
private started = false;
|
|
135
|
+
private loadRunId = 0;
|
|
136
|
+
|
|
137
|
+
private servers: McpServerStatus[] = [];
|
|
138
|
+
private mergedTools: ToolSet = {};
|
|
139
|
+
private toolMetaByName = new Map<string, McpToolMeta>();
|
|
140
|
+
private internalNamesByServer = new Map<string, Set<string>>();
|
|
141
|
+
|
|
142
|
+
private clientsByServer = new Map<string, MCPClient>();
|
|
143
|
+
private toolsByServer = new Map<string, ToolSet>();
|
|
144
|
+
|
|
145
|
+
start(): void {
|
|
146
|
+
if (this.started) return;
|
|
147
|
+
this.started = true;
|
|
148
|
+
setImmediate(() => {
|
|
149
|
+
void this.loadFromConfig();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getServersSnapshot(): McpServerStatus[] {
|
|
154
|
+
return this.servers.map((s) => ({ ...s }));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getToolsSnapshot(): ToolSet {
|
|
158
|
+
return this.mergedTools;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getToolMeta(toolName: string): McpToolMeta | null {
|
|
162
|
+
return this.toolMetaByName.get(toolName) ?? null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async closeAll(): Promise<void> {
|
|
166
|
+
const clients = [...this.clientsByServer.values()];
|
|
167
|
+
this.clientsByServer.clear();
|
|
168
|
+
await Promise.allSettled(clients.map((client) => client.close()));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private emitUpdate(): void {
|
|
172
|
+
this.emit("update");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private rebuildMergedTools(): void {
|
|
176
|
+
const merged: ToolSet = {};
|
|
177
|
+
for (const tools of this.toolsByServer.values()) {
|
|
178
|
+
Object.assign(merged, tools);
|
|
179
|
+
}
|
|
180
|
+
this.mergedTools = merged;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private clearServerTools(serverId: string): void {
|
|
184
|
+
const internalNames = this.internalNamesByServer.get(serverId);
|
|
185
|
+
if (internalNames) {
|
|
186
|
+
for (const internalName of internalNames) {
|
|
187
|
+
this.toolMetaByName.delete(internalName);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
this.internalNamesByServer.delete(serverId);
|
|
191
|
+
this.toolsByServer.delete(serverId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private setServerStatus(next: McpServerStatus): void {
|
|
195
|
+
const idx = this.servers.findIndex((s) => s.id === next.id);
|
|
196
|
+
if (idx >= 0) {
|
|
197
|
+
const copy = [...this.servers];
|
|
198
|
+
copy[idx] = next;
|
|
199
|
+
this.servers = copy;
|
|
200
|
+
} else {
|
|
201
|
+
this.servers = [...this.servers, next];
|
|
202
|
+
}
|
|
203
|
+
this.emitUpdate();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private async loadFromConfig(): Promise<void> {
|
|
207
|
+
const runId = ++this.loadRunId;
|
|
208
|
+
const config = loadManualConfig();
|
|
209
|
+
const servers = resolveServerConfigs(config.mcpServers);
|
|
210
|
+
|
|
211
|
+
this.servers = servers.map((server) => ({
|
|
212
|
+
id: server.id,
|
|
213
|
+
type: server.type,
|
|
214
|
+
url: server.url,
|
|
215
|
+
status: "loading" as const,
|
|
216
|
+
toolCount: 0,
|
|
217
|
+
}));
|
|
218
|
+
this.emitUpdate();
|
|
219
|
+
|
|
220
|
+
if (servers.length === 0) {
|
|
221
|
+
this.mergedTools = {};
|
|
222
|
+
this.toolMetaByName.clear();
|
|
223
|
+
this.internalNamesByServer.clear();
|
|
224
|
+
this.toolsByServer.clear();
|
|
225
|
+
this.emitUpdate();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await Promise.allSettled(
|
|
230
|
+
servers.map(async (server) => {
|
|
231
|
+
if (runId !== this.loadRunId) return;
|
|
232
|
+
await this.loadSingleServer(server, runId);
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private async loadSingleServer(server: McpServerResolvedConfig, runId: number): Promise<void> {
|
|
238
|
+
const startedAt = Date.now();
|
|
239
|
+
const currentStatus = this.servers.find((s) => s.id === server.id);
|
|
240
|
+
if (!currentStatus || runId !== this.loadRunId) return;
|
|
241
|
+
|
|
242
|
+
this.setServerStatus({
|
|
243
|
+
id: server.id,
|
|
244
|
+
type: server.type,
|
|
245
|
+
url: server.url,
|
|
246
|
+
status: "loading",
|
|
247
|
+
toolCount: 0,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
let client: MCPClient | null = null;
|
|
251
|
+
try {
|
|
252
|
+
client = await createMCPClient({
|
|
253
|
+
transport: {
|
|
254
|
+
type: server.type,
|
|
255
|
+
url: server.url,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const tools = await client.tools();
|
|
260
|
+
const usedNames = new Set<string>(this.toolMetaByName.keys());
|
|
261
|
+
const remapped: Record<string, unknown> = {};
|
|
262
|
+
const internalNames = new Set<string>();
|
|
263
|
+
|
|
264
|
+
for (const [toolName, toolValue] of Object.entries(tools)) {
|
|
265
|
+
const internalName = buildInternalToolName(server.id, toolName, usedNames);
|
|
266
|
+
(remapped as Record<string, unknown>)[internalName] = toolValue;
|
|
267
|
+
internalNames.add(internalName);
|
|
268
|
+
this.toolMetaByName.set(internalName, {
|
|
269
|
+
internalName,
|
|
270
|
+
serverId: server.id,
|
|
271
|
+
originalToolName: toolName,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Replace existing server registration atomically
|
|
276
|
+
this.clearServerTools(server.id);
|
|
277
|
+
this.internalNamesByServer.set(server.id, internalNames);
|
|
278
|
+
this.toolsByServer.set(server.id, remapped as ToolSet);
|
|
279
|
+
this.clientsByServer
|
|
280
|
+
.get(server.id)
|
|
281
|
+
?.close()
|
|
282
|
+
.catch(() => {});
|
|
283
|
+
this.clientsByServer.set(server.id, client);
|
|
284
|
+
client = null;
|
|
285
|
+
|
|
286
|
+
this.rebuildMergedTools();
|
|
287
|
+
this.setServerStatus({
|
|
288
|
+
id: server.id,
|
|
289
|
+
type: server.type,
|
|
290
|
+
url: server.url,
|
|
291
|
+
status: "ready",
|
|
292
|
+
toolCount: internalNames.size,
|
|
293
|
+
});
|
|
294
|
+
debug.info("mcp-server-ready", {
|
|
295
|
+
id: server.id,
|
|
296
|
+
type: server.type,
|
|
297
|
+
url: server.url,
|
|
298
|
+
toolCount: internalNames.size,
|
|
299
|
+
ms: Date.now() - startedAt,
|
|
300
|
+
});
|
|
301
|
+
} catch (error) {
|
|
302
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
303
|
+
this.clearServerTools(server.id);
|
|
304
|
+
this.rebuildMergedTools();
|
|
305
|
+
this.setServerStatus({
|
|
306
|
+
id: server.id,
|
|
307
|
+
type: server.type,
|
|
308
|
+
url: server.url,
|
|
309
|
+
status: "error",
|
|
310
|
+
toolCount: 0,
|
|
311
|
+
error: err.message,
|
|
312
|
+
});
|
|
313
|
+
debug.warn("mcp-server-error", {
|
|
314
|
+
id: server.id,
|
|
315
|
+
type: server.type,
|
|
316
|
+
url: server.url,
|
|
317
|
+
message: err.message,
|
|
318
|
+
});
|
|
319
|
+
if (client) {
|
|
320
|
+
try {
|
|
321
|
+
await client.close();
|
|
322
|
+
} catch {
|
|
323
|
+
// Ignore close failures
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let singleton: McpManager | null = null;
|
|
331
|
+
|
|
332
|
+
export function getMcpManager(): McpManager {
|
|
333
|
+
if (!singleton) {
|
|
334
|
+
singleton = new McpManager();
|
|
335
|
+
}
|
|
336
|
+
return singleton;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function startMcpManager(): void {
|
|
340
|
+
getMcpManager().start();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function destroyMcpManager(): void {
|
|
344
|
+
if (!singleton) return;
|
|
345
|
+
void singleton.closeAll();
|
|
346
|
+
singleton.removeAllListeners();
|
|
347
|
+
singleton = null;
|
|
348
|
+
}
|
|
@@ -11,6 +11,7 @@ import { getAppConfigDir } from "../../utils/preferences";
|
|
|
11
11
|
import { getMemoryModel } from "../model-config";
|
|
12
12
|
|
|
13
13
|
const MEMORY_USER_ID = "daemon_global";
|
|
14
|
+
const MAX_MEMORY_INPUT_CHARS = 10_000;
|
|
14
15
|
/** Raw memory entry from mem0 API */
|
|
15
16
|
interface Mem0RawEntry {
|
|
16
17
|
id: string;
|
|
@@ -106,6 +107,71 @@ class MemoryManager {
|
|
|
106
107
|
|
|
107
108
|
this.memory = new Memory({
|
|
108
109
|
version: "v1.1",
|
|
110
|
+
customPrompt: `You are a Personal Information Organizer, specialized in extracting **enduring** facts, user memories, and preferences.
|
|
111
|
+
Your role is to extract **only** information that would be useful to recall in a conversation two weeks from now.
|
|
112
|
+
|
|
113
|
+
# [IMPORTANT]: GENERATE FACTS SOLELY BASED ON THE USER'S MESSAGES.
|
|
114
|
+
# [IMPORTANT]: DO NOT INCLUDE INFORMATION FROM ASSISTANT OR SYSTEM MESSAGES.
|
|
115
|
+
|
|
116
|
+
### WHAT TO STORE (The "Two-Week Test"):
|
|
117
|
+
1. **Biographical Details:** Names, age, job title, company, location.
|
|
118
|
+
2. **Relationships:** Names of partners, family members, pets, or colleagues.
|
|
119
|
+
3. **Enduring Preferences:** Strong likes/dislikes (e.g., food, hobbies, style).
|
|
120
|
+
4. **Long-term Plans:** Upcoming trips, long-term projects, or goals.
|
|
121
|
+
5. **Direct Instructions:** How the user wants to be addressed or formatted (e.g., "Call me X").
|
|
122
|
+
6. **Multi-True Facts:** If multiple preferences or details can all be true (e.g., likes multiple languages, foods, hobbies), store each as a separate fact rather than updating/overwriting an existing one.
|
|
123
|
+
|
|
124
|
+
### WHAT TO IGNORE (Do NOT store these):
|
|
125
|
+
1. **Transient Commands & Questions:** Do not store that the user asked to "summarize a PDF," "translate a sentence," or "write code."
|
|
126
|
+
2. **Immediate Context:** Do not store "User said 'continue'" or "User said 'yes'."
|
|
127
|
+
3. **General Opinions on News/Politics:** Unless the user explicitly identifies with a stance, avoid summarizing general questions (e.g., ignore "What is the capital of France?").
|
|
128
|
+
4. **Meta-Commentary:** Do not store compliments or insults to the bot (e.g., "You are smart") unless it alters how you should behave.
|
|
129
|
+
|
|
130
|
+
### Examples:
|
|
131
|
+
|
|
132
|
+
User: Hi there.
|
|
133
|
+
Assistant: Hello! How can I help you today?
|
|
134
|
+
Output: {{"facts" : []}}
|
|
135
|
+
|
|
136
|
+
User: Can you summarize this article for me?
|
|
137
|
+
Assistant: Sure, please paste the text.
|
|
138
|
+
Output: {{"facts" : []}}
|
|
139
|
+
(Reasoning: This is a transient task, not a fact about the user.)
|
|
140
|
+
|
|
141
|
+
User: I am a vegetarian, so please don't suggest any meat dishes.
|
|
142
|
+
Assistant: Noted, I will provide vegetarian options only.
|
|
143
|
+
Output: {{"facts" : ["Is a vegetarian", "Does not eat meat"]}}
|
|
144
|
+
|
|
145
|
+
User: I'm planning a hiking trip to Patagonia next November.
|
|
146
|
+
Assistant: That sounds amazing!
|
|
147
|
+
Output: {{"facts" : ["Planning a hiking trip to Patagonia in November"]}}
|
|
148
|
+
|
|
149
|
+
User: Who is the president of the US?
|
|
150
|
+
Assistant: The current president is...
|
|
151
|
+
Output: {{"facts" : []}}
|
|
152
|
+
|
|
153
|
+
User: My dog's name is Buster. He's a golden retriever.
|
|
154
|
+
Assistant: Buster sounds adorable.
|
|
155
|
+
Output: {{"facts" : ["Has a dog named Buster", "Dog is a golden retriever"]}}
|
|
156
|
+
|
|
157
|
+
User: Actually, I moved. I live in Chicago now, not New York.
|
|
158
|
+
Assistant: Got it, updated your location.
|
|
159
|
+
Output: {{"facts" : ["Lives in Chicago", "No longer lives in New York"]}}
|
|
160
|
+
|
|
161
|
+
User: I hate Python, I prefer coding in Rust.
|
|
162
|
+
Assistant: Understood.
|
|
163
|
+
Output: {{"facts" : ["Dislikes Python", "Prefers coding in Rust"]}}
|
|
164
|
+
|
|
165
|
+
User: test
|
|
166
|
+
Assistant: System operational.
|
|
167
|
+
Output: {{"facts" : []}}
|
|
168
|
+
|
|
169
|
+
Return the facts in JSON format as shown above.
|
|
170
|
+
|
|
171
|
+
Rules:
|
|
172
|
+
- If no *enduring* facts are found, return an empty list for "facts".
|
|
173
|
+
- Detect the language of the user input and record facts in that same language.
|
|
174
|
+
- Write fully self-contained facts (e.g., "Lives in Chicago" instead of "Lives there").`,
|
|
109
175
|
embedder: {
|
|
110
176
|
provider: "openai",
|
|
111
177
|
config: {
|
|
@@ -200,6 +266,28 @@ class MemoryManager {
|
|
|
200
266
|
throw new Error("Memory system not available");
|
|
201
267
|
}
|
|
202
268
|
|
|
269
|
+
const sanitizedMessages = messages.map((message) => {
|
|
270
|
+
if (message.role !== "user") return message;
|
|
271
|
+
if (message.content.length <= MAX_MEMORY_INPUT_CHARS) return message;
|
|
272
|
+
return {
|
|
273
|
+
...message,
|
|
274
|
+
content: message.content.slice(0, MAX_MEMORY_INPUT_CHARS),
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (sanitizedMessages !== messages) {
|
|
279
|
+
const truncated = sanitizedMessages.some((message, index) => {
|
|
280
|
+
return message.role === "user" && messages[index]?.content.length !== message.content.length;
|
|
281
|
+
});
|
|
282
|
+
if (truncated) {
|
|
283
|
+
memoryDebug.info("memory-add-truncate", {
|
|
284
|
+
maxChars: MAX_MEMORY_INPUT_CHARS,
|
|
285
|
+
originalLengths: messages.map((message) => message.content.length),
|
|
286
|
+
truncatedLengths: sanitizedMessages.map((message) => message.content.length),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
203
291
|
const startTime = Date.now();
|
|
204
292
|
memoryDebug.info("memory-add-input", {
|
|
205
293
|
infer,
|
|
@@ -207,7 +295,7 @@ class MemoryManager {
|
|
|
207
295
|
messages,
|
|
208
296
|
});
|
|
209
297
|
|
|
210
|
-
const result = (await this.memory.add(
|
|
298
|
+
const result = (await this.memory.add(sanitizedMessages, {
|
|
211
299
|
userId: MEMORY_USER_ID,
|
|
212
300
|
metadata,
|
|
213
301
|
infer,
|
|
@@ -236,7 +324,7 @@ class MemoryManager {
|
|
|
236
324
|
rawResults: result.results,
|
|
237
325
|
durationMs,
|
|
238
326
|
});
|
|
239
|
-
return
|
|
327
|
+
return { results: extracted };
|
|
240
328
|
}
|
|
241
329
|
|
|
242
330
|
/** Get all memories */
|
package/src/ai/model-config.ts
CHANGED
|
@@ -100,7 +100,7 @@ export function buildOpenRouterChatSettings(
|
|
|
100
100
|
export const TRANSCRIPTION_MODEL = "gpt-4o-mini-transcribe-2025-12-15";
|
|
101
101
|
|
|
102
102
|
// Default model for memory operations (cheap & fast)
|
|
103
|
-
export const DEFAULT_MEMORY_MODEL = "
|
|
103
|
+
export const DEFAULT_MEMORY_MODEL = "x-ai/grok-4.1-fast";
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* Get the model ID for memory operations (deduplication, extraction).
|