@vornrun/connector-ollama 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/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@vornrun/connector-ollama`.
4
+
5
+ ## 0.1.0
6
+
7
+ First release.
8
+
9
+ Trigger a workflow when a model on a local Ollama server is added, updated or
10
+ loaded into memory, and let a workflow step chat, generate a completion,
11
+ embed text, and list, show, pull, copy or delete models.
12
+
13
+ - **Triggers:** `modelChanged`, `modelLoaded`.
14
+ - **Actions:** `chat`, `generate`, `embed`, `listModels`, `showModel`,
15
+ `listRunningModels`, `version`, `pullModel`, `deleteModel`, `copyModel`.
16
+ - **Signing in:** none. The one setting is the server URL from
17
+ `OLLAMA_HOST`, `http://localhost:11434` by default, with a trailing `/api`
18
+ stripped and a bare `host:port` given `http://`. A hosted or proxied server
19
+ takes its key from `OLLAMA_API_KEY` in the environment, sent as
20
+ `Authorization: Bearer` only when set; the SDK refuses a secret config
21
+ field on a connector with no sign-in, so a host that does not pass its
22
+ environment through cannot supply the key.
23
+
24
+ Every action and both polls go through one small client rather than declared
25
+ SDK requests, because a declared request cannot express what the API needs:
26
+ a 10 minute timeout on chat, generate and pull against 30 seconds elsewhere,
27
+ `stream: false` on every generation, a Bearer header read from the
28
+ environment, one retry after two seconds on a connection refused with the
29
+ note that the server may be starting, and the 404 `model '<name>' not found`
30
+ reported as `Model '<name>' is not present on the server; pull it first`
31
+ without ever pulling on the caller's behalf. Every other failure is reported
32
+ as the server's `error` message with the HTTP status, and every generation
33
+ returns the reply as `raw` beside the text, `doneReason` and the nanosecond
34
+ timings.
35
+
36
+ Both triggers poll on the SDK's timestamp dedupe strategy. Models are keyed
37
+ `<name>@<digest>` and stamped with `modified_at`, so a pull that changes a
38
+ digest fires again and the first poll delivers every model present. Loaded
39
+ models are keyed `<name>@<expires_at>` and stamped with `expires_at`, so a
40
+ model fires each time it appears with an expiry not yet seen. Both times are
41
+ reduced to UTC milliseconds for the cursor; the raw values stay on the item.
42
+
43
+ Ships as a pack with a conformance receipt covering the dedupe replay of both
44
+ triggers and the mock run of every action. No runtime dependencies: `fetch`,
45
+ `AbortController` and `JSON` cover the client.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @vornrun/connector-ollama
2
+
3
+ Trigger Vorn workflows when a model on a local Ollama server is added,
4
+ updated or loaded into memory, and chat, generate a completion, embed text,
5
+ and list, show, pull, copy or delete models from a workflow step. Talks to
6
+ the Ollama REST API at `http://localhost:11434/api` by default.
7
+
8
+ ## Signing in
9
+
10
+ Nothing to sign in with: Ollama binds `127.0.0.1:11434` and its API takes no
11
+ credentials, so the connector declares `auth: { rung: 'none' }`. The only
12
+ setting is the server URL.
13
+
14
+ | Setting | Environment | Required | What it does |
15
+ | --- | --- | --- | --- |
16
+ | Server URL | `OLLAMA_HOST` | no | The server origin, `http://localhost:11434` by default. A trailing `/` or `/api` is stripped, and a bare `host:port` such as `0.0.0.0:11434` gets `http://` in front, so the same value the `ollama` CLI reads works here. |
17
+
18
+ A hosted or reverse-proxied server that wants a key takes it from the
19
+ `OLLAMA_API_KEY` environment variable, sent as `Authorization: Bearer <key>`
20
+ only when it is set. There is no config field for it: the connector SDK
21
+ refuses a secret field on a connector that declares it needs no sign-in, so
22
+ the key rides on the environment. A host that does not pass its environment
23
+ through to the connector cannot supply it.
24
+
25
+ ## Errors, timeouts and retries
26
+
27
+ Every request is JSON and every reply is one JSON object: the connector sends
28
+ `stream: false` on chat, generate and pull, so an answer is never a stream of
29
+ lines. A failed call carries `{ "error": "<message>" }` and is reported as
30
+ `<message> (HTTP <status>)`. A model that is not on the server answers 404
31
+ `model '<name>' not found`, reported as `Model '<name>' is not present on the
32
+ server; pull it first (HTTP 404)`; the connector never pulls on the caller's
33
+ behalf. A `503` means the server is overloaded and is reported as is.
34
+
35
+ Chat, generate and pull can take minutes on a laptop and get a 10 minute
36
+ timeout; every other call gets 30 seconds. A connection refused is retried
37
+ once after two seconds, and a second refusal is reported as `Ollama did not
38
+ answer at <url>; the server may be starting or not running`. Nothing else is
39
+ retried; there are no rate limits, the server queues requests. Every
40
+ duration in a reply is nanoseconds and is passed through untouched.
41
+
42
+ ## Triggers
43
+
44
+ Both poll the whole list, since neither endpoint pages, and both dedupe on
45
+ the SDK's timestamp strategy with the time reduced to UTC milliseconds.
46
+
47
+ ### `modelChanged` — a model was added or updated
48
+
49
+ Polls `GET /api/tags` and fires once per model build: the item id is
50
+ `<name>@<digest>` stamped with `modified_at`, so a pull that updates a tag
51
+ fires again and the same build never fires twice. The first poll delivers
52
+ every model present. Each item carries `name`, `digest`, `size`,
53
+ `modified_at`, `family` and `parameter_size` beside the full `details`, a
54
+ title such as `qwen2.5-coder:7b (qwen2, 7.6B)`, and the library page as
55
+ `url` when the name has no namespace and no remote host.
56
+
57
+ ### `modelLoaded` — a model was loaded into memory
58
+
59
+ Polls `GET /api/ps` and fires when a model appears with an `expires_at` not
60
+ yet seen: the item id is `<name>@<expires_at>` stamped with `expires_at`.
61
+ Every request the model serves moves the expiry forward, so a busy model
62
+ fires again on each poll; the default workflow polls every five minutes, and
63
+ a slower interval reports fewer repeats. Each item carries `name`, `digest`,
64
+ `size`, `size_vram`, `expires_at`, `context_length` and `details`.
65
+
66
+ ## Actions
67
+
68
+ `model` is the `model:tag` name; the tag defaults to `latest`. `format` is
69
+ the word `json` or a JSON schema object, sent untouched; `options` is the
70
+ runtime options object such as `{ "temperature": 0.2, "num_predict": 256 }`;
71
+ `keepAlive` is sent as `keep_alive`, a duration such as `10m` or `0` to
72
+ unload at once.
73
+
74
+ | Action | Idempotent | What it does |
75
+ | --- | --- | --- |
76
+ | `chat` | no | `POST /api/chat` with `model`, `messages` (a single user text or a JSON array of `{ role, content }`), optional `system` (prepended as a system turn), `format`, `options`, `keepAlive`. Returns `content`, `thinking`, `toolCalls`, `doneReason`, `evalCount`, `promptEvalCount`, `totalDuration`, `loadDuration`, `promptEvalDuration`, `evalDuration`, `model`, `raw`. |
77
+ | `generate` | no | `POST /api/generate` with `model`, `prompt`, optional `system`, `format`, `options`, `keepAlive`. Returns `response`, `thinking` and the same timings and `raw`. |
78
+ | `embed` | yes | `POST /api/embed` with `model`, `input` (text or a JSON array of texts), optional `truncate`. Returns `embeddings`, `count`, `model`, `promptEvalCount`, `totalDuration`, `loadDuration`, `raw`. |
79
+ | `listModels` | yes | `GET /api/tags`. Returns `models`, `count`, `raw`. |
80
+ | `showModel` | yes | `POST /api/show` with `model`, optional `verbose`. Returns `model`, `details`, `capabilities`, `modifiedAt`, `parameters`, `template`, `license`, `modelInfo`, `raw`. |
81
+ | `listRunningModels` | yes | `GET /api/ps`. Returns `models`, `count`, `raw`. |
82
+ | `version` | yes | `GET /api/version`. Returns `version`. The live check probes this. |
83
+ | `pullModel` | no | `POST /api/pull` with `model`, optional `insecure`, `stream: false`. Returns the final `status` and `raw`. Downloads, so it is slow. |
84
+ | `deleteModel` | no | `DELETE /api/delete` with `model`. Returns `deleted`, `model`. A second call answers 404. |
85
+ | `copyModel` | no | `POST /api/copy` with `source`, `destination`. Returns `copied`, `source`, `destination`. |
86
+
87
+ A runner that does not serve embeddings answers `501 This server does not
88
+ support embeddings`; `showModel` lists `embedding` under `capabilities` for a
89
+ model that does.
90
+
91
+ ## Checks
92
+
93
+ From the repository root:
94
+
95
+ ```sh
96
+ yarn typecheck && yarn test && yarn build
97
+ node node_modules/@vornrun/connector-sdk/dist/cli.js check ./packages/ollama/dist/index.js --mock --receipt packages/ollama/verified.json
98
+ ```
99
+
100
+ `packages/ollama/scripts/check.sh` runs exactly this. Tests make no network
101
+ calls: the client takes an injected `fetch` and sleep, and the API key test
102
+ injects the environment.
103
+
104
+ `packages/ollama/scripts/check-live.sh` calls `GET /api/version` on
105
+ `OLLAMA_HOST` (default `http://localhost:11434`) and exits 0 with a note when
106
+ no server answers. With a server it lists models, shows `OLLAMA_MODEL`
107
+ (default `qwen2.5-coder:7b`), lists running models, embeds `hello` with
108
+ `OLLAMA_EMBED_MODEL` or the same model (a runner that does not serve
109
+ embeddings is reported as a note), then runs `vorn-connector check --live`
110
+ against the built package. Nothing is generated, pulled, deleted or copied.
111
+
112
+ ## Built from
113
+
114
+ The API reference was the only source. Two of its pages have moved; the new
115
+ address is listed beside the old one.
116
+
117
+ - API introduction: https://docs.ollama.com/api
118
+ - Generate a completion: https://docs.ollama.com/api/generate
119
+ - Generate a chat message: https://docs.ollama.com/api/chat
120
+ - Generate embeddings: https://docs.ollama.com/api/embed
121
+ - List models: https://docs.ollama.com/api/tags
122
+ - Show a model: https://docs.ollama.com/api/show (moved to https://docs.ollama.com/api-reference/show-model-details)
123
+ - List running models: https://docs.ollama.com/api/ps
124
+ - Pull a model: https://docs.ollama.com/api/pull
125
+ - Delete a model: https://docs.ollama.com/api/delete
126
+ - Copy a model: https://docs.ollama.com/api/copy
127
+ - Version: https://docs.ollama.com/api/version (moved to https://docs.ollama.com/api-reference/get-version)
128
+ - Structured outputs: https://docs.ollama.com/capabilities/structured-outputs
129
+ - Markdown source of the reference: https://github.com/ollama/ollama/blob/main/docs/api.md
130
+ - FAQ, `OLLAMA_HOST`, `keep_alive`, queueing and the 503: https://docs.ollama.com/faq
131
+ - Cloud, `OLLAMA_API_KEY` and `Authorization: Bearer`: https://docs.ollama.com/cloud
@@ -0,0 +1,52 @@
1
+ import * as _vornrun_connector_sdk from '@vornrun/connector-sdk';
2
+
3
+ declare const DEFAULT_BASE_URL = "http://localhost:11434";
4
+ /** Chat, generate and pull can take minutes on a laptop. */
5
+ declare const LONG_TIMEOUT_MS: number;
6
+ declare const DEFAULT_TIMEOUT_MS = 30000;
7
+ /** How long the one retry waits for a server that may be starting. */
8
+ declare const CONNECTION_RETRY_WAIT_MS = 2000;
9
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
10
+ type Sleep = (ms: number) => Promise<void>;
11
+ declare function normalizeBaseUrl(raw: unknown): string;
12
+ declare class OllamaApiError extends Error {
13
+ readonly status: number;
14
+ readonly detail: string;
15
+ constructor(status: number, body: unknown);
16
+ }
17
+ declare function describeFailure(detail: string): string;
18
+ interface RequestOptions {
19
+ body?: unknown;
20
+ timeoutMs?: number;
21
+ }
22
+ interface OllamaClient {
23
+ readonly baseUrl: string;
24
+ request<T = unknown>(method: string, path: string, options?: RequestOptions): Promise<T>;
25
+ get<T = unknown>(path: string): Promise<T>;
26
+ post<T = unknown>(path: string, body: unknown, timeoutMs?: number): Promise<T>;
27
+ }
28
+ interface OllamaClientOptions {
29
+ baseUrl?: string;
30
+ /** Sent as `Authorization: Bearer` only when set, for a hosted or proxied server. */
31
+ apiKey?: string;
32
+ /** Injected in tests, so nothing reaches the network. */
33
+ fetchImpl?: FetchLike;
34
+ /** Injected in tests, so the connection retry spends no real time. */
35
+ sleep?: Sleep;
36
+ }
37
+ declare function createOllamaClient(options?: OllamaClientOptions): OllamaClient;
38
+
39
+ declare const SAMPLE_MODEL_NAME = "qwen2.5-coder:7b";
40
+ interface OllamaConnectorOptions {
41
+ version?: string;
42
+ /** Injected in tests, so nothing reaches the network. */
43
+ fetchImpl?: FetchLike;
44
+ /** Injected in tests, so the connection retry spends no real time. */
45
+ sleep?: Sleep;
46
+ /** Where the optional API key and the preflight host are read from; defaults to the process environment. */
47
+ env?: NodeJS.ProcessEnv;
48
+ }
49
+ declare function createOllamaConnector(options?: OllamaConnectorOptions): _vornrun_connector_sdk.Connector;
50
+ declare const connector: _vornrun_connector_sdk.Connector;
51
+
52
+ export { CONNECTION_RETRY_WAIT_MS, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, LONG_TIMEOUT_MS, OllamaApiError, type OllamaClient, type OllamaClientOptions, type OllamaConnectorOptions, SAMPLE_MODEL_NAME, connector, createOllamaClient, createOllamaConnector, connector as default, describeFailure, normalizeBaseUrl };
package/dist/index.js ADDED
@@ -0,0 +1,827 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/connector.ts
4
+ import { defineConnector } from "@vornrun/connector-sdk";
5
+
6
+ // src/client.ts
7
+ var DEFAULT_BASE_URL = "http://localhost:11434";
8
+ var LONG_TIMEOUT_MS = 10 * 6e4;
9
+ var DEFAULT_TIMEOUT_MS = 3e4;
10
+ var CONNECTION_RETRY_WAIT_MS = 2e3;
11
+ var MAX_ERROR_BODY = 300;
12
+ var MODEL_NOT_FOUND = /^model '([^']*)' not found$/;
13
+ function normalizeBaseUrl(raw) {
14
+ let url = String(raw ?? "").trim();
15
+ if (url === "") return DEFAULT_BASE_URL;
16
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) url = `http://${url}`;
17
+ url = url.replace(/\/+$/, "");
18
+ url = url.replace(/\/api$/i, "");
19
+ return url.replace(/\/+$/, "");
20
+ }
21
+ var OllamaApiError = class extends Error {
22
+ status;
23
+ detail;
24
+ constructor(status, body) {
25
+ const detail = errorMessage(body);
26
+ super(`${describeFailure(detail)} (HTTP ${status})`);
27
+ this.name = "OllamaApiError";
28
+ this.status = status;
29
+ this.detail = detail;
30
+ }
31
+ };
32
+ function errorMessage(body) {
33
+ const error = body?.error;
34
+ if (typeof error === "string" && error.trim() !== "") return error.trim();
35
+ const raw = typeof body === "string" ? body : body === void 0 ? "" : JSON.stringify(body);
36
+ const trimmed = raw.trim();
37
+ if (trimmed === "") return "no body";
38
+ return trimmed.length > MAX_ERROR_BODY ? `${trimmed.slice(0, MAX_ERROR_BODY)}\u2026` : trimmed;
39
+ }
40
+ function describeFailure(detail) {
41
+ const missing = MODEL_NOT_FOUND.exec(detail);
42
+ return missing ? `Model '${missing[1]}' is not present on the server; pull it first` : detail;
43
+ }
44
+ function isConnectionRefused(error) {
45
+ let current = error;
46
+ for (let depth = 0; depth < 4 && typeof current === "object" && current !== null; depth += 1) {
47
+ const candidate = current;
48
+ if (candidate.code === "ECONNREFUSED") return true;
49
+ if (Array.isArray(candidate.errors) && candidate.errors.some(isConnectionRefused)) return true;
50
+ current = candidate.cause;
51
+ }
52
+ return false;
53
+ }
54
+ var defaultSleep = (ms) => new Promise((resolve) => {
55
+ setTimeout(resolve, ms);
56
+ });
57
+ var defaultFetch = (input, init) => globalThis.fetch(input, init);
58
+ async function readBody(response) {
59
+ const text2 = await response.text();
60
+ if (text2 === "") return void 0;
61
+ try {
62
+ return JSON.parse(text2);
63
+ } catch {
64
+ return text2;
65
+ }
66
+ }
67
+ function createOllamaClient(options = {}) {
68
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
69
+ const fetchImpl = options.fetchImpl ?? defaultFetch;
70
+ const sleep = options.sleep ?? defaultSleep;
71
+ const apiKey = String(options.apiKey ?? "").trim();
72
+ const headers = {
73
+ accept: "application/json",
74
+ ...apiKey !== "" && { authorization: `Bearer ${apiKey}` }
75
+ };
76
+ async function send(url, init, timeoutMs, route) {
77
+ const controller = new AbortController();
78
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
79
+ timer.unref?.();
80
+ try {
81
+ return await fetchImpl(url, { ...init, signal: controller.signal });
82
+ } catch (error) {
83
+ if (controller.signal.aborted) {
84
+ throw new Error(`Ollama did not answer ${route} within ${Math.round(timeoutMs / 1e3)}s`);
85
+ }
86
+ throw error;
87
+ } finally {
88
+ clearTimeout(timer);
89
+ }
90
+ }
91
+ async function request(method, path, opts = {}) {
92
+ const url = `${baseUrl}/api/${path.replace(/^\//, "")}`;
93
+ const route = `${method.toUpperCase()} /api/${path.replace(/^\//, "")}`;
94
+ const init = {
95
+ method: method.toUpperCase(),
96
+ headers: opts.body === void 0 ? headers : { ...headers, "content-type": "application/json" },
97
+ ...opts.body !== void 0 && { body: JSON.stringify(opts.body) }
98
+ };
99
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
100
+ let response;
101
+ try {
102
+ response = await send(url, init, timeoutMs, route);
103
+ } catch (error) {
104
+ if (!isConnectionRefused(error)) throw error;
105
+ await sleep(CONNECTION_RETRY_WAIT_MS);
106
+ try {
107
+ response = await send(url, init, timeoutMs, route);
108
+ } catch (again) {
109
+ if (!isConnectionRefused(again)) throw again;
110
+ throw new Error(`Ollama did not answer at ${baseUrl}; the server may be starting or not running`);
111
+ }
112
+ }
113
+ const body = await readBody(response);
114
+ if (!response.ok) throw new OllamaApiError(response.status, body);
115
+ return body;
116
+ }
117
+ return {
118
+ baseUrl,
119
+ request,
120
+ get: (path) => request("GET", path),
121
+ post: (path, body, timeoutMs) => request("POST", path, { body, ...timeoutMs !== void 0 && { timeoutMs } })
122
+ };
123
+ }
124
+
125
+ // src/items.ts
126
+ var SAMPLE_MODEL = {
127
+ name: "qwen2.5-coder:7b",
128
+ model: "qwen2.5-coder:7b",
129
+ modified_at: "2026-08-02T16:07:41.209152383-06:00",
130
+ size: 4683087561,
131
+ digest: "dae161e27b0e90dd1856c8bb3209201fd6736d8eb66298e75ed87571486f4364",
132
+ details: {
133
+ parent_model: "",
134
+ format: "gguf",
135
+ family: "qwen2",
136
+ families: ["qwen2"],
137
+ parameter_size: "7.6B",
138
+ quantization_level: "Q4_K_M"
139
+ }
140
+ };
141
+ var SAMPLE_RUNNING_MODEL = {
142
+ name: "qwen2.5-coder:7b",
143
+ model: "qwen2.5-coder:7b",
144
+ size: 4740716952,
145
+ size_vram: 4740716952,
146
+ digest: "dae161e27b0e90dd1856c8bb3209201fd6736d8eb66298e75ed87571486f4364",
147
+ details: SAMPLE_MODEL.details,
148
+ expires_at: "2026-09-10T07:31:16.885215-06:00",
149
+ context_length: 4096
150
+ };
151
+ function text(value) {
152
+ const trimmed = String(value ?? "").trim();
153
+ return trimmed === "" ? void 0 : trimmed;
154
+ }
155
+ function flag(value) {
156
+ return value === true || /^(true|1|yes)$/i.test(String(value ?? "").trim());
157
+ }
158
+ function textOrJsonArray(value) {
159
+ if (Array.isArray(value)) return value;
160
+ const raw = String(value ?? "");
161
+ if (raw.trim().startsWith("[")) {
162
+ try {
163
+ const parsed = JSON.parse(raw);
164
+ if (Array.isArray(parsed)) return parsed;
165
+ } catch {
166
+ }
167
+ }
168
+ return raw;
169
+ }
170
+ function jsonObject(value, key) {
171
+ if (value === void 0 || value === null || value === "") return void 0;
172
+ if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${key} must be a JSON object`);
173
+ return value;
174
+ }
175
+ function messagesArg(value, system) {
176
+ const parsed = typeof value === "object" && value !== null && !Array.isArray(value) ? [] : textOrJsonArray(value);
177
+ const turns = typeof parsed === "string" ? [{ role: "user", content: parsed }] : parsed.filter((turn) => typeof turn === "object" && turn !== null && !Array.isArray(turn));
178
+ if (system !== void 0 && turns[0]?.role !== "system") return [{ role: "system", content: system }, ...turns];
179
+ return turns;
180
+ }
181
+ function formatArg(value) {
182
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) return value;
183
+ const raw = text(value);
184
+ if (raw === void 0) return void 0;
185
+ if (raw.startsWith("{")) {
186
+ try {
187
+ const parsed = JSON.parse(raw);
188
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
189
+ } catch {
190
+ }
191
+ }
192
+ return raw;
193
+ }
194
+ function keepAliveArg(value) {
195
+ const raw = text(value);
196
+ if (raw === void 0) return void 0;
197
+ return /^-?\d+$/.test(raw) ? Number(raw) : raw;
198
+ }
199
+ function isoOf(value) {
200
+ const at = Date.parse(String(value ?? ""));
201
+ return Number.isNaN(at) ? void 0 : new Date(at).toISOString();
202
+ }
203
+ function gigabytes(bytes) {
204
+ return `${((bytes ?? 0) / 1e9).toFixed(1)} GB`;
205
+ }
206
+ function libraryUrl(model) {
207
+ const name = model.name ?? model.model ?? "";
208
+ if (name === "" || name.includes("/") || text(model.remote_host) !== void 0) return void 0;
209
+ return `https://ollama.com/library/${encodeURIComponent(name.split(":")[0])}`;
210
+ }
211
+ function modelToItem(model) {
212
+ const name = model.name ?? model.model ?? "";
213
+ const digest = model.digest ?? "";
214
+ const family = text(model.details?.family);
215
+ const size = text(model.details?.parameter_size);
216
+ const detail = [family, size].filter((part) => part !== void 0).join(", ");
217
+ const url = libraryUrl(model);
218
+ const updatedAt = isoOf(model.modified_at);
219
+ return {
220
+ externalId: `${name}@${digest}`,
221
+ title: detail ? `${name} (${detail})` : name,
222
+ ...url && { url },
223
+ ...updatedAt && { updatedAt },
224
+ data: {
225
+ name,
226
+ digest,
227
+ size: model.size ?? 0,
228
+ modified_at: model.modified_at ?? null,
229
+ family: family ?? null,
230
+ parameter_size: size ?? null,
231
+ details: model.details ?? {}
232
+ }
233
+ };
234
+ }
235
+ function runningToItem(model) {
236
+ const name = model.name ?? model.model ?? "";
237
+ const expires = model.expires_at ?? "";
238
+ const updatedAt = isoOf(model.expires_at);
239
+ return {
240
+ externalId: `${name}@${expires}`,
241
+ title: `${name} loaded (${gigabytes(model.size_vram ?? model.size)} in VRAM, context ${model.context_length ?? 0})`,
242
+ ...updatedAt && { updatedAt },
243
+ data: {
244
+ name,
245
+ digest: model.digest ?? "",
246
+ size: model.size ?? 0,
247
+ size_vram: model.size_vram ?? 0,
248
+ expires_at: model.expires_at ?? null,
249
+ context_length: model.context_length ?? 0,
250
+ details: model.details ?? {}
251
+ }
252
+ };
253
+ }
254
+
255
+ // package.json
256
+ var package_default = {
257
+ name: "@vornrun/connector-ollama",
258
+ version: "0.1.0",
259
+ description: "Trigger workflows when a local Ollama model is added, updated or loaded into memory, and chat, generate, embed, and list, show, pull, copy or delete models from a step.",
260
+ type: "module",
261
+ license: "MIT",
262
+ author: "Javier Canizalez <javier-canizalez@outlook.com>",
263
+ repository: {
264
+ type: "git",
265
+ url: "git+https://github.com/vorn-run/connectors.git",
266
+ directory: "packages/ollama"
267
+ },
268
+ keywords: [
269
+ "vorn",
270
+ "connector",
271
+ "ollama",
272
+ "llm",
273
+ "mcp"
274
+ ],
275
+ bin: {
276
+ "vorn-connector-ollama": "dist/index.js"
277
+ },
278
+ main: "./dist/index.js",
279
+ types: "./dist/index.d.ts",
280
+ exports: {
281
+ ".": {
282
+ types: "./dist/index.d.ts",
283
+ default: "./dist/index.js"
284
+ }
285
+ },
286
+ files: [
287
+ "dist",
288
+ "README.md",
289
+ "CHANGELOG.md"
290
+ ],
291
+ scripts: {
292
+ build: "tsup",
293
+ typecheck: "tsc --noEmit",
294
+ test: "vitest run"
295
+ },
296
+ dependencies: {
297
+ "@vornrun/connector-sdk": "^0.7.0-beta.14"
298
+ },
299
+ devDependencies: {
300
+ "@types/node": "^22.10.2",
301
+ "@vitest/coverage-v8": "^4.1.10",
302
+ tsup: "^8.5.1",
303
+ typescript: "^6.0.3",
304
+ vitest: "^4.1.10"
305
+ },
306
+ vorn: {
307
+ category: "AI",
308
+ keywords: [
309
+ "ollama",
310
+ "llm",
311
+ "local models",
312
+ "chat",
313
+ "generate",
314
+ "embeddings",
315
+ "qwen",
316
+ "llama",
317
+ "gemma"
318
+ ],
319
+ auth: "Nothing to sign in with: Ollama serves http://localhost:11434 without credentials. Set OLLAMA_HOST for another server and OLLAMA_API_KEY for a hosted one.",
320
+ packs: true
321
+ }
322
+ };
323
+
324
+ // src/connector.ts
325
+ var SAMPLE_MODEL_NAME = "qwen2.5-coder:7b";
326
+ var MODEL_INPUT = {
327
+ key: "model",
328
+ label: "Model",
329
+ required: true,
330
+ description: `The model name in model:tag form; the tag defaults to latest. listModels returns what is present.`,
331
+ builderHint: `A name that is not on the server answers 404 and is reported as "not present; pull it first"; nothing is pulled on the caller's behalf.`
332
+ };
333
+ var SYSTEM_INPUT = {
334
+ key: "system",
335
+ label: "System prompt",
336
+ description: "Instructions for the model, kept apart from the conversation.",
337
+ builderHint: "On chat it is prepended as a system turn unless the messages already start with one; on generate it is sent as system."
338
+ };
339
+ var FORMAT_INPUT = {
340
+ key: "format",
341
+ label: "Format",
342
+ description: "json for any valid JSON, or a JSON schema object the answer must match.",
343
+ builderHint: "Sent as format untouched. With a schema, also describe it in the prompt so the model grounds its answer; the reply is text, so parse content yourself."
344
+ };
345
+ var OPTIONS_INPUT = {
346
+ key: "options",
347
+ label: "Options",
348
+ type: "json",
349
+ description: 'Runtime options such as { "temperature": 0.2, "num_predict": 256 }.',
350
+ builderHint: "Passed through as options: temperature, num_predict, seed, top_k, top_p, min_p, stop, num_ctx and any other Modelfile parameter."
351
+ };
352
+ var KEEP_ALIVE_INPUT = {
353
+ key: "keepAlive",
354
+ label: "Keep alive",
355
+ description: "How long the model stays loaded after the reply, such as 10m; 0 unloads it at once.",
356
+ builderHint: "Sent as keep_alive. A duration string or a number of seconds; a negative number keeps it loaded until the server stops."
357
+ };
358
+ var TIMING_OUTPUTS = [
359
+ { key: "evalCount", type: "number", description: "Tokens generated in the reply" },
360
+ { key: "promptEvalCount", type: "number", description: "Prompt tokens evaluated" },
361
+ { key: "totalDuration", type: "number", description: "Whole call in nanoseconds" },
362
+ { key: "loadDuration", type: "number", description: "Loading the model, in nanoseconds" },
363
+ { key: "promptEvalDuration", type: "number", description: "Evaluating the prompt, in nanoseconds" },
364
+ { key: "evalDuration", type: "number", description: "Generating the reply, in nanoseconds" },
365
+ { key: "model", description: "The model that answered" },
366
+ { key: "raw", description: "The whole response as the server returned it" }
367
+ ];
368
+ function timings(body) {
369
+ return {
370
+ doneReason: body.done_reason ?? null,
371
+ evalCount: body.eval_count ?? 0,
372
+ promptEvalCount: body.prompt_eval_count ?? 0,
373
+ totalDuration: body.total_duration ?? 0,
374
+ loadDuration: body.load_duration ?? 0,
375
+ promptEvalDuration: body.prompt_eval_duration ?? 0,
376
+ evalDuration: body.eval_duration ?? 0,
377
+ model: body.model ?? null,
378
+ raw: body
379
+ };
380
+ }
381
+ function createOllamaConnector(options = {}) {
382
+ const env = options.env ?? process.env;
383
+ function clientFor(config, fetchImpl) {
384
+ return createOllamaClient({
385
+ baseUrl: normalizeBaseUrl(config.baseUrl),
386
+ ...text(env.OLLAMA_API_KEY) && { apiKey: text(env.OLLAMA_API_KEY) },
387
+ fetchImpl: options.fetchImpl ?? fetchImpl,
388
+ ...options.sleep && { sleep: options.sleep }
389
+ });
390
+ }
391
+ function requiredArg(args, key) {
392
+ const value = text(args[key]);
393
+ if (value === void 0) throw new Error(`${key} is required`);
394
+ return value;
395
+ }
396
+ async function fetchModels(context) {
397
+ const body = await clientFor(context.config, context.fetch).get("tags");
398
+ return (Array.isArray(body?.models) ? body.models : []).filter((model) => text(model?.name ?? model?.model) !== void 0).sort((left, right) => String(left.modified_at ?? "").localeCompare(String(right.modified_at ?? ""))).map(modelToItem);
399
+ }
400
+ async function fetchRunningModels(context) {
401
+ const body = await clientFor(context.config, context.fetch).get("ps");
402
+ return (Array.isArray(body?.models) ? body.models : []).filter((model) => text(model?.name ?? model?.model) !== void 0).sort((left, right) => String(left.expires_at ?? "").localeCompare(String(right.expires_at ?? ""))).map(runningToItem);
403
+ }
404
+ return defineConnector({
405
+ id: "ollama",
406
+ name: "Ollama",
407
+ version: options.version ?? package_default.version,
408
+ description: "Trigger workflows when a local Ollama model is added, updated or loaded into memory, and chat, generate, embed, and list, show, pull, copy or delete models from a step.",
409
+ // The llama's face as the spec draws it: a rounded head, two tall ears, and the eyes and muzzle
410
+ // punched through by reversed winding, since the icon carries path data only and no fill-rule.
411
+ icon: {
412
+ viewBox: "0 0 24 24",
413
+ paths: [
414
+ [
415
+ "M9 8H15A5 5 0 0 1 20 13V17A5 5 0 0 1 15 22H9A5 5 0 0 1 4 17V13A5 5 0 0 1 9 8Z",
416
+ "M6 3.5A1.5 1.5 0 0 1 7.5 2A1.5 1.5 0 0 1 9 3.5V10H6Z",
417
+ "M15 3.5A1.5 1.5 0 0 1 16.5 2A1.5 1.5 0 0 1 18 3.5V10H15Z",
418
+ "M8.3 14A1.2 1.2 0 0 0 10.7 14A1.2 1.2 0 0 0 8.3 14Z",
419
+ "M13.3 14A1.2 1.2 0 0 0 15.7 14A1.2 1.2 0 0 0 13.3 14Z",
420
+ "M11.25 17.5A1.25 1.25 0 0 0 10 18.75A1.25 1.25 0 0 0 11.25 20H12.75A1.25 1.25 0 0 0 14 18.75A1.25 1.25 0 0 0 12.75 17.5Z"
421
+ ].join("")
422
+ ]
423
+ },
424
+ auth: { rung: "none" },
425
+ config: [
426
+ {
427
+ key: "baseUrl",
428
+ env: "OLLAMA_HOST",
429
+ label: "Server URL",
430
+ default: DEFAULT_BASE_URL,
431
+ description: `The server origin, ${DEFAULT_BASE_URL} by default. A trailing / or /api is stripped and a bare host:port gets http://.`,
432
+ builderHint: "The same variable the ollama CLI reads. A hosted or proxied server that wants a key takes it from OLLAMA_API_KEY in the environment, sent as Authorization: Bearer; the SDK refuses a secret field on a connector that needs no sign-in, so there is no config field for it."
433
+ }
434
+ ],
435
+ async preflight() {
436
+ const client = clientFor({ baseUrl: env.OLLAMA_HOST });
437
+ try {
438
+ const body = await client.get("version");
439
+ return { ok: true, message: `Ollama ${body?.version ?? "unknown version"} answered at ${client.baseUrl}` };
440
+ } catch (error) {
441
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
442
+ }
443
+ },
444
+ triggers: [
445
+ {
446
+ type: "modelChanged",
447
+ label: "A model was added or updated",
448
+ description: "Fires once per model build from GET /api/tags: a new name, or a pull that changed a digest. The first poll delivers every model present.",
449
+ defaultWorkflow: { name: "Ollama: added or updated models", defaultCronFromMinutes: 15 },
450
+ dedupe: "timestamp",
451
+ sample: [modelToItem(SAMPLE_MODEL)],
452
+ fetch: fetchModels
453
+ },
454
+ {
455
+ type: "modelLoaded",
456
+ label: "A model was loaded into memory",
457
+ description: "Fires when a model appears in GET /api/ps with an expiry not yet seen. Every request extends the expiry, so a busy model fires again on each poll; poll slowly.",
458
+ defaultWorkflow: { name: "Ollama: loaded models", defaultCronFromMinutes: 5 },
459
+ dedupe: "timestamp",
460
+ sample: [runningToItem(SAMPLE_RUNNING_MODEL)],
461
+ fetch: fetchRunningModels
462
+ }
463
+ ],
464
+ actions: [
465
+ {
466
+ type: "chat",
467
+ label: "Chat",
468
+ description: "Generate the next assistant message of a conversation with POST /api/chat. Every call runs a generation.",
469
+ idempotent: false,
470
+ inputs: [
471
+ MODEL_INPUT,
472
+ {
473
+ key: "messages",
474
+ label: "Messages",
475
+ required: true,
476
+ description: 'A single user text, or a JSON array of { "role", "content" } with role system, user, assistant or tool.',
477
+ builderHint: 'Text becomes [{ role: "user", content: text }]; a value starting with [ is parsed as the array and passed through, so images and tool_calls on a turn survive.'
478
+ },
479
+ SYSTEM_INPUT,
480
+ FORMAT_INPUT,
481
+ OPTIONS_INPUT,
482
+ KEEP_ALIVE_INPUT
483
+ ],
484
+ outputs: [
485
+ { key: "content", description: "The assistant message text" },
486
+ { key: "thinking", description: "The thinking text when the model produced one, else null" },
487
+ { key: "toolCalls", description: "The tool_calls array when the model made any, else null" },
488
+ { key: "doneReason", description: "stop, length or load" },
489
+ ...TIMING_OUTPUTS
490
+ ],
491
+ async run(args, context) {
492
+ const system = text(args.system);
493
+ const format = formatArg(args.format);
494
+ const modelOptions = jsonObject(args.options, "options");
495
+ const keepAlive = keepAliveArg(args.keepAlive);
496
+ const body = await clientFor(context.config, context.fetch).post(
497
+ "chat",
498
+ {
499
+ model: requiredArg(args, "model"),
500
+ messages: messagesArg(args.messages, system),
501
+ ...format !== void 0 && { format },
502
+ ...modelOptions && { options: modelOptions },
503
+ ...keepAlive !== void 0 && { keep_alive: keepAlive },
504
+ stream: false
505
+ },
506
+ LONG_TIMEOUT_MS
507
+ );
508
+ const reply = body ?? {};
509
+ return {
510
+ content: reply.message?.content ?? "",
511
+ thinking: reply.message?.thinking ?? null,
512
+ toolCalls: reply.message?.tool_calls ?? null,
513
+ ...timings(reply)
514
+ };
515
+ }
516
+ },
517
+ {
518
+ type: "generate",
519
+ label: "Generate a completion",
520
+ description: "Generate a response for a prompt with POST /api/generate. Every call runs a generation.",
521
+ idempotent: false,
522
+ inputs: [
523
+ MODEL_INPUT,
524
+ {
525
+ key: "prompt",
526
+ label: "Prompt",
527
+ required: true,
528
+ description: "Text for the model to generate a response from.",
529
+ builderHint: "Sent as prompt. Use chat for a conversation with roles; this is the raw completion endpoint."
530
+ },
531
+ SYSTEM_INPUT,
532
+ FORMAT_INPUT,
533
+ OPTIONS_INPUT,
534
+ KEEP_ALIVE_INPUT
535
+ ],
536
+ outputs: [
537
+ { key: "response", description: "The generated text" },
538
+ { key: "thinking", description: "The thinking text when the model produced one, else null" },
539
+ { key: "doneReason", description: "stop, length or load" },
540
+ ...TIMING_OUTPUTS
541
+ ],
542
+ async run(args, context) {
543
+ const system = text(args.system);
544
+ const format = formatArg(args.format);
545
+ const modelOptions = jsonObject(args.options, "options");
546
+ const keepAlive = keepAliveArg(args.keepAlive);
547
+ const body = await clientFor(context.config, context.fetch).post(
548
+ "generate",
549
+ {
550
+ model: requiredArg(args, "model"),
551
+ prompt: requiredArg(args, "prompt"),
552
+ ...system && { system },
553
+ ...format !== void 0 && { format },
554
+ ...modelOptions && { options: modelOptions },
555
+ ...keepAlive !== void 0 && { keep_alive: keepAlive },
556
+ stream: false
557
+ },
558
+ LONG_TIMEOUT_MS
559
+ );
560
+ const reply = body ?? {};
561
+ return { response: reply.response ?? "", thinking: reply.thinking ?? null, ...timings(reply) };
562
+ }
563
+ },
564
+ {
565
+ type: "embed",
566
+ label: "Generate embeddings",
567
+ description: "Turn text into embedding vectors with POST /api/embed. The same input and model give the same vectors.",
568
+ idempotent: true,
569
+ sample: { model: SAMPLE_MODEL_NAME, input: "hello" },
570
+ inputs: [
571
+ {
572
+ ...MODEL_INPUT,
573
+ builderHint: `${MODEL_INPUT.builderHint} A runner that does not serve embeddings answers 501; showModel lists embedding under capabilities when it does.`
574
+ },
575
+ {
576
+ key: "input",
577
+ label: "Input",
578
+ required: true,
579
+ description: "Text, or a JSON array of texts, to embed in one call.",
580
+ builderHint: "A value starting with [ is parsed as the array; one vector comes back per entry, in order."
581
+ },
582
+ {
583
+ key: "truncate",
584
+ label: "Truncate",
585
+ type: "boolean",
586
+ description: "Truncate an input that exceeds the context window instead of failing. The server default is true.",
587
+ builderHint: "Sent as truncate only when set; false makes an oversized input an error."
588
+ }
589
+ ],
590
+ outputs: [
591
+ { key: "embeddings", description: "One number array per input, in input order" },
592
+ { key: "count", type: "number", description: "How many vectors" },
593
+ { key: "model", description: "The model used" },
594
+ { key: "promptEvalCount", type: "number", description: "Input tokens processed" },
595
+ { key: "totalDuration", type: "number", description: "Whole call in nanoseconds" },
596
+ { key: "loadDuration", type: "number", description: "Loading the model, in nanoseconds" },
597
+ { key: "raw", description: "The whole response as the server returned it" }
598
+ ],
599
+ async run(args, context) {
600
+ const truncate = text(args.truncate);
601
+ const body = await clientFor(context.config, context.fetch).post("embed", {
602
+ model: requiredArg(args, "model"),
603
+ input: textOrJsonArray(args.input),
604
+ ...truncate !== void 0 && { truncate: flag(truncate) }
605
+ });
606
+ const embeddings = Array.isArray(body?.embeddings) ? body.embeddings : [];
607
+ return {
608
+ embeddings,
609
+ count: embeddings.length,
610
+ model: body?.model ?? null,
611
+ promptEvalCount: body?.prompt_eval_count ?? 0,
612
+ totalDuration: body?.total_duration ?? 0,
613
+ loadDuration: body?.load_duration ?? 0,
614
+ raw: body ?? {}
615
+ };
616
+ }
617
+ },
618
+ {
619
+ type: "listModels",
620
+ label: "List models",
621
+ description: "List the models on the server with GET /api/tags.",
622
+ idempotent: true,
623
+ sample: {},
624
+ outputs: [
625
+ { key: "models", description: "Array of { name, model, modified_at, size, digest, details }" },
626
+ { key: "count", type: "number", description: "How many models" },
627
+ { key: "raw", description: "The whole response as the server returned it" }
628
+ ],
629
+ async run(_args, context) {
630
+ const body = await clientFor(context.config, context.fetch).get("tags");
631
+ const models = Array.isArray(body?.models) ? body.models : [];
632
+ return { models, count: models.length, raw: body ?? {} };
633
+ }
634
+ },
635
+ {
636
+ type: "showModel",
637
+ label: "Show a model",
638
+ description: "Read a model\u2019s details, capabilities, parameters, template and license with POST /api/show.",
639
+ idempotent: true,
640
+ sample: { model: SAMPLE_MODEL_NAME },
641
+ inputs: [
642
+ MODEL_INPUT,
643
+ {
644
+ key: "verbose",
645
+ label: "Verbose",
646
+ type: "boolean",
647
+ description: "Include the large fields, such as the full tokenizer, in modelInfo.",
648
+ builderHint: "Sent as verbose only when true; the reply can run to megabytes."
649
+ }
650
+ ],
651
+ outputs: [
652
+ { key: "model", description: "The model name asked for; the reply carries none" },
653
+ { key: "details", description: "{ parent_model, format, family, families, parameter_size, quantization_level }" },
654
+ { key: "capabilities", description: "Array such as completion, tools, vision, embedding" },
655
+ { key: "modifiedAt", description: "When the model was last modified, ISO 8601" },
656
+ { key: "parameters", description: "Model parameter settings as text" },
657
+ { key: "template", description: "The prompt template" },
658
+ { key: "license", description: "The license text" },
659
+ { key: "modelInfo", description: "Additional model metadata" },
660
+ { key: "raw", description: "The whole response as the server returned it" }
661
+ ],
662
+ async run(args, context) {
663
+ const model = requiredArg(args, "model");
664
+ const body = await clientFor(context.config, context.fetch).post("show", {
665
+ model,
666
+ ...flag(args.verbose) && { verbose: true }
667
+ });
668
+ return {
669
+ model,
670
+ details: body?.details ?? {},
671
+ capabilities: Array.isArray(body?.capabilities) ? body.capabilities : [],
672
+ modifiedAt: body?.modified_at ?? null,
673
+ parameters: body?.parameters ?? "",
674
+ template: body?.template ?? "",
675
+ license: body?.license ?? "",
676
+ modelInfo: body?.model_info ?? {},
677
+ raw: body ?? {}
678
+ };
679
+ }
680
+ },
681
+ {
682
+ type: "listRunningModels",
683
+ label: "List running models",
684
+ description: "List the models loaded into memory with GET /api/ps.",
685
+ idempotent: true,
686
+ sample: {},
687
+ outputs: [
688
+ { key: "models", description: "Array of { name, model, size, digest, details, expires_at, size_vram, context_length }" },
689
+ { key: "count", type: "number", description: "How many models are loaded" },
690
+ { key: "raw", description: "The whole response as the server returned it" }
691
+ ],
692
+ async run(_args, context) {
693
+ const body = await clientFor(context.config, context.fetch).get("ps");
694
+ const models = Array.isArray(body?.models) ? body.models : [];
695
+ return { models, count: models.length, raw: body ?? {} };
696
+ }
697
+ },
698
+ {
699
+ type: "version",
700
+ label: "Get the server version",
701
+ description: "Read the server version with GET /api/version. The live check probes this.",
702
+ idempotent: true,
703
+ sample: {},
704
+ outputs: [{ key: "version", description: "The server version, such as 0.12.6" }],
705
+ async run(_args, context) {
706
+ const body = await clientFor(context.config, context.fetch).get("version");
707
+ return { version: body?.version ?? null };
708
+ }
709
+ },
710
+ {
711
+ type: "pullModel",
712
+ label: "Pull a model",
713
+ description: "Download a model from the library with POST /api/pull. Downloads, so it is slow and not repeatable in a live check.",
714
+ idempotent: false,
715
+ inputs: [
716
+ { ...MODEL_INPUT, builderHint: "A library name such as gemma3, or user/model for a shared one. A cancelled pull resumes where it left off." },
717
+ {
718
+ key: "insecure",
719
+ label: "Insecure",
720
+ type: "boolean",
721
+ description: "Allow downloading over an insecure connection.",
722
+ builderHint: "Sent as insecure only when true; only for a library you run yourself during development."
723
+ }
724
+ ],
725
+ outputs: [
726
+ { key: "status", description: "The final status, success when the model is present" },
727
+ { key: "raw", description: "The whole response as the server returned it" }
728
+ ],
729
+ async run(args, context) {
730
+ const body = await clientFor(context.config, context.fetch).post(
731
+ "pull",
732
+ { model: requiredArg(args, "model"), ...flag(args.insecure) && { insecure: true }, stream: false },
733
+ LONG_TIMEOUT_MS
734
+ );
735
+ return { status: body?.status ?? null, raw: body ?? {} };
736
+ }
737
+ },
738
+ {
739
+ type: "deleteModel",
740
+ label: "Delete a model",
741
+ description: "Delete a model and its data with DELETE /api/delete. A second call answers 404.",
742
+ idempotent: false,
743
+ inputs: [{ ...MODEL_INPUT, builderHint: "Deleting a tag another name was copied from leaves the copy in place; layers are shared." }],
744
+ outputs: [
745
+ { key: "deleted", type: "boolean", description: "True once the server answered 200" },
746
+ { key: "model", description: "The model that was deleted" }
747
+ ],
748
+ async run(args, context) {
749
+ const model = requiredArg(args, "model");
750
+ await clientFor(context.config, context.fetch).request("DELETE", "delete", { body: { model } });
751
+ return { deleted: true, model };
752
+ }
753
+ },
754
+ {
755
+ type: "copyModel",
756
+ label: "Copy a model",
757
+ description: "Create a new name for an existing model with POST /api/copy. Creates a tag, so it is not repeatable.",
758
+ idempotent: false,
759
+ inputs: [
760
+ {
761
+ key: "source",
762
+ label: "Source",
763
+ required: true,
764
+ description: "Existing model name to copy from.",
765
+ builderHint: "A missing source answers 404 and is reported as not present."
766
+ },
767
+ {
768
+ key: "destination",
769
+ label: "Destination",
770
+ required: true,
771
+ description: "New model name to create.",
772
+ builderHint: "model:tag form; an existing name is overwritten."
773
+ }
774
+ ],
775
+ outputs: [
776
+ { key: "copied", type: "boolean", description: "True once the server answered 200" },
777
+ { key: "source", description: "The name copied from" },
778
+ { key: "destination", description: "The name created" }
779
+ ],
780
+ async run(args, context) {
781
+ const source = requiredArg(args, "source");
782
+ const destination = requiredArg(args, "destination");
783
+ await clientFor(context.config, context.fetch).post("copy", { source, destination });
784
+ return { copied: true, source, destination };
785
+ }
786
+ }
787
+ ]
788
+ });
789
+ }
790
+ var connector = createOllamaConnector();
791
+
792
+ // src/entry.ts
793
+ import { realpathSync } from "fs";
794
+ import { fileURLToPath } from "url";
795
+ import { serveConnector } from "@vornrun/connector-sdk";
796
+ function isEntryPoint(moduleUrl, argv = process.argv) {
797
+ const invoked = argv[1];
798
+ if (invoked === void 0) return false;
799
+ try {
800
+ return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(invoked);
801
+ } catch {
802
+ return false;
803
+ }
804
+ }
805
+ async function serveIfEntryPoint(moduleUrl, serve = serveConnector) {
806
+ if (!isEntryPoint(moduleUrl)) return false;
807
+ await serve(connector);
808
+ return true;
809
+ }
810
+
811
+ // src/index.ts
812
+ var index_default = connector;
813
+ await serveIfEntryPoint(import.meta.url);
814
+ export {
815
+ CONNECTION_RETRY_WAIT_MS,
816
+ DEFAULT_BASE_URL,
817
+ DEFAULT_TIMEOUT_MS,
818
+ LONG_TIMEOUT_MS,
819
+ OllamaApiError,
820
+ SAMPLE_MODEL_NAME,
821
+ connector,
822
+ createOllamaClient,
823
+ createOllamaConnector,
824
+ index_default as default,
825
+ describeFailure,
826
+ normalizeBaseUrl
827
+ };
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@vornrun/connector-ollama",
3
+ "version": "0.1.0",
4
+ "description": "Trigger workflows when a local Ollama model is added, updated or loaded into memory, and chat, generate, embed, and list, show, pull, copy or delete models from a step.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Javier Canizalez <javier-canizalez@outlook.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vorn-run/connectors.git",
11
+ "directory": "packages/ollama"
12
+ },
13
+ "keywords": [
14
+ "vorn",
15
+ "connector",
16
+ "ollama",
17
+ "llm",
18
+ "mcp"
19
+ ],
20
+ "bin": {
21
+ "vorn-connector-ollama": "dist/index.js"
22
+ },
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "CHANGELOG.md"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "vitest run"
40
+ },
41
+ "dependencies": {
42
+ "@vornrun/connector-sdk": "^0.7.0-beta.14"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.10.2",
46
+ "@vitest/coverage-v8": "^4.1.10",
47
+ "tsup": "^8.5.1",
48
+ "typescript": "^6.0.3",
49
+ "vitest": "^4.1.10"
50
+ },
51
+ "vorn": {
52
+ "category": "AI",
53
+ "keywords": [
54
+ "ollama",
55
+ "llm",
56
+ "local models",
57
+ "chat",
58
+ "generate",
59
+ "embeddings",
60
+ "qwen",
61
+ "llama",
62
+ "gemma"
63
+ ],
64
+ "auth": "Nothing to sign in with: Ollama serves http://localhost:11434 without credentials. Set OLLAMA_HOST for another server and OLLAMA_API_KEY for a hosted one.",
65
+ "packs": true
66
+ }
67
+ }