code-ollama 0.10.0 → 0.12.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  [![build](https://github.com/ai-action/code-ollama/actions/workflows/build.yml/badge.svg)](https://github.com/ai-action/code-ollama/actions/workflows/build.yml)
14
14
  [![codecov](https://codecov.io/gh/ai-action/code-ollama/graph/badge.svg?token=gRGUasRn2k)](https://codecov.io/gh/ai-action/code-ollama)
15
15
 
16
- 🦙 [Ollama](https://ollama.com/) coding agent that runs in your terminal.
16
+ 🦙 [Ollama](https://ollama.com/) coding agent that runs in your terminal. Read the [wiki](https://github.com/ai-action/code-ollama/wiki).
17
17
 
18
18
  ## Quick Start
19
19
 
@@ -0,0 +1,46 @@
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ //#region \0rolldown/runtime.js
4
+ var __defProp = Object.defineProperty;
5
+ var __exportAll = (all, no_symbols) => {
6
+ let target = {};
7
+ for (var name in all) __defProp(target, name, {
8
+ get: all[name],
9
+ enumerable: true
10
+ });
11
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
12
+ return target;
13
+ };
14
+ //#endregion
15
+ //#region src/utils/tools/shell.ts
16
+ var shell_exports = /* @__PURE__ */ __exportAll({
17
+ execShell: () => execShell,
18
+ runShell: () => runShell
19
+ });
20
+ var execAsync = promisify(exec);
21
+ var SHELL_EXEC_OPTIONS = {
22
+ timeout: 3e4,
23
+ maxBuffer: 1024 * 1024
24
+ };
25
+ /**
26
+ * Execute shell command with shared options (throws on error)
27
+ */
28
+ function execShell(command) {
29
+ return execAsync(command, SHELL_EXEC_OPTIONS);
30
+ }
31
+ /**
32
+ * Execute shell command
33
+ */
34
+ async function runShell(command) {
35
+ try {
36
+ const { stdout, stderr } = await execShell(command);
37
+ return { content: stdout || stderr };
38
+ } catch (error) {
39
+ return {
40
+ content: "",
41
+ error: `Command failed: ${error instanceof Error ? error.message : String(error)}`
42
+ };
43
+ }
44
+ }
45
+ //#endregion
46
+ export { shell_exports as n, runShell as t };
@@ -1,4 +1,4 @@
1
- import { _ as VERSION, a as tick, c as setClearHandler, d as loadConfig, f as saveConfig, g as PLAN_GENERATION_INSTRUCTION, h as ROLE, i as executeTool, l as listModels, m as withSystemMessage, n as TOOLS, o as clear, p as resetSystemMessage, r as WRITE_TOOLS, s as reset, t as READ_TOOLS, u as streamChat } from "../cli.js";
1
+ import { _ as USER, a as tick, c as setClearHandler, d as loadConfig, f as saveConfig, g as SYSTEM, h as ASSISTANT, i as WRITE_TOOLS, l as listModels, m as withSystemMessage, n as READ_TOOLS, o as clear, p as resetSystemMessage, r as TOOLS, s as reset, t as executeTool, u as streamChat, v as PLAN_GENERATION_INSTRUCTION, y as VERSION } from "../cli.js";
2
2
  import { readdirSync } from "node:fs";
3
3
  import { join, relative } from "node:path";
4
4
  import { homedir } from "node:os";
@@ -19,6 +19,10 @@ var LIST = [
19
19
  name: "/model",
20
20
  description: "switch the model"
21
21
  },
22
+ {
23
+ name: "/search",
24
+ description: "configure web search"
25
+ },
22
26
  {
23
27
  name: "/exit",
24
28
  description: "exit the application"
@@ -30,11 +34,9 @@ var APPROVE = "approve";
30
34
  var REJECT = "reject";
31
35
  //#endregion
32
36
  //#region src/constants/mode.ts
33
- var NAME = {
34
- SAFE: "safe",
35
- AUTO: "auto",
36
- PLAN: "plan"
37
- };
37
+ var SAFE = "safe";
38
+ var AUTO = "auto";
39
+ var PLAN = "plan";
38
40
  var LABEL = {
39
41
  safe: "Safe",
40
42
  auto: "Auto",
@@ -97,7 +99,7 @@ var CodeBlock = memo(function CodeBlock({ code, language, role }) {
97
99
  code,
98
100
  language
99
101
  ]);
100
- const isSystem = role === ROLE.SYSTEM;
102
+ const isSystem = role === SYSTEM;
101
103
  return /* @__PURE__ */ jsx(Box, {
102
104
  flexDirection: "column",
103
105
  borderStyle: "round",
@@ -145,9 +147,9 @@ var TURN_ABORTED_MESSAGE = [
145
147
  //#region src/components/Messages/Messages.tsx
146
148
  function getMessageColor(role) {
147
149
  switch (role) {
148
- case ROLE.USER: return "black";
149
- case ROLE.ASSISTANT: return "cyan";
150
- case ROLE.SYSTEM: return "gray";
150
+ case USER: return "black";
151
+ case ASSISTANT: return "cyan";
152
+ case SYSTEM: return "gray";
151
153
  default: return;
152
154
  }
153
155
  }
@@ -192,8 +194,8 @@ function parseContent(content) {
192
194
  }
193
195
  var Message = memo(function Message({ message }) {
194
196
  const messageColor = getMessageColor(message.role);
195
- const isSystem = message.role === ROLE.SYSTEM;
196
- const isUser = message.role === ROLE.USER;
197
+ const isSystem = message.role === SYSTEM;
198
+ const isUser = message.role === USER;
197
199
  if (isSystem) return /* @__PURE__ */ jsx(Box, {
198
200
  flexDirection: "column",
199
201
  marginBottom: 1,
@@ -306,15 +308,15 @@ function SelectPromptHint({ message = "Select option", escapeLabel = "cancel" })
306
308
  var options$1 = [
307
309
  {
308
310
  label: "Auto - Execute tools automatically",
309
- value: NAME.AUTO
311
+ value: AUTO
310
312
  },
311
313
  {
312
314
  label: "Safe - Approve each tool",
313
- value: NAME.SAFE
315
+ value: SAFE
314
316
  },
315
317
  {
316
318
  label: "Cancel - Continue planning",
317
- value: NAME.PLAN
319
+ value: PLAN
318
320
  }
319
321
  ];
320
322
  function PlanApproval({ planContent, onModeChange }) {
@@ -324,7 +326,7 @@ function PlanApproval({ planContent, onModeChange }) {
324
326
  onModeChange(value);
325
327
  }, [onModeChange]),
326
328
  onCancel: useCallback(() => {
327
- onModeChange(NAME.PLAN);
329
+ onModeChange(PLAN);
328
330
  }, [onModeChange]),
329
331
  children: /* @__PURE__ */ jsxs(Box, {
330
332
  flexDirection: "column",
@@ -529,6 +531,16 @@ function getMentionMatch(input) {
529
531
  query: match[2]
530
532
  };
531
533
  }
534
+ /**
535
+ * Sort files alphabetically within each group:
536
+ * 1. Non-dot files first
537
+ * 2. Dot files second
538
+ */
539
+ function sortFilePaths(left, right) {
540
+ const isDotLeft = left.split("/").some((segment) => segment.startsWith("."));
541
+ if (isDotLeft !== right.split("/").some((segment) => segment.startsWith("."))) return isDotLeft ? 1 : -1;
542
+ return left.localeCompare(right);
543
+ }
532
544
  function buildNextInput(input, filePath) {
533
545
  const mentionMatch = getMentionMatch(input);
534
546
  // v8 ignore next 3
@@ -559,7 +571,7 @@ function listProjectFilesFallback(rootDir) {
559
571
  }
560
572
  }
561
573
  walk(rootDir);
562
- return filePaths.sort((left, right) => left.localeCompare(right));
574
+ return filePaths.sort(sortFilePaths);
563
575
  }
564
576
  function listProjectFilesWithRipgrep(rootDir) {
565
577
  return new Promise((resolve, reject) => {
@@ -571,7 +583,7 @@ function listProjectFilesWithRipgrep(rootDir) {
571
583
  reject(error);
572
584
  return;
573
585
  }
574
- resolve(stdout.split("\n").map((line) => line.trim()).filter(Boolean).map(normalizePath).sort((left, right) => left.localeCompare(right)));
586
+ resolve(stdout.split("\n").map((line) => line.trim()).filter(Boolean).map(normalizePath).sort(sortFilePaths));
575
587
  });
576
588
  });
577
589
  }
@@ -770,7 +782,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
770
782
  }, [sessionId]);
771
783
  const buildToolResultMessage = useCallback((toolName, result) => {
772
784
  if (result.error?.startsWith("Tool not allowed:")) return {
773
- role: ROLE.SYSTEM,
785
+ role: SYSTEM,
774
786
  content: [
775
787
  `Tool ${toolName} was blocked by execution policy`,
776
788
  ACTION_NOT_PERFORMED,
@@ -779,12 +791,12 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
779
791
  ].join("\n")
780
792
  };
781
793
  return {
782
- role: ROLE.SYSTEM,
794
+ role: SYSTEM,
783
795
  content: `Tool ${toolName} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
784
796
  };
785
797
  }, []);
786
798
  const buildPlanModeCorrectionMessage = useCallback((toolName) => ({
787
- role: ROLE.SYSTEM,
799
+ role: SYSTEM,
788
800
  content: [
789
801
  `Plan mode policy: ${toolName} cannot be executed during planning`,
790
802
  ACTION_NOT_PERFORMED,
@@ -800,7 +812,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
800
812
  setStreamingMessage(null);
801
813
  setInterruptReason(INTERRUPT_REASON.INTERRUPTED);
802
814
  setMessages((prev) => [...prev, {
803
- role: ROLE.USER,
815
+ role: USER,
804
816
  content: TURN_ABORTED_MESSAGE
805
817
  }]);
806
818
  }, []);
@@ -808,7 +820,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
808
820
  const controller = new AbortController();
809
821
  abortControllerRef.current = controller;
810
822
  const assistantMessage = {
811
- role: ROLE.ASSISTANT,
823
+ role: ASSISTANT,
812
824
  content: ""
813
825
  };
814
826
  let committedMessages = currentMessages;
@@ -816,7 +828,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
816
828
  const commitAssistantMessage = () => {
817
829
  if (assistantCommitted) {
818
830
  // v8 ignore next
819
- if (committedMessages.at(-1)?.role === ROLE.ASSISTANT) {
831
+ if (committedMessages.at(-1)?.role === "assistant") {
820
832
  committedMessages = [...committedMessages.slice(0, -1), { ...assistantMessage }];
821
833
  setMessages(committedMessages);
822
834
  }
@@ -843,10 +855,10 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
843
855
  } else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
844
856
  const requiresApproval = WRITE_TOOLS.has(toolCall.function.name);
845
857
  // v8 ignore start
846
- const allowedTools = executionMode === NAME.PLAN ? READ_TOOLS : void 0;
858
+ const allowedTools = executionMode === "plan" ? READ_TOOLS : void 0;
847
859
  // v8 ignore stop
848
860
  const updatedMessages = commitAssistantMessage();
849
- if (executionMode === NAME.SAFE && requiresApproval) {
861
+ if (executionMode === "safe" && requiresApproval) {
850
862
  setPendingToolCall(toolCall);
851
863
  setIsLoading(false);
852
864
  return;
@@ -881,7 +893,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
881
893
  const controller = new AbortController();
882
894
  abortControllerRef.current = controller;
883
895
  const assistantMessage = {
884
- role: ROLE.ASSISTANT,
896
+ role: ASSISTANT,
885
897
  content: ""
886
898
  };
887
899
  let committedMessages = currentMessages;
@@ -889,7 +901,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
889
901
  const commitAssistantMessage = () => {
890
902
  if (assistantCommitted) {
891
903
  // v8 ignore next
892
- if (committedMessages.at(-1)?.role === ROLE.ASSISTANT) {
904
+ if (committedMessages.at(-1)?.role === "assistant") {
893
905
  committedMessages = [...committedMessages.slice(0, -1), { ...assistantMessage }];
894
906
  setMessages(committedMessages);
895
907
  }
@@ -934,12 +946,12 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
934
946
  await prewarmCodeBlocks(assistantMessage.content);
935
947
  const researchMessages = commitAssistantMessage();
936
948
  const planInstruction = {
937
- role: ROLE.SYSTEM,
949
+ role: SYSTEM,
938
950
  content: PLAN_GENERATION_INSTRUCTION
939
951
  };
940
952
  const planMessages = [...researchMessages, planInstruction];
941
953
  const planAssistantMessage = {
942
- role: ROLE.ASSISTANT,
954
+ role: ASSISTANT,
943
955
  content: ""
944
956
  };
945
957
  setStreamingMessage(planAssistantMessage);
@@ -984,26 +996,26 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
984
996
  buildToolResultMessage,
985
997
  model
986
998
  ]);
987
- const handlePlanApproval = useCallback(async (choice) => {
999
+ const handlePlanApproval = useCallback(async (mode) => {
988
1000
  // v8 ignore next
989
1001
  if (!pendingPlan) return;
990
1002
  const { messages: planMessages } = pendingPlan;
991
1003
  setPendingPlan(null);
992
- if (choice === NAME.PLAN) {
993
- onModeChange(NAME.PLAN);
1004
+ if (mode === "plan") {
1005
+ onModeChange(PLAN);
994
1006
  const cancelMessage = {
995
- role: ROLE.SYSTEM,
1007
+ role: SYSTEM,
996
1008
  content: "Continuing in Plan mode. No tools were executed."
997
1009
  };
998
1010
  setMessages((previousMessages) => [...previousMessages, cancelMessage]);
999
1011
  return;
1000
1012
  }
1001
- const selectedMode = choice === NAME.AUTO ? NAME.AUTO : NAME.SAFE;
1013
+ const selectedMode = mode === "auto" ? AUTO : SAFE;
1002
1014
  onModeChange(selectedMode);
1003
1015
  setIsLoading(true);
1004
1016
  const executeInstruction = {
1005
- role: ROLE.SYSTEM,
1006
- content: choice === NAME.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."
1017
+ role: SYSTEM,
1018
+ 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."
1007
1019
  };
1008
1020
  await processStream([...planMessages, executeInstruction], selectedMode);
1009
1021
  }, [
@@ -1021,7 +1033,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
1021
1033
  case APPROVE: {
1022
1034
  const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
1023
1035
  const toolResultMessage = {
1024
- role: ROLE.SYSTEM,
1036
+ role: SYSTEM,
1025
1037
  content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
1026
1038
  };
1027
1039
  const newMessages = [...messages, toolResultMessage];
@@ -1031,7 +1043,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
1031
1043
  }
1032
1044
  case REJECT:
1033
1045
  setMessages((previousMessages) => [...previousMessages, {
1034
- role: ROLE.USER,
1046
+ role: USER,
1035
1047
  content: TURN_ABORTED_MESSAGE
1036
1048
  }]);
1037
1049
  setIsLoading(false);
@@ -1053,12 +1065,12 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
1053
1065
  }
1054
1066
  setIsLoading(true);
1055
1067
  const userMessage = {
1056
- role: ROLE.USER,
1068
+ role: USER,
1057
1069
  content: userContent
1058
1070
  };
1059
1071
  const updatedMessages = [...messages, userMessage];
1060
1072
  setMessages(updatedMessages);
1061
- if (mode === NAME.PLAN) await processStreamReadOnly(updatedMessages);
1073
+ if (mode === "plan") await processStreamReadOnly(updatedMessages);
1062
1074
  else await processStream(updatedMessages);
1063
1075
  }, [
1064
1076
  messages,
@@ -1078,7 +1090,7 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
1078
1090
  }),
1079
1091
  pendingPlan && /* @__PURE__ */ jsx(PlanApproval, {
1080
1092
  planContent: pendingPlan.planContent,
1081
- onModeChange: (selectedMode) => void handlePlanApproval(selectedMode)
1093
+ onModeChange: handlePlanApproval
1082
1094
  }),
1083
1095
  !pendingPlan && pendingToolCall && /* @__PURE__ */ jsx(ToolApproval, {
1084
1096
  toolCall: pendingToolCall,
@@ -1106,9 +1118,9 @@ function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
1106
1118
  //#region src/components/Footer.tsx
1107
1119
  function getModeColor(mode) {
1108
1120
  switch (mode) {
1109
- case NAME.PLAN: return "blue";
1110
- case NAME.AUTO: return "red";
1111
- case NAME.SAFE: return "green";
1121
+ case PLAN: return "blue";
1122
+ case AUTO: return "red";
1123
+ case SAFE: return "green";
1112
1124
  // v8 ignore next
1113
1125
  default: return;
1114
1126
  }
@@ -1202,8 +1214,7 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
1202
1214
  const [options, setOptions] = useState([]);
1203
1215
  const [error, setError] = useState(null);
1204
1216
  useInput(async (_input, key) => {
1205
- if (!options.length) return;
1206
- if (key.return) {
1217
+ if (!error && options.length && key.return) {
1207
1218
  await tick();
1208
1219
  onClose();
1209
1220
  }
@@ -1240,26 +1251,144 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
1240
1251
  });
1241
1252
  }
1242
1253
  //#endregion
1254
+ //#region src/components/SearchSettings.tsx
1255
+ var View = /* @__PURE__ */ function(View) {
1256
+ View["Menu"] = "menu";
1257
+ View["Edit"] = "edit";
1258
+ return View;
1259
+ }(View || {});
1260
+ var Action = /* @__PURE__ */ function(Action) {
1261
+ Action["Set"] = "set";
1262
+ Action["Clear"] = "clear";
1263
+ Action["Cancel"] = "cancel";
1264
+ return Action;
1265
+ }(Action || {});
1266
+ function SearchSettings({ currentUrl, onClose, onSave }) {
1267
+ const [view, setView] = useState(View.Menu);
1268
+ const [draftUrl, setDraftUrl] = useState(currentUrl ?? "");
1269
+ const [error, setError] = useState(null);
1270
+ const options = useMemo(() => {
1271
+ const nextOptions = [{
1272
+ label: currentUrl ? "Update SearXNG URL" : "Set SearXNG URL",
1273
+ value: Action.Set
1274
+ }];
1275
+ if (currentUrl) nextOptions.push({
1276
+ label: "Clear SearXNG URL",
1277
+ value: Action.Clear
1278
+ });
1279
+ nextOptions.push({
1280
+ label: "Cancel",
1281
+ value: Action.Cancel
1282
+ });
1283
+ return nextOptions;
1284
+ }, [currentUrl]);
1285
+ const handleChange = useCallback((value) => {
1286
+ setError(null);
1287
+ switch (value) {
1288
+ case Action.Set:
1289
+ setDraftUrl(currentUrl ?? "");
1290
+ setView(View.Edit);
1291
+ break;
1292
+ case Action.Clear:
1293
+ onSave(void 0);
1294
+ break;
1295
+ case Action.Cancel:
1296
+ default: onClose();
1297
+ }
1298
+ }, [
1299
+ currentUrl,
1300
+ onClose,
1301
+ onSave
1302
+ ]);
1303
+ const handleSubmit = useCallback((value) => {
1304
+ const trimmedValue = value.trim();
1305
+ if (!trimmedValue) {
1306
+ setError("Enter a URL or press Esc to cancel.");
1307
+ return;
1308
+ }
1309
+ try {
1310
+ const url = new URL(trimmedValue);
1311
+ if (!["http:", "https:"].includes(url.protocol)) {
1312
+ setError("URL must use http or https.");
1313
+ return;
1314
+ }
1315
+ onSave(url.toString());
1316
+ } catch {
1317
+ setError("Enter a valid URL.");
1318
+ }
1319
+ }, [onSave]);
1320
+ useInput((input, key) => {
1321
+ if (view === View.Edit && (key.escape || key.ctrl && input === "c")) {
1322
+ setDraftUrl(currentUrl ?? "");
1323
+ setError(null);
1324
+ setView(View.Menu);
1325
+ }
1326
+ });
1327
+ if (view === View.Edit) return /* @__PURE__ */ jsxs(Box, {
1328
+ flexDirection: "column",
1329
+ children: [
1330
+ /* @__PURE__ */ jsx(Text, { children: "Set the SearXNG base URL. DuckDuckGo remains the fallback." }),
1331
+ /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
1332
+ value: draftUrl,
1333
+ onChange: setDraftUrl,
1334
+ onSubmit: handleSubmit,
1335
+ placeholder: "http://localhost:8080"
1336
+ })] }),
1337
+ error && /* @__PURE__ */ jsx(Text, {
1338
+ color: "red",
1339
+ children: error
1340
+ }),
1341
+ /* @__PURE__ */ jsx(Text, {
1342
+ dimColor: true,
1343
+ children: "Press Enter to save, Esc to go back."
1344
+ })
1345
+ ]
1346
+ });
1347
+ return /* @__PURE__ */ jsxs(SelectPrompt, {
1348
+ options,
1349
+ onChange: handleChange,
1350
+ onCancel: onClose,
1351
+ children: [
1352
+ /* @__PURE__ */ jsxs(Text, { children: ["SearXNG URL: ", /* @__PURE__ */ jsx(Text, {
1353
+ color: "cyan",
1354
+ children: currentUrl ?? "not set"
1355
+ })] }),
1356
+ /* @__PURE__ */ jsx(Text, { children: "DuckDuckGo fallback remains available." }),
1357
+ /* @__PURE__ */ jsx(SelectPromptHint, { message: "Manage web search settings" })
1358
+ ]
1359
+ });
1360
+ }
1361
+ //#endregion
1243
1362
  //#region src/components/App.tsx
1363
+ var SCREEN = /* @__PURE__ */ function(SCREEN) {
1364
+ SCREEN["CHAT"] = "chat";
1365
+ SCREEN["MODEL_PICKER"] = "model-picker";
1366
+ SCREEN["SEARCH_SETTINGS"] = "search-settings";
1367
+ return SCREEN;
1368
+ }(SCREEN || {});
1244
1369
  function App() {
1245
1370
  const { exit } = useApp();
1246
- const [model, setModel] = useState(() => loadConfig().model);
1247
- const [picking, setPicking] = useState(false);
1248
- const [mode, setMode] = useState(NAME.SAFE);
1371
+ const [appConfig, setAppConfig] = useState(() => loadConfig());
1372
+ const [currentScreen, setScreen] = useState(SCREEN.CHAT);
1373
+ const [mode, setMode] = useState(SAFE);
1249
1374
  const [sessionId, setSessionId] = useState(0);
1250
1375
  const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
1376
+ const { model, searxngBaseUrl } = appConfig;
1251
1377
  const handleHeaderLoad = useCallback(() => {
1252
1378
  setIsHeaderLoaded(true);
1253
1379
  }, []);
1254
1380
  const handleCommand = useCallback((command) => {
1255
1381
  switch (command) {
1256
1382
  case "/model":
1257
- setPicking(true);
1383
+ setScreen(SCREEN.MODEL_PICKER);
1384
+ break;
1385
+ case "/search":
1386
+ setScreen(SCREEN.SEARCH_SETTINGS);
1258
1387
  break;
1259
1388
  case "/clear":
1260
1389
  resetSystemMessage();
1261
1390
  clear();
1262
- setPicking(false);
1391
+ setScreen(SCREEN.CHAT);
1263
1392
  setSessionId((sessionId) => sessionId + 1);
1264
1393
  break;
1265
1394
  case "/exit":
@@ -1268,34 +1397,52 @@ function App() {
1268
1397
  }
1269
1398
  }, [exit]);
1270
1399
  const handleSelect = useCallback((selected) => {
1271
- setModel(selected);
1400
+ setAppConfig((currentConfig) => ({
1401
+ ...currentConfig,
1402
+ model: selected
1403
+ }));
1272
1404
  saveConfig({ model: selected });
1273
- setPicking(false);
1405
+ setScreen(SCREEN.CHAT);
1406
+ }, []);
1407
+ const handleSaveSearch = useCallback((url) => {
1408
+ setAppConfig((currentConfig) => ({
1409
+ ...currentConfig,
1410
+ searxngBaseUrl: url
1411
+ }));
1412
+ saveConfig({ searxngBaseUrl: url });
1413
+ setScreen(SCREEN.CHAT);
1274
1414
  }, []);
1275
1415
  const handleClose = useCallback(() => {
1276
- setPicking(false);
1416
+ setScreen(SCREEN.CHAT);
1277
1417
  }, []);
1278
1418
  const handleToggleMode = useCallback(() => {
1279
1419
  setMode((mode) => {
1280
1420
  switch (mode) {
1281
- case NAME.SAFE: return NAME.AUTO;
1282
- case NAME.AUTO: return NAME.PLAN;
1283
- case NAME.PLAN:
1284
- default: return NAME.SAFE;
1421
+ case SAFE: return AUTO;
1422
+ case AUTO: return PLAN;
1423
+ case PLAN:
1424
+ default: return SAFE;
1285
1425
  }
1286
1426
  });
1287
1427
  }, []);
1288
- let body;
1289
- switch (true) {
1290
- case picking:
1291
- body = /* @__PURE__ */ jsx(ModelPicker, {
1428
+ let screenContent;
1429
+ switch (currentScreen) {
1430
+ case SCREEN.MODEL_PICKER:
1431
+ screenContent = /* @__PURE__ */ jsx(ModelPicker, {
1292
1432
  currentModel: model,
1293
1433
  onSelect: handleSelect,
1294
1434
  onClose: handleClose
1295
1435
  });
1296
1436
  break;
1297
- default:
1298
- body = /* @__PURE__ */ jsx(Chat, {
1437
+ case SCREEN.SEARCH_SETTINGS:
1438
+ screenContent = /* @__PURE__ */ jsx(SearchSettings, {
1439
+ currentUrl: searxngBaseUrl,
1440
+ onSave: handleSaveSearch,
1441
+ onClose: handleClose
1442
+ });
1443
+ break;
1444
+ case SCREEN.CHAT:
1445
+ screenContent = /* @__PURE__ */ jsx(Chat, {
1299
1446
  model,
1300
1447
  onCommand: handleCommand,
1301
1448
  mode,
@@ -1311,7 +1458,7 @@ function App() {
1311
1458
  model,
1312
1459
  onLoad: handleHeaderLoad
1313
1460
  }),
1314
- isHeaderLoaded && body,
1461
+ isHeaderLoaded && screenContent,
1315
1462
  /* @__PURE__ */ jsx(Footer, {
1316
1463
  mode,
1317
1464
  model,
package/dist/cli.js CHANGED
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ import { t as runShell } from "./assets/shell-CipXM_WI.js";
2
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
3
4
  import cac from "cac";
4
5
  import { join } from "node:path";
5
6
  import { homedir } from "node:os";
6
7
  import { Ollama } from "ollama";
7
- import { exec } from "node:child_process";
8
- import { promisify } from "node:util";
8
+ //#region package.json
9
+ var name = "code-ollama";
10
+ var version = "0.12.0";
9
11
  //#endregion
10
12
  //#region src/constants/package.ts
11
- var VERSION = "0.10.0";
13
+ var NAME = name;
14
+ var VERSION = version;
12
15
  //#endregion
13
16
  //#region src/constants/prompt.ts
14
- var BASE_SYSTEM_PROMPT = `You are a coding assistant that helps users write, edit, and understand code. You have access to tools for reading files, writing files, running shell commands, and searching code
17
+ var BASE_SYSTEM_PROMPT = `You are a coding assistant that helps users write, edit, and understand code. You have access to tools for reading files, writing files, running shell commands, searching code, and searching the web
15
18
 
16
19
  Follow these rules:
17
20
  1. Always use available tools rather than guessing file contents or code behavior
@@ -28,13 +31,15 @@ var TOOL_INSTRUCTIONS = `Available tools:
28
31
  - edit_file: Replace one exact text match in a file (requires approval)
29
32
  - list_dir: List files in a directory
30
33
  - grep_search: Search code with regex
34
+ - web_search: Search the web for current or external information
31
35
  - run_shell: Execute shell commands (requires approval)
32
36
 
33
37
  Always use tools when you need to:
34
38
  - Check file contents before referencing them
35
39
  - Make file changes
36
40
  - Explore project structure
37
- - Search the codebase`;
41
+ - Search the codebase
42
+ - Look up current or external information`;
38
43
  var PLAN_GENERATION_INSTRUCTION = `Based on the research above, decide whether the user request needs code or shell execution
39
44
 
40
45
  If the request needs changes or commands, respond with a plan checklist only
@@ -50,22 +55,19 @@ Only include write_file, edit_file, and run_shell tools in the checklist
50
55
  If no execution is needed, answer normally`;
51
56
  //#endregion
52
57
  //#region src/constants/role.ts
53
- var ROLE = {
54
- USER: "user",
55
- ASSISTANT: "assistant",
56
- SYSTEM: "system"
57
- };
58
+ var USER = "user";
59
+ var ASSISTANT = "assistant";
60
+ var SYSTEM = "system";
58
61
  //#endregion
59
62
  //#region src/constants/tool.ts
60
- var NAME = {
61
- READ_FILE: "read_file",
62
- WRITE_FILE: "write_file",
63
- EDIT_FILE: "edit_file",
64
- RUN_SHELL: "run_shell",
65
- LIST_DIR: "list_dir",
66
- GREP_SEARCH: "grep_search",
67
- VIEW_RANGE: "view_range"
68
- };
63
+ var READ_FILE = "read_file";
64
+ var WRITE_FILE = "write_file";
65
+ var EDIT_FILE = "edit_file";
66
+ var RUN_SHELL = "run_shell";
67
+ var LIST_DIR = "list_dir";
68
+ var GREP_SEARCH = "grep_search";
69
+ var VIEW_RANGE = "view_range";
70
+ var WEB_SEARCH = "web_search";
69
71
  //#endregion
70
72
  //#region src/utils/agents.ts
71
73
  var AGENTS_FILE = "AGENTS.md";
@@ -88,7 +90,7 @@ function buildSystemPrompt() {
88
90
  var systemMessage = null;
89
91
  function createSystemMessage() {
90
92
  return {
91
- role: ROLE.SYSTEM,
93
+ role: SYSTEM,
92
94
  content: buildSystemPrompt()
93
95
  };
94
96
  }
@@ -101,12 +103,10 @@ function withSystemMessage(messages) {
101
103
  }
102
104
  //#endregion
103
105
  //#region src/utils/config.ts
104
- var CONFIG_DIR = join(homedir(), ".code-ollama");
105
- var CONFIG_PATH = join(CONFIG_DIR, "config.json");
106
- var DEFAULTS = {
107
- host: "http://localhost:11434",
108
- model: "gemma4"
109
- };
106
+ var CONFIG_DIRECTORY = join(homedir(), `.${NAME}`);
107
+ var CONFIG_PATH = join(CONFIG_DIRECTORY, "config.json");
108
+ var DEFAULT_HOST = "http://localhost:11434";
109
+ var DEFAULT_MODEL$1 = "gemma4";
110
110
  function readFile$1() {
111
111
  if (!existsSync(CONFIG_PATH)) return {};
112
112
  try {
@@ -118,8 +118,9 @@ function readFile$1() {
118
118
  function loadConfig() {
119
119
  const file = readFile$1();
120
120
  return {
121
- host: process.env.OLLAMA_HOST ?? file.host ?? DEFAULTS.host,
122
- model: process.env.OLLAMA_MODEL ?? file.model ?? DEFAULTS.model
121
+ host: process.env.OLLAMA_HOST ?? file.host ?? DEFAULT_HOST,
122
+ model: process.env.OLLAMA_MODEL ?? file.model ?? DEFAULT_MODEL$1,
123
+ searxngBaseUrl: file.searxngBaseUrl
123
124
  };
124
125
  }
125
126
  function saveConfig(patch) {
@@ -127,7 +128,7 @@ function saveConfig(patch) {
127
128
  ...readFile$1(),
128
129
  ...patch
129
130
  };
130
- mkdirSync(CONFIG_DIR, { recursive: true });
131
+ mkdirSync(CONFIG_DIRECTORY, { recursive: true });
131
132
  writeFileSync(CONFIG_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
132
133
  }
133
134
  //#endregion
@@ -188,8 +189,7 @@ function reset() {
188
189
  //#region src/utils/time.ts
189
190
  var tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
190
191
  //#endregion
191
- //#region src/utils/tools.ts
192
- var execAsync = promisify(exec);
192
+ //#region src/utils/tools/definitions.ts
193
193
  /**
194
194
  * Helper to define tool parameters
195
195
  */
@@ -211,11 +211,11 @@ function defineTool(name, description, params, required) {
211
211
  * Tool definitions for Ollama API
212
212
  */
213
213
  var TOOLS = [
214
- defineTool(NAME.READ_FILE, "Read the contents of a file at the specified path", { path: {
214
+ defineTool(READ_FILE, "Read the contents of a file at the specified path", { path: {
215
215
  type: "string",
216
216
  description: "The path to the file to read"
217
217
  } }, ["path"]),
218
- defineTool(NAME.WRITE_FILE, "Write content to a file at the specified path", {
218
+ defineTool(WRITE_FILE, "Write content to a file at the specified path", {
219
219
  path: {
220
220
  type: "string",
221
221
  description: "The path to the file to write"
@@ -225,7 +225,7 @@ var TOOLS = [
225
225
  description: "The content to write to the file"
226
226
  }
227
227
  }, ["path", "content"]),
228
- defineTool(NAME.EDIT_FILE, "Replace one exact text match in an existing file at the specified path", {
228
+ defineTool(EDIT_FILE, "Replace one exact text match in an existing file at the specified path", {
229
229
  path: {
230
230
  type: "string",
231
231
  description: "The path to the file to edit"
@@ -243,15 +243,15 @@ var TOOLS = [
243
243
  "oldText",
244
244
  "newText"
245
245
  ]),
246
- defineTool(NAME.RUN_SHELL, "Execute a shell command", { command: {
246
+ defineTool(RUN_SHELL, "Execute a shell command", { command: {
247
247
  type: "string",
248
248
  description: "The shell command to execute"
249
249
  } }, ["command"]),
250
- defineTool(NAME.LIST_DIR, "List the contents of a directory", { path: {
250
+ defineTool(LIST_DIR, "List the contents of a directory", { path: {
251
251
  type: "string",
252
252
  description: "The path to the directory to list"
253
253
  } }, ["path"]),
254
- defineTool(NAME.GREP_SEARCH, "Search for a pattern in files within a directory", {
254
+ defineTool(GREP_SEARCH, "Search for a pattern in files within a directory", {
255
255
  pattern: {
256
256
  type: "string",
257
257
  description: "The regex pattern to search for"
@@ -261,7 +261,7 @@ var TOOLS = [
261
261
  description: "The directory path to search in"
262
262
  }
263
263
  }, ["pattern", "path"]),
264
- defineTool(NAME.VIEW_RANGE, "View a specific range of lines from a file", {
264
+ defineTool(VIEW_RANGE, "View a specific range of lines from a file", {
265
265
  path: {
266
266
  type: "string",
267
267
  description: "The path to the file"
@@ -278,41 +278,26 @@ var TOOLS = [
278
278
  "path",
279
279
  "start",
280
280
  "end"
281
- ])
281
+ ]),
282
+ defineTool(WEB_SEARCH, "Search the web for external or current information", { query: {
283
+ type: "string",
284
+ description: "The search query to look up"
285
+ } }, ["query"])
282
286
  ];
283
287
  var READ_TOOLS = new Set([
284
- NAME.READ_FILE,
285
- NAME.LIST_DIR,
286
- NAME.GREP_SEARCH,
287
- NAME.VIEW_RANGE
288
+ READ_FILE,
289
+ LIST_DIR,
290
+ GREP_SEARCH,
291
+ VIEW_RANGE,
292
+ WEB_SEARCH
288
293
  ]);
289
294
  var WRITE_TOOLS = new Set([
290
- NAME.WRITE_FILE,
291
- NAME.EDIT_FILE,
292
- NAME.RUN_SHELL
295
+ WRITE_FILE,
296
+ EDIT_FILE,
297
+ RUN_SHELL
293
298
  ]);
294
- /**
295
- * Execute a tool by name with arguments
296
- */
297
- async function executeTool(name, args, options) {
298
- if (options?.allowedTools && !options.allowedTools.has(name)) return {
299
- content: "",
300
- error: `Tool not allowed: ${name}`
301
- };
302
- switch (name) {
303
- case NAME.READ_FILE: return readFile(args.path);
304
- case NAME.WRITE_FILE: return writeFile(args.path, args.content);
305
- case NAME.EDIT_FILE: return editFile(args.path, args.oldText, args.newText);
306
- case NAME.RUN_SHELL: return runShell(args.command);
307
- case NAME.LIST_DIR: return listDir(args.path);
308
- case NAME.GREP_SEARCH: return await grepSearch(args.pattern, args.path);
309
- case NAME.VIEW_RANGE: return viewRange(args.path, args.start, args.end);
310
- default: return {
311
- content: "",
312
- error: `Unknown tool: ${name}`
313
- };
314
- }
315
- }
299
+ //#endregion
300
+ //#region src/utils/tools/filesystem.ts
316
301
  /**
317
302
  * Read file contents
318
303
  */
@@ -371,27 +356,27 @@ function editFile(filePath, oldText, newText) {
371
356
  };
372
357
  }
373
358
  }
374
- var SHELL_EXEC_OPTIONS = {
375
- timeout: 3e4,
376
- maxBuffer: 1024 * 1024
377
- };
378
- /**
379
- * Execute shell command with shared options (throws on error)
380
- */
381
- function execShell(command) {
382
- return execAsync(command, SHELL_EXEC_OPTIONS);
383
- }
384
359
  /**
385
- * Execute shell command
360
+ * View specific line range from file
386
361
  */
387
- async function runShell(command) {
362
+ function viewRange(filePath, start, end) {
388
363
  try {
389
- const { stdout, stderr } = await execShell(command);
390
- return { content: stdout || stderr };
364
+ if (!existsSync(filePath)) return {
365
+ content: "",
366
+ error: `File not found: ${filePath}`
367
+ };
368
+ const lines = readFileSync(filePath, "utf8").split("\n");
369
+ const startIdx = Math.max(0, start - 1);
370
+ const endIdx = Math.min(lines.length, end);
371
+ if (startIdx >= lines.length || startIdx > endIdx) return {
372
+ content: "",
373
+ error: "Invalid line range"
374
+ };
375
+ return { content: lines.slice(startIdx, endIdx).join("\n") };
391
376
  } catch (error) {
392
377
  return {
393
378
  content: "",
394
- error: `Command failed: ${error instanceof Error ? error.message : String(error)}`
379
+ error: `Failed to view range: ${error instanceof Error ? error.message : String(error)}`
395
380
  };
396
381
  }
397
382
  }
@@ -418,8 +403,9 @@ function listDir(dirPath) {
418
403
  * Search for pattern in files using ripgrep if available, fallback to Node.js
419
404
  */
420
405
  async function grepSearch(pattern, dirPath) {
406
+ const { execShell } = await import("./assets/shell-CipXM_WI.js").then((n) => n.n);
421
407
  try {
422
- const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/"/g, "\\\"")}" "${dirPath}"`);
408
+ const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}" "${dirPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`);
423
409
  // v8 ignore next
424
410
  return { content: stdout || "No matches found" };
425
411
  } catch {}
@@ -455,27 +441,162 @@ async function grepSearch(pattern, dirPath) {
455
441
  };
456
442
  }
457
443
  }
444
+ //#endregion
445
+ //#region src/utils/tools/web/fetch.ts
446
+ var FETCH_TIMEOUT_MS = 1e4;
458
447
  /**
459
- * View specific line range from file
448
+ * Fetch text from URL with timeout and headers
460
449
  */
461
- function viewRange(filePath, start, end) {
450
+ async function fetchText(url, headers) {
451
+ const response = await fetch(url, {
452
+ headers: {
453
+ "user-agent": `${NAME}/${VERSION}`,
454
+ ...headers
455
+ },
456
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
457
+ });
458
+ if (!response.ok) throw new Error(`HTTP ${response.status.toString()}`);
459
+ return response.text();
460
+ }
461
+ //#endregion
462
+ //#region src/utils/tools/web/utils.ts
463
+ /**
464
+ * Strip HTML tags from a string
465
+ */
466
+ function stripTags(value) {
467
+ return value.replace(/<[^>]+>/g, " ");
468
+ }
469
+ /**
470
+ * Decode HTML entities
471
+ */
472
+ function decodeHtml(value) {
473
+ return value.replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&#x27;/g, "'").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&");
474
+ }
475
+ /**
476
+ * Clean whitespace in text
477
+ */
478
+ function cleanText(value) {
479
+ return value.replace(/\s+/g, " ").trim();
480
+ }
481
+ /**
482
+ * Truncate text to max length with ellipsis
483
+ */
484
+ function truncate(value, maxLength) {
485
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1).trimEnd()}…` : value;
486
+ }
487
+ //#endregion
488
+ //#region src/utils/tools/web/search.ts
489
+ var SEARCH_RESULT_LIMIT = 5;
490
+ async function webSearch(query) {
491
+ const trimmedQuery = query.trim();
492
+ if (!trimmedQuery) return {
493
+ content: "",
494
+ error: "Search query cannot be empty"
495
+ };
496
+ const { searxngBaseUrl } = loadConfig();
497
+ let searxngIssue = null;
498
+ if (searxngBaseUrl) try {
499
+ const searxngResults = await searchSearxng(searxngBaseUrl, trimmedQuery);
500
+ if (searxngResults.length) return { content: formatSearchResults("SearXNG", searxngResults) };
501
+ searxngIssue = "SearXNG returned no results";
502
+ } catch (error) {
503
+ searxngIssue = `SearXNG failed: ${error instanceof Error ? error.message : String(error)}`;
504
+ }
462
505
  try {
463
- if (!existsSync(filePath)) return {
464
- content: "",
465
- error: `File not found: ${filePath}`
466
- };
467
- const lines = readFileSync(filePath, "utf8").split("\n");
468
- const startIdx = Math.max(0, start - 1);
469
- const endIdx = Math.min(lines.length, end);
470
- if (startIdx >= lines.length || startIdx > endIdx) return {
471
- content: "",
472
- error: "Invalid line range"
473
- };
474
- return { content: lines.slice(startIdx, endIdx).join("\n") };
506
+ const duckDuckGoResults = await searchDuckDuckGo(trimmedQuery);
507
+ if (duckDuckGoResults.length) return { content: formatSearchResults("DuckDuckGo", duckDuckGoResults, searxngIssue ? `${searxngIssue}. Using DuckDuckGo fallback.` : void 0) };
508
+ if (searxngIssue) return { content: `No web results found. ${searxngIssue}. DuckDuckGo also returned no results.` };
509
+ return { content: "No web results found." };
475
510
  } catch (error) {
511
+ const duckDuckGoIssue = `DuckDuckGo failed: ${error instanceof Error ? error.message : String(error)}`;
476
512
  return {
477
513
  content: "",
478
- error: `Failed to view range: ${error instanceof Error ? error.message : String(error)}`
514
+ error: searxngIssue ? `${searxngIssue}; ${duckDuckGoIssue}` : duckDuckGoIssue
515
+ };
516
+ }
517
+ }
518
+ async function searchSearxng(baseUrl, query) {
519
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
520
+ const url = new URL(`${normalizedBaseUrl}/search`);
521
+ url.searchParams.set("q", query);
522
+ url.searchParams.set("format", "json");
523
+ url.searchParams.set("language", "en-US");
524
+ const response = await fetchText(url.toString(), { Accept: "application/json" });
525
+ return normalizeResults(JSON.parse(response).results?.map((result) => ({
526
+ title: result.title ?? "",
527
+ url: result.url ?? "",
528
+ snippet: result.content ?? ""
529
+ })) ?? []);
530
+ }
531
+ async function searchDuckDuckGo(query) {
532
+ const url = new URL("https://html.duckduckgo.com/html/");
533
+ url.searchParams.set("q", query);
534
+ return parseDuckDuckGoResults(await fetchText(url.toString(), { Accept: "text/html" }));
535
+ }
536
+ function parseDuckDuckGoResults(html) {
537
+ const results = [];
538
+ for (const match of html.matchAll(/<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]*class="result__snippet"[^>]*>|<div[^>]*class="result__snippet"[^>]*>)([\s\S]*?)(?:<\/a>|<\/div>)/g)) {
539
+ const url = normalizeDuckDuckGoUrl(match[1]);
540
+ const title = decodeHtml(stripTags(match[2]));
541
+ const snippet = decodeHtml(stripTags(match[3]));
542
+ if (!url || !title) continue;
543
+ results.push({
544
+ title,
545
+ url,
546
+ snippet
547
+ });
548
+ if (results.length >= SEARCH_RESULT_LIMIT) break;
549
+ }
550
+ return normalizeResults(results);
551
+ }
552
+ function normalizeDuckDuckGoUrl(url) {
553
+ try {
554
+ const parsedUrl = new URL(url, "https://duckduckgo.com");
555
+ const redirectedUrl = parsedUrl.searchParams.get("uddg");
556
+ return redirectedUrl ? decodeURIComponent(redirectedUrl) : parsedUrl.toString();
557
+ } catch {
558
+ return url;
559
+ }
560
+ }
561
+ function normalizeResults(results) {
562
+ return results.map((result) => ({
563
+ title: cleanText(result.title),
564
+ url: result.url.trim(),
565
+ snippet: cleanText(result.snippet)
566
+ })).filter((result) => result.title && result.url).slice(0, SEARCH_RESULT_LIMIT);
567
+ }
568
+ function formatSearchResults(source, results, note) {
569
+ const lines = [`Source: ${source}`];
570
+ if (note) lines.push(`Note: ${note}`);
571
+ for (const [index, result] of results.entries()) {
572
+ lines.push(`${(index + 1).toString()}. ${result.title}`);
573
+ lines.push(` URL: ${result.url}`);
574
+ if (result.snippet) lines.push(` Snippet: ${truncate(result.snippet, 240)}`);
575
+ }
576
+ return lines.join("\n");
577
+ }
578
+ //#endregion
579
+ //#region src/utils/tools/dispatcher.ts
580
+ /**
581
+ * Execute a tool by name with arguments
582
+ */
583
+ async function executeTool(name, args, options) {
584
+ if (options?.allowedTools && !options.allowedTools.has(name)) return {
585
+ content: "",
586
+ error: `Tool not allowed: ${name}`
587
+ };
588
+ switch (name) {
589
+ case READ_FILE: return readFile(args.path);
590
+ case WRITE_FILE: return writeFile(args.path, args.content);
591
+ case EDIT_FILE: return editFile(args.path, args.oldText, args.newText);
592
+ case RUN_SHELL: return runShell(args.command);
593
+ case LIST_DIR: return listDir(args.path);
594
+ case GREP_SEARCH: return await grepSearch(args.pattern, args.path);
595
+ case VIEW_RANGE: return viewRange(args.path, args.start, args.end);
596
+ case WEB_SEARCH: return await webSearch(args.query);
597
+ default: return {
598
+ content: "",
599
+ error: `Unknown tool: ${name}`
479
600
  };
480
601
  }
481
602
  }
@@ -496,14 +617,14 @@ cli.command("run <model> <prompt>", "Run a one-off prompt").action(async (model,
496
617
  });
497
618
  async function runPrompt(model, prompt) {
498
619
  await processRunStream([createSystemMessage(), {
499
- role: ROLE.USER,
620
+ role: USER,
500
621
  content: prompt
501
622
  }], model);
502
623
  process.stdout.write("\n");
503
624
  }
504
625
  async function processRunStream(messages, model) {
505
626
  const assistantMessage = {
506
- role: ROLE.ASSISTANT,
627
+ role: ASSISTANT,
507
628
  content: ""
508
629
  };
509
630
  for await (const chunk of streamChat(messages, model, TOOLS)) {
@@ -515,7 +636,7 @@ async function processRunStream(messages, model) {
515
636
  for (const toolCall of chunk.tool_calls) {
516
637
  const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
517
638
  const toolResultMessage = {
518
- role: ROLE.SYSTEM,
639
+ role: SYSTEM,
519
640
  content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
520
641
  };
521
642
  await processRunStream([
@@ -529,7 +650,7 @@ async function processRunStream(messages, model) {
529
650
  }
530
651
  async function main(args = process.argv.slice(2)) {
531
652
  if (!args.length) {
532
- const { renderApp } = await import("./assets/tui-D2NgQSV7.js");
653
+ const { renderApp } = await import("./assets/tui-BSnwVbDN.js");
533
654
  reset();
534
655
  renderApp();
535
656
  return;
@@ -552,4 +673,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
552
673
  if (isEntrypoint()) main();
553
674
  // v8 ignore stop
554
675
  //#endregion
555
- export { VERSION as _, tick as a, setClearHandler as c, loadConfig as d, saveConfig as f, PLAN_GENERATION_INSTRUCTION as g, ROLE as h, executeTool as i, listModels as l, withSystemMessage as m, main, TOOLS as n, clear as o, resetSystemMessage as p, WRITE_TOOLS as r, reset as s, READ_TOOLS as t, streamChat as u };
676
+ export { USER as _, tick as a, setClearHandler as c, loadConfig as d, saveConfig as f, SYSTEM as g, ASSISTANT as h, WRITE_TOOLS as i, listModels as l, withSystemMessage as m, main, READ_TOOLS as n, clear as o, resetSystemMessage as p, TOOLS as r, reset as s, executeTool as t, streamChat as u, PLAN_GENERATION_INSTRUCTION as v, VERSION as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",
@@ -62,7 +62,7 @@
62
62
  "globals": "17.6.0",
63
63
  "husky": "9.1.7",
64
64
  "ink-testing-library": "4.0.0",
65
- "lint-staged": "17.0.3",
65
+ "lint-staged": "17.0.4",
66
66
  "prettier": "3.8.3",
67
67
  "publint": "0.3.20",
68
68
  "tsx": "4.21.0",