code-ollama 0.23.1 → 0.24.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.
@@ -3,8 +3,8 @@ 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";
5
5
  import { exec } from "node:child_process";
6
- import { Box, Static, Text, render, useApp, useInput, useStdout } from "ink";
7
- import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
6
+ import { Box, Static, Text, render, useApp, useInput, usePaste, useStdout } from "ink";
7
+ import { memo, useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
9
  import { ProgressBar, Select, Spinner } from "@inkjs/ui";
10
10
  import { Marked } from "marked";
@@ -747,36 +747,46 @@ function ToolApproval({ toolCall, onDecision, theme = getTheme() }) {
747
747
  //#region src/components/TextInput/TextInput.tsx
748
748
  function buildLineSegments(displayValue, cursorPosition, width) {
749
749
  const safeWidth = Math.max(1, width);
750
- const cursorChar = displayValue[cursorPosition] || " ";
751
- const renderValue = displayValue.slice(0, cursorPosition) + cursorChar + displayValue.slice(cursorPosition + 1);
752
- const totalLength = Math.max(1, renderValue.length);
753
750
  const lines = [];
754
- for (let start = 0; start < totalLength; start += safeWidth) {
755
- const end = start + safeWidth;
756
- const text = renderValue.slice(start, end);
757
- const hasCursor = cursorPosition >= start && cursorPosition < end;
758
- if (!hasCursor) {
751
+ const logicalLines = displayValue.split("\n");
752
+ let lineStart = 0;
753
+ for (const [lineIndex, logicalLine] of logicalLines.entries()) {
754
+ const lineEnd = lineStart + logicalLine.length;
755
+ const hasCursorOnLine = cursorPosition >= lineStart && cursorPosition <= lineEnd;
756
+ const cursorOffset = cursorPosition - lineStart;
757
+ const renderValue = hasCursorOnLine && cursorOffset === logicalLine.length ? `${logicalLine} ` : logicalLine;
758
+ const totalLength = Math.max(1, renderValue.length);
759
+ for (let start = 0; start < totalLength; start += safeWidth) {
760
+ const end = start + safeWidth;
761
+ const text = renderValue.slice(start, end);
762
+ const hasCursor = hasCursorOnLine && cursorOffset >= start && cursorOffset < end;
763
+ if (!hasCursor) {
764
+ lines.push({
765
+ text,
766
+ hasCursor,
767
+ beforeCursor: "",
768
+ cursorChar: " ",
769
+ afterCursor: ""
770
+ });
771
+ continue;
772
+ }
773
+ const offset = cursorOffset - start;
759
774
  lines.push({
760
775
  text,
761
776
  hasCursor,
762
- beforeCursor: "",
763
- cursorChar: " ",
764
- afterCursor: ""
777
+ beforeCursor: text.slice(0, offset),
778
+ cursorChar: text[offset],
779
+ afterCursor: text.slice(offset + 1)
765
780
  });
766
- continue;
767
781
  }
768
- const offset = cursorPosition - start;
769
- lines.push({
770
- text,
771
- hasCursor,
772
- beforeCursor: text.slice(0, offset),
773
- cursorChar: text[offset] || " ",
774
- afterCursor: text.slice(offset + 1)
775
- });
782
+ lineStart = lineEnd + (lineIndex < logicalLines.length - 1 ? 1 : 0);
776
783
  }
777
784
  return lines;
778
785
  }
779
- function TextInput({ value, isDisabled = false, placeholder, cursorPosition: externalCursorPosition, wrapIndent = 0, onChange, onSubmit }) {
786
+ function normalizePastedText(input) {
787
+ return input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
788
+ }
789
+ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: externalCursorPosition, allowMultilinePaste = false, wrapIndent = 0, onChange, onSubmit }) {
780
790
  const { stdout } = useStdout();
781
791
  const [cursorPosition, setCursorPosition] = useState(value.length);
782
792
  const prevValueRef = useRef(value);
@@ -798,9 +808,24 @@ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: ext
798
808
  cursorPosition,
799
809
  externalCursorPosition
800
810
  ]);
811
+ const insertText = useCallback((text) => {
812
+ onChange(value.slice(0, cursorPosition) + text + value.slice(cursorPosition));
813
+ setCursorPosition(cursorPosition + text.length);
814
+ }, [
815
+ cursorPosition,
816
+ onChange,
817
+ value
818
+ ]);
819
+ usePaste((text) => {
820
+ insertText(normalizePastedText(text));
821
+ }, { isActive: allowMultilinePaste && !isDisabled });
801
822
  useInput((input, key) => {
802
823
  // v8 ignore next
803
824
  if (isDisabled) return;
825
+ if (allowMultilinePaste && input.length > 1 && /[\r\n]/.test(input)) {
826
+ insertText(normalizePastedText(input));
827
+ return;
828
+ }
804
829
  if (key.return) {
805
830
  onSubmit(value);
806
831
  setCursorPosition(0);
@@ -845,10 +870,7 @@ function TextInput({ value, isDisabled = false, placeholder, cursorPosition: ext
845
870
  }
846
871
  if (key.ctrl) return;
847
872
  // v8 ignore start
848
- if (input) {
849
- onChange(value.slice(0, cursorPosition) + input + value.slice(cursorPosition));
850
- setCursorPosition(cursorPosition + input.length);
851
- }
873
+ if (input) insertText(input);
852
874
  // v8 ignore stop
853
875
  }, { isActive: !isDisabled });
854
876
  const displayValue = value || (placeholder ?? "");
@@ -948,25 +970,6 @@ function extractImageAttachments(input) {
948
970
  };
949
971
  }
950
972
  //#endregion
951
- //#region src/components/Chat/CommandMenu.tsx
952
- function getMatchingCommands(input) {
953
- const normalizedInput = input.trim().toLowerCase();
954
- if (!normalizedInput.startsWith("/")) return [];
955
- return LIST.filter(({ name }) => name.toLowerCase().startsWith(normalizedInput)).map(({ name, description }) => ({
956
- label: `${name} - ${description}`,
957
- value: name
958
- }));
959
- }
960
- function CommandMenu({ input, onSubmit }) {
961
- const commandOptions = useMemo(() => getMatchingCommands(input), [input]);
962
- if (!commandOptions.length) return null;
963
- return /* @__PURE__ */ jsx(SelectPrompt, {
964
- highlightText: input,
965
- onChange: onSubmit,
966
- options: commandOptions
967
- });
968
- }
969
- //#endregion
970
973
  //#region src/components/Suggestions/Suggestions.tsx
971
974
  var DEFAULT_MAX_VISIBLE_OPTIONS = 5;
972
975
  function Suggestions({ options, isDisabled = false, maxVisibleOptions = DEFAULT_MAX_VISIBLE_OPTIONS, resetKey, onHighlight, onSelect }) {
@@ -999,7 +1002,7 @@ function Suggestions({ options, isDisabled = false, maxVisibleOptions = DEFAULT_
999
1002
  setFocusedIndex((currentIndex) => Math.max(currentIndex - 1, 0));
1000
1003
  return;
1001
1004
  }
1002
- if (key.tab || key.return || input === " " || input === "\r") onSelect(options[focusedIndex]);
1005
+ if (key.tab || key.return) onSelect(options[focusedIndex]);
1003
1006
  });
1004
1007
  if (!options.length) return null;
1005
1008
  const visibleStart = Math.min(Math.max(0, focusedIndex - maxVisibleOptions + 1), Math.max(0, options.length - maxVisibleOptions));
@@ -1017,6 +1020,26 @@ function Suggestions({ options, isDisabled = false, maxVisibleOptions = DEFAULT_
1017
1020
  });
1018
1021
  }
1019
1022
  //#endregion
1023
+ //#region src/components/Chat/CommandMenu.tsx
1024
+ function getMatchingCommands(input) {
1025
+ const normalizedInput = input.trim().toLowerCase();
1026
+ if (!normalizedInput.startsWith("/")) return [];
1027
+ return LIST.filter(({ name }) => name.toLowerCase().startsWith(normalizedInput)).map(({ name, description }) => ({
1028
+ label: `${name} - ${description}`,
1029
+ value: name
1030
+ }));
1031
+ }
1032
+ function CommandMenu({ input, onSubmit }) {
1033
+ const commandOptions = useMemo(() => getMatchingCommands(input), [input]);
1034
+ if (!commandOptions.length) return null;
1035
+ return /* @__PURE__ */ jsx(Suggestions, {
1036
+ onSelect: (option) => {
1037
+ onSubmit(option.value);
1038
+ },
1039
+ options: commandOptions
1040
+ });
1041
+ }
1042
+ //#endregion
1020
1043
  //#region src/components/Chat/FileSuggestions.tsx
1021
1044
  var MENTION_PATTERN = /(^|.)@(\S+)/;
1022
1045
  var RIPGREP_MAX_BUFFER = 10 * 1024 * 1024;
@@ -1263,7 +1286,8 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1263
1286
  onSubmit,
1264
1287
  resetInput
1265
1288
  ]);
1266
- const showCommandMenu = input.startsWith("/");
1289
+ const isMultilineInput = input.includes("\n");
1290
+ const showCommandMenu = input.startsWith("/") && !isMultilineInput;
1267
1291
  const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
1268
1292
  const handleHistoryNavigation = useCallback((direction) => {
1269
1293
  if (!history.length || showFileSuggestions || hasAttachments) return;
@@ -1305,7 +1329,7 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1305
1329
  showFileSuggestions
1306
1330
  ]);
1307
1331
  const handleSubmitText = useCallback((value) => {
1308
- if (value.startsWith("/")) return;
1332
+ if (value.startsWith("/") && !value.includes("\n")) return;
1309
1333
  if (hasFileSuggestionQuery(value)) {
1310
1334
  if (fileSuggestionRef.current) handleSelectFileSuggestion(fileSuggestionRef.current);
1311
1335
  return;
@@ -1365,6 +1389,7 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1365
1389
  value: input,
1366
1390
  isDisabled,
1367
1391
  cursorPosition,
1392
+ allowMultilinePaste: true,
1368
1393
  wrapIndent,
1369
1394
  onChange: handleInputChange,
1370
1395
  onSubmit: handleSubmitText,
@@ -1389,6 +1414,21 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1389
1414
  var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
1390
1415
  var PLAN_CHECKLIST_REMINDER = "Then display the plan using either the Plan Needs Input or Proposed Plan Markdown template";
1391
1416
  var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
1417
+ var ChatActionType = /* @__PURE__ */ function(ChatActionType) {
1418
+ ChatActionType["AppendMessage"] = "append-message";
1419
+ ChatActionType["ClearPendingPlan"] = "clear-pending-plan";
1420
+ ChatActionType["ClearPendingToolCall"] = "clear-pending-tool-call";
1421
+ ChatActionType["CommitMessages"] = "commit-messages";
1422
+ ChatActionType["Interrupt"] = "interrupt";
1423
+ ChatActionType["RequestPlanReview"] = "request-plan-review";
1424
+ ChatActionType["RequestToolApproval"] = "request-tool-approval";
1425
+ ChatActionType["ResetSession"] = "reset-session";
1426
+ ChatActionType["SetLoading"] = "set-loading";
1427
+ ChatActionType["SetStreamingMessage"] = "set-streaming-message";
1428
+ ChatActionType["StartTurn"] = "start-turn";
1429
+ ChatActionType["ToolRejected"] = "tool-rejected";
1430
+ return ChatActionType;
1431
+ }({});
1392
1432
  var InterruptReason = /* @__PURE__ */ function(InterruptReason) {
1393
1433
  InterruptReason["Interrupted"] = "interrupted";
1394
1434
  InterruptReason["Rejected"] = "rejected";
@@ -1406,27 +1446,91 @@ function hasExecutablePlan(content) {
1406
1446
  return lines.slice(executionStepsIndex + 1, nextSectionIndex === -1 ? void 0 : nextSectionIndex).some((line) => /^(?:[-*]|\d+[.)])\s+\S/.test(line.trim()));
1407
1447
  }
1408
1448
  //#endregion
1449
+ //#region src/components/Chat/reducer.ts
1450
+ function createInitialChatState(messages = []) {
1451
+ return {
1452
+ messages,
1453
+ streamingMessage: null,
1454
+ isLoading: false,
1455
+ pendingToolCall: null,
1456
+ pendingPlan: null,
1457
+ interruptReason: null
1458
+ };
1459
+ }
1460
+ function chatReducer(state, action) {
1461
+ switch (action.type) {
1462
+ case ChatActionType.AppendMessage: return {
1463
+ ...state,
1464
+ messages: [...state.messages, action.message]
1465
+ };
1466
+ case ChatActionType.ClearPendingPlan: return {
1467
+ ...state,
1468
+ pendingPlan: null
1469
+ };
1470
+ case ChatActionType.ClearPendingToolCall: return {
1471
+ ...state,
1472
+ pendingToolCall: null
1473
+ };
1474
+ case ChatActionType.CommitMessages: return {
1475
+ ...state,
1476
+ messages: action.messages
1477
+ };
1478
+ case ChatActionType.Interrupt: return {
1479
+ ...state,
1480
+ messages: [...state.messages, action.message],
1481
+ streamingMessage: null,
1482
+ isLoading: false,
1483
+ interruptReason: InterruptReason.Interrupted
1484
+ };
1485
+ case ChatActionType.RequestPlanReview: return {
1486
+ ...state,
1487
+ pendingPlan: action.pendingPlan,
1488
+ isLoading: false
1489
+ };
1490
+ case ChatActionType.RequestToolApproval: return {
1491
+ ...state,
1492
+ pendingToolCall: action.pendingToolCall,
1493
+ isLoading: false
1494
+ };
1495
+ case ChatActionType.ResetSession: return createInitialChatState(action.messages);
1496
+ case ChatActionType.SetLoading: return {
1497
+ ...state,
1498
+ isLoading: action.isLoading
1499
+ };
1500
+ case ChatActionType.SetStreamingMessage: return {
1501
+ ...state,
1502
+ streamingMessage: action.message
1503
+ };
1504
+ case ChatActionType.StartTurn: return {
1505
+ ...state,
1506
+ messages: [...state.messages, action.message],
1507
+ isLoading: true,
1508
+ interruptReason: null
1509
+ };
1510
+ case ChatActionType.ToolRejected: return {
1511
+ ...state,
1512
+ messages: action.messages,
1513
+ isLoading: false,
1514
+ interruptReason: InterruptReason.Rejected
1515
+ };
1516
+ }
1517
+ }
1518
+ //#endregion
1409
1519
  //#region src/components/Chat/Chat.tsx
1410
1520
  var MAX_TOOL_TURNS = 25;
1411
1521
  var MAX_TOOL_INTENT_CORRECTIONS = 2;
1412
1522
  function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId, theme = getTheme() }) {
1413
1523
  const sessionMessages = initialMessages ?? [];
1414
1524
  const history = useMemo(() => sessionMessages.flatMap(({ role, content }) => role === "user" && !content.startsWith("/") ? [content] : []), [sessionMessages]);
1415
- const [messages, setMessages] = useState(sessionMessages);
1416
- const [streamingMessage, setStreamingMessage] = useState(null);
1417
- const [isLoading, setIsLoading] = useState(false);
1418
- const [pendingToolCall, setPendingToolCall] = useState(null);
1419
- const [pendingPlan, setPendingPlan] = useState(null);
1420
- const [interruptReason, setInterruptReason] = useState(null);
1525
+ const [state, dispatch] = useReducer(chatReducer, sessionMessages, createInitialChatState);
1526
+ const { messages, streamingMessage, isLoading, pendingToolCall, pendingPlan, interruptReason } = state;
1421
1527
  const abortControllerRef = useRef(null);
1422
1528
  const persistedSnapshotRef = useRef("");
1423
1529
  useEffect(() => {
1424
- setMessages(sessionMessages);
1425
- setStreamingMessage(null);
1426
- setIsLoading(false);
1427
- setPendingToolCall(null);
1428
- setPendingPlan(null);
1429
- setInterruptReason(null);
1530
+ dispatch({
1531
+ type: ChatActionType.ResetSession,
1532
+ messages: sessionMessages
1533
+ });
1430
1534
  persistedSnapshotRef.current = JSON.stringify(sessionMessages);
1431
1535
  }, [sessionId]);
1432
1536
  useEffect(() => {
@@ -1467,13 +1571,13 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1467
1571
  const handleInterrupt = useCallback(() => {
1468
1572
  abortControllerRef.current?.abort();
1469
1573
  abortControllerRef.current = null;
1470
- setIsLoading(false);
1471
- setStreamingMessage(null);
1472
- setInterruptReason(InterruptReason.Interrupted);
1473
- setMessages((prev) => [...prev, {
1474
- role: USER,
1475
- content: TURN_ABORTED_MESSAGE
1476
- }]);
1574
+ dispatch({
1575
+ type: ChatActionType.Interrupt,
1576
+ message: {
1577
+ role: USER,
1578
+ content: TURN_ABORTED_MESSAGE
1579
+ }
1580
+ });
1477
1581
  }, []);
1478
1582
  const processStream = useCallback(async (currentMessages, executionMode = mode) => {
1479
1583
  const modelName = model;
@@ -1498,27 +1602,45 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1498
1602
  if (assistantCommitted) {
1499
1603
  if (committedMessages.at(-1)?.role === "assistant") {
1500
1604
  committedMessages = [...committedMessages.slice(0, -1), { ...assistantMessage }];
1501
- setMessages(committedMessages);
1605
+ dispatch({
1606
+ type: ChatActionType.CommitMessages,
1607
+ messages: committedMessages
1608
+ });
1502
1609
  }
1503
1610
  return committedMessages;
1504
1611
  }
1505
1612
  // v8 ignore stop
1506
1613
  assistantCommitted = true;
1507
- setStreamingMessage(null);
1614
+ dispatch({
1615
+ type: ChatActionType.SetStreamingMessage,
1616
+ message: null
1617
+ });
1508
1618
  if (!assistantMessage.content) {
1509
- setMessages(committedMessages);
1619
+ dispatch({
1620
+ type: ChatActionType.CommitMessages,
1621
+ messages: committedMessages
1622
+ });
1510
1623
  return committedMessages;
1511
1624
  }
1512
1625
  committedMessages = [...committedMessages, { ...assistantMessage }];
1513
- setMessages(committedMessages);
1626
+ dispatch({
1627
+ type: ChatActionType.CommitMessages,
1628
+ messages: committedMessages
1629
+ });
1514
1630
  return committedMessages;
1515
1631
  };
1516
- setStreamingMessage(assistantMessage);
1632
+ dispatch({
1633
+ type: ChatActionType.SetStreamingMessage,
1634
+ message: assistantMessage
1635
+ });
1517
1636
  let nextMessages = null;
1518
1637
  for await (const chunk of streamChat(withSystemMessage(activeMessages), modelName, TOOLS, controller.signal)) {
1519
1638
  if (chunk.type === "content") {
1520
1639
  assistantMessage.content = sanitizeAssistantContent(assistantMessage.content + chunk.content);
1521
- setStreamingMessage({ ...assistantMessage });
1640
+ dispatch({
1641
+ type: ChatActionType.SetStreamingMessage,
1642
+ message: { ...assistantMessage }
1643
+ });
1522
1644
  continue;
1523
1645
  }
1524
1646
  if (chunk.tool_calls.length === 0) continue;
@@ -1527,12 +1649,14 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1527
1649
  for (const toolCall of chunk.tool_calls) try {
1528
1650
  const normalized = normalizeToolCall(toolCall);
1529
1651
  if (executionMode === "safe" && normalized.requiresApproval) {
1530
- setPendingToolCall({
1531
- toolCall,
1532
- messages: [...updatedMessages, ...toolResultMessages],
1533
- executionMode
1652
+ dispatch({
1653
+ type: ChatActionType.RequestToolApproval,
1654
+ pendingToolCall: {
1655
+ toolCall,
1656
+ messages: [...updatedMessages, ...toolResultMessages],
1657
+ executionMode
1658
+ }
1534
1659
  });
1535
- setIsLoading(false);
1536
1660
  return;
1537
1661
  }
1538
1662
  // v8 ignore next
@@ -1547,7 +1671,10 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1547
1671
  }));
1548
1672
  }
1549
1673
  nextMessages = [...updatedMessages, ...toolResultMessages];
1550
- setMessages(nextMessages);
1674
+ dispatch({
1675
+ type: ChatActionType.CommitMessages,
1676
+ messages: nextMessages
1677
+ });
1551
1678
  break;
1552
1679
  }
1553
1680
  if (!nextMessages) {
@@ -1559,7 +1686,10 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1559
1686
  role: SYSTEM,
1560
1687
  content: TOOL_INTENT_CORRECTION
1561
1688
  }];
1562
- setMessages(activeMessages);
1689
+ dispatch({
1690
+ type: ChatActionType.CommitMessages,
1691
+ messages: activeMessages
1692
+ });
1563
1693
  continue;
1564
1694
  }
1565
1695
  return;
@@ -1568,14 +1698,18 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1568
1698
  toolIntentCorrections = 0;
1569
1699
  /* v8 ignore start */
1570
1700
  if (toolTurns >= MAX_TOOL_TURNS) {
1571
- setMessages([...nextMessages, {
1701
+ const stoppedMessages = [...nextMessages, {
1572
1702
  role: SYSTEM,
1573
1703
  content: [
1574
1704
  "Tool execution stopped because the maximum tool turn limit was reached",
1575
1705
  ACTION_NOT_PERFORMED,
1576
1706
  "Summarize completed work and explain what remains without calling more tools."
1577
1707
  ].join("\n")
1578
- }]);
1708
+ }];
1709
+ dispatch({
1710
+ type: ChatActionType.CommitMessages,
1711
+ messages: stoppedMessages
1712
+ });
1579
1713
  return;
1580
1714
  }
1581
1715
  /* v8 ignore stop */
@@ -1589,13 +1723,22 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1589
1723
  content: `Error: ${error instanceof Error ? error.message : String(error)}`
1590
1724
  };
1591
1725
  await prewarmCodeBlocks(errorMessage.content, theme);
1592
- setStreamingMessage(null);
1593
- setMessages([...activeMessages, errorMessage]);
1726
+ dispatch({
1727
+ type: ChatActionType.SetStreamingMessage,
1728
+ message: null
1729
+ });
1730
+ dispatch({
1731
+ type: ChatActionType.CommitMessages,
1732
+ messages: [...activeMessages, errorMessage]
1733
+ });
1594
1734
  }
1595
1735
  } finally {
1596
1736
  // v8 ignore next
1597
1737
  if (abortControllerRef.current === controller) abortControllerRef.current = null;
1598
- setIsLoading(false);
1738
+ dispatch({
1739
+ type: ChatActionType.SetLoading,
1740
+ isLoading: false
1741
+ });
1599
1742
  }
1600
1743
  }, [
1601
1744
  buildToolResultMessage,
@@ -1625,22 +1768,37 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1625
1768
  if (assistantCommitted) {
1626
1769
  if (committedMessages.at(-1)?.role === "assistant") {
1627
1770
  committedMessages = [...committedMessages.slice(0, -1), { ...assistantMessage }];
1628
- setMessages(committedMessages);
1771
+ dispatch({
1772
+ type: ChatActionType.CommitMessages,
1773
+ messages: committedMessages
1774
+ });
1629
1775
  }
1630
1776
  return committedMessages;
1631
1777
  }
1632
1778
  /* v8 ignore stop */
1633
1779
  assistantCommitted = true;
1634
- setStreamingMessage(null);
1780
+ dispatch({
1781
+ type: ChatActionType.SetStreamingMessage,
1782
+ message: null
1783
+ });
1635
1784
  if (!assistantMessage.content) {
1636
- setMessages(committedMessages);
1785
+ dispatch({
1786
+ type: ChatActionType.CommitMessages,
1787
+ messages: committedMessages
1788
+ });
1637
1789
  return committedMessages;
1638
1790
  }
1639
1791
  committedMessages = [...committedMessages, { ...assistantMessage }];
1640
- setMessages(committedMessages);
1792
+ dispatch({
1793
+ type: ChatActionType.CommitMessages,
1794
+ messages: committedMessages
1795
+ });
1641
1796
  return committedMessages;
1642
1797
  };
1643
- setStreamingMessage(emptyAssistantMessage);
1798
+ dispatch({
1799
+ type: ChatActionType.SetStreamingMessage,
1800
+ message: emptyAssistantMessage
1801
+ });
1644
1802
  try {
1645
1803
  const readOnlyTools = TOOLS.filter((tool) => READ_TOOLS.has(tool.function.name));
1646
1804
  const planResearchMessages = [...currentMessages, {
@@ -1652,18 +1810,30 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1652
1810
  if (controller.signal.aborted) return;
1653
1811
  if (chunk.type === "content") {
1654
1812
  assistantMessage.content = sanitizeAssistantContent(assistantMessage.content + chunk.content);
1655
- setStreamingMessage({ ...assistantMessage });
1813
+ dispatch({
1814
+ type: ChatActionType.SetStreamingMessage,
1815
+ message: { ...assistantMessage }
1816
+ });
1656
1817
  } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
1657
1818
  const toolName = toolCall.function.name;
1658
1819
  if (!READ_TOOLS.has(toolName)) {
1659
1820
  const correctionMessage = buildPlanModeCorrectionMessage(toolName);
1660
- setStreamingMessage(null);
1821
+ dispatch({
1822
+ type: ChatActionType.SetStreamingMessage,
1823
+ message: null
1824
+ });
1661
1825
  const newMessages = [...committedMessages, correctionMessage];
1662
- setMessages(newMessages);
1826
+ dispatch({
1827
+ type: ChatActionType.CommitMessages,
1828
+ messages: newMessages
1829
+ });
1663
1830
  await processStreamReadOnly(newMessages);
1664
1831
  return;
1665
1832
  }
1666
- setStreamingMessage(emptyAssistantMessage);
1833
+ dispatch({
1834
+ type: ChatActionType.SetStreamingMessage,
1835
+ message: emptyAssistantMessage
1836
+ });
1667
1837
  assistantMessage.content = "";
1668
1838
  const updatedMessages = committedMessages;
1669
1839
  let normalized;
@@ -1676,14 +1846,20 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1676
1846
  error: error instanceof Error ? error.message : String(error)
1677
1847
  });
1678
1848
  const newMessages = [...updatedMessages, toolResultMessage];
1679
- setMessages(newMessages);
1849
+ dispatch({
1850
+ type: ChatActionType.CommitMessages,
1851
+ messages: newMessages
1852
+ });
1680
1853
  await processStreamReadOnly(newMessages);
1681
1854
  return;
1682
1855
  }
1683
1856
  const result = await executeTool(normalized.name, normalized.arguments, { allowedTools: READ_TOOLS });
1684
1857
  const toolResultMessage = buildToolResultMessage(normalized.name, result, normalized.arguments);
1685
1858
  const newMessages = [...updatedMessages, toolResultMessage];
1686
- setMessages(newMessages);
1859
+ dispatch({
1860
+ type: ChatActionType.CommitMessages,
1861
+ messages: newMessages
1862
+ });
1687
1863
  await processStreamReadOnly(newMessages);
1688
1864
  return;
1689
1865
  }
@@ -1691,11 +1867,13 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1691
1867
  await prewarmCodeBlocks(assistantMessage.content, theme);
1692
1868
  const researchMessages = commitAssistantMessage();
1693
1869
  if (hasExecutablePlan(assistantMessage.content)) {
1694
- setPendingPlan({
1695
- planContent: assistantMessage.content,
1696
- messages: researchMessages
1870
+ dispatch({
1871
+ type: ChatActionType.RequestPlanReview,
1872
+ pendingPlan: {
1873
+ planContent: assistantMessage.content,
1874
+ messages: researchMessages
1875
+ }
1697
1876
  });
1698
- setIsLoading(false);
1699
1877
  return;
1700
1878
  }
1701
1879
  const planInstruction = {
@@ -1707,32 +1885,60 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1707
1885
  role: ASSISTANT,
1708
1886
  content: ""
1709
1887
  };
1710
- setStreamingMessage(emptyAssistantMessage);
1888
+ dispatch({
1889
+ type: ChatActionType.SetStreamingMessage,
1890
+ message: emptyAssistantMessage
1891
+ });
1711
1892
  try {
1712
1893
  for await (const chunk of streamChat(withSystemMessage(planMessages), modelName, [], controller.signal)) {
1713
1894
  // v8 ignore next 3
1714
1895
  if (controller.signal.aborted) return;
1715
1896
  if (chunk.type === "content") {
1716
1897
  planAssistantMessage.content = sanitizeAssistantContent(planAssistantMessage.content + chunk.content);
1717
- setStreamingMessage({ ...planAssistantMessage });
1898
+ dispatch({
1899
+ type: ChatActionType.SetStreamingMessage,
1900
+ message: { ...planAssistantMessage }
1901
+ });
1718
1902
  }
1719
1903
  }
1720
1904
  } catch (error) {
1721
1905
  // v8 ignore next
1722
1906
  planAssistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
1723
- setMessages([...planMessages, { ...planAssistantMessage }]);
1724
- setStreamingMessage(null);
1725
- setIsLoading(false);
1907
+ const errorPlanMessages = [...planMessages, { ...planAssistantMessage }];
1908
+ dispatch({
1909
+ type: ChatActionType.CommitMessages,
1910
+ messages: errorPlanMessages
1911
+ });
1912
+ dispatch({
1913
+ type: ChatActionType.SetStreamingMessage,
1914
+ message: null
1915
+ });
1916
+ dispatch({
1917
+ type: ChatActionType.SetLoading,
1918
+ isLoading: false
1919
+ });
1726
1920
  return;
1727
1921
  }
1728
1922
  const finalPlanMessages = [...planMessages, { ...planAssistantMessage }];
1729
- setMessages(finalPlanMessages);
1730
- setStreamingMessage(null);
1731
- if (hasExecutablePlan(planAssistantMessage.content)) setPendingPlan({
1732
- planContent: planAssistantMessage.content,
1923
+ dispatch({
1924
+ type: ChatActionType.CommitMessages,
1733
1925
  messages: finalPlanMessages
1734
1926
  });
1735
- setIsLoading(false);
1927
+ dispatch({
1928
+ type: ChatActionType.SetStreamingMessage,
1929
+ message: null
1930
+ });
1931
+ if (hasExecutablePlan(planAssistantMessage.content)) dispatch({
1932
+ type: ChatActionType.RequestPlanReview,
1933
+ pendingPlan: {
1934
+ planContent: planAssistantMessage.content,
1935
+ messages: finalPlanMessages
1936
+ }
1937
+ });
1938
+ dispatch({
1939
+ type: ChatActionType.SetLoading,
1940
+ isLoading: false
1941
+ });
1736
1942
  } catch (error) {
1737
1943
  // v8 ignore next
1738
1944
  if (!controller.signal.aborted) {
@@ -1742,7 +1948,10 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1742
1948
  }
1743
1949
  } finally {
1744
1950
  if (abortControllerRef.current === controller) abortControllerRef.current = null;
1745
- setIsLoading(false);
1951
+ dispatch({
1952
+ type: ChatActionType.SetLoading,
1953
+ isLoading: false
1954
+ });
1746
1955
  }
1747
1956
  }, [
1748
1957
  buildPlanModeCorrectionMessage,
@@ -1754,19 +1963,25 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1754
1963
  // v8 ignore next
1755
1964
  if (!pendingPlan) return;
1756
1965
  const { messages: planMessages } = pendingPlan;
1757
- setPendingPlan(null);
1966
+ dispatch({ type: ChatActionType.ClearPendingPlan });
1758
1967
  if (mode === "plan") {
1759
1968
  onModeChange(PLAN);
1760
1969
  const cancelMessage = {
1761
1970
  role: SYSTEM,
1762
1971
  content: "Continuing in Plan mode. No tools were executed."
1763
1972
  };
1764
- setMessages((previousMessages) => [...previousMessages, cancelMessage]);
1973
+ dispatch({
1974
+ type: ChatActionType.AppendMessage,
1975
+ message: cancelMessage
1976
+ });
1765
1977
  return;
1766
1978
  }
1767
1979
  const selectedMode = mode === "auto" ? AUTO : SAFE;
1768
1980
  onModeChange(selectedMode);
1769
- setIsLoading(true);
1981
+ dispatch({
1982
+ type: ChatActionType.SetLoading,
1983
+ isLoading: true
1984
+ });
1770
1985
  const executeInstruction = {
1771
1986
  role: SYSTEM,
1772
1987
  content: mode === "auto" ? "Execute the plan above. Use tools as needed without asking for further confirmation." : "Execute the plan above one step at a time. Wait for user approval before each tool call that modifies files or runs commands."
@@ -1781,14 +1996,20 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1781
1996
  // v8 ignore next
1782
1997
  if (!pendingToolCall) return;
1783
1998
  const { executionMode, messages: approvedMessages, toolCall } = pendingToolCall;
1784
- setPendingToolCall(null);
1785
- setIsLoading(true);
1999
+ dispatch({ type: ChatActionType.ClearPendingToolCall });
2000
+ dispatch({
2001
+ type: ChatActionType.SetLoading,
2002
+ isLoading: true
2003
+ });
1786
2004
  switch (decision) {
1787
2005
  case APPROVE: {
1788
2006
  const result = await executeToolCall(toolCall);
1789
2007
  const toolResultMessage = buildToolResultMessage(toolCall.function.name, result, toolCall.function.arguments);
1790
2008
  const newMessages = [...approvedMessages, toolResultMessage];
1791
- setMessages(newMessages);
2009
+ dispatch({
2010
+ type: ChatActionType.CommitMessages,
2011
+ messages: newMessages
2012
+ });
1792
2013
  await processStream(newMessages, executionMode);
1793
2014
  break;
1794
2015
  }
@@ -1800,9 +2021,10 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1800
2021
  error: "Tool call rejected by user"
1801
2022
  }, toolCall.function.arguments)
1802
2023
  };
1803
- setMessages([...approvedMessages, toolResultMessage]);
1804
- setIsLoading(false);
1805
- setInterruptReason(InterruptReason.Rejected);
2024
+ dispatch({
2025
+ type: ChatActionType.ToolRejected,
2026
+ messages: [...approvedMessages, toolResultMessage]
2027
+ });
1806
2028
  break;
1807
2029
  }
1808
2030
  }
@@ -1812,21 +2034,22 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1812
2034
  processStream
1813
2035
  ]);
1814
2036
  const handleSubmit = useCallback(async ({ content, images }) => {
1815
- setInterruptReason(null);
1816
2037
  const userContent = content.trim();
1817
2038
  if (!userContent && !images?.length) return;
1818
2039
  if (userContent.startsWith("/")) {
1819
2040
  onCommand(userContent);
1820
2041
  return;
1821
2042
  }
1822
- setIsLoading(true);
1823
2043
  const userMessage = {
1824
2044
  role: USER,
1825
2045
  content: userContent,
1826
2046
  ...images?.length ? { images } : {}
1827
2047
  };
1828
2048
  const updatedMessages = [...messages, userMessage];
1829
- setMessages(updatedMessages);
2049
+ dispatch({
2050
+ type: ChatActionType.StartTurn,
2051
+ message: userMessage
2052
+ });
1830
2053
  if (mode === "plan") await processStreamReadOnly(updatedMessages);
1831
2054
  else await processStream(updatedMessages);
1832
2055
  }, [
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
3
3
  import cac from "cac";
4
4
  import { homedir, tmpdir } from "node:os";
5
5
  import { join } from "node:path";
@@ -50,7 +50,7 @@ var LIST$1 = [
50
50
  //#endregion
51
51
  //#region package.json
52
52
  var name = "code-ollama";
53
- var version = "0.23.1";
53
+ var version = "0.24.0";
54
54
  //#endregion
55
55
  //#region src/constants/package.ts
56
56
  var NAME = name;
@@ -107,13 +107,16 @@ var BACK = {
107
107
  //#endregion
108
108
  //#region src/constants/tool.ts
109
109
  var tool_exports = /* @__PURE__ */ __exportAll({
110
+ CREATE_DIRECTORY: () => CREATE_DIRECTORY,
111
+ DELETE_PATH: () => DELETE_PATH,
110
112
  EDIT_FILE: () => EDIT_FILE,
113
+ FIND_FILES: () => FIND_FILES,
111
114
  GREP_SEARCH: () => GREP_SEARCH,
112
115
  LIST_DIR: () => LIST_DIR,
113
116
  READ_FILE: () => READ_FILE,
114
117
  READ_TOOL_NAMES: () => READ_TOOL_NAMES,
118
+ RENAME_PATH: () => RENAME_PATH,
115
119
  RUN_SHELL: () => RUN_SHELL,
116
- VIEW_RANGE: () => VIEW_RANGE,
117
120
  WEB_FETCH: () => WEB_FETCH,
118
121
  WEB_SEARCH: () => WEB_SEARCH,
119
122
  WRITE_FILE: () => WRITE_FILE,
@@ -122,23 +125,29 @@ var tool_exports = /* @__PURE__ */ __exportAll({
122
125
  var READ_FILE = "read_file";
123
126
  var WRITE_FILE = "write_file";
124
127
  var EDIT_FILE = "edit_file";
128
+ var CREATE_DIRECTORY = "create_directory";
129
+ var RENAME_PATH = "rename_path";
130
+ var DELETE_PATH = "delete_path";
125
131
  var RUN_SHELL = "run_shell";
126
132
  var LIST_DIR = "list_dir";
133
+ var FIND_FILES = "find_files";
127
134
  var GREP_SEARCH = "grep_search";
128
- var VIEW_RANGE = "view_range";
129
135
  var WEB_SEARCH = "web_search";
130
136
  var WEB_FETCH = "web_fetch";
131
137
  var READ_TOOL_NAMES = [
132
138
  READ_FILE,
133
139
  LIST_DIR,
140
+ FIND_FILES,
134
141
  GREP_SEARCH,
135
- VIEW_RANGE,
136
142
  WEB_SEARCH,
137
143
  WEB_FETCH
138
144
  ];
139
145
  var WRITE_TOOL_NAMES = [
140
146
  WRITE_FILE,
141
147
  EDIT_FILE,
148
+ CREATE_DIRECTORY,
149
+ RENAME_PATH,
150
+ DELETE_PATH,
142
151
  RUN_SHELL
143
152
  ];
144
153
  //#endregion
@@ -157,10 +166,14 @@ Follow these rules:
157
166
 
158
167
  When tools return results, incorporate them into your response naturally`;
159
168
  var TOOL_INSTRUCTIONS = `Available tools:
160
- - read_file: Read file contents at a path
169
+ - read_file: Read file contents at a path; supports startLine, endLine, and maxLines options
161
170
  - write_file: Write content to a file (requires approval)
162
171
  - edit_file: Replace one exact text match in a file (requires approval)
172
+ - create_directory: Create a directory and missing parent directories (requires approval)
173
+ - rename_path: Rename or move a file or directory without overwriting existing destinations (requires approval)
174
+ - delete_path: Delete a file or directory; non-empty directories require recursive=true (requires approval)
163
175
  - list_dir: List files in a directory
176
+ - find_files: Recursively find files by optional substring or wildcard path pattern; supports includeHidden and ignoredDirs options
164
177
  - grep_search: Search code with regex
165
178
  - web_search: Search the web for current or external information
166
179
  - run_shell: Execute shell commands (requires approval)
@@ -815,10 +828,24 @@ function defineTool(name, description, params, required) {
815
828
  * Tool definitions for Ollama API
816
829
  */
817
830
  var TOOLS = [
818
- defineTool(READ_FILE, "Read the contents of a file at the specified path", { path: {
819
- type: "string",
820
- description: "The path to the file to read"
821
- } }, ["path"]),
831
+ defineTool(READ_FILE, "Read the contents of a file at the specified path, optionally limited by line range", {
832
+ path: {
833
+ type: "string",
834
+ description: "The path to the file to read"
835
+ },
836
+ startLine: {
837
+ type: "number",
838
+ description: "Optional starting line number to read from (1-indexed)"
839
+ },
840
+ endLine: {
841
+ type: "number",
842
+ description: "Optional ending line number to read through (inclusive)"
843
+ },
844
+ maxLines: {
845
+ type: "number",
846
+ description: "Optional maximum number of lines to read; cannot be combined with endLine"
847
+ }
848
+ }, ["path"]),
822
849
  defineTool(WRITE_FILE, "Write content to a file at the specified path", {
823
850
  path: {
824
851
  type: "string",
@@ -847,6 +874,30 @@ var TOOLS = [
847
874
  "oldText",
848
875
  "newText"
849
876
  ]),
877
+ defineTool(CREATE_DIRECTORY, "Create a directory and any missing parent directories at the specified path", { path: {
878
+ type: "string",
879
+ description: "The directory path to create"
880
+ } }, ["path"]),
881
+ defineTool(RENAME_PATH, "Rename or move an existing file or directory to a new path", {
882
+ from: {
883
+ type: "string",
884
+ description: "The existing file or directory path to rename or move"
885
+ },
886
+ to: {
887
+ type: "string",
888
+ description: "The destination path for the renamed or moved item"
889
+ }
890
+ }, ["from", "to"]),
891
+ defineTool(DELETE_PATH, "Delete a file or directory at the specified path", {
892
+ path: {
893
+ type: "string",
894
+ description: "The file or directory path to delete"
895
+ },
896
+ recursive: {
897
+ type: "boolean",
898
+ description: "Whether to delete non-empty directories recursively; use false for files and empty directories"
899
+ }
900
+ }, ["path", "recursive"]),
850
901
  defineTool(RUN_SHELL, "Execute a shell command", { command: {
851
902
  type: "string",
852
903
  description: "The shell command to execute"
@@ -855,6 +906,28 @@ var TOOLS = [
855
906
  type: "string",
856
907
  description: "The path to the directory to list"
857
908
  } }, ["path"]),
909
+ defineTool(FIND_FILES, "Recursively find files under a directory, optionally matching a simple substring or wildcard pattern", {
910
+ path: {
911
+ type: "string",
912
+ description: "The directory path to search in"
913
+ },
914
+ pattern: {
915
+ type: "string",
916
+ description: "Optional case-insensitive substring or wildcard pattern to match against file paths"
917
+ },
918
+ includeHidden: {
919
+ type: "boolean",
920
+ description: "Whether to include hidden files and directories; defaults to false"
921
+ },
922
+ ignoredDirs: {
923
+ type: "array",
924
+ description: "Optional directory names or simple wildcard patterns to skip instead of the default ignored directory list; .git is always skipped",
925
+ items: {
926
+ type: "string",
927
+ description: "Directory name or wildcard pattern to skip"
928
+ }
929
+ }
930
+ }, ["path"]),
858
931
  defineTool(GREP_SEARCH, "Search files within a directory; multi-word queries also match common code identifier forms", {
859
932
  pattern: {
860
933
  type: "string",
@@ -865,24 +938,6 @@ var TOOLS = [
865
938
  description: "The directory path to search in"
866
939
  }
867
940
  }, ["pattern", "path"]),
868
- defineTool(VIEW_RANGE, "View a specific range of lines from a file", {
869
- path: {
870
- type: "string",
871
- description: "The path to the file"
872
- },
873
- start: {
874
- type: "number",
875
- description: "The starting line number (1-indexed)"
876
- },
877
- end: {
878
- type: "number",
879
- description: "The ending line number (inclusive)"
880
- }
881
- }, [
882
- "path",
883
- "start",
884
- "end"
885
- ]),
886
941
  defineTool(WEB_SEARCH, "Search the web for external or current information", { query: {
887
942
  type: "string",
888
943
  description: "The search query to look up"
@@ -926,6 +981,17 @@ async function runShell(command) {
926
981
  var DIFF_CONTEXT_LINES = 3;
927
982
  var DIFF_MAX_LINES = 120;
928
983
  var DIFF_MAX_CHARS = 12e3;
984
+ var DEFAULT_FIND_FILES_IGNORED_DIRS = [
985
+ "node_modules",
986
+ "__pycache__",
987
+ ".*cache",
988
+ ".tox",
989
+ ".venv",
990
+ "venv",
991
+ "dist",
992
+ "build",
993
+ "coverage"
994
+ ];
929
995
  function splitLines(content) {
930
996
  return content.split("\n");
931
997
  }
@@ -993,16 +1059,63 @@ function buildSearchPatterns(pattern) {
993
1059
  function capitalize(value) {
994
1060
  return value.charAt(0).toUpperCase() + value.slice(1);
995
1061
  }
1062
+ function escapeRegExp(value) {
1063
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1064
+ }
1065
+ function fileMatchesPattern(filePath, pattern) {
1066
+ const trimmedPattern = pattern?.trim();
1067
+ if (!trimmedPattern) return true;
1068
+ const normalizedPath = filePath.toLowerCase();
1069
+ const normalizedFileName = normalizedPath.slice(Math.max(normalizedPath.lastIndexOf("/"), normalizedPath.lastIndexOf("\\")) + 1);
1070
+ const normalizedPattern = trimmedPattern.toLowerCase();
1071
+ if (!normalizedPattern.includes("*") && !normalizedPattern.includes("?")) return normalizedPath.includes(normalizedPattern);
1072
+ const regexPattern = normalizedPattern.split("").map((char) => {
1073
+ if (char === "*") return ".*";
1074
+ if (char === "?") return ".";
1075
+ return escapeRegExp(char);
1076
+ }).join("");
1077
+ const regex = new RegExp(`^${regexPattern}$`);
1078
+ return regex.test(normalizedPath) || regex.test(normalizedFileName);
1079
+ }
1080
+ function valueMatchesWildcardPattern(value, pattern) {
1081
+ const normalizedValue = value.toLowerCase();
1082
+ const normalizedPattern = pattern.trim().toLowerCase();
1083
+ if (!normalizedPattern.includes("*") && !normalizedPattern.includes("?")) return normalizedValue === normalizedPattern;
1084
+ const regexPattern = normalizedPattern.split("").map((char) => {
1085
+ if (char === "*") return ".*";
1086
+ if (char === "?") return ".";
1087
+ return escapeRegExp(char);
1088
+ }).join("");
1089
+ return new RegExp(`^${regexPattern}$`).test(normalizedValue);
1090
+ }
1091
+ function directoryMatchesIgnoredPattern(dirName, ignoredDirs) {
1092
+ for (const ignoredDir of ignoredDirs) if (valueMatchesWildcardPattern(dirName, ignoredDir)) return true;
1093
+ return false;
1094
+ }
1095
+ function formatNumberedLines(lines, startLine) {
1096
+ return lines.map((line, index) => `${String(startLine + index)}: ${line}`).join("\n");
1097
+ }
996
1098
  /**
997
1099
  * Read file contents
998
1100
  */
999
- function readFile(filePath) {
1101
+ function readFile(filePath, options = {}) {
1000
1102
  try {
1001
1103
  if (!existsSync(filePath)) return {
1002
1104
  content: "",
1003
1105
  error: `File not found: ${filePath}`
1004
1106
  };
1005
- return { content: readFileSync(filePath, "utf8") };
1107
+ const content = readFileSync(filePath, "utf8");
1108
+ if (!(options.startLine !== void 0 || options.endLine !== void 0 || options.maxLines !== void 0)) return { content };
1109
+ const lines = content.split("\n");
1110
+ const startLine = options.startLine ?? 1;
1111
+ const endLine = options.endLine ?? startLine + (options.maxLines ?? lines.length) - 1;
1112
+ const startIndex = startLine - 1;
1113
+ const endIndex = Math.min(lines.length, endLine);
1114
+ if (startIndex >= lines.length) return {
1115
+ content: "",
1116
+ error: "Invalid line range"
1117
+ };
1118
+ return { content: formatNumberedLines(lines.slice(startIndex, endIndex), startLine) };
1006
1119
  } catch (error) {
1007
1120
  return {
1008
1121
  content: "",
@@ -1067,26 +1180,73 @@ function editFile(filePath, oldText, newText) {
1067
1180
  }
1068
1181
  }
1069
1182
  /**
1070
- * View specific line range from file
1183
+ * Create a directory and any missing parent directories
1071
1184
  */
1072
- function viewRange(filePath, start, end) {
1185
+ function createDirectory(dirPath) {
1073
1186
  try {
1074
- if (!existsSync(filePath)) return {
1187
+ if (existsSync(dirPath)) {
1188
+ if (statSync(dirPath).isDirectory()) return { content: `Directory already exists: ${dirPath}` };
1189
+ return {
1190
+ content: "",
1191
+ error: `Path already exists and is not a directory: ${dirPath}`
1192
+ };
1193
+ }
1194
+ mkdirSync(dirPath, { recursive: true });
1195
+ return { content: `Directory created successfully: ${dirPath}` };
1196
+ } catch (error) {
1197
+ return {
1075
1198
  content: "",
1076
- error: `File not found: ${filePath}`
1199
+ error: `Failed to create directory: ${error instanceof Error ? error.message : String(error)}`
1077
1200
  };
1078
- const lines = readFileSync(filePath, "utf8").split("\n");
1079
- const startIdx = Math.max(0, start - 1);
1080
- const endIdx = Math.min(lines.length, end);
1081
- if (startIdx >= lines.length || startIdx > endIdx) return {
1201
+ }
1202
+ }
1203
+ /**
1204
+ * Rename or move an existing file or directory
1205
+ */
1206
+ function renamePath(fromPath, toPath) {
1207
+ try {
1208
+ if (!existsSync(fromPath)) return {
1082
1209
  content: "",
1083
- error: "Invalid line range"
1210
+ error: `Source path not found: ${fromPath}`
1211
+ };
1212
+ if (existsSync(toPath)) return {
1213
+ content: "",
1214
+ error: `Destination path already exists: ${toPath}`
1084
1215
  };
1085
- return { content: lines.slice(startIdx, endIdx).join("\n") };
1216
+ renameSync(fromPath, toPath);
1217
+ return { content: `Path renamed successfully: ${fromPath} -> ${toPath}` };
1086
1218
  } catch (error) {
1087
1219
  return {
1088
1220
  content: "",
1089
- error: `Failed to view range: ${error instanceof Error ? error.message : String(error)}`
1221
+ error: `Failed to rename path: ${error instanceof Error ? error.message : String(error)}`
1222
+ };
1223
+ }
1224
+ }
1225
+ /**
1226
+ * Delete a file or directory
1227
+ */
1228
+ function deletePath(path, recursive) {
1229
+ try {
1230
+ if (!existsSync(path)) return {
1231
+ content: "",
1232
+ error: `Path not found: ${path}`
1233
+ };
1234
+ if (statSync(path).isDirectory()) {
1235
+ if (readdirSync(path).length > 0 && !recursive) return {
1236
+ content: "",
1237
+ error: `Directory is not empty; set recursive to true to delete: ${path}`
1238
+ };
1239
+ if (recursive) rmSync(path, {
1240
+ recursive: true,
1241
+ force: false
1242
+ });
1243
+ else rmdirSync(path);
1244
+ } else rmSync(path, { force: false });
1245
+ return { content: `Path deleted successfully: ${path}` };
1246
+ } catch (error) {
1247
+ return {
1248
+ content: "",
1249
+ error: `Failed to delete path: ${error instanceof Error ? error.message : String(error)}`
1090
1250
  };
1091
1251
  }
1092
1252
  }
@@ -1110,6 +1270,40 @@ function listDir(dirPath) {
1110
1270
  }
1111
1271
  }
1112
1272
  /**
1273
+ * Recursively find files by path
1274
+ */
1275
+ function findFiles(dirPath, options = {}) {
1276
+ try {
1277
+ if (!existsSync(dirPath)) return {
1278
+ content: "",
1279
+ error: `Directory not found: ${dirPath}`
1280
+ };
1281
+ if (!statSync(dirPath).isDirectory()) return {
1282
+ content: "",
1283
+ error: `Path is not a directory: ${dirPath}`
1284
+ };
1285
+ const results = [];
1286
+ const includeHidden = options.includeHidden ?? false;
1287
+ const ignoredDirs = new Set(options.ignoredDirs ?? DEFAULT_FIND_FILES_IGNORED_DIRS);
1288
+ function searchDirectory(currentPath) {
1289
+ const entries = readdirSync(currentPath, { withFileTypes: true });
1290
+ for (const entry of entries) {
1291
+ const fullPath = join(currentPath, entry.name);
1292
+ if (entry.isDirectory()) {
1293
+ if (entry.name !== ".git" && !directoryMatchesIgnoredPattern(entry.name, ignoredDirs) && (includeHidden || !entry.name.startsWith("."))) searchDirectory(fullPath);
1294
+ } else if (entry.isFile() && (includeHidden || !entry.name.startsWith(".")) && fileMatchesPattern(fullPath, options.pattern)) results.push(fullPath);
1295
+ }
1296
+ }
1297
+ searchDirectory(dirPath);
1298
+ return { content: results.join("\n") };
1299
+ } catch (error) {
1300
+ return {
1301
+ content: "",
1302
+ error: `Failed to find files: ${error instanceof Error ? error.message : String(error)}`
1303
+ };
1304
+ }
1305
+ }
1306
+ /**
1113
1307
  * Search for pattern in files using ripgrep if available, fallback to Node.js
1114
1308
  */
1115
1309
  async function grepSearch(pattern, dirPath) {
@@ -1333,10 +1527,13 @@ var REQUIRED_STRING_ARGS = {
1333
1527
  "oldText",
1334
1528
  "newText"
1335
1529
  ],
1530
+ [CREATE_DIRECTORY]: ["path"],
1531
+ [RENAME_PATH]: ["from", "to"],
1532
+ [DELETE_PATH]: ["path"],
1336
1533
  [RUN_SHELL]: ["command"],
1337
1534
  [LIST_DIR]: ["path"],
1535
+ [FIND_FILES]: ["path"],
1338
1536
  [GREP_SEARCH]: ["pattern", "path"],
1339
- [VIEW_RANGE]: ["path"],
1340
1537
  [WEB_SEARCH]: ["query"],
1341
1538
  [WEB_FETCH]: ["url"]
1342
1539
  };
@@ -1351,14 +1548,44 @@ function validateArgs(name, args) {
1351
1548
  content: "",
1352
1549
  error: `Missing required argument: ${key} (received keys: ${received})`
1353
1550
  };
1354
- if (name === "view_range") {
1355
- if (!Number.isInteger(args.start) || !Number.isInteger(args.end)) return {
1551
+ if (name === "read_file") {
1552
+ for (const key of [
1553
+ "startLine",
1554
+ "endLine",
1555
+ "maxLines"
1556
+ ]) if (args[key] !== void 0 && !Number.isInteger(args[key])) return {
1557
+ content: "",
1558
+ error: `Invalid optional numeric argument: ${key} (received keys: ${received})`
1559
+ };
1560
+ if (typeof args.startLine === "number" && args.startLine < 1 || typeof args.endLine === "number" && args.endLine < 1 || typeof args.maxLines === "number" && args.maxLines < 1) return {
1561
+ content: "",
1562
+ error: "Invalid read range: startLine, endLine, and maxLines must be >= 1"
1563
+ };
1564
+ if (args.endLine !== void 0 && args.maxLines !== void 0) return {
1356
1565
  content: "",
1357
- error: `Missing required numeric arguments: start, end (received keys: ${received})`
1566
+ error: "Invalid read range: endLine cannot be combined with maxLines"
1358
1567
  };
1359
- if (args.start < 1 || args.end < args.start) return {
1568
+ if (typeof args.startLine === "number" && typeof args.endLine === "number" && args.endLine < args.startLine) return {
1569
+ content: "",
1570
+ error: "Invalid read range: endLine must be >= startLine"
1571
+ };
1572
+ }
1573
+ if (name === "delete_path" && typeof args.recursive !== "boolean") return {
1574
+ content: "",
1575
+ error: `Missing required boolean argument: recursive (received keys: ${received})`
1576
+ };
1577
+ if (name === "find_files" && args.pattern !== void 0 && typeof args.pattern !== "string") return {
1578
+ content: "",
1579
+ error: `Invalid optional argument: pattern must be a string (received keys: ${received})`
1580
+ };
1581
+ if (name === "find_files" && args.includeHidden !== void 0 && typeof args.includeHidden !== "boolean") return {
1582
+ content: "",
1583
+ error: `Invalid optional argument: includeHidden must be a boolean (received keys: ${received})`
1584
+ };
1585
+ if (name === "find_files" && args.ignoredDirs !== void 0) {
1586
+ if (!Array.isArray(args.ignoredDirs) || !args.ignoredDirs.every((value) => typeof value === "string")) return {
1360
1587
  content: "",
1361
- error: "Invalid line range: start must be >= 1 and end must be >= start"
1588
+ error: `Invalid optional argument: ignoredDirs must be an array of strings (received keys: ${received})`
1362
1589
  };
1363
1590
  }
1364
1591
  if (name === "web_fetch") try {
@@ -1435,13 +1662,24 @@ async function executeTool(name, args, options) {
1435
1662
  if (invalid) return invalid;
1436
1663
  const stringArgs = args;
1437
1664
  switch (name) {
1438
- case READ_FILE: return readFile(stringArgs.path);
1665
+ case READ_FILE: return readFile(stringArgs.path, {
1666
+ endLine: args.endLine,
1667
+ maxLines: args.maxLines,
1668
+ startLine: args.startLine
1669
+ });
1439
1670
  case WRITE_FILE: return writeFile(stringArgs.path, stringArgs.content);
1440
1671
  case EDIT_FILE: return editFile(stringArgs.path, stringArgs.oldText, stringArgs.newText);
1672
+ case CREATE_DIRECTORY: return createDirectory(stringArgs.path);
1673
+ case RENAME_PATH: return renamePath(stringArgs.from, stringArgs.to);
1674
+ case DELETE_PATH: return deletePath(stringArgs.path, args.recursive);
1441
1675
  case RUN_SHELL: return runShell(stringArgs.command);
1442
1676
  case LIST_DIR: return listDir(stringArgs.path);
1677
+ case FIND_FILES: return findFiles(stringArgs.path, {
1678
+ ignoredDirs: args.ignoredDirs,
1679
+ includeHidden: args.includeHidden,
1680
+ pattern: stringArgs.pattern
1681
+ });
1443
1682
  case GREP_SEARCH: return await grepSearch(stringArgs.pattern, stringArgs.path);
1444
- case VIEW_RANGE: return viewRange(stringArgs.path, args.start, args.end);
1445
1683
  case WEB_SEARCH: return await webSearch(stringArgs.query);
1446
1684
  case WEB_FETCH: return await webFetch(stringArgs.url);
1447
1685
  // v8 ignore next 2
@@ -1582,7 +1820,7 @@ async function main(args = process.argv.slice(2)) {
1582
1820
  else await launchTui();
1583
1821
  }
1584
1822
  async function launchTui(sessionId) {
1585
- const { renderApp } = await import("./assets/tui-iewVFcZW.js");
1823
+ const { renderApp } = await import("./assets/tui-DEmaVgHT.js");
1586
1824
  reset();
1587
1825
  renderApp(sessionId);
1588
1826
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.23.1",
3
+ "version": "0.24.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",