code-ollama 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +64 -0
- package/dist/assets/tui-DSR1MJGd.js +438 -0
- package/dist/cli.js +448 -0
- package/package.json +78 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Menglin "Mark" Xu <mark@remarkablemark.org>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# code-ollama
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/code-ollama)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/code-ollama)
|
|
6
|
+
[](https://github.com/ai-action/code-ollama/actions/workflows/build.yml)
|
|
7
|
+
[](https://codecov.io/gh/ai-action/code-ollama)
|
|
8
|
+
|
|
9
|
+
🦙 [Ollama](https://ollama.com/) coding agent that runs in your terminal.
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx code-ollama
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Install the [CLI](https://www.npmjs.com/package/code-ollama) globally:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install --global code-ollama
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### TUI
|
|
28
|
+
|
|
29
|
+
Open the TUI:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
code-ollama
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or use the alias:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
collama
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### CLI
|
|
42
|
+
|
|
43
|
+
Show the version:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
code-ollama --version
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Show the help:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
code-ollama --help
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Run a one-off prompt:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
# code-ollama run <model> <prompt>
|
|
59
|
+
code-ollama run gemma4 "review diff"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
[MIT](https://github.com/ai-action/code-ollama/blob/master/LICENSE)
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { a as streamChat, c as createSystemMessage, i as listModels, l as ROLE, n as TOOLS_REQUIRING_APPROVAL, o as loadConfig, r as executeTool, s as saveConfig, t as TOOLS, u as VERSION } from "../cli.js";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { Box, Text, render, useInput } from "ink";
|
|
4
|
+
import { useCallback, useEffect, useState } from "react";
|
|
5
|
+
import { Select, Spinner, TextInput } from "@inkjs/ui";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/constants/commands.ts
|
|
8
|
+
var COMMANDS = [{
|
|
9
|
+
name: "/model",
|
|
10
|
+
description: "switch the model"
|
|
11
|
+
}];
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/constants/ui.ts
|
|
14
|
+
var HEADER_PREFIX = "🦙";
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/components/Autocomplete.tsx
|
|
17
|
+
function getMatches(input) {
|
|
18
|
+
if (!input.startsWith("/")) return [];
|
|
19
|
+
return COMMANDS.filter((command) => command.name.startsWith(input));
|
|
20
|
+
}
|
|
21
|
+
function Autocomplete({ isDisabled = false, onSubmit }) {
|
|
22
|
+
const [value, setValue] = useState("");
|
|
23
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
24
|
+
const [inputKey, setInputKey] = useState(0);
|
|
25
|
+
const matches = getMatches(value);
|
|
26
|
+
const isCommandMode = value.startsWith("/");
|
|
27
|
+
useInput((_char, key) => {
|
|
28
|
+
// v8 ignore next
|
|
29
|
+
if (!isCommandMode) return;
|
|
30
|
+
if (key.upArrow) {
|
|
31
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (key.downArrow) {
|
|
35
|
+
setSelectedIndex((i) => Math.min(matches.length - 1, i + 1));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (key.tab && matches.length > 0) {
|
|
39
|
+
setValue((matches[selectedIndex] ?? matches[0]).name);
|
|
40
|
+
setSelectedIndex(0);
|
|
41
|
+
setInputKey((key) => key + 1);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}, { isActive: !isDisabled && isCommandMode });
|
|
45
|
+
const handleSubmit = useCallback((input) => {
|
|
46
|
+
const trimmed = (isCommandMode && matches.length > 0 && matches[selectedIndex] ? matches[selectedIndex].name : input).trim();
|
|
47
|
+
if (trimmed) {
|
|
48
|
+
onSubmit(trimmed);
|
|
49
|
+
setValue("");
|
|
50
|
+
setSelectedIndex(0);
|
|
51
|
+
setInputKey((key) => key + 1);
|
|
52
|
+
}
|
|
53
|
+
}, [
|
|
54
|
+
isCommandMode,
|
|
55
|
+
matches,
|
|
56
|
+
onSubmit,
|
|
57
|
+
selectedIndex
|
|
58
|
+
]);
|
|
59
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
60
|
+
flexDirection: "column",
|
|
61
|
+
children: [/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
|
|
62
|
+
isDisabled,
|
|
63
|
+
defaultValue: value,
|
|
64
|
+
onChange: setValue,
|
|
65
|
+
onSubmit: handleSubmit
|
|
66
|
+
}, inputKey)] }), isCommandMode && matches.length > 0 && /* @__PURE__ */ jsx(Box, {
|
|
67
|
+
flexDirection: "column",
|
|
68
|
+
marginLeft: 2,
|
|
69
|
+
children: matches.map((command, index) => {
|
|
70
|
+
const isHighlighted = index === selectedIndex;
|
|
71
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
72
|
+
gap: 3,
|
|
73
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
74
|
+
color: isHighlighted ? "cyan" : void 0,
|
|
75
|
+
bold: isHighlighted,
|
|
76
|
+
children: command.name
|
|
77
|
+
}), /* @__PURE__ */ jsx(Text, {
|
|
78
|
+
dimColor: true,
|
|
79
|
+
children: command.description
|
|
80
|
+
})]
|
|
81
|
+
}, command.name);
|
|
82
|
+
})
|
|
83
|
+
})]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/components/Messages.tsx
|
|
88
|
+
function getMessageColor(role) {
|
|
89
|
+
switch (role) {
|
|
90
|
+
case ROLE.USER: return "black";
|
|
91
|
+
case ROLE.ASSISTANT: return "blue";
|
|
92
|
+
case ROLE.SYSTEM: return "gray";
|
|
93
|
+
default: return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function Messages({ messages, isLoading }) {
|
|
97
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
98
|
+
flexDirection: "column",
|
|
99
|
+
children: [messages.map((message, index) => /* @__PURE__ */ jsx(Box, {
|
|
100
|
+
marginBottom: 1,
|
|
101
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
102
|
+
color: getMessageColor(message.role),
|
|
103
|
+
dimColor: message.role === ROLE.SYSTEM,
|
|
104
|
+
children: [message.role === ROLE.USER ? "> " : "", message.content]
|
|
105
|
+
})
|
|
106
|
+
}, index)), isLoading && messages[messages.length - 1]?.content === "" && /* @__PURE__ */ jsx(Box, {
|
|
107
|
+
marginTop: -1,
|
|
108
|
+
marginBottom: 1,
|
|
109
|
+
children: /* @__PURE__ */ jsx(Spinner, { label: "Thinking..." })
|
|
110
|
+
})]
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/components/ToolApproval.tsx
|
|
115
|
+
function ToolApproval({ toolCall, onApprove, onReject }) {
|
|
116
|
+
const [selected, setSelected] = useState("yes");
|
|
117
|
+
useInput((_, key) => {
|
|
118
|
+
if (key.return) if (selected === "yes") onApprove();
|
|
119
|
+
else onReject();
|
|
120
|
+
else if (key.leftArrow || key.rightArrow) setSelected((prev) => prev === "yes" ? "no" : "yes");
|
|
121
|
+
// v8 ignore stop
|
|
122
|
+
});
|
|
123
|
+
const args = JSON.stringify(toolCall.function.arguments, null, 2);
|
|
124
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
125
|
+
flexDirection: "column",
|
|
126
|
+
marginY: 1,
|
|
127
|
+
children: [
|
|
128
|
+
/* @__PURE__ */ jsx(Text, {
|
|
129
|
+
color: "yellow",
|
|
130
|
+
bold: true,
|
|
131
|
+
children: "⚠️ Tool requires approval:"
|
|
132
|
+
}),
|
|
133
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
134
|
+
marginX: 2,
|
|
135
|
+
flexDirection: "column",
|
|
136
|
+
children: [/* @__PURE__ */ jsxs(Text, { children: [
|
|
137
|
+
/* @__PURE__ */ jsx(Text, {
|
|
138
|
+
bold: true,
|
|
139
|
+
children: "Tool:"
|
|
140
|
+
}),
|
|
141
|
+
" ",
|
|
142
|
+
toolCall.function.name
|
|
143
|
+
] }), /* @__PURE__ */ jsxs(Text, { children: [
|
|
144
|
+
/* @__PURE__ */ jsx(Text, {
|
|
145
|
+
bold: true,
|
|
146
|
+
children: "Arguments:"
|
|
147
|
+
}),
|
|
148
|
+
" ",
|
|
149
|
+
args
|
|
150
|
+
] })]
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ jsxs(Box, {
|
|
153
|
+
marginTop: 1,
|
|
154
|
+
gap: 2,
|
|
155
|
+
children: [/* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsxs(Text, {
|
|
156
|
+
color: selected === "yes" ? "green" : void 0,
|
|
157
|
+
children: [selected === "yes" ? "▶ " : " ", "✓ Yes (Enter)"]
|
|
158
|
+
}) }), /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsxs(Text, {
|
|
159
|
+
color: selected === "no" ? "red" : void 0,
|
|
160
|
+
children: [selected === "no" ? "▶ " : " ", "✗ No (Esc)"]
|
|
161
|
+
}) })]
|
|
162
|
+
})
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/components/Chat.tsx
|
|
168
|
+
function Chat({ model, onCommand, autoExecute }) {
|
|
169
|
+
const [messages, setMessages] = useState([createSystemMessage()]);
|
|
170
|
+
const [submitKey, setSubmitKey] = useState(0);
|
|
171
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
172
|
+
const [pendingToolCall, setPendingToolCall] = useState(null);
|
|
173
|
+
const processStream = useCallback(async (currentMessages) => {
|
|
174
|
+
const assistantMessage = {
|
|
175
|
+
role: ROLE.ASSISTANT,
|
|
176
|
+
content: ""
|
|
177
|
+
};
|
|
178
|
+
setMessages((previousMessages) => [...previousMessages, assistantMessage]);
|
|
179
|
+
try {
|
|
180
|
+
for await (const chunk of streamChat(currentMessages, model, TOOLS)) if (chunk.type === "content") {
|
|
181
|
+
assistantMessage.content += chunk.content;
|
|
182
|
+
setMessages((previousMessages) => {
|
|
183
|
+
const newMessages = [...previousMessages];
|
|
184
|
+
newMessages[newMessages.length - 1] = { ...assistantMessage };
|
|
185
|
+
return newMessages;
|
|
186
|
+
});
|
|
187
|
+
} else if (chunk.type === "tool_calls") for (const toolCall of chunk.tool_calls) {
|
|
188
|
+
const requiresApproval = TOOLS_REQUIRING_APPROVAL.has(toolCall.function.name);
|
|
189
|
+
if (!autoExecute && requiresApproval) {
|
|
190
|
+
setPendingToolCall(toolCall);
|
|
191
|
+
setIsLoading(false);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
|
|
195
|
+
const toolResultMessage = {
|
|
196
|
+
role: ROLE.SYSTEM,
|
|
197
|
+
content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
|
|
198
|
+
};
|
|
199
|
+
const newMessages = [
|
|
200
|
+
...currentMessages,
|
|
201
|
+
assistantMessage,
|
|
202
|
+
toolResultMessage
|
|
203
|
+
];
|
|
204
|
+
setMessages((previousMessages) => [...previousMessages, toolResultMessage]);
|
|
205
|
+
await processStream(newMessages);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
assistantMessage.content = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
210
|
+
setMessages((previousMessages) => {
|
|
211
|
+
const newMessages = [...previousMessages];
|
|
212
|
+
newMessages[newMessages.length - 1] = { ...assistantMessage };
|
|
213
|
+
return newMessages;
|
|
214
|
+
});
|
|
215
|
+
} finally {
|
|
216
|
+
setIsLoading(false);
|
|
217
|
+
}
|
|
218
|
+
}, [model, autoExecute]);
|
|
219
|
+
const handleToolApproval = useCallback(async (approved) => {
|
|
220
|
+
// v8 ignore next
|
|
221
|
+
if (!pendingToolCall) return;
|
|
222
|
+
const toolCall = pendingToolCall;
|
|
223
|
+
setPendingToolCall(null);
|
|
224
|
+
setIsLoading(true);
|
|
225
|
+
if (approved) {
|
|
226
|
+
const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
|
|
227
|
+
const toolResultMessage = {
|
|
228
|
+
role: ROLE.SYSTEM,
|
|
229
|
+
content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
|
|
230
|
+
};
|
|
231
|
+
const newMessages = [...messages, toolResultMessage];
|
|
232
|
+
setMessages((previousMessages) => [...previousMessages, toolResultMessage]);
|
|
233
|
+
await processStream(newMessages);
|
|
234
|
+
} else {
|
|
235
|
+
const rejectionMessage = {
|
|
236
|
+
role: ROLE.SYSTEM,
|
|
237
|
+
content: `User declined to execute tool ${toolCall.function.name}`
|
|
238
|
+
};
|
|
239
|
+
const newMessages = [...messages, rejectionMessage];
|
|
240
|
+
setMessages((previousMessages) => [...previousMessages, rejectionMessage]);
|
|
241
|
+
await processStream(newMessages);
|
|
242
|
+
}
|
|
243
|
+
}, [
|
|
244
|
+
pendingToolCall,
|
|
245
|
+
messages,
|
|
246
|
+
processStream
|
|
247
|
+
]);
|
|
248
|
+
const handleSubmit = useCallback(async (value) => {
|
|
249
|
+
const userContent = value.trim();
|
|
250
|
+
if (!userContent) return;
|
|
251
|
+
setSubmitKey((key) => key + 1);
|
|
252
|
+
if (userContent.startsWith("/")) {
|
|
253
|
+
onCommand(userContent);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
setIsLoading(true);
|
|
257
|
+
const userMessage = {
|
|
258
|
+
role: ROLE.USER,
|
|
259
|
+
content: userContent
|
|
260
|
+
};
|
|
261
|
+
setMessages((previousMessages) => [...previousMessages, userMessage]);
|
|
262
|
+
await processStream([...messages, userMessage]);
|
|
263
|
+
}, [
|
|
264
|
+
messages,
|
|
265
|
+
onCommand,
|
|
266
|
+
processStream
|
|
267
|
+
]);
|
|
268
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
269
|
+
flexDirection: "column",
|
|
270
|
+
children: [
|
|
271
|
+
/* @__PURE__ */ jsx(Messages, {
|
|
272
|
+
messages: messages.slice(1),
|
|
273
|
+
isLoading
|
|
274
|
+
}),
|
|
275
|
+
pendingToolCall && /* @__PURE__ */ jsx(ToolApproval, {
|
|
276
|
+
toolCall: pendingToolCall,
|
|
277
|
+
onApprove: () => void handleToolApproval(true),
|
|
278
|
+
onReject: () => void handleToolApproval(false)
|
|
279
|
+
}),
|
|
280
|
+
!pendingToolCall && /* @__PURE__ */ jsx(Autocomplete, {
|
|
281
|
+
isDisabled: isLoading,
|
|
282
|
+
onSubmit: (val) => {
|
|
283
|
+
handleSubmit(val);
|
|
284
|
+
}
|
|
285
|
+
}, submitKey)
|
|
286
|
+
]
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/components/Footer.tsx
|
|
291
|
+
function Footer({ autoExecute, onToggleMode }) {
|
|
292
|
+
useInput((_, key) => {
|
|
293
|
+
if (key.tab && key.shift) onToggleMode();
|
|
294
|
+
});
|
|
295
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
296
|
+
justifyContent: "space-between",
|
|
297
|
+
marginTop: 1,
|
|
298
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
299
|
+
dimColor: true,
|
|
300
|
+
children: [
|
|
301
|
+
"Mode: ",
|
|
302
|
+
autoExecute ? "Auto" : "Safe",
|
|
303
|
+
" (Shift+Tab to toggle)"
|
|
304
|
+
]
|
|
305
|
+
})
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/components/Header.tsx
|
|
310
|
+
function abbreviatePath(dir) {
|
|
311
|
+
const home = homedir();
|
|
312
|
+
return dir.startsWith(home) ? `~${dir.slice(home.length)}` : dir;
|
|
313
|
+
}
|
|
314
|
+
function Header({ model }) {
|
|
315
|
+
const directory = abbreviatePath(process.cwd());
|
|
316
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
317
|
+
borderStyle: "round",
|
|
318
|
+
flexDirection: "column",
|
|
319
|
+
paddingX: 1,
|
|
320
|
+
children: [
|
|
321
|
+
/* @__PURE__ */ jsxs(Text, { children: [/* @__PURE__ */ jsxs(Text, {
|
|
322
|
+
bold: true,
|
|
323
|
+
children: [HEADER_PREFIX, "Code Ollama"]
|
|
324
|
+
}), /* @__PURE__ */ jsxs(Text, {
|
|
325
|
+
dimColor: true,
|
|
326
|
+
children: [
|
|
327
|
+
" (v",
|
|
328
|
+
VERSION,
|
|
329
|
+
")"
|
|
330
|
+
]
|
|
331
|
+
})] }),
|
|
332
|
+
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
333
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
334
|
+
/* @__PURE__ */ jsx(Text, {
|
|
335
|
+
dimColor: true,
|
|
336
|
+
children: "model:".padEnd(11)
|
|
337
|
+
}),
|
|
338
|
+
/* @__PURE__ */ jsxs(Text, { children: [model, " "] }),
|
|
339
|
+
/* @__PURE__ */ jsx(Text, {
|
|
340
|
+
color: "cyan",
|
|
341
|
+
children: "/model"
|
|
342
|
+
}),
|
|
343
|
+
/* @__PURE__ */ jsx(Text, {
|
|
344
|
+
dimColor: true,
|
|
345
|
+
children: " to switch"
|
|
346
|
+
})
|
|
347
|
+
] }),
|
|
348
|
+
/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
|
|
349
|
+
dimColor: true,
|
|
350
|
+
children: "directory:".padEnd(11)
|
|
351
|
+
}), /* @__PURE__ */ jsx(Text, { children: directory })] })
|
|
352
|
+
]
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/components/ModelPicker.tsx
|
|
357
|
+
function ModelPicker({ currentModel, onSelect, onCancel }) {
|
|
358
|
+
const [options, setOptions] = useState([]);
|
|
359
|
+
const [error, setError] = useState(null);
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
async function load() {
|
|
362
|
+
try {
|
|
363
|
+
setOptions((await listModels()).map((name) => ({
|
|
364
|
+
label: name,
|
|
365
|
+
value: name
|
|
366
|
+
})));
|
|
367
|
+
} catch (err) {
|
|
368
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
load();
|
|
372
|
+
}, []);
|
|
373
|
+
useInput((_, key) => {
|
|
374
|
+
if (key.escape) onCancel();
|
|
375
|
+
});
|
|
376
|
+
if (error) return /* @__PURE__ */ jsxs(Text, {
|
|
377
|
+
color: "red",
|
|
378
|
+
children: ["Error loading models: ", error]
|
|
379
|
+
});
|
|
380
|
+
if (!options.length) return /* @__PURE__ */ jsx(Spinner, { label: "Loading models..." });
|
|
381
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
382
|
+
flexDirection: "column",
|
|
383
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
384
|
+
dimColor: true,
|
|
385
|
+
children: "Select a model (↑↓ + Enter to confirm, Esc to cancel)"
|
|
386
|
+
}), /* @__PURE__ */ jsx(Select, {
|
|
387
|
+
options,
|
|
388
|
+
defaultValue: currentModel,
|
|
389
|
+
onChange: onSelect
|
|
390
|
+
})]
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/components/App.tsx
|
|
395
|
+
function App() {
|
|
396
|
+
const [model, setModel] = useState(() => loadConfig().model);
|
|
397
|
+
const [picking, setPicking] = useState(false);
|
|
398
|
+
const [autoExecute, setAutoExecute] = useState(false);
|
|
399
|
+
const handleCommand = useCallback((command) => {
|
|
400
|
+
if (command === "/model") setPicking(true);
|
|
401
|
+
}, []);
|
|
402
|
+
const handleSelect = useCallback((selected) => {
|
|
403
|
+
setModel(selected);
|
|
404
|
+
saveConfig({ model: selected });
|
|
405
|
+
setPicking(false);
|
|
406
|
+
}, []);
|
|
407
|
+
const handleCancel = useCallback(() => {
|
|
408
|
+
setPicking(false);
|
|
409
|
+
}, []);
|
|
410
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
411
|
+
flexDirection: "column",
|
|
412
|
+
children: [
|
|
413
|
+
/* @__PURE__ */ jsx(Header, { model }),
|
|
414
|
+
picking ? /* @__PURE__ */ jsx(ModelPicker, {
|
|
415
|
+
currentModel: model,
|
|
416
|
+
onSelect: handleSelect,
|
|
417
|
+
onCancel: handleCancel
|
|
418
|
+
}) : /* @__PURE__ */ jsx(Chat, {
|
|
419
|
+
model,
|
|
420
|
+
onCommand: handleCommand,
|
|
421
|
+
autoExecute
|
|
422
|
+
}),
|
|
423
|
+
/* @__PURE__ */ jsx(Footer, {
|
|
424
|
+
autoExecute,
|
|
425
|
+
onToggleMode: () => {
|
|
426
|
+
setAutoExecute((isAutoExecute) => !isAutoExecute);
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
]
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/tui.tsx
|
|
434
|
+
function renderApp() {
|
|
435
|
+
render(/* @__PURE__ */ jsx(App, {}));
|
|
436
|
+
}
|
|
437
|
+
//#endregion
|
|
438
|
+
export { renderApp };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import cac from "cac";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { Ollama } from "ollama";
|
|
7
|
+
import { exec } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/constants/package.ts
|
|
11
|
+
var VERSION = "0.1.1";
|
|
12
|
+
//#endregion
|
|
13
|
+
//#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
|
|
15
|
+
|
|
16
|
+
Follow these rules:
|
|
17
|
+
1. Always use available tools rather than guessing file contents or code behavior
|
|
18
|
+
2. Read files before editing them to understand context
|
|
19
|
+
3. When writing files, provide complete, working code
|
|
20
|
+
4. Explain your reasoning when making non-trivial changes
|
|
21
|
+
5. Prefer minimal changes that achieve the goal
|
|
22
|
+
6. Confirm with the user before destructive operations
|
|
23
|
+
|
|
24
|
+
When tools return results, incorporate them into your response naturally`;
|
|
25
|
+
var TOOL_INSTRUCTIONS = `Available tools:
|
|
26
|
+
- read_file: Read file contents at a path
|
|
27
|
+
- write_file: Write content to a file (requires approval)
|
|
28
|
+
- edit_file: Make precise edits to a file
|
|
29
|
+
- list_dir: List files in a directory
|
|
30
|
+
- grep_search: Search code with regex
|
|
31
|
+
- run_shell: Execute shell commands (requires approval)
|
|
32
|
+
|
|
33
|
+
Always use tools when you need to:
|
|
34
|
+
- Check file contents before referencing them
|
|
35
|
+
- Make file changes
|
|
36
|
+
- Explore project structure
|
|
37
|
+
- Search the codebase`;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/constants/role.ts
|
|
40
|
+
var ROLE = {
|
|
41
|
+
USER: "user",
|
|
42
|
+
ASSISTANT: "assistant",
|
|
43
|
+
SYSTEM: "system"
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/constants/tool.ts
|
|
47
|
+
var NAME = {
|
|
48
|
+
READ_FILE: "read_file",
|
|
49
|
+
WRITE_FILE: "write_file",
|
|
50
|
+
RUN_SHELL: "run_shell",
|
|
51
|
+
LIST_DIR: "list_dir",
|
|
52
|
+
GREP_SEARCH: "grep_search",
|
|
53
|
+
VIEW_RANGE: "view_range"
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/utils/agents.ts
|
|
57
|
+
var AGENTS_FILE = "AGENTS.md";
|
|
58
|
+
function loadAgentsContent() {
|
|
59
|
+
const agentsPath = join(process.cwd(), AGENTS_FILE);
|
|
60
|
+
if (!existsSync(agentsPath)) return null;
|
|
61
|
+
try {
|
|
62
|
+
return readFileSync(agentsPath, "utf8");
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function buildSystemPrompt() {
|
|
68
|
+
const parts = [BASE_SYSTEM_PROMPT];
|
|
69
|
+
const agentsContent = loadAgentsContent();
|
|
70
|
+
if (agentsContent) parts.push("\n\nProject context from AGENTS.md:\n", agentsContent);
|
|
71
|
+
parts.push("\n\n", TOOL_INSTRUCTIONS);
|
|
72
|
+
return parts.join("");
|
|
73
|
+
}
|
|
74
|
+
function createSystemMessage() {
|
|
75
|
+
return {
|
|
76
|
+
role: ROLE.SYSTEM,
|
|
77
|
+
content: buildSystemPrompt()
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/utils/config.ts
|
|
82
|
+
var CONFIG_DIR = join(homedir(), ".code-ollama");
|
|
83
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
84
|
+
var DEFAULTS = {
|
|
85
|
+
host: "http://localhost:11434",
|
|
86
|
+
model: "gemma4"
|
|
87
|
+
};
|
|
88
|
+
function readFile$1() {
|
|
89
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
92
|
+
} catch {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function loadConfig() {
|
|
97
|
+
const file = readFile$1();
|
|
98
|
+
return {
|
|
99
|
+
host: process.env.OLLAMA_HOST ?? file.host ?? DEFAULTS.host,
|
|
100
|
+
model: process.env.OLLAMA_MODEL ?? file.model ?? DEFAULTS.model
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function saveConfig(patch) {
|
|
104
|
+
const updated = {
|
|
105
|
+
...readFile$1(),
|
|
106
|
+
...patch
|
|
107
|
+
};
|
|
108
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
109
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/utils/ollama.ts
|
|
113
|
+
var { host, model: DEFAULT_MODEL } = loadConfig();
|
|
114
|
+
var client = new Ollama({ host });
|
|
115
|
+
async function* streamChat(messages, model = DEFAULT_MODEL, tools) {
|
|
116
|
+
const response = await client.chat({
|
|
117
|
+
model,
|
|
118
|
+
messages,
|
|
119
|
+
stream: true,
|
|
120
|
+
tools
|
|
121
|
+
});
|
|
122
|
+
for await (const chunk of response) {
|
|
123
|
+
if (chunk.message.content) yield {
|
|
124
|
+
type: "content",
|
|
125
|
+
content: chunk.message.content
|
|
126
|
+
};
|
|
127
|
+
if (chunk.message.tool_calls) yield {
|
|
128
|
+
type: "tool_calls",
|
|
129
|
+
tool_calls: chunk.message.tool_calls
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function listModels() {
|
|
134
|
+
const { models } = await client.list();
|
|
135
|
+
return models.map(({ name }) => name);
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/utils/screen.ts
|
|
139
|
+
var CLEAR = "\x1Bc";
|
|
140
|
+
function clear() {
|
|
141
|
+
process.stdout.write(CLEAR);
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/utils/tools.ts
|
|
145
|
+
var execAsync = promisify(exec);
|
|
146
|
+
/**
|
|
147
|
+
* Helper to define tool parameters
|
|
148
|
+
*/
|
|
149
|
+
function defineTool(name, description, params, required) {
|
|
150
|
+
return {
|
|
151
|
+
type: "function",
|
|
152
|
+
function: {
|
|
153
|
+
name,
|
|
154
|
+
description,
|
|
155
|
+
parameters: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: params,
|
|
158
|
+
required
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Tool definitions for Ollama API
|
|
165
|
+
*/
|
|
166
|
+
var TOOLS = [
|
|
167
|
+
defineTool(NAME.READ_FILE, "Read the contents of a file at the specified path", { path: {
|
|
168
|
+
type: "string",
|
|
169
|
+
description: "The path to the file to read"
|
|
170
|
+
} }, ["path"]),
|
|
171
|
+
defineTool(NAME.WRITE_FILE, "Write content to a file at the specified path", {
|
|
172
|
+
path: {
|
|
173
|
+
type: "string",
|
|
174
|
+
description: "The path to the file to write"
|
|
175
|
+
},
|
|
176
|
+
content: {
|
|
177
|
+
type: "string",
|
|
178
|
+
description: "The content to write to the file"
|
|
179
|
+
}
|
|
180
|
+
}, ["path", "content"]),
|
|
181
|
+
defineTool(NAME.RUN_SHELL, "Execute a shell command", { command: {
|
|
182
|
+
type: "string",
|
|
183
|
+
description: "The shell command to execute"
|
|
184
|
+
} }, ["command"]),
|
|
185
|
+
defineTool(NAME.LIST_DIR, "List the contents of a directory", { path: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description: "The path to the directory to list"
|
|
188
|
+
} }, ["path"]),
|
|
189
|
+
defineTool(NAME.GREP_SEARCH, "Search for a pattern in files within a directory", {
|
|
190
|
+
pattern: {
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "The regex pattern to search for"
|
|
193
|
+
},
|
|
194
|
+
path: {
|
|
195
|
+
type: "string",
|
|
196
|
+
description: "The directory path to search in"
|
|
197
|
+
}
|
|
198
|
+
}, ["pattern", "path"]),
|
|
199
|
+
defineTool(NAME.VIEW_RANGE, "View a specific range of lines from a file", {
|
|
200
|
+
path: {
|
|
201
|
+
type: "string",
|
|
202
|
+
description: "The path to the file"
|
|
203
|
+
},
|
|
204
|
+
start: {
|
|
205
|
+
type: "number",
|
|
206
|
+
description: "The starting line number (1-indexed)"
|
|
207
|
+
},
|
|
208
|
+
end: {
|
|
209
|
+
type: "number",
|
|
210
|
+
description: "The ending line number (inclusive)"
|
|
211
|
+
}
|
|
212
|
+
}, [
|
|
213
|
+
"path",
|
|
214
|
+
"start",
|
|
215
|
+
"end"
|
|
216
|
+
])
|
|
217
|
+
];
|
|
218
|
+
var TOOLS_REQUIRING_APPROVAL = new Set([NAME.WRITE_FILE, NAME.RUN_SHELL]);
|
|
219
|
+
/**
|
|
220
|
+
* Execute a tool by name with arguments
|
|
221
|
+
*/
|
|
222
|
+
async function executeTool(name, args) {
|
|
223
|
+
switch (name) {
|
|
224
|
+
case NAME.READ_FILE: return readFile(args.path);
|
|
225
|
+
case NAME.WRITE_FILE: return writeFile(args.path, args.content);
|
|
226
|
+
case NAME.RUN_SHELL: return runShell(args.command);
|
|
227
|
+
case NAME.LIST_DIR: return listDir(args.path);
|
|
228
|
+
case NAME.GREP_SEARCH: return await grepSearch(args.pattern, args.path);
|
|
229
|
+
case NAME.VIEW_RANGE: return viewRange(args.path, args.start, args.end);
|
|
230
|
+
default: return {
|
|
231
|
+
content: "",
|
|
232
|
+
error: `Unknown tool: ${name}`
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Read file contents
|
|
238
|
+
*/
|
|
239
|
+
function readFile(filePath) {
|
|
240
|
+
try {
|
|
241
|
+
if (!existsSync(filePath)) return {
|
|
242
|
+
content: "",
|
|
243
|
+
error: `File not found: ${filePath}`
|
|
244
|
+
};
|
|
245
|
+
return { content: readFileSync(filePath, "utf8") };
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return {
|
|
248
|
+
content: "",
|
|
249
|
+
error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Write content to file
|
|
255
|
+
*/
|
|
256
|
+
function writeFile(filePath, content) {
|
|
257
|
+
try {
|
|
258
|
+
writeFileSync(filePath, content, "utf8");
|
|
259
|
+
return { content: `File written successfully: ${filePath}` };
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return {
|
|
262
|
+
content: "",
|
|
263
|
+
error: `Failed to write file: ${error instanceof Error ? error.message : String(error)}`
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
var SHELL_EXEC_OPTIONS = {
|
|
268
|
+
timeout: 3e4,
|
|
269
|
+
maxBuffer: 1024 * 1024
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* Execute shell command with shared options (throws on error)
|
|
273
|
+
*/
|
|
274
|
+
function execShell(command) {
|
|
275
|
+
return execAsync(command, SHELL_EXEC_OPTIONS);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Execute shell command
|
|
279
|
+
*/
|
|
280
|
+
async function runShell(command) {
|
|
281
|
+
try {
|
|
282
|
+
const { stdout, stderr } = await execShell(command);
|
|
283
|
+
return { content: stdout || stderr };
|
|
284
|
+
} catch (error) {
|
|
285
|
+
return {
|
|
286
|
+
content: "",
|
|
287
|
+
error: `Command failed: ${error instanceof Error ? error.message : String(error)}`
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* List directory contents
|
|
293
|
+
*/
|
|
294
|
+
function listDir(dirPath) {
|
|
295
|
+
try {
|
|
296
|
+
if (!existsSync(dirPath)) return {
|
|
297
|
+
content: "",
|
|
298
|
+
error: `Directory not found: ${dirPath}`
|
|
299
|
+
};
|
|
300
|
+
return { content: readdirSync(dirPath, { withFileTypes: true }).map((entry) => {
|
|
301
|
+
return `[${entry.isDirectory() ? "d" : "f"}] ${entry.name}`;
|
|
302
|
+
}).join("\n") };
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return {
|
|
305
|
+
content: "",
|
|
306
|
+
error: `Failed to list directory: ${error instanceof Error ? error.message : String(error)}`
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Search for pattern in files using ripgrep if available, fallback to Node.js
|
|
312
|
+
*/
|
|
313
|
+
async function grepSearch(pattern, dirPath) {
|
|
314
|
+
try {
|
|
315
|
+
const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/"/g, "\\\"")}" "${dirPath}"`);
|
|
316
|
+
// v8 ignore next
|
|
317
|
+
return { content: stdout || "No matches found" };
|
|
318
|
+
} catch {}
|
|
319
|
+
try {
|
|
320
|
+
if (!existsSync(dirPath)) return {
|
|
321
|
+
content: "",
|
|
322
|
+
error: `Directory not found: ${dirPath}`
|
|
323
|
+
};
|
|
324
|
+
const regex = new RegExp(pattern, "g");
|
|
325
|
+
const results = [];
|
|
326
|
+
function searchDirectory(currentPath) {
|
|
327
|
+
const entries = readdirSync(currentPath, { withFileTypes: true });
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
const fullPath = join(currentPath, entry.name);
|
|
330
|
+
if (entry.isDirectory()) {
|
|
331
|
+
if (!entry.name.startsWith(".") && entry.name !== "node_modules") searchDirectory(fullPath);
|
|
332
|
+
} else if (entry.isFile()) try {
|
|
333
|
+
const lines = readFileSync(fullPath, "utf8").split("\n");
|
|
334
|
+
for (let i = 0; i < lines.length; i++) {
|
|
335
|
+
if (regex.test(lines[i])) results.push(`${fullPath}:${(i + 1).toString()}: ${lines[i].trim()}`);
|
|
336
|
+
regex.lastIndex = 0;
|
|
337
|
+
}
|
|
338
|
+
} catch {}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
searchDirectory(dirPath);
|
|
342
|
+
if (results.length === 0) return { content: "No matches found" };
|
|
343
|
+
return { content: results.join("\n") };
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return {
|
|
346
|
+
content: "",
|
|
347
|
+
error: `Search failed: ${error instanceof Error ? error.message : String(error)}`
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* View specific line range from file
|
|
353
|
+
*/
|
|
354
|
+
function viewRange(filePath, start, end) {
|
|
355
|
+
try {
|
|
356
|
+
if (!existsSync(filePath)) return {
|
|
357
|
+
content: "",
|
|
358
|
+
error: `File not found: ${filePath}`
|
|
359
|
+
};
|
|
360
|
+
const lines = readFileSync(filePath, "utf8").split("\n");
|
|
361
|
+
const startIdx = Math.max(0, start - 1);
|
|
362
|
+
const endIdx = Math.min(lines.length, end);
|
|
363
|
+
if (startIdx >= lines.length || startIdx > endIdx) return {
|
|
364
|
+
content: "",
|
|
365
|
+
error: "Invalid line range"
|
|
366
|
+
};
|
|
367
|
+
return { content: lines.slice(startIdx, endIdx).join("\n") };
|
|
368
|
+
} catch (error) {
|
|
369
|
+
return {
|
|
370
|
+
content: "",
|
|
371
|
+
error: `Failed to view range: ${error instanceof Error ? error.message : String(error)}`
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/cli.ts
|
|
377
|
+
var cli = cac("code-ollama");
|
|
378
|
+
cli.version(VERSION);
|
|
379
|
+
cli.help();
|
|
380
|
+
cli.command("run <model> <prompt>", "Run a one-off prompt").action(async (model, prompt) => {
|
|
381
|
+
try {
|
|
382
|
+
await runPrompt(model, prompt);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
// v8 ignore next
|
|
385
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
386
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
387
|
+
process.exitCode = 1;
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
async function runPrompt(model, prompt) {
|
|
391
|
+
await processRunStream([createSystemMessage(), {
|
|
392
|
+
role: ROLE.USER,
|
|
393
|
+
content: prompt
|
|
394
|
+
}], model);
|
|
395
|
+
process.stdout.write("\n");
|
|
396
|
+
}
|
|
397
|
+
async function processRunStream(messages, model) {
|
|
398
|
+
const assistantMessage = {
|
|
399
|
+
role: ROLE.ASSISTANT,
|
|
400
|
+
content: ""
|
|
401
|
+
};
|
|
402
|
+
for await (const chunk of streamChat(messages, model, TOOLS)) {
|
|
403
|
+
if (chunk.type === "content") {
|
|
404
|
+
assistantMessage.content += chunk.content;
|
|
405
|
+
process.stdout.write(chunk.content);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
for (const toolCall of chunk.tool_calls) {
|
|
409
|
+
const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
|
|
410
|
+
const toolResultMessage = {
|
|
411
|
+
role: ROLE.SYSTEM,
|
|
412
|
+
content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
|
|
413
|
+
};
|
|
414
|
+
await processRunStream([
|
|
415
|
+
...messages,
|
|
416
|
+
assistantMessage,
|
|
417
|
+
toolResultMessage
|
|
418
|
+
], model);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async function main(args = process.argv.slice(2)) {
|
|
424
|
+
if (!args.length) {
|
|
425
|
+
const { renderApp } = await import("./assets/tui-DSR1MJGd.js");
|
|
426
|
+
clear();
|
|
427
|
+
renderApp();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
cli.parse([
|
|
431
|
+
"node",
|
|
432
|
+
"code-ollama",
|
|
433
|
+
...args
|
|
434
|
+
]);
|
|
435
|
+
}
|
|
436
|
+
/* v8 ignore start */
|
|
437
|
+
function isEntrypoint(argv1 = process.argv[1]) {
|
|
438
|
+
if (!argv1) return false;
|
|
439
|
+
try {
|
|
440
|
+
return realpathSync(argv1) === import.meta.filename;
|
|
441
|
+
} catch {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (isEntrypoint()) main();
|
|
446
|
+
/* v8 ignore stop */
|
|
447
|
+
//#endregion
|
|
448
|
+
export { streamChat as a, createSystemMessage as c, listModels as i, ROLE as l, main, TOOLS_REQUIRING_APPROVAL as n, loadConfig as o, executeTool as r, saveConfig as s, TOOLS as t, VERSION as u };
|
package/package.json
CHANGED
|
@@ -1,4 +1,81 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-ollama",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Ollama coding agent that runs in your terminal",
|
|
5
|
+
"author": "Mark <mark@remarkablemark.org> (https://remarkablemark.org)",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"code-ollama": "./dist/cli.js",
|
|
9
|
+
"collama": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "vite build",
|
|
13
|
+
"start": "tsx --tsconfig tsconfig.test.json src/cli.ts",
|
|
14
|
+
"clean": "rm -rf coverage dist docs",
|
|
15
|
+
"lint": "eslint .",
|
|
16
|
+
"lint:fix": "npm run lint -- --fix",
|
|
17
|
+
"lint:package": "publint",
|
|
18
|
+
"lint:tsc": "tsc --build",
|
|
19
|
+
"prepare": "husky",
|
|
20
|
+
"prepublishOnly": "npm run build && npm run lint && npm run lint:tsc && npm run test:ci",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:ci": "CI=true npm test -- --color --coverage",
|
|
23
|
+
"test:watch": "vitest --coverage.enabled=false"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/ai-action/code-ollama.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/ai-action/code-ollama/issues"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"code-ollama",
|
|
34
|
+
"collama",
|
|
35
|
+
"code",
|
|
36
|
+
"ollama",
|
|
37
|
+
"tui",
|
|
38
|
+
"cli",
|
|
39
|
+
"ai"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@inkjs/ui": "2.0.0",
|
|
43
|
+
"cac": "7.0.0",
|
|
44
|
+
"ink": "7.0.1",
|
|
45
|
+
"ollama": "0.6.3",
|
|
46
|
+
"react": "19.2.5"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@commitlint/cli": "20.5.3",
|
|
50
|
+
"@commitlint/config-conventional": "20.5.3",
|
|
51
|
+
"@eslint/compat": "2.0.5",
|
|
52
|
+
"@eslint/js": "10.0.1",
|
|
53
|
+
"@types/node": "25.6.0",
|
|
54
|
+
"@types/react": "19.2.14",
|
|
55
|
+
"@vitest/coverage-v8": "4.1.5",
|
|
56
|
+
"eslint": "10.3.0",
|
|
57
|
+
"eslint-plugin-prettier": "5.5.5",
|
|
58
|
+
"eslint-plugin-simple-import-sort": "13.0.0",
|
|
59
|
+
"globals": "17.6.0",
|
|
60
|
+
"husky": "9.1.7",
|
|
61
|
+
"ink-testing-library": "4.0.0",
|
|
62
|
+
"lint-staged": "16.4.0",
|
|
63
|
+
"prettier": "3.8.3",
|
|
64
|
+
"publint": "0.3.18",
|
|
65
|
+
"tsx": "4.21.0",
|
|
66
|
+
"typescript": "6.0.3",
|
|
67
|
+
"typescript-eslint": "8.59.2",
|
|
68
|
+
"vite": "8.0.10",
|
|
69
|
+
"vitest": "4.1.5"
|
|
70
|
+
},
|
|
71
|
+
"files": [
|
|
72
|
+
"dist/"
|
|
73
|
+
],
|
|
74
|
+
"funding": [
|
|
75
|
+
{
|
|
76
|
+
"type": "github",
|
|
77
|
+
"url": "https://github.com/sponsors/remarkablemark"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"license": "MIT"
|
|
4
81
|
}
|