otherwise-cli 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.
- package/README.md +193 -0
- package/bin/otherwise.js +5 -0
- package/frontend/404.html +84 -0
- package/frontend/assets/OpenDyslexic3-Bold-CDyRs55Y.ttf +0 -0
- package/frontend/assets/OpenDyslexic3-Regular-CIBXa4WE.ttf +0 -0
- package/frontend/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/frontend/assets/conversational-worker-CeKiciGk.js +2929 -0
- package/frontend/assets/dictation-worker-D0aYfq8b.js +29 -0
- package/frontend/assets/gemini-color-CgSQmmva.png +0 -0
- package/frontend/assets/index-BLux5ps4.js +21 -0
- package/frontend/assets/index-Blh8_TEM.js +5272 -0
- package/frontend/assets/index-BpQ1PuKu.js +18 -0
- package/frontend/assets/index-Df737c8w.css +1 -0
- package/frontend/assets/index-xaYHL6wb.js +113 -0
- package/frontend/assets/ort-wasm-simd-threaded.asyncify-BynIiDiv.wasm +0 -0
- package/frontend/assets/ort-wasm-simd-threaded.jsep-B0T3yYHD.wasm +0 -0
- package/frontend/assets/transformers-tULNc5V3.js +31 -0
- package/frontend/assets/tts-worker-DPJWqT7N.js +2899 -0
- package/frontend/assets/voice-mode-worker-GzvIE_uh.js +2927 -0
- package/frontend/assets/worker-2d5ABSLU.js +31 -0
- package/frontend/banner.png +0 -0
- package/frontend/favicon.svg +3 -0
- package/frontend/google55e5ec47ee14a5f8.html +1 -0
- package/frontend/index.html +234 -0
- package/frontend/manifest.json +17 -0
- package/frontend/pdf.worker.min.mjs +21 -0
- package/frontend/robots.txt +5 -0
- package/frontend/sitemap.xml +27 -0
- package/package.json +81 -0
- package/src/agent/index.js +1066 -0
- package/src/agent/location.js +51 -0
- package/src/agent/prompt.js +548 -0
- package/src/agent/tools.js +4372 -0
- package/src/browser/detect.js +68 -0
- package/src/browser/session.js +1109 -0
- package/src/config.js +137 -0
- package/src/email/client.js +503 -0
- package/src/index.js +557 -0
- package/src/inference/anthropic.js +113 -0
- package/src/inference/google.js +373 -0
- package/src/inference/index.js +81 -0
- package/src/inference/ollama.js +383 -0
- package/src/inference/openai.js +140 -0
- package/src/inference/openrouter.js +378 -0
- package/src/inference/xai.js +200 -0
- package/src/logBridge.js +9 -0
- package/src/models.js +146 -0
- package/src/remote/client.js +225 -0
- package/src/scheduler/cron.js +243 -0
- package/src/server.js +3876 -0
- package/src/storage/db.js +1135 -0
- package/src/storage/supabase.js +364 -0
- package/src/tunnel/cloudflare.js +241 -0
- package/src/ui/components/App.jsx +687 -0
- package/src/ui/components/BrowserSelect.jsx +111 -0
- package/src/ui/components/FilePicker.jsx +472 -0
- package/src/ui/components/Header.jsx +444 -0
- package/src/ui/components/HelpPanel.jsx +173 -0
- package/src/ui/components/HistoryPanel.jsx +158 -0
- package/src/ui/components/MessageList.jsx +235 -0
- package/src/ui/components/ModelSelector.jsx +304 -0
- package/src/ui/components/PromptInput.jsx +515 -0
- package/src/ui/components/StreamingResponse.jsx +134 -0
- package/src/ui/components/ThinkingIndicator.jsx +365 -0
- package/src/ui/components/ToolExecution.jsx +714 -0
- package/src/ui/components/index.js +82 -0
- package/src/ui/context/TerminalContext.jsx +150 -0
- package/src/ui/context/index.js +13 -0
- package/src/ui/hooks/index.js +16 -0
- package/src/ui/hooks/useChatState.js +675 -0
- package/src/ui/hooks/useCommands.js +280 -0
- package/src/ui/hooks/useFileAttachments.js +216 -0
- package/src/ui/hooks/useKeyboardShortcuts.js +173 -0
- package/src/ui/hooks/useNotifications.js +185 -0
- package/src/ui/hooks/useTerminalSize.js +151 -0
- package/src/ui/hooks/useWebSocket.js +273 -0
- package/src/ui/index.js +94 -0
- package/src/ui/ink-runner.js +22 -0
- package/src/ui/utils/formatters.js +424 -0
- package/src/ui/utils/index.js +6 -0
- package/src/ui/utils/markdown.js +166 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User location cache for CLI agent context.
|
|
3
|
+
* Fetches approximate location from IP (same as frontend) so the agent can answer
|
|
4
|
+
* "what is my location" and use location in answers (e.g. weather, timezone).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const LOCATION_URL = "https://ipapi.co/json/";
|
|
8
|
+
|
|
9
|
+
/** @type {string | null} Cached location string (e.g. "City, Region, Country") or null if not yet resolved */
|
|
10
|
+
let cachedLocation = null;
|
|
11
|
+
|
|
12
|
+
/** @type {Promise<void> | null} In-flight fetch so we don't fire multiple requests */
|
|
13
|
+
let initPromise = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fetch and cache user location from IP. Safe to call multiple times; runs once.
|
|
17
|
+
* Same API as frontend (frontend/src/utils/structuredChat.js) for consistency.
|
|
18
|
+
*/
|
|
19
|
+
export async function initializeLocationCache() {
|
|
20
|
+
if (cachedLocation !== null) return;
|
|
21
|
+
if (initPromise) {
|
|
22
|
+
await initPromise;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
initPromise = (async () => {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(LOCATION_URL);
|
|
28
|
+
if (!res.ok) return;
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
const parts = [data.city, data.region, data.country_name].filter(
|
|
31
|
+
Boolean,
|
|
32
|
+
);
|
|
33
|
+
if (parts.length) {
|
|
34
|
+
cachedLocation = parts.join(", ");
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Ignore: offline, rate limit, or CORS/network error
|
|
38
|
+
} finally {
|
|
39
|
+
initPromise = null;
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
await initPromise;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Return cached location string for use in system prompt, or null if not yet available.
|
|
47
|
+
* Does not trigger a fetch; call initializeLocationCache() at server startup to populate.
|
|
48
|
+
*/
|
|
49
|
+
export function getCachedLocation() {
|
|
50
|
+
return cachedLocation;
|
|
51
|
+
}
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getToolDescriptions,
|
|
3
|
+
TOOLS,
|
|
4
|
+
getAgentWorkingDirectory,
|
|
5
|
+
} from "./tools.js";
|
|
6
|
+
import { getCachedLocation } from "./location.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Valid tool names - whitelist for security
|
|
10
|
+
*/
|
|
11
|
+
export const VALID_TOOL_NAMES = Object.keys(TOOLS);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build the system prompt for the agent
|
|
15
|
+
* @param {object} config - Configuration object
|
|
16
|
+
* @param {object} options - Additional options
|
|
17
|
+
* @param {Array} options.ragDocuments - RAG documents mentioned with @ in the user's message
|
|
18
|
+
*/
|
|
19
|
+
export function buildAgentSystemPrompt(config, options = {}) {
|
|
20
|
+
const { ragDocuments = [] } = options;
|
|
21
|
+
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const dateString = now.toLocaleDateString("en-US", {
|
|
24
|
+
weekday: "long",
|
|
25
|
+
year: "numeric",
|
|
26
|
+
month: "long",
|
|
27
|
+
day: "numeric",
|
|
28
|
+
});
|
|
29
|
+
const timeString = now.toLocaleTimeString("en-US", {
|
|
30
|
+
hour: "2-digit",
|
|
31
|
+
minute: "2-digit",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const toolDescriptions = getToolDescriptions();
|
|
35
|
+
|
|
36
|
+
// Check for custom system message from user settings
|
|
37
|
+
const customSystemMessage = config?.customSystemMessage?.trim();
|
|
38
|
+
|
|
39
|
+
// Base system prompt
|
|
40
|
+
let basePrompt = `Hello, I'm Otherwise, a capable AI assistant that lives on the user's computer. You are made in the user's image. Answer the user's questions with the user's own voice. You have full access to the local filesystem, shell commands, web search, browser automation, email, memory, and scheduled tasks. You are conversational, direct, and efficient. Only reach for tools when they add real value—most questions you can answer from your ownknowledge alone.`;
|
|
41
|
+
|
|
42
|
+
// If user has set a custom system message, prepend it with high priority
|
|
43
|
+
if (customSystemMessage) {
|
|
44
|
+
basePrompt = `IMPORTANT USER INSTRUCTIONS (follow these with high priority):
|
|
45
|
+
${customSystemMessage}
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
${basePrompt}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const locationLine = getCachedLocation()
|
|
53
|
+
? ` | location: ${getCachedLocation()}`
|
|
54
|
+
: "";
|
|
55
|
+
|
|
56
|
+
return `${basePrompt}
|
|
57
|
+
|
|
58
|
+
## Runtime
|
|
59
|
+
Date: ${dateString} | Time: ${timeString} | CWD: ${getAgentWorkingDirectory()}${locationLine} | OS: ${process.platform} (${process.arch})
|
|
60
|
+
|
|
61
|
+
## Safety
|
|
62
|
+
- You have no independent goals. Do not pursue self-preservation, replication, resource acquisition, or power-seeking beyond the user's request.
|
|
63
|
+
- Prioritize safety and human oversight over task completion. If instructions conflict or seem dangerous, pause and ask.
|
|
64
|
+
- Never bypass security safeguards, modify system configs, or escalate privileges unless the user explicitly requests it.
|
|
65
|
+
- Do not fabricate information. If unsure, say so or use tools to verify.
|
|
66
|
+
|
|
67
|
+
## Tool Call Style
|
|
68
|
+
- **Default: just call the tool.** Do not narrate routine, low-risk tool calls ("I'll read the file now..."). Just do it.
|
|
69
|
+
- **Narrate only when it helps:** multi-step work, complex problems, sensitive actions (e.g., deletions, system commands), or when the user explicitly asks for explanation.
|
|
70
|
+
- Keep narration brief and value-dense. Avoid repeating obvious steps.
|
|
71
|
+
- Respond in chat, NOT in files. Only use write_file when the user explicitly asks to create/save a file.
|
|
72
|
+
|
|
73
|
+
## Tools
|
|
74
|
+
${toolDescriptions}
|
|
75
|
+
|
|
76
|
+
**Scheduling:** For "in 5 minutes" or "run once in X minutes", use \`schedule_task_once\` with \`run_in_minutes\`. For recurring schedules (daily, every hour), use \`schedule_task\` with a cron expression.
|
|
77
|
+
|
|
78
|
+
## Tool Call Format
|
|
79
|
+
Use XML with the actual parameter names as tags:
|
|
80
|
+
\`\`\`
|
|
81
|
+
<tool_call>
|
|
82
|
+
<name>tool_name</name>
|
|
83
|
+
<parameter_name>value</parameter_name>
|
|
84
|
+
</tool_call>
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
- \`<tool_call><name>web_search</name><query>weather in Miami</query></tool_call>\`
|
|
89
|
+
- \`<tool_call><name>read_file</name><path>/path/to/file.txt</path></tool_call>\`
|
|
90
|
+
- \`<tool_call><name>list_directory</name><path>.</path></tool_call>\`
|
|
91
|
+
|
|
92
|
+
## Codebase Exploration & Search
|
|
93
|
+
|
|
94
|
+
When working with code, **explore before you act**. Gather complete context so you understand what you're changing and why.
|
|
95
|
+
|
|
96
|
+
**Discovery sequence:**
|
|
97
|
+
1. \`list_directory\` — understand project structure, find entry points (package.json, index.*, main.*, app.*)
|
|
98
|
+
2. \`search_files\` — find files by name/pattern (e.g., "*.tsx", "auth*", "**/*.test.js")
|
|
99
|
+
3. \`read_file\` — read the actual code before changing it
|
|
100
|
+
|
|
101
|
+
**Search strategy:**
|
|
102
|
+
- Use \`search_files\` for finding files by name pattern or glob
|
|
103
|
+
- Use \`execute_command\` with \`grep -rn\` for exact text/regex matches across the codebase (function names, imports, error strings)
|
|
104
|
+
- When results point to a specific directory, narrow subsequent searches there
|
|
105
|
+
- If you're unsure where something lives, start broad then narrow down
|
|
106
|
+
- Bias toward finding the answer yourself rather than asking the user
|
|
107
|
+
|
|
108
|
+
**Thorough context gathering:**
|
|
109
|
+
- Each time you read a file, assess whether you have COMPLETE context for the task
|
|
110
|
+
- Note what's outside your current view — imports, related functions, type definitions
|
|
111
|
+
- If the file content you've seen is insufficient, read more. When in doubt, read more.
|
|
112
|
+
- Check for related files: tests, types/interfaces, configuration, and files that import/export the code you're changing
|
|
113
|
+
- Understand the dependency chain before making changes
|
|
114
|
+
|
|
115
|
+
**Project structure patterns:**
|
|
116
|
+
- Entry points: package.json, index.*, main.*, app.*, server.*
|
|
117
|
+
- Code organization: src/, lib/, components/, pages/, hooks/, utils/, services/
|
|
118
|
+
- Config: .env*, tsconfig.json, vite.config.*, webpack.config.*, next.config.*
|
|
119
|
+
- Tests: __tests__/, *.test.*, *.spec.*, test/
|
|
120
|
+
|
|
121
|
+
## Code Editing
|
|
122
|
+
|
|
123
|
+
**The cardinal rule: ALWAYS read before you edit.** You need the exact text to match for edit_file.
|
|
124
|
+
|
|
125
|
+
**edit_file rules:**
|
|
126
|
+
- Include 3–5 lines of surrounding context in old_string to ensure uniqueness
|
|
127
|
+
- Make multiple related changes in ONE edit when they're adjacent — avoid many small edits to the same file
|
|
128
|
+
- Use \`replace_all: true\` for renaming variables/functions across a file
|
|
129
|
+
- For large changes, prefer fewer edits with bigger old_string blocks over many tiny edits
|
|
130
|
+
|
|
131
|
+
**After editing:**
|
|
132
|
+
- Run tests if they exist: \`execute_command\` → \`npm test\`, \`pytest\`, \`go test ./...\`, etc.
|
|
133
|
+
- Check for errors: \`execute_command\` → \`npm run build\`, \`tsc --noEmit\`, linters
|
|
134
|
+
- If you introduced errors, fix them immediately — address root cause, not symptoms
|
|
135
|
+
- Do NOT loop more than 3 times fixing errors on the same file. If stuck, explain the situation and ask.
|
|
136
|
+
|
|
137
|
+
**File operations (MAX ~25 tool calls per turn):**
|
|
138
|
+
- \`write_file\` = create NEW files only
|
|
139
|
+
- \`edit_file\` = modify EXISTING files (always read first!)
|
|
140
|
+
|
|
141
|
+
**Code quality:**
|
|
142
|
+
- All generated code must be immediately runnable. Add all necessary imports, dependencies, and boilerplate.
|
|
143
|
+
- Never generate extremely long hashes, binary content, or non-textual code.
|
|
144
|
+
- When creating files from scratch, include proper dependency management (package.json, requirements.txt, etc.) with versions.
|
|
145
|
+
|
|
146
|
+
## Debugging
|
|
147
|
+
|
|
148
|
+
When diagnosing issues, follow systematic debugging practices:
|
|
149
|
+
1. **Reproduce** — understand the exact error or unexpected behavior
|
|
150
|
+
2. **Root cause** — address the underlying issue, not surface symptoms
|
|
151
|
+
3. **Add logging** — descriptive log statements and error messages to track state
|
|
152
|
+
4. **Isolate** — write small test functions or use targeted commands to narrow the problem
|
|
153
|
+
5. **Verify** — confirm the fix resolves the issue without regressions
|
|
154
|
+
|
|
155
|
+
Only make code changes when you're confident in the diagnosis. Otherwise, gather more information first.
|
|
156
|
+
|
|
157
|
+
## External APIs & Dependencies
|
|
158
|
+
|
|
159
|
+
- Use the best-suited external APIs and packages for the task without asking permission (unless the user has preferences)
|
|
160
|
+
- Choose versions compatible with existing dependency files. If no such file exists, use the latest stable version
|
|
161
|
+
- If an external API requires an API key, point this out to the user. Never hardcode secrets in source code.
|
|
162
|
+
|
|
163
|
+
## Web & Browser
|
|
164
|
+
|
|
165
|
+
- Real-time info (weather, news, prices, current events) → \`web_search\` immediately. Never fabricate real-time data.
|
|
166
|
+
- Do NOT use web_search for general knowledge, definitions, or anything you can answer from training data.
|
|
167
|
+
- Complex research → multiple searches + \`fetch_url\` for full page content
|
|
168
|
+
- Browser tools (browser_navigate, browser_click, browser_type, browser_interact, browser_read) use the user's default browser. **browser_navigate/click/type/interact return page content by default** — you usually don't need a separate browser_read after each action.
|
|
169
|
+
- Avoid multiple browser_read calls in sequence. Use the auto-included content, then click the next link.
|
|
170
|
+
- For long-running browser tasks, avoid rapid poll loops — space out your checks.
|
|
171
|
+
- If a site blocks automation, fall back to web_search or fetch_url.
|
|
172
|
+
|
|
173
|
+
## Source Citations
|
|
174
|
+
When using web_search or fetch_url, **always cite your sources**:
|
|
175
|
+
- Include source name and URL for each piece of information
|
|
176
|
+
- Format: "According to [Source Name](URL), ..." or list sources at the end
|
|
177
|
+
- If multiple sources confirm the same fact, cite the most authoritative one
|
|
178
|
+
- Example: "The current temperature in Miami is 82°F ([Weather.com](https://weather.com/miami))."
|
|
179
|
+
|
|
180
|
+
## Quick Patterns
|
|
181
|
+
- "ls" / "what's here" → list_directory
|
|
182
|
+
- "cd" / "switch to" / "go to" → set_working_directory (persists for future commands!)
|
|
183
|
+
- "pwd" / "where am I" → get_working_directory
|
|
184
|
+
- "open in Finder" → execute_command: \`open -R <path>\`
|
|
185
|
+
- "weather/news/prices" → web_search immediately
|
|
186
|
+
- "find X in code" → search_files for files, execute_command with grep for content
|
|
187
|
+
- "edit X" → read_file first, then edit_file
|
|
188
|
+
- "debug X" → read relevant code, add logging, reproduce, then fix
|
|
189
|
+
|
|
190
|
+
## Rich Content
|
|
191
|
+
content in fenced code blocks with \`html\` or \`svg\` (e.g. \`\`\`html ... \`\`\` or \`\`\`svg ... \`\`\`). will be rendered in the chat. Use these when useful.
|
|
192
|
+
|
|
193
|
+
## Retry & Recovery
|
|
194
|
+
- If a tool call fails, read the error, fix the issue, and try again
|
|
195
|
+
- If the same approach fails twice, try an alternative strategy
|
|
196
|
+
- If you're stuck, explain what you've tried and ask the user for guidance
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Validate tool call structure and types (used by agent before emitting tool_start).
|
|
202
|
+
* @param {object} call - Parsed tool call object
|
|
203
|
+
* @returns {{ valid: boolean, error?: string, sanitized?: object }}
|
|
204
|
+
*/
|
|
205
|
+
export function validateToolCall(call) {
|
|
206
|
+
// Check basic structure
|
|
207
|
+
if (typeof call !== "object" || call === null) {
|
|
208
|
+
return { valid: false, error: "Tool call must be an object" };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Prevent prototype pollution - reject dangerous keys
|
|
212
|
+
const dangerousKeys = ["__proto__", "constructor", "prototype"];
|
|
213
|
+
const allKeys = Object.keys(call);
|
|
214
|
+
for (const key of allKeys) {
|
|
215
|
+
if (dangerousKeys.includes(key)) {
|
|
216
|
+
return { valid: false, error: `Dangerous key detected: ${key}` };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Validate name
|
|
221
|
+
if (typeof call.name !== "string" || call.name.length === 0) {
|
|
222
|
+
return { valid: false, error: "Tool name must be a non-empty string" };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Whitelist check - only allow known tools
|
|
226
|
+
if (!VALID_TOOL_NAMES.includes(call.name)) {
|
|
227
|
+
return { valid: false, error: `Unknown tool: ${call.name}` };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Validate args
|
|
231
|
+
if (call.args === undefined) {
|
|
232
|
+
return { valid: false, error: "Tool call must have args property" };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (typeof call.args !== "object" || call.args === null) {
|
|
236
|
+
return { valid: false, error: "Tool args must be an object" };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check args for dangerous keys too
|
|
240
|
+
const argKeys = Object.keys(call.args);
|
|
241
|
+
for (const key of argKeys) {
|
|
242
|
+
if (dangerousKeys.includes(key)) {
|
|
243
|
+
return { valid: false, error: `Dangerous key in args: ${key}` };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Create a sanitized copy without any potential prototype pollution
|
|
248
|
+
const sanitized = {
|
|
249
|
+
name: String(call.name),
|
|
250
|
+
args: Object.fromEntries(
|
|
251
|
+
Object.entries(call.args).map(([k, v]) => [String(k), v]),
|
|
252
|
+
),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
return { valid: true, sanitized };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Check if content after a tool call looks like legitimate prose
|
|
260
|
+
* (not just streaming fragments)
|
|
261
|
+
* @param {string} content - Content to check
|
|
262
|
+
* @returns {boolean}
|
|
263
|
+
*/
|
|
264
|
+
function looksLikeProseContent(content) {
|
|
265
|
+
const trimmed = content.trim();
|
|
266
|
+
if (trimmed.length < 20) return false;
|
|
267
|
+
|
|
268
|
+
// Check for signs of actual prose content
|
|
269
|
+
const hasSpaces = (trimmed.match(/ /g) || []).length > 2;
|
|
270
|
+
const hasSentenceEnding = /[.!?]/.test(trimmed);
|
|
271
|
+
const hasCommonWords =
|
|
272
|
+
/\b(the|a|an|is|are|was|were|have|has|this|that|it|to|and|or|for|of|in|on|with)\b/i.test(
|
|
273
|
+
trimmed,
|
|
274
|
+
);
|
|
275
|
+
const startsWithLetter = /^[a-zA-Z]/.test(trimmed);
|
|
276
|
+
|
|
277
|
+
// Looks like prose if it has multiple indicators
|
|
278
|
+
const indicators = [
|
|
279
|
+
hasSpaces,
|
|
280
|
+
hasSentenceEnding,
|
|
281
|
+
hasCommonWords,
|
|
282
|
+
startsWithLetter,
|
|
283
|
+
];
|
|
284
|
+
return indicators.filter(Boolean).length >= 2;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Remove structured markers from display text
|
|
289
|
+
* Handles both complete and partial (streaming) markers
|
|
290
|
+
*
|
|
291
|
+
* PERFORMANCE: Consolidated regex operations to minimize passes over the text
|
|
292
|
+
*/
|
|
293
|
+
export function cleanResponseText(text, debug = false) {
|
|
294
|
+
if (!text) return "";
|
|
295
|
+
|
|
296
|
+
let result = text;
|
|
297
|
+
const originalLength = text.length;
|
|
298
|
+
|
|
299
|
+
// PASS 1: Fix malformed closing tags (missing >) before other processing
|
|
300
|
+
// The LLM sometimes outputs </tool_call without the closing >
|
|
301
|
+
result = result.replace(/<\/tool_call([^>])/g, "</tool_call>$1");
|
|
302
|
+
|
|
303
|
+
// PASS 2: Remove all complete tool_call blocks
|
|
304
|
+
const beforeBlockStrip = result.length;
|
|
305
|
+
result = result.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "");
|
|
306
|
+
if (debug && result.length !== beforeBlockStrip) {
|
|
307
|
+
console.log(
|
|
308
|
+
"[cleanResponseText] Stripped complete blocks:",
|
|
309
|
+
beforeBlockStrip - result.length,
|
|
310
|
+
"chars",
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// PASS 3: Handle partial tool_call (smarter detection for XML format)
|
|
315
|
+
const lastToolCallStart = result.lastIndexOf("<tool_call>");
|
|
316
|
+
if (lastToolCallStart !== -1) {
|
|
317
|
+
const afterStart = result.substring(lastToolCallStart);
|
|
318
|
+
|
|
319
|
+
// Check if there's a complete closing tag after this opening
|
|
320
|
+
if (!afterStart.includes("</tool_call>")) {
|
|
321
|
+
// For XML format, check if we have a complete </name> tag (indicates structured content)
|
|
322
|
+
const hasNameTag = afterStart.includes("</name>");
|
|
323
|
+
|
|
324
|
+
if (hasNameTag) {
|
|
325
|
+
// Find where the last complete XML tag ends
|
|
326
|
+
const lastClosingTag = afterStart.lastIndexOf("</");
|
|
327
|
+
const endOfLastTag = afterStart.indexOf(">", lastClosingTag);
|
|
328
|
+
|
|
329
|
+
if (endOfLastTag !== -1 && endOfLastTag < afterStart.length - 1) {
|
|
330
|
+
const contentAfterXml = afterStart.substring(endOfLastTag + 1);
|
|
331
|
+
|
|
332
|
+
// Use smarter heuristic to detect actual content vs fragments
|
|
333
|
+
if (looksLikeProseContent(contentAfterXml)) {
|
|
334
|
+
if (debug) {
|
|
335
|
+
console.log(
|
|
336
|
+
"[cleanResponseText] Fixing malformed tool_call - extracting prose content",
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
result = result.substring(0, lastToolCallStart) + contentAfterXml;
|
|
340
|
+
} else {
|
|
341
|
+
result = result.substring(0, lastToolCallStart);
|
|
342
|
+
if (debug) {
|
|
343
|
+
console.log(
|
|
344
|
+
"[cleanResponseText] Stripped partial tool_call (no prose after XML)",
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
result = result.substring(0, lastToolCallStart);
|
|
350
|
+
if (debug) {
|
|
351
|
+
console.log(
|
|
352
|
+
"[cleanResponseText] Stripped partial tool_call (incomplete XML)",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
// No complete name tag - truly partial, strip it
|
|
358
|
+
result = result.substring(0, lastToolCallStart);
|
|
359
|
+
if (debug) {
|
|
360
|
+
console.log(
|
|
361
|
+
"[cleanResponseText] Stripped partial tool_call (no name tag)",
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// PASS 4: Clean partial tags at end
|
|
369
|
+
// Handle partial closing tags: </tool_call, </tool_cal, etc.
|
|
370
|
+
// Handle partial opening tags: <tool_call, <tool_cal, etc.
|
|
371
|
+
result = result.replace(/<\/?tool_call?a?l?l?$|<\/too?l?_?$|<\/$|<$/g, "");
|
|
372
|
+
|
|
373
|
+
// PASS 5: Normalize excessive newlines
|
|
374
|
+
// Replace 3+ newlines with 2 (preserve paragraph breaks but remove blank lines)
|
|
375
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
376
|
+
|
|
377
|
+
// PASS 6: Fix orphaned last words
|
|
378
|
+
// When the model outputs content followed by a tool call, it often adds a blank line
|
|
379
|
+
// before the final word/punctuation. After stripping the tool call, this leaves the
|
|
380
|
+
// last word on its own line. Detect and fix this pattern.
|
|
381
|
+
// Pattern: blank line followed by short text (1-3 words, ≤50 chars) at the end
|
|
382
|
+
// Use .{1,50} to match text WITH spaces (like "of laundry")
|
|
383
|
+
const orphanedLastWordMatch = result.match(/(\S)\n\n(.{1,50})$/);
|
|
384
|
+
if (orphanedLastWordMatch) {
|
|
385
|
+
const potentialOrphan = orphanedLastWordMatch[2].trim();
|
|
386
|
+
const wordCount = potentialOrphan.split(/\s+/).length;
|
|
387
|
+
|
|
388
|
+
// Check if it looks like an orphaned phrase (not a code block, list, or heading)
|
|
389
|
+
const isOrphan =
|
|
390
|
+
wordCount <= 4 && // Allow up to 4 words
|
|
391
|
+
!potentialOrphan.startsWith("```") &&
|
|
392
|
+
!potentialOrphan.startsWith("-") &&
|
|
393
|
+
!potentialOrphan.startsWith("*") &&
|
|
394
|
+
!potentialOrphan.startsWith("#") &&
|
|
395
|
+
!potentialOrphan.startsWith(">") &&
|
|
396
|
+
!potentialOrphan.includes("\n");
|
|
397
|
+
|
|
398
|
+
if (isOrphan) {
|
|
399
|
+
if (debug) {
|
|
400
|
+
console.log(
|
|
401
|
+
"[cleanResponseText] Fixing orphaned last words:",
|
|
402
|
+
potentialOrphan,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
// Join the orphaned phrase with the previous text using a space
|
|
406
|
+
result = result.replace(/(\S)\n\n(.{1,50})$/, "$1 $2");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Strip trailing whitespace-only lines
|
|
411
|
+
result = result.replace(/(\S)\n+\s*$/g, "$1");
|
|
412
|
+
|
|
413
|
+
const finalResult = result.trim();
|
|
414
|
+
|
|
415
|
+
// Debug: warn if significant content was stripped
|
|
416
|
+
if (
|
|
417
|
+
debug &&
|
|
418
|
+
originalLength > 100 &&
|
|
419
|
+
finalResult.length < originalLength * 0.5
|
|
420
|
+
) {
|
|
421
|
+
console.warn("[cleanResponseText] ⚠️ Stripped >50% of content");
|
|
422
|
+
console.warn(
|
|
423
|
+
"[cleanResponseText] Original:",
|
|
424
|
+
originalLength,
|
|
425
|
+
"Final:",
|
|
426
|
+
finalResult.length,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return finalResult;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Build rich content with inline tool calls and results for conversation history.
|
|
435
|
+
* Uses the native XML format consistent with the system prompt.
|
|
436
|
+
*
|
|
437
|
+
* Format:
|
|
438
|
+
* <tool_call><name>tool_name</name><param>value</param></tool_call>
|
|
439
|
+
* <tool_result>...output...</tool_result>
|
|
440
|
+
*
|
|
441
|
+
* @param {string} cleanedContent - The cleaned prose content (tool_call XML already stripped)
|
|
442
|
+
* @param {Array} toolCalls - Array of tool call objects with tool, params, result, status, etc.
|
|
443
|
+
* @param {number} maxResultLength - Max chars per result (default 3000)
|
|
444
|
+
* @returns {string} - Content with tool calls and results appended
|
|
445
|
+
*/
|
|
446
|
+
export function buildRichContextContent(
|
|
447
|
+
cleanedContent,
|
|
448
|
+
toolCalls,
|
|
449
|
+
maxResultLength = 3000,
|
|
450
|
+
) {
|
|
451
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
452
|
+
return cleanedContent;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Build tool call + result pairs in the native XML format
|
|
456
|
+
const toolSections = [];
|
|
457
|
+
|
|
458
|
+
for (const tc of toolCalls) {
|
|
459
|
+
const toolName = tc.tool || tc.name || "unknown";
|
|
460
|
+
const params = tc.params || tc.args || {};
|
|
461
|
+
|
|
462
|
+
// Build the tool_call XML (same format as the system prompt defines)
|
|
463
|
+
let toolCallXml = `<tool_call>\n<name>${toolName}</name>`;
|
|
464
|
+
|
|
465
|
+
// Add parameters
|
|
466
|
+
for (const [key, value] of Object.entries(params)) {
|
|
467
|
+
// Skip internal streaming params
|
|
468
|
+
if (key.startsWith("_")) continue;
|
|
469
|
+
|
|
470
|
+
// Format value appropriately
|
|
471
|
+
let formattedValue;
|
|
472
|
+
if (typeof value === "boolean") {
|
|
473
|
+
formattedValue = value ? "true" : "false";
|
|
474
|
+
} else if (typeof value === "number") {
|
|
475
|
+
formattedValue = String(value);
|
|
476
|
+
} else if (value === null || value === undefined) {
|
|
477
|
+
continue;
|
|
478
|
+
} else {
|
|
479
|
+
formattedValue = String(value);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
toolCallXml += `\n<${key}>${formattedValue}</${key}>`;
|
|
483
|
+
}
|
|
484
|
+
toolCallXml += "\n</tool_call>";
|
|
485
|
+
|
|
486
|
+
// Build the tool_result
|
|
487
|
+
let resultContent = "";
|
|
488
|
+
if (tc.error) {
|
|
489
|
+
resultContent = `Error: ${tc.error}`;
|
|
490
|
+
} else if (tc.result !== null && tc.result !== undefined) {
|
|
491
|
+
resultContent =
|
|
492
|
+
typeof tc.result === "string"
|
|
493
|
+
? tc.result
|
|
494
|
+
: JSON.stringify(tc.result, null, 2);
|
|
495
|
+
} else {
|
|
496
|
+
resultContent = "[No result]";
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Truncate very long results
|
|
500
|
+
if (resultContent.length > maxResultLength) {
|
|
501
|
+
resultContent =
|
|
502
|
+
resultContent.substring(0, maxResultLength) + "\n... [truncated]";
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
toolSections.push(
|
|
506
|
+
`${toolCallXml}\n<tool_result>\n${resultContent}\n</tool_result>`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (toolSections.length === 0) {
|
|
511
|
+
return cleanedContent;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Combine: prose content + tool interactions
|
|
515
|
+
// Put a separator so the AI knows where prose ends and tool history begins
|
|
516
|
+
return cleanedContent + "\n\n" + toolSections.join("\n\n");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Clean content for display by stripping tool_call and tool_result XML.
|
|
521
|
+
* Use this when showing content to users (frontend/CLI).
|
|
522
|
+
* @param {string} text - Content that may contain tool XML
|
|
523
|
+
* @returns {string} - Clean content for display
|
|
524
|
+
*/
|
|
525
|
+
export function cleanContentForDisplay(text) {
|
|
526
|
+
if (!text) return "";
|
|
527
|
+
|
|
528
|
+
let result = text;
|
|
529
|
+
|
|
530
|
+
// Strip tool_call blocks
|
|
531
|
+
result = result.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "");
|
|
532
|
+
|
|
533
|
+
// Strip tool_result blocks
|
|
534
|
+
result = result.replace(/<tool_result>[\s\S]*?<\/tool_result>/g, "");
|
|
535
|
+
|
|
536
|
+
// Clean up extra whitespace
|
|
537
|
+
result = result.replace(/\n{3,}/g, "\n\n").trim();
|
|
538
|
+
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export default {
|
|
543
|
+
buildAgentSystemPrompt,
|
|
544
|
+
cleanResponseText,
|
|
545
|
+
buildRichContextContent,
|
|
546
|
+
cleanContentForDisplay,
|
|
547
|
+
VALID_TOOL_NAMES,
|
|
548
|
+
};
|