code-ollama 0.12.0 → 0.13.1

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.
@@ -113,10 +113,100 @@ var CodeBlock = memo(function CodeBlock({ code, language, role }) {
113
113
  });
114
114
  });
115
115
  //#endregion
116
+ //#region src/components/Markdown/extensions.ts
117
+ var LATEX_COMMANDS = {
118
+ "\\rightarrow": "→",
119
+ "\\leftarrow": "←",
120
+ "\\Rightarrow": "⇒",
121
+ "\\Leftarrow": "⇐",
122
+ "\\leftrightarrow": "↔",
123
+ "\\Leftrightarrow": "⟺",
124
+ "\\uparrow": "↑",
125
+ "\\downarrow": "↓",
126
+ "\\to": "→",
127
+ "\\gets": "←",
128
+ "\\times": "×",
129
+ "\\div": "÷",
130
+ "\\pm": "±",
131
+ "\\leq": "≤",
132
+ "\\geq": "≥",
133
+ "\\neq": "≠",
134
+ "\\approx": "≈",
135
+ "\\equiv": "≡",
136
+ "\\infty": "∞",
137
+ "\\sum": "∑",
138
+ "\\prod": "∏",
139
+ "\\sqrt": "√",
140
+ "\\partial": "∂",
141
+ "\\nabla": "∇",
142
+ "\\in": "∈",
143
+ "\\notin": "∉",
144
+ "\\subset": "⊂",
145
+ "\\supset": "⊃",
146
+ "\\cup": "∪",
147
+ "\\cap": "∩",
148
+ "\\emptyset": "∅",
149
+ "\\alpha": "α",
150
+ "\\beta": "β",
151
+ "\\gamma": "γ",
152
+ "\\delta": "δ",
153
+ "\\epsilon": "ε",
154
+ "\\theta": "θ",
155
+ "\\lambda": "λ",
156
+ "\\mu": "μ",
157
+ "\\pi": "π",
158
+ "\\sigma": "σ",
159
+ "\\tau": "τ",
160
+ "\\phi": "φ",
161
+ "\\omega": "ω",
162
+ "\\$": "$",
163
+ "\\%": "%",
164
+ "\\&": "&",
165
+ "\\#": "#",
166
+ "\\{": "{",
167
+ "\\}": "}",
168
+ "\\^": "^",
169
+ "\\_": "_",
170
+ "\\cdot": "·",
171
+ "\\ldots": "…",
172
+ "\\cdots": "⋯"
173
+ };
174
+ function convertLatex(math) {
175
+ let result = math.trim();
176
+ for (const [cmd, unicode] of Object.entries(LATEX_COMMANDS)) result = result.replaceAll(cmd, unicode);
177
+ result = result.replace(/\\frac\{([^}]*)\}\{([^}]*)\}/g, "$1/$2");
178
+ result = result.replace(/\^\{([^}]*)\}/g, "^$1");
179
+ result = result.replace(/_\{([^}]*)\}/g, "_$1");
180
+ result = result.replace(/\\[,;!: ]/g, " ");
181
+ result = result.replace(/\\[a-zA-Z]+\{([^}]*)\}/g, "$1");
182
+ result = result.replace(/\\[a-zA-Z]+/g, "");
183
+ return result.trim();
184
+ }
185
+ var inlineMathExtension = {
186
+ name: "inlineMath",
187
+ level: "inline",
188
+ start: (src) => src.indexOf("$"),
189
+ tokenizer(src) {
190
+ const match = /^\$([^$\n]+?)\$/.exec(src);
191
+ if (match) return {
192
+ type: "inlineMath",
193
+ raw: match[0],
194
+ math: match[1]
195
+ };
196
+ },
197
+ renderer(token) {
198
+ // v8 ignore next
199
+ return convertLatex(token.math ?? "");
200
+ }
201
+ };
202
+ //#endregion
116
203
  //#region src/components/Markdown/Markdown.tsx
117
204
  var HR_PLACEHOLDER = "__CODE_OLLAMA_HR_PLACEHOLDER__";
118
205
  marked.use(markedTerminal({ theme: "gitHub" }));
119
- marked.use({ renderer: { hr: () => `${HR_PLACEHOLDER}\n` } });
206
+ marked.use({
207
+ extensions: [inlineMathExtension],
208
+ renderer: { hr: () => `${HR_PLACEHOLDER}\n` }
209
+ });
120
210
  function renderMarkdown(content, hrWidth) {
121
211
  const hr = "─".repeat(Math.max(1, hrWidth));
122
212
  try {
@@ -1213,6 +1303,9 @@ function Header({ model, onLoad }) {
1213
1303
  function ModelPicker({ currentModel, onSelect, onClose }) {
1214
1304
  const [options, setOptions] = useState([]);
1215
1305
  const [error, setError] = useState(null);
1306
+ const handleChange = useCallback((model) => {
1307
+ onSelect({ model });
1308
+ }, [onSelect]);
1216
1309
  useInput(async (_input, key) => {
1217
1310
  if (!error && options.length && key.return) {
1218
1311
  await tick();
@@ -1245,7 +1338,7 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
1245
1338
  return /* @__PURE__ */ jsx(SelectPrompt, {
1246
1339
  options,
1247
1340
  defaultValue: currentModel,
1248
- onChange: onSelect,
1341
+ onChange: handleChange,
1249
1342
  onCancel: onClose,
1250
1343
  children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Select a model" })
1251
1344
  });
@@ -1290,7 +1383,7 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1290
1383
  setView(View.Edit);
1291
1384
  break;
1292
1385
  case Action.Clear:
1293
- onSave(void 0);
1386
+ onSave({ searxngBaseUrl: void 0 });
1294
1387
  break;
1295
1388
  case Action.Cancel:
1296
1389
  default: onClose();
@@ -1312,7 +1405,7 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
1312
1405
  setError("URL must use http or https.");
1313
1406
  return;
1314
1407
  }
1315
- onSave(url.toString());
1408
+ onSave({ searxngBaseUrl: url.toString() });
1316
1409
  } catch {
1317
1410
  setError("Enter a valid URL.");
1318
1411
  }
@@ -1368,12 +1461,11 @@ var SCREEN = /* @__PURE__ */ function(SCREEN) {
1368
1461
  }(SCREEN || {});
1369
1462
  function App() {
1370
1463
  const { exit } = useApp();
1371
- const [appConfig, setAppConfig] = useState(() => loadConfig());
1464
+ const [appConfig, setConfig] = useState(() => loadConfig());
1372
1465
  const [currentScreen, setScreen] = useState(SCREEN.CHAT);
1373
1466
  const [mode, setMode] = useState(SAFE);
1374
1467
  const [sessionId, setSessionId] = useState(0);
1375
1468
  const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
1376
- const { model, searxngBaseUrl } = appConfig;
1377
1469
  const handleHeaderLoad = useCallback(() => {
1378
1470
  setIsHeaderLoaded(true);
1379
1471
  }, []);
@@ -1396,20 +1488,12 @@ function App() {
1396
1488
  break;
1397
1489
  }
1398
1490
  }, [exit]);
1399
- const handleSelect = useCallback((selected) => {
1400
- setAppConfig((currentConfig) => ({
1401
- ...currentConfig,
1402
- model: selected
1403
- }));
1404
- saveConfig({ model: selected });
1405
- setScreen(SCREEN.CHAT);
1406
- }, []);
1407
- const handleSaveSearch = useCallback((url) => {
1408
- setAppConfig((currentConfig) => ({
1409
- ...currentConfig,
1410
- searxngBaseUrl: url
1491
+ const handleUpdateConfig = useCallback((update) => {
1492
+ setConfig((current) => ({
1493
+ ...current,
1494
+ ...update
1411
1495
  }));
1412
- saveConfig({ searxngBaseUrl: url });
1496
+ saveConfig(update);
1413
1497
  setScreen(SCREEN.CHAT);
1414
1498
  }, []);
1415
1499
  const handleClose = useCallback(() => {
@@ -1429,21 +1513,21 @@ function App() {
1429
1513
  switch (currentScreen) {
1430
1514
  case SCREEN.MODEL_PICKER:
1431
1515
  screenContent = /* @__PURE__ */ jsx(ModelPicker, {
1432
- currentModel: model,
1433
- onSelect: handleSelect,
1516
+ currentModel: appConfig.model,
1517
+ onSelect: handleUpdateConfig,
1434
1518
  onClose: handleClose
1435
1519
  });
1436
1520
  break;
1437
1521
  case SCREEN.SEARCH_SETTINGS:
1438
1522
  screenContent = /* @__PURE__ */ jsx(SearchSettings, {
1439
- currentUrl: searxngBaseUrl,
1440
- onSave: handleSaveSearch,
1523
+ currentUrl: appConfig.searxngBaseUrl,
1524
+ onSave: handleUpdateConfig,
1441
1525
  onClose: handleClose
1442
1526
  });
1443
1527
  break;
1444
1528
  case SCREEN.CHAT:
1445
1529
  screenContent = /* @__PURE__ */ jsx(Chat, {
1446
- model,
1530
+ model: appConfig.model,
1447
1531
  onCommand: handleCommand,
1448
1532
  mode,
1449
1533
  onModeChange: setMode,
@@ -1455,13 +1539,13 @@ function App() {
1455
1539
  flexDirection: "column",
1456
1540
  children: [
1457
1541
  /* @__PURE__ */ jsx(Header, {
1458
- model,
1542
+ model: appConfig.model,
1459
1543
  onLoad: handleHeaderLoad
1460
1544
  }),
1461
1545
  isHeaderLoaded && screenContent,
1462
1546
  /* @__PURE__ */ jsx(Footer, {
1463
1547
  mode,
1464
- model,
1548
+ model: appConfig.model,
1465
1549
  onToggleMode: handleToggleMode
1466
1550
  })
1467
1551
  ]
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { homedir } from "node:os";
7
7
  import { Ollama } from "ollama";
8
8
  //#region package.json
9
9
  var name = "code-ollama";
10
- var version = "0.12.0";
10
+ var version = "0.13.1";
11
11
  //#endregion
12
12
  //#region src/constants/package.ts
13
13
  var NAME = name;
@@ -68,6 +68,7 @@ var LIST_DIR = "list_dir";
68
68
  var GREP_SEARCH = "grep_search";
69
69
  var VIEW_RANGE = "view_range";
70
70
  var WEB_SEARCH = "web_search";
71
+ var WEB_FETCH = "web_fetch";
71
72
  //#endregion
72
73
  //#region src/utils/agents.ts
73
74
  var AGENTS_FILE = "AGENTS.md";
@@ -282,14 +283,19 @@ var TOOLS = [
282
283
  defineTool(WEB_SEARCH, "Search the web for external or current information", { query: {
283
284
  type: "string",
284
285
  description: "The search query to look up"
285
- } }, ["query"])
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"])
286
291
  ];
287
292
  var READ_TOOLS = new Set([
288
293
  READ_FILE,
289
294
  LIST_DIR,
290
295
  GREP_SEARCH,
291
296
  VIEW_RANGE,
292
- WEB_SEARCH
297
+ WEB_SEARCH,
298
+ WEB_FETCH
293
299
  ]);
294
300
  var WRITE_TOOLS = new Set([
295
301
  WRITE_FILE,
@@ -444,13 +450,14 @@ async function grepSearch(pattern, dirPath) {
444
450
  //#endregion
445
451
  //#region src/utils/tools/web/fetch.ts
446
452
  var FETCH_TIMEOUT_MS = 1e4;
453
+ var BASE_HEADERS = { "user-agent": `${NAME}/${VERSION}` };
447
454
  /**
448
455
  * Fetch text from URL with timeout and headers
449
456
  */
450
457
  async function fetchText(url, headers) {
451
458
  const response = await fetch(url, {
452
459
  headers: {
453
- "user-agent": `${NAME}/${VERSION}`,
460
+ ...BASE_HEADERS,
454
461
  ...headers
455
462
  },
456
463
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
@@ -458,6 +465,21 @@ async function fetchText(url, headers) {
458
465
  if (!response.ok) throw new Error(`HTTP ${response.status.toString()}`);
459
466
  return response.text();
460
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
+ }
461
483
  //#endregion
462
484
  //#region src/utils/tools/web/utils.ts
463
485
  /**
@@ -485,6 +507,30 @@ function truncate(value, maxLength) {
485
507
  return value.length > maxLength ? `${value.slice(0, maxLength - 1).trimEnd()}…` : value;
486
508
  }
487
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
+ };
521
+ try {
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 {
528
+ content: "",
529
+ error: `Failed to fetch page: ${error instanceof Error ? error.message : String(error)}`
530
+ };
531
+ }
532
+ }
533
+ //#endregion
488
534
  //#region src/utils/tools/web/search.ts
489
535
  var SEARCH_RESULT_LIMIT = 5;
490
536
  async function webSearch(query) {
@@ -496,7 +542,7 @@ async function webSearch(query) {
496
542
  const { searxngBaseUrl } = loadConfig();
497
543
  let searxngIssue = null;
498
544
  if (searxngBaseUrl) try {
499
- const searxngResults = await searchSearxng(searxngBaseUrl, trimmedQuery);
545
+ const searxngResults = await searchSearXNG(searxngBaseUrl, trimmedQuery);
500
546
  if (searxngResults.length) return { content: formatSearchResults("SearXNG", searxngResults) };
501
547
  searxngIssue = "SearXNG returned no results";
502
548
  } catch (error) {
@@ -515,14 +561,13 @@ async function webSearch(query) {
515
561
  };
516
562
  }
517
563
  }
518
- async function searchSearxng(baseUrl, query) {
564
+ async function searchSearXNG(baseUrl, query) {
519
565
  const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
520
566
  const url = new URL(`${normalizedBaseUrl}/search`);
521
567
  url.searchParams.set("q", query);
522
568
  url.searchParams.set("format", "json");
523
569
  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) => ({
570
+ return normalizeResults((await fetchJSON(url.toString())).results?.map((result) => ({
526
571
  title: result.title ?? "",
527
572
  url: result.url ?? "",
528
573
  snippet: result.content ?? ""
@@ -594,6 +639,7 @@ async function executeTool(name, args, options) {
594
639
  case GREP_SEARCH: return await grepSearch(args.pattern, args.path);
595
640
  case VIEW_RANGE: return viewRange(args.path, args.start, args.end);
596
641
  case WEB_SEARCH: return await webSearch(args.query);
642
+ case WEB_FETCH: return await webFetch(args.url);
597
643
  default: return {
598
644
  content: "",
599
645
  error: `Unknown tool: ${name}`
@@ -650,7 +696,7 @@ async function processRunStream(messages, model) {
650
696
  }
651
697
  async function main(args = process.argv.slice(2)) {
652
698
  if (!args.length) {
653
- const { renderApp } = await import("./assets/tui-BSnwVbDN.js");
699
+ const { renderApp } = await import("./assets/tui-CCa_vqh0.js");
654
700
  reset();
655
701
  renderApp();
656
702
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
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",