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 +1 -1
- package/dist/assets/shell-CipXM_WI.js +46 -0
- package/dist/assets/{tui-D2NgQSV7.js → tui-BSnwVbDN.js} +213 -66
- package/dist/cli.js +225 -104
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
[](https://github.com/ai-action/code-ollama/actions/workflows/build.yml)
|
|
14
14
|
[](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
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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 ===
|
|
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
|
|
149
|
-
case
|
|
150
|
-
case
|
|
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 ===
|
|
196
|
-
const isUser = message.role ===
|
|
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:
|
|
311
|
+
value: AUTO
|
|
310
312
|
},
|
|
311
313
|
{
|
|
312
314
|
label: "Safe - Approve each tool",
|
|
313
|
-
value:
|
|
315
|
+
value: SAFE
|
|
314
316
|
},
|
|
315
317
|
{
|
|
316
318
|
label: "Cancel - Continue planning",
|
|
317
|
-
value:
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 ===
|
|
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 ===
|
|
858
|
+
const allowedTools = executionMode === "plan" ? READ_TOOLS : void 0;
|
|
847
859
|
// v8 ignore stop
|
|
848
860
|
const updatedMessages = commitAssistantMessage();
|
|
849
|
-
if (executionMode ===
|
|
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:
|
|
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 ===
|
|
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:
|
|
949
|
+
role: SYSTEM,
|
|
938
950
|
content: PLAN_GENERATION_INSTRUCTION
|
|
939
951
|
};
|
|
940
952
|
const planMessages = [...researchMessages, planInstruction];
|
|
941
953
|
const planAssistantMessage = {
|
|
942
|
-
role:
|
|
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 (
|
|
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 (
|
|
993
|
-
onModeChange(
|
|
1004
|
+
if (mode === "plan") {
|
|
1005
|
+
onModeChange(PLAN);
|
|
994
1006
|
const cancelMessage = {
|
|
995
|
-
role:
|
|
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 =
|
|
1013
|
+
const selectedMode = mode === "auto" ? AUTO : SAFE;
|
|
1002
1014
|
onModeChange(selectedMode);
|
|
1003
1015
|
setIsLoading(true);
|
|
1004
1016
|
const executeInstruction = {
|
|
1005
|
-
role:
|
|
1006
|
-
content:
|
|
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:
|
|
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:
|
|
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:
|
|
1068
|
+
role: USER,
|
|
1057
1069
|
content: userContent
|
|
1058
1070
|
};
|
|
1059
1071
|
const updatedMessages = [...messages, userMessage];
|
|
1060
1072
|
setMessages(updatedMessages);
|
|
1061
|
-
if (mode ===
|
|
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:
|
|
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
|
|
1110
|
-
case
|
|
1111
|
-
case
|
|
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)
|
|
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 [
|
|
1247
|
-
const [
|
|
1248
|
-
const [mode, setMode] = useState(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1400
|
+
setAppConfig((currentConfig) => ({
|
|
1401
|
+
...currentConfig,
|
|
1402
|
+
model: selected
|
|
1403
|
+
}));
|
|
1272
1404
|
saveConfig({ model: selected });
|
|
1273
|
-
|
|
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
|
-
|
|
1416
|
+
setScreen(SCREEN.CHAT);
|
|
1277
1417
|
}, []);
|
|
1278
1418
|
const handleToggleMode = useCallback(() => {
|
|
1279
1419
|
setMode((mode) => {
|
|
1280
1420
|
switch (mode) {
|
|
1281
|
-
case
|
|
1282
|
-
case
|
|
1283
|
-
case
|
|
1284
|
-
default: return
|
|
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
|
|
1289
|
-
switch (
|
|
1290
|
-
case
|
|
1291
|
-
|
|
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
|
-
|
|
1298
|
-
|
|
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 &&
|
|
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
|
-
|
|
8
|
-
|
|
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
|
|
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
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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:
|
|
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
|
|
105
|
-
var CONFIG_PATH = join(
|
|
106
|
-
var
|
|
107
|
-
|
|
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 ??
|
|
122
|
-
model: process.env.OLLAMA_MODEL ?? file.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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
295
|
+
WRITE_FILE,
|
|
296
|
+
EDIT_FILE,
|
|
297
|
+
RUN_SHELL
|
|
293
298
|
]);
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
*
|
|
360
|
+
* View specific line range from file
|
|
386
361
|
*/
|
|
387
|
-
|
|
362
|
+
function viewRange(filePath, start, end) {
|
|
388
363
|
try {
|
|
389
|
-
|
|
390
|
-
|
|
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: `
|
|
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
|
-
*
|
|
448
|
+
* Fetch text from URL with timeout and headers
|
|
460
449
|
*/
|
|
461
|
-
function
|
|
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(/"/g, "\"").replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'").replace(/ /g, " ").replace(/&/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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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-
|
|
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 {
|
|
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.
|
|
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.
|
|
65
|
+
"lint-staged": "17.0.4",
|
|
66
66
|
"prettier": "3.8.3",
|
|
67
67
|
"publint": "0.3.20",
|
|
68
68
|
"tsx": "4.21.0",
|