code-ollama 0.1.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/dist/cli.js CHANGED
@@ -1,70 +1,448 @@
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.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
5
376
  //#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) => {
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) => {
8
381
  try {
9
- await u(e, t);
10
- } catch (e) {
382
+ await runPrompt(model, prompt);
383
+ } catch (error) {
11
384
  // v8 ignore next
12
- let t = e instanceof Error ? e.message : "Unknown error";
13
- process.stderr.write(`Error: ${t}\n`), process.exitCode = 1;
385
+ const message = error instanceof Error ? error.message : "Unknown error";
386
+ process.stderr.write(`Error: ${message}\n`);
387
+ process.exitCode = 1;
14
388
  }
15
389
  });
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,
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,
25
400
  content: ""
26
401
  };
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);
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);
30
406
  continue;
31
407
  }
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}` : ""}`
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}` : ""}`
36
413
  };
37
- await d([
38
- ...e,
39
- n,
40
- s
41
- ], t);
414
+ await processRunStream([
415
+ ...messages,
416
+ assistantMessage,
417
+ toolResultMessage
418
+ ], model);
42
419
  return;
43
420
  }
44
421
  }
45
422
  }
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();
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();
50
428
  return;
51
429
  }
52
- l.parse([
430
+ cli.parse([
53
431
  "node",
54
432
  "code-ollama",
55
- ...e
433
+ ...args
56
434
  ]);
57
435
  }
58
436
  /* v8 ignore start */
59
- function p(e = process.argv[1]) {
60
- if (!e) return !1;
437
+ function isEntrypoint(argv1 = process.argv[1]) {
438
+ if (!argv1) return false;
61
439
  try {
62
- return s(e) === import.meta.filename;
440
+ return realpathSync(argv1) === import.meta.filename;
63
441
  } catch {
64
- return !1;
442
+ return false;
65
443
  }
66
444
  }
67
- p() && f();
445
+ if (isEntrypoint()) main();
68
446
  /* v8 ignore stop */
69
447
  //#endregion
70
- export { f as main };
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,6 +1,6 @@
1
1
  {
2
2
  "name": "code-ollama",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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
  },