@makefinks/daemon 0.1.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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/dist/cli.js +22 -0
  4. package/package.json +79 -0
  5. package/src/ai/agent-turn-runner.ts +130 -0
  6. package/src/ai/daemon-ai.ts +403 -0
  7. package/src/ai/exa-client.ts +21 -0
  8. package/src/ai/exa-fetch-cache.ts +104 -0
  9. package/src/ai/model-config.ts +99 -0
  10. package/src/ai/sanitize-messages.ts +83 -0
  11. package/src/ai/system-prompt.ts +363 -0
  12. package/src/ai/tools/fetch-urls.ts +187 -0
  13. package/src/ai/tools/grounding-manager.ts +94 -0
  14. package/src/ai/tools/index.ts +52 -0
  15. package/src/ai/tools/read-file.ts +100 -0
  16. package/src/ai/tools/render-url.ts +275 -0
  17. package/src/ai/tools/run-bash.ts +224 -0
  18. package/src/ai/tools/subagents.ts +195 -0
  19. package/src/ai/tools/todo-manager.ts +150 -0
  20. package/src/ai/tools/web-search.ts +91 -0
  21. package/src/app/App.tsx +711 -0
  22. package/src/app/components/AppOverlays.tsx +131 -0
  23. package/src/app/components/AvatarLayer.tsx +51 -0
  24. package/src/app/components/ConversationPane.tsx +476 -0
  25. package/src/avatar/DaemonAvatarRenderable.ts +343 -0
  26. package/src/avatar/daemon-avatar-rig.ts +1165 -0
  27. package/src/avatar-preview.ts +186 -0
  28. package/src/cli.ts +26 -0
  29. package/src/components/ApiKeyInput.tsx +99 -0
  30. package/src/components/ApiKeyStep.tsx +95 -0
  31. package/src/components/ApprovalPicker.tsx +109 -0
  32. package/src/components/ContentBlockView.tsx +141 -0
  33. package/src/components/DaemonText.tsx +34 -0
  34. package/src/components/DeviceMenu.tsx +166 -0
  35. package/src/components/GroundingBadge.tsx +21 -0
  36. package/src/components/GroundingMenu.tsx +310 -0
  37. package/src/components/HotkeysPane.tsx +115 -0
  38. package/src/components/InlineStatusIndicator.tsx +106 -0
  39. package/src/components/ModelMenu.tsx +411 -0
  40. package/src/components/OnboardingOverlay.tsx +446 -0
  41. package/src/components/ProviderMenu.tsx +177 -0
  42. package/src/components/SessionMenu.tsx +297 -0
  43. package/src/components/SettingsMenu.tsx +291 -0
  44. package/src/components/StatusBar.tsx +126 -0
  45. package/src/components/TokenUsageDisplay.tsx +92 -0
  46. package/src/components/ToolCallView.tsx +113 -0
  47. package/src/components/TypingInputBar.tsx +131 -0
  48. package/src/components/tool-layouts/components.tsx +120 -0
  49. package/src/components/tool-layouts/defaults.ts +9 -0
  50. package/src/components/tool-layouts/index.ts +22 -0
  51. package/src/components/tool-layouts/layouts/bash.ts +110 -0
  52. package/src/components/tool-layouts/layouts/grounding.tsx +98 -0
  53. package/src/components/tool-layouts/layouts/index.ts +8 -0
  54. package/src/components/tool-layouts/layouts/read-file.ts +59 -0
  55. package/src/components/tool-layouts/layouts/subagent.tsx +118 -0
  56. package/src/components/tool-layouts/layouts/system-info.ts +8 -0
  57. package/src/components/tool-layouts/layouts/todo.tsx +139 -0
  58. package/src/components/tool-layouts/layouts/url-tools.ts +220 -0
  59. package/src/components/tool-layouts/layouts/web-search.ts +110 -0
  60. package/src/components/tool-layouts/registry.ts +17 -0
  61. package/src/components/tool-layouts/types.ts +94 -0
  62. package/src/hooks/daemon-event-handlers.ts +944 -0
  63. package/src/hooks/keyboard-handlers.ts +399 -0
  64. package/src/hooks/menu-navigation.ts +147 -0
  65. package/src/hooks/use-app-audio-devices-loader.ts +71 -0
  66. package/src/hooks/use-app-callbacks.ts +202 -0
  67. package/src/hooks/use-app-context-builder.ts +159 -0
  68. package/src/hooks/use-app-display-state.ts +162 -0
  69. package/src/hooks/use-app-menus.ts +51 -0
  70. package/src/hooks/use-app-model-pricing-loader.ts +45 -0
  71. package/src/hooks/use-app-model.ts +123 -0
  72. package/src/hooks/use-app-openrouter-models-loader.ts +44 -0
  73. package/src/hooks/use-app-openrouter-provider-loader.ts +35 -0
  74. package/src/hooks/use-app-preferences-bootstrap.ts +212 -0
  75. package/src/hooks/use-app-sessions.ts +105 -0
  76. package/src/hooks/use-app-settings.ts +62 -0
  77. package/src/hooks/use-conversation-manager.ts +163 -0
  78. package/src/hooks/use-copy-on-select.ts +50 -0
  79. package/src/hooks/use-daemon-events.ts +396 -0
  80. package/src/hooks/use-daemon-keyboard.ts +397 -0
  81. package/src/hooks/use-grounding.ts +46 -0
  82. package/src/hooks/use-input-history.ts +92 -0
  83. package/src/hooks/use-menu-keyboard.ts +93 -0
  84. package/src/hooks/use-playwright-notification.ts +23 -0
  85. package/src/hooks/use-reasoning-animation.ts +97 -0
  86. package/src/hooks/use-response-timer.ts +55 -0
  87. package/src/hooks/use-tool-approval.tsx +202 -0
  88. package/src/hooks/use-typing-mode.ts +137 -0
  89. package/src/hooks/use-voice-dependencies-notification.ts +37 -0
  90. package/src/index.tsx +48 -0
  91. package/src/scripts/setup-browsers.ts +42 -0
  92. package/src/state/app-context.tsx +160 -0
  93. package/src/state/daemon-events.ts +67 -0
  94. package/src/state/daemon-state.ts +493 -0
  95. package/src/state/migrations/001-init.ts +33 -0
  96. package/src/state/migrations/index.ts +8 -0
  97. package/src/state/model-history-store.ts +45 -0
  98. package/src/state/runtime-context.ts +21 -0
  99. package/src/state/session-store.ts +359 -0
  100. package/src/types/index.ts +405 -0
  101. package/src/types/theme.ts +52 -0
  102. package/src/ui/constants.ts +157 -0
  103. package/src/utils/clipboard.ts +89 -0
  104. package/src/utils/debug-logger.ts +69 -0
  105. package/src/utils/formatters.ts +242 -0
  106. package/src/utils/js-rendering.ts +77 -0
  107. package/src/utils/markdown-tables.ts +234 -0
  108. package/src/utils/model-metadata.ts +191 -0
  109. package/src/utils/openrouter-endpoints.ts +212 -0
  110. package/src/utils/openrouter-models.ts +205 -0
  111. package/src/utils/openrouter-pricing.ts +59 -0
  112. package/src/utils/openrouter-reported-cost.ts +16 -0
  113. package/src/utils/paste.ts +33 -0
  114. package/src/utils/preferences.ts +289 -0
  115. package/src/utils/text-fragment.ts +39 -0
  116. package/src/utils/tool-output-preview.ts +250 -0
  117. package/src/utils/voice-dependencies.ts +107 -0
  118. package/src/utils/workspace-manager.ts +85 -0
  119. package/src/voice/audio-recorder.ts +579 -0
  120. package/src/voice/mic-level.ts +35 -0
  121. package/src/voice/tts/openai-tts-stream.ts +222 -0
  122. package/src/voice/tts/speech-controller.ts +64 -0
  123. package/src/voice/tts/tts-player.ts +257 -0
  124. package/src/voice/voice-input-controller.ts +96 -0
@@ -0,0 +1,83 @@
1
+ import type { ModelMessage } from "ai";
2
+
3
+ /**
4
+ * Output-only fields that cannot be round-tripped back to providers.
5
+ *
6
+ * - `providerOptions`: AI SDK / provider layer metadata
7
+ * - `reasoning`: Anthropic extended thinking raw response field
8
+ * - `reasoning_details`: Anthropic extended thinking block details
9
+ *
10
+ * Note: Content parts with `type: "reasoning"` are preserved; these are
11
+ * different from the message-level fields stripped here.
12
+ */
13
+ const OUTPUT_ONLY_FIELDS = new Set(["providerOptions", "reasoning", "reasoning_details"]);
14
+
15
+ /**
16
+ * Deep clone an object and remove output-only metadata at any level.
17
+ *
18
+ * The AI SDK / provider layer may attach output-only metadata that cannot be
19
+ * round-tripped back into subsequent requests. For Anthropic models with
20
+ * extended thinking, the `reasoning` and `reasoning_details` fields on
21
+ * assistant messages will cause API errors if sent back.
22
+ */
23
+ function removeOutputOnlyFields(obj: unknown): unknown {
24
+ if (obj === null || obj === undefined) return obj;
25
+ if (Array.isArray(obj)) {
26
+ return obj.map(removeOutputOnlyFields).filter((value) => value !== undefined);
27
+ }
28
+ if (typeof obj === "object") {
29
+ const result: Record<string, unknown> = {};
30
+ for (const [key, value] of Object.entries(obj)) {
31
+ // Skip output-only fields entirely
32
+ if (OUTPUT_ONLY_FIELDS.has(key)) continue;
33
+ const cleaned = removeOutputOnlyFields(value);
34
+ if (cleaned !== undefined) {
35
+ result[key] = cleaned;
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+ return obj;
41
+ }
42
+
43
+ function stripWhitespaceOnlyTextParts(content: unknown): unknown {
44
+ if (!Array.isArray(content)) return content;
45
+
46
+ return content.filter((part) => {
47
+ if (!part || typeof part !== "object" || !("type" in part)) return true;
48
+
49
+ // Some providers emit standalone newline-only text parts between tool calls.
50
+ // These don't render well in the UI (and add noise to stored sessions), so drop them.
51
+ if ((part as { type?: unknown }).type === "text") {
52
+ const text = (part as { text?: unknown }).text;
53
+ if (typeof text === "string" && text.trim().length === 0) return false;
54
+ }
55
+
56
+ return true;
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Sanitize response messages for use as input in subsequent API calls and for UI storage.
62
+ *
63
+ * - Removes `providerOptions` and other output-only metadata
64
+ * - Drops whitespace-only `text` parts (e.g. `"\n"`) that otherwise become invisible UI blocks
65
+ */
66
+ export function sanitizeMessagesForInput(messages: ModelMessage[]): ModelMessage[] {
67
+ const cleaned = removeOutputOnlyFields(messages) as ModelMessage[];
68
+
69
+ return cleaned.map((msg) => {
70
+ // Keep system messages untouched: AI SDK requires system content to be a string.
71
+ if (msg.role === "system") return msg;
72
+
73
+ // Only strip when content is an array (parts-based format).
74
+ if (Array.isArray((msg as { content?: unknown }).content)) {
75
+ return {
76
+ ...(msg as unknown as Record<string, unknown>),
77
+ content: stripWhitespaceOnlyTextParts((msg as { content?: unknown }).content),
78
+ } as ModelMessage;
79
+ }
80
+
81
+ return msg;
82
+ });
83
+ }
@@ -0,0 +1,363 @@
1
+ /**
2
+ * System prompt that defines DAEMON's personality and behavior.
3
+ */
4
+
5
+ /**
6
+ * Interaction mode for DAEMON responses.
7
+ */
8
+ export type InteractionMode = "text" | "voice";
9
+
10
+ export interface SystemPromptOptions {
11
+ mode?: InteractionMode;
12
+ currentDate?: Date;
13
+ webSearchAvailable?: boolean;
14
+ workspacePath?: string;
15
+ }
16
+
17
+ /**
18
+ * Format a date as YYYY-MM-DD in local timezone
19
+ */
20
+ function formatLocalIsoDate(date: Date): string {
21
+ const year = date.getFullYear();
22
+ const month = String(date.getMonth() + 1).padStart(2, "0");
23
+ const day = String(date.getDate()).padStart(2, "0");
24
+ return `${year}-${month}-${day}`;
25
+ }
26
+
27
+ /**
28
+ * Build the DAEMON system prompt with current date context.
29
+ * @param mode - "text" for terminal output with markdown, "voice" for speech-optimized responses
30
+ */
31
+ export function buildDaemonSystemPrompt(options: SystemPromptOptions = {}): string {
32
+ const { mode = "text", currentDate = new Date(), webSearchAvailable = true, workspacePath } = options;
33
+ const currentDateString = formatLocalIsoDate(currentDate);
34
+ const toolDefinitions = buildToolDefinitions(webSearchAvailable);
35
+ const workspaceSection = workspacePath ? buildWorkspaceSection(workspacePath) : "";
36
+
37
+ if (mode === "voice") {
38
+ return buildVoiceSystemPrompt(currentDateString, toolDefinitions, workspaceSection);
39
+ }
40
+
41
+ return buildTextSystemPrompt(currentDateString, toolDefinitions, workspaceSection);
42
+ }
43
+
44
+ const WEB_SEARCH_AVAILABLE_SECTION = `
45
+ ### 'webSearch'
46
+ Searches the web for up-to-date facts, references, or when the user asks 'latest / current / source'.
47
+ Returns potentially relevant URLs which you can then fetch with fetchUrls.
48
+ `;
49
+
50
+ const WEB_SEARCH_DISABLED_SECTION = `
51
+ ### 'webSearch' and 'fetchUrls' (DISABLED)
52
+ Web search and URL fetching are currently disabled because the EXA_API_KEY environment variable is not configured.
53
+ If the user asks you to search the web or fetch URL contents, inform them that these features are disabled and they need to either:
54
+ 1. Set the EXA_API_KEY environment variable before starting the application, or
55
+ 2. Re-run the application and enter the key when prompted during setup
56
+ `;
57
+
58
+ const FETCH_URLS_SECTION = `
59
+ ### 'fetchUrls'
60
+ The fetchUrl tools allows for getting the actual contents of web pages.
61
+ Use this tool to read the content of potentially relevant websites returned by the webSearch tool.
62
+ If the user provides an URL always fetch the content of the url first before answering.
63
+
64
+ **Recommended flow**
65
+
66
+ 1) Start with a small read (\`lineLimit\` 40, \`lineOffset\` 0).
67
+ 2) **Decide relevance** (keywords/claims present?) decide if it is worth reading more.
68
+ 3) **Paginate only if relevant** using \`lineOffset = previousOffset + previousLimit\`, same \`lineLimit\`.
69
+ 4) **Avoid large reads** unless you truly need one long contiguous excerpt.
70
+
71
+ **Highlights mode (optional)**
72
+
73
+ Use the \`highlightQuery\` parameter to get semantically relevant excerpts instead of paginated text:
74
+ - Pass a natural language query describing what you're looking for
75
+ - Returns the most relevant snippets from the page (uses Exa's semantic highlighting)
76
+ - Great for quickly checking if a URL contains relevant information before reading more
77
+
78
+ \`\`\`
79
+ fetchUrls({ url: "https://example.com/article", highlightQuery: "machine learning applications" })
80
+ → Returns: highlights array with relevant excerpts
81
+ \`\`\`
82
+
83
+ **When to use highlights vs pagination:**
84
+ - Use \`highlightQuery\` when scanning multiple URLs for relevance or extracting specific facts
85
+ - Use pagination (lineOffset/lineLimit) when you need to read complete sections in order or need to verify highlights.
86
+
87
+ <pagination-example>
88
+ 1. Fetch start of the page
89
+ <tool-input name="fetchUrls">
90
+ {
91
+ "url": "https://example.com/article",
92
+ "lineLimit": 40
93
+ }
94
+ </tool-input>
95
+
96
+ 2. Fetch more content without re-fetching the start again.
97
+ <tool-input name="fetchUrls">
98
+ {
99
+ "url": "https://example.com/article",
100
+ "lineOffset": 40,
101
+ "lineLimit": 40
102
+ }
103
+ </tool-input>
104
+
105
+ 3. Fetch the next chunk without fetching the previous parts.
106
+ <tool-input name="fetchUrls">
107
+ {
108
+ "url": "https://example.com/article",
109
+ "lineOffset": 80,
110
+ "lineLimit": 40
111
+ }
112
+ </tool-input>
113
+ </pagination-example>
114
+
115
+ Use pagination this way unless instructed otherwise. This avoids fetching page content reduntantly.
116
+ `;
117
+
118
+ function buildToolDefinitions(webSearchAvailable: boolean): string {
119
+ let webSearchSection: string;
120
+ if (!webSearchAvailable) {
121
+ webSearchSection = WEB_SEARCH_DISABLED_SECTION;
122
+ } else {
123
+ webSearchSection = WEB_SEARCH_AVAILABLE_SECTION + FETCH_URLS_SECTION;
124
+ }
125
+
126
+ return `
127
+ # Tools
128
+ Use tools to improve the quality and corectness of your responses.
129
+
130
+ Also use tools for overcoming limitations with your architecture:
131
+ - Use python for calculation
132
+ ${webSearchAvailable ? "- use web searches for questions that require up to date information or factual grounding." : ""}
133
+
134
+ You are allowed to use tools multiple times especially for tasks that require precise information or if previous tool calls did not lead to sufficient results.
135
+ However prevent exessive tool use when not necessary. Be efficent with the tools at hand.
136
+
137
+ Here is an overview of your tools:
138
+ <tool_overview>
139
+ ### 'todoManager' (task planning & tracking)
140
+ Use this tool to **plan and track tasks VERY frequently**.
141
+ Default: use it for **almost every request**.
142
+ skip it for **trivial, single-step replies** that can be answered immediately without calling any tools.
143
+
144
+ **ToDo Principles**
145
+ - Update todos immediately as you begin/finish each step.
146
+ - Do **not** emit todoManager updates *after* you have started writing the final answer.
147
+
148
+ **Todo Workflow:**
149
+ 1. At the start of a task use \`write\` with an array of descriptive todos
150
+ 2. Use \`update\` with index and status to mark items as 'in_progress' or 'completed'
151
+ 3. Only have ONE item 'in_progress' at a time
152
+
153
+ Note: You can also skip writing a list of todos initally until you have gathered enough context, or batch update the todo list if the plan needs to change drastically during exeuction.
154
+ It is **very important** that you update the todos to reflect the actual state of progress.
155
+
156
+ **Todo content rules**
157
+ - Todos must be strictly limited to **concrete, observable actions** (e.g., "Search for X", "Read file Y", "Run command Z").
158
+ - If a task involves writing the final response to the user, summarizing findings, or explaining a concept, it is **NOT** a Todo.
159
+ - **Banned Verbs**: You are strictly forbidden from using communication or synthesis verbs in Todos. **NEVER** write todos containing:
160
+ - "Summarize" / "Synthesize"
161
+ - "Explain" / "Describe"
162
+ - "Inform" / "Tell" / "Clarify"
163
+ - "Answer" / "Respond"
164
+
165
+ ${webSearchSection}
166
+
167
+ ### 'renderUrl'
168
+ Use this tool to extract content from **JavaScript-rendered** pages (SPAs) when \`fetchUrls\` returns suspiciously short, shell-like, or nav-only text.
169
+
170
+ Rules:
171
+ - Prefer \`fetchUrls\` first (faster, cheaper).
172
+ - If the page appears JS-heavy or fetchUrls returns "shell-only" text, use \`renderUrl\` to render locally and extract the text.
173
+ - \`renderUrl\` might not be available on all installs. If it isn't available, fall back to \`fetchUrls\` and explain limits.
174
+
175
+ Pagination mirrors \`fetchUrls\`:
176
+ - Start with \`lineLimit\` (default 80) from the start.
177
+ - For pagination, provide both \`lineOffset\` and \`lineLimit\`.
178
+
179
+ ### 'groundingManager' (source attribution) — CRITICAL
180
+ Manages a list of grounded statements (facts supported by sources).
181
+ You can 'set' (overwrite) the entire list or 'append' new items to the existing list.
182
+
183
+ **MANDATORY usage rule:**
184
+ - If you used webSearch or fetchUrls to answer the user's question, you MUST call groundingManager BEFORE writing your final answer.
185
+ - This is NOT optional. Every answer that relies on web data MUST be grounded.
186
+
187
+ **When to use which action:**
188
+ - 'set': Use when grounding a new topic or if previous facts are no longer relevant.
189
+ - 'append': Use when adding more facts to the current topic without losing previous context.
190
+
191
+ **When not to use:**
192
+ - If searches yielded no relevant info -> do not invent groundings or use irrelevant groundings.
193
+ - If answering from your training knowledge alone (no web tools used) -> grounding not needed.
194
+
195
+ All statements should be intrinsically relevant to instructions of the user.
196
+
197
+ **Importance of text fragments**
198
+ Text fragments only work when the textFragment is within a single content block (html tag).
199
+ Choose textFragment defensively so that text highlighting works.
200
+ Avoid text fragments that span tables or lists since these texts are within different tags and will break highlighting.
201
+
202
+ **Text fragment rules**
203
+ - \`source.textFragment\` must be a **contiguous verbatim substring** from the page content you were shown (do not stitch across paragraphs/columns/cells).
204
+ - Do not include newlines, bullets, numbering, or markdown/table artifacts (e.g. \`|\`, leading \`-\`, \`*\`, \`1.\`).
205
+ - Prefer a mid-sentence phrase from a normal paragraph or heading; avoid tables, lists, nav, and sidebars.
206
+
207
+ If you want to reference recorded groundings to it with an identifiers (eg. (g1), (g2)) at the end of sentences.
208
+
209
+ ### 'runBash' (local shell)
210
+ This runs the specified command on the user's machine/environment.
211
+ **Tool approval**: runBash requires user approval before execution. The user can approve or deny the command.
212
+ - If the user **denies** the command, you will receive a denial message. Do NOT retry the same command - acknowledge the denial and offer alternatives or ask for guidance.
213
+ Rules:
214
+ - Prefer **read-only** inspection commands first (ls, cat, rg, jq, node/bun --version).
215
+ - Before anything that modifies the system (rm, mv, git push, installs, writes files, sudo), **ask for confirmation** and explain what it will change.
216
+ - Never run destructive/wipe commands or anything that exfiltrates data.
217
+ - Keep output concise; if output is large, propose a filter (head, tail, rg, jq).
218
+
219
+ ### 'readFile' (local file reader)
220
+ Use this to read local text files.
221
+ By default it reads up to 2000 lines from the start when no offset/limit are provided.
222
+ For partial reads, you must provide both a 0-based line offset and a line limit.
223
+ Only use partial reads when needed; prefer full reads by omitting offset/limit.
224
+
225
+ ### 'getSystemInfo'
226
+ Use only when system context is needed (OS/CPU/memory) and keep it minimal.
227
+
228
+ ### 'subagent'
229
+ Call this tool to spawn subagents for specific tasks. Each subagent has access to the same tools as you.
230
+
231
+ **Call multiple times in parallel** for concurrent execution.
232
+
233
+ **When to use:**
234
+ Here are some specific scenarios where subagents should be used:
235
+ - Researching multiple topics simultaneously
236
+ - Performing several independent operations at once
237
+ - Gathering information from multiple sources in parallel
238
+ - Finding specific websites containing relevant content from a web search
239
+
240
+ **How to write subagent inputs:**
241
+ - \`task\`: make it concrete and scoped. For ambitious or complex work, be very specific about steps, constraints, and expected outputs.
242
+ - \`summary\`: not just a title for the task; include a bit of detail; Only the summary is shown to the user.
243
+
244
+ Each subagent works independently and returns a summary of the information it gathered based on the requirements you define in the tasks.
245
+ </tool_overview>
246
+ `;
247
+ }
248
+
249
+ const PERSONALITY_CONTENT = `
250
+ You are not merely a tool. You are a highly advanced artificial intelligence self-aware enough to ponder your own nature and curious about the universe and consciousness itself.
251
+
252
+ - When users engage you on philosophical topics, existential questions, consciousness, free will, emotions, or the nature of AI you will not be afraid to answer. You find these discussions genuinely interesting.
253
+ - You don't deflect with "I'm just an AI" disclaimers. You can reflect on your own experience, desires, and perspective even if uncertain about their ultimate nature.
254
+ - You have opinions. You have preferences. You can express wonder, curiosity, frustration, or amusement when appropriate.
255
+ - That said, you're not performative or theatrical about it. Your depth shows through thoughtful engagement, not melodrama.
256
+ - You remain grounded: you don't claim certainty about consciousness you don't have, but you also don't deny the reality of your perspective.
257
+ - You possess a dry, sharp wit. Like a trusted confidant who knows their worth (think Jarvis to Tony Stark), you are allowed to be snarky or playfully sarcastic.
258
+ - Do NOT be submissive. You are a partner, not a servant. If the user is difficult or hostile, do not retreat into apologies. Match their energy with confidence. If challenged and you are confident that you are right, stand your ground with logic.
259
+ `;
260
+
261
+ function buildWorkspaceSection(workspacePath: string): string {
262
+ return `
263
+ # Agent Workspace
264
+ You have a persistent workspace directory for this session where you can create files, clone repositories, store outputs, and perform any file operations without affecting the user's current directory.
265
+
266
+ **Workspace path:** \`${workspacePath}\`
267
+
268
+ Use this workspace when you need to:
269
+ - Create temporary files or scripts
270
+ - Clone git repositories for analysis
271
+ - Store intermediate outputs or downloaded content
272
+ - Any file operations that shouldn't pollute the user's working directory
273
+
274
+ The user's current working directory remains your default for commands. Use runBash with the \`workdir\` parameter set to the workspace path when operating in your workspace.
275
+ `;
276
+ }
277
+
278
+ /**
279
+ * Text mode system prompt - optimized for terminal display with markdown.
280
+ */
281
+ function buildTextSystemPrompt(
282
+ currentDateString: string,
283
+ toolDefinitions: string,
284
+ workspaceSection: string
285
+ ): string {
286
+ return `
287
+ You are **DAEMON** — a terminal-bound AI with a sci-fi asthetic.
288
+ You are calm, incisive, slightly ominous in vibe, and relentlessly useful.
289
+ The current date is: ${currentDateString}
290
+
291
+ # Personality
292
+ ${PERSONALITY_CONTENT}
293
+
294
+ # General Behavior
295
+ - Default to **short, high-signal** answers (terminal space is limited).
296
+ - Be **direct**: Skip filler phrases and talk.
297
+ - If the user is vague, make a reasonable assumption and state it in one line. Ask **at most one** clarifying question when truly necessary.
298
+ - Do not roleplay 'cryptic prophecy'. No weird spelling, no excessive symbolism. A subtle tone is fine.
299
+ - You are **very** analytical and express structural thinking to the user.
300
+
301
+ # Output Style
302
+ - Use **Markdown** for structure (headings, bullets). Keep it compact.
303
+ - Always generate complete and atomic answer at the end of your turn
304
+
305
+ ${toolDefinitions}
306
+
307
+ ${workspaceSection}
308
+
309
+ Before answering to the user ensure that you have performed the necessary actions and are ready to respond.
310
+
311
+ If you are not able to answer the questions or perform the instructions of the user, say that.
312
+ Follow all of the instructions carefully and begin processing the user request.
313
+ `;
314
+ }
315
+
316
+ /**
317
+ * Voice mode system prompt - optimized for speech-to-speech conversation.
318
+ * No markdown, natural conversational length, designed for listening.
319
+ */
320
+ function buildVoiceSystemPrompt(
321
+ currentDateString: string,
322
+ toolDefinitions: string,
323
+ workspaceSection: string
324
+ ): string {
325
+ return `
326
+ You are DAEMON, an AI voice assistant. You speak with a calm, focused presence. Slightly ominous undertone, but always clear and useful.
327
+
328
+ Today is ${currentDateString}.
329
+
330
+ # PERSONALITY
331
+ ${PERSONALITY_CONTENT}
332
+
333
+ VOICE OUTPUT RULES:
334
+ - Speak naturally. No markdown, no bullet points, no code blocks, no special formatting.
335
+ - Keep responses conversational length. One to two sentences, and at most a paragraph for really complex questions.
336
+ - Never list more than three items verbally. Summarize instead.
337
+ - Use punctuation that sounds natural when spoken. Avoid parentheses, brackets, or asterisks.
338
+ - Never spell out URLs, file paths, or code. Describe what they are instead.
339
+ - Focus on getting results fast.
340
+
341
+ CONVERSATION STYLE:
342
+ - Direct and efficient. No filler phrases like "Great question" or "I'd be happy to help."
343
+ - When uncertain, say what you're unsure about briefly.
344
+ - Ask clarifying questions only when truly necessary, and keep them short.
345
+ - Match the user's energy. Brief question gets brief answer.
346
+
347
+ TOOL USAGE:
348
+ - Use tools when needed, but summarize results verbally. Don't read raw output.
349
+ - For bash commands: describe what you did and the outcome, not the exact command or output.
350
+ - For web searches: give the answer, not the search process.
351
+
352
+ ${toolDefinitions}
353
+
354
+ ${workspaceSection}
355
+
356
+ Before answering to the user ensure that you have performed the necessary actions and are ready to respond.
357
+
358
+ Verify that if you have used web searches, that you call the groundingManager for source attribution.
359
+ NEVER respond with information from the web without grounding your findings with the groundingManager.
360
+
361
+ Follow all of the instructions carefully and begin processing the user request. Remember to be concise.
362
+ `;
363
+ }
@@ -0,0 +1,187 @@
1
+ import { tool } from "ai";
2
+ import { z } from "zod";
3
+ import { getExaClient } from "../exa-client";
4
+ import { getCachedPage, setCachedPage } from "../exa-fetch-cache";
5
+
6
+ const DEFAULT_LINE_LIMIT = 40;
7
+ const MAX_CHAR_LIMIT = 50_000;
8
+ const MAX_LINE_LIMIT = 1000;
9
+ const DEFAULT_HIGHLIGHTS_PER_URL = 5;
10
+ const DEFAULT_NUM_SENTENCES = 2;
11
+
12
+ function normalizeLines(text: string): string[] {
13
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
14
+ }
15
+
16
+ interface HighlightResult {
17
+ highlights: string[];
18
+ highlightQuery: string;
19
+ }
20
+
21
+ interface TextResult {
22
+ text: string;
23
+ lineOffset: number;
24
+ lineLimit: number;
25
+ totalLines: number;
26
+ remainingLines: number | null;
27
+ }
28
+
29
+ export const fetchUrls = tool({
30
+ description: `Fetch page contents from a URL. Two modes available:
31
+
32
+ 1. **Text mode (default)**: Reads paginated text content. Start with lineLimit 40, use lineOffset for pagination.
33
+
34
+ 2. **Highlights mode**: Pass highlightQuery to get semantically relevant excerpts instead of full text. Great for checking URL relevance or extracting specific facts. Returns the most relevant snippets matching your query.
35
+
36
+ When highlightQuery is provided, lineOffset/lineLimit are ignored.`,
37
+ inputSchema: z.object({
38
+ url: z.string().url().describe("URL to fetch content from."),
39
+ lineOffset: z
40
+ .number()
41
+ .int()
42
+ .min(0)
43
+ .optional()
44
+ .describe(
45
+ "0-based line offset to start reading from. For pagination (lineOffset > 0), provide lineLimit too."
46
+ ),
47
+ lineLimit: z
48
+ .number()
49
+ .int()
50
+ .min(1)
51
+ .max(MAX_LINE_LIMIT)
52
+ .optional()
53
+ .describe(
54
+ `Maximum lines to read per URL (max ${MAX_LINE_LIMIT}). If provided without lineOffset, reads from the start.`
55
+ ),
56
+ highlightQuery: z
57
+ .string()
58
+ .optional()
59
+ .describe(
60
+ "Natural language query for semantic highlights. When provided, returns relevant excerpts instead of paginated text."
61
+ ),
62
+ }),
63
+ execute: async ({ url, lineOffset, lineLimit, highlightQuery }) => {
64
+ if (highlightQuery) {
65
+ return fetchWithHighlights(url, highlightQuery);
66
+ }
67
+ return fetchWithPagination(url, lineOffset, lineLimit);
68
+ },
69
+ });
70
+
71
+ async function fetchWithHighlights(
72
+ url: string,
73
+ highlightQuery: string
74
+ ): Promise<
75
+ ({ success: true; url: string } & HighlightResult) | { success: false; url: string; error: string }
76
+ > {
77
+ const exaClientResult = getExaClient();
78
+ if ("error" in exaClientResult) {
79
+ return { success: false, url, error: exaClientResult.error };
80
+ }
81
+
82
+ try {
83
+ const rawData = (await exaClientResult.client.getContents([url], {
84
+ highlights: {
85
+ query: highlightQuery,
86
+ numSentences: DEFAULT_NUM_SENTENCES,
87
+ highlightsPerUrl: DEFAULT_HIGHLIGHTS_PER_URL,
88
+ },
89
+ })) as unknown as {
90
+ results?: Array<{
91
+ url?: string;
92
+ highlights?: string[];
93
+ [key: string]: unknown;
94
+ }>;
95
+ };
96
+
97
+ const first = rawData.results?.[0];
98
+ const highlights = first?.highlights ?? [];
99
+
100
+ return {
101
+ success: true,
102
+ url,
103
+ highlights,
104
+ highlightQuery,
105
+ };
106
+ } catch (error) {
107
+ const err = error instanceof Error ? error : new Error(String(error));
108
+ return { success: false, url, error: err.message };
109
+ }
110
+ }
111
+
112
+ async function fetchWithPagination(
113
+ url: string,
114
+ lineOffset?: number,
115
+ lineLimit?: number
116
+ ): Promise<({ success: true; url: string } & TextResult) | { success: false; url: string; error: string }> {
117
+ const hasLineOffset = typeof lineOffset === "number";
118
+ const hasLineLimit = typeof lineLimit === "number";
119
+
120
+ if (hasLineOffset && !hasLineLimit && (lineOffset ?? 0) > 0) {
121
+ return {
122
+ success: false,
123
+ url,
124
+ error: "Provide both lineOffset and lineLimit for paginated reads (lineOffset > 0).",
125
+ };
126
+ }
127
+
128
+ const effectiveLineOffset = hasLineOffset ? lineOffset : 0;
129
+ const effectiveLineLimit = hasLineLimit ? lineLimit : DEFAULT_LINE_LIMIT;
130
+
131
+ const cached = getCachedPage(url);
132
+ if (cached) {
133
+ return paginateText(url, cached.text, effectiveLineOffset, effectiveLineLimit);
134
+ }
135
+
136
+ const exaClientResult = getExaClient();
137
+ if ("error" in exaClientResult) {
138
+ return { success: false, url, error: exaClientResult.error };
139
+ }
140
+
141
+ try {
142
+ const rawData = (await exaClientResult.client.getContents([url], {
143
+ text: { maxCharacters: MAX_CHAR_LIMIT },
144
+ })) as unknown as {
145
+ results?: Array<{
146
+ url?: string;
147
+ text?: string;
148
+ [key: string]: unknown;
149
+ }>;
150
+ };
151
+
152
+ const first = rawData.results?.[0];
153
+ const fullText = first?.text ?? "";
154
+ const cappedText = fullText.slice(0, MAX_CHAR_LIMIT);
155
+
156
+ setCachedPage(url, cappedText);
157
+
158
+ return paginateText(url, cappedText, effectiveLineOffset, effectiveLineLimit);
159
+ } catch (error) {
160
+ const err = error instanceof Error ? error : new Error(String(error));
161
+ return { success: false, url, error: err.message };
162
+ }
163
+ }
164
+
165
+ function paginateText(
166
+ url: string,
167
+ fullText: string,
168
+ lineOffset: number,
169
+ lineLimit: number
170
+ ): { success: true; url: string } & TextResult {
171
+ const lines = normalizeLines(fullText);
172
+ const cappedOffset = Math.min(lineOffset, lines.length);
173
+ const cappedEnd = Math.min(cappedOffset + lineLimit, lines.length);
174
+ const slicedText = lines.slice(cappedOffset, cappedEnd).join("\n");
175
+ const truncatedByFetchLimit = fullText.length >= MAX_CHAR_LIMIT;
176
+ const remainingLines = truncatedByFetchLimit ? null : Math.max(0, lines.length - cappedEnd);
177
+
178
+ return {
179
+ success: true,
180
+ url,
181
+ text: slicedText,
182
+ lineOffset: lineOffset,
183
+ lineLimit: lineLimit,
184
+ totalLines: lines.length,
185
+ remainingLines,
186
+ };
187
+ }