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.
- package/dist/assets/{tui-DEmaVgHT.js → tui-CboegfoT.js} +60 -4
- package/dist/cli.js +314 -56
- package/package.json +2 -2
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
*
|
|
1199
|
+
* Create a directory and any missing parent directories
|
|
1071
1200
|
*/
|
|
1072
|
-
function
|
|
1201
|
+
function createDirectory(dirPath) {
|
|
1073
1202
|
try {
|
|
1074
|
-
if (
|
|
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: `
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 === "
|
|
1355
|
-
|
|
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:
|
|
1582
|
+
error: "Invalid read range: endLine cannot be combined with maxLines"
|
|
1358
1583
|
};
|
|
1359
|
-
if (args.
|
|
1584
|
+
if (typeof args.startLine === "number" && typeof args.endLine === "number" && args.endLine < args.startLine) return {
|
|
1360
1585
|
content: "",
|
|
1361
|
-
error: "Invalid
|
|
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
|
|
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-
|
|
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.
|
|
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
|
|
23
|
+
"test:watch": "vitest"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|