@graphorin/embedder-ollama 0.5.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @graphorin/embedder-ollama
2
2
 
3
+ ## 0.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#142](https://github.com/o-stepper/graphorin/pull/142) [`436d6ca`](https://github.com/o-stepper/graphorin/commit/436d6ca5ebbd16df094e915682d3915c279a8430) Thanks [@o-stepper](https://github.com/o-stepper)! - Version constants and version-bearing strings now derive from each package's manifest at build time (`VERSION = pkg.version`; writer ids, client/server info, OTLP framework attributes, build-info metrics interpolate it). No behavioral change at the current version: the rendered strings are byte-identical. A release bump no longer edits source; the new `check-version-consistency` gate fails any reintroduced hardcoded framework version.
8
+
9
+ - Updated dependencies [[`436d6ca`](https://github.com/o-stepper/graphorin/commit/436d6ca5ebbd16df094e915682d3915c279a8430)]:
10
+ - @graphorin/core@0.6.1
11
+
12
+ ## 0.6.0
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [[`32f20c1`](https://github.com/o-stepper/graphorin/commit/32f20c110f184f8cef7eec85bf39f5f07c886cb6), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627)]:
17
+ - @graphorin/core@0.6.0
18
+
19
+ ## 0.5.0
20
+
21
+ First version published to the npm registry (with Sigstore build
22
+ provenance). The 0.2.0, 0.3.0, and 0.4.0 versions were internal lockstep
23
+ milestones and were never published. All `@graphorin/*` packages release
24
+ lockstep at the same version; the full release notes for 0.2.0-0.5.0 live
25
+ in the repository-level
26
+ [CHANGELOG](https://github.com/o-stepper/graphorin/blob/main/CHANGELOG.md).
27
+
3
28
  ## 0.1.0
4
29
 
5
30
  ### Minor Changes
package/README.md CHANGED
@@ -4,15 +4,27 @@
4
4
 
5
5
  `@graphorin/embedder-ollama` wraps the local Ollama HTTP API to produce
6
6
  dense embeddings without the bundled-model overhead of
7
- `@graphorin/embedder-transformersjs`. The package targets four models
8
- out of the box:
7
+ `@graphorin/embedder-transformersjs`. The package ships known output
8
+ dimensions for ten common Ollama embedding models out of the box
9
+ (`KNOWN_OLLAMA_MODEL_DIMS`); any other model works too by passing an
10
+ explicit `dim`:
9
11
 
10
12
  | Model | Dim | Notes |
11
13
  |---|---:|---|
12
14
  | `nomic-embed-text` (default) | 768 | Multilingual; the de-facto Ollama default. |
13
15
  | `mxbai-embed-large` | 1024 | Strong English; competitive with cloud peers. |
14
16
  | `snowflake-arctic-embed` | 1024 | Strong English. |
17
+ | `snowflake-arctic-embed2` | 1024 | Multilingual successor to arctic-embed. |
15
18
  | `bge-m3` | 1024 | Multilingual; same family also ships via the transformers.js adapter. |
19
+ | `bge-large` | 1024 | Strong English. |
20
+ | `embeddinggemma` | 768 | Multilingual (Google). |
21
+ | `paraphrase-multilingual` | 768 | Multilingual paraphrase family. |
22
+ | `all-minilm` | 384 | Compact English. |
23
+ | `granite-embedding` | 384 | Compact (IBM). |
24
+
25
+ Multi-size models (e.g. tags whose dimension depends on the `:0.6b` /
26
+ `:4b` / `:8b` variant) are deliberately omitted from the known-dims
27
+ map so an ambiguous bind fails loudly instead of baking a wrong width.
16
28
 
17
29
  ## Install
18
30
 
@@ -40,19 +52,19 @@ console.log(embedder.id(), embedder.dim(), vec.length);
40
52
 
41
53
  ## Trust + privacy
42
54
 
43
- The embedder operates against the same `LocalProviderTrust`
44
- classifier as the Ollama LLM provider adapter (Phase 06). When
45
- `baseUrl` resolves to a loopback host (`localhost`, `127.0.0.1`,
46
- `::1`), the trust class is permanent `loopback` and accepts every
47
- sensitivity tier (`'public'`, `'internal'`, `'secret'`). Remote /
48
- private-network hosts trigger a one-time WARN per process and the
49
- runtime caller may pass an explicit `acceptsSensitivity` override.
55
+ The embedder itself performs no trust classification - it is a thin
56
+ client for whatever `baseUrl` you give it, and it never talks to
57
+ anything else. Point it at a loopback Ollama (`http://127.0.0.1:11434`,
58
+ the default) and embeddings never leave the machine. Trust
59
+ classification and sensitivity gating for LLM traffic live one layer
60
+ up, in `@graphorin/provider`'s `LocalProviderTrust` classifier; apply
61
+ the same judgement before pointing this embedder at a remote host.
50
62
 
51
63
  ## Versioning of `embedder_id`
52
64
 
53
65
  The canonical id includes the Ollama model digest discovered via
54
66
  `POST /api/show` at construction time. A model upgrade in the same
55
- Ollama instance produces a different digest and therefore a
67
+ Ollama instance produces a different digest - and therefore a
56
68
  different `embedder_id`. The default `lock-on-first` policy in
57
69
  `@graphorin/store-sqlite` then fires the same migration path the
58
70
  existing `transformersjs` swap takes.
@@ -63,4 +75,4 @@ MIT © 2026 Oleksiy Stepurenko.
63
75
 
64
76
  ---
65
77
 
66
- **Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
78
+ **Project Graphorin** · v0.6.1 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
package/dist/index.d.ts CHANGED
@@ -2,8 +2,7 @@ import { EmbedOptions, EmbedderProvider } from "@graphorin/core/contracts";
2
2
 
3
3
  //#region src/index.d.ts
4
4
 
5
- /** Canonical version constant. Mirrors the `package.json` version. */
6
- declare const VERSION = "0.5.0";
5
+ declare const VERSION: string;
7
6
  /**
8
7
  * Default Ollama base URL. Operators that run Ollama on a non-default
9
8
  * port pass an explicit `baseUrl`.
@@ -103,10 +102,10 @@ declare function createOllamaEmbedder(options?: OllamaEmbedderOptions): OllamaEm
103
102
  declare class OllamaEmbedder implements EmbedderProvider {
104
103
  #private;
105
104
  constructor(options: OllamaEmbedderOptions);
106
- /** The canonical embedder id `'ollama:<model>@<dim-or-digest>'`. */
105
+ /** The canonical embedder id - `'ollama:<model>@<dim-or-digest>'`. */
107
106
  id(): string;
108
107
  /**
109
- * Output dimension the explicit `dim` option, the resolved width from the
108
+ * Output dimension - the explicit `dim` option, the resolved width from the
110
109
  * first `embed()`, or a known-family default. PS-11: throws for an unknown
111
110
  * model with no `dim` hint instead of returning `0` (which the store would
112
111
  * persist and use to create a `float[0]` vec0 table, silently breaking
@@ -115,7 +114,7 @@ declare class OllamaEmbedder implements EmbedderProvider {
115
114
  */
116
115
  dim(): number;
117
116
  /**
118
- * Deterministic hash over the embedder's full configuration
117
+ * Deterministic hash over the embedder's full configuration -
119
118
  * including the discovered digest. A model upgrade in the same
120
119
  * Ollama instance changes the digest (and therefore the hash), so
121
120
  * `lock-on-first` correctly fires a migration path instead of
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;AAoFA;AAkBa,cA3FA,OAAA,GAsGX,OAAA;AASF;AASA;;;;;AAwFa,cAxMA,uBAAA,GAwMA,wBAAA;;;;AAyJb;;;cAzVa,oBAAA;;;;;;UAOI,qBAAA;;;;;;;;;;;;;;;;8BAgBa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkCjB,mBAAA,SAA4B,KAAA;;;;;;;;;;;;;;cAkB5B,yBAAyB;;;;;;;;iBAoBtB,oBAAA,WAA8B,wBAA6B;;;;;;cAS9D,cAAA,YAA0B;;uBAchB;;;;;;;;;;;;;;;;;;;;eAwEZ,8BACD,eACL,QAAQ,cAAc;;;;;;;;;iBAyJX,mBAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;AAsFa,cAzEA,OAyEoB,EAAA,MAAA;AAkBjC;AAoBA;AASA;;;;AAwF2B,cAxMd,uBAAA,GAwMc,wBAAA;;;;;AAyJ3B;;cAzVa,oBAAA;;;;;;UAOI,qBAAA;;;;;;;;;;;;;;;;8BAgBa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkCjB,mBAAA,SAA4B,KAAA;;;;;;;;;;;;;;cAkB5B,yBAAyB;;;;;;;;iBAoBtB,oBAAA,WAA8B,wBAA6B;;;;;;cAS9D,cAAA,YAA0B;;uBAchB;;;;;;;;;;;;;;;;;;;;eAwEZ,8BACD,eACL,QAAQ,cAAc;;;;;;;;;iBAyJX,mBAAA"}
package/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
+ import { version } from "./package.js";
1
2
  import { createHash } from "node:crypto";
2
3
 
3
4
  //#region src/index.ts
4
5
  /**
5
- * @graphorin/embedder-ollama first-class opt-in alternative embedder
6
+ * @graphorin/embedder-ollama - first-class opt-in alternative embedder
6
7
  * for the Graphorin framework. Wraps the local Ollama HTTP API.
7
8
  *
8
9
  * @packageDocumentation
9
10
  */
10
- /** Canonical version constant. Mirrors the `package.json` version. */
11
- const VERSION = "0.5.0";
11
+ /** Canonical version constant, derived from `package.json` at build time. */
12
+ const VERSION = version;
12
13
  /**
13
14
  * Default Ollama base URL. Operators that run Ollama on a non-default
14
15
  * port pass an explicit `baseUrl`.
@@ -98,21 +99,21 @@ var OllamaEmbedder = class {
98
99
  * Build the `{ signal }` fetch-init fragment, combining the caller's
99
100
  * abort signal (if any) with the per-request timeout so a hung Ollama
100
101
  * daemon cannot stall the embed call. Returns `{}` when neither
101
- * applies. Call once per fetch each call mints a fresh timeout.
102
+ * applies. Call once per fetch - each call mints a fresh timeout.
102
103
  */
103
104
  #signalInit(opts) {
104
105
  const timeoutSignal = this.#timeoutMs > 0 ? AbortSignal.timeout(this.#timeoutMs) : void 0;
105
106
  const signal = opts.signal !== void 0 && timeoutSignal !== void 0 ? AbortSignal.any([opts.signal, timeoutSignal]) : opts.signal ?? timeoutSignal;
106
107
  return signal === void 0 ? {} : { signal };
107
108
  }
108
- /** The canonical embedder id `'ollama:<model>@<dim-or-digest>'`. */
109
+ /** The canonical embedder id - `'ollama:<model>@<dim-or-digest>'`. */
109
110
  id() {
110
111
  const dim = this.dim();
111
112
  const digest = this.#digest !== null ? `:${this.#digest.slice(0, 12)}` : "";
112
113
  return `ollama:${this.#model}@${dim}${digest}`;
113
114
  }
114
115
  /**
115
- * Output dimension the explicit `dim` option, the resolved width from the
116
+ * Output dimension - the explicit `dim` option, the resolved width from the
116
117
  * first `embed()`, or a known-family default. PS-11: throws for an unknown
117
118
  * model with no `dim` hint instead of returning `0` (which the store would
118
119
  * persist and use to create a `float[0]` vec0 table, silently breaking
@@ -126,7 +127,7 @@ var OllamaEmbedder = class {
126
127
  throw new OllamaEmbedderError(`[graphorin/embedder-ollama] unknown output dimension for model '${this.#model}'. Pass \`dim\` to ollamaEmbedder({ model, dim }), or call embed() once to resolve it from a response, before registering the embedder.`);
127
128
  }
128
129
  /**
129
- * Deterministic hash over the embedder's full configuration
130
+ * Deterministic hash over the embedder's full configuration -
130
131
  * including the discovered digest. A model upgrade in the same
131
132
  * Ollama instance changes the digest (and therefore the hash), so
132
133
  * `lock-on-first` correctly fires a migration path instead of
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["statusCode?: number","KNOWN_OLLAMA_MODEL_DIMS: ReadonlyMap<string, number>","#model","#baseUrl","#fetchImpl","#skipDigestProbe","#embedPath","#legacyEmbedPath","#showPath","#timeoutMs","#digest","#resolvedDim","#ensureInitialized","#callEmbed","#initialized","#initializing","#probeDigest","resp: Response","#signalInit","#legacyEmbedPerText","out: Float32Array[]"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @graphorin/embedder-ollama — first-class opt-in alternative embedder\n * for the Graphorin framework. Wraps the local Ollama HTTP API.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport type { EmbedderProvider, EmbedOptions } from '@graphorin/core/contracts';\n\n/** Canonical version constant. Mirrors the `package.json` version. */\nexport const VERSION = '0.5.0';\n\n/**\n * Default Ollama base URL. Operators that run Ollama on a non-default\n * port pass an explicit `baseUrl`.\n *\n * @stable\n */\nexport const DEFAULT_OLLAMA_BASE_URL = 'http://127.0.0.1:11434';\n\n/**\n * Default Ollama model. Matches the de-facto choice in the Ollama\n * community for general-purpose multilingual embeddings.\n *\n * @stable\n */\nexport const DEFAULT_OLLAMA_MODEL = 'nomic-embed-text';\n\n/**\n * Options accepted by {@link createOllamaEmbedder}.\n *\n * @stable\n */\nexport interface OllamaEmbedderOptions {\n /** Default `'nomic-embed-text'`. */\n readonly model?: string;\n /** Default `'http://127.0.0.1:11434'`. */\n readonly baseUrl?: string;\n /**\n * Optional dimensionality hint. When known up-front, the canonical\n * id is stable from the first `embed()` call instead of being\n * resolved from the response.\n */\n readonly dim?: number;\n /**\n * Override `fetch`. Used by the test suite to inject a mock HTTP\n * fixture. Production callers should leave this unset so the\n * embedder uses the platform's `globalThis.fetch`.\n */\n readonly fetchImpl?: typeof fetch;\n /**\n * If `true`, skip the `POST /api/show` model-digest probe at\n * construction. Used in test fixtures where the digest is\n * pre-populated.\n */\n readonly skipDigestProbe?: boolean;\n /**\n * Optional pre-resolved digest (e.g. from a probe done elsewhere).\n * When set, the embedder uses this value instead of issuing a\n * `POST /api/show` request.\n */\n readonly digest?: string;\n /** Optional API path override (default `'/api/embed'`). */\n readonly embedPath?: string;\n /** Optional API path override (default `'/api/embeddings'`). */\n readonly legacyEmbedPath?: string;\n /** Optional API path override (default `'/api/show'`). */\n readonly showPath?: string;\n /**\n * Per-request hard timeout in milliseconds. Default `30000`. Each\n * HTTP call (`/api/show`, `/api/embed`, legacy `/api/embeddings`) is\n * aborted if the Ollama daemon does not respond in time, so a hung\n * daemon never stalls the caller. A per-call {@link EmbedOptions.signal}\n * is combined with this timeout. Set to `0` to disable.\n */\n readonly timeoutMs?: number;\n}\n\n/**\n * Raised when the Ollama HTTP API returns a non-2xx response.\n *\n * @stable\n */\nexport class OllamaEmbedderError extends Error {\n override readonly name = 'OllamaEmbedderError';\n constructor(\n message: string,\n public readonly statusCode?: number,\n options?: { cause?: unknown },\n ) {\n super(message, options);\n }\n}\n\n/**\n * Model -> output-dimension hints used to seed the canonical id before the\n * first `embed()` resolves the real width from a response. Only single-width\n * families are listed; size-variant families (e.g. `qwen3-embedding`, whose\n * dim depends on the `:0.6b` / `:4b` / `:8b` tag) are deliberately omitted so\n * an ambiguous bind fails loudly rather than baking a wrong width (PS-11).\n */\nexport const KNOWN_OLLAMA_MODEL_DIMS: ReadonlyMap<string, number> = new Map([\n ['nomic-embed-text', 768],\n ['mxbai-embed-large', 1024],\n ['snowflake-arctic-embed', 1024],\n ['snowflake-arctic-embed2', 1024],\n ['bge-m3', 1024],\n ['bge-large', 1024],\n ['embeddinggemma', 768],\n ['all-minilm', 384],\n ['granite-embedding', 384],\n ['paraphrase-multilingual', 768],\n]);\n\n/**\n * Build an Ollama-backed embedder. The first `embed()` call issues a\n * `POST /api/show` to capture the model digest; subsequent calls hit\n * the embedding endpoint directly.\n *\n * @stable\n */\nexport function createOllamaEmbedder(options: OllamaEmbedderOptions = {}): OllamaEmbedder {\n return new OllamaEmbedder(options);\n}\n\n/**\n * `EmbedderProvider` implementation that talks to the Ollama HTTP API.\n *\n * @stable\n */\nexport class OllamaEmbedder implements EmbedderProvider {\n readonly #model: string;\n readonly #baseUrl: string;\n readonly #fetchImpl: typeof fetch;\n readonly #skipDigestProbe: boolean;\n readonly #embedPath: string;\n readonly #legacyEmbedPath: string;\n readonly #showPath: string;\n readonly #timeoutMs: number;\n #digest: string | null;\n #resolvedDim: number | null;\n #initialized = false;\n #initializing: Promise<void> | null = null;\n\n constructor(options: OllamaEmbedderOptions) {\n this.#model = options.model ?? DEFAULT_OLLAMA_MODEL;\n this.#baseUrl = stripTrailingSlashesLocal(options.baseUrl ?? DEFAULT_OLLAMA_BASE_URL);\n this.#fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.#skipDigestProbe = options.skipDigestProbe ?? false;\n this.#embedPath = options.embedPath ?? '/api/embed';\n this.#legacyEmbedPath = options.legacyEmbedPath ?? '/api/embeddings';\n this.#showPath = options.showPath ?? '/api/show';\n this.#timeoutMs = options.timeoutMs ?? 30_000;\n this.#digest = options.digest ?? null;\n this.#resolvedDim = options.dim ?? KNOWN_OLLAMA_MODEL_DIMS.get(this.#model) ?? null;\n }\n\n /**\n * Build the `{ signal }` fetch-init fragment, combining the caller's\n * abort signal (if any) with the per-request timeout so a hung Ollama\n * daemon cannot stall the embed call. Returns `{}` when neither\n * applies. Call once per fetch — each call mints a fresh timeout.\n */\n #signalInit(opts: EmbedOptions): { signal?: AbortSignal } {\n const timeoutSignal = this.#timeoutMs > 0 ? AbortSignal.timeout(this.#timeoutMs) : undefined;\n const signal =\n opts.signal !== undefined && timeoutSignal !== undefined\n ? AbortSignal.any([opts.signal, timeoutSignal])\n : (opts.signal ?? timeoutSignal);\n return signal === undefined ? {} : { signal };\n }\n\n /** The canonical embedder id — `'ollama:<model>@<dim-or-digest>'`. */\n id(): string {\n const dim = this.dim();\n const digest = this.#digest !== null ? `:${this.#digest.slice(0, 12)}` : '';\n return `ollama:${this.#model}@${dim}${digest}`;\n }\n\n /**\n * Output dimension — the explicit `dim` option, the resolved width from the\n * first `embed()`, or a known-family default. PS-11: throws for an unknown\n * model with no `dim` hint instead of returning `0` (which the store would\n * persist and use to create a `float[0]` vec0 table, silently breaking\n * vector search). Pass `dim` for any model not in {@link\n * KNOWN_OLLAMA_MODEL_DIMS}, or call `embed()` once first to resolve it.\n */\n dim(): number {\n if (this.#resolvedDim !== null) return this.#resolvedDim;\n const known = KNOWN_OLLAMA_MODEL_DIMS.get(this.#model);\n if (known !== undefined) return known;\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] unknown output dimension for model '${this.#model}'. ` +\n 'Pass `dim` to ollamaEmbedder({ model, dim }), or call embed() once to resolve it ' +\n 'from a response, before registering the embedder.',\n );\n }\n\n /**\n * Deterministic hash over the embedder's full configuration —\n * including the discovered digest. A model upgrade in the same\n * Ollama instance changes the digest (and therefore the hash), so\n * `lock-on-first` correctly fires a migration path instead of\n * silently reusing the same `embedder_id`.\n */\n configHash(): string {\n return canonicalConfigHash({\n adapter: 'ollama',\n model: this.#model,\n baseUrl: this.#baseUrl,\n digest: this.#digest ?? null,\n dim: this.#resolvedDim ?? null,\n });\n }\n\n async embed(\n texts: ReadonlyArray<string>,\n opts: EmbedOptions = {},\n ): Promise<ReadonlyArray<Float32Array>> {\n if (texts.length === 0) return [];\n await this.#ensureInitialized(opts);\n return this.#callEmbed(texts, opts);\n }\n\n async #ensureInitialized(opts: EmbedOptions): Promise<void> {\n if (this.#initialized) return;\n if (this.#initializing !== null) return this.#initializing;\n const inflight = (async () => {\n try {\n if (!this.#skipDigestProbe && this.#digest === null) {\n await this.#probeDigest(opts);\n }\n this.#initialized = true;\n } finally {\n // Always clear the in-flight slot so a transient failure does\n // not poison every subsequent call with the same rejection.\n this.#initializing = null;\n }\n })();\n this.#initializing = inflight;\n return inflight;\n }\n\n async #probeDigest(opts: EmbedOptions): Promise<void> {\n const url = `${this.#baseUrl}${this.#showPath}`;\n let resp: Response;\n try {\n resp = await this.#fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ name: this.#model }),\n ...this.#signalInit(opts),\n });\n } catch (err) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] failed to reach Ollama at ${url}`,\n undefined,\n { cause: err },\n );\n }\n if (!resp.ok) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] /api/show returned ${resp.status} for model '${this.#model}'`,\n resp.status,\n );\n }\n const json = (await resp.json()) as { digest?: string };\n if (typeof json.digest === 'string' && json.digest.length > 0) {\n this.#digest = json.digest;\n }\n }\n\n async #callEmbed(\n texts: ReadonlyArray<string>,\n opts: EmbedOptions,\n ): Promise<ReadonlyArray<Float32Array>> {\n const batchUrl = `${this.#baseUrl}${this.#embedPath}`;\n const batchBody = JSON.stringify({ model: this.#model, input: [...texts] });\n let resp: Response;\n try {\n resp = await this.#fetchImpl(batchUrl, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: batchBody,\n ...this.#signalInit(opts),\n });\n } catch (err) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] failed to reach Ollama at ${batchUrl}`,\n undefined,\n { cause: err },\n );\n }\n if (resp.status === 404) {\n // Older Ollama versions only expose the legacy single-input path.\n return this.#legacyEmbedPerText(texts, opts);\n }\n if (!resp.ok) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] /api/embed returned ${resp.status}`,\n resp.status,\n );\n }\n const json = (await resp.json()) as { embeddings?: number[][] };\n const list = json.embeddings;\n if (!Array.isArray(list) || list.length !== texts.length) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] /api/embed response had unexpected shape`,\n );\n }\n const out: Float32Array[] = [];\n for (let i = 0; i < list.length; i++) {\n const row = list[i];\n if (!Array.isArray(row)) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] embedding row ${i} is not an array`,\n );\n }\n const f32 = Float32Array.from(row);\n if (this.#resolvedDim === null) {\n this.#resolvedDim = f32.length;\n } else if (this.#resolvedDim !== f32.length) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] embedding dim drifted: expected ${this.#resolvedDim}, got ${f32.length}`,\n );\n }\n out.push(f32);\n }\n return out;\n }\n\n async #legacyEmbedPerText(\n texts: ReadonlyArray<string>,\n opts: EmbedOptions,\n ): Promise<ReadonlyArray<Float32Array>> {\n const url = `${this.#baseUrl}${this.#legacyEmbedPath}`;\n const out: Float32Array[] = [];\n for (const text of texts) {\n const resp = await this.#fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ model: this.#model, prompt: text }),\n ...this.#signalInit(opts),\n });\n if (!resp.ok) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] /api/embeddings returned ${resp.status}`,\n resp.status,\n );\n }\n const json = (await resp.json()) as { embedding?: number[] };\n if (!Array.isArray(json.embedding)) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] /api/embeddings response has no 'embedding' array`,\n );\n }\n const f32 = Float32Array.from(json.embedding);\n if (this.#resolvedDim === null) this.#resolvedDim = f32.length;\n out.push(f32);\n }\n return out;\n }\n}\n\n/**\n * Canonical-JSON deterministic hash over an embedder configuration.\n * Object keys are sorted lexicographically so the resulting hash is\n * stable across `JSON.stringify` reorderings.\n *\n * @stable\n */\nexport function canonicalConfigHash(config: unknown): string {\n return createHash('sha256').update(canonicalize(config), 'utf8').digest('hex');\n}\n\nfunction canonicalize(value: unknown): string {\n if (value === null) return 'null';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n if (typeof value === 'number') return Number.isFinite(value) ? value.toString() : 'null';\n if (typeof value === 'string') return JSON.stringify(value);\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(',')}]`;\n }\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n const keys = Object.keys(obj).sort();\n return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(',')}}`;\n }\n return 'null';\n}\n\n/**\n * Strip every trailing `/` from a URL string in `O(n)`. Kept regex-free\n * so CodeQL does not flag the (otherwise harmless) `/+` anchored\n * quantifier as a polynomial-redos vector.\n */\nfunction stripTrailingSlashesLocal(url: string): string {\n let end = url.length;\n while (end > 0 && url.charCodeAt(end - 1) === 0x2f) end -= 1;\n return end === url.length ? url : url.slice(0, end);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["VERSION: string","pkg.version","statusCode?: number","KNOWN_OLLAMA_MODEL_DIMS: ReadonlyMap<string, number>","#model","#baseUrl","#fetchImpl","#skipDigestProbe","#embedPath","#legacyEmbedPath","#showPath","#timeoutMs","#digest","#resolvedDim","#ensureInitialized","#callEmbed","#initialized","#initializing","#probeDigest","resp: Response","#signalInit","#legacyEmbedPerText","out: Float32Array[]"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @graphorin/embedder-ollama - first-class opt-in alternative embedder\n * for the Graphorin framework. Wraps the local Ollama HTTP API.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport type { EmbedderProvider, EmbedOptions } from '@graphorin/core/contracts';\n\n/** Canonical version constant, derived from `package.json` at build time. */\nimport pkg from '../package.json' with { type: 'json' };\n\nexport const VERSION: string = pkg.version;\n\n/**\n * Default Ollama base URL. Operators that run Ollama on a non-default\n * port pass an explicit `baseUrl`.\n *\n * @stable\n */\nexport const DEFAULT_OLLAMA_BASE_URL = 'http://127.0.0.1:11434';\n\n/**\n * Default Ollama model. Matches the de-facto choice in the Ollama\n * community for general-purpose multilingual embeddings.\n *\n * @stable\n */\nexport const DEFAULT_OLLAMA_MODEL = 'nomic-embed-text';\n\n/**\n * Options accepted by {@link createOllamaEmbedder}.\n *\n * @stable\n */\nexport interface OllamaEmbedderOptions {\n /** Default `'nomic-embed-text'`. */\n readonly model?: string;\n /** Default `'http://127.0.0.1:11434'`. */\n readonly baseUrl?: string;\n /**\n * Optional dimensionality hint. When known up-front, the canonical\n * id is stable from the first `embed()` call instead of being\n * resolved from the response.\n */\n readonly dim?: number;\n /**\n * Override `fetch`. Used by the test suite to inject a mock HTTP\n * fixture. Production callers should leave this unset so the\n * embedder uses the platform's `globalThis.fetch`.\n */\n readonly fetchImpl?: typeof fetch;\n /**\n * If `true`, skip the `POST /api/show` model-digest probe at\n * construction. Used in test fixtures where the digest is\n * pre-populated.\n */\n readonly skipDigestProbe?: boolean;\n /**\n * Optional pre-resolved digest (e.g. from a probe done elsewhere).\n * When set, the embedder uses this value instead of issuing a\n * `POST /api/show` request.\n */\n readonly digest?: string;\n /** Optional API path override (default `'/api/embed'`). */\n readonly embedPath?: string;\n /** Optional API path override (default `'/api/embeddings'`). */\n readonly legacyEmbedPath?: string;\n /** Optional API path override (default `'/api/show'`). */\n readonly showPath?: string;\n /**\n * Per-request hard timeout in milliseconds. Default `30000`. Each\n * HTTP call (`/api/show`, `/api/embed`, legacy `/api/embeddings`) is\n * aborted if the Ollama daemon does not respond in time, so a hung\n * daemon never stalls the caller. A per-call {@link EmbedOptions.signal}\n * is combined with this timeout. Set to `0` to disable.\n */\n readonly timeoutMs?: number;\n}\n\n/**\n * Raised when the Ollama HTTP API returns a non-2xx response.\n *\n * @stable\n */\nexport class OllamaEmbedderError extends Error {\n override readonly name = 'OllamaEmbedderError';\n constructor(\n message: string,\n public readonly statusCode?: number,\n options?: { cause?: unknown },\n ) {\n super(message, options);\n }\n}\n\n/**\n * Model -> output-dimension hints used to seed the canonical id before the\n * first `embed()` resolves the real width from a response. Only single-width\n * families are listed; size-variant families (e.g. `qwen3-embedding`, whose\n * dim depends on the `:0.6b` / `:4b` / `:8b` tag) are deliberately omitted so\n * an ambiguous bind fails loudly rather than baking a wrong width (PS-11).\n */\nexport const KNOWN_OLLAMA_MODEL_DIMS: ReadonlyMap<string, number> = new Map([\n ['nomic-embed-text', 768],\n ['mxbai-embed-large', 1024],\n ['snowflake-arctic-embed', 1024],\n ['snowflake-arctic-embed2', 1024],\n ['bge-m3', 1024],\n ['bge-large', 1024],\n ['embeddinggemma', 768],\n ['all-minilm', 384],\n ['granite-embedding', 384],\n ['paraphrase-multilingual', 768],\n]);\n\n/**\n * Build an Ollama-backed embedder. The first `embed()` call issues a\n * `POST /api/show` to capture the model digest; subsequent calls hit\n * the embedding endpoint directly.\n *\n * @stable\n */\nexport function createOllamaEmbedder(options: OllamaEmbedderOptions = {}): OllamaEmbedder {\n return new OllamaEmbedder(options);\n}\n\n/**\n * `EmbedderProvider` implementation that talks to the Ollama HTTP API.\n *\n * @stable\n */\nexport class OllamaEmbedder implements EmbedderProvider {\n readonly #model: string;\n readonly #baseUrl: string;\n readonly #fetchImpl: typeof fetch;\n readonly #skipDigestProbe: boolean;\n readonly #embedPath: string;\n readonly #legacyEmbedPath: string;\n readonly #showPath: string;\n readonly #timeoutMs: number;\n #digest: string | null;\n #resolvedDim: number | null;\n #initialized = false;\n #initializing: Promise<void> | null = null;\n\n constructor(options: OllamaEmbedderOptions) {\n this.#model = options.model ?? DEFAULT_OLLAMA_MODEL;\n this.#baseUrl = stripTrailingSlashesLocal(options.baseUrl ?? DEFAULT_OLLAMA_BASE_URL);\n this.#fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.#skipDigestProbe = options.skipDigestProbe ?? false;\n this.#embedPath = options.embedPath ?? '/api/embed';\n this.#legacyEmbedPath = options.legacyEmbedPath ?? '/api/embeddings';\n this.#showPath = options.showPath ?? '/api/show';\n this.#timeoutMs = options.timeoutMs ?? 30_000;\n this.#digest = options.digest ?? null;\n this.#resolvedDim = options.dim ?? KNOWN_OLLAMA_MODEL_DIMS.get(this.#model) ?? null;\n }\n\n /**\n * Build the `{ signal }` fetch-init fragment, combining the caller's\n * abort signal (if any) with the per-request timeout so a hung Ollama\n * daemon cannot stall the embed call. Returns `{}` when neither\n * applies. Call once per fetch - each call mints a fresh timeout.\n */\n #signalInit(opts: EmbedOptions): { signal?: AbortSignal } {\n const timeoutSignal = this.#timeoutMs > 0 ? AbortSignal.timeout(this.#timeoutMs) : undefined;\n const signal =\n opts.signal !== undefined && timeoutSignal !== undefined\n ? AbortSignal.any([opts.signal, timeoutSignal])\n : (opts.signal ?? timeoutSignal);\n return signal === undefined ? {} : { signal };\n }\n\n /** The canonical embedder id - `'ollama:<model>@<dim-or-digest>'`. */\n id(): string {\n const dim = this.dim();\n const digest = this.#digest !== null ? `:${this.#digest.slice(0, 12)}` : '';\n return `ollama:${this.#model}@${dim}${digest}`;\n }\n\n /**\n * Output dimension - the explicit `dim` option, the resolved width from the\n * first `embed()`, or a known-family default. PS-11: throws for an unknown\n * model with no `dim` hint instead of returning `0` (which the store would\n * persist and use to create a `float[0]` vec0 table, silently breaking\n * vector search). Pass `dim` for any model not in {@link\n * KNOWN_OLLAMA_MODEL_DIMS}, or call `embed()` once first to resolve it.\n */\n dim(): number {\n if (this.#resolvedDim !== null) return this.#resolvedDim;\n const known = KNOWN_OLLAMA_MODEL_DIMS.get(this.#model);\n if (known !== undefined) return known;\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] unknown output dimension for model '${this.#model}'. ` +\n 'Pass `dim` to ollamaEmbedder({ model, dim }), or call embed() once to resolve it ' +\n 'from a response, before registering the embedder.',\n );\n }\n\n /**\n * Deterministic hash over the embedder's full configuration -\n * including the discovered digest. A model upgrade in the same\n * Ollama instance changes the digest (and therefore the hash), so\n * `lock-on-first` correctly fires a migration path instead of\n * silently reusing the same `embedder_id`.\n */\n configHash(): string {\n return canonicalConfigHash({\n adapter: 'ollama',\n model: this.#model,\n baseUrl: this.#baseUrl,\n digest: this.#digest ?? null,\n dim: this.#resolvedDim ?? null,\n });\n }\n\n async embed(\n texts: ReadonlyArray<string>,\n opts: EmbedOptions = {},\n ): Promise<ReadonlyArray<Float32Array>> {\n if (texts.length === 0) return [];\n await this.#ensureInitialized(opts);\n return this.#callEmbed(texts, opts);\n }\n\n async #ensureInitialized(opts: EmbedOptions): Promise<void> {\n if (this.#initialized) return;\n if (this.#initializing !== null) return this.#initializing;\n const inflight = (async () => {\n try {\n if (!this.#skipDigestProbe && this.#digest === null) {\n await this.#probeDigest(opts);\n }\n this.#initialized = true;\n } finally {\n // Always clear the in-flight slot so a transient failure does\n // not poison every subsequent call with the same rejection.\n this.#initializing = null;\n }\n })();\n this.#initializing = inflight;\n return inflight;\n }\n\n async #probeDigest(opts: EmbedOptions): Promise<void> {\n const url = `${this.#baseUrl}${this.#showPath}`;\n let resp: Response;\n try {\n resp = await this.#fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ name: this.#model }),\n ...this.#signalInit(opts),\n });\n } catch (err) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] failed to reach Ollama at ${url}`,\n undefined,\n { cause: err },\n );\n }\n if (!resp.ok) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] /api/show returned ${resp.status} for model '${this.#model}'`,\n resp.status,\n );\n }\n const json = (await resp.json()) as { digest?: string };\n if (typeof json.digest === 'string' && json.digest.length > 0) {\n this.#digest = json.digest;\n }\n }\n\n async #callEmbed(\n texts: ReadonlyArray<string>,\n opts: EmbedOptions,\n ): Promise<ReadonlyArray<Float32Array>> {\n const batchUrl = `${this.#baseUrl}${this.#embedPath}`;\n const batchBody = JSON.stringify({ model: this.#model, input: [...texts] });\n let resp: Response;\n try {\n resp = await this.#fetchImpl(batchUrl, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: batchBody,\n ...this.#signalInit(opts),\n });\n } catch (err) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] failed to reach Ollama at ${batchUrl}`,\n undefined,\n { cause: err },\n );\n }\n if (resp.status === 404) {\n // Older Ollama versions only expose the legacy single-input path.\n return this.#legacyEmbedPerText(texts, opts);\n }\n if (!resp.ok) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] /api/embed returned ${resp.status}`,\n resp.status,\n );\n }\n const json = (await resp.json()) as { embeddings?: number[][] };\n const list = json.embeddings;\n if (!Array.isArray(list) || list.length !== texts.length) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] /api/embed response had unexpected shape`,\n );\n }\n const out: Float32Array[] = [];\n for (let i = 0; i < list.length; i++) {\n const row = list[i];\n if (!Array.isArray(row)) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] embedding row ${i} is not an array`,\n );\n }\n const f32 = Float32Array.from(row);\n if (this.#resolvedDim === null) {\n this.#resolvedDim = f32.length;\n } else if (this.#resolvedDim !== f32.length) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] embedding dim drifted: expected ${this.#resolvedDim}, got ${f32.length}`,\n );\n }\n out.push(f32);\n }\n return out;\n }\n\n async #legacyEmbedPerText(\n texts: ReadonlyArray<string>,\n opts: EmbedOptions,\n ): Promise<ReadonlyArray<Float32Array>> {\n const url = `${this.#baseUrl}${this.#legacyEmbedPath}`;\n const out: Float32Array[] = [];\n for (const text of texts) {\n const resp = await this.#fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ model: this.#model, prompt: text }),\n ...this.#signalInit(opts),\n });\n if (!resp.ok) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] /api/embeddings returned ${resp.status}`,\n resp.status,\n );\n }\n const json = (await resp.json()) as { embedding?: number[] };\n if (!Array.isArray(json.embedding)) {\n throw new OllamaEmbedderError(\n `[graphorin/embedder-ollama] /api/embeddings response has no 'embedding' array`,\n );\n }\n const f32 = Float32Array.from(json.embedding);\n if (this.#resolvedDim === null) this.#resolvedDim = f32.length;\n out.push(f32);\n }\n return out;\n }\n}\n\n/**\n * Canonical-JSON deterministic hash over an embedder configuration.\n * Object keys are sorted lexicographically so the resulting hash is\n * stable across `JSON.stringify` reorderings.\n *\n * @stable\n */\nexport function canonicalConfigHash(config: unknown): string {\n return createHash('sha256').update(canonicalize(config), 'utf8').digest('hex');\n}\n\nfunction canonicalize(value: unknown): string {\n if (value === null) return 'null';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n if (typeof value === 'number') return Number.isFinite(value) ? value.toString() : 'null';\n if (typeof value === 'string') return JSON.stringify(value);\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(',')}]`;\n }\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n const keys = Object.keys(obj).sort();\n return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(',')}}`;\n }\n return 'null';\n}\n\n/**\n * Strip every trailing `/` from a URL string in `O(n)`. Kept regex-free\n * so CodeQL does not flag the (otherwise harmless) `/+` anchored\n * quantifier as a polynomial-redos vector.\n */\nfunction stripTrailingSlashesLocal(url: string): string {\n let end = url.length;\n while (end > 0 && url.charCodeAt(end - 1) === 0x2f) end -= 1;\n return end === url.length ? url : url.slice(0, end);\n}\n"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ //#region package.json
2
+ var version = "0.6.1";
3
+
4
+ //#endregion
5
+ export { version };
6
+ //# sourceMappingURL=package.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.js","names":[],"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"@graphorin/embedder-ollama\",\n \"version\": \"0.6.1\",\n \"description\": \"First-class opt-in alternative to @graphorin/embedder-transformersjs for the Graphorin framework. Wraps the local Ollama HTTP API to produce dense embeddings via models such as `nomic-embed-text` (768-dim, multilingual default), `mxbai-embed-large` (1024-dim), `snowflake-arctic-embed` (1024-dim), and `bge-m3` (1024-dim). Implements the `EmbedderProvider` contract with a deterministic configHash that includes the model digest reported by `POST /api/show`, so a model upgrade in the same Ollama instance produces a different embedder_id and the multi-table per-embedder vec0 layout in `@graphorin/store-sqlite` triggers a clean migration path.\",\n \"license\": \"MIT\",\n \"author\": \"Oleksiy Stepurenko\",\n \"homepage\": \"https://github.com/o-stepper/graphorin/tree/main/packages/embedder-ollama\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/o-stepper/graphorin.git\",\n \"directory\": \"packages/embedder-ollama\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/o-stepper/graphorin/issues\"\n },\n \"keywords\": [\n \"graphorin\",\n \"ai\",\n \"agents\",\n \"framework\",\n \"embeddings\",\n \"ollama\",\n \"nomic-embed-text\",\n \"bge-m3\",\n \"local-first\"\n ],\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=22.0.0\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit && tsc -p tsconfig.tests.json\",\n \"test\": \"vitest run\",\n \"lint\": \"biome check .\",\n \"clean\": \"rimraf dist .turbo *.tsbuildinfo\"\n },\n \"dependencies\": {\n \"@graphorin/core\": \"workspace:*\"\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"provenance\": true\n },\n \"devDependencies\": {\n \"@graphorin/store-sqlite\": \"workspace:*\"\n }\n}\n"],"mappings":";cAEa"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphorin/embedder-ollama",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "First-class opt-in alternative to @graphorin/embedder-transformersjs for the Graphorin framework. Wraps the local Ollama HTTP API to produce dense embeddings via models such as `nomic-embed-text` (768-dim, multilingual default), `mxbai-embed-large` (1024-dim), `snowflake-arctic-embed` (1024-dim), and `bge-m3` (1024-dim). Implements the `EmbedderProvider` contract with a deterministic configHash that includes the model digest reported by `POST /api/show`, so a model upgrade in the same Ollama instance produces a different embedder_id and the multi-table per-embedder vec0 layout in `@graphorin/store-sqlite` triggers a clean migration path.",
5
5
  "license": "MIT",
6
6
  "author": "Oleksiy Stepurenko",
@@ -45,14 +45,14 @@
45
45
  "LICENSE"
46
46
  ],
47
47
  "dependencies": {
48
- "@graphorin/core": "0.5.0"
48
+ "@graphorin/core": "0.6.1"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public",
52
52
  "provenance": true
53
53
  },
54
54
  "devDependencies": {
55
- "@graphorin/store-sqlite": "0.5.0"
55
+ "@graphorin/store-sqlite": "0.6.1"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsdown",