@makefinks/daemon 0.7.2 → 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.
@@ -42,6 +42,7 @@ class DaemonStateManager {
42
42
  private _reasoningEffort: ReasoningEffort = "medium";
43
43
  private _bashApprovalLevel: BashApprovalLevel = "dangerous";
44
44
  private _toolToggles: ToolToggles = { ...DEFAULT_TOOL_TOGGLES };
45
+ private _memoryEnabled = true;
45
46
  private _outputDeviceName: string | undefined = undefined;
46
47
  private _turnId = 0;
47
48
  private speechRunId = 0;
@@ -147,6 +148,14 @@ class DaemonStateManager {
147
148
  this._toolToggles = toggles;
148
149
  }
149
150
 
151
+ get memoryEnabled(): boolean {
152
+ return this._memoryEnabled;
153
+ }
154
+
155
+ set memoryEnabled(enabled: boolean) {
156
+ this._memoryEnabled = enabled;
157
+ }
158
+
150
159
  get outputDeviceName(): string | undefined {
151
160
  return this._outputDeviceName;
152
161
  }
@@ -317,6 +326,7 @@ class DaemonStateManager {
317
326
  this.emitEvent("responseToken", token);
318
327
  },
319
328
  onStepUsage: (usage) => this.emitEvent("stepUsage", usage),
329
+ onMemorySaved: (preview) => this.emitEvent("memorySaved", preview),
320
330
  }
321
331
  );
322
332
 
@@ -169,6 +169,7 @@ export interface StreamCallbacks {
169
169
  onSubagentToolResult?: (toolCallId: string, toolName: string, success: boolean) => void;
170
170
  onSubagentComplete?: (toolCallId: string, success: boolean) => void;
171
171
  onStepUsage?: (usage: TokenUsage) => void;
172
+ onMemorySaved?: (preview: MemoryToastPreview) => void;
172
173
  onComplete?: (
173
174
  fullText: string,
174
175
  responseMessages: ModelMessage[],
@@ -179,6 +180,13 @@ export interface StreamCallbacks {
179
180
  onError?: (error: Error) => void;
180
181
  }
181
182
 
183
+ export type MemoryToastOperation = "ADD" | "UPDATE" | "ADD/UPDATE";
184
+
185
+ export interface MemoryToastPreview {
186
+ operation: MemoryToastOperation;
187
+ description: string;
188
+ }
189
+
182
190
  /**
183
191
  * Audio device information
184
192
  */
@@ -312,6 +320,8 @@ export interface AppPreferences {
312
320
  showFullReasoning?: boolean;
313
321
  /** Show tool output previews */
314
322
  showToolOutput?: boolean;
323
+ /** Enable memory injection + auto-write */
324
+ memoryEnabled?: boolean;
315
325
  /** Bash command approval level */
316
326
  bashApprovalLevel?: BashApprovalLevel;
317
327
  /** Tool toggles (on/off) */
@@ -4,8 +4,20 @@ import { getAppConfigDir } from "./preferences";
4
4
 
5
5
  const CONFIG_FILE = "config.json";
6
6
 
7
+ export type McpTransportType = "http" | "sse";
8
+
9
+ export interface McpServerConfig {
10
+ /** Optional stable id. If omitted, derived from URL. */
11
+ id?: string;
12
+ /** Transport type for the MCP server. */
13
+ type: McpTransportType;
14
+ /** MCP endpoint URL. */
15
+ url: string;
16
+ }
17
+
7
18
  export interface ManualConfig {
8
19
  memoryModel?: string;
20
+ mcpServers?: McpServerConfig[];
9
21
  }
10
22
 
11
23
  let cachedConfig: ManualConfig | null = null;
@@ -17,6 +29,10 @@ function getConfigPath(): string {
17
29
  return path.join(getAppConfigDir(), CONFIG_FILE);
18
30
  }
19
31
 
32
+ export function getManualConfigPath(): string {
33
+ return getConfigPath();
34
+ }
35
+
20
36
  export function loadManualConfig(): ManualConfig {
21
37
  const now = Date.now();
22
38
 
@@ -56,6 +72,23 @@ function parseManualConfig(raw: Record<string, unknown>): ManualConfig {
56
72
  config.memoryModel = raw.memoryModel.trim();
57
73
  }
58
74
 
75
+ if (Array.isArray(raw.mcpServers)) {
76
+ const servers: McpServerConfig[] = [];
77
+ for (const entry of raw.mcpServers) {
78
+ if (typeof entry !== "object" || entry === null) continue;
79
+ const obj = entry as Record<string, unknown>;
80
+ const type = obj.type;
81
+ const url = obj.url;
82
+ if (type !== "http" && type !== "sse") continue;
83
+ if (typeof url !== "string" || url.trim().length === 0) continue;
84
+ const id = typeof obj.id === "string" && obj.id.trim().length > 0 ? obj.id.trim() : undefined;
85
+ servers.push({ id, type, url: url.trim() });
86
+ }
87
+ if (servers.length > 0) {
88
+ config.mcpServers = servers;
89
+ }
90
+ }
91
+
59
92
  return config;
60
93
  }
61
94
 
@@ -109,6 +109,9 @@ export function parsePreferences(raw: unknown): AppPreferences | null {
109
109
  if (typeof raw.showToolOutput === "boolean") {
110
110
  prefs.showToolOutput = raw.showToolOutput;
111
111
  }
112
+ if (typeof raw.memoryEnabled === "boolean") {
113
+ prefs.memoryEnabled = raw.memoryEnabled;
114
+ }
112
115
  if (isRecord(raw.toolToggles)) {
113
116
  const record = raw.toolToggles;
114
117
  const next: Record<string, boolean> = {};
@@ -194,6 +194,66 @@ function formatReadFileResult(result: unknown): string | null {
194
194
  return `${path}${range}:\n${content}`;
195
195
  }
196
196
 
197
+ function tryStringify(value: unknown): string {
198
+ if (typeof value === "string") return value;
199
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
200
+ return String(value);
201
+ if (value === null) return "null";
202
+ if (value === undefined) return "undefined";
203
+ try {
204
+ return JSON.stringify(value, null, 2);
205
+ } catch {
206
+ return String(value);
207
+ }
208
+ }
209
+
210
+ type McpContentLike = {
211
+ type?: unknown;
212
+ text?: unknown;
213
+ content?: unknown;
214
+ data?: unknown;
215
+ };
216
+
217
+ function extractMcpContentText(item: unknown): string {
218
+ if (!isRecord(item)) return "";
219
+ const it = item as McpContentLike;
220
+ if (typeof it.text === "string") return it.text;
221
+ if (typeof it.content === "string") return it.content;
222
+ if (typeof it.type === "string" && it.type === "text" && typeof it.data === "string") return it.data;
223
+ return "";
224
+ }
225
+
226
+ function formatMcpLikeResult(result: unknown): string | null {
227
+ if (!isRecord(result)) return null;
228
+
229
+ const structuredContent =
230
+ "structuredContent" in result ? (result as UnknownRecord).structuredContent : undefined;
231
+ if (structuredContent !== undefined) {
232
+ const raw = tryStringify(structuredContent);
233
+ return raw.trim().length > 0 ? raw : null;
234
+ }
235
+
236
+ const content = "content" in result ? (result as UnknownRecord).content : undefined;
237
+ if (Array.isArray(content)) {
238
+ const pieces = content
239
+ .map(extractMcpContentText)
240
+ .map((t) => t.trim())
241
+ .filter((t) => t.length > 0);
242
+ const joined = pieces.join("\n");
243
+ if (joined.trim().length > 0) {
244
+ const isError = (result as UnknownRecord).isError === true;
245
+ if (isError && !joined.toLowerCase().startsWith("error:")) {
246
+ return `error: ${joined}`;
247
+ }
248
+ return joined;
249
+ }
250
+ }
251
+
252
+ // Fallback: show the raw container (truncated downstream)
253
+ const fallback = tryStringify(result);
254
+ return fallback.trim().length > 0 ? fallback : null;
255
+ }
256
+
197
257
  /**
198
258
  * Format a very small, terminal-safe preview of a tool result.
199
259
  * Intended for the tool call UI box (not full logs).
@@ -248,3 +308,47 @@ export function formatToolOutputPreview(toolName: string, result: unknown): stri
248
308
 
249
309
  return outputLines.length > 0 ? outputLines : null;
250
310
  }
311
+
312
+ /**
313
+ * Generic preview formatter for dynamic tools (e.g. MCP).
314
+ */
315
+ export function formatGenericToolOutputPreview(result: unknown): string[] | null {
316
+ if (result === undefined) return null;
317
+
318
+ const raw = formatMcpLikeResult(result) ?? tryStringify(result);
319
+ if (!raw.trim()) return ["(no output)"];
320
+
321
+ const MAX_LINES = 4;
322
+ const MAX_CHARS_PER_LINE = 160;
323
+ const MAX_TOTAL_CHARS = 260;
324
+
325
+ const { lines, truncated: lineTruncated } = splitPreviewLines(raw, MAX_LINES);
326
+
327
+ let usedChars = 0;
328
+ const outputLines: string[] = [];
329
+ let anyTruncated = lineTruncated;
330
+
331
+ for (const line of lines) {
332
+ const remaining = Math.max(0, MAX_TOTAL_CHARS - usedChars);
333
+ if (remaining <= 0) {
334
+ anyTruncated = true;
335
+ break;
336
+ }
337
+ const { text: truncatedLine, truncated } = truncateText(line, Math.min(MAX_CHARS_PER_LINE, remaining));
338
+ anyTruncated = anyTruncated || truncated;
339
+ outputLines.push(truncatedLine);
340
+ usedChars += truncatedLine.length;
341
+ }
342
+
343
+ if (outputLines.length === 0) return ["(no output)"];
344
+
345
+ if (anyTruncated && outputLines.length > 0) {
346
+ const last = outputLines[outputLines.length - 1] ?? "";
347
+ if (!last.endsWith("…")) {
348
+ const { text } = truncateText(last, Math.max(1, last.length - 1));
349
+ outputLines[outputLines.length - 1] = `${text}…`.replace(/……$/, "…");
350
+ }
351
+ }
352
+
353
+ return outputLines;
354
+ }