aiforcecli-local 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # aiforcecli-local
2
+
3
+ A standalone, local-first coding-agent CLI for DeepSeek, GLM, and Qwen models running through Ollama. The npm package is `aiforcecli-local`, and the installed executable is `aiforce-local`. It can run independently or through the `aiforcecli-chat` adapter.
4
+
5
+ ## Capabilities
6
+
7
+ - Inspect and search a selected repository.
8
+ - Create text files and apply exact, reviewable replacements.
9
+ - Ask before every shell command, including tests and builds.
10
+ - Stream tool activity and command output.
11
+ - Produce deterministic change summaries and compact diffs without requiring Git.
12
+ - Resume local sessions.
13
+ - Emit JSONL events for automation and future adapters.
14
+ - Use native Ollama tool calling with an enforced fallback for models without native tool support.
15
+ - Run entirely locally with no API keys or per-token fees.
16
+
17
+ ## Requirements
18
+
19
+ - Node.js 20 or newer
20
+ - Ollama
21
+ - Enough RAM or VRAM for the selected model
22
+ - A model pulled into Ollama
23
+
24
+ ## Install for local development
25
+
26
+ ```powershell
27
+ git clone https://github.com/apoorviy/aiforce-local.git
28
+ cd aiforce-local
29
+ npm test
30
+ npm link
31
+ ```
32
+
33
+ Verify the runtime and available presets:
34
+
35
+ ```powershell
36
+ aiforce-local doctor
37
+ aiforce-local models
38
+ ```
39
+
40
+ ## Model setup
41
+
42
+ ```powershell
43
+ aiforce-local pull qwen
44
+ aiforce-local pull glm
45
+ aiforce-local pull deepseek
46
+ ```
47
+
48
+ Aliases currently resolve to:
49
+
50
+ - `deepseek` -> `deepseek-coder-v2:16b`
51
+ - `glm` -> `glm4:9b`
52
+ - `qwen` -> `qwen2.5-coder:7b`
53
+
54
+ You can supply any exact Ollama tag instead. Upstream availability and model licenses should be checked before redistribution.
55
+
56
+ ## Run the coding agent
57
+
58
+ From a repository:
59
+
60
+ ```powershell
61
+ cd C:\path\to\project
62
+ aiforce-local agent "Add input validation and tests" --model glm
63
+ ```
64
+
65
+ Or select a repository explicitly:
66
+
67
+ ```powershell
68
+ aiforce-local agent "Fix the failing parser test" --cwd C:\path\to\project --model qwen
69
+ ```
70
+
71
+ When the agent requests a command, the CLI displays the exact command, repository, and timeout:
72
+
73
+ ```text
74
+ Command requested in C:\path\to\project:
75
+ npm test
76
+ Timeout: 120000ms
77
+ Approve this command? [y/N]
78
+ ```
79
+
80
+ Only `y` or `yes` executes it. Non-interactive sessions deny commands automatically.
81
+
82
+ Resume a session using the identifier printed at startup:
83
+
84
+ ```powershell
85
+ aiforce-local agent "Continue and address the remaining failure" --resume local-... --cwd C:\path\to\project
86
+ ```
87
+
88
+ Emit machine-readable JSONL events:
89
+
90
+ ```powershell
91
+ aiforce-local agent "Explain the project structure" --json
92
+ ```
93
+
94
+ ## Inference-only commands
95
+
96
+ The original simple prompt and chat commands remain available:
97
+
98
+ ```powershell
99
+ aiforce-local run "Explain dependency injection" --model qwen
100
+ aiforce-local chat --model glm
101
+ ```
102
+
103
+ Type `/exit` to leave interactive chat.
104
+
105
+ ## Safety model
106
+
107
+ - Model-directed paths are resolved against the repository's real path.
108
+ - Absolute paths, traversal, symlink escapes, `.git`, dependency/build directories, and common credential files are blocked.
109
+ - Reads, searches, output, command duration, file size, and agent turns are bounded.
110
+ - Existing files can only be changed through exact text replacement.
111
+ - File deletion is not available.
112
+ - Every shell command requires a separate approval; there is no approve-all option.
113
+ - An ungrounded model response is rejected until the model inspects the repository.
114
+ - Successful edits are fed back as authoritative state to prevent duplicate or stale actions.
115
+
116
+ Use the agent in a Git repository so ordinary Git review and rollback remain available.
117
+
118
+ ## Configuration
119
+
120
+ - `OLLAMA_HOST`: Ollama server URL. Default: `http://127.0.0.1:11434`.
121
+ - `AIFORCE_LOCAL_DATA_DIR`: optional base directory for resumable session data.
122
+
123
+ By default, sessions are stored under the platform's local application-data directory. Session records include prompts and tool results; do not ask the agent to read secrets.
124
+
125
+ ## Development and evaluation
126
+
127
+ ```powershell
128
+ npm test
129
+ npm run check
130
+ ```
131
+
132
+ The suite includes deterministic behavior evaluations for tool trajectory, command denial, path confinement, editing, grounding enforcement, native-tool fallback, loose GLM tool syntax, and session persistence. Live model quality still depends on the selected model and machine.
133
+
134
+ ## Current boundaries
135
+
136
+ - Local CLI only; no cloud deployment.
137
+ - Integration with `aiforcecli-chat` is available through its built-in `aiforce-local` adapter.
138
+ - No file deletion, browser automation, MCP, or unattended command execution.
139
+ - Smaller local models can be slower and less reliable than hosted coding agents; the grounding and state gates prevent several common failure modes but cannot make every model equally capable.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "aiforcecli-local",
3
+ "version": "0.1.0",
4
+ "description": "Standalone local coding-agent CLI for DeepSeek, GLM, and Qwen models through Ollama.",
5
+ "private": false,
6
+ "type": "module",
7
+ "bin": {
8
+ "aiforce-local": "src/cli.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/apoorviy/aiforce-local.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/apoorviy/aiforce-local/issues"
16
+ },
17
+ "homepage": "https://github.com/apoorviy/aiforce-local#readme",
18
+ "files": [
19
+ "src",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "test": "node --test --test-isolation=none",
25
+ "check": "node --check src/agent-tools.js && node --check src/cli.js && node --check src/coding-agent.js && node --check src/command-runner.js && node --check src/models.js && node --check src/ollama-client.js && node --check src/session-store.js && node --check src/workspace.js",
26
+ "prepublishOnly": "npm test && npm run check"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "keywords": [
32
+ "ollama",
33
+ "deepseek",
34
+ "glm",
35
+ "qwen",
36
+ "local-ai"
37
+ ],
38
+ "author": "Atharv Iyer",
39
+ "license": "MIT"
40
+ }
@@ -0,0 +1,115 @@
1
+ import { runApprovedCommand } from './command-runner.js';
2
+
3
+ export const TOOL_DEFINITIONS = Object.freeze([
4
+ tool('list_files', 'List files inside the selected repository. Use this before guessing paths.', {
5
+ path: stringProperty('Relative directory path. Defaults to the repository root.'),
6
+ }),
7
+ tool('read_file', 'Read a bounded line range from a UTF-8 text file in the repository.', {
8
+ path: stringProperty('Relative file path.'),
9
+ start_line: integerProperty('First line, one-based.'),
10
+ end_line: integerProperty('Last line, inclusive. At most 1,000 lines per call.'),
11
+ }, ['path']),
12
+ tool('search_text', 'Search for literal text across repository text files.', {
13
+ query: stringProperty('Literal text to find.'),
14
+ path: stringProperty('Relative directory to search. Defaults to the repository root.'),
15
+ }, ['query']),
16
+ tool('create_file', 'Create a new UTF-8 text file. This fails if the path already exists.', {
17
+ path: stringProperty('Relative file path.'),
18
+ content: stringProperty('Complete text content for the new file.'),
19
+ }, ['path', 'content']),
20
+ tool('replace_text', 'Edit an existing file by replacing exact text. Read the file first and include enough old_text context to make the match unique.', {
21
+ path: stringProperty('Relative file path.'),
22
+ old_text: stringProperty('Exact existing text to replace.'),
23
+ new_text: stringProperty('Replacement text.'),
24
+ replace_all: { type: 'boolean', description: 'Replace every occurrence. Defaults to false.' },
25
+ }, ['path', 'old_text', 'new_text']),
26
+ tool('run_command', 'Request execution of a shell command from the repository root. The user must approve every call before it runs.', {
27
+ command: stringProperty('Exact shell command to show the user for approval.'),
28
+ timeout_ms: integerProperty('Timeout between 1,000 and 300,000 milliseconds.'),
29
+ }, ['command']),
30
+ ]);
31
+
32
+ export async function executeTool(name, rawArguments, context) {
33
+ const args = normalizeArguments(rawArguments);
34
+ const { workspace, approveCommand, onCommandOutput, commandRunner = runApprovedCommand } = context;
35
+ switch (name) {
36
+ case 'list_files':
37
+ return workspace.listFiles(optionalString(args.path, '.'));
38
+ case 'read_file':
39
+ return workspace.readFile(requiredString(args.path, 'path'), {
40
+ startLine: optionalInteger(args.start_line, 1),
41
+ endLine: optionalInteger(args.end_line, 400),
42
+ });
43
+ case 'search_text':
44
+ return workspace.searchText(requiredString(args.query, 'query'), optionalString(args.path, '.'));
45
+ case 'create_file':
46
+ return workspace.createFile(requiredString(args.path, 'path'), requiredString(args.content, 'content', { allowEmpty: true }));
47
+ case 'replace_text':
48
+ return workspace.replaceText(
49
+ requiredString(args.path, 'path'),
50
+ requiredString(args.old_text, 'old_text'),
51
+ requiredString(args.new_text, 'new_text', { allowEmpty: true }),
52
+ { replaceAll: args.replace_all === true },
53
+ );
54
+ case 'run_command': {
55
+ const result = await commandRunner({
56
+ command: requiredString(args.command, 'command'),
57
+ cwd: workspace.root,
58
+ approve: approveCommand,
59
+ timeoutMs: optionalInteger(args.timeout_ms, 120_000),
60
+ onOutput: onCommandOutput,
61
+ });
62
+ return result.approved ? result : { ...result, ok: false, error: 'Command denied by user.' };
63
+ }
64
+ default:
65
+ throw new Error(`Unknown tool: ${name}`);
66
+ }
67
+ }
68
+
69
+ function tool(name, description, properties, required = []) {
70
+ return {
71
+ type: 'function',
72
+ function: {
73
+ name,
74
+ description,
75
+ parameters: { type: 'object', properties, required, additionalProperties: false },
76
+ },
77
+ };
78
+ }
79
+
80
+ function stringProperty(description) {
81
+ return { type: 'string', description };
82
+ }
83
+
84
+ function integerProperty(description) {
85
+ return { type: 'integer', description };
86
+ }
87
+
88
+ function normalizeArguments(value) {
89
+ if (value === undefined || value === null) return {};
90
+ if (typeof value === 'object' && !Array.isArray(value)) return value;
91
+ if (typeof value === 'string') {
92
+ try {
93
+ const parsed = JSON.parse(value);
94
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
95
+ } catch {
96
+ throw new Error('Tool arguments are not valid JSON.');
97
+ }
98
+ }
99
+ throw new Error('Tool arguments must be an object.');
100
+ }
101
+
102
+ function requiredString(value, name, { allowEmpty = false } = {}) {
103
+ if (typeof value !== 'string' || (!allowEmpty && !value.trim())) throw new Error(`${name} must be a${allowEmpty ? '' : ' non-empty'} string.`);
104
+ return value;
105
+ }
106
+
107
+ function optionalString(value, fallback) {
108
+ return value === undefined ? fallback : requiredString(value, 'value');
109
+ }
110
+
111
+ function optionalInteger(value, fallback) {
112
+ if (value === undefined) return fallback;
113
+ if (!Number.isInteger(value)) throw new Error('Expected an integer value.');
114
+ return value;
115
+ }
package/src/cli.js ADDED
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import readline from 'node:readline/promises';
7
+ import { stdin as input, stdout as output, stderr as errorOutput } from 'node:process';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { runCodingAgent } from './coding-agent.js';
10
+ import { OllamaClient, OllamaError } from './ollama-client.js';
11
+ import { listPresets, resolveModel } from './models.js';
12
+ import { SessionStore } from './session-store.js';
13
+ import { Workspace } from './workspace.js';
14
+
15
+ const HELP = `aiforce-local - run open models locally through Ollama
16
+
17
+ Usage:
18
+ aiforce-local doctor
19
+ aiforce-local models
20
+ aiforce-local pull <deepseek|glm|qwen|ollama-tag>
21
+ aiforce-local run <prompt> [--model <alias|tag>]
22
+ aiforce-local chat [--model <alias|tag>]
23
+ aiforce-local agent <task> [--model <alias|tag>] [--cwd <path>]
24
+ aiforce-local agent <follow-up> --resume <session-id> [--cwd <path>]
25
+
26
+ Agent options:
27
+ --model, -m Model alias or exact Ollama tag (default: qwen)
28
+ --cwd, -c Repository directory (default: current directory)
29
+ --resume Resume a prior coding-agent session
30
+ --max-turns Tool-loop safety limit, 1-100 (default: 24)
31
+ --json Emit JSONL events; approvals still use the terminal
32
+
33
+ Environment:
34
+ OLLAMA_HOST Ollama server URL (default: http://127.0.0.1:11434)
35
+ AIFORCE_LOCAL_DATA_DIR Override the session-data directory
36
+ `;
37
+
38
+ export async function main(argv = process.argv.slice(2), dependencies = {}) {
39
+ const client = dependencies.client ?? new OllamaClient();
40
+ const [command = 'help', ...args] = argv;
41
+
42
+ switch (command) {
43
+ case 'help':
44
+ case '--help':
45
+ case '-h':
46
+ output.write(HELP);
47
+ return 0;
48
+ case 'doctor':
49
+ return doctor(client);
50
+ case 'models':
51
+ return models(client);
52
+ case 'pull':
53
+ return pull(client, args);
54
+ case 'run':
55
+ return run(client, args);
56
+ case 'chat':
57
+ return chat(client, args);
58
+ case 'agent':
59
+ case 'exec':
60
+ return agent(client, args, dependencies);
61
+ default:
62
+ throw new Error(`Unknown command "${command}". Run with --help for usage.`);
63
+ }
64
+ }
65
+
66
+ async function doctor(client) {
67
+ const info = await client.version();
68
+ output.write(`Ollama ${info.version ?? 'unknown'} at ${client.baseUrl}\n`);
69
+ output.write('Local inference has no per-token API charge.\n');
70
+ return 0;
71
+ }
72
+
73
+ async function models(client) {
74
+ const installed = new Set((await client.models()).map((model) => model.name));
75
+ for (const preset of listPresets()) {
76
+ output.write(`${preset.alias.padEnd(10)} ${preset.tag.padEnd(28)} ${installed.has(preset.tag) ? 'installed' : 'not installed'}\n`);
77
+ }
78
+ return 0;
79
+ }
80
+
81
+ async function pull(client, args) {
82
+ const model = resolveModel(args[0]);
83
+ output.write(`Pulling ${model}\n`);
84
+ for await (const event of client.pull(model)) {
85
+ if (event.total && event.completed) {
86
+ const percent = Math.floor((event.completed / event.total) * 100);
87
+ output.write(`\r${event.status ?? 'downloading'} ${percent}%`);
88
+ } else if (event.status) {
89
+ output.write(`\r${event.status}`);
90
+ }
91
+ }
92
+ output.write(`\nReady: ${model}\n`);
93
+ return 0;
94
+ }
95
+
96
+ async function run(client, args) {
97
+ const { model, positionals } = parseOptions(args);
98
+ const prompt = positionals.join(' ').trim();
99
+ if (!prompt) throw new Error('run requires a prompt.');
100
+ await streamAnswer(client, resolveModel(model ?? 'qwen'), [{ role: 'user', content: prompt }]);
101
+ return 0;
102
+ }
103
+
104
+ async function chat(client, args) {
105
+ const { model } = parseOptions(args);
106
+ const selected = resolveModel(model ?? 'qwen');
107
+ const terminal = readline.createInterface({ input, output });
108
+ const messages = [];
109
+ output.write(`Model: ${selected}. Type /exit to quit.\n`);
110
+ try {
111
+ while (true) {
112
+ const prompt = (await terminal.question('> ')).trim();
113
+ if (!prompt) continue;
114
+ if (prompt === '/exit') break;
115
+ messages.push({ role: 'user', content: prompt });
116
+ const answer = await streamAnswer(client, selected, messages);
117
+ messages.push({ role: 'assistant', content: answer });
118
+ }
119
+ } finally {
120
+ terminal.close();
121
+ }
122
+ return 0;
123
+ }
124
+
125
+ async function agent(client, args, dependencies) {
126
+ const options = parseAgentOptions(args);
127
+ const workspace = dependencies.workspace ?? await Workspace.open(options.cwd);
128
+ const sessionStore = dependencies.sessionStore ?? new SessionStore();
129
+ const selectedModel = options.model ? resolveModel(options.model) : options.resume ? undefined : resolveModel('qwen');
130
+ const emit = dependencies.emit ?? createEventRenderer({ json: options.json });
131
+ const approveCommand = dependencies.approveCommand ?? createTerminalApprover();
132
+ const task = options.positionals.join(' ').trim();
133
+ if (!task && !options.resume) throw new Error('agent requires a task or --resume session id.');
134
+ await runCodingAgent({
135
+ client,
136
+ workspace,
137
+ task,
138
+ model: selectedModel,
139
+ approveCommand,
140
+ sessionStore,
141
+ resumeSession: options.resume,
142
+ maxTurns: options.maxTurns,
143
+ emit,
144
+ commandRunner: dependencies.commandRunner,
145
+ });
146
+ return 0;
147
+ }
148
+
149
+ async function streamAnswer(client, model, messages) {
150
+ let answer = '';
151
+ for await (const event of client.chat({ model, messages })) {
152
+ const text = event.message?.content ?? '';
153
+ answer += text;
154
+ output.write(text);
155
+ }
156
+ output.write('\n');
157
+ return answer;
158
+ }
159
+
160
+ export function parseOptions(args) {
161
+ const positionals = [];
162
+ let model;
163
+ for (let index = 0; index < args.length; index += 1) {
164
+ const arg = args[index];
165
+ if (arg === '--model' || arg === '-m') {
166
+ model = args[index + 1];
167
+ if (!model) throw new Error(`${arg} requires a value.`);
168
+ index += 1;
169
+ } else {
170
+ positionals.push(arg);
171
+ }
172
+ }
173
+ return { model, positionals };
174
+ }
175
+
176
+ export function parseAgentOptions(args) {
177
+ const options = {
178
+ model: undefined,
179
+ cwd: process.cwd(),
180
+ resume: undefined,
181
+ maxTurns: 24,
182
+ json: false,
183
+ positionals: [],
184
+ };
185
+ for (let index = 0; index < args.length; index += 1) {
186
+ const arg = args[index];
187
+ if (arg === '--json') {
188
+ options.json = true;
189
+ continue;
190
+ }
191
+ const optionNames = new Map([
192
+ ['--model', 'model'], ['-m', 'model'],
193
+ ['--cwd', 'cwd'], ['-c', 'cwd'],
194
+ ['--resume', 'resume'],
195
+ ['--max-turns', 'maxTurns'],
196
+ ]);
197
+ const key = optionNames.get(arg);
198
+ if (!key) {
199
+ if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}`);
200
+ options.positionals.push(arg);
201
+ continue;
202
+ }
203
+ const value = args[index + 1];
204
+ if (!value) throw new Error(`${arg} requires a value.`);
205
+ index += 1;
206
+ options[key] = key === 'maxTurns' ? Number.parseInt(value, 10) : value;
207
+ }
208
+ if (!Number.isInteger(options.maxTurns) || options.maxTurns < 1 || options.maxTurns > 100) {
209
+ throw new Error('--max-turns must be an integer between 1 and 100.');
210
+ }
211
+ return options;
212
+ }
213
+
214
+ function createTerminalApprover() {
215
+ return async ({ command, cwd, timeoutMs }) => {
216
+ errorOutput.write(`\nCommand requested in ${cwd}:\n ${command}\nTimeout: ${timeoutMs}ms\n`);
217
+ if (!input.isTTY) {
218
+ errorOutput.write('Denied: command approval requires an interactive terminal.\n');
219
+ return false;
220
+ }
221
+ const terminal = readline.createInterface({ input, output: errorOutput });
222
+ try {
223
+ const answer = (await terminal.question('Approve this command? [y/N] ')).trim().toLowerCase();
224
+ return answer === 'y' || answer === 'yes';
225
+ } finally {
226
+ terminal.close();
227
+ }
228
+ };
229
+ }
230
+
231
+ function createEventRenderer({ json }) {
232
+ if (json) return (event) => output.write(`${JSON.stringify(event)}\n`);
233
+ return (event) => {
234
+ switch (event.type) {
235
+ case 'session':
236
+ errorOutput.write(`session: ${event.sessionId}\nmodel: ${event.model}\nrepository: ${event.root}\n`);
237
+ break;
238
+ case 'assistant_delta':
239
+ output.write(event.text);
240
+ break;
241
+ case 'tool_call':
242
+ errorOutput.write(`\n-> ${event.name} ${formatCompact(event.arguments)}\n`);
243
+ break;
244
+ case 'tool_result':
245
+ errorOutput.write(`<- ${event.name} ${formatCompact(event.result)}\n`);
246
+ break;
247
+ case 'command_output':
248
+ (event.stream === 'stderr' ? errorOutput : output).write(event.text);
249
+ break;
250
+ case 'final':
251
+ output.write('\n');
252
+ if (event.changes.length) {
253
+ output.write(`\nChanges (${event.changes.length}):\n`);
254
+ for (const change of event.changes) output.write(` ${change.status}: ${change.path}\n`);
255
+ if (event.diff) output.write(`\n${event.diff}`);
256
+ } else {
257
+ output.write('\nNo files changed.\n');
258
+ }
259
+ break;
260
+ default:
261
+ break;
262
+ }
263
+ };
264
+ }
265
+
266
+ function formatCompact(value) {
267
+ const text = typeof value === 'string' ? value : JSON.stringify(value);
268
+ return text.length > 500 ? `${text.slice(0, 500)}...` : text;
269
+ }
270
+
271
+ const isEntryPoint = (() => {
272
+ if (!process.argv[1]) return false;
273
+ try {
274
+ const entry = fs.realpathSync(path.resolve(process.argv[1]));
275
+ const module = fs.realpathSync(fileURLToPath(import.meta.url));
276
+ return process.platform === 'win32' ? entry.toLowerCase() === module.toLowerCase() : entry === module;
277
+ } catch {
278
+ return false;
279
+ }
280
+ })();
281
+ if (isEntryPoint) {
282
+ main().then(
283
+ (code) => { process.exitCode = code; },
284
+ (error) => {
285
+ const prefix = error instanceof OllamaError ? 'Ollama' : 'Error';
286
+ console.error(`${prefix}: ${error.message}`);
287
+ process.exitCode = 1;
288
+ },
289
+ );
290
+ }