code-ollama 0.11.0 → 0.12.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 CHANGED
@@ -13,7 +13,7 @@
13
13
  [![build](https://github.com/ai-action/code-ollama/actions/workflows/build.yml/badge.svg)](https://github.com/ai-action/code-ollama/actions/workflows/build.yml)
14
14
  [![codecov](https://codecov.io/gh/ai-action/code-ollama/graph/badge.svg?token=gRGUasRn2k)](https://codecov.io/gh/ai-action/code-ollama)
15
15
 
16
- 🦙 [Ollama](https://ollama.com/) coding agent that runs in your terminal.
16
+ 🦙 [Ollama](https://ollama.com/) coding agent that runs in your terminal. Read the [wiki](https://github.com/ai-action/code-ollama/wiki).
17
17
 
18
18
  ## Quick Start
19
19
 
@@ -0,0 +1,46 @@
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ //#region \0rolldown/runtime.js
4
+ var __defProp = Object.defineProperty;
5
+ var __exportAll = (all, no_symbols) => {
6
+ let target = {};
7
+ for (var name in all) __defProp(target, name, {
8
+ get: all[name],
9
+ enumerable: true
10
+ });
11
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
12
+ return target;
13
+ };
14
+ //#endregion
15
+ //#region src/utils/tools/shell.ts
16
+ var shell_exports = /* @__PURE__ */ __exportAll({
17
+ execShell: () => execShell,
18
+ runShell: () => runShell
19
+ });
20
+ var execAsync = promisify(exec);
21
+ var SHELL_EXEC_OPTIONS = {
22
+ timeout: 3e4,
23
+ maxBuffer: 1024 * 1024
24
+ };
25
+ /**
26
+ * Execute shell command with shared options (throws on error)
27
+ */
28
+ function execShell(command) {
29
+ return execAsync(command, SHELL_EXEC_OPTIONS);
30
+ }
31
+ /**
32
+ * Execute shell command
33
+ */
34
+ async function runShell(command) {
35
+ try {
36
+ const { stdout, stderr } = await execShell(command);
37
+ return { content: stdout || stderr };
38
+ } catch (error) {
39
+ return {
40
+ content: "",
41
+ error: `Command failed: ${error instanceof Error ? error.message : String(error)}`
42
+ };
43
+ }
44
+ }
45
+ //#endregion
46
+ export { shell_exports as n, runShell as t };
@@ -1,4 +1,4 @@
1
- import { _ as USER, a as tick, c as setClearHandler, d as loadConfig, f as saveConfig, g as SYSTEM, h as ASSISTANT, i as executeTool, l as listModels, m as withSystemMessage, n as TOOLS, o as clear, p as resetSystemMessage, r as WRITE_TOOLS, s as reset, t as READ_TOOLS, u as streamChat, v as PLAN_GENERATION_INSTRUCTION, y as VERSION } from "../cli.js";
1
+ import { _ as USER, a as tick, c as setClearHandler, d as loadConfig, f as saveConfig, g as SYSTEM, h as ASSISTANT, i as WRITE_TOOLS, l as listModels, m as withSystemMessage, n as READ_TOOLS, o as clear, p as resetSystemMessage, r as TOOLS, s as reset, t as executeTool, u as streamChat, v as PLAN_GENERATION_INSTRUCTION, y as VERSION } from "../cli.js";
2
2
  import { readdirSync } from "node:fs";
3
3
  import { join, relative } from "node:path";
4
4
  import { homedir } from "node:os";
@@ -19,6 +19,10 @@ var LIST = [
19
19
  name: "/model",
20
20
  description: "switch the model"
21
21
  },
22
+ {
23
+ name: "/search",
24
+ description: "configure web search"
25
+ },
22
26
  {
23
27
  name: "/exit",
24
28
  description: "exit the application"
@@ -1247,19 +1251,129 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
1247
1251
  });
1248
1252
  }
1249
1253
  //#endregion
1254
+ //#region src/components/SearchSettings.tsx
1255
+ var View = /* @__PURE__ */ function(View) {
1256
+ View["Menu"] = "menu";
1257
+ View["Edit"] = "edit";
1258
+ return View;
1259
+ }(View || {});
1260
+ var Action = /* @__PURE__ */ function(Action) {
1261
+ Action["Set"] = "set";
1262
+ Action["Clear"] = "clear";
1263
+ Action["Cancel"] = "cancel";
1264
+ return Action;
1265
+ }(Action || {});
1266
+ function SearchSettings({ currentUrl, onClose, onSave }) {
1267
+ const [view, setView] = useState(View.Menu);
1268
+ const [draftUrl, setDraftUrl] = useState(currentUrl ?? "");
1269
+ const [error, setError] = useState(null);
1270
+ const options = useMemo(() => {
1271
+ const nextOptions = [{
1272
+ label: currentUrl ? "Update SearXNG URL" : "Set SearXNG URL",
1273
+ value: Action.Set
1274
+ }];
1275
+ if (currentUrl) nextOptions.push({
1276
+ label: "Clear SearXNG URL",
1277
+ value: Action.Clear
1278
+ });
1279
+ nextOptions.push({
1280
+ label: "Cancel",
1281
+ value: Action.Cancel
1282
+ });
1283
+ return nextOptions;
1284
+ }, [currentUrl]);
1285
+ const handleChange = useCallback((value) => {
1286
+ setError(null);
1287
+ switch (value) {
1288
+ case Action.Set:
1289
+ setDraftUrl(currentUrl ?? "");
1290
+ setView(View.Edit);
1291
+ break;
1292
+ case Action.Clear:
1293
+ onSave(void 0);
1294
+ break;
1295
+ case Action.Cancel:
1296
+ default: onClose();
1297
+ }
1298
+ }, [
1299
+ currentUrl,
1300
+ onClose,
1301
+ onSave
1302
+ ]);
1303
+ const handleSubmit = useCallback((value) => {
1304
+ const trimmedValue = value.trim();
1305
+ if (!trimmedValue) {
1306
+ setError("Enter a URL or press Esc to cancel.");
1307
+ return;
1308
+ }
1309
+ try {
1310
+ const url = new URL(trimmedValue);
1311
+ if (!["http:", "https:"].includes(url.protocol)) {
1312
+ setError("URL must use http or https.");
1313
+ return;
1314
+ }
1315
+ onSave(url.toString());
1316
+ } catch {
1317
+ setError("Enter a valid URL.");
1318
+ }
1319
+ }, [onSave]);
1320
+ useInput((input, key) => {
1321
+ if (view === View.Edit && (key.escape || key.ctrl && input === "c")) {
1322
+ setDraftUrl(currentUrl ?? "");
1323
+ setError(null);
1324
+ setView(View.Menu);
1325
+ }
1326
+ });
1327
+ if (view === View.Edit) return /* @__PURE__ */ jsxs(Box, {
1328
+ flexDirection: "column",
1329
+ children: [
1330
+ /* @__PURE__ */ jsx(Text, { children: "Set the SearXNG base URL. DuckDuckGo remains the fallback." }),
1331
+ /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
1332
+ value: draftUrl,
1333
+ onChange: setDraftUrl,
1334
+ onSubmit: handleSubmit,
1335
+ placeholder: "http://localhost:8080"
1336
+ })] }),
1337
+ error && /* @__PURE__ */ jsx(Text, {
1338
+ color: "red",
1339
+ children: error
1340
+ }),
1341
+ /* @__PURE__ */ jsx(Text, {
1342
+ dimColor: true,
1343
+ children: "Press Enter to save, Esc to go back."
1344
+ })
1345
+ ]
1346
+ });
1347
+ return /* @__PURE__ */ jsxs(SelectPrompt, {
1348
+ options,
1349
+ onChange: handleChange,
1350
+ onCancel: onClose,
1351
+ children: [
1352
+ /* @__PURE__ */ jsxs(Text, { children: ["SearXNG URL: ", /* @__PURE__ */ jsx(Text, {
1353
+ color: "cyan",
1354
+ children: currentUrl ?? "not set"
1355
+ })] }),
1356
+ /* @__PURE__ */ jsx(Text, { children: "DuckDuckGo fallback remains available." }),
1357
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "Manage web search settings" })
1358
+ ]
1359
+ });
1360
+ }
1361
+ //#endregion
1250
1362
  //#region src/components/App.tsx
1251
1363
  var SCREEN = /* @__PURE__ */ function(SCREEN) {
1252
1364
  SCREEN["CHAT"] = "chat";
1253
1365
  SCREEN["MODEL_PICKER"] = "model-picker";
1366
+ SCREEN["SEARCH_SETTINGS"] = "search-settings";
1254
1367
  return SCREEN;
1255
1368
  }(SCREEN || {});
1256
1369
  function App() {
1257
1370
  const { exit } = useApp();
1258
- const [model, setModel] = useState(() => loadConfig().model);
1371
+ const [appConfig, setAppConfig] = useState(() => loadConfig());
1259
1372
  const [currentScreen, setScreen] = useState(SCREEN.CHAT);
1260
1373
  const [mode, setMode] = useState(SAFE);
1261
1374
  const [sessionId, setSessionId] = useState(0);
1262
1375
  const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
1376
+ const { model, searxngBaseUrl } = appConfig;
1263
1377
  const handleHeaderLoad = useCallback(() => {
1264
1378
  setIsHeaderLoaded(true);
1265
1379
  }, []);
@@ -1268,6 +1382,9 @@ function App() {
1268
1382
  case "/model":
1269
1383
  setScreen(SCREEN.MODEL_PICKER);
1270
1384
  break;
1385
+ case "/search":
1386
+ setScreen(SCREEN.SEARCH_SETTINGS);
1387
+ break;
1271
1388
  case "/clear":
1272
1389
  resetSystemMessage();
1273
1390
  clear();
@@ -1280,10 +1397,21 @@ function App() {
1280
1397
  }
1281
1398
  }, [exit]);
1282
1399
  const handleSelect = useCallback((selected) => {
1283
- setModel(selected);
1400
+ setAppConfig((currentConfig) => ({
1401
+ ...currentConfig,
1402
+ model: selected
1403
+ }));
1284
1404
  saveConfig({ model: selected });
1285
1405
  setScreen(SCREEN.CHAT);
1286
1406
  }, []);
1407
+ const handleSaveSearch = useCallback((url) => {
1408
+ setAppConfig((currentConfig) => ({
1409
+ ...currentConfig,
1410
+ searxngBaseUrl: url
1411
+ }));
1412
+ saveConfig({ searxngBaseUrl: url });
1413
+ setScreen(SCREEN.CHAT);
1414
+ }, []);
1287
1415
  const handleClose = useCallback(() => {
1288
1416
  setScreen(SCREEN.CHAT);
1289
1417
  }, []);
@@ -1306,6 +1434,13 @@ function App() {
1306
1434
  onClose: handleClose
1307
1435
  });
1308
1436
  break;
1437
+ case SCREEN.SEARCH_SETTINGS:
1438
+ screenContent = /* @__PURE__ */ jsx(SearchSettings, {
1439
+ currentUrl: searxngBaseUrl,
1440
+ onSave: handleSaveSearch,
1441
+ onClose: handleClose
1442
+ });
1443
+ break;
1309
1444
  case SCREEN.CHAT:
1310
1445
  screenContent = /* @__PURE__ */ jsx(Chat, {
1311
1446
  model,
package/dist/cli.js CHANGED
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ import { t as runShell } from "./assets/shell-CipXM_WI.js";
2
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
3
4
  import cac from "cac";
4
5
  import { join } from "node:path";
5
6
  import { homedir } from "node:os";
6
7
  import { Ollama } from "ollama";
7
- import { exec } from "node:child_process";
8
- import { promisify } from "node:util";
8
+ //#region package.json
9
+ var name = "code-ollama";
10
+ var version = "0.12.0";
9
11
  //#endregion
10
12
  //#region src/constants/package.ts
11
- var VERSION = "0.11.0";
13
+ var NAME = name;
14
+ var VERSION = version;
12
15
  //#endregion
13
16
  //#region src/constants/prompt.ts
14
- var BASE_SYSTEM_PROMPT = `You are a coding assistant that helps users write, edit, and understand code. You have access to tools for reading files, writing files, running shell commands, and searching code
17
+ var BASE_SYSTEM_PROMPT = `You are a coding assistant that helps users write, edit, and understand code. You have access to tools for reading files, writing files, running shell commands, searching code, and searching the web
15
18
 
16
19
  Follow these rules:
17
20
  1. Always use available tools rather than guessing file contents or code behavior
@@ -28,13 +31,15 @@ var TOOL_INSTRUCTIONS = `Available tools:
28
31
  - edit_file: Replace one exact text match in a file (requires approval)
29
32
  - list_dir: List files in a directory
30
33
  - grep_search: Search code with regex
34
+ - web_search: Search the web for current or external information
31
35
  - run_shell: Execute shell commands (requires approval)
32
36
 
33
37
  Always use tools when you need to:
34
38
  - Check file contents before referencing them
35
39
  - Make file changes
36
40
  - Explore project structure
37
- - Search the codebase`;
41
+ - Search the codebase
42
+ - Look up current or external information`;
38
43
  var PLAN_GENERATION_INSTRUCTION = `Based on the research above, decide whether the user request needs code or shell execution
39
44
 
40
45
  If the request needs changes or commands, respond with a plan checklist only
@@ -62,6 +67,7 @@ var RUN_SHELL = "run_shell";
62
67
  var LIST_DIR = "list_dir";
63
68
  var GREP_SEARCH = "grep_search";
64
69
  var VIEW_RANGE = "view_range";
70
+ var WEB_SEARCH = "web_search";
65
71
  //#endregion
66
72
  //#region src/utils/agents.ts
67
73
  var AGENTS_FILE = "AGENTS.md";
@@ -97,12 +103,10 @@ function withSystemMessage(messages) {
97
103
  }
98
104
  //#endregion
99
105
  //#region src/utils/config.ts
100
- var CONFIG_DIR = join(homedir(), ".code-ollama");
101
- var CONFIG_PATH = join(CONFIG_DIR, "config.json");
102
- var DEFAULTS = {
103
- host: "http://localhost:11434",
104
- model: "gemma4"
105
- };
106
+ var CONFIG_DIRECTORY = join(homedir(), `.${NAME}`);
107
+ var CONFIG_PATH = join(CONFIG_DIRECTORY, "config.json");
108
+ var DEFAULT_HOST = "http://localhost:11434";
109
+ var DEFAULT_MODEL$1 = "gemma4";
106
110
  function readFile$1() {
107
111
  if (!existsSync(CONFIG_PATH)) return {};
108
112
  try {
@@ -114,8 +118,9 @@ function readFile$1() {
114
118
  function loadConfig() {
115
119
  const file = readFile$1();
116
120
  return {
117
- host: process.env.OLLAMA_HOST ?? file.host ?? DEFAULTS.host,
118
- model: process.env.OLLAMA_MODEL ?? file.model ?? DEFAULTS.model
121
+ host: process.env.OLLAMA_HOST ?? file.host ?? DEFAULT_HOST,
122
+ model: process.env.OLLAMA_MODEL ?? file.model ?? DEFAULT_MODEL$1,
123
+ searxngBaseUrl: file.searxngBaseUrl
119
124
  };
120
125
  }
121
126
  function saveConfig(patch) {
@@ -123,7 +128,7 @@ function saveConfig(patch) {
123
128
  ...readFile$1(),
124
129
  ...patch
125
130
  };
126
- mkdirSync(CONFIG_DIR, { recursive: true });
131
+ mkdirSync(CONFIG_DIRECTORY, { recursive: true });
127
132
  writeFileSync(CONFIG_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
128
133
  }
129
134
  //#endregion
@@ -184,8 +189,7 @@ function reset() {
184
189
  //#region src/utils/time.ts
185
190
  var tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
186
191
  //#endregion
187
- //#region src/utils/tools.ts
188
- var execAsync = promisify(exec);
192
+ //#region src/utils/tools/definitions.ts
189
193
  /**
190
194
  * Helper to define tool parameters
191
195
  */
@@ -274,41 +278,26 @@ var TOOLS = [
274
278
  "path",
275
279
  "start",
276
280
  "end"
277
- ])
281
+ ]),
282
+ defineTool(WEB_SEARCH, "Search the web for external or current information", { query: {
283
+ type: "string",
284
+ description: "The search query to look up"
285
+ } }, ["query"])
278
286
  ];
279
287
  var READ_TOOLS = new Set([
280
288
  READ_FILE,
281
289
  LIST_DIR,
282
290
  GREP_SEARCH,
283
- VIEW_RANGE
291
+ VIEW_RANGE,
292
+ WEB_SEARCH
284
293
  ]);
285
294
  var WRITE_TOOLS = new Set([
286
295
  WRITE_FILE,
287
296
  EDIT_FILE,
288
297
  RUN_SHELL
289
298
  ]);
290
- /**
291
- * Execute a tool by name with arguments
292
- */
293
- async function executeTool(name, args, options) {
294
- if (options?.allowedTools && !options.allowedTools.has(name)) return {
295
- content: "",
296
- error: `Tool not allowed: ${name}`
297
- };
298
- switch (name) {
299
- case READ_FILE: return readFile(args.path);
300
- case WRITE_FILE: return writeFile(args.path, args.content);
301
- case EDIT_FILE: return editFile(args.path, args.oldText, args.newText);
302
- case RUN_SHELL: return runShell(args.command);
303
- case LIST_DIR: return listDir(args.path);
304
- case GREP_SEARCH: return await grepSearch(args.pattern, args.path);
305
- case VIEW_RANGE: return viewRange(args.path, args.start, args.end);
306
- default: return {
307
- content: "",
308
- error: `Unknown tool: ${name}`
309
- };
310
- }
311
- }
299
+ //#endregion
300
+ //#region src/utils/tools/filesystem.ts
312
301
  /**
313
302
  * Read file contents
314
303
  */
@@ -367,27 +356,27 @@ function editFile(filePath, oldText, newText) {
367
356
  };
368
357
  }
369
358
  }
370
- var SHELL_EXEC_OPTIONS = {
371
- timeout: 3e4,
372
- maxBuffer: 1024 * 1024
373
- };
374
359
  /**
375
- * Execute shell command with shared options (throws on error)
376
- */
377
- function execShell(command) {
378
- return execAsync(command, SHELL_EXEC_OPTIONS);
379
- }
380
- /**
381
- * Execute shell command
360
+ * View specific line range from file
382
361
  */
383
- async function runShell(command) {
362
+ function viewRange(filePath, start, end) {
384
363
  try {
385
- const { stdout, stderr } = await execShell(command);
386
- return { content: stdout || stderr };
364
+ if (!existsSync(filePath)) return {
365
+ content: "",
366
+ error: `File not found: ${filePath}`
367
+ };
368
+ const lines = readFileSync(filePath, "utf8").split("\n");
369
+ const startIdx = Math.max(0, start - 1);
370
+ const endIdx = Math.min(lines.length, end);
371
+ if (startIdx >= lines.length || startIdx > endIdx) return {
372
+ content: "",
373
+ error: "Invalid line range"
374
+ };
375
+ return { content: lines.slice(startIdx, endIdx).join("\n") };
387
376
  } catch (error) {
388
377
  return {
389
378
  content: "",
390
- error: `Command failed: ${error instanceof Error ? error.message : String(error)}`
379
+ error: `Failed to view range: ${error instanceof Error ? error.message : String(error)}`
391
380
  };
392
381
  }
393
382
  }
@@ -414,8 +403,9 @@ function listDir(dirPath) {
414
403
  * Search for pattern in files using ripgrep if available, fallback to Node.js
415
404
  */
416
405
  async function grepSearch(pattern, dirPath) {
406
+ const { execShell } = await import("./assets/shell-CipXM_WI.js").then((n) => n.n);
417
407
  try {
418
- const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/"/g, "\\\"")}" "${dirPath}"`);
408
+ const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}" "${dirPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`);
419
409
  // v8 ignore next
420
410
  return { content: stdout || "No matches found" };
421
411
  } catch {}
@@ -451,27 +441,162 @@ async function grepSearch(pattern, dirPath) {
451
441
  };
452
442
  }
453
443
  }
444
+ //#endregion
445
+ //#region src/utils/tools/web/fetch.ts
446
+ var FETCH_TIMEOUT_MS = 1e4;
454
447
  /**
455
- * View specific line range from file
448
+ * Fetch text from URL with timeout and headers
456
449
  */
457
- function viewRange(filePath, start, end) {
450
+ async function fetchText(url, headers) {
451
+ const response = await fetch(url, {
452
+ headers: {
453
+ "user-agent": `${NAME}/${VERSION}`,
454
+ ...headers
455
+ },
456
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
457
+ });
458
+ if (!response.ok) throw new Error(`HTTP ${response.status.toString()}`);
459
+ return response.text();
460
+ }
461
+ //#endregion
462
+ //#region src/utils/tools/web/utils.ts
463
+ /**
464
+ * Strip HTML tags from a string
465
+ */
466
+ function stripTags(value) {
467
+ return value.replace(/<[^>]+>/g, " ");
468
+ }
469
+ /**
470
+ * Decode HTML entities
471
+ */
472
+ function decodeHtml(value) {
473
+ return value.replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&#x27;/g, "'").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&");
474
+ }
475
+ /**
476
+ * Clean whitespace in text
477
+ */
478
+ function cleanText(value) {
479
+ return value.replace(/\s+/g, " ").trim();
480
+ }
481
+ /**
482
+ * Truncate text to max length with ellipsis
483
+ */
484
+ function truncate(value, maxLength) {
485
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1).trimEnd()}…` : value;
486
+ }
487
+ //#endregion
488
+ //#region src/utils/tools/web/search.ts
489
+ var SEARCH_RESULT_LIMIT = 5;
490
+ async function webSearch(query) {
491
+ const trimmedQuery = query.trim();
492
+ if (!trimmedQuery) return {
493
+ content: "",
494
+ error: "Search query cannot be empty"
495
+ };
496
+ const { searxngBaseUrl } = loadConfig();
497
+ let searxngIssue = null;
498
+ if (searxngBaseUrl) try {
499
+ const searxngResults = await searchSearxng(searxngBaseUrl, trimmedQuery);
500
+ if (searxngResults.length) return { content: formatSearchResults("SearXNG", searxngResults) };
501
+ searxngIssue = "SearXNG returned no results";
502
+ } catch (error) {
503
+ searxngIssue = `SearXNG failed: ${error instanceof Error ? error.message : String(error)}`;
504
+ }
458
505
  try {
459
- if (!existsSync(filePath)) return {
460
- content: "",
461
- error: `File not found: ${filePath}`
462
- };
463
- const lines = readFileSync(filePath, "utf8").split("\n");
464
- const startIdx = Math.max(0, start - 1);
465
- const endIdx = Math.min(lines.length, end);
466
- if (startIdx >= lines.length || startIdx > endIdx) return {
467
- content: "",
468
- error: "Invalid line range"
469
- };
470
- return { content: lines.slice(startIdx, endIdx).join("\n") };
506
+ const duckDuckGoResults = await searchDuckDuckGo(trimmedQuery);
507
+ if (duckDuckGoResults.length) return { content: formatSearchResults("DuckDuckGo", duckDuckGoResults, searxngIssue ? `${searxngIssue}. Using DuckDuckGo fallback.` : void 0) };
508
+ if (searxngIssue) return { content: `No web results found. ${searxngIssue}. DuckDuckGo also returned no results.` };
509
+ return { content: "No web results found." };
471
510
  } catch (error) {
511
+ const duckDuckGoIssue = `DuckDuckGo failed: ${error instanceof Error ? error.message : String(error)}`;
472
512
  return {
473
513
  content: "",
474
- error: `Failed to view range: ${error instanceof Error ? error.message : String(error)}`
514
+ error: searxngIssue ? `${searxngIssue}; ${duckDuckGoIssue}` : duckDuckGoIssue
515
+ };
516
+ }
517
+ }
518
+ async function searchSearxng(baseUrl, query) {
519
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
520
+ const url = new URL(`${normalizedBaseUrl}/search`);
521
+ url.searchParams.set("q", query);
522
+ url.searchParams.set("format", "json");
523
+ url.searchParams.set("language", "en-US");
524
+ const response = await fetchText(url.toString(), { Accept: "application/json" });
525
+ return normalizeResults(JSON.parse(response).results?.map((result) => ({
526
+ title: result.title ?? "",
527
+ url: result.url ?? "",
528
+ snippet: result.content ?? ""
529
+ })) ?? []);
530
+ }
531
+ async function searchDuckDuckGo(query) {
532
+ const url = new URL("https://html.duckduckgo.com/html/");
533
+ url.searchParams.set("q", query);
534
+ return parseDuckDuckGoResults(await fetchText(url.toString(), { Accept: "text/html" }));
535
+ }
536
+ function parseDuckDuckGoResults(html) {
537
+ const results = [];
538
+ for (const match of html.matchAll(/<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]*class="result__snippet"[^>]*>|<div[^>]*class="result__snippet"[^>]*>)([\s\S]*?)(?:<\/a>|<\/div>)/g)) {
539
+ const url = normalizeDuckDuckGoUrl(match[1]);
540
+ const title = decodeHtml(stripTags(match[2]));
541
+ const snippet = decodeHtml(stripTags(match[3]));
542
+ if (!url || !title) continue;
543
+ results.push({
544
+ title,
545
+ url,
546
+ snippet
547
+ });
548
+ if (results.length >= SEARCH_RESULT_LIMIT) break;
549
+ }
550
+ return normalizeResults(results);
551
+ }
552
+ function normalizeDuckDuckGoUrl(url) {
553
+ try {
554
+ const parsedUrl = new URL(url, "https://duckduckgo.com");
555
+ const redirectedUrl = parsedUrl.searchParams.get("uddg");
556
+ return redirectedUrl ? decodeURIComponent(redirectedUrl) : parsedUrl.toString();
557
+ } catch {
558
+ return url;
559
+ }
560
+ }
561
+ function normalizeResults(results) {
562
+ return results.map((result) => ({
563
+ title: cleanText(result.title),
564
+ url: result.url.trim(),
565
+ snippet: cleanText(result.snippet)
566
+ })).filter((result) => result.title && result.url).slice(0, SEARCH_RESULT_LIMIT);
567
+ }
568
+ function formatSearchResults(source, results, note) {
569
+ const lines = [`Source: ${source}`];
570
+ if (note) lines.push(`Note: ${note}`);
571
+ for (const [index, result] of results.entries()) {
572
+ lines.push(`${(index + 1).toString()}. ${result.title}`);
573
+ lines.push(` URL: ${result.url}`);
574
+ if (result.snippet) lines.push(` Snippet: ${truncate(result.snippet, 240)}`);
575
+ }
576
+ return lines.join("\n");
577
+ }
578
+ //#endregion
579
+ //#region src/utils/tools/dispatcher.ts
580
+ /**
581
+ * Execute a tool by name with arguments
582
+ */
583
+ async function executeTool(name, args, options) {
584
+ if (options?.allowedTools && !options.allowedTools.has(name)) return {
585
+ content: "",
586
+ error: `Tool not allowed: ${name}`
587
+ };
588
+ switch (name) {
589
+ case READ_FILE: return readFile(args.path);
590
+ case WRITE_FILE: return writeFile(args.path, args.content);
591
+ case EDIT_FILE: return editFile(args.path, args.oldText, args.newText);
592
+ case RUN_SHELL: return runShell(args.command);
593
+ case LIST_DIR: return listDir(args.path);
594
+ case GREP_SEARCH: return await grepSearch(args.pattern, args.path);
595
+ case VIEW_RANGE: return viewRange(args.path, args.start, args.end);
596
+ case WEB_SEARCH: return await webSearch(args.query);
597
+ default: return {
598
+ content: "",
599
+ error: `Unknown tool: ${name}`
475
600
  };
476
601
  }
477
602
  }
@@ -525,7 +650,7 @@ async function processRunStream(messages, model) {
525
650
  }
526
651
  async function main(args = process.argv.slice(2)) {
527
652
  if (!args.length) {
528
- const { renderApp } = await import("./assets/tui-Bc6tEJF4.js");
653
+ const { renderApp } = await import("./assets/tui-BSnwVbDN.js");
529
654
  reset();
530
655
  renderApp();
531
656
  return;
@@ -548,4 +673,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
548
673
  if (isEntrypoint()) main();
549
674
  // v8 ignore stop
550
675
  //#endregion
551
- export { USER as _, tick as a, setClearHandler as c, loadConfig as d, saveConfig as f, SYSTEM as g, ASSISTANT as h, executeTool as i, listModels as l, withSystemMessage as m, main, TOOLS as n, clear as o, resetSystemMessage as p, WRITE_TOOLS as r, reset as s, READ_TOOLS as t, streamChat as u, PLAN_GENERATION_INSTRUCTION as v, VERSION as y };
676
+ export { USER as _, tick as a, setClearHandler as c, loadConfig as d, saveConfig as f, SYSTEM as g, ASSISTANT as h, WRITE_TOOLS as i, listModels as l, withSystemMessage as m, main, READ_TOOLS as n, clear as o, resetSystemMessage as p, TOOLS as r, reset as s, executeTool as t, streamChat as u, PLAN_GENERATION_INSTRUCTION as v, VERSION as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Ollama coding agent that runs in your terminal",
5
5
  "author": "Mark <mark@remarkablemark.org> (https://remarkablemark.org)",
6
6
  "type": "module",
@@ -62,7 +62,7 @@
62
62
  "globals": "17.6.0",
63
63
  "husky": "9.1.7",
64
64
  "ink-testing-library": "4.0.0",
65
- "lint-staged": "17.0.3",
65
+ "lint-staged": "17.0.4",
66
66
  "prettier": "3.8.3",
67
67
  "publint": "0.3.20",
68
68
  "tsx": "4.21.0",