code-ollama 0.20.0 → 0.21.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.
package/README.md CHANGED
@@ -21,6 +21,9 @@
21
21
  npx code-ollama
22
22
  ```
23
23
 
24
+ > [!IMPORTANT]
25
+ > If you see an error that says server/model is unavailable, then follow these [steps](https://github.com/ai-action/code-ollama/wiki/Ollama).
26
+
24
27
  ## Install
25
28
 
26
29
  Install the [CLI](https://www.npmjs.com/package/code-ollama) globally:
@@ -1,4 +1,4 @@
1
- import { A as WARNING, B as LABEL, C as loadConfig, D as resetSystemMessage, E as saveClipboardImage, F as USER, G as VERSION, H as SAFE, I as PLAN_GENERATION_INSTRUCTION, K as LIST, L as BACK, M as getTheme, N as ASSISTANT, O as withSystemMessage, P as SYSTEM, R as CATALOG, S as streamChat, T as removeClipboardImage, U as APPROVE, V as PLAN, W as REJECT, _ as setClearHandler, a as tick, b as listModels, c as appendMessage, d as deleteSessionIfEmpty, f as listSessions, g as reset, h as clear, i as WRITE_TOOLS, j as LIST$1, k as HEADER_PREFIX, l as createSession, m as updateSessionModel, n as READ_TOOLS, o as color, p as loadSession, r as TOOLS, s as write, t as executeTool, u as deleteSession, v as checkHealth, w as saveConfig, x as pullModel, y as deleteModel, z as AUTO } from "../cli.js";
1
+ import { A as saveClipboardImage, B as PLAN_GENERATION_INSTRUCTION, C as deleteModel, D as loadConfig, E as streamChat, F as LIST$1, G as PLAN, H as CATALOG, I as getTheme, J as REJECT, K as SAFE, L as ASSISTANT, M as withSystemMessage, N as HEADER_PREFIX, O as saveConfig, P as WARNING, R as SYSTEM, S as checkHealth, T as pullModel, U as AUTO, V as BACK, W as LABEL, X as VERSION, Y as NAME, Z as LIST, _ as loadSession, a as normalizeToolCall, b as reset, c as WRITE_TOOLS, d as write, f as appendMessage, g as listSessions, h as deleteSessionIfEmpty, i as formatToolResultContent, j as resetSystemMessage, k as removeClipboardImage, l as tick, m as deleteSession, n as executeTool, o as READ_TOOLS, p as createSession, q as APPROVE, r as executeToolCall, s as TOOLS, t as checkForUpdate, u as color, v as updateSessionModel, w as listModels, x as setClearHandler, y as clear, z as USER } from "../cli.js";
2
2
  import { existsSync, readdirSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { basename, extname, isAbsolute, join, relative, resolve } from "node:path";
@@ -1362,6 +1362,7 @@ function hasExecutablePlan(content) {
1362
1362
  }
1363
1363
  //#endregion
1364
1364
  //#region src/components/Chat/Chat.tsx
1365
+ var MAX_TOOL_TURNS = 25;
1365
1366
  function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId, theme = getTheme() }) {
1366
1367
  const sessionMessages = initialMessages ?? [];
1367
1368
  const history = useMemo(() => sessionMessages.flatMap(({ role, content }) => role === "user" && !content.startsWith("/") ? [content] : []), [sessionMessages]);
@@ -1400,7 +1401,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1400
1401
  };
1401
1402
  return {
1402
1403
  role: SYSTEM,
1403
- content: `Tool ${toolName} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
1404
+ content: formatToolResultContent(toolName, result)
1404
1405
  };
1405
1406
  }, []);
1406
1407
  const buildPlanModeCorrectionMessage = useCallback((toolName) => ({
@@ -1425,72 +1426,112 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1425
1426
  }]);
1426
1427
  }, []);
1427
1428
  const processStream = useCallback(async (currentMessages, executionMode = mode) => {
1429
+ const modelName = model;
1428
1430
  // v8 ignore next
1429
- if (!model) throw new Error("Model is required");
1431
+ if (!modelName) throw new Error("Model is required");
1430
1432
  const controller = new AbortController();
1431
1433
  abortControllerRef.current = controller;
1432
- const assistantMessage = {
1433
- role: ASSISTANT,
1434
- content: ""
1435
- };
1436
- let committedMessages = currentMessages;
1437
- let assistantCommitted = false;
1438
- const commitAssistantMessage = () => {
1439
- if (assistantCommitted) {
1440
- // v8 ignore next
1441
- if (committedMessages.at(-1)?.role === "assistant") {
1442
- committedMessages = [...committedMessages.slice(0, -1), { ...assistantMessage }];
1443
- setMessages(committedMessages);
1444
- }
1445
- return committedMessages;
1446
- }
1447
- assistantCommitted = true;
1448
- setStreamingMessage(null);
1449
- if (!assistantMessage.content) {
1450
- setMessages(committedMessages);
1451
- return committedMessages;
1452
- }
1453
- committedMessages = [...committedMessages, { ...assistantMessage }];
1454
- setMessages(committedMessages);
1455
- return committedMessages;
1456
- };
1457
- setStreamingMessage(assistantMessage);
1434
+ let activeMessages = currentMessages;
1435
+ let toolTurns = 0;
1458
1436
  try {
1459
- for await (const chunk of streamChat(withSystemMessage(currentMessages), model, TOOLS, controller.signal)) {
1460
- // v8 ignore next 3
1461
- if (controller.signal.aborted) return;
1462
- if (chunk.type === "content") {
1463
- assistantMessage.content += chunk.content;
1464
- setStreamingMessage({ ...assistantMessage });
1465
- } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
1466
- const requiresApproval = WRITE_TOOLS.has(toolCall.function.name);
1437
+ while (!controller.signal.aborted) {
1438
+ const assistantMessage = {
1439
+ role: ASSISTANT,
1440
+ content: ""
1441
+ };
1442
+ let committedMessages = activeMessages;
1443
+ let assistantCommitted = false;
1444
+ const commitAssistantMessage = () => {
1467
1445
  // v8 ignore start
1468
- const allowedTools = executionMode === "plan" ? READ_TOOLS : void 0;
1446
+ if (assistantCommitted) {
1447
+ if (committedMessages.at(-1)?.role === "assistant") {
1448
+ committedMessages = [...committedMessages.slice(0, -1), { ...assistantMessage }];
1449
+ setMessages(committedMessages);
1450
+ }
1451
+ return committedMessages;
1452
+ }
1469
1453
  // v8 ignore stop
1454
+ assistantCommitted = true;
1455
+ setStreamingMessage(null);
1456
+ if (!assistantMessage.content) {
1457
+ setMessages(committedMessages);
1458
+ return committedMessages;
1459
+ }
1460
+ committedMessages = [...committedMessages, { ...assistantMessage }];
1461
+ setMessages(committedMessages);
1462
+ return committedMessages;
1463
+ };
1464
+ setStreamingMessage(assistantMessage);
1465
+ let nextMessages = null;
1466
+ for await (const chunk of streamChat(withSystemMessage(activeMessages), modelName, TOOLS, controller.signal)) {
1467
+ if (chunk.type === "content") {
1468
+ assistantMessage.content += chunk.content;
1469
+ setStreamingMessage({ ...assistantMessage });
1470
+ continue;
1471
+ }
1472
+ if (chunk.tool_calls.length === 0) continue;
1470
1473
  const updatedMessages = commitAssistantMessage();
1471
- if (executionMode === "safe" && requiresApproval) {
1472
- setPendingToolCall(toolCall);
1473
- setIsLoading(false);
1474
- return;
1474
+ const toolResultMessages = [];
1475
+ for (const toolCall of chunk.tool_calls) try {
1476
+ const normalized = normalizeToolCall(toolCall);
1477
+ if (executionMode === "safe" && normalized.requiresApproval) {
1478
+ setPendingToolCall({
1479
+ toolCall,
1480
+ messages: [...updatedMessages, ...toolResultMessages],
1481
+ executionMode
1482
+ });
1483
+ setIsLoading(false);
1484
+ return;
1485
+ }
1486
+ // v8 ignore next
1487
+ const allowedTools = executionMode === "plan" ? READ_TOOLS : void 0;
1488
+ const result = await executeTool(normalized.name, normalized.arguments, { allowedTools });
1489
+ toolResultMessages.push(buildToolResultMessage(normalized.name, result));
1490
+ } catch (error) {
1491
+ toolResultMessages.push(buildToolResultMessage(toolCall.function.name, {
1492
+ content: "",
1493
+ // v8 ignore next
1494
+ error: error instanceof Error ? error.message : String(error)
1495
+ }));
1475
1496
  }
1476
- const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools });
1477
- const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
1478
- const newMessages = [...updatedMessages, toolResultMessage];
1479
- setMessages(newMessages);
1480
- await processStream(newMessages, executionMode);
1497
+ nextMessages = [...updatedMessages, ...toolResultMessages];
1498
+ setMessages(nextMessages);
1499
+ break;
1500
+ }
1501
+ if (!nextMessages) {
1502
+ await prewarmCodeBlocks(assistantMessage.content, theme);
1503
+ commitAssistantMessage();
1481
1504
  return;
1482
1505
  }
1506
+ toolTurns += 1;
1507
+ /* v8 ignore start */
1508
+ if (toolTurns >= MAX_TOOL_TURNS) {
1509
+ setMessages([...nextMessages, {
1510
+ role: SYSTEM,
1511
+ content: [
1512
+ "Tool execution stopped because the maximum tool turn limit was reached",
1513
+ ACTION_NOT_PERFORMED,
1514
+ "Summarize completed work and explain what remains without calling more tools."
1515
+ ].join("\n")
1516
+ }]);
1517
+ return;
1518
+ }
1519
+ /* v8 ignore stop */
1520
+ activeMessages = nextMessages;
1483
1521
  }
1484
- await prewarmCodeBlocks(assistantMessage.content, theme);
1485
- commitAssistantMessage();
1486
1522
  } catch (error) {
1487
1523
  // v8 ignore next
1488
1524
  if (!controller.signal.aborted) {
1489
- assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
1490
- await prewarmCodeBlocks(assistantMessage.content, theme);
1491
- commitAssistantMessage();
1525
+ const errorMessage = {
1526
+ role: ASSISTANT,
1527
+ content: `Error: ${error instanceof Error ? error.message : String(error)}`
1528
+ };
1529
+ await prewarmCodeBlocks(errorMessage.content, theme);
1530
+ setStreamingMessage(null);
1531
+ setMessages([...activeMessages, errorMessage]);
1492
1532
  }
1493
1533
  } finally {
1534
+ // v8 ignore next
1494
1535
  if (abortControllerRef.current === controller) abortControllerRef.current = null;
1495
1536
  setIsLoading(false);
1496
1537
  }
@@ -1542,15 +1583,29 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1542
1583
  setStreamingMessage({ ...assistantMessage });
1543
1584
  } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
1544
1585
  const updatedMessages = commitAssistantMessage();
1545
- if (!READ_TOOLS.has(toolCall.function.name)) {
1546
- const correctionMessage = buildPlanModeCorrectionMessage(toolCall.function.name);
1586
+ let normalized;
1587
+ try {
1588
+ normalized = normalizeToolCall(toolCall);
1589
+ } catch (error) {
1590
+ /* v8 ignore start */
1591
+ const toolResultMessage = buildToolResultMessage(toolCall.function.name, {
1592
+ content: "",
1593
+ error: error instanceof Error ? error.message : String(error)
1594
+ });
1595
+ const newMessages = [...updatedMessages, toolResultMessage];
1596
+ setMessages(newMessages);
1597
+ await processStreamReadOnly(newMessages);
1598
+ return;
1599
+ }
1600
+ if (!READ_TOOLS.has(normalized.name)) {
1601
+ const correctionMessage = buildPlanModeCorrectionMessage(normalized.name);
1547
1602
  const newMessages = [...updatedMessages, correctionMessage];
1548
1603
  setMessages(newMessages);
1549
1604
  await processStreamReadOnly(newMessages);
1550
1605
  return;
1551
1606
  }
1552
- const result = await executeTool(toolCall.function.name, toolCall.function.arguments, { allowedTools: READ_TOOLS });
1553
- const toolResultMessage = buildToolResultMessage(toolCall.function.name, result);
1607
+ const result = await executeTool(normalized.name, normalized.arguments, { allowedTools: READ_TOOLS });
1608
+ const toolResultMessage = buildToolResultMessage(normalized.name, result);
1554
1609
  const newMessages = [...updatedMessages, toolResultMessage];
1555
1610
  setMessages(newMessages);
1556
1611
  await processStreamReadOnly(newMessages);
@@ -1641,35 +1696,36 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1641
1696
  const handleToolApproval = useCallback(async (decision) => {
1642
1697
  // v8 ignore next
1643
1698
  if (!pendingToolCall) return;
1644
- const toolCall = pendingToolCall;
1699
+ const { executionMode, messages: approvedMessages, toolCall } = pendingToolCall;
1645
1700
  setPendingToolCall(null);
1646
1701
  setIsLoading(true);
1647
1702
  switch (decision) {
1648
1703
  case APPROVE: {
1649
- const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
1704
+ const result = await executeToolCall(toolCall);
1650
1705
  const toolResultMessage = {
1651
1706
  role: SYSTEM,
1652
- content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
1707
+ content: formatToolResultContent(toolCall.function.name, result)
1653
1708
  };
1654
- const newMessages = [...messages, toolResultMessage];
1655
- setMessages((previousMessages) => [...previousMessages, toolResultMessage]);
1656
- await processStream(newMessages);
1709
+ const newMessages = [...approvedMessages, toolResultMessage];
1710
+ setMessages(newMessages);
1711
+ await processStream(newMessages, executionMode);
1657
1712
  break;
1658
1713
  }
1659
- case REJECT:
1660
- setMessages((previousMessages) => [...previousMessages, {
1661
- role: USER,
1662
- content: TURN_ABORTED_MESSAGE
1663
- }]);
1714
+ case REJECT: {
1715
+ const toolResultMessage = {
1716
+ role: SYSTEM,
1717
+ content: formatToolResultContent(toolCall.function.name, {
1718
+ content: "",
1719
+ error: "Tool call rejected by user"
1720
+ })
1721
+ };
1722
+ setMessages([...approvedMessages, toolResultMessage]);
1664
1723
  setIsLoading(false);
1665
1724
  setInterruptReason(InterruptReason.Rejected);
1666
1725
  break;
1726
+ }
1667
1727
  }
1668
- }, [
1669
- pendingToolCall,
1670
- messages,
1671
- processStream
1672
- ]);
1728
+ }, [pendingToolCall, processStream]);
1673
1729
  const handleSubmit = useCallback(async ({ content, images }) => {
1674
1730
  setInterruptReason(null);
1675
1731
  const userContent = content.trim();
@@ -1711,7 +1767,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1711
1767
  theme
1712
1768
  }),
1713
1769
  !pendingPlan && pendingToolCall && /* @__PURE__ */ jsx(ToolApproval, {
1714
- toolCall: pendingToolCall,
1770
+ toolCall: pendingToolCall.toolCall,
1715
1771
  onDecision: handleToolApproval,
1716
1772
  theme
1717
1773
  }),
@@ -1783,12 +1839,9 @@ function abbreviatePath(dir) {
1783
1839
  const home = homedir();
1784
1840
  return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
1785
1841
  }
1786
- function Header({ model, onLoad, theme = getTheme() }) {
1842
+ function Header({ model, theme = getTheme() }) {
1787
1843
  const directory = abbreviatePath(process.cwd());
1788
1844
  const modelLabel = model || "not configured";
1789
- useEffect(() => {
1790
- onLoad();
1791
- }, []);
1792
1845
  return /* @__PURE__ */ jsx(Static, {
1793
1846
  items: [0],
1794
1847
  children: (key) => /* @__PURE__ */ jsxs(Box, {
@@ -2781,6 +2834,63 @@ function ThemeSettings({ currentTheme, onClose, onPreview, onSave }) {
2781
2834
  });
2782
2835
  }
2783
2836
  //#endregion
2837
+ //#region src/components/UpdateBanner/UpdateBanner.tsx
2838
+ var RELEASES_URL = `https://github.com/ai-action/${NAME}/releases`;
2839
+ function UpdateBanner({ onLoad, theme }) {
2840
+ const [latestVersion, setLatestVersion] = useState();
2841
+ useEffect(() => {
2842
+ checkForUpdate().then(setLatestVersion).finally(async () => {
2843
+ await tick();
2844
+ onLoad();
2845
+ });
2846
+ }, []);
2847
+ if (!latestVersion) return null;
2848
+ return /* @__PURE__ */ jsx(Static, {
2849
+ items: [0],
2850
+ children: (key) => /* @__PURE__ */ jsxs(Box, {
2851
+ borderStyle: "bold",
2852
+ flexDirection: "column",
2853
+ paddingX: 1,
2854
+ children: [
2855
+ /* @__PURE__ */ jsxs(Text, { children: [
2856
+ /* @__PURE__ */ jsx(Text, {
2857
+ italic: true,
2858
+ children: "🚀 Update available! "
2859
+ }),
2860
+ /* @__PURE__ */ jsx(Text, {
2861
+ color: theme.colors.secondary,
2862
+ children: VERSION
2863
+ }),
2864
+ " →",
2865
+ " ",
2866
+ /* @__PURE__ */ jsx(Text, {
2867
+ color: theme.colors.secondary,
2868
+ children: latestVersion
2869
+ })
2870
+ ] }),
2871
+ /* @__PURE__ */ jsx(Box, {
2872
+ marginBottom: 1,
2873
+ children: /* @__PURE__ */ jsxs(Text, { children: [
2874
+ "Run to update:",
2875
+ " ",
2876
+ /* @__PURE__ */ jsxs(Text, {
2877
+ color: theme.colors.command,
2878
+ children: ["npm i -g ", NAME]
2879
+ })
2880
+ ] })
2881
+ }),
2882
+ /* @__PURE__ */ jsx(Text, { children: "See release notes:" }),
2883
+ /* @__PURE__ */ jsx(Text, {
2884
+ color: theme.colors.accent,
2885
+ dimColor: true,
2886
+ underline: true,
2887
+ children: RELEASES_URL
2888
+ })
2889
+ ]
2890
+ }, key)
2891
+ });
2892
+ }
2893
+ //#endregion
2784
2894
  //#region src/components/App/constants.ts
2785
2895
  var Screen = /* @__PURE__ */ function(Screen) {
2786
2896
  Screen["Chat"] = "chat";
@@ -3002,7 +3112,7 @@ function ReadinessCheck({ errorMessage, onCommand, setupState, theme = getTheme(
3002
3112
  function App({ sessionId }) {
3003
3113
  const [appConfig, setConfig] = useState(() => loadConfig());
3004
3114
  const [mode, setMode] = useState(SAFE);
3005
- const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
3115
+ const [isLoaded, setIsLoaded] = useState(false);
3006
3116
  const [setupState, setSetupState] = useState(() => appConfig.model ? ReadinessState.Ready : ReadinessState.MissingModelConfig);
3007
3117
  const [setupErrorMessage, setSetupErrorMessage] = useState(null);
3008
3118
  const { currentScreen, setScreen, handleClose, handleCommand } = useScreenRouter();
@@ -3066,19 +3176,6 @@ function App({ sessionId }) {
3066
3176
  onUpdateConfig: handleUpdateConfig,
3067
3177
  setScreen
3068
3178
  });
3069
- const handleHeaderLoad = useCallback(() => {
3070
- setIsHeaderLoaded(true);
3071
- }, []);
3072
- const handleToggleMode = useCallback(() => {
3073
- setMode((mode) => {
3074
- switch (mode) {
3075
- case SAFE: return AUTO;
3076
- case AUTO: return PLAN;
3077
- case PLAN:
3078
- default: return SAFE;
3079
- }
3080
- });
3081
- }, []);
3082
3179
  const handleChatCommand = useCallback((command) => {
3083
3180
  handleCommand(command, {
3084
3181
  model: appConfig.model ?? "",
@@ -3093,18 +3190,6 @@ function App({ sessionId }) {
3093
3190
  handleCreateSession,
3094
3191
  setPreviewThemeId
3095
3192
  ]);
3096
- const handleDeleteSessionAndStay = useCallback((sid) => {
3097
- handleDeleteSession(sid);
3098
- setScreen(Screen.SessionManager);
3099
- }, [handleDeleteSession, setScreen]);
3100
- const handleOpenSessionAndNavigate = useCallback((sid) => {
3101
- handleOpenSession(sid);
3102
- setScreen(Screen.Chat);
3103
- }, [handleOpenSession, setScreen]);
3104
- const handleCreateSessionAndNavigate = useCallback(() => {
3105
- handleCreateSession();
3106
- setScreen(Screen.Chat);
3107
- }, [handleCreateSession, setScreen]);
3108
3193
  let screenContent;
3109
3194
  switch (currentScreen) {
3110
3195
  case Screen.ModelManager:
@@ -3127,9 +3212,18 @@ function App({ sessionId }) {
3127
3212
  screenContent = /* @__PURE__ */ jsx(SessionManager, {
3128
3213
  currentSessionId: activeSession.metadata.id,
3129
3214
  onClose: handleClose,
3130
- onDelete: handleDeleteSessionAndStay,
3131
- onNew: handleCreateSessionAndNavigate,
3132
- onOpen: handleOpenSessionAndNavigate,
3215
+ onDelete: (sessionId) => {
3216
+ handleDeleteSession(sessionId);
3217
+ setScreen(Screen.SessionManager);
3218
+ },
3219
+ onNew: () => {
3220
+ handleCreateSession();
3221
+ setScreen(Screen.Chat);
3222
+ },
3223
+ onOpen: (sessionId) => {
3224
+ handleOpenSession(sessionId);
3225
+ setScreen(Screen.Chat);
3226
+ },
3133
3227
  theme: activeTheme
3134
3228
  });
3135
3229
  break;
@@ -3164,14 +3258,27 @@ function App({ sessionId }) {
3164
3258
  children: [
3165
3259
  /* @__PURE__ */ jsx(Header, {
3166
3260
  model: appConfig.model ?? "",
3167
- onLoad: handleHeaderLoad,
3168
3261
  theme: activeTheme
3169
3262
  }),
3170
- isHeaderLoaded && screenContent,
3263
+ /* @__PURE__ */ jsx(UpdateBanner, {
3264
+ onLoad: () => {
3265
+ setIsLoaded(true);
3266
+ },
3267
+ theme: activeTheme
3268
+ }),
3269
+ isLoaded && screenContent,
3171
3270
  /* @__PURE__ */ jsx(Footer, {
3172
3271
  mode,
3173
3272
  model: appConfig.model ?? "",
3174
- onToggleMode: handleToggleMode,
3273
+ onToggleMode: () => {
3274
+ setMode((mode) => {
3275
+ switch (mode) {
3276
+ case SAFE: return AUTO;
3277
+ case AUTO: return PLAN;
3278
+ case PLAN: return SAFE;
3279
+ }
3280
+ });
3281
+ },
3175
3282
  theme: activeTheme
3176
3283
  })
3177
3284
  ]
package/dist/cli.js CHANGED
@@ -8,6 +8,18 @@ import { randomUUID } from "node:crypto";
8
8
  import { Ollama } from "ollama";
9
9
  import { v7 } from "uuid";
10
10
  import { promisify } from "node:util";
11
+ //#region \0rolldown/runtime.js
12
+ var __defProp = Object.defineProperty;
13
+ var __exportAll = (all, no_symbols) => {
14
+ let target = {};
15
+ for (var name in all) __defProp(target, name, {
16
+ get: all[name],
17
+ enumerable: true
18
+ });
19
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
20
+ return target;
21
+ };
22
+ //#endregion
11
23
  //#region src/constants/command.ts
12
24
  var LIST$1 = [
13
25
  {
@@ -38,7 +50,7 @@ var LIST$1 = [
38
50
  //#endregion
39
51
  //#region package.json
40
52
  var name = "code-ollama";
41
- var version = "0.20.0";
53
+ var version = "0.21.1";
42
54
  //#endregion
43
55
  //#region src/constants/package.ts
44
56
  var NAME = name;
@@ -280,6 +292,17 @@ function getTheme(themeId = DEFAULT_THEME_ID) {
280
292
  }
281
293
  //#endregion
282
294
  //#region src/constants/tool.ts
295
+ var tool_exports = /* @__PURE__ */ __exportAll({
296
+ EDIT_FILE: () => EDIT_FILE,
297
+ GREP_SEARCH: () => GREP_SEARCH,
298
+ LIST_DIR: () => LIST_DIR,
299
+ READ_FILE: () => READ_FILE,
300
+ RUN_SHELL: () => RUN_SHELL,
301
+ VIEW_RANGE: () => VIEW_RANGE,
302
+ WEB_FETCH: () => WEB_FETCH,
303
+ WEB_SEARCH: () => WEB_SEARCH,
304
+ WRITE_FILE: () => WRITE_FILE
305
+ });
283
306
  var READ_FILE = "read_file";
284
307
  var WRITE_FILE = "write_file";
285
308
  var EDIT_FILE = "edit_file";
@@ -1170,18 +1193,85 @@ var REQUIRED_STRING_ARGS = {
1170
1193
  [WEB_SEARCH]: ["query"],
1171
1194
  [WEB_FETCH]: ["url"]
1172
1195
  };
1196
+ var TOOL_NAMES = new Set(Object.values(tool_exports));
1197
+ function isToolName(name) {
1198
+ return TOOL_NAMES.has(name);
1199
+ }
1173
1200
  function validateArgs(name, args) {
1174
- const required = REQUIRED_STRING_ARGS[name] ?? [];
1201
+ const required = REQUIRED_STRING_ARGS[name];
1175
1202
  const received = Object.keys(args).join(", ") || "none";
1176
- for (const key of required) if (typeof args[key] !== "string") return {
1203
+ for (const key of required) if (typeof args[key] !== "string" || !args[key]) return {
1177
1204
  content: "",
1178
1205
  error: `Missing required argument: ${key} (received keys: ${received})`
1179
1206
  };
1207
+ if (name === "view_range") {
1208
+ if (!Number.isInteger(args.start) || !Number.isInteger(args.end)) return {
1209
+ content: "",
1210
+ error: `Missing required numeric arguments: start, end (received keys: ${received})`
1211
+ };
1212
+ if (args.start < 1 || args.end < args.start) return {
1213
+ content: "",
1214
+ error: "Invalid line range: start must be >= 1 and end must be >= start"
1215
+ };
1216
+ }
1217
+ if (name === "web_fetch") try {
1218
+ const url = new URL(args.url);
1219
+ if (url.protocol !== "http:" && url.protocol !== "https:") return {
1220
+ content: "",
1221
+ error: "URL must use http or https"
1222
+ };
1223
+ } catch {
1224
+ return {
1225
+ content: "",
1226
+ error: "Invalid URL"
1227
+ };
1228
+ }
1229
+ }
1230
+ function normalizeToolCall(toolCall) {
1231
+ const name = toolCall.function.name;
1232
+ const rawArguments = toolCall.function.arguments;
1233
+ if (!isToolName(name)) throw new Error(`Unknown tool: ${name}`);
1234
+ if (typeof rawArguments !== "object" || rawArguments === null || Array.isArray(rawArguments)) throw new Error(`Invalid arguments for tool: ${name}`);
1235
+ const normalizedArguments = rawArguments;
1236
+ const invalid = validateArgs(name, normalizedArguments);
1237
+ if (invalid?.error) throw new Error(invalid.error);
1238
+ return {
1239
+ name,
1240
+ arguments: normalizedArguments,
1241
+ requiresApproval: WRITE_TOOLS.has(name)
1242
+ };
1243
+ }
1244
+ function formatToolResultContent(toolName, result) {
1245
+ const status = result.error ? "The requested action was NOT performed" : "";
1246
+ const content = result.content ? `\n${result.content}` : "";
1247
+ const error = result.error ? `\nError: ${result.error}` : "";
1248
+ return [
1249
+ `Tool ${toolName} result:`,
1250
+ status,
1251
+ content.trim(),
1252
+ error.trim()
1253
+ ].filter(Boolean).join("\n");
1254
+ }
1255
+ async function executeToolCall(toolCall, options) {
1256
+ try {
1257
+ const normalized = normalizeToolCall(toolCall);
1258
+ return await executeTool(normalized.name, normalized.arguments, options);
1259
+ } catch (error) {
1260
+ return {
1261
+ content: "",
1262
+ // v8 ignore next
1263
+ error: error instanceof Error ? error.message : String(error)
1264
+ };
1265
+ }
1180
1266
  }
1181
1267
  /**
1182
1268
  * Execute a tool by name with arguments
1183
1269
  */
1184
1270
  async function executeTool(name, args, options) {
1271
+ if (!isToolName(name)) return {
1272
+ content: "",
1273
+ error: `Unknown tool: ${name}`
1274
+ };
1185
1275
  if (options?.allowedTools && !options.allowedTools.has(name)) return {
1186
1276
  content: "",
1187
1277
  error: `Tool not allowed: ${name}`
@@ -1199,6 +1289,7 @@ async function executeTool(name, args, options) {
1199
1289
  case VIEW_RANGE: return viewRange(stringArgs.path, args.start, args.end);
1200
1290
  case WEB_SEARCH: return await webSearch(stringArgs.query);
1201
1291
  case WEB_FETCH: return await webFetch(stringArgs.url);
1292
+ // v8 ignore next 2
1202
1293
  default: return {
1203
1294
  content: "",
1204
1295
  error: `Unknown tool: ${name}`
@@ -1206,8 +1297,41 @@ async function executeTool(name, args, options) {
1206
1297
  }
1207
1298
  }
1208
1299
  //#endregion
1300
+ //#region src/utils/update.ts
1301
+ var REGISTRY_URL = `https://registry.npmjs.org/${NAME}/latest`;
1302
+ function getSemver(version) {
1303
+ return version.split(".").map(Number);
1304
+ }
1305
+ /**
1306
+ * Check if the latest version is newer than the current version
1307
+ * @param current The current version
1308
+ * @param latest The latest version
1309
+ * @returns true if the latest version is newer, false otherwise
1310
+ */
1311
+ function isVersionNewer(current, latest) {
1312
+ const [currentMajor, currentMinor, currentPatch] = getSemver(current);
1313
+ const [latestMajor, latestMinor, latestPatch] = getSemver(latest);
1314
+ if (latestMajor !== currentMajor) return latestMajor > currentMajor;
1315
+ if (latestMinor !== currentMinor) return latestMinor > currentMinor;
1316
+ return latestPatch > currentPatch;
1317
+ }
1318
+ /**
1319
+ * Check if a newer version is available on npm
1320
+ * @returns The latest version if available, undefined otherwise
1321
+ */
1322
+ async function checkForUpdate() {
1323
+ try {
1324
+ const response = await fetch(REGISTRY_URL);
1325
+ if (!response.ok) return;
1326
+ const { version: latestVersion } = await response.json();
1327
+ if (!latestVersion) return;
1328
+ if (isVersionNewer(VERSION, latestVersion)) return latestVersion;
1329
+ } catch {}
1330
+ }
1331
+ //#endregion
1209
1332
  //#region src/cli.ts
1210
1333
  var cli = cac("code-ollama");
1334
+ var MAX_TOOL_TURNS = 25;
1211
1335
  cli.version(VERSION);
1212
1336
  cli.help();
1213
1337
  cli.command("run <model> <prompt>", "Run a one-off prompt").action(async (model, prompt) => {
@@ -1240,30 +1364,40 @@ async function runPrompt(model, prompt) {
1240
1364
  write("\n");
1241
1365
  }
1242
1366
  async function processRunStream(messages, model) {
1243
- const assistantMessage = {
1244
- role: ASSISTANT,
1245
- content: ""
1246
- };
1247
- for await (const chunk of streamChat(messages, model, TOOLS)) {
1248
- if (chunk.type === "content") {
1249
- assistantMessage.content += chunk.content;
1250
- write(chunk.content);
1251
- continue;
1252
- }
1253
- for (const toolCall of chunk.tool_calls) {
1254
- const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
1255
- const toolResultMessage = {
1256
- role: SYSTEM,
1257
- content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
1258
- };
1259
- await processRunStream([
1260
- ...messages,
1261
- assistantMessage,
1262
- toolResultMessage
1263
- ], model);
1264
- return;
1367
+ let activeMessages = messages;
1368
+ let toolTurns = 0;
1369
+ while (toolTurns < MAX_TOOL_TURNS) {
1370
+ const assistantMessage = {
1371
+ role: ASSISTANT,
1372
+ content: ""
1373
+ };
1374
+ let nextMessages = null;
1375
+ for await (const chunk of streamChat(activeMessages, model, TOOLS)) {
1376
+ if (chunk.type === "content") {
1377
+ assistantMessage.content += chunk.content;
1378
+ write(chunk.content);
1379
+ continue;
1380
+ }
1381
+ // v8 ignore next 3
1382
+ if (chunk.tool_calls.length === 0) continue;
1383
+ const committedMessages = [...activeMessages, assistantMessage];
1384
+ const toolResultMessages = [];
1385
+ for (const toolCall of chunk.tool_calls) {
1386
+ const result = await executeToolCall(toolCall);
1387
+ toolResultMessages.push({
1388
+ role: SYSTEM,
1389
+ content: formatToolResultContent(toolCall.function.name, result)
1390
+ });
1391
+ }
1392
+ nextMessages = [...committedMessages, ...toolResultMessages];
1393
+ break;
1265
1394
  }
1395
+ if (!nextMessages) return;
1396
+ activeMessages = nextMessages;
1397
+ toolTurns += 1;
1266
1398
  }
1399
+ // v8 ignore next 3
1400
+ writeError("Tool execution stopped because the maximum tool turn limit was reached\n");
1267
1401
  }
1268
1402
  async function main(args = process.argv.slice(2)) {
1269
1403
  if (args.length) cli.parse([
@@ -1274,7 +1408,7 @@ async function main(args = process.argv.slice(2)) {
1274
1408
  else await launchTui();
1275
1409
  }
1276
1410
  async function launchTui(sessionId) {
1277
- const { renderApp } = await import("./assets/tui-DDbMjcMS.js");
1411
+ const { renderApp } = await import("./assets/tui-BHkHnKUC.js");
1278
1412
  reset();
1279
1413
  renderApp(sessionId);
1280
1414
  }
@@ -1290,4 +1424,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
1290
1424
  if (isEntrypoint()) main();
1291
1425
  // v8 ignore stop
1292
1426
  //#endregion
1293
- export { WARNING as A, LABEL as B, loadConfig as C, resetSystemMessage as D, saveClipboardImage as E, USER as F, VERSION as G, SAFE as H, PLAN_GENERATION_INSTRUCTION as I, LIST$1 as K, BACK as L, getTheme as M, ASSISTANT as N, withSystemMessage as O, SYSTEM as P, CATALOG as R, streamChat as S, removeClipboardImage as T, APPROVE as U, PLAN as V, REJECT as W, setClearHandler as _, tick as a, listModels as b, appendMessage as c, deleteSessionIfEmpty as d, listSessions as f, reset as g, clear as h, WRITE_TOOLS as i, LIST as j, HEADER_PREFIX as k, createSession as l, updateSessionModel as m, main, READ_TOOLS as n, color as o, loadSession as p, TOOLS as r, write as s, executeTool as t, deleteSession as u, checkHealth as v, saveConfig as w, pullModel as x, deleteModel as y, AUTO as z };
1427
+ export { saveClipboardImage as A, PLAN_GENERATION_INSTRUCTION as B, deleteModel as C, loadConfig as D, streamChat as E, LIST as F, PLAN as G, CATALOG as H, getTheme as I, REJECT as J, SAFE as K, ASSISTANT as L, withSystemMessage as M, HEADER_PREFIX as N, saveConfig as O, WARNING as P, SYSTEM as R, checkHealth as S, pullModel as T, AUTO as U, BACK as V, LABEL as W, VERSION as X, NAME as Y, LIST$1 as Z, loadSession as _, normalizeToolCall as a, reset as b, WRITE_TOOLS as c, write as d, appendMessage as f, listSessions as g, deleteSessionIfEmpty as h, formatToolResultContent as i, resetSystemMessage as j, removeClipboardImage as k, tick as l, deleteSession as m, main, executeTool as n, READ_TOOLS as o, createSession as p, APPROVE as q, executeToolCall as r, TOOLS as s, checkForUpdate as t, color as u, updateSessionModel as v, listModels as w, setClearHandler as x, clear as y, USER as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.20.0",
3
+ "version": "0.21.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",
@@ -42,34 +42,34 @@
42
42
  "@inkjs/ui": "2.0.0",
43
43
  "@shikijs/cli": "4.1.0",
44
44
  "cac": "7.0.0",
45
- "ink": "7.0.3",
45
+ "ink": "7.0.5",
46
46
  "marked": "15.0.12",
47
47
  "marked-terminal": "7.3.0",
48
48
  "ollama": "0.6.3",
49
- "react": "19.2.6",
49
+ "react": "19.2.7",
50
50
  "uuid": "14.0.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@commitlint/cli": "21.0.1",
54
- "@commitlint/config-conventional": "21.0.1",
53
+ "@commitlint/cli": "21.0.2",
54
+ "@commitlint/config-conventional": "21.0.2",
55
55
  "@eslint/js": "10.0.1",
56
56
  "@types/node": "25.9.1",
57
- "@types/react": "19.2.15",
58
- "@vitest/coverage-v8": "4.1.7",
59
- "eslint": "10.4.0",
60
- "eslint-plugin-prettier": "5.5.5",
57
+ "@types/react": "19.2.16",
58
+ "@vitest/coverage-v8": "4.1.8",
59
+ "eslint": "10.4.1",
60
+ "eslint-plugin-prettier": "5.5.6",
61
61
  "eslint-plugin-simple-import-sort": "13.0.0",
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.5",
65
+ "lint-staged": "17.0.7",
66
66
  "prettier": "3.8.3",
67
67
  "publint": "0.3.21",
68
- "tsx": "4.22.3",
68
+ "tsx": "4.22.4",
69
69
  "typescript": "6.0.3",
70
- "typescript-eslint": "8.59.4",
71
- "vite": "8.0.14",
72
- "vitest": "4.1.7"
70
+ "typescript-eslint": "8.60.1",
71
+ "vite": "8.0.16",
72
+ "vitest": "4.1.8"
73
73
  },
74
74
  "files": [
75
75
  "dist/"