code-ollama 0.11.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-Bc6tEJF4.js → tui-BSnwVbDN.js} +138 -3
- package/dist/cli.js +198 -73
- 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 USER, a as tick, c as setClearHandler, d as loadConfig, f as saveConfig, g as SYSTEM, h as ASSISTANT, i 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"
|
|
@@ -1247,19 +1251,129 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
|
|
|
1247
1251
|
});
|
|
1248
1252
|
}
|
|
1249
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
|
|
1250
1362
|
//#region src/components/App.tsx
|
|
1251
1363
|
var SCREEN = /* @__PURE__ */ function(SCREEN) {
|
|
1252
1364
|
SCREEN["CHAT"] = "chat";
|
|
1253
1365
|
SCREEN["MODEL_PICKER"] = "model-picker";
|
|
1366
|
+
SCREEN["SEARCH_SETTINGS"] = "search-settings";
|
|
1254
1367
|
return SCREEN;
|
|
1255
1368
|
}(SCREEN || {});
|
|
1256
1369
|
function App() {
|
|
1257
1370
|
const { exit } = useApp();
|
|
1258
|
-
const [
|
|
1371
|
+
const [appConfig, setAppConfig] = useState(() => loadConfig());
|
|
1259
1372
|
const [currentScreen, setScreen] = useState(SCREEN.CHAT);
|
|
1260
1373
|
const [mode, setMode] = useState(SAFE);
|
|
1261
1374
|
const [sessionId, setSessionId] = useState(0);
|
|
1262
1375
|
const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
|
|
1376
|
+
const { model, searxngBaseUrl } = appConfig;
|
|
1263
1377
|
const handleHeaderLoad = useCallback(() => {
|
|
1264
1378
|
setIsHeaderLoaded(true);
|
|
1265
1379
|
}, []);
|
|
@@ -1268,6 +1382,9 @@ function App() {
|
|
|
1268
1382
|
case "/model":
|
|
1269
1383
|
setScreen(SCREEN.MODEL_PICKER);
|
|
1270
1384
|
break;
|
|
1385
|
+
case "/search":
|
|
1386
|
+
setScreen(SCREEN.SEARCH_SETTINGS);
|
|
1387
|
+
break;
|
|
1271
1388
|
case "/clear":
|
|
1272
1389
|
resetSystemMessage();
|
|
1273
1390
|
clear();
|
|
@@ -1280,10 +1397,21 @@ function App() {
|
|
|
1280
1397
|
}
|
|
1281
1398
|
}, [exit]);
|
|
1282
1399
|
const handleSelect = useCallback((selected) => {
|
|
1283
|
-
|
|
1400
|
+
setAppConfig((currentConfig) => ({
|
|
1401
|
+
...currentConfig,
|
|
1402
|
+
model: selected
|
|
1403
|
+
}));
|
|
1284
1404
|
saveConfig({ model: selected });
|
|
1285
1405
|
setScreen(SCREEN.CHAT);
|
|
1286
1406
|
}, []);
|
|
1407
|
+
const handleSaveSearch = useCallback((url) => {
|
|
1408
|
+
setAppConfig((currentConfig) => ({
|
|
1409
|
+
...currentConfig,
|
|
1410
|
+
searxngBaseUrl: url
|
|
1411
|
+
}));
|
|
1412
|
+
saveConfig({ searxngBaseUrl: url });
|
|
1413
|
+
setScreen(SCREEN.CHAT);
|
|
1414
|
+
}, []);
|
|
1287
1415
|
const handleClose = useCallback(() => {
|
|
1288
1416
|
setScreen(SCREEN.CHAT);
|
|
1289
1417
|
}, []);
|
|
@@ -1306,6 +1434,13 @@ function App() {
|
|
|
1306
1434
|
onClose: handleClose
|
|
1307
1435
|
});
|
|
1308
1436
|
break;
|
|
1437
|
+
case SCREEN.SEARCH_SETTINGS:
|
|
1438
|
+
screenContent = /* @__PURE__ */ jsx(SearchSettings, {
|
|
1439
|
+
currentUrl: searxngBaseUrl,
|
|
1440
|
+
onSave: handleSaveSearch,
|
|
1441
|
+
onClose: handleClose
|
|
1442
|
+
});
|
|
1443
|
+
break;
|
|
1309
1444
|
case SCREEN.CHAT:
|
|
1310
1445
|
screenContent = /* @__PURE__ */ jsx(Chat, {
|
|
1311
1446
|
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
|
|
@@ -62,6 +67,7 @@ var RUN_SHELL = "run_shell";
|
|
|
62
67
|
var LIST_DIR = "list_dir";
|
|
63
68
|
var GREP_SEARCH = "grep_search";
|
|
64
69
|
var VIEW_RANGE = "view_range";
|
|
70
|
+
var WEB_SEARCH = "web_search";
|
|
65
71
|
//#endregion
|
|
66
72
|
//#region src/utils/agents.ts
|
|
67
73
|
var AGENTS_FILE = "AGENTS.md";
|
|
@@ -97,12 +103,10 @@ function withSystemMessage(messages) {
|
|
|
97
103
|
}
|
|
98
104
|
//#endregion
|
|
99
105
|
//#region src/utils/config.ts
|
|
100
|
-
var
|
|
101
|
-
var CONFIG_PATH = join(
|
|
102
|
-
var
|
|
103
|
-
|
|
104
|
-
model: "gemma4"
|
|
105
|
-
};
|
|
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";
|
|
106
110
|
function readFile$1() {
|
|
107
111
|
if (!existsSync(CONFIG_PATH)) return {};
|
|
108
112
|
try {
|
|
@@ -114,8 +118,9 @@ function readFile$1() {
|
|
|
114
118
|
function loadConfig() {
|
|
115
119
|
const file = readFile$1();
|
|
116
120
|
return {
|
|
117
|
-
host: process.env.OLLAMA_HOST ?? file.host ??
|
|
118
|
-
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
|
|
119
124
|
};
|
|
120
125
|
}
|
|
121
126
|
function saveConfig(patch) {
|
|
@@ -123,7 +128,7 @@ function saveConfig(patch) {
|
|
|
123
128
|
...readFile$1(),
|
|
124
129
|
...patch
|
|
125
130
|
};
|
|
126
|
-
mkdirSync(
|
|
131
|
+
mkdirSync(CONFIG_DIRECTORY, { recursive: true });
|
|
127
132
|
writeFileSync(CONFIG_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
128
133
|
}
|
|
129
134
|
//#endregion
|
|
@@ -184,8 +189,7 @@ function reset() {
|
|
|
184
189
|
//#region src/utils/time.ts
|
|
185
190
|
var tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
186
191
|
//#endregion
|
|
187
|
-
//#region src/utils/tools.ts
|
|
188
|
-
var execAsync = promisify(exec);
|
|
192
|
+
//#region src/utils/tools/definitions.ts
|
|
189
193
|
/**
|
|
190
194
|
* Helper to define tool parameters
|
|
191
195
|
*/
|
|
@@ -274,41 +278,26 @@ var TOOLS = [
|
|
|
274
278
|
"path",
|
|
275
279
|
"start",
|
|
276
280
|
"end"
|
|
277
|
-
])
|
|
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"])
|
|
278
286
|
];
|
|
279
287
|
var READ_TOOLS = new Set([
|
|
280
288
|
READ_FILE,
|
|
281
289
|
LIST_DIR,
|
|
282
290
|
GREP_SEARCH,
|
|
283
|
-
VIEW_RANGE
|
|
291
|
+
VIEW_RANGE,
|
|
292
|
+
WEB_SEARCH
|
|
284
293
|
]);
|
|
285
294
|
var WRITE_TOOLS = new Set([
|
|
286
295
|
WRITE_FILE,
|
|
287
296
|
EDIT_FILE,
|
|
288
297
|
RUN_SHELL
|
|
289
298
|
]);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
*/
|
|
293
|
-
async function executeTool(name, args, options) {
|
|
294
|
-
if (options?.allowedTools && !options.allowedTools.has(name)) return {
|
|
295
|
-
content: "",
|
|
296
|
-
error: `Tool not allowed: ${name}`
|
|
297
|
-
};
|
|
298
|
-
switch (name) {
|
|
299
|
-
case READ_FILE: return readFile(args.path);
|
|
300
|
-
case WRITE_FILE: return writeFile(args.path, args.content);
|
|
301
|
-
case EDIT_FILE: return editFile(args.path, args.oldText, args.newText);
|
|
302
|
-
case RUN_SHELL: return runShell(args.command);
|
|
303
|
-
case LIST_DIR: return listDir(args.path);
|
|
304
|
-
case GREP_SEARCH: return await grepSearch(args.pattern, args.path);
|
|
305
|
-
case VIEW_RANGE: return viewRange(args.path, args.start, args.end);
|
|
306
|
-
default: return {
|
|
307
|
-
content: "",
|
|
308
|
-
error: `Unknown tool: ${name}`
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
}
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/utils/tools/filesystem.ts
|
|
312
301
|
/**
|
|
313
302
|
* Read file contents
|
|
314
303
|
*/
|
|
@@ -367,27 +356,27 @@ function editFile(filePath, oldText, newText) {
|
|
|
367
356
|
};
|
|
368
357
|
}
|
|
369
358
|
}
|
|
370
|
-
var SHELL_EXEC_OPTIONS = {
|
|
371
|
-
timeout: 3e4,
|
|
372
|
-
maxBuffer: 1024 * 1024
|
|
373
|
-
};
|
|
374
359
|
/**
|
|
375
|
-
*
|
|
376
|
-
*/
|
|
377
|
-
function execShell(command) {
|
|
378
|
-
return execAsync(command, SHELL_EXEC_OPTIONS);
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Execute shell command
|
|
360
|
+
* View specific line range from file
|
|
382
361
|
*/
|
|
383
|
-
|
|
362
|
+
function viewRange(filePath, start, end) {
|
|
384
363
|
try {
|
|
385
|
-
|
|
386
|
-
|
|
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") };
|
|
387
376
|
} catch (error) {
|
|
388
377
|
return {
|
|
389
378
|
content: "",
|
|
390
|
-
error: `
|
|
379
|
+
error: `Failed to view range: ${error instanceof Error ? error.message : String(error)}`
|
|
391
380
|
};
|
|
392
381
|
}
|
|
393
382
|
}
|
|
@@ -414,8 +403,9 @@ function listDir(dirPath) {
|
|
|
414
403
|
* Search for pattern in files using ripgrep if available, fallback to Node.js
|
|
415
404
|
*/
|
|
416
405
|
async function grepSearch(pattern, dirPath) {
|
|
406
|
+
const { execShell } = await import("./assets/shell-CipXM_WI.js").then((n) => n.n);
|
|
417
407
|
try {
|
|
418
|
-
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, "\\\"")}"`);
|
|
419
409
|
// v8 ignore next
|
|
420
410
|
return { content: stdout || "No matches found" };
|
|
421
411
|
} catch {}
|
|
@@ -451,27 +441,162 @@ async function grepSearch(pattern, dirPath) {
|
|
|
451
441
|
};
|
|
452
442
|
}
|
|
453
443
|
}
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/utils/tools/web/fetch.ts
|
|
446
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
454
447
|
/**
|
|
455
|
-
*
|
|
448
|
+
* Fetch text from URL with timeout and headers
|
|
456
449
|
*/
|
|
457
|
-
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
|
+
}
|
|
458
505
|
try {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
};
|
|
463
|
-
const lines = readFileSync(filePath, "utf8").split("\n");
|
|
464
|
-
const startIdx = Math.max(0, start - 1);
|
|
465
|
-
const endIdx = Math.min(lines.length, end);
|
|
466
|
-
if (startIdx >= lines.length || startIdx > endIdx) return {
|
|
467
|
-
content: "",
|
|
468
|
-
error: "Invalid line range"
|
|
469
|
-
};
|
|
470
|
-
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." };
|
|
471
510
|
} catch (error) {
|
|
511
|
+
const duckDuckGoIssue = `DuckDuckGo failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
472
512
|
return {
|
|
473
513
|
content: "",
|
|
474
|
-
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}`
|
|
475
600
|
};
|
|
476
601
|
}
|
|
477
602
|
}
|
|
@@ -525,7 +650,7 @@ async function processRunStream(messages, model) {
|
|
|
525
650
|
}
|
|
526
651
|
async function main(args = process.argv.slice(2)) {
|
|
527
652
|
if (!args.length) {
|
|
528
|
-
const { renderApp } = await import("./assets/tui-
|
|
653
|
+
const { renderApp } = await import("./assets/tui-BSnwVbDN.js");
|
|
529
654
|
reset();
|
|
530
655
|
renderApp();
|
|
531
656
|
return;
|
|
@@ -548,4 +673,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
|
|
|
548
673
|
if (isEntrypoint()) main();
|
|
549
674
|
// v8 ignore stop
|
|
550
675
|
//#endregion
|
|
551
|
-
export { USER as _, tick as a, setClearHandler as c, loadConfig as d, saveConfig as f, SYSTEM as g, ASSISTANT as h,
|
|
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",
|