code-ollama 0.23.2 → 0.24.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.
@@ -1411,7 +1411,7 @@ function ChatInput({ history: sessionHistory, isDisabled = false, onInterrupt, o
1411
1411
  }
1412
1412
  //#endregion
1413
1413
  //#region src/components/Chat/constants.ts
1414
- var ACTION_NOT_PERFORMED = "The requested action was NOT performed";
1414
+ var ACTION_NOT_PERFORMED = "The requested action did not complete successfully";
1415
1415
  var PLAN_CHECKLIST_REMINDER = "Then display the plan using either the Plan Needs Input or Proposed Plan Markdown template";
1416
1416
  var PLAN_EXECUTION_REMINDER = `Do not claim success and do not call ${Array.from(WRITE_TOOLS).join(", ")} until the user approves execution`;
1417
1417
  var ChatActionType = /* @__PURE__ */ function(ChatActionType) {
@@ -1445,6 +1445,18 @@ function hasExecutablePlan(content) {
1445
1445
  const nextSectionIndex = lines.findIndex((line, index) => index > executionStepsIndex && /^#{1,6}\s+\S/.test(line.trim()));
1446
1446
  return lines.slice(executionStepsIndex + 1, nextSectionIndex === -1 ? void 0 : nextSectionIndex).some((line) => /^(?:[-*]|\d+[.)])\s+\S/.test(line.trim()));
1447
1447
  }
1448
+ function isPlanModeFinal(content) {
1449
+ const firstHeading = content.split("\n").find((line) => line.trim())?.trim().toLowerCase();
1450
+ return isPlanNeedsInput(content) || firstHeading === "## proposed plan";
1451
+ }
1452
+ function isPlanNeedsInput(content) {
1453
+ return content.split("\n").find((line) => line.trim())?.trim().toLowerCase() === "## plan needs input";
1454
+ }
1455
+ function isDirectPlanAnswer(content) {
1456
+ const normalized = content.trim();
1457
+ if (!normalized || isPlanModeFinal(normalized)) return false;
1458
+ return !/^(?:research(?: is)? complete|done)\.?$/i.test(normalized);
1459
+ }
1448
1460
  //#endregion
1449
1461
  //#region src/components/Chat/reducer.ts
1450
1462
  function createInitialChatState(messages = []) {
@@ -1667,7 +1679,9 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1667
1679
  toolResultMessages.push(buildToolResultMessage(toolCall.function.name, {
1668
1680
  content: "",
1669
1681
  // v8 ignore next
1670
- error: error instanceof Error ? error.message : String(error)
1682
+ error: error instanceof Error ? error.message : String(error),
1683
+ // v8 ignore next
1684
+ ...error instanceof Error && error.stack ? { stack: error.stack } : {}
1671
1685
  }));
1672
1686
  }
1673
1687
  nextMessages = [...updatedMessages, ...toolResultMessages];
@@ -1746,7 +1760,7 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1746
1760
  mode,
1747
1761
  theme
1748
1762
  ]);
1749
- const processStreamReadOnly = useCallback(async (currentMessages) => {
1763
+ const processStreamReadOnly = useCallback(async (currentMessages, toolIntentCorrections = 0) => {
1750
1764
  const modelName = model;
1751
1765
  // v8 ignore next
1752
1766
  if (!modelName) throw new Error("Model is required");
@@ -1843,7 +1857,9 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1843
1857
  /* v8 ignore start */
1844
1858
  const toolResultMessage = buildToolResultMessage(toolCall.function.name, {
1845
1859
  content: "",
1846
- error: error instanceof Error ? error.message : String(error)
1860
+ error: error instanceof Error ? error.message : String(error),
1861
+ // v8 ignore next
1862
+ ...error instanceof Error && error.stack ? { stack: error.stack } : {}
1847
1863
  });
1848
1864
  const newMessages = [...updatedMessages, toolResultMessage];
1849
1865
  dispatch({
@@ -1866,6 +1882,13 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1866
1882
  }
1867
1883
  await prewarmCodeBlocks(assistantMessage.content, theme);
1868
1884
  const researchMessages = commitAssistantMessage();
1885
+ if (isPlanNeedsInput(assistantMessage.content)) {
1886
+ dispatch({
1887
+ type: ChatActionType.SetLoading,
1888
+ isLoading: false
1889
+ });
1890
+ return;
1891
+ }
1869
1892
  if (hasExecutablePlan(assistantMessage.content)) {
1870
1893
  dispatch({
1871
1894
  type: ChatActionType.RequestPlanReview,
@@ -1876,6 +1899,32 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
1876
1899
  });
1877
1900
  return;
1878
1901
  }
1902
+ if (hasUncalledToolIntent(assistantMessage.content) && toolIntentCorrections < MAX_TOOL_INTENT_CORRECTIONS) {
1903
+ const correctedMessages = [...researchMessages, {
1904
+ role: SYSTEM,
1905
+ content: TOOL_INTENT_CORRECTION
1906
+ }];
1907
+ dispatch({
1908
+ type: ChatActionType.CommitMessages,
1909
+ messages: correctedMessages
1910
+ });
1911
+ await processStreamReadOnly(correctedMessages, toolIntentCorrections + 1);
1912
+ return;
1913
+ }
1914
+ if (isPlanModeFinal(assistantMessage.content)) {
1915
+ dispatch({
1916
+ type: ChatActionType.SetLoading,
1917
+ isLoading: false
1918
+ });
1919
+ return;
1920
+ }
1921
+ if (currentMessages.some((message) => !!message.toolResult) && isDirectPlanAnswer(assistantMessage.content)) {
1922
+ dispatch({
1923
+ type: ChatActionType.SetLoading,
1924
+ isLoading: false
1925
+ });
1926
+ return;
1927
+ }
1879
1928
  const planInstruction = {
1880
1929
  role: SYSTEM,
1881
1930
  content: PLAN_GENERATION_INSTRUCTION
@@ -2003,6 +2052,13 @@ function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onMod
2003
2052
  });
2004
2053
  switch (decision) {
2005
2054
  case APPROVE: {
2055
+ dispatch({
2056
+ type: ChatActionType.SetStreamingMessage,
2057
+ message: {
2058
+ role: ASSISTANT,
2059
+ content: ""
2060
+ }
2061
+ });
2006
2062
  const result = await executeToolCall(toolCall);
2007
2063
  const toolResultMessage = buildToolResultMessage(toolCall.function.name, result, toolCall.function.arguments);
2008
2064
  const newMessages = [...approvedMessages, toolResultMessage];
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.2";
53
+ var version = "0.24.1";
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)
@@ -217,10 +230,18 @@ Use the exact headings shown below
217
230
  ${PLAN_RESPONSE_TEMPLATE}`;
218
231
  var PLAN_INSTRUCTION = `Plan mode is active
219
232
 
233
+ Explore first:
234
+ - If the user provides an exact file path, inspect it with read_file before planning changes
235
+ - If the user asks "where", names an identifier/symbol, or asks where behavior is implemented, search the codebase with grep_search before answering
236
+ - If the user asks about project structure without a target identifier or path, use list_dir or find_files to locate likely files
237
+ - Prefer targeted grep_search for exact names over broad directory listing when the user provides an identifier
238
+ - After each read-only tool result, decide whether another read-only tool would materially improve the answer
239
+ - Do not produce Plan Needs Input while also saying you will use another read-only tool; call that tool instead
240
+
220
241
  Only use read-only tools: ${PLAN_READ_TOOLS}
221
242
  Do not call ${PLAN_WRITE_TOOLS} during Plan mode
222
243
  Use read-only tools to resolve discoverable facts before asking questions
223
- If the user asks to search, inspect, find, read, or locate something, use read-only tools immediately
244
+ If the user asks to search, inspect, find, read, locate, change, adjust, update, edit, configure, or identify something, use read-only tools immediately
224
245
  Only ask questions for user preferences or product decisions that cannot be discovered from available tools
225
246
  When enough context is available, stop calling tools and produce either Plan Needs Input or Proposed Plan using the required template
226
247
 
@@ -815,10 +836,24 @@ function defineTool(name, description, params, required) {
815
836
  * Tool definitions for Ollama API
816
837
  */
817
838
  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"]),
839
+ defineTool(READ_FILE, "Read the contents of a file at the specified path, optionally limited by line range", {
840
+ path: {
841
+ type: "string",
842
+ description: "The path to the file to read"
843
+ },
844
+ startLine: {
845
+ type: "number",
846
+ description: "Optional starting line number to read from (1-indexed)"
847
+ },
848
+ endLine: {
849
+ type: "number",
850
+ description: "Optional ending line number to read through (inclusive)"
851
+ },
852
+ maxLines: {
853
+ type: "number",
854
+ description: "Optional maximum number of lines to read; cannot be combined with endLine"
855
+ }
856
+ }, ["path"]),
822
857
  defineTool(WRITE_FILE, "Write content to a file at the specified path", {
823
858
  path: {
824
859
  type: "string",
@@ -847,6 +882,30 @@ var TOOLS = [
847
882
  "oldText",
848
883
  "newText"
849
884
  ]),
885
+ defineTool(CREATE_DIRECTORY, "Create a directory and any missing parent directories at the specified path", { path: {
886
+ type: "string",
887
+ description: "The directory path to create"
888
+ } }, ["path"]),
889
+ defineTool(RENAME_PATH, "Rename or move an existing file or directory to a new path", {
890
+ from: {
891
+ type: "string",
892
+ description: "The existing file or directory path to rename or move"
893
+ },
894
+ to: {
895
+ type: "string",
896
+ description: "The destination path for the renamed or moved item"
897
+ }
898
+ }, ["from", "to"]),
899
+ defineTool(DELETE_PATH, "Delete a file or directory at the specified path", {
900
+ path: {
901
+ type: "string",
902
+ description: "The file or directory path to delete"
903
+ },
904
+ recursive: {
905
+ type: "boolean",
906
+ description: "Whether to delete non-empty directories recursively; use false for files and empty directories"
907
+ }
908
+ }, ["path", "recursive"]),
850
909
  defineTool(RUN_SHELL, "Execute a shell command", { command: {
851
910
  type: "string",
852
911
  description: "The shell command to execute"
@@ -855,6 +914,28 @@ var TOOLS = [
855
914
  type: "string",
856
915
  description: "The path to the directory to list"
857
916
  } }, ["path"]),
917
+ defineTool(FIND_FILES, "Recursively find files under a directory, optionally matching a simple substring or wildcard pattern", {
918
+ path: {
919
+ type: "string",
920
+ description: "The directory path to search in"
921
+ },
922
+ pattern: {
923
+ type: "string",
924
+ description: "Optional case-insensitive substring or wildcard pattern to match against file paths"
925
+ },
926
+ includeHidden: {
927
+ type: "boolean",
928
+ description: "Whether to include hidden files and directories; defaults to false"
929
+ },
930
+ ignoredDirs: {
931
+ type: "array",
932
+ description: "Optional directory names or simple wildcard patterns to skip instead of the default ignored directory list; .git is always skipped",
933
+ items: {
934
+ type: "string",
935
+ description: "Directory name or wildcard pattern to skip"
936
+ }
937
+ }
938
+ }, ["path"]),
858
939
  defineTool(GREP_SEARCH, "Search files within a directory; multi-word queries also match common code identifier forms", {
859
940
  pattern: {
860
941
  type: "string",
@@ -865,24 +946,6 @@ var TOOLS = [
865
946
  description: "The directory path to search in"
866
947
  }
867
948
  }, ["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
949
  defineTool(WEB_SEARCH, "Search the web for external or current information", { query: {
887
950
  type: "string",
888
951
  description: "The search query to look up"
@@ -901,6 +964,11 @@ var SHELL_EXEC_OPTIONS = {
901
964
  timeout: 3e4,
902
965
  maxBuffer: 1024 * 1024
903
966
  };
967
+ function getErrorOutput(error) {
968
+ if (typeof error !== "object" || error === null) return "";
969
+ const output = error;
970
+ return [output.stdout, output.stderr].filter((value) => typeof value === "string" && !!value).join("\n");
971
+ }
904
972
  /**
905
973
  * Execute shell command with shared options (throws on error)
906
974
  */
@@ -915,9 +983,12 @@ async function runShell(command) {
915
983
  const { stdout, stderr } = await execShell(command);
916
984
  return { content: stdout || stderr };
917
985
  } catch (error) {
986
+ const message = error instanceof Error ? error.message : String(error);
918
987
  return {
919
- content: "",
920
- error: `Command failed: ${error instanceof Error ? error.message : String(error)}`
988
+ content: getErrorOutput(error),
989
+ error: `Command failed: ${message}`,
990
+ // v8 ignore next
991
+ ...error instanceof Error && error.stack ? { stack: error.stack } : {}
921
992
  };
922
993
  }
923
994
  }
@@ -926,6 +997,17 @@ async function runShell(command) {
926
997
  var DIFF_CONTEXT_LINES = 3;
927
998
  var DIFF_MAX_LINES = 120;
928
999
  var DIFF_MAX_CHARS = 12e3;
1000
+ var DEFAULT_FIND_FILES_IGNORED_DIRS = [
1001
+ "node_modules",
1002
+ "__pycache__",
1003
+ ".*cache",
1004
+ ".tox",
1005
+ ".venv",
1006
+ "venv",
1007
+ "dist",
1008
+ "build",
1009
+ "coverage"
1010
+ ];
929
1011
  function splitLines(content) {
930
1012
  return content.split("\n");
931
1013
  }
@@ -993,16 +1075,63 @@ function buildSearchPatterns(pattern) {
993
1075
  function capitalize(value) {
994
1076
  return value.charAt(0).toUpperCase() + value.slice(1);
995
1077
  }
1078
+ function escapeRegExp(value) {
1079
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1080
+ }
1081
+ function fileMatchesPattern(filePath, pattern) {
1082
+ const trimmedPattern = pattern?.trim();
1083
+ if (!trimmedPattern) return true;
1084
+ const normalizedPath = filePath.toLowerCase();
1085
+ const normalizedFileName = normalizedPath.slice(Math.max(normalizedPath.lastIndexOf("/"), normalizedPath.lastIndexOf("\\")) + 1);
1086
+ const normalizedPattern = trimmedPattern.toLowerCase();
1087
+ if (!normalizedPattern.includes("*") && !normalizedPattern.includes("?")) return normalizedPath.includes(normalizedPattern);
1088
+ const regexPattern = normalizedPattern.split("").map((char) => {
1089
+ if (char === "*") return ".*";
1090
+ if (char === "?") return ".";
1091
+ return escapeRegExp(char);
1092
+ }).join("");
1093
+ const regex = new RegExp(`^${regexPattern}$`);
1094
+ return regex.test(normalizedPath) || regex.test(normalizedFileName);
1095
+ }
1096
+ function valueMatchesWildcardPattern(value, pattern) {
1097
+ const normalizedValue = value.toLowerCase();
1098
+ const normalizedPattern = pattern.trim().toLowerCase();
1099
+ if (!normalizedPattern.includes("*") && !normalizedPattern.includes("?")) return normalizedValue === normalizedPattern;
1100
+ const regexPattern = normalizedPattern.split("").map((char) => {
1101
+ if (char === "*") return ".*";
1102
+ if (char === "?") return ".";
1103
+ return escapeRegExp(char);
1104
+ }).join("");
1105
+ return new RegExp(`^${regexPattern}$`).test(normalizedValue);
1106
+ }
1107
+ function directoryMatchesIgnoredPattern(dirName, ignoredDirs) {
1108
+ for (const ignoredDir of ignoredDirs) if (valueMatchesWildcardPattern(dirName, ignoredDir)) return true;
1109
+ return false;
1110
+ }
1111
+ function formatNumberedLines(lines, startLine) {
1112
+ return lines.map((line, index) => `${String(startLine + index)}: ${line}`).join("\n");
1113
+ }
996
1114
  /**
997
1115
  * Read file contents
998
1116
  */
999
- function readFile(filePath) {
1117
+ function readFile(filePath, options = {}) {
1000
1118
  try {
1001
1119
  if (!existsSync(filePath)) return {
1002
1120
  content: "",
1003
1121
  error: `File not found: ${filePath}`
1004
1122
  };
1005
- return { content: readFileSync(filePath, "utf8") };
1123
+ const content = readFileSync(filePath, "utf8");
1124
+ if (!(options.startLine !== void 0 || options.endLine !== void 0 || options.maxLines !== void 0)) return { content };
1125
+ const lines = content.split("\n");
1126
+ const startLine = options.startLine ?? 1;
1127
+ const endLine = options.endLine ?? startLine + (options.maxLines ?? lines.length) - 1;
1128
+ const startIndex = startLine - 1;
1129
+ const endIndex = Math.min(lines.length, endLine);
1130
+ if (startIndex >= lines.length) return {
1131
+ content: "",
1132
+ error: "Invalid line range"
1133
+ };
1134
+ return { content: formatNumberedLines(lines.slice(startIndex, endIndex), startLine) };
1006
1135
  } catch (error) {
1007
1136
  return {
1008
1137
  content: "",
@@ -1067,26 +1196,73 @@ function editFile(filePath, oldText, newText) {
1067
1196
  }
1068
1197
  }
1069
1198
  /**
1070
- * View specific line range from file
1199
+ * Create a directory and any missing parent directories
1071
1200
  */
1072
- function viewRange(filePath, start, end) {
1201
+ function createDirectory(dirPath) {
1073
1202
  try {
1074
- if (!existsSync(filePath)) return {
1203
+ if (existsSync(dirPath)) {
1204
+ if (statSync(dirPath).isDirectory()) return { content: `Directory already exists: ${dirPath}` };
1205
+ return {
1206
+ content: "",
1207
+ error: `Path already exists and is not a directory: ${dirPath}`
1208
+ };
1209
+ }
1210
+ mkdirSync(dirPath, { recursive: true });
1211
+ return { content: `Directory created successfully: ${dirPath}` };
1212
+ } catch (error) {
1213
+ return {
1075
1214
  content: "",
1076
- error: `File not found: ${filePath}`
1215
+ error: `Failed to create directory: ${error instanceof Error ? error.message : String(error)}`
1216
+ };
1217
+ }
1218
+ }
1219
+ /**
1220
+ * Rename or move an existing file or directory
1221
+ */
1222
+ function renamePath(fromPath, toPath) {
1223
+ try {
1224
+ if (!existsSync(fromPath)) return {
1225
+ content: "",
1226
+ error: `Source path not found: ${fromPath}`
1077
1227
  };
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 {
1228
+ if (existsSync(toPath)) return {
1082
1229
  content: "",
1083
- error: "Invalid line range"
1230
+ error: `Destination path already exists: ${toPath}`
1231
+ };
1232
+ renameSync(fromPath, toPath);
1233
+ return { content: `Path renamed successfully: ${fromPath} -> ${toPath}` };
1234
+ } catch (error) {
1235
+ return {
1236
+ content: "",
1237
+ error: `Failed to rename path: ${error instanceof Error ? error.message : String(error)}`
1238
+ };
1239
+ }
1240
+ }
1241
+ /**
1242
+ * Delete a file or directory
1243
+ */
1244
+ function deletePath(path, recursive) {
1245
+ try {
1246
+ if (!existsSync(path)) return {
1247
+ content: "",
1248
+ error: `Path not found: ${path}`
1084
1249
  };
1085
- return { content: lines.slice(startIdx, endIdx).join("\n") };
1250
+ if (statSync(path).isDirectory()) {
1251
+ if (readdirSync(path).length > 0 && !recursive) return {
1252
+ content: "",
1253
+ error: `Directory is not empty; set recursive to true to delete: ${path}`
1254
+ };
1255
+ if (recursive) rmSync(path, {
1256
+ recursive: true,
1257
+ force: false
1258
+ });
1259
+ else rmdirSync(path);
1260
+ } else rmSync(path, { force: false });
1261
+ return { content: `Path deleted successfully: ${path}` };
1086
1262
  } catch (error) {
1087
1263
  return {
1088
1264
  content: "",
1089
- error: `Failed to view range: ${error instanceof Error ? error.message : String(error)}`
1265
+ error: `Failed to delete path: ${error instanceof Error ? error.message : String(error)}`
1090
1266
  };
1091
1267
  }
1092
1268
  }
@@ -1110,6 +1286,40 @@ function listDir(dirPath) {
1110
1286
  }
1111
1287
  }
1112
1288
  /**
1289
+ * Recursively find files by path
1290
+ */
1291
+ function findFiles(dirPath, options = {}) {
1292
+ try {
1293
+ if (!existsSync(dirPath)) return {
1294
+ content: "",
1295
+ error: `Directory not found: ${dirPath}`
1296
+ };
1297
+ if (!statSync(dirPath).isDirectory()) return {
1298
+ content: "",
1299
+ error: `Path is not a directory: ${dirPath}`
1300
+ };
1301
+ const results = [];
1302
+ const includeHidden = options.includeHidden ?? false;
1303
+ const ignoredDirs = new Set(options.ignoredDirs ?? DEFAULT_FIND_FILES_IGNORED_DIRS);
1304
+ function searchDirectory(currentPath) {
1305
+ const entries = readdirSync(currentPath, { withFileTypes: true });
1306
+ for (const entry of entries) {
1307
+ const fullPath = join(currentPath, entry.name);
1308
+ if (entry.isDirectory()) {
1309
+ if (entry.name !== ".git" && !directoryMatchesIgnoredPattern(entry.name, ignoredDirs) && (includeHidden || !entry.name.startsWith("."))) searchDirectory(fullPath);
1310
+ } else if (entry.isFile() && (includeHidden || !entry.name.startsWith(".")) && fileMatchesPattern(fullPath, options.pattern)) results.push(fullPath);
1311
+ }
1312
+ }
1313
+ searchDirectory(dirPath);
1314
+ return { content: results.join("\n") };
1315
+ } catch (error) {
1316
+ return {
1317
+ content: "",
1318
+ error: `Failed to find files: ${error instanceof Error ? error.message : String(error)}`
1319
+ };
1320
+ }
1321
+ }
1322
+ /**
1113
1323
  * Search for pattern in files using ripgrep if available, fallback to Node.js
1114
1324
  */
1115
1325
  async function grepSearch(pattern, dirPath) {
@@ -1333,10 +1543,13 @@ var REQUIRED_STRING_ARGS = {
1333
1543
  "oldText",
1334
1544
  "newText"
1335
1545
  ],
1546
+ [CREATE_DIRECTORY]: ["path"],
1547
+ [RENAME_PATH]: ["from", "to"],
1548
+ [DELETE_PATH]: ["path"],
1336
1549
  [RUN_SHELL]: ["command"],
1337
1550
  [LIST_DIR]: ["path"],
1551
+ [FIND_FILES]: ["path"],
1338
1552
  [GREP_SEARCH]: ["pattern", "path"],
1339
- [VIEW_RANGE]: ["path"],
1340
1553
  [WEB_SEARCH]: ["query"],
1341
1554
  [WEB_FETCH]: ["url"]
1342
1555
  };
@@ -1351,14 +1564,44 @@ function validateArgs(name, args) {
1351
1564
  content: "",
1352
1565
  error: `Missing required argument: ${key} (received keys: ${received})`
1353
1566
  };
1354
- if (name === "view_range") {
1355
- if (!Number.isInteger(args.start) || !Number.isInteger(args.end)) return {
1567
+ if (name === "read_file") {
1568
+ for (const key of [
1569
+ "startLine",
1570
+ "endLine",
1571
+ "maxLines"
1572
+ ]) if (args[key] !== void 0 && !Number.isInteger(args[key])) return {
1573
+ content: "",
1574
+ error: `Invalid optional numeric argument: ${key} (received keys: ${received})`
1575
+ };
1576
+ if (typeof args.startLine === "number" && args.startLine < 1 || typeof args.endLine === "number" && args.endLine < 1 || typeof args.maxLines === "number" && args.maxLines < 1) return {
1577
+ content: "",
1578
+ error: "Invalid read range: startLine, endLine, and maxLines must be >= 1"
1579
+ };
1580
+ if (args.endLine !== void 0 && args.maxLines !== void 0) return {
1356
1581
  content: "",
1357
- error: `Missing required numeric arguments: start, end (received keys: ${received})`
1582
+ error: "Invalid read range: endLine cannot be combined with maxLines"
1358
1583
  };
1359
- if (args.start < 1 || args.end < args.start) return {
1584
+ if (typeof args.startLine === "number" && typeof args.endLine === "number" && args.endLine < args.startLine) return {
1360
1585
  content: "",
1361
- error: "Invalid line range: start must be >= 1 and end must be >= start"
1586
+ error: "Invalid read range: endLine must be >= startLine"
1587
+ };
1588
+ }
1589
+ if (name === "delete_path" && typeof args.recursive !== "boolean") return {
1590
+ content: "",
1591
+ error: `Missing required boolean argument: recursive (received keys: ${received})`
1592
+ };
1593
+ if (name === "find_files" && args.pattern !== void 0 && typeof args.pattern !== "string") return {
1594
+ content: "",
1595
+ error: `Invalid optional argument: pattern must be a string (received keys: ${received})`
1596
+ };
1597
+ if (name === "find_files" && args.includeHidden !== void 0 && typeof args.includeHidden !== "boolean") return {
1598
+ content: "",
1599
+ error: `Invalid optional argument: includeHidden must be a boolean (received keys: ${received})`
1600
+ };
1601
+ if (name === "find_files" && args.ignoredDirs !== void 0) {
1602
+ if (!Array.isArray(args.ignoredDirs) || !args.ignoredDirs.every((value) => typeof value === "string")) return {
1603
+ content: "",
1604
+ error: `Invalid optional argument: ignoredDirs must be an array of strings (received keys: ${received})`
1362
1605
  };
1363
1606
  }
1364
1607
  if (name === "web_fetch") try {
@@ -1390,14 +1633,16 @@ function normalizeToolCall(toolCall) {
1390
1633
  }
1391
1634
  function formatToolResultContent(toolName, result, args) {
1392
1635
  const formattedArgs = args ? `(${formatToolArguments(args)})` : "";
1393
- const status = result.error ? "The requested action was NOT performed" : "";
1636
+ const status = result.error ? "The requested action did not complete successfully" : "";
1394
1637
  const content = result.content ? `\n${result.content}` : "";
1395
1638
  const error = result.error ? `\nError: ${result.error}` : "";
1639
+ const stack = result.error && result.stack ? `\nStack trace:\n${result.stack}` : "";
1396
1640
  return [
1397
1641
  `Tool ${toolName}${formattedArgs} result:`,
1398
1642
  status,
1399
1643
  content.trim(),
1400
- error.trim()
1644
+ error.trim(),
1645
+ stack.trim()
1401
1646
  ].filter(Boolean).join("\n");
1402
1647
  }
1403
1648
  function formatToolArguments(args) {
@@ -1415,7 +1660,9 @@ async function executeToolCall(toolCall, options) {
1415
1660
  return {
1416
1661
  content: "",
1417
1662
  // v8 ignore next
1418
- error: error instanceof Error ? error.message : String(error)
1663
+ error: error instanceof Error ? error.message : String(error),
1664
+ // v8 ignore next
1665
+ ...error instanceof Error && error.stack ? { stack: error.stack } : {}
1419
1666
  };
1420
1667
  }
1421
1668
  }
@@ -1435,13 +1682,24 @@ async function executeTool(name, args, options) {
1435
1682
  if (invalid) return invalid;
1436
1683
  const stringArgs = args;
1437
1684
  switch (name) {
1438
- case READ_FILE: return readFile(stringArgs.path);
1685
+ case READ_FILE: return readFile(stringArgs.path, {
1686
+ endLine: args.endLine,
1687
+ maxLines: args.maxLines,
1688
+ startLine: args.startLine
1689
+ });
1439
1690
  case WRITE_FILE: return writeFile(stringArgs.path, stringArgs.content);
1440
1691
  case EDIT_FILE: return editFile(stringArgs.path, stringArgs.oldText, stringArgs.newText);
1692
+ case CREATE_DIRECTORY: return createDirectory(stringArgs.path);
1693
+ case RENAME_PATH: return renamePath(stringArgs.from, stringArgs.to);
1694
+ case DELETE_PATH: return deletePath(stringArgs.path, args.recursive);
1441
1695
  case RUN_SHELL: return runShell(stringArgs.command);
1442
1696
  case LIST_DIR: return listDir(stringArgs.path);
1697
+ case FIND_FILES: return findFiles(stringArgs.path, {
1698
+ ignoredDirs: args.ignoredDirs,
1699
+ includeHidden: args.includeHidden,
1700
+ pattern: stringArgs.pattern
1701
+ });
1443
1702
  case GREP_SEARCH: return await grepSearch(stringArgs.pattern, stringArgs.path);
1444
- case VIEW_RANGE: return viewRange(stringArgs.path, args.start, args.end);
1445
1703
  case WEB_SEARCH: return await webSearch(stringArgs.query);
1446
1704
  case WEB_FETCH: return await webFetch(stringArgs.url);
1447
1705
  // v8 ignore next 2
@@ -1582,7 +1840,7 @@ async function main(args = process.argv.slice(2)) {
1582
1840
  else await launchTui();
1583
1841
  }
1584
1842
  async function launchTui(sessionId) {
1585
- const { renderApp } = await import("./assets/tui-DEmaVgHT.js");
1843
+ const { renderApp } = await import("./assets/tui-CboegfoT.js");
1586
1844
  reset();
1587
1845
  renderApp(sessionId);
1588
1846
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.23.2",
3
+ "version": "0.24.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",
@@ -20,7 +20,7 @@
20
20
  "prepublishOnly": "npm run build && npm run lint && npm run lint:tsc && npm run test:ci",
21
21
  "test": "vitest run",
22
22
  "test:ci": "CI=true npm test -- --color --coverage",
23
- "test:watch": "vitest --coverage.enabled=false"
23
+ "test:watch": "vitest"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",