code-ollama 0.11.0 → 0.13.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-ByqNs9kx.js} +140 -11
- package/dist/cli.js +242 -71
- 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"
|
|
@@ -1209,6 +1213,9 @@ function Header({ model, onLoad }) {
|
|
|
1209
1213
|
function ModelPicker({ currentModel, onSelect, onClose }) {
|
|
1210
1214
|
const [options, setOptions] = useState([]);
|
|
1211
1215
|
const [error, setError] = useState(null);
|
|
1216
|
+
const handleChange = useCallback((model) => {
|
|
1217
|
+
onSelect({ model });
|
|
1218
|
+
}, [onSelect]);
|
|
1212
1219
|
useInput(async (_input, key) => {
|
|
1213
1220
|
if (!error && options.length && key.return) {
|
|
1214
1221
|
await tick();
|
|
@@ -1241,21 +1248,130 @@ function ModelPicker({ currentModel, onSelect, onClose }) {
|
|
|
1241
1248
|
return /* @__PURE__ */ jsx(SelectPrompt, {
|
|
1242
1249
|
options,
|
|
1243
1250
|
defaultValue: currentModel,
|
|
1244
|
-
onChange:
|
|
1251
|
+
onChange: handleChange,
|
|
1245
1252
|
onCancel: onClose,
|
|
1246
1253
|
children: /* @__PURE__ */ jsx(SelectPromptHint, { message: "Select a model" })
|
|
1247
1254
|
});
|
|
1248
1255
|
}
|
|
1249
1256
|
//#endregion
|
|
1257
|
+
//#region src/components/SearchSettings.tsx
|
|
1258
|
+
var View = /* @__PURE__ */ function(View) {
|
|
1259
|
+
View["Menu"] = "menu";
|
|
1260
|
+
View["Edit"] = "edit";
|
|
1261
|
+
return View;
|
|
1262
|
+
}(View || {});
|
|
1263
|
+
var Action = /* @__PURE__ */ function(Action) {
|
|
1264
|
+
Action["Set"] = "set";
|
|
1265
|
+
Action["Clear"] = "clear";
|
|
1266
|
+
Action["Cancel"] = "cancel";
|
|
1267
|
+
return Action;
|
|
1268
|
+
}(Action || {});
|
|
1269
|
+
function SearchSettings({ currentUrl, onClose, onSave }) {
|
|
1270
|
+
const [view, setView] = useState(View.Menu);
|
|
1271
|
+
const [draftUrl, setDraftUrl] = useState(currentUrl ?? "");
|
|
1272
|
+
const [error, setError] = useState(null);
|
|
1273
|
+
const options = useMemo(() => {
|
|
1274
|
+
const nextOptions = [{
|
|
1275
|
+
label: currentUrl ? "Update SearXNG URL" : "Set SearXNG URL",
|
|
1276
|
+
value: Action.Set
|
|
1277
|
+
}];
|
|
1278
|
+
if (currentUrl) nextOptions.push({
|
|
1279
|
+
label: "Clear SearXNG URL",
|
|
1280
|
+
value: Action.Clear
|
|
1281
|
+
});
|
|
1282
|
+
nextOptions.push({
|
|
1283
|
+
label: "Cancel",
|
|
1284
|
+
value: Action.Cancel
|
|
1285
|
+
});
|
|
1286
|
+
return nextOptions;
|
|
1287
|
+
}, [currentUrl]);
|
|
1288
|
+
const handleChange = useCallback((value) => {
|
|
1289
|
+
setError(null);
|
|
1290
|
+
switch (value) {
|
|
1291
|
+
case Action.Set:
|
|
1292
|
+
setDraftUrl(currentUrl ?? "");
|
|
1293
|
+
setView(View.Edit);
|
|
1294
|
+
break;
|
|
1295
|
+
case Action.Clear:
|
|
1296
|
+
onSave({ searxngBaseUrl: void 0 });
|
|
1297
|
+
break;
|
|
1298
|
+
case Action.Cancel:
|
|
1299
|
+
default: onClose();
|
|
1300
|
+
}
|
|
1301
|
+
}, [
|
|
1302
|
+
currentUrl,
|
|
1303
|
+
onClose,
|
|
1304
|
+
onSave
|
|
1305
|
+
]);
|
|
1306
|
+
const handleSubmit = useCallback((value) => {
|
|
1307
|
+
const trimmedValue = value.trim();
|
|
1308
|
+
if (!trimmedValue) {
|
|
1309
|
+
setError("Enter a URL or press Esc to cancel.");
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
try {
|
|
1313
|
+
const url = new URL(trimmedValue);
|
|
1314
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
1315
|
+
setError("URL must use http or https.");
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
onSave({ searxngBaseUrl: url.toString() });
|
|
1319
|
+
} catch {
|
|
1320
|
+
setError("Enter a valid URL.");
|
|
1321
|
+
}
|
|
1322
|
+
}, [onSave]);
|
|
1323
|
+
useInput((input, key) => {
|
|
1324
|
+
if (view === View.Edit && (key.escape || key.ctrl && input === "c")) {
|
|
1325
|
+
setDraftUrl(currentUrl ?? "");
|
|
1326
|
+
setError(null);
|
|
1327
|
+
setView(View.Menu);
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
if (view === View.Edit) return /* @__PURE__ */ jsxs(Box, {
|
|
1331
|
+
flexDirection: "column",
|
|
1332
|
+
children: [
|
|
1333
|
+
/* @__PURE__ */ jsx(Text, { children: "Set the SearXNG base URL. DuckDuckGo remains the fallback." }),
|
|
1334
|
+
/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, { children: "> " }), /* @__PURE__ */ jsx(TextInput, {
|
|
1335
|
+
value: draftUrl,
|
|
1336
|
+
onChange: setDraftUrl,
|
|
1337
|
+
onSubmit: handleSubmit,
|
|
1338
|
+
placeholder: "http://localhost:8080"
|
|
1339
|
+
})] }),
|
|
1340
|
+
error && /* @__PURE__ */ jsx(Text, {
|
|
1341
|
+
color: "red",
|
|
1342
|
+
children: error
|
|
1343
|
+
}),
|
|
1344
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1345
|
+
dimColor: true,
|
|
1346
|
+
children: "Press Enter to save, Esc to go back."
|
|
1347
|
+
})
|
|
1348
|
+
]
|
|
1349
|
+
});
|
|
1350
|
+
return /* @__PURE__ */ jsxs(SelectPrompt, {
|
|
1351
|
+
options,
|
|
1352
|
+
onChange: handleChange,
|
|
1353
|
+
onCancel: onClose,
|
|
1354
|
+
children: [
|
|
1355
|
+
/* @__PURE__ */ jsxs(Text, { children: ["SearXNG URL: ", /* @__PURE__ */ jsx(Text, {
|
|
1356
|
+
color: "cyan",
|
|
1357
|
+
children: currentUrl ?? "not set"
|
|
1358
|
+
})] }),
|
|
1359
|
+
/* @__PURE__ */ jsx(Text, { children: "DuckDuckGo fallback remains available." }),
|
|
1360
|
+
/* @__PURE__ */ jsx(SelectPromptHint, { message: "Manage web search settings" })
|
|
1361
|
+
]
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
//#endregion
|
|
1250
1365
|
//#region src/components/App.tsx
|
|
1251
1366
|
var SCREEN = /* @__PURE__ */ function(SCREEN) {
|
|
1252
1367
|
SCREEN["CHAT"] = "chat";
|
|
1253
1368
|
SCREEN["MODEL_PICKER"] = "model-picker";
|
|
1369
|
+
SCREEN["SEARCH_SETTINGS"] = "search-settings";
|
|
1254
1370
|
return SCREEN;
|
|
1255
1371
|
}(SCREEN || {});
|
|
1256
1372
|
function App() {
|
|
1257
1373
|
const { exit } = useApp();
|
|
1258
|
-
const [
|
|
1374
|
+
const [appConfig, setConfig] = useState(() => loadConfig());
|
|
1259
1375
|
const [currentScreen, setScreen] = useState(SCREEN.CHAT);
|
|
1260
1376
|
const [mode, setMode] = useState(SAFE);
|
|
1261
1377
|
const [sessionId, setSessionId] = useState(0);
|
|
@@ -1268,6 +1384,9 @@ function App() {
|
|
|
1268
1384
|
case "/model":
|
|
1269
1385
|
setScreen(SCREEN.MODEL_PICKER);
|
|
1270
1386
|
break;
|
|
1387
|
+
case "/search":
|
|
1388
|
+
setScreen(SCREEN.SEARCH_SETTINGS);
|
|
1389
|
+
break;
|
|
1271
1390
|
case "/clear":
|
|
1272
1391
|
resetSystemMessage();
|
|
1273
1392
|
clear();
|
|
@@ -1279,9 +1398,12 @@ function App() {
|
|
|
1279
1398
|
break;
|
|
1280
1399
|
}
|
|
1281
1400
|
}, [exit]);
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1284
|
-
|
|
1401
|
+
const handleUpdateConfig = useCallback((update) => {
|
|
1402
|
+
setConfig((current) => ({
|
|
1403
|
+
...current,
|
|
1404
|
+
...update
|
|
1405
|
+
}));
|
|
1406
|
+
saveConfig(update);
|
|
1285
1407
|
setScreen(SCREEN.CHAT);
|
|
1286
1408
|
}, []);
|
|
1287
1409
|
const handleClose = useCallback(() => {
|
|
@@ -1301,14 +1423,21 @@ function App() {
|
|
|
1301
1423
|
switch (currentScreen) {
|
|
1302
1424
|
case SCREEN.MODEL_PICKER:
|
|
1303
1425
|
screenContent = /* @__PURE__ */ jsx(ModelPicker, {
|
|
1304
|
-
currentModel: model,
|
|
1305
|
-
onSelect:
|
|
1426
|
+
currentModel: appConfig.model,
|
|
1427
|
+
onSelect: handleUpdateConfig,
|
|
1428
|
+
onClose: handleClose
|
|
1429
|
+
});
|
|
1430
|
+
break;
|
|
1431
|
+
case SCREEN.SEARCH_SETTINGS:
|
|
1432
|
+
screenContent = /* @__PURE__ */ jsx(SearchSettings, {
|
|
1433
|
+
currentUrl: appConfig.searxngBaseUrl,
|
|
1434
|
+
onSave: handleUpdateConfig,
|
|
1306
1435
|
onClose: handleClose
|
|
1307
1436
|
});
|
|
1308
1437
|
break;
|
|
1309
1438
|
case SCREEN.CHAT:
|
|
1310
1439
|
screenContent = /* @__PURE__ */ jsx(Chat, {
|
|
1311
|
-
model,
|
|
1440
|
+
model: appConfig.model,
|
|
1312
1441
|
onCommand: handleCommand,
|
|
1313
1442
|
mode,
|
|
1314
1443
|
onModeChange: setMode,
|
|
@@ -1320,13 +1449,13 @@ function App() {
|
|
|
1320
1449
|
flexDirection: "column",
|
|
1321
1450
|
children: [
|
|
1322
1451
|
/* @__PURE__ */ jsx(Header, {
|
|
1323
|
-
model,
|
|
1452
|
+
model: appConfig.model,
|
|
1324
1453
|
onLoad: handleHeaderLoad
|
|
1325
1454
|
}),
|
|
1326
1455
|
isHeaderLoaded && screenContent,
|
|
1327
1456
|
/* @__PURE__ */ jsx(Footer, {
|
|
1328
1457
|
mode,
|
|
1329
|
-
model,
|
|
1458
|
+
model: appConfig.model,
|
|
1330
1459
|
onToggleMode: handleToggleMode
|
|
1331
1460
|
})
|
|
1332
1461
|
]
|
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.13.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,8 @@ 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";
|
|
71
|
+
var WEB_FETCH = "web_fetch";
|
|
65
72
|
//#endregion
|
|
66
73
|
//#region src/utils/agents.ts
|
|
67
74
|
var AGENTS_FILE = "AGENTS.md";
|
|
@@ -97,12 +104,10 @@ function withSystemMessage(messages) {
|
|
|
97
104
|
}
|
|
98
105
|
//#endregion
|
|
99
106
|
//#region src/utils/config.ts
|
|
100
|
-
var
|
|
101
|
-
var CONFIG_PATH = join(
|
|
102
|
-
var
|
|
103
|
-
|
|
104
|
-
model: "gemma4"
|
|
105
|
-
};
|
|
107
|
+
var CONFIG_DIRECTORY = join(homedir(), `.${NAME}`);
|
|
108
|
+
var CONFIG_PATH = join(CONFIG_DIRECTORY, "config.json");
|
|
109
|
+
var DEFAULT_HOST = "http://localhost:11434";
|
|
110
|
+
var DEFAULT_MODEL$1 = "gemma4";
|
|
106
111
|
function readFile$1() {
|
|
107
112
|
if (!existsSync(CONFIG_PATH)) return {};
|
|
108
113
|
try {
|
|
@@ -114,8 +119,9 @@ function readFile$1() {
|
|
|
114
119
|
function loadConfig() {
|
|
115
120
|
const file = readFile$1();
|
|
116
121
|
return {
|
|
117
|
-
host: process.env.OLLAMA_HOST ?? file.host ??
|
|
118
|
-
model: process.env.OLLAMA_MODEL ?? file.model ??
|
|
122
|
+
host: process.env.OLLAMA_HOST ?? file.host ?? DEFAULT_HOST,
|
|
123
|
+
model: process.env.OLLAMA_MODEL ?? file.model ?? DEFAULT_MODEL$1,
|
|
124
|
+
searxngBaseUrl: file.searxngBaseUrl
|
|
119
125
|
};
|
|
120
126
|
}
|
|
121
127
|
function saveConfig(patch) {
|
|
@@ -123,7 +129,7 @@ function saveConfig(patch) {
|
|
|
123
129
|
...readFile$1(),
|
|
124
130
|
...patch
|
|
125
131
|
};
|
|
126
|
-
mkdirSync(
|
|
132
|
+
mkdirSync(CONFIG_DIRECTORY, { recursive: true });
|
|
127
133
|
writeFileSync(CONFIG_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
128
134
|
}
|
|
129
135
|
//#endregion
|
|
@@ -184,8 +190,7 @@ function reset() {
|
|
|
184
190
|
//#region src/utils/time.ts
|
|
185
191
|
var tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
186
192
|
//#endregion
|
|
187
|
-
//#region src/utils/tools.ts
|
|
188
|
-
var execAsync = promisify(exec);
|
|
193
|
+
//#region src/utils/tools/definitions.ts
|
|
189
194
|
/**
|
|
190
195
|
* Helper to define tool parameters
|
|
191
196
|
*/
|
|
@@ -274,41 +279,31 @@ var TOOLS = [
|
|
|
274
279
|
"path",
|
|
275
280
|
"start",
|
|
276
281
|
"end"
|
|
277
|
-
])
|
|
282
|
+
]),
|
|
283
|
+
defineTool(WEB_SEARCH, "Search the web for external or current information", { query: {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "The search query to look up"
|
|
286
|
+
} }, ["query"]),
|
|
287
|
+
defineTool(WEB_FETCH, "Fetch the readable content of a webpage at the given URL", { url: {
|
|
288
|
+
type: "string",
|
|
289
|
+
description: "The full URL of the page to fetch"
|
|
290
|
+
} }, ["url"])
|
|
278
291
|
];
|
|
279
292
|
var READ_TOOLS = new Set([
|
|
280
293
|
READ_FILE,
|
|
281
294
|
LIST_DIR,
|
|
282
295
|
GREP_SEARCH,
|
|
283
|
-
VIEW_RANGE
|
|
296
|
+
VIEW_RANGE,
|
|
297
|
+
WEB_SEARCH,
|
|
298
|
+
WEB_FETCH
|
|
284
299
|
]);
|
|
285
300
|
var WRITE_TOOLS = new Set([
|
|
286
301
|
WRITE_FILE,
|
|
287
302
|
EDIT_FILE,
|
|
288
303
|
RUN_SHELL
|
|
289
304
|
]);
|
|
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
|
-
}
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/utils/tools/filesystem.ts
|
|
312
307
|
/**
|
|
313
308
|
* Read file contents
|
|
314
309
|
*/
|
|
@@ -367,27 +362,27 @@ function editFile(filePath, oldText, newText) {
|
|
|
367
362
|
};
|
|
368
363
|
}
|
|
369
364
|
}
|
|
370
|
-
var SHELL_EXEC_OPTIONS = {
|
|
371
|
-
timeout: 3e4,
|
|
372
|
-
maxBuffer: 1024 * 1024
|
|
373
|
-
};
|
|
374
|
-
/**
|
|
375
|
-
* Execute shell command with shared options (throws on error)
|
|
376
|
-
*/
|
|
377
|
-
function execShell(command) {
|
|
378
|
-
return execAsync(command, SHELL_EXEC_OPTIONS);
|
|
379
|
-
}
|
|
380
365
|
/**
|
|
381
|
-
*
|
|
366
|
+
* View specific line range from file
|
|
382
367
|
*/
|
|
383
|
-
|
|
368
|
+
function viewRange(filePath, start, end) {
|
|
384
369
|
try {
|
|
385
|
-
|
|
386
|
-
|
|
370
|
+
if (!existsSync(filePath)) return {
|
|
371
|
+
content: "",
|
|
372
|
+
error: `File not found: ${filePath}`
|
|
373
|
+
};
|
|
374
|
+
const lines = readFileSync(filePath, "utf8").split("\n");
|
|
375
|
+
const startIdx = Math.max(0, start - 1);
|
|
376
|
+
const endIdx = Math.min(lines.length, end);
|
|
377
|
+
if (startIdx >= lines.length || startIdx > endIdx) return {
|
|
378
|
+
content: "",
|
|
379
|
+
error: "Invalid line range"
|
|
380
|
+
};
|
|
381
|
+
return { content: lines.slice(startIdx, endIdx).join("\n") };
|
|
387
382
|
} catch (error) {
|
|
388
383
|
return {
|
|
389
384
|
content: "",
|
|
390
|
-
error: `
|
|
385
|
+
error: `Failed to view range: ${error instanceof Error ? error.message : String(error)}`
|
|
391
386
|
};
|
|
392
387
|
}
|
|
393
388
|
}
|
|
@@ -414,8 +409,9 @@ function listDir(dirPath) {
|
|
|
414
409
|
* Search for pattern in files using ripgrep if available, fallback to Node.js
|
|
415
410
|
*/
|
|
416
411
|
async function grepSearch(pattern, dirPath) {
|
|
412
|
+
const { execShell } = await import("./assets/shell-CipXM_WI.js").then((n) => n.n);
|
|
417
413
|
try {
|
|
418
|
-
const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/"/g, "\\\"")}" "${dirPath}"`);
|
|
414
|
+
const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}" "${dirPath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`);
|
|
419
415
|
// v8 ignore next
|
|
420
416
|
return { content: stdout || "No matches found" };
|
|
421
417
|
} catch {}
|
|
@@ -451,27 +447,202 @@ async function grepSearch(pattern, dirPath) {
|
|
|
451
447
|
};
|
|
452
448
|
}
|
|
453
449
|
}
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/utils/tools/web/fetch.ts
|
|
452
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
453
|
+
var BASE_HEADERS = { "user-agent": `${NAME}/${VERSION}` };
|
|
454
454
|
/**
|
|
455
|
-
*
|
|
455
|
+
* Fetch text from URL with timeout and headers
|
|
456
456
|
*/
|
|
457
|
-
function
|
|
457
|
+
async function fetchText(url, headers) {
|
|
458
|
+
const response = await fetch(url, {
|
|
459
|
+
headers: {
|
|
460
|
+
...BASE_HEADERS,
|
|
461
|
+
...headers
|
|
462
|
+
},
|
|
463
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
464
|
+
});
|
|
465
|
+
if (!response.ok) throw new Error(`HTTP ${response.status.toString()}`);
|
|
466
|
+
return response.text();
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Fetch and parse JSON from URL with timeout and headers
|
|
470
|
+
*/
|
|
471
|
+
async function fetchJSON(url, headers = {}) {
|
|
472
|
+
const response = await fetch(url, {
|
|
473
|
+
headers: {
|
|
474
|
+
...BASE_HEADERS,
|
|
475
|
+
Accept: "application/json",
|
|
476
|
+
...headers
|
|
477
|
+
},
|
|
478
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
479
|
+
});
|
|
480
|
+
if (!response.ok) throw new Error(`HTTP ${response.status.toString()}`);
|
|
481
|
+
return response.json();
|
|
482
|
+
}
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/utils/tools/web/utils.ts
|
|
485
|
+
/**
|
|
486
|
+
* Strip HTML tags from a string
|
|
487
|
+
*/
|
|
488
|
+
function stripTags(value) {
|
|
489
|
+
return value.replace(/<[^>]+>/g, " ");
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Decode HTML entities
|
|
493
|
+
*/
|
|
494
|
+
function decodeHtml(value) {
|
|
495
|
+
return value.replace(/"/g, "\"").replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'").replace(/ /g, " ").replace(/&/g, "&");
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Clean whitespace in text
|
|
499
|
+
*/
|
|
500
|
+
function cleanText(value) {
|
|
501
|
+
return value.replace(/\s+/g, " ").trim();
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Truncate text to max length with ellipsis
|
|
505
|
+
*/
|
|
506
|
+
function truncate(value, maxLength) {
|
|
507
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 1).trimEnd()}…` : value;
|
|
508
|
+
}
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/utils/tools/web/fetch-page.ts
|
|
511
|
+
var JINA_READER_BASE_URL = "https://r.jina.ai/";
|
|
512
|
+
/**
|
|
513
|
+
* Fetch readable page content via Jina Reader, with fallback to direct fetch + HTML stripping
|
|
514
|
+
*/
|
|
515
|
+
async function webFetch(url) {
|
|
516
|
+
const trimmedUrl = url.trim();
|
|
517
|
+
if (!trimmedUrl) return {
|
|
518
|
+
content: "",
|
|
519
|
+
error: "URL cannot be empty"
|
|
520
|
+
};
|
|
458
521
|
try {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const endIdx = Math.min(lines.length, end);
|
|
466
|
-
if (startIdx >= lines.length || startIdx > endIdx) return {
|
|
522
|
+
return { content: await fetchText(`${JINA_READER_BASE_URL}${trimmedUrl}`, { Accept: "text/plain" }) };
|
|
523
|
+
} catch {}
|
|
524
|
+
try {
|
|
525
|
+
return { content: `Note: Jina Reader unavailable, falling back to raw fetch.\n\n${cleanText(stripTags(await fetchText(trimmedUrl, { Accept: "text/html" })))}` };
|
|
526
|
+
} catch (error) {
|
|
527
|
+
return {
|
|
467
528
|
content: "",
|
|
468
|
-
error:
|
|
529
|
+
error: `Failed to fetch page: ${error instanceof Error ? error.message : String(error)}`
|
|
469
530
|
};
|
|
470
|
-
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
//#endregion
|
|
534
|
+
//#region src/utils/tools/web/search.ts
|
|
535
|
+
var SEARCH_RESULT_LIMIT = 5;
|
|
536
|
+
async function webSearch(query) {
|
|
537
|
+
const trimmedQuery = query.trim();
|
|
538
|
+
if (!trimmedQuery) return {
|
|
539
|
+
content: "",
|
|
540
|
+
error: "Search query cannot be empty"
|
|
541
|
+
};
|
|
542
|
+
const { searxngBaseUrl } = loadConfig();
|
|
543
|
+
let searxngIssue = null;
|
|
544
|
+
if (searxngBaseUrl) try {
|
|
545
|
+
const searxngResults = await searchSearXNG(searxngBaseUrl, trimmedQuery);
|
|
546
|
+
if (searxngResults.length) return { content: formatSearchResults("SearXNG", searxngResults) };
|
|
547
|
+
searxngIssue = "SearXNG returned no results";
|
|
548
|
+
} catch (error) {
|
|
549
|
+
searxngIssue = `SearXNG failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const duckDuckGoResults = await searchDuckDuckGo(trimmedQuery);
|
|
553
|
+
if (duckDuckGoResults.length) return { content: formatSearchResults("DuckDuckGo", duckDuckGoResults, searxngIssue ? `${searxngIssue}. Using DuckDuckGo fallback.` : void 0) };
|
|
554
|
+
if (searxngIssue) return { content: `No web results found. ${searxngIssue}. DuckDuckGo also returned no results.` };
|
|
555
|
+
return { content: "No web results found." };
|
|
471
556
|
} catch (error) {
|
|
557
|
+
const duckDuckGoIssue = `DuckDuckGo failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
472
558
|
return {
|
|
473
559
|
content: "",
|
|
474
|
-
error:
|
|
560
|
+
error: searxngIssue ? `${searxngIssue}; ${duckDuckGoIssue}` : duckDuckGoIssue
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async function searchSearXNG(baseUrl, query) {
|
|
565
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
566
|
+
const url = new URL(`${normalizedBaseUrl}/search`);
|
|
567
|
+
url.searchParams.set("q", query);
|
|
568
|
+
url.searchParams.set("format", "json");
|
|
569
|
+
url.searchParams.set("language", "en-US");
|
|
570
|
+
return normalizeResults((await fetchJSON(url.toString())).results?.map((result) => ({
|
|
571
|
+
title: result.title ?? "",
|
|
572
|
+
url: result.url ?? "",
|
|
573
|
+
snippet: result.content ?? ""
|
|
574
|
+
})) ?? []);
|
|
575
|
+
}
|
|
576
|
+
async function searchDuckDuckGo(query) {
|
|
577
|
+
const url = new URL("https://html.duckduckgo.com/html/");
|
|
578
|
+
url.searchParams.set("q", query);
|
|
579
|
+
return parseDuckDuckGoResults(await fetchText(url.toString(), { Accept: "text/html" }));
|
|
580
|
+
}
|
|
581
|
+
function parseDuckDuckGoResults(html) {
|
|
582
|
+
const results = [];
|
|
583
|
+
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)) {
|
|
584
|
+
const url = normalizeDuckDuckGoUrl(match[1]);
|
|
585
|
+
const title = decodeHtml(stripTags(match[2]));
|
|
586
|
+
const snippet = decodeHtml(stripTags(match[3]));
|
|
587
|
+
if (!url || !title) continue;
|
|
588
|
+
results.push({
|
|
589
|
+
title,
|
|
590
|
+
url,
|
|
591
|
+
snippet
|
|
592
|
+
});
|
|
593
|
+
if (results.length >= SEARCH_RESULT_LIMIT) break;
|
|
594
|
+
}
|
|
595
|
+
return normalizeResults(results);
|
|
596
|
+
}
|
|
597
|
+
function normalizeDuckDuckGoUrl(url) {
|
|
598
|
+
try {
|
|
599
|
+
const parsedUrl = new URL(url, "https://duckduckgo.com");
|
|
600
|
+
const redirectedUrl = parsedUrl.searchParams.get("uddg");
|
|
601
|
+
return redirectedUrl ? decodeURIComponent(redirectedUrl) : parsedUrl.toString();
|
|
602
|
+
} catch {
|
|
603
|
+
return url;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function normalizeResults(results) {
|
|
607
|
+
return results.map((result) => ({
|
|
608
|
+
title: cleanText(result.title),
|
|
609
|
+
url: result.url.trim(),
|
|
610
|
+
snippet: cleanText(result.snippet)
|
|
611
|
+
})).filter((result) => result.title && result.url).slice(0, SEARCH_RESULT_LIMIT);
|
|
612
|
+
}
|
|
613
|
+
function formatSearchResults(source, results, note) {
|
|
614
|
+
const lines = [`Source: ${source}`];
|
|
615
|
+
if (note) lines.push(`Note: ${note}`);
|
|
616
|
+
for (const [index, result] of results.entries()) {
|
|
617
|
+
lines.push(`${(index + 1).toString()}. ${result.title}`);
|
|
618
|
+
lines.push(` URL: ${result.url}`);
|
|
619
|
+
if (result.snippet) lines.push(` Snippet: ${truncate(result.snippet, 240)}`);
|
|
620
|
+
}
|
|
621
|
+
return lines.join("\n");
|
|
622
|
+
}
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region src/utils/tools/dispatcher.ts
|
|
625
|
+
/**
|
|
626
|
+
* Execute a tool by name with arguments
|
|
627
|
+
*/
|
|
628
|
+
async function executeTool(name, args, options) {
|
|
629
|
+
if (options?.allowedTools && !options.allowedTools.has(name)) return {
|
|
630
|
+
content: "",
|
|
631
|
+
error: `Tool not allowed: ${name}`
|
|
632
|
+
};
|
|
633
|
+
switch (name) {
|
|
634
|
+
case READ_FILE: return readFile(args.path);
|
|
635
|
+
case WRITE_FILE: return writeFile(args.path, args.content);
|
|
636
|
+
case EDIT_FILE: return editFile(args.path, args.oldText, args.newText);
|
|
637
|
+
case RUN_SHELL: return runShell(args.command);
|
|
638
|
+
case LIST_DIR: return listDir(args.path);
|
|
639
|
+
case GREP_SEARCH: return await grepSearch(args.pattern, args.path);
|
|
640
|
+
case VIEW_RANGE: return viewRange(args.path, args.start, args.end);
|
|
641
|
+
case WEB_SEARCH: return await webSearch(args.query);
|
|
642
|
+
case WEB_FETCH: return await webFetch(args.url);
|
|
643
|
+
default: return {
|
|
644
|
+
content: "",
|
|
645
|
+
error: `Unknown tool: ${name}`
|
|
475
646
|
};
|
|
476
647
|
}
|
|
477
648
|
}
|
|
@@ -525,7 +696,7 @@ async function processRunStream(messages, model) {
|
|
|
525
696
|
}
|
|
526
697
|
async function main(args = process.argv.slice(2)) {
|
|
527
698
|
if (!args.length) {
|
|
528
|
-
const { renderApp } = await import("./assets/tui-
|
|
699
|
+
const { renderApp } = await import("./assets/tui-ByqNs9kx.js");
|
|
529
700
|
reset();
|
|
530
701
|
renderApp();
|
|
531
702
|
return;
|
|
@@ -548,4 +719,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
|
|
|
548
719
|
if (isEntrypoint()) main();
|
|
549
720
|
// v8 ignore stop
|
|
550
721
|
//#endregion
|
|
551
|
-
export { USER as _, tick as a, setClearHandler as c, loadConfig as d, saveConfig as f, SYSTEM as g, ASSISTANT as h,
|
|
722
|
+
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.13.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",
|