code-ollama 0.1.0 → 0.2.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/dist/cli.js CHANGED
@@ -1,70 +1,499 @@
1
1
  #!/usr/bin/env node
2
- import { d as e, i as t, l as n, o as r, r as i, t as a, u as o } from "./utils-DBXrYZEs.js";
3
- import { realpathSync as s } from "node:fs";
4
- import c from "cac";
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.2.0";
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: Replace one exact text match in a file (requires approval)
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
+ EDIT_FILE: "edit_file",
51
+ RUN_SHELL: "run_shell",
52
+ LIST_DIR: "list_dir",
53
+ GREP_SEARCH: "grep_search",
54
+ VIEW_RANGE: "view_range"
55
+ };
56
+ //#endregion
57
+ //#region src/utils/agents.ts
58
+ var AGENTS_FILE = "AGENTS.md";
59
+ function loadAgentsContent() {
60
+ const agentsPath = join(process.cwd(), AGENTS_FILE);
61
+ if (!existsSync(agentsPath)) return null;
62
+ try {
63
+ return readFileSync(agentsPath, "utf8");
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ function buildSystemPrompt() {
69
+ const parts = [BASE_SYSTEM_PROMPT];
70
+ const agentsContent = loadAgentsContent();
71
+ if (agentsContent) parts.push("\n\nProject context from AGENTS.md:\n", agentsContent);
72
+ parts.push("\n\n", TOOL_INSTRUCTIONS);
73
+ return parts.join("");
74
+ }
75
+ function createSystemMessage() {
76
+ return {
77
+ role: ROLE.SYSTEM,
78
+ content: buildSystemPrompt()
79
+ };
80
+ }
81
+ //#endregion
82
+ //#region src/utils/config.ts
83
+ var CONFIG_DIR = join(homedir(), ".code-ollama");
84
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
85
+ var DEFAULTS = {
86
+ host: "http://localhost:11434",
87
+ model: "gemma4"
88
+ };
89
+ function readFile$1() {
90
+ if (!existsSync(CONFIG_PATH)) return {};
91
+ try {
92
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
93
+ } catch {
94
+ return {};
95
+ }
96
+ }
97
+ function loadConfig() {
98
+ const file = readFile$1();
99
+ return {
100
+ host: process.env.OLLAMA_HOST ?? file.host ?? DEFAULTS.host,
101
+ model: process.env.OLLAMA_MODEL ?? file.model ?? DEFAULTS.model
102
+ };
103
+ }
104
+ function saveConfig(patch) {
105
+ const updated = {
106
+ ...readFile$1(),
107
+ ...patch
108
+ };
109
+ mkdirSync(CONFIG_DIR, { recursive: true });
110
+ writeFileSync(CONFIG_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
111
+ }
112
+ //#endregion
113
+ //#region src/utils/ollama.ts
114
+ var { host, model: DEFAULT_MODEL } = loadConfig();
115
+ var client = new Ollama({ host });
116
+ async function* streamChat(messages, model = DEFAULT_MODEL, tools) {
117
+ const response = await client.chat({
118
+ model,
119
+ messages,
120
+ stream: true,
121
+ tools
122
+ });
123
+ for await (const chunk of response) {
124
+ if (chunk.message.content) yield {
125
+ type: "content",
126
+ content: chunk.message.content
127
+ };
128
+ if (chunk.message.tool_calls) yield {
129
+ type: "tool_calls",
130
+ tool_calls: chunk.message.tool_calls
131
+ };
132
+ }
133
+ }
134
+ async function listModels() {
135
+ const { models } = await client.list();
136
+ return models.map(({ name }) => name);
137
+ }
138
+ //#endregion
139
+ //#region src/utils/screen.ts
140
+ var CLEAR = "\x1Bc";
141
+ function clear() {
142
+ process.stdout.write(CLEAR);
143
+ }
144
+ //#endregion
145
+ //#region src/utils/tools.ts
146
+ var execAsync = promisify(exec);
147
+ /**
148
+ * Helper to define tool parameters
149
+ */
150
+ function defineTool(name, description, params, required) {
151
+ return {
152
+ type: "function",
153
+ function: {
154
+ name,
155
+ description,
156
+ parameters: {
157
+ type: "object",
158
+ properties: params,
159
+ required
160
+ }
161
+ }
162
+ };
163
+ }
164
+ /**
165
+ * Tool definitions for Ollama API
166
+ */
167
+ var TOOLS = [
168
+ defineTool(NAME.READ_FILE, "Read the contents of a file at the specified path", { path: {
169
+ type: "string",
170
+ description: "The path to the file to read"
171
+ } }, ["path"]),
172
+ defineTool(NAME.WRITE_FILE, "Write content to a file at the specified path", {
173
+ path: {
174
+ type: "string",
175
+ description: "The path to the file to write"
176
+ },
177
+ content: {
178
+ type: "string",
179
+ description: "The content to write to the file"
180
+ }
181
+ }, ["path", "content"]),
182
+ defineTool(NAME.EDIT_FILE, "Replace one exact text match in an existing file at the specified path", {
183
+ path: {
184
+ type: "string",
185
+ description: "The path to the file to edit"
186
+ },
187
+ oldText: {
188
+ type: "string",
189
+ description: "The exact existing text to replace"
190
+ },
191
+ newText: {
192
+ type: "string",
193
+ description: "The replacement text to write in place of oldText"
194
+ }
195
+ }, [
196
+ "path",
197
+ "oldText",
198
+ "newText"
199
+ ]),
200
+ defineTool(NAME.RUN_SHELL, "Execute a shell command", { command: {
201
+ type: "string",
202
+ description: "The shell command to execute"
203
+ } }, ["command"]),
204
+ defineTool(NAME.LIST_DIR, "List the contents of a directory", { path: {
205
+ type: "string",
206
+ description: "The path to the directory to list"
207
+ } }, ["path"]),
208
+ defineTool(NAME.GREP_SEARCH, "Search for a pattern in files within a directory", {
209
+ pattern: {
210
+ type: "string",
211
+ description: "The regex pattern to search for"
212
+ },
213
+ path: {
214
+ type: "string",
215
+ description: "The directory path to search in"
216
+ }
217
+ }, ["pattern", "path"]),
218
+ defineTool(NAME.VIEW_RANGE, "View a specific range of lines from a file", {
219
+ path: {
220
+ type: "string",
221
+ description: "The path to the file"
222
+ },
223
+ start: {
224
+ type: "number",
225
+ description: "The starting line number (1-indexed)"
226
+ },
227
+ end: {
228
+ type: "number",
229
+ description: "The ending line number (inclusive)"
230
+ }
231
+ }, [
232
+ "path",
233
+ "start",
234
+ "end"
235
+ ])
236
+ ];
237
+ var TOOLS_REQUIRING_APPROVAL = new Set([
238
+ NAME.WRITE_FILE,
239
+ NAME.EDIT_FILE,
240
+ NAME.RUN_SHELL
241
+ ]);
242
+ /**
243
+ * Execute a tool by name with arguments
244
+ */
245
+ async function executeTool(name, args) {
246
+ switch (name) {
247
+ case NAME.READ_FILE: return readFile(args.path);
248
+ case NAME.WRITE_FILE: return writeFile(args.path, args.content);
249
+ case NAME.EDIT_FILE: return editFile(args.path, args.oldText, args.newText);
250
+ case NAME.RUN_SHELL: return runShell(args.command);
251
+ case NAME.LIST_DIR: return listDir(args.path);
252
+ case NAME.GREP_SEARCH: return await grepSearch(args.pattern, args.path);
253
+ case NAME.VIEW_RANGE: return viewRange(args.path, args.start, args.end);
254
+ default: return {
255
+ content: "",
256
+ error: `Unknown tool: ${name}`
257
+ };
258
+ }
259
+ }
260
+ /**
261
+ * Read file contents
262
+ */
263
+ function readFile(filePath) {
264
+ try {
265
+ if (!existsSync(filePath)) return {
266
+ content: "",
267
+ error: `File not found: ${filePath}`
268
+ };
269
+ return { content: readFileSync(filePath, "utf8") };
270
+ } catch (error) {
271
+ return {
272
+ content: "",
273
+ error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`
274
+ };
275
+ }
276
+ }
277
+ /**
278
+ * Write content to file
279
+ */
280
+ function writeFile(filePath, content) {
281
+ try {
282
+ writeFileSync(filePath, content, "utf8");
283
+ return { content: `File written successfully: ${filePath}` };
284
+ } catch (error) {
285
+ return {
286
+ content: "",
287
+ error: `Failed to write file: ${error instanceof Error ? error.message : String(error)}`
288
+ };
289
+ }
290
+ }
291
+ /**
292
+ * Replace one exact text match in an existing file
293
+ */
294
+ function editFile(filePath, oldText, newText) {
295
+ try {
296
+ if (!existsSync(filePath)) return {
297
+ content: "",
298
+ error: `File not found: ${filePath}`
299
+ };
300
+ const content = readFileSync(filePath, "utf8");
301
+ if (!content.includes(oldText)) return {
302
+ content: "",
303
+ error: `Exact text not found in file: ${filePath}`
304
+ };
305
+ if (content.split(oldText).length - 1 > 1) return {
306
+ content: "",
307
+ error: `Exact text matched multiple locations in file: ${filePath}`
308
+ };
309
+ writeFileSync(filePath, content.replace(oldText, newText), "utf8");
310
+ return { content: `File edited successfully: ${filePath}` };
311
+ } catch (error) {
312
+ return {
313
+ content: "",
314
+ error: `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`
315
+ };
316
+ }
317
+ }
318
+ var SHELL_EXEC_OPTIONS = {
319
+ timeout: 3e4,
320
+ maxBuffer: 1024 * 1024
321
+ };
322
+ /**
323
+ * Execute shell command with shared options (throws on error)
324
+ */
325
+ function execShell(command) {
326
+ return execAsync(command, SHELL_EXEC_OPTIONS);
327
+ }
328
+ /**
329
+ * Execute shell command
330
+ */
331
+ async function runShell(command) {
332
+ try {
333
+ const { stdout, stderr } = await execShell(command);
334
+ return { content: stdout || stderr };
335
+ } catch (error) {
336
+ return {
337
+ content: "",
338
+ error: `Command failed: ${error instanceof Error ? error.message : String(error)}`
339
+ };
340
+ }
341
+ }
342
+ /**
343
+ * List directory contents
344
+ */
345
+ function listDir(dirPath) {
346
+ try {
347
+ if (!existsSync(dirPath)) return {
348
+ content: "",
349
+ error: `Directory not found: ${dirPath}`
350
+ };
351
+ return { content: readdirSync(dirPath, { withFileTypes: true }).map((entry) => {
352
+ return `[${entry.isDirectory() ? "d" : "f"}] ${entry.name}`;
353
+ }).join("\n") };
354
+ } catch (error) {
355
+ return {
356
+ content: "",
357
+ error: `Failed to list directory: ${error instanceof Error ? error.message : String(error)}`
358
+ };
359
+ }
360
+ }
361
+ /**
362
+ * Search for pattern in files using ripgrep if available, fallback to Node.js
363
+ */
364
+ async function grepSearch(pattern, dirPath) {
365
+ try {
366
+ const { stdout } = await execShell(`rg --line-number --no-heading --smart-case "${pattern.replace(/"/g, "\\\"")}" "${dirPath}"`);
367
+ // v8 ignore next
368
+ return { content: stdout || "No matches found" };
369
+ } catch {}
370
+ try {
371
+ if (!existsSync(dirPath)) return {
372
+ content: "",
373
+ error: `Directory not found: ${dirPath}`
374
+ };
375
+ const regex = new RegExp(pattern, "g");
376
+ const results = [];
377
+ function searchDirectory(currentPath) {
378
+ const entries = readdirSync(currentPath, { withFileTypes: true });
379
+ for (const entry of entries) {
380
+ const fullPath = join(currentPath, entry.name);
381
+ if (entry.isDirectory()) {
382
+ if (!entry.name.startsWith(".") && entry.name !== "node_modules") searchDirectory(fullPath);
383
+ } else if (entry.isFile()) try {
384
+ const lines = readFileSync(fullPath, "utf8").split("\n");
385
+ for (let i = 0; i < lines.length; i++) {
386
+ if (regex.test(lines[i])) results.push(`${fullPath}:${(i + 1).toString()}: ${lines[i].trim()}`);
387
+ regex.lastIndex = 0;
388
+ }
389
+ } catch {}
390
+ }
391
+ }
392
+ searchDirectory(dirPath);
393
+ if (results.length === 0) return { content: "No matches found" };
394
+ return { content: results.join("\n") };
395
+ } catch (error) {
396
+ return {
397
+ content: "",
398
+ error: `Search failed: ${error instanceof Error ? error.message : String(error)}`
399
+ };
400
+ }
401
+ }
402
+ /**
403
+ * View specific line range from file
404
+ */
405
+ function viewRange(filePath, start, end) {
406
+ try {
407
+ if (!existsSync(filePath)) return {
408
+ content: "",
409
+ error: `File not found: ${filePath}`
410
+ };
411
+ const lines = readFileSync(filePath, "utf8").split("\n");
412
+ const startIdx = Math.max(0, start - 1);
413
+ const endIdx = Math.min(lines.length, end);
414
+ if (startIdx >= lines.length || startIdx > endIdx) return {
415
+ content: "",
416
+ error: "Invalid line range"
417
+ };
418
+ return { content: lines.slice(startIdx, endIdx).join("\n") };
419
+ } catch (error) {
420
+ return {
421
+ content: "",
422
+ error: `Failed to view range: ${error instanceof Error ? error.message : String(error)}`
423
+ };
424
+ }
425
+ }
426
+ //#endregion
5
427
  //#region src/cli.ts
6
- var l = c("code-ollama");
7
- l.version(e), l.help(), l.command("run <model> <prompt>", "Run a one-off prompt").action(async (e, t) => {
428
+ var cli = cac("code-ollama");
429
+ cli.version(VERSION);
430
+ cli.help();
431
+ cli.command("run <model> <prompt>", "Run a one-off prompt").action(async (model, prompt) => {
8
432
  try {
9
- await u(e, t);
10
- } catch (e) {
433
+ await runPrompt(model, prompt);
434
+ } catch (error) {
11
435
  // v8 ignore next
12
- let t = e instanceof Error ? e.message : "Unknown error";
13
- process.stderr.write(`Error: ${t}\n`), process.exitCode = 1;
436
+ const message = error instanceof Error ? error.message : "Unknown error";
437
+ process.stderr.write(`Error: ${message}\n`);
438
+ process.exitCode = 1;
14
439
  }
15
440
  });
16
- async function u(e, t) {
17
- await d([n(), {
18
- role: o.USER,
19
- content: t
20
- }], e), process.stdout.write("\n");
21
- }
22
- async function d(e, t) {
23
- let n = {
24
- role: o.ASSISTANT,
441
+ async function runPrompt(model, prompt) {
442
+ await processRunStream([createSystemMessage(), {
443
+ role: ROLE.USER,
444
+ content: prompt
445
+ }], model);
446
+ process.stdout.write("\n");
447
+ }
448
+ async function processRunStream(messages, model) {
449
+ const assistantMessage = {
450
+ role: ROLE.ASSISTANT,
25
451
  content: ""
26
452
  };
27
- for await (let s of r(e, t, a)) {
28
- if (s.type === "content") {
29
- n.content += s.content, process.stdout.write(s.content);
453
+ for await (const chunk of streamChat(messages, model, TOOLS)) {
454
+ if (chunk.type === "content") {
455
+ assistantMessage.content += chunk.content;
456
+ process.stdout.write(chunk.content);
30
457
  continue;
31
458
  }
32
- for (let r of s.tool_calls) {
33
- let a = await i(r.function.name, r.function.arguments), s = {
34
- role: o.SYSTEM,
35
- content: `Tool ${r.function.name} result:\n${a.content}${a.error ? `\nError: ${a.error}` : ""}`
459
+ for (const toolCall of chunk.tool_calls) {
460
+ const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
461
+ const toolResultMessage = {
462
+ role: ROLE.SYSTEM,
463
+ content: `Tool ${toolCall.function.name} result:\n${result.content}${result.error ? `\nError: ${result.error}` : ""}`
36
464
  };
37
- await d([
38
- ...e,
39
- n,
40
- s
41
- ], t);
465
+ await processRunStream([
466
+ ...messages,
467
+ assistantMessage,
468
+ toolResultMessage
469
+ ], model);
42
470
  return;
43
471
  }
44
472
  }
45
473
  }
46
- async function f(e = process.argv.slice(2)) {
47
- if (!e.length) {
48
- let { renderApp: e } = await import("./tui-Bu6wAbeu.js");
49
- t(), e();
474
+ async function main(args = process.argv.slice(2)) {
475
+ if (!args.length) {
476
+ const { renderApp } = await import("./assets/tui-DSR1MJGd.js");
477
+ clear();
478
+ renderApp();
50
479
  return;
51
480
  }
52
- l.parse([
481
+ cli.parse([
53
482
  "node",
54
483
  "code-ollama",
55
- ...e
484
+ ...args
56
485
  ]);
57
486
  }
58
487
  /* v8 ignore start */
59
- function p(e = process.argv[1]) {
60
- if (!e) return !1;
488
+ function isEntrypoint(argv1 = process.argv[1]) {
489
+ if (!argv1) return false;
61
490
  try {
62
- return s(e) === import.meta.filename;
491
+ return realpathSync(argv1) === import.meta.filename;
63
492
  } catch {
64
- return !1;
493
+ return false;
65
494
  }
66
495
  }
67
- p() && f();
496
+ if (isEntrypoint()) main();
68
497
  /* v8 ignore stop */
69
498
  //#endregion
70
- export { f as main };
499
+ 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,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -64,7 +64,7 @@
64
64
  "publint": "0.3.18",
65
65
  "tsx": "4.21.0",
66
66
  "typescript": "6.0.3",
67
- "typescript-eslint": "8.59.1",
67
+ "typescript-eslint": "8.59.2",
68
68
  "vite": "8.0.10",
69
69
  "vitest": "4.1.5"
70
70
  },