@makefinks/daemon 0.9.0 → 0.10.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/package.json CHANGED
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "module": "src/index.tsx",
30
30
  "type": "module",
31
- "version": "0.9.0",
31
+ "version": "0.10.0",
32
32
  "bin": {
33
33
  "daemon": "dist/cli.js"
34
34
  },
@@ -77,13 +77,21 @@
77
77
  "@ai-sdk/openai": "^3.0.0",
78
78
  "@openrouter/ai-sdk-provider": "^2.1.0",
79
79
  "@opentui-ui/toast": "^0.0.3",
80
- "@opentui/core": "^0.1.63",
81
- "@opentui/react": "^0.1.63",
80
+ "@opentui/core": "0.1.75",
81
+ "@opentui/react": "0.1.75",
82
82
  "ai": "^6.0.0",
83
83
  "exa-js": "^2.0.12",
84
84
  "mem0ai": "^2.2.1",
85
85
  "openai": "^6.16.0",
86
86
  "opentui-spinner": "^0.0.6",
87
87
  "react": "^19.2.3"
88
+ },
89
+ "optionalDependencies": {
90
+ "@opentui/core-win32-x64": "0.1.75",
91
+ "@opentui/core-win32-arm64": "0.1.75",
92
+ "@opentui/core-darwin-x64": "0.1.75",
93
+ "@opentui/core-darwin-arm64": "0.1.75",
94
+ "@opentui/core-linux-x64": "0.1.75",
95
+ "@opentui/core-linux-arm64": "0.1.75"
88
96
  }
89
97
  }
@@ -15,14 +15,14 @@ import {
15
15
  import { getDaemonManager } from "../state/daemon-state";
16
16
  import { getRuntimeContext } from "../state/runtime-context";
17
17
  import type {
18
+ MemoryToastOperation,
19
+ MemoryToastPreview,
18
20
  ReasoningEffort,
19
21
  StreamCallbacks,
20
22
  TokenUsage,
21
23
  ToolApprovalRequest,
22
24
  ToolApprovalResponse,
23
25
  TranscriptionResult,
24
- MemoryToastPreview,
25
- MemoryToastOperation,
26
26
  } from "../types";
27
27
  import { debug, toolDebug } from "../utils/debug-logger";
28
28
  import { getOpenRouterReportedCost } from "../utils/openrouter-reported-cost";
@@ -321,7 +321,7 @@ export async function generateResponse(
321
321
  async function persistConversationMemory(
322
322
  userMessage: string,
323
323
  assistantMessage: string
324
- ): Promise<string | null> {
324
+ ): Promise<MemoryToastPreview | null> {
325
325
  const userTextForMemory = userMessage.trim();
326
326
  const assistantTextForMemory = assistantMessage.trim();
327
327
 
@@ -302,9 +302,13 @@ Rules:
302
302
  })) as Mem0RawAddResult;
303
303
 
304
304
  const extracted = result.results.map((r) => {
305
- const event =
305
+ const rawEvent =
306
306
  (r as unknown as { metadata?: { event?: string } }).metadata?.event ??
307
307
  (r as { event?: string }).event;
308
+ const validEvents = ["ADD", "UPDATE", "DELETE", "NONE"] as const;
309
+ const event = validEvents.includes(rawEvent as (typeof validEvents)[number])
310
+ ? (rawEvent as (typeof validEvents)[number])
311
+ : "NONE";
308
312
  return {
309
313
  id: r.id,
310
314
  memory: r.memory,
@@ -9,14 +9,14 @@ import { loadManualConfig } from "../utils/config";
9
9
  // Available models for selection (OpenRouter format)
10
10
  export const AVAILABLE_MODELS: ModelOption[] = [
11
11
  { id: "x-ai/grok-4.1-fast", name: "Grok 4.1 Fast" },
12
+ { id: "arcee-ai/trinity-large-preview:free", name: "Trinity Large Preview" },
12
13
  { id: "z-ai/glm-4.7", name: "GLM 4.7" },
13
14
  { id: "minimax/minimax-m2.1", name: "Minimax M2.1" },
14
15
  { id: "google/gemini-3-flash-preview", name: "Gemini 3 Flash" },
15
16
  { id: "google/gemini-3-pro-preview", name: "Gemini 3 Pro" },
16
17
  { id: "openai/gpt-5.2", name: "GPT 5.2" },
17
- { id: "moonshotai/kimi-k2-thinking", name: "Kimi K2 Thinking" },
18
+ { id: "moonshotai/kimi-k2.5", name: "Kimi K2.5" },
18
19
  { id: "openai/gpt-oss-120b:exacto", name: "GPT-OSS-120" },
19
- { id: "mistralai/devstral-2512:free", name: "Mistral Devstral" },
20
20
  { id: "nvidia/nemotron-3-nano-30b-a3b:free", name: "Nemotron 3 Nano" },
21
21
  ];
22
22
 
@@ -9,6 +9,7 @@ export type InteractionMode = "text" | "voice";
9
9
 
10
10
  export interface ToolAvailability {
11
11
  readFile: boolean;
12
+ writeFile: boolean;
12
13
  runBash: boolean;
13
14
  webSearch: boolean;
14
15
  fetchUrls: boolean;
@@ -64,6 +65,7 @@ export function buildDaemonSystemPrompt(options: SystemPromptOptions = {}): stri
64
65
  function normalizeToolAvailability(toolAvailability?: Partial<ToolAvailability>): ToolAvailability {
65
66
  return {
66
67
  readFile: toolAvailability?.readFile ?? true,
68
+ writeFile: toolAvailability?.writeFile ?? true,
67
69
  runBash: toolAvailability?.runBash ?? true,
68
70
  webSearch: toolAvailability?.webSearch ?? true,
69
71
  fetchUrls: toolAvailability?.fetchUrls ?? true,
@@ -243,6 +245,19 @@ Fetch multiple URLs in one call:
243
245
  By default it reads up to 2000 lines from the start when no offset/limit are provided.
244
246
  For partial reads, you must provide both a 0-based line offset and a line limit.
245
247
  `,
248
+ writeFile: `
249
+ ### 'writeFile' (local file writer)
250
+ Use this to write content to files. Creates new files or overwrites existing ones.
251
+ Automatically creates parent directories if they don't exist.
252
+
253
+ **CRITICAL: Always report the correct file location to the user**
254
+ - When you write a file, explicitly tell the user the full path where it was saved
255
+ - If the file is in the workspace, say "I have saved it to my workspace at: [full path]"
256
+ - If the file is in the current working directory, say "I have saved it to: [path]"
257
+ - Do NOT give commands like "cat filename" or "open filename" unless the file is actually in the current working directory
258
+ - For files in the workspace, give the full path: "cat /full/path/to/file" or tell the user to navigate there first
259
+ `,
260
+
246
261
  subagent: `
247
262
  ### 'subagent'
248
263
  Call this tool to spawn subagents for specific tasks.
@@ -260,6 +275,7 @@ function buildToolDefinitions(availability: ToolAvailability): string {
260
275
  if (availability.groundingManager) blocks.push(TOOL_SECTIONS.groundingManager);
261
276
  if (availability.runBash) blocks.push(TOOL_SECTIONS.runBash);
262
277
  if (availability.readFile) blocks.push(TOOL_SECTIONS.readFile);
278
+ if (availability.writeFile) blocks.push(TOOL_SECTIONS.writeFile);
263
279
  if (availability.subagent) blocks.push(TOOL_SECTIONS.subagent);
264
280
 
265
281
  const webNote =
@@ -63,15 +63,28 @@ export const fetchUrls = tool({
63
63
  return `<fetchUrls error="${escapeXmlAttribute(exaClientResult.error)}" />`;
64
64
  }
65
65
 
66
- const normalizedRequests = requests.map((request) => {
67
- const hasLineOffset = typeof request.lineOffset === "number";
68
- const hasLineLimit = typeof request.lineLimit === "number";
69
- const invalidPagination = hasLineOffset && !hasLineLimit && (request.lineOffset ?? 0) > 0;
66
+ interface NormalizedRequest {
67
+ url: string;
68
+ lineOffset?: number;
69
+ lineLimit?: number;
70
+ invalidPagination: boolean;
71
+ effectiveLineOffset: number;
72
+ effectiveLineLimit: number;
73
+ }
74
+
75
+ const normalizedRequests = requests.map((request): NormalizedRequest => {
76
+ const lineOffset = request.lineOffset;
77
+ const lineLimit = request.lineLimit;
78
+ const hasLineOffset = typeof lineOffset === "number";
79
+ const hasLineLimit = typeof lineLimit === "number";
80
+ const invalidPagination = hasLineOffset && !hasLineLimit && (lineOffset ?? 0) > 0;
70
81
  return {
71
- ...request,
82
+ url: request.url,
83
+ lineOffset,
84
+ lineLimit,
72
85
  invalidPagination,
73
- effectiveLineOffset: hasLineOffset ? request.lineOffset : 0,
74
- effectiveLineLimit: hasLineLimit ? request.lineLimit : DEFAULT_LINE_LIMIT,
86
+ effectiveLineOffset: hasLineOffset ? lineOffset : 0,
87
+ effectiveLineLimit: hasLineLimit ? lineLimit : DEFAULT_LINE_LIMIT,
75
88
  };
76
89
  });
77
90
 
@@ -8,6 +8,7 @@ import { runBash } from "./run-bash";
8
8
  import { subagent } from "./subagents";
9
9
  import { todoManager } from "./todo-manager";
10
10
  import { webSearch } from "./web-search";
11
+ import { writeFile } from "./write-file";
11
12
 
12
13
  import type { ToolToggleId, ToolToggles } from "../../types";
13
14
  import { detectLocalPlaywrightChromium } from "../../utils/js-rendering";
@@ -40,6 +41,7 @@ type ToolGateResult = {
40
41
 
41
42
  const TOOL_REGISTRY: ToolEntry[] = [
42
43
  { id: "readFile", toggleKey: "readFile", tool: readFile },
44
+ { id: "writeFile", toggleKey: "writeFile", tool: writeFile },
43
45
  { id: "runBash", toggleKey: "runBash", tool: runBash },
44
46
  { id: "webSearch", toggleKey: "webSearch", tool: webSearch, gate: gateExa },
45
47
  { id: "fetchUrls", toggleKey: "fetchUrls", tool: fetchUrls, gate: gateExa },
@@ -68,6 +70,7 @@ async function gateRenderUrl(): Promise<ToolGateResult> {
68
70
  function normalizeToggles(toggles?: ToolToggles): ToolToggles {
69
71
  return {
70
72
  readFile: toggles?.readFile ?? true,
73
+ writeFile: toggles?.writeFile ?? true,
71
74
  runBash: toggles?.runBash ?? true,
72
75
  webSearch: toggles?.webSearch ?? true,
73
76
  fetchUrls: toggles?.fetchUrls ?? true,
@@ -166,6 +169,7 @@ export async function buildToolSet(
166
169
  export function getToolLabels(): Record<ToolId, string> {
167
170
  return {
168
171
  readFile: "readFile",
172
+ writeFile: "writeFile",
169
173
  runBash: "runBash",
170
174
  webSearch: "webSearch",
171
175
  fetchUrls: "fetchUrls",
@@ -179,6 +183,7 @@ export function getToolLabels(): Record<ToolId, string> {
179
183
  export function getDefaultToolOrder(): ToolId[] {
180
184
  return [
181
185
  "readFile",
186
+ "writeFile",
182
187
  "runBash",
183
188
  "webSearch",
184
189
  "fetchUrls",
@@ -192,6 +197,7 @@ export function getDefaultToolOrder(): ToolId[] {
192
197
  export function createToolAvailabilitySnapshot(availability: ToolAvailabilityMap): Record<ToolId, boolean> {
193
198
  return {
194
199
  readFile: availability.readFile?.enabled ?? false,
200
+ writeFile: availability.writeFile?.enabled ?? false,
195
201
  runBash: availability.runBash?.enabled ?? false,
196
202
  webSearch: availability.webSearch?.enabled ?? false,
197
203
  fetchUrls: availability.fetchUrls?.enabled ?? false,
@@ -0,0 +1,51 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { tool } from "ai";
4
+ import { z } from "zod";
5
+
6
+ export const writeFile = tool({
7
+ description:
8
+ "Write content to a file. Creates the file if it doesn't exist, or overwrites it if it does. Supports append mode to add content to existing files. Use this to create scripts, save outputs, write configuration files, or generate any text-based file.",
9
+ inputSchema: z.object({
10
+ path: z
11
+ .string()
12
+ .describe("Path to the file to write. Can be absolute or relative to the current working directory."),
13
+ content: z.string().describe("The content to write to the file."),
14
+ append: z
15
+ .boolean()
16
+ .optional()
17
+ .default(false)
18
+ .describe("If true, append to the file instead of overwriting. Creates the file if it doesn't exist."),
19
+ }),
20
+ execute: async ({ path: filePath, content, append }) => {
21
+ try {
22
+ const resolvedPath = path.resolve(filePath);
23
+ const dir = path.dirname(resolvedPath);
24
+
25
+ // Create parent directories if they don't exist
26
+ if (!fs.existsSync(dir)) {
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ }
29
+
30
+ // Write or append to the file
31
+ if (append) {
32
+ fs.appendFileSync(resolvedPath, content, "utf8");
33
+ } else {
34
+ fs.writeFileSync(resolvedPath, content, "utf8");
35
+ }
36
+
37
+ return {
38
+ success: true,
39
+ path: resolvedPath,
40
+ bytesWritten: Buffer.byteLength(content, "utf8"),
41
+ };
42
+ } catch (error: unknown) {
43
+ const err = error instanceof Error ? error : new Error(String(error));
44
+ return {
45
+ success: false,
46
+ path: filePath,
47
+ error: err.message,
48
+ };
49
+ }
50
+ },
51
+ });
@@ -2,6 +2,7 @@ import "./bash";
2
2
  import "./web-search";
3
3
  import "./url-tools";
4
4
  import "./read-file";
5
+ import "./write-file.tsx";
5
6
  import "./subagent";
6
7
  import "./todo";
7
8
  import "./system-info";
@@ -1,6 +1,7 @@
1
- import type { ToolLayoutConfig, ToolHeader } from "../types";
2
1
  import { COLORS } from "../../../ui/constants";
3
2
  import { registerToolLayout } from "../registry";
3
+ import type { ToolBody } from "../types";
4
+ import type { ToolHeader, ToolLayoutConfig } from "../types";
4
5
 
5
6
  type UnknownRecord = Record<string, unknown>;
6
7
 
@@ -106,6 +107,7 @@ type ExaLikeItem = {
106
107
  remainingLines?: unknown;
107
108
  totalLines?: unknown;
108
109
  error?: unknown;
110
+ success?: unknown;
109
111
  };
110
112
 
111
113
  function formatExaItemLabel(item: ExaLikeItem): string {
@@ -0,0 +1,117 @@
1
+ import { pathToFiletype } from "@opentui/core";
2
+ import React from "react";
3
+ import { COLORS, REASONING_MARKDOWN_STYLE } from "../../../ui/constants";
4
+ import { registerToolLayout } from "../registry";
5
+ import type { ToolHeader, ToolLayoutConfig, ToolLayoutRenderProps } from "../types";
6
+
7
+ type UnknownRecord = Record<string, unknown>;
8
+
9
+ function isRecord(value: unknown): value is UnknownRecord {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+
13
+ function extractPath(input: unknown): string | null {
14
+ if (!isRecord(input)) return null;
15
+ if ("path" in input && typeof input.path === "string") {
16
+ return input.path;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function extractContent(input: unknown): string | null {
22
+ if (!isRecord(input)) return null;
23
+ if ("content" in input && typeof input.content === "string") {
24
+ return input.content;
25
+ }
26
+ return null;
27
+ }
28
+
29
+ function extractAppend(input: unknown): boolean {
30
+ if (!isRecord(input)) return false;
31
+ if ("append" in input && typeof input.append === "boolean") {
32
+ return input.append;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ function WriteFileBody({ call, result }: ToolLayoutRenderProps) {
38
+ if (!isRecord(result)) return null;
39
+ if (result.success === false && typeof result.error === "string") {
40
+ return (
41
+ <box paddingLeft={2}>
42
+ <text>
43
+ <span fg={COLORS.STATUS_FAILED}>{`error: ${result.error}`}</span>
44
+ </text>
45
+ </box>
46
+ );
47
+ }
48
+ if (result.success !== true) return null;
49
+
50
+ const content = extractContent(call.input) ?? "";
51
+ const path = extractPath(call.input) ?? "";
52
+
53
+ // Detect filetype from path for syntax highlighting
54
+ const filetype = pathToFiletype(path);
55
+
56
+ // Format content preview
57
+ let previewContent = "";
58
+ if (content.trim()) {
59
+ const MAX_LINES = 4;
60
+ const MAX_CHARS = 160;
61
+ const contentLines = content
62
+ .split("\n")
63
+ .slice(0, MAX_LINES)
64
+ .map((line) => (line.length > MAX_CHARS ? `${line.slice(0, MAX_CHARS - 1)}…` : line));
65
+
66
+ const totalLines = content.split("\n").length;
67
+ if (totalLines > MAX_LINES) {
68
+ contentLines.push(`... (${totalLines - MAX_LINES} more lines)`);
69
+ }
70
+ previewContent = contentLines.join("\n");
71
+ } else {
72
+ previewContent = "(empty file)";
73
+ }
74
+
75
+ return (
76
+ <box flexDirection="column" paddingLeft={2} marginTop={0}>
77
+ <box
78
+ borderStyle="single"
79
+ borderColor={COLORS.TOOL_INPUT_BORDER}
80
+ paddingLeft={1}
81
+ paddingRight={1}
82
+ paddingTop={0}
83
+ paddingBottom={0}
84
+ >
85
+ <code
86
+ content={previewContent}
87
+ filetype={filetype}
88
+ syntaxStyle={REASONING_MARKDOWN_STYLE}
89
+ conceal={true}
90
+ drawUnstyledText={false}
91
+ />
92
+ </box>
93
+ </box>
94
+ );
95
+ }
96
+
97
+ export const writeFileLayout: ToolLayoutConfig = {
98
+ abbreviation: "write",
99
+
100
+ getHeader: (input): ToolHeader | null => {
101
+ const path = extractPath(input);
102
+ if (!path) return null;
103
+ const append = extractAppend(input);
104
+ const filetype = pathToFiletype(path);
105
+
106
+ const parts: string[] = [];
107
+ if (filetype) parts.push(filetype);
108
+ if (append) parts.push("append");
109
+
110
+ const secondary = parts.length > 0 ? parts.join(" · ") : undefined;
111
+ return { primary: path, secondary, secondaryStyle: "dim" };
112
+ },
113
+
114
+ renderBody: WriteFileBody,
115
+ };
116
+
117
+ registerToolLayout("writeFile", writeFileLayout);
@@ -269,6 +269,7 @@ export type VoiceInteractionType = "direct" | "review";
269
269
 
270
270
  export type ToolToggleId =
271
271
  | "readFile"
272
+ | "writeFile"
272
273
  | "runBash"
273
274
  | "webSearch"
274
275
  | "fetchUrls"
@@ -281,6 +282,7 @@ export type ToolToggles = Record<ToolToggleId, boolean>;
281
282
 
282
283
  export const DEFAULT_TOOL_TOGGLES: ToolToggles = {
283
284
  readFile: true,
285
+ writeFile: true,
284
286
  runBash: true,
285
287
  webSearch: true,
286
288
  fetchUrls: true,
@@ -53,6 +53,7 @@ function parseFetchUrlsXml(result: string): {
53
53
  const attrs: Record<string, string> = {};
54
54
  for (const attr of attrText.matchAll(/(\w+)="([^"]*)"/g)) {
55
55
  const key = attr[1];
56
+ if (!key) continue;
56
57
  const value = attr[2] ?? "";
57
58
  attrs[key] = unescapeXmlAttribute(value);
58
59
  }
@@ -65,6 +65,10 @@ type ExaLikeItem = {
65
65
  text?: unknown;
66
66
  lineOffset?: unknown;
67
67
  lineLimit?: unknown;
68
+ success?: unknown;
69
+ error?: unknown;
70
+ remainingLines?: unknown;
71
+ totalLines?: unknown;
68
72
  };
69
73
 
70
74
  function extractExaItems(data: unknown): ExaLikeItem[] | null {
@@ -146,8 +150,9 @@ function formatExaFetchResult(result: unknown): string | null {
146
150
  if (headerLine) lines.push(headerLine);
147
151
  if (normalLines[0]) lines.push(normalLines[0]);
148
152
 
149
- if (errorLines.length > 0) {
150
- lines.push(errorLines[0]);
153
+ const firstErrorLine = errorLines[0];
154
+ if (firstErrorLine) {
155
+ lines.push(firstErrorLine);
151
156
  } else if (contentLine) {
152
157
  lines.push(contentLine);
153
158
  }