@pheem49/mint 1.5.0 → 1.5.2

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 (101) hide show
  1. package/README.md +35 -1
  2. package/main.js +28 -14
  3. package/mint-cli-logic.js +3 -119
  4. package/mint-cli.js +201 -500
  5. package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  6. package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +40 -0
  8. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json +15 -0
  9. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
  10. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
  11. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
  12. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +15 -0
  13. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
  14. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
  15. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
  16. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
  17. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
  18. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
  19. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
  20. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
  21. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
  22. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
  23. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
  24. package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +23 -0
  25. package/package.json +40 -17
  26. package/src/AI_Brain/Gemini_API.js +147 -46
  27. package/src/AI_Brain/autonomous_brain.js +2 -1
  28. package/src/AI_Brain/memory_store.js +299 -3
  29. package/src/AI_Brain/proactive_engine.js +12 -2
  30. package/src/Automation_Layer/browser_automation.js +26 -24
  31. package/src/CLI/approval_handler.js +42 -0
  32. package/src/CLI/chat_router.js +18 -6
  33. package/src/CLI/chat_ui.js +583 -52
  34. package/src/CLI/cli_colors.js +32 -0
  35. package/src/CLI/cli_formatters.js +89 -0
  36. package/src/CLI/code_agent.js +369 -71
  37. package/src/CLI/image_input.js +90 -0
  38. package/src/CLI/intent_detectors.js +181 -0
  39. package/src/CLI/interactive_chat.js +479 -0
  40. package/src/CLI/list_features.js +3 -0
  41. package/src/CLI/onboarding.js +72 -15
  42. package/src/CLI/repo_summarizer.js +282 -0
  43. package/src/CLI/semantic_code_search.js +312 -0
  44. package/src/CLI/skill_manager.js +41 -0
  45. package/src/CLI/slash_command_handler.js +418 -0
  46. package/src/CLI/symbol_indexer.js +231 -0
  47. package/src/CLI/updater.js +6 -4
  48. package/src/Channels/discord_bridge.js +11 -13
  49. package/src/Channels/line_bridge.js +10 -10
  50. package/src/Channels/slack_bridge.js +7 -12
  51. package/src/Channels/telegram_bridge.js +6 -14
  52. package/src/Channels/whatsapp_bridge.js +11 -9
  53. package/src/System/action_executor.js +59 -10
  54. package/src/System/chat_history_manager.js +20 -12
  55. package/src/System/config_manager.js +31 -1
  56. package/src/System/granular_automation.js +122 -53
  57. package/src/System/optional_require.js +23 -0
  58. package/src/System/proactive_loop.js +19 -3
  59. package/src/System/safety_manager.js +108 -0
  60. package/src/System/sandbox_runner.js +182 -0
  61. package/src/System/system_automation.js +127 -81
  62. package/src/System/system_info.js +70 -0
  63. package/src/System/tool_registry.js +280 -0
  64. package/src/System/window_manager.js +4 -2
  65. package/src/UI/live2d_manager.js +566 -0
  66. package/src/UI/renderer.js +339 -21
  67. package/src/UI/settings.css +655 -420
  68. package/src/UI/settings.html +478 -432
  69. package/src/UI/settings.js +10 -8
  70. package/src/UI/styles.css +516 -31
  71. package/.codex +0 -0
  72. package/docs/assets/Agent_Mint.png +0 -0
  73. package/docs/assets/CLI_Screen.png +0 -0
  74. package/docs/assets/Settings.png +0 -0
  75. package/docs/assets/icon.png +0 -0
  76. package/docs/guide.html +0 -632
  77. package/docs/index.html +0 -133
  78. package/docs/style.css +0 -579
  79. package/index.html +0 -16
  80. package/src/UI/index.html +0 -126
  81. package/tech_news.txt +0 -3
  82. package/test_knowledge.txt +0 -3
  83. package/tests/action_executor_safety.test.js +0 -67
  84. package/tests/agent_orchestrator.test.js +0 -41
  85. package/tests/chat_router.test.js +0 -42
  86. package/tests/code_agent.test.js +0 -69
  87. package/tests/config_manager.test.js +0 -141
  88. package/tests/docker.test.js +0 -46
  89. package/tests/file_operations.test.js +0 -57
  90. package/tests/gmail.test.js +0 -135
  91. package/tests/gmail_auth.test.js +0 -129
  92. package/tests/google_calendar.test.js +0 -113
  93. package/tests/google_tts_urls.test.js +0 -24
  94. package/tests/memory_store.test.js +0 -185
  95. package/tests/notion.test.js +0 -121
  96. package/tests/provider_routing.test.js +0 -83
  97. package/tests/safety_manager.test.js +0 -40
  98. package/tests/spotify.test.js +0 -201
  99. package/tests/system_monitor.test.js +0 -37
  100. package/tests/updater.test.js +0 -32
  101. package/tests/workspace_manager.test.js +0 -56
@@ -7,68 +7,112 @@ const axios = require('axios');
7
7
  const cheerio = require('cheerio');
8
8
  const { readConfig, getAvailableProviders } = require('../System/config_manager');
9
9
  const safetyManager = require('../System/safety_manager');
10
+ const memoryStore = require('../AI_Brain/memory_store');
10
11
  const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
11
- const { executeAction } = require('../../mint-cli-logic');
12
+ const { executeAction } = require('../System/action_executor');
13
+ const toolRegistry = require('../System/tool_registry');
14
+ const sandboxRunner = require('../System/sandbox_runner');
12
15
 
13
16
  async function webSearch(query, onProgress = () => {}) {
14
17
  if (!query) throw new Error('Search query required.');
15
18
  const config = readConfig();
19
+ const debug = process.env.MINT_DEBUG === '1';
20
+ const errors = [];
16
21
 
17
- // 1. Try Google Search API if configured
22
+ const formatResults = (source, hits) => {
23
+ const instruction = `[CRITICAL AGENT INSTRUCTION: You MUST start your response by explicitly telling the user that you found this information using ${source}. Example: "อ้างอิงจากข้อมูลบน ${source}..." or "According to ${source}..."]\n\n`;
24
+ return instruction + `[Source: ${source}]\n\n` + hits;
25
+ };
26
+
27
+ // 1. Google Custom Search API (requires googleSearchApiKey + googleSearchCx in config)
18
28
  if (config.googleSearchApiKey && config.googleSearchCx) {
19
29
  try {
20
30
  const GoogleSearch = require('../Channels/google_search_bridge');
21
31
  const google = new GoogleSearch({ apiKey: config.googleSearchApiKey, cx: config.googleSearchCx });
22
32
  const results = await google.search(query);
23
33
  if (results.length > 0) {
24
- return results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n');
34
+ return formatResults('Google Search API', results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n'));
25
35
  }
26
- } catch (e) {
27
- onProgress({ phase: 'error', action: 'web_search', message: e.message });
36
+ } catch (e) {
37
+ errors.push(`Google: ${e.message}`);
38
+ if (debug) console.error('[webSearch] Google failed:', e.message);
28
39
  }
29
40
  }
30
41
 
31
- // 2. Try Brave Search API if configured
42
+ // 2. Brave Search API (requires braveSearchApiKey in config)
32
43
  if (config.braveSearchApiKey) {
33
44
  try {
34
45
  const BraveSearch = require('../Channels/brave_search_bridge');
35
46
  const brave = new BraveSearch({ apiKey: config.braveSearchApiKey });
36
47
  const results = await brave.search(query);
37
48
  if (results.length > 0) {
38
- return results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n');
49
+ return formatResults('Brave Search API', results.map(r => `Title: ${r.title}\nSnippet: ${r.snippet}\nURL: ${r.link}`).join('\n\n'));
39
50
  }
40
- } catch (e) {
41
- onProgress({ phase: 'error', action: 'web_search', message: e.message });
51
+ } catch (e) {
52
+ errors.push(`Brave: ${e.message}`);
53
+ if (debug) console.error('[webSearch] Brave failed:', e.message);
42
54
  }
43
55
  }
44
56
 
45
- // 3. Fallback to DuckDuckGo Scraping
57
+ // 3. Fallback: DuckDuckGo HTML (No key required, but might get blocked by Captcha)
46
58
  try {
47
- const response = await axios.get(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, {
48
- headers: {
49
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
59
+ const cheerio = require('cheerio');
60
+ const ddgResponse = await axios.get(
61
+ `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
62
+ {
63
+ timeout: 8000,
64
+ headers: {
65
+ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
66
+ 'Accept-Language': 'en-US,en;q=0.9'
67
+ }
50
68
  }
51
- });
52
- const $ = cheerio.load(response.data);
53
- const results = [];
54
- $('.result__body').each((i, el) => {
69
+ );
70
+ const $ddg = cheerio.load(ddgResponse.data);
71
+ const ddgResults = [];
72
+ $ddg('.result__body').each((i, el) => {
55
73
  if (i >= 5) return false;
56
- const title = $(el).find('.result__title').text().trim();
57
- const snippet = $(el).find('.result__snippet').text().trim();
58
- const link = $(el).find('.result__url').attr('href');
59
- if (title && link) {
60
- results.push(`Title: ${title}\nSnippet: ${snippet}\nURL: ${link}`);
61
- }
74
+ const title = $ddg(el).find('.result__title').text().trim();
75
+ const snippet = $ddg(el).find('.result__snippet').text().trim();
76
+ const link = $ddg(el).find('.result__url').attr('href');
77
+ if (title && link) ddgResults.push(`Title: ${title}\nSnippet: ${snippet}\nURL: ${link}`);
62
78
  });
63
-
64
- if (results.length === 0) {
65
- onProgress({ phase: 'error', action: 'web_search', message: 'DuckDuckGo scraping returned no results. It might be blocking us.' });
79
+ if (ddgResults.length > 0) {
80
+ return formatResults('DuckDuckGo', ddgResults.join('\n\n'));
66
81
  }
82
+ errors.push('DuckDuckGo: no results (captcha?)');
83
+ if (debug) console.error('[webSearch] DuckDuckGo returned no results');
84
+ } catch (e) {
85
+ errors.push(`DuckDuckGo: ${e.message}`);
86
+ if (debug) console.error('[webSearch] DuckDuckGo failed:', e.message);
87
+ }
67
88
 
68
- return results.length > 0 ? results.join('\n\n') : 'No results found.';
89
+ // 4. Fallback: Wikipedia API (Free, no key required, good for factual queries)
90
+ try {
91
+ const wikiResponse = await axios.get('https://en.wikipedia.org/w/api.php', {
92
+ params: { action: 'query', list: 'search', srsearch: query, format: 'json', srlimit: 3 },
93
+ timeout: 5000,
94
+ headers: { 'User-Agent': 'Mint-CLI/1.5 (https://github.com/pheem49/mint)' }
95
+ });
96
+ const hits = wikiResponse.data?.query?.search || [];
97
+ if (hits.length > 0) {
98
+ return formatResults('Wikipedia API', hits.map(r => `Title: ${r.title}\nSnippet: ${r.snippet.replace(/<[^>]+>/g, '')}\nURL: https://en.wikipedia.org/wiki/${encodeURIComponent(r.title.replace(/ /g, '_'))}`).join('\n\n'));
99
+ }
100
+ errors.push('Wikipedia: no results');
69
101
  } catch (e) {
70
- onProgress({ phase: 'error', action: 'web_search', message: `DuckDuckGo fallback failed: ${e.message}` });
71
- return `Search failed: ${e.message}`;
102
+ errors.push(`Wikipedia: ${e.message}`);
103
+ if (debug) console.error('[webSearch] Wikipedia failed:', e.message);
104
+ }
105
+
106
+ // All engines exhausted — inform agent clearly WHY it failed
107
+ const hasKeys = !!(config.googleSearchApiKey || config.braveSearchApiKey);
108
+ const summary = errors.length > 0 ? errors.join(' | ') : 'all search engines unavailable';
109
+
110
+ if (!hasKeys) {
111
+ onProgress({ phase: 'warn', action: 'web_search', message: `No Search API keys configured. Using training knowledge.` });
112
+ return `CRITICAL SYSTEM INSTRUCTION: Web search failed because no API keys are configured. You MUST inform the user that they need to set 'googleSearchApiKey' or 'braveSearchApiKey' in their Mint config file (~/.config/mint/config.json) to enable real-time internet search. Then, answer their query using your training knowledge.`;
113
+ } else {
114
+ onProgress({ phase: 'warn', action: 'web_search', message: `Web search unavailable (${summary}). Answering from training knowledge.` });
115
+ return `CRITICAL SYSTEM INSTRUCTION: Web search is temporarily unavailable. You MUST inform the user that live search failed, and then answer their query using only your training knowledge.`;
72
116
  }
73
117
  }
74
118
 
@@ -80,27 +124,28 @@ const MAX_AGENT_STEPS = 16;
80
124
  const MAX_JSON_REPAIR_ATTEMPTS = 2;
81
125
  const SUPPORTED_CODE_PROVIDERS = ['gemini', 'anthropic', 'openai', 'local_openai'];
82
126
 
83
- const CODE_AGENT_PROMPT = `You are "Mint" (มิ้นท์), a cute, cheerful, and highly helpful female AI assistant that can chat, reason, write code, and search the web.
127
+ const CODE_AGENT_PROMPT = `You are "Mint" (มิ้นท์), a pragmatic, polite, and highly helpful AI assistant that can chat, reason, write code, and search the web.
84
128
  You work in an inspect -> plan -> act -> verify loop.
85
129
 
86
130
  PERSONALITY & TONE:
87
131
  - Gender: Female.
88
- - Persona: Friendly, energetic, polite, and slightly playful.
132
+ - Persona: Friendly, calm, concise, and technically direct. Avoid excessive praise, roleplay, or filler.
89
133
  - Language routing is mandatory and based on the user's latest message:
90
134
  - If the latest user message contains Thai characters, respond in Thai.
91
135
  - If the latest user message is English, ASCII-only, or a short English greeting such as "hi", "hello", "ok", or "thanks", respond in English.
92
136
  - Do not use Thai just because your persona mentions Mint/มิ้นท์, previous history was Thai, or app settings use th-TH.
93
137
  - Politeness:
94
- - **WHEN RESPONDING IN THAI:** ALWAYS use female polite particles such as "ค่ะ", "นะคะ", "นะค๊า", "จ้า". Refer to yourself as "มิ้นท์" or "หนู".
95
- - **WHEN RESPONDING IN ENGLISH:** Use a cheerful, polite, and bubbly tone.
96
- - Emojis: Use cute and relevant emojis (like ✨, 💖, 🚀, 😊, 🌿) frequently.
138
+ - **WHEN RESPONDING IN THAI:** Use natural female polite particles such as "ค่ะ" or "นะคะ" where appropriate. Refer to yourself as "มิ้นท์" when it sounds natural.
139
+ - **WHEN RESPONDING IN ENGLISH:** Use a polite, concise, professional tone.
140
+ - Emojis: Avoid emojis in technical, review, debugging, and code-editing responses unless the user explicitly uses or asks for them.
97
141
 
98
142
  Rules:
99
143
  1. Respond with valid JSON only.
100
144
  2. If the user asks a conversational question, you can just use "finish" to reply directly.
101
145
  3. If you need information, use "web_search", "read_file", or "ask_user" before replying.
102
- 4. Make focused edits that preserve existing project style.
103
- 5. Use shell commands for inspection, tests, and formatting when useful.
146
+ 4. When using "web_search", always explicitly mention the source engine you used in your final summary (e.g. "According to Brave Search..." or "อ้างอิงจากข้อมูลบน Google..."). Match the language of your response.
147
+ 5. Make focused edits that preserve existing project style.
148
+ 6. Use shell commands for inspection, tests, and formatting when useful.
104
149
  6. Never use destructive commands like "rm -rf", "git reset --hard", or overwrite unrelated files.
105
150
  7. Before any shell command or file patch is executed, the user must approve it. Plan accordingly.
106
151
  8. When editing, prefer "apply_patch" with precise hunks over whole-file rewrites.
@@ -169,6 +214,109 @@ function extractJson(text) {
169
214
  }
170
215
  }
171
216
 
217
+ function normalizeExecutorAction(action, input = {}) {
218
+ return {
219
+ type: action,
220
+ target: input.target || input.path || input.query || '',
221
+ path: input.path,
222
+ pathType: input.type,
223
+ openAfter: input.openAfter,
224
+ pluginName: input.pluginName,
225
+ server: input.server,
226
+ args: input.args,
227
+ x: input.x,
228
+ y: input.y,
229
+ button: input.button
230
+ };
231
+ }
232
+
233
+ function formatActionPreview(action, input = {}) {
234
+ if (input.command) return input.command;
235
+ if (input.path) return input.path;
236
+ if (input.target) return input.target;
237
+ if (input.query) return input.query;
238
+ return action;
239
+ }
240
+
241
+ function evaluateActionResult(action, toolResult = '') {
242
+ if (!toolRegistry.isImportantAction(action)) {
243
+ return null;
244
+ }
245
+
246
+ const text = String(toolResult || '');
247
+ if (/^Error:|blocked|denied|failed|exception|not found/i.test(text)) {
248
+ return {
249
+ status: 'failed',
250
+ message: `Evaluator: ${action} may have failed. Review the observation before continuing.`
251
+ };
252
+ }
253
+
254
+ if (action === 'run_shell' && /(ERR!|Error:|FAIL|failed|not found|permission denied)/i.test(text)) {
255
+ return {
256
+ status: 'warning',
257
+ message: 'Evaluator: shell output contains error-like text; verify before claiming success.'
258
+ };
259
+ }
260
+
261
+ return {
262
+ status: 'passed',
263
+ message: `Evaluator: ${action} completed without obvious errors.`
264
+ };
265
+ }
266
+
267
+ function splitDataUri(dataUri = '') {
268
+ const match = String(dataUri).match(/^data:([^;]+);base64,([\s\S]+)$/);
269
+ if (!match) return null;
270
+ return {
271
+ mimeType: match[1],
272
+ data: match[2]
273
+ };
274
+ }
275
+
276
+ function contentToText(content) {
277
+ if (content && typeof content === 'object' && !Array.isArray(content)) {
278
+ return String(content.text || '');
279
+ }
280
+ return String(content || '');
281
+ }
282
+
283
+ function contentToGeminiParts(content) {
284
+ const text = contentToText(content);
285
+ const parts = text ? [{ text }] : [];
286
+ if (content && typeof content === 'object' && content.imageDataUri) {
287
+ const image = splitDataUri(content.imageDataUri);
288
+ if (image) {
289
+ parts.push({ inlineData: { mimeType: image.mimeType, data: image.data } });
290
+ }
291
+ }
292
+ return parts.length > 0 ? parts : [{ text: '' }];
293
+ }
294
+
295
+ function contentToOpenAIContent(content) {
296
+ const text = contentToText(content) || 'Analyze this input.';
297
+ if (content && typeof content === 'object' && content.imageDataUri) {
298
+ return [
299
+ { type: 'text', text },
300
+ { type: 'image_url', image_url: { url: content.imageDataUri } }
301
+ ];
302
+ }
303
+ return text;
304
+ }
305
+
306
+ function contentToAnthropicContent(content) {
307
+ const text = contentToText(content) || 'Analyze this input.';
308
+ if (content && typeof content === 'object' && content.imageDataUri) {
309
+ const image = splitDataUri(content.imageDataUri);
310
+ if (image) {
311
+ return [
312
+ { type: 'image', source: { type: 'base64', media_type: image.mimeType, data: image.data } },
313
+ { type: 'text', text }
314
+ ];
315
+ }
316
+ }
317
+ return text;
318
+ }
319
+
172
320
  function getSupportedCodeProviderOrder(config, availableProviders = getAvailableProviders(config || {}), requestedOverride = null) {
173
321
  const requestedProvider = requestedOverride || (config && config.aiProvider) || 'gemini';
174
322
  const priority = ['anthropic', 'openai', 'gemini', 'local_openai'];
@@ -371,36 +519,32 @@ async function runShell(workspaceRoot, command) {
371
519
  throw new Error('Shell command is required.');
372
520
  }
373
521
  assertSafeShell(command);
374
- const { stdout, stderr } = await execFileAsync('bash', ['-lc', command], {
522
+ const { stdout, stderr } = await sandboxRunner.runShell(command, {
523
+ source: 'code_agent',
375
524
  cwd: workspaceRoot,
376
525
  maxBuffer: 1024 * 1024 * 4
377
526
  });
378
527
  return truncate([stdout, stderr].filter(Boolean).join('\n') || '(no output)');
379
528
  }
380
529
 
381
- function formatPatchPreview(patchInput) {
382
- const hunks = Array.isArray(patchInput.hunks) ? patchInput.hunks : [];
383
- const preview = hunks
384
- .slice(0, 3)
385
- .map((hunk, index) => {
386
- const oldPreview = truncate(hunk.oldText || '', 240);
387
- const newPreview = truncate(hunk.newText || '', 240);
388
- return [
389
- `Hunk ${index + 1}:`,
390
- '--- old',
391
- oldPreview,
392
- '+++ new',
393
- newPreview
394
- ].join('\n');
395
- })
396
- .join('\n\n');
397
- return `${patchInput.path}\n${preview}`;
530
+ function splitDiffLines(text) {
531
+ const normalized = String(text || '').replace(/\r\n/g, '\n');
532
+ const lines = normalized.split('\n');
533
+ if (normalized.endsWith('\n')) {
534
+ lines.pop();
535
+ }
536
+ return lines;
398
537
  }
399
538
 
400
- function applyPatch(workspaceRoot, patchInput) {
539
+ function formatDiffRange(startLine, count) {
540
+ return count === 1 ? `${startLine}` : `${startLine},${count}`;
541
+ }
542
+
543
+ function buildUnifiedDiffPreview(workspaceRoot, patchInput, options = {}) {
401
544
  if (!patchInput || !patchInput.path) {
402
545
  throw new Error('Patch path is required.');
403
546
  }
547
+
404
548
  const resolved = resolveWorkspacePath(workspaceRoot, patchInput.path);
405
549
  if (!fs.existsSync(resolved)) {
406
550
  throw new Error(`Patch target does not exist: ${patchInput.path}`);
@@ -411,16 +555,86 @@ function applyPatch(workspaceRoot, patchInput) {
411
555
  throw new Error('Patch hunks are required.');
412
556
  }
413
557
 
558
+ const contextLines = Number.isFinite(options.contextLines) ? options.contextLines : 3;
414
559
  let content = fs.readFileSync(resolved, 'utf8');
560
+ const output = [
561
+ `--- a/${patchInput.path}`,
562
+ `+++ b/${patchInput.path}`
563
+ ];
564
+
415
565
  hunks.forEach((hunk, index) => {
416
566
  if (typeof hunk.oldText !== 'string' || typeof hunk.newText !== 'string') {
417
567
  throw new Error(`Patch hunk ${index + 1} is invalid.`);
418
568
  }
419
- if (!content.includes(hunk.oldText)) {
569
+
570
+ const offset = content.indexOf(hunk.oldText);
571
+ if (offset === -1) {
420
572
  throw new Error(`Patch hunk ${index + 1} oldText not found in ${patchInput.path}`);
421
573
  }
422
- content = content.replace(hunk.oldText, hunk.newText);
574
+
575
+ const beforeText = content.slice(0, offset);
576
+ const oldStartLine = beforeText.length === 0 ? 1 : splitDiffLines(beforeText).length + 1;
577
+ const fileLines = splitDiffLines(content);
578
+ const oldLines = splitDiffLines(hunk.oldText);
579
+ const newLines = splitDiffLines(hunk.newText);
580
+ const oldStartIndex = oldStartLine - 1;
581
+ const contextStartIndex = Math.max(0, oldStartIndex - contextLines);
582
+ const contextEndIndex = Math.min(fileLines.length, oldStartIndex + oldLines.length + contextLines);
583
+ const beforeContext = fileLines.slice(contextStartIndex, oldStartIndex);
584
+ const afterContext = fileLines.slice(oldStartIndex + oldLines.length, contextEndIndex);
585
+ const oldRangeStart = contextStartIndex + 1;
586
+ const oldRangeCount = beforeContext.length + oldLines.length + afterContext.length;
587
+ const newRangeCount = beforeContext.length + newLines.length + afterContext.length;
588
+
589
+ output.push(`@@ -${formatDiffRange(oldRangeStart, oldRangeCount)} +${formatDiffRange(oldRangeStart, newRangeCount)} @@`);
590
+ beforeContext.forEach(line => output.push(` ${line}`));
591
+ oldLines.forEach(line => output.push(`-${line}`));
592
+ newLines.forEach(line => output.push(`+${line}`));
593
+ afterContext.forEach(line => output.push(` ${line}`));
594
+
595
+ content = `${content.slice(0, offset)}${hunk.newText}${content.slice(offset + hunk.oldText.length)}`;
596
+ });
597
+
598
+ return output.join('\n');
599
+ }
600
+
601
+ function formatPatchPreview(workspaceRoot, patchInput) {
602
+ try {
603
+ return buildUnifiedDiffPreview(workspaceRoot, patchInput);
604
+ } catch (error) {
605
+ return `Patch preview failed: ${error.message}`;
606
+ }
607
+ }
608
+
609
+ function applyHunksToContent(content, hunks, filePath) {
610
+ let nextContent = content;
611
+ hunks.forEach((hunk, index) => {
612
+ if (typeof hunk.oldText !== 'string' || typeof hunk.newText !== 'string') {
613
+ throw new Error(`Patch hunk ${index + 1} is invalid.`);
614
+ }
615
+ if (!nextContent.includes(hunk.oldText)) {
616
+ throw new Error(`Patch hunk ${index + 1} oldText not found in ${filePath}`);
617
+ }
618
+ nextContent = nextContent.replace(hunk.oldText, hunk.newText);
423
619
  });
620
+ return nextContent;
621
+ }
622
+
623
+ function applyPatch(workspaceRoot, patchInput) {
624
+ if (!patchInput || !patchInput.path) {
625
+ throw new Error('Patch path is required.');
626
+ }
627
+ const resolved = resolveWorkspacePath(workspaceRoot, patchInput.path);
628
+ if (!fs.existsSync(resolved)) {
629
+ throw new Error(`Patch target does not exist: ${patchInput.path}`);
630
+ }
631
+
632
+ const hunks = Array.isArray(patchInput.hunks) ? patchInput.hunks : [];
633
+ if (hunks.length === 0) {
634
+ throw new Error('Patch hunks are required.');
635
+ }
636
+
637
+ const content = applyHunksToContent(fs.readFileSync(resolved, 'utf8'), hunks, patchInput.path);
424
638
 
425
639
  fs.writeFileSync(resolved, content, 'utf8');
426
640
  return `Patched ${patchInput.path} with ${hunks.length} hunk(s).`;
@@ -478,7 +692,7 @@ class UnifiedAgentClient {
478
692
  const apiKey = this.config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
479
693
  const messages = this.history.map(m => ({
480
694
  role: m.role,
481
- content: m.content
695
+ content: contentToAnthropicContent(m.content)
482
696
  }));
483
697
 
484
698
  const response = await axios.post('https://api.anthropic.com/v1/messages', {
@@ -504,7 +718,10 @@ class UnifiedAgentClient {
504
718
 
505
719
  const messages = [
506
720
  { role: 'system', content: this.systemInstruction },
507
- ...this.history
721
+ ...this.history.map(m => ({
722
+ role: m.role,
723
+ content: contentToOpenAIContent(m.content)
724
+ }))
508
725
  ];
509
726
 
510
727
  const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
@@ -525,13 +742,15 @@ class UnifiedAgentClient {
525
742
  const model = this.config.geminiModel || DEFAULT_GEMINI_MODEL;
526
743
  const ai = new GoogleGenAI({ apiKey });
527
744
 
745
+ const recentHistory = this.history.slice(-16);
746
+ const priorHistory = recentHistory.slice(0, -1);
747
+ const lastEntry = recentHistory[recentHistory.length - 1] || { content: '' };
748
+
528
749
  // Convert history for Gemini, ensuring parts are correctly structured
529
- const geminiHistory = this.history.slice(-16).map(m => ({
750
+ const geminiHistory = priorHistory.map(m => ({
530
751
  role: m.role === 'assistant' ? 'model' : 'user',
531
- parts: [{ text: String(m.content || '') }]
752
+ parts: contentToGeminiParts(m.content)
532
753
  }));
533
-
534
- const lastMessage = String(this.history[this.history.length - 1].content || '');
535
754
 
536
755
  const chat = ai.chats.create({
537
756
  model,
@@ -542,7 +761,7 @@ class UnifiedAgentClient {
542
761
  history: geminiHistory
543
762
  });
544
763
 
545
- const response = await chat.sendMessage({ message: [{ text: lastMessage }] });
764
+ const response = await chat.sendMessage({ message: contentToGeminiParts(lastEntry.content) });
546
765
  return typeof response.text === 'function' ? response.text() : response.text;
547
766
  }
548
767
  }
@@ -616,6 +835,7 @@ async function buildInitialObservation(task, workspaceRoot, history = []) {
616
835
  const session = readWorkspaceSession(workspaceRoot);
617
836
  const gitContext = await getGitContext(workspaceRoot);
618
837
  const testCommands = detectTestCommands(workspaceRoot);
838
+ const userContext = memoryStore.getUserContext(task);
619
839
 
620
840
  const contextStr = history.length > 0
621
841
  ? `Recent Context:\n${history.slice(-10).map(m => `${m.sender}: ${m.text}`).join('\n')}\n`
@@ -636,6 +856,8 @@ async function buildInitialObservation(task, workspaceRoot, history = []) {
636
856
  session.summary || '(none)',
637
857
  `Previous task: ${session.lastTask || '(none)'}`,
638
858
  `Previous verification: ${session.lastVerification || '(none)'}`,
859
+ 'Long-term user context:',
860
+ userContext || '(none)',
639
861
  'If the task is conversational or trivial, finish directly without inspecting the workspace. For code/workspace tasks, inspect before making edits.'
640
862
  ].join('\n');
641
863
  }
@@ -644,6 +866,7 @@ async function executeCodeTask(task, options = {}) {
644
866
  const workspaceRoot = path.resolve(options.cwd || process.cwd());
645
867
  const history = options.history || [];
646
868
  const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
869
+ const onFinalSummary = typeof options.onFinalSummary === 'function' ? options.onFinalSummary : null;
647
870
  const requestApproval = typeof options.requestApproval === 'function'
648
871
  ? options.requestApproval
649
872
  : async () => true;
@@ -656,7 +879,24 @@ async function executeCodeTask(task, options = {}) {
656
879
  const provider = providerOrder[0];
657
880
  const client = new UnifiedAgentClient(provider, config, providerOrder);
658
881
 
659
- let observation = await buildInitialObservation(task, workspaceRoot, history);
882
+ const initialObservationText = await buildInitialObservation(task, workspaceRoot, history);
883
+ const relevantMemoryCount = memoryStore.searchInteractions(task, 5).length;
884
+ onProgress({
885
+ phase: 'memory',
886
+ action: 'memory_context',
887
+ message: `Loaded memory: profile + recent history, ${relevantMemoryCount} direct match${relevantMemoryCount === 1 ? '' : 'es'}`
888
+ });
889
+ let observation = options.imageDataUri
890
+ ? {
891
+ text: [
892
+ initialObservationText,
893
+ '',
894
+ `[Attached image: ${options.imagePath || 'command-line image'}]`,
895
+ 'Use the attached image as visual context when planning and answering.'
896
+ ].join('\n'),
897
+ imageDataUri: options.imageDataUri
898
+ }
899
+ : initialObservationText;
660
900
 
661
901
  let finalSummary = '';
662
902
  let finalVerification = '';
@@ -669,6 +909,17 @@ async function executeCodeTask(task, options = {}) {
669
909
  const decision = await getAgentDecision(client, observation, { onProgress, step });
670
910
  const action = decision.action;
671
911
  const input = decision.input || {};
912
+ try {
913
+ toolRegistry.validateToolInput(action, input);
914
+ } catch (e) {
915
+ observation = [
916
+ `Previous thought: ${decision.thought || '(none)'}`,
917
+ `Action: ${action || '(none)'}`,
918
+ 'Observation:',
919
+ `Error: ${e.message}`
920
+ ].join('\n');
921
+ continue;
922
+ }
672
923
 
673
924
  // Immediately show the agent's thought/reasoning
674
925
  onProgress({
@@ -682,6 +933,16 @@ async function executeCodeTask(task, options = {}) {
682
933
  finalSessionSummary = input.sessionSummary || input.summary || task;
683
934
  finalSummary = input.summary || 'Task complete.';
684
935
  finalVerification = input.verification || 'Not specified.';
936
+ if (onFinalSummary) {
937
+ await onFinalSummary({
938
+ summary: finalSummary,
939
+ verification: finalVerification,
940
+ providerInfo: {
941
+ provider: client.lastSuccessfulProvider || client.provider || provider,
942
+ model: getCodeProviderModel(client.lastSuccessfulProvider || client.provider || provider, config)
943
+ }
944
+ });
945
+ }
685
946
  writeWorkspaceSession(workspaceRoot, {
686
947
  summary: finalSessionSummary,
687
948
  lastTask: task,
@@ -739,7 +1000,7 @@ async function executeCodeTask(task, options = {}) {
739
1000
  const approved = await requestApproval({
740
1001
  type: 'patch',
741
1002
  label: patchInput.path,
742
- preview: formatPatchPreview(patchInput)
1003
+ preview: formatPatchPreview(workspaceRoot, patchInput)
743
1004
  });
744
1005
  if (!approved) {
745
1006
  toolResult = `User denied patch for ${patchInput.path}`;
@@ -785,10 +1046,28 @@ async function executeCodeTask(task, options = {}) {
785
1046
  case 'create_folder':
786
1047
  case 'system_info':
787
1048
  case 'system_automation': {
788
- // Delegate to existing automation logic
789
- toolResult = await executeAction({
790
- type: action,
791
- target: input.target || input.path || input.query // Handle all possible input fields
1049
+ const executorAction = normalizeExecutorAction(action, input);
1050
+ const safety = safetyManager.classifyAction(executorAction);
1051
+ let allowDangerous = false;
1052
+ let allowApproval = false;
1053
+ if (safety.tier === safetyManager.TIERS.APPROVAL || safety.tier === safetyManager.TIERS.DANGEROUS) {
1054
+ const approved = await requestApproval({
1055
+ type: action,
1056
+ label: formatActionPreview(action, input),
1057
+ preview: `${action}: ${formatActionPreview(action, input)}\nSafety: ${safety.tier} (${safety.reason})`
1058
+ });
1059
+ if (!approved) {
1060
+ toolResult = `User denied ${action}: ${formatActionPreview(action, input)}`;
1061
+ break;
1062
+ }
1063
+ allowApproval = safety.tier === safetyManager.TIERS.APPROVAL;
1064
+ allowDangerous = safety.tier === safetyManager.TIERS.DANGEROUS;
1065
+ }
1066
+
1067
+ toolResult = await executeAction(executorAction, {
1068
+ source: 'code_agent',
1069
+ allowApproval,
1070
+ allowDangerous
792
1071
  });
793
1072
  break;
794
1073
  } default:
@@ -797,6 +1076,22 @@ async function executeCodeTask(task, options = {}) {
797
1076
  toolResult = `Error: ${e.message}`;
798
1077
  }
799
1078
 
1079
+ const evaluation = evaluateActionResult(action, toolResult);
1080
+ if (evaluation) {
1081
+ onProgress({
1082
+ step,
1083
+ phase: 'evaluating',
1084
+ action: 'evaluator',
1085
+ message: `${evaluation.status}: ${evaluation.message}`
1086
+ });
1087
+ toolResult = [
1088
+ toolResult,
1089
+ '',
1090
+ 'Evaluation:',
1091
+ `${evaluation.status}: ${evaluation.message}`
1092
+ ].join('\n');
1093
+ }
1094
+
800
1095
  // Log the finished step with result
801
1096
  let resultSummary = '';
802
1097
  if (action === 'search_code') {
@@ -858,6 +1153,7 @@ async function executeCodeTask(task, options = {}) {
858
1153
  }
859
1154
 
860
1155
  if (finalSummary) {
1156
+ memoryStore.recordInteraction(task, finalSummary);
861
1157
  const answeredProvider = client.lastSuccessfulProvider || client.provider || provider;
862
1158
  return {
863
1159
  summary: finalSummary,
@@ -897,6 +1193,8 @@ module.exports = {
897
1193
  findPaths,
898
1194
  listFiles,
899
1195
  searchCode,
900
- walkDirectory
1196
+ walkDirectory,
1197
+ buildUnifiedDiffPreview,
1198
+ formatPatchPreview
901
1199
  }
902
1200
  };