code-ollama 0.11.0 → 0.13.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"
@@ -1209,6 +1213,9 @@ function Header({ model, onLoad }) {
1209
1213
  function ModelPicker({ currentModel, onSelect, onClose }) {
1210
1214
  const [options, setOptions] = useState([]);
1211
1215
  const [error, setError] = useState(null);
1216
+ const handleChange = useCallback((model) => {
1217
+ onSelect({ model });
1218
+ }, [onSelect]);
1212
1219
  useInput(async (_input, key) => {
1213
1220
  if (!error && options.length && key.return) {
1214
1221
  await tick();
@@ -1241,21 +1248,130 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
1241
1248
  return /* @__PURE__ */ jsx(SelectPrompt, {
1242
1249
  options,
1243
1250
  defaultValue: currentModel,
1244
- onChange: onSelect,
1251
+ onChange: handleChange,
1245
1252
  onCancel: onClose,
1246
1253
  children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Select a model" })
1247
1254
  });
1248
1255
  }
1249
1256
  //#endregion
1257
+ //#region src/components/SearchSettings.tsx
1258
+ var View = /* @__PURE__ */ function(View) {
1259
+ View["Menu"] = "menu";
1260
+ View["Edit"] = "edit";
1261
+ return View;
1262
+ }(View || {});
1263
+ var Action = /* @__PURE__ */ function(Action) {
1264
+ Action["Set"] = "set";
1265
+ Action["Clear"] = "clear";
1266
+ Action["Cancel"] = "cancel";
1267
+ return Action;
1268
+ }(Action || {});
1269
+ function SearchSettings({ currentUrl, onClose, onSave }) {
1270
+ const [view, setView] = useState(View.Menu);
1271
+ const [draftUrl, setDraftUrl] = useState(currentUrl ?? "");
1272
+ const [error, setError] = useState(null);
1273
+ const options = useMemo(() => {
1274
+ const nextOptions = [{
1275
+ label: currentUrl ? "Update SearXNG URL" : "Set SearXNG URL",
1276
+ value: Action.Set
1277
+ }];
1278
+ if (currentUrl) nextOptions.push({
1279
+ label: "Clear SearXNG URL",
1280
+ value: Action.Clear
1281
+ });
1282
+ nextOptions.push({
1283
+ label: "Cancel",
1284
+ value: Action.Cancel
1285
+ });
1286
+ return nextOptions;
1287
+ }, [currentUrl]);
1288
+ const handleChange = useCallback((value) => {
1289
+ setError(null);
1290
+ switch (value) {
1291
+ case Action.Set:
1292
+ setDraftUrl(currentUrl ?? "");
1293
+ setView(View.Edit);
1294
+ break;
1295
+ case Action.Clear:
1296
+ onSave({ searxngBaseUrl: void 0 });
1297
+ break;
1298
+ case Action.Cancel:
1299
+ default: onClose();
1300
+ }
1301
+ }, [
1302
+ currentUrl,
1303
+ onClose,
1304
+ onSave
1305
+ ]);
1306
+ const handleSubmit = useCallback((value) => {
1307
+ const trimmedValue = value.trim();
1308
+ if (!trimmedValue) {
1309
+ setError("Enter a URL or press Esc to cancel.");
1310
+ return;
1311
+ }
1312
+ try {
1313
+ const url = new URL(trimmedValue);
1314
+ if (!["http:", "https:"].includes(url.protocol)) {
1315
+ setError("URL must use http or https.");
1316
+ return;
1317
+ }
1318
+ onSave({ searxngBaseUrl: url.toString() });
1319
+ } catch {
1320
+ setError("Enter a valid URL.");
1321
+ }
1322
+ }, [onSave]);
1323
+ useInput((input, key) => {
1324
+ if (view === View.Edit && (key.escape || key.ctrl && input === "c")) {
1325
+ setDraftUrl(currentUrl ?? "");
1326
+ setError(null);
1327
+ setView(View.Menu);
1328
+ }
1329
+ });
1330
+ if (view === View.Edit) return /* @__PURE__ */ jsxs(Box, {
1331
+ flexDirection: "column",
1332
+ children: [
1333
+ /* @__PURE__ */ jsx(Text, { children: "Set the SearXNG base URL. DuckDuckGo remains the fallback." }),
1334
+ /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
1335
+ value: draftUrl,
1336
+ onChange: setDraftUrl,
1337
+ onSubmit: handleSubmit,
1338
+ placeholder: "http://localhost:8080"
1339
+ })] }),
1340
+ error && /* @__PURE__ */ jsx(Text, {
1341
+ color: "red",
1342
+ children: error
1343
+ }),
1344
+ /* @__PURE__ */ jsx(Text, {
1345
+ dimColor: true,
1346
+ children: "Press Enter to save, Esc to go back."
1347
+ })
1348
+ ]
1349
+ });
1350
+ return /* @__PURE__ */ jsxs(SelectPrompt, {
1351
+ options,
1352
+ onChange: handleChange,
1353
+ onCancel: onClose,
1354
+ children: [
1355
+ /* @__PURE__ */ jsxs(Text, { children: ["SearXNG URL: ", /* @__PURE__ */ jsx(Text, {
1356
+ color: "cyan",
1357
+ children: currentUrl ?? "not set"
1358
+ })] }),
1359
+ /* @__PURE__ */ jsx(Text, { children: "DuckDuckGo fallback remains available." }),
1360
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "Manage web search settings" })
1361
+ ]
1362
+ });
1363
+ }
1364
+ //#endregion
1250
1365
  //#region src/components/App.tsx
1251
1366
  var SCREEN = /* @__PURE__ */ function(SCREEN) {
1252
1367
  SCREEN["CHAT"] = "chat";
1253
1368
  SCREEN["MODEL_PICKER"] = "model-picker";
1369
+ SCREEN["SEARCH_SETTINGS"] = "search-settings";
1254
1370
  return SCREEN;
1255
1371
  }(SCREEN || {});
1256
1372
  function App() {
1257
1373
  const { exit } = useApp();
1258
- const [model, setModel] = useState(() => loadConfig().model);
1374
+ const [appConfig, setConfig] = useState(() => loadConfig());
1259
1375
  const [currentScreen, setScreen] = useState(SCREEN.CHAT);
1260
1376
  const [mode, setMode] = useState(SAFE);
1261
1377
  const [sessionId, setSessionId] = useState(0);
@@ -1268,6 +1384,9 @@ function App() {
1268
1384
  case "/model":
1269
1385
  setScreen(SCREEN.MODEL_PICKER);
1270
1386
  break;
1387
+ case "/search":
1388
+ setScreen(SCREEN.SEARCH_SETTINGS);
1389
+ break;
1271
1390
  case "/clear":
1272
1391
  resetSystemMessage();
1273
1392
  clear();
@@ -1279,9 +1398,12 @@ function App() {
1279
1398
  break;
1280
1399
  }
1281
1400
  }, [exit]);
1282
- const handleSelect = useCallback((selected) => {
1283
- setModel(selected);
1284
- saveConfig({ model: selected });
1401
+ const handleUpdateConfig = useCallback((update) => {
1402
+ setConfig((current) => ({
1403
+ ...current,
1404
+ ...update
1405
+ }));
1406
+ saveConfig(update);
1285
1407
  setScreen(SCREEN.CHAT);
1286
1408
  }, []);
1287
1409
  const handleClose = useCallback(() => {
@@ -1301,14 +1423,21 @@ function App() {
1301
1423
  switch (currentScreen) {
1302
1424
  case SCREEN.MODEL_PICKER:
1303
1425
  screenContent = /* @__PURE__ */ jsx(ModelPicker, {
1304
- currentModel: model,
1305
- onSelect: handleSelect,
1426
+ currentModel: appConfig.model,
1427
+ onSelect: handleUpdateConfig,
1428
+ onClose: handleClose
1429
+ });
1430
+ break;
1431
+ case SCREEN.SEARCH_SETTINGS:
1432
+ screenContent = /* @__PURE__ */ jsx(SearchSettings, {
1433
+ currentUrl: appConfig.searxngBaseUrl,
1434
+ onSave: handleUpdateConfig,
1306
1435
  onClose: handleClose
1307
1436
  });
1308
1437
  break;
1309
1438
  case SCREEN.CHAT:
1310
1439
  screenContent = /* @__PURE__ */ jsx(Chat, {
1311
- model,
1440
+ model: appConfig.model,
1312
1441
  onCommand: handleCommand,
1313
1442
  mode,
1314
1443
  onModeChange: setMode,
@@ -1320,13 +1449,13 @@ function App() {
1320
1449
  flexDirection: "column",
1321
1450
  children: [
1322
1451
  /* @__PURE__ */ jsx(Header, {
1323
- model,
1452
+ model: appConfig.model,
1324
1453
  onLoad: handleHeaderLoad
1325
1454
  }),
1326
1455
  isHeaderLoaded && screenContent,
1327
1456
  /* @__PURE__ */ jsx(Footer, {
1328
1457
  mode,
1329
- model,
1458
+ model: appConfig.model,
1330
1459
  onToggleMode: handleToggleMode
1331
1460
  })
1332
1461
  ]
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.13.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,8 @@ 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";
71
+ var WEB_FETCH = "web_fetch";
65
72
  //#endregion
66
73
  //#region src/utils/agents.ts
67
74
  var AGENTS_FILE = "AGENTS.md";
@@ -97,12 +104,10 @@ function withSystemMessage(messages) {
97
104
  }
98
105
  //#endregion
99
106
  //#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
- };
107
+ var CONFIG_DIRECTORY = join(homedir(), `.${NAME}`);
108
+ var CONFIG_PATH = join(CONFIG_DIRECTORY, "config.json");
109
+ var DEFAULT_HOST = "http://localhost:11434";
110
+ var DEFAULT_MODEL$1 = "gemma4";
106
111
  function readFile$1() {
107
112
  if (!existsSync(CONFIG_PATH)) return {};
108
113
  try {
@@ -114,8 +119,9 @@ function readFile$1() {
114
119
  function loadConfig() {
115
120
  const file = readFile$1();
116
121
  return {
117
- host: process.env.OLLAMA_HOST ?? file.host ?? DEFAULTS.host,
118
- model: process.env.OLLAMA_MODEL ?? file.model ?? DEFAULTS.model
122
+ host: process.env.OLLAMA_HOST ?? file.host ?? DEFAULT_HOST,
123
+ model: process.env.OLLAMA_MODEL ?? file.model ?? DEFAULT_MODEL$1,
124
+ searxngBaseUrl: file.searxngBaseUrl
119
125
  };
120
126
  }
121
127
  function saveConfig(patch) {
@@ -123,7 +129,7 @@ function saveConfig(patch) {
123
129
  ...readFile$1(),
124
130
  ...patch
125
131
  };
126
- mkdirSync(CONFIG_DIR, { recursive: true });
132
+ mkdirSync(CONFIG_DIRECTORY, { recursive: true });
127
133
  writeFileSync(CONFIG_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
128
134
  }
129
135
  //#endregion
@@ -184,8 +190,7 @@ function reset() {
184
190
  //#region src/utils/time.ts
185
191
  var tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
186
192
  //#endregion
187
- //#region src/utils/tools.ts
188
- var execAsync = promisify(exec);
193
+ //#region src/utils/tools/definitions.ts
189
194
  /**
190
195
  * Helper to define tool parameters
191
196
  */
@@ -274,41 +279,31 @@ var TOOLS = [
274
279
  "path",
275
280
  "start",
276
281
  "end"
277
- ])
282
+ ]),
283
+ defineTool(WEB_SEARCH, "Search the web for external or current information", { query: {
284
+ type: "string",
285
+ description: "The search query to look up"
286
+ } }, ["query"]),
287
+ defineTool(WEB_FETCH, "Fetch the readable content of a webpage at the given URL", { url: {
288
+ type: "string",
289
+ description: "The full URL of the page to fetch"
290
+ } }, ["url"])
278
291
  ];
279
292
  var READ_TOOLS = new Set([
280
293
  READ_FILE,
281
294
  LIST_DIR,
282
295
  GREP_SEARCH,
283
- VIEW_RANGE
296
+ VIEW_RANGE,
297
+ WEB_SEARCH,
298
+ WEB_FETCH
284
299
  ]);
285
300
  var WRITE_TOOLS = new Set([
286
301
  WRITE_FILE,
287
302
  EDIT_FILE,
288
303
  RUN_SHELL
289
304
  ]);
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
- }
305
+ //#endregion
306
+ //#region src/utils/tools/filesystem.ts
312
307
  /**
313
308
  * Read file contents
314
309
  */
@@ -367,27 +362,27 @@ function editFile(filePath, oldText, newText) {
367
362
  };
368
363
  }
369
364
  }
370
- var SHELL_EXEC_OPTIONS = {
371
- timeout: 3e4,
372
- maxBuffer: 1024 * 1024
373
- };
374
- /**
375
- * Execute shell command with shared options (throws on error)
376
- */
377
- function execShell(command) {
378
- return execAsync(command, SHELL_EXEC_OPTIONS);
379
- }
380
365
  /**
381
- * Execute shell command
366
+ * View specific line range from file
382
367
  */
383
- async function runShell(command) {
368
+ function viewRange(filePath, start, end) {
384
369
  try {
385
- const { stdout, stderr } = await execShell(command);
386
- return { content: stdout || stderr };
370
+ if (!existsSync(filePath)) return {
371
+ content: "",
372
+ error: `File not found: ${filePath}`
373
+ };
374
+ const lines = readFileSync(filePath, "utf8").split("\n");
375
+ const startIdx = Math.max(0, start - 1);
376
+ const endIdx = Math.min(lines.length, end);
377
+ if (startIdx >= lines.length || startIdx > endIdx) return {
378
+ content: "",
379
+ error: "Invalid line range"
380
+ };
381
+ return { content: lines.slice(startIdx, endIdx).join("\n") };
387
382
  } catch (error) {
388
383
  return {
389
384
  content: "",
390
- error: `Command failed: ${error instanceof Error ? error.message : String(error)}`
385
+ error: `Failed to view range: ${error instanceof Error ? error.message : String(error)}`
391
386
  };
392
387
  }
393
388
  }
@@ -414,8 +409,9 @@ function listDir(dirPath) {
414
409
  * Search for pattern in files using ripgrep if available, fallback to Node.js
415
410
  */
416
411
  async function grepSearch(pattern, dirPath) {
412
+ const { execShell } = await import("./assets/shell-CipXM_WI.js").then((n) => n.n);
417
413
  try {
418
- const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/"/g, "\\\"")}" "${dirPath}"`);
414
+ const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}" "${dirPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`);
419
415
  // v8 ignore next
420
416
  return { content: stdout || "No matches found" };
421
417
  } catch {}
@@ -451,27 +447,202 @@ async function grepSearch(pattern, dirPath) {
451
447
  };
452
448
  }
453
449
  }
450
+ //#endregion
451
+ //#region src/utils/tools/web/fetch.ts
452
+ var FETCH_TIMEOUT_MS = 1e4;
453
+ var BASE_HEADERS = { "user-agent": `${NAME}/${VERSION}` };
454
454
  /**
455
- * View specific line range from file
455
+ * Fetch text from URL with timeout and headers
456
456
  */
457
- function viewRange(filePath, start, end) {
457
+ async function fetchText(url, headers) {
458
+ const response = await fetch(url, {
459
+ headers: {
460
+ ...BASE_HEADERS,
461
+ ...headers
462
+ },
463
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
464
+ });
465
+ if (!response.ok) throw new Error(`HTTP ${response.status.toString()}`);
466
+ return response.text();
467
+ }
468
+ /**
469
+ * Fetch and parse JSON from URL with timeout and headers
470
+ */
471
+ async function fetchJSON(url, headers = {}) {
472
+ const response = await fetch(url, {
473
+ headers: {
474
+ ...BASE_HEADERS,
475
+ Accept: "application/json",
476
+ ...headers
477
+ },
478
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
479
+ });
480
+ if (!response.ok) throw new Error(`HTTP ${response.status.toString()}`);
481
+ return response.json();
482
+ }
483
+ //#endregion
484
+ //#region src/utils/tools/web/utils.ts
485
+ /**
486
+ * Strip HTML tags from a string
487
+ */
488
+ function stripTags(value) {
489
+ return value.replace(/<[^>]+>/g, " ");
490
+ }
491
+ /**
492
+ * Decode HTML entities
493
+ */
494
+ function decodeHtml(value) {
495
+ return value.replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&#x27;/g, "'").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&");
496
+ }
497
+ /**
498
+ * Clean whitespace in text
499
+ */
500
+ function cleanText(value) {
501
+ return value.replace(/\s+/g, " ").trim();
502
+ }
503
+ /**
504
+ * Truncate text to max length with ellipsis
505
+ */
506
+ function truncate(value, maxLength) {
507
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1).trimEnd()}…` : value;
508
+ }
509
+ //#endregion
510
+ //#region src/utils/tools/web/fetch-page.ts
511
+ var JINA_READER_BASE_URL = "https://r.jina.ai/";
512
+ /**
513
+ * Fetch readable page content via Jina Reader, with fallback to direct fetch + HTML stripping
514
+ */
515
+ async function webFetch(url) {
516
+ const trimmedUrl = url.trim();
517
+ if (!trimmedUrl) return {
518
+ content: "",
519
+ error: "URL cannot be empty"
520
+ };
458
521
  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 {
522
+ return { content: await fetchText(`${JINA_READER_BASE_URL}${trimmedUrl}`, { Accept: "text/plain" }) };
523
+ } catch {}
524
+ try {
525
+ return { content: `Note: Jina Reader unavailable, falling back to raw fetch.\n\n${cleanText(stripTags(await fetchText(trimmedUrl, { Accept: "text/html" })))}` };
526
+ } catch (error) {
527
+ return {
467
528
  content: "",
468
- error: "Invalid line range"
529
+ error: `Failed to fetch page: ${error instanceof Error ? error.message : String(error)}`
469
530
  };
470
- return { content: lines.slice(startIdx, endIdx).join("\n") };
531
+ }
532
+ }
533
+ //#endregion
534
+ //#region src/utils/tools/web/search.ts
535
+ var SEARCH_RESULT_LIMIT = 5;
536
+ async function webSearch(query) {
537
+ const trimmedQuery = query.trim();
538
+ if (!trimmedQuery) return {
539
+ content: "",
540
+ error: "Search query cannot be empty"
541
+ };
542
+ const { searxngBaseUrl } = loadConfig();
543
+ let searxngIssue = null;
544
+ if (searxngBaseUrl) try {
545
+ const searxngResults = await searchSearXNG(searxngBaseUrl, trimmedQuery);
546
+ if (searxngResults.length) return { content: formatSearchResults("SearXNG", searxngResults) };
547
+ searxngIssue = "SearXNG returned no results";
548
+ } catch (error) {
549
+ searxngIssue = `SearXNG failed: ${error instanceof Error ? error.message : String(error)}`;
550
+ }
551
+ try {
552
+ const duckDuckGoResults = await searchDuckDuckGo(trimmedQuery);
553
+ if (duckDuckGoResults.length) return { content: formatSearchResults("DuckDuckGo", duckDuckGoResults, searxngIssue ? `${searxngIssue}. Using DuckDuckGo fallback.` : void 0) };
554
+ if (searxngIssue) return { content: `No web results found. ${searxngIssue}. DuckDuckGo also returned no results.` };
555
+ return { content: "No web results found." };
471
556
  } catch (error) {
557
+ const duckDuckGoIssue = `DuckDuckGo failed: ${error instanceof Error ? error.message : String(error)}`;
472
558
  return {
473
559
  content: "",
474
- error: `Failed to view range: ${error instanceof Error ? error.message : String(error)}`
560
+ error: searxngIssue ? `${searxngIssue}; ${duckDuckGoIssue}` : duckDuckGoIssue
561
+ };
562
+ }
563
+ }
564
+ async function searchSearXNG(baseUrl, query) {
565
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
566
+ const url = new URL(`${normalizedBaseUrl}/search`);
567
+ url.searchParams.set("q", query);
568
+ url.searchParams.set("format", "json");
569
+ url.searchParams.set("language", "en-US");
570
+ return normalizeResults((await fetchJSON(url.toString())).results?.map((result) => ({
571
+ title: result.title ?? "",
572
+ url: result.url ?? "",
573
+ snippet: result.content ?? ""
574
+ })) ?? []);
575
+ }
576
+ async function searchDuckDuckGo(query) {
577
+ const url = new URL("https://html.duckduckgo.com/html/");
578
+ url.searchParams.set("q", query);
579
+ return parseDuckDuckGoResults(await fetchText(url.toString(), { Accept: "text/html" }));
580
+ }
581
+ function parseDuckDuckGoResults(html) {
582
+ const results = [];
583
+ 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)) {
584
+ const url = normalizeDuckDuckGoUrl(match[1]);
585
+ const title = decodeHtml(stripTags(match[2]));
586
+ const snippet = decodeHtml(stripTags(match[3]));
587
+ if (!url || !title) continue;
588
+ results.push({
589
+ title,
590
+ url,
591
+ snippet
592
+ });
593
+ if (results.length >= SEARCH_RESULT_LIMIT) break;
594
+ }
595
+ return normalizeResults(results);
596
+ }
597
+ function normalizeDuckDuckGoUrl(url) {
598
+ try {
599
+ const parsedUrl = new URL(url, "https://duckduckgo.com");
600
+ const redirectedUrl = parsedUrl.searchParams.get("uddg");
601
+ return redirectedUrl ? decodeURIComponent(redirectedUrl) : parsedUrl.toString();
602
+ } catch {
603
+ return url;
604
+ }
605
+ }
606
+ function normalizeResults(results) {
607
+ return results.map((result) => ({
608
+ title: cleanText(result.title),
609
+ url: result.url.trim(),
610
+ snippet: cleanText(result.snippet)
611
+ })).filter((result) => result.title && result.url).slice(0, SEARCH_RESULT_LIMIT);
612
+ }
613
+ function formatSearchResults(source, results, note) {
614
+ const lines = [`Source: ${source}`];
615
+ if (note) lines.push(`Note: ${note}`);
616
+ for (const [index, result] of results.entries()) {
617
+ lines.push(`${(index + 1).toString()}. ${result.title}`);
618
+ lines.push(` URL: ${result.url}`);
619
+ if (result.snippet) lines.push(` Snippet: ${truncate(result.snippet, 240)}`);
620
+ }
621
+ return lines.join("\n");
622
+ }
623
+ //#endregion
624
+ //#region src/utils/tools/dispatcher.ts
625
+ /**
626
+ * Execute a tool by name with arguments
627
+ */
628
+ async function executeTool(name, args, options) {
629
+ if (options?.allowedTools && !options.allowedTools.has(name)) return {
630
+ content: "",
631
+ error: `Tool not allowed: ${name}`
632
+ };
633
+ switch (name) {
634
+ case READ_FILE: return readFile(args.path);
635
+ case WRITE_FILE: return writeFile(args.path, args.content);
636
+ case EDIT_FILE: return editFile(args.path, args.oldText, args.newText);
637
+ case RUN_SHELL: return runShell(args.command);
638
+ case LIST_DIR: return listDir(args.path);
639
+ case GREP_SEARCH: return await grepSearch(args.pattern, args.path);
640
+ case VIEW_RANGE: return viewRange(args.path, args.start, args.end);
641
+ case WEB_SEARCH: return await webSearch(args.query);
642
+ case WEB_FETCH: return await webFetch(args.url);
643
+ default: return {
644
+ content: "",
645
+ error: `Unknown tool: ${name}`
475
646
  };
476
647
  }
477
648
  }
@@ -525,7 +696,7 @@ async function processRunStream(messages, model) {
525
696
  }
526
697
  async function main(args = process.argv.slice(2)) {
527
698
  if (!args.length) {
528
- const { renderApp } = await import("./assets/tui-Bc6tEJF4.js");
699
+ const { renderApp } = await import("./assets/tui-ByqNs9kx.js");
529
700
  reset();
530
701
  renderApp();
531
702
  return;
@@ -548,4 +719,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
548
719
  if (isEntrypoint()) main();
549
720
  // v8 ignore stop
550
721
  //#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 };
722
+ 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.13.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",