mikser-io 9.50.2 → 9.57.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/CLAUDE.md CHANGED
@@ -137,8 +137,15 @@ brevity.
137
137
  descriptors in `runtime.options.plugins` are projected to their
138
138
  `render-${name}` / `post-${name}` identifier so workers can
139
139
  resolve them via dynamic import.
140
- - `database/` — `createSqliteDatabase()`, `registerSchema()`,
141
- `useDatabase()` (the `mikser_meta` table stamps schema_version).
140
+ - `database/index.js`the CACHE only. `createSqliteDatabase()`,
141
+ `registerSchema()`, `useDatabase()` (the `mikser_meta` table stamps
142
+ schema_version). Nothing durable lives here any more.
143
+ - `database/durable.js` — the durable store. `registerMigrations()`,
144
+ `useDurableDatabase()` (a knex instance), `runMigrations()`,
145
+ `closeDurableDatabase()`. `adoptFromCache` carries tables out of a
146
+ pre-9.56 database, driven by the `durable_tables` record the old design
147
+ kept in `mikser_meta` — names alone would move any cache table that
148
+ happened to collide. `ensureIgnored` adds the file to `.gitignore`.
142
149
  `sift-to-sql.js` translates sift filters to SQL WHERE clauses
143
150
  against `INDEXED_COLUMNS`; un-pushed clauses fall through to
144
151
  JS-side sift. `query-context.js` is the AsyncLocalStorage that
@@ -150,6 +157,12 @@ brevity.
150
157
  - `server.js` — Express bring-up: CLI flags (`--server`, `--cors`,
151
158
  `--no-cors`), trust-proxy, CORS (with extensible header arrays for
152
159
  plugins to push onto), late-binding static mount + listen.
160
+ - `report.js` — the `--json` build report. `warnings` is a VIEW of
161
+ `logger.warn`; `faults` is a view of `logger.error` **carrying a
162
+ `code`** — a subsystem declaring it cannot work, deduped by that code,
163
+ never cleared per cycle, and surfaced in `mikser_ping`. The log call is
164
+ the only registration for either; there is no `reportWarning()` or
165
+ `registerFault()` and there must not be.
153
166
  - `logger.js` — pino + pino-pretty (inline) + gauge progress + custom
154
167
  Writable for progress coordination + `pino.multistream` for
155
168
  third-party shipping. Two registration surfaces for transports:
@@ -434,15 +447,23 @@ Test coverage: `test/unit/source-sweep.test.js`.
434
447
  - **0008** — MCP-UI rendering + action delivery. Lives in
435
448
  `mikser-io-mcp/documentation/decisions/` (not core — moved with
436
449
  MCP).
437
- - **0009** — Sqlite is the engine's persistence substrate. Single
438
- `runtime/mikser.sqlite` holds `mikser_entities` / `mikser_refs` /
439
- `mikser_snapshots` / `mikser_journal` (+ `mikser_meta` for the
440
- schema-version stamp). Sift→SQL pushdown + LRU for findById;
441
- worker-side read-only sqlite for sync template helpers.
442
- `registerSchema(name, sql)` + `useDatabase()` is the plugin-side
443
- persistence pattern. Journal-on-sqlite (Phase 7) enables `--resume`;
444
- auto-persist (Phase 9) means plugins mutate the yielded entity and
445
- the journal writes back without an explicit `updateEntry` call.
450
+ - **0009** — TWO databases, split by whether the data is DERIVED.
451
+ `runtime/mikser.sqlite` is the CACHE: sqlite + better-sqlite3, synchronous
452
+ (render workers need a sync handle for template helpers), holding
453
+ `mikser_entities` / `mikser_refs` / `mikser_snapshots` / `mikser_journal` /
454
+ `mikser_meta`. Deletable at any moment; a wipe is `unlink`.
455
+ `mikser.data.sqlite` at the WORKING-FOLDER ROOT is the DURABLE store: auth
456
+ grants, the change-set log — behind **knex**, async, main-thread only, so it
457
+ can point at Postgres via `config.database.durable`. Built by
458
+ `registerMigrations(owner, [{ name, up }])`, per owner, recorded in
459
+ `mikser_migrations`; `registerSchema(..., { durable: true })` now THROWS.
460
+ Outside `runtime/` because that folder exists to be deleted, and gitignored
461
+ on creation because it holds credentials. `runtime.start()` closes the knex
462
+ pool for a one-shot run or the process never exits.
463
+ Sift→SQL pushdown + LRU for findById on the cache side.
464
+ Journal-on-sqlite (Phase 7) enables `--resume`; auto-persist (Phase 9) means
465
+ plugins mutate the yielded entity and the journal writes back without an
466
+ explicit `updateEntry` call.
446
467
  - **0010** — Plugin bundles + factory-call form + inline options.
447
468
  Plugins are imported by name and called as factories;
448
469
  `plugins: []` carries factory returns, never strings.
@@ -62,9 +62,17 @@ logger.info('Hello %s', 'world')
62
62
  logger.debug({ data }, 'Debug message')
63
63
  logger.warn('Something might be wrong')
64
64
  logger.error('Something failed: %s', err.message)
65
+ logger.error({ code: 'vector-index' }, 'The index is unreadable: %s', err.message) // a fault
65
66
  logger.notice('Completion message') // styled green in info mode
66
67
  ```
67
68
 
69
+ Two levels are also read by the build report, and the log call is the only way
70
+ to raise either. `logger.warn({ code, ...fields }, msg)` becomes a `warnings`
71
+ entry for that cycle. `logger.error` **with a `code`** becomes a *fault* — a
72
+ subsystem declaring it cannot work — which persists across cycles and shows up
73
+ in `mikser_ping`. Uncoded error lines are ordinary errors and are not captured.
74
+ See [Diagnostics → Faults](./diagnostics.md#faults).
75
+
68
76
  ---
69
77
 
70
78
  ## Runtime Singleton
@@ -844,6 +852,36 @@ AsyncLocalStorage than the engine's, queries record no edges, and index pages,
844
852
  sitemaps and feeds silently stop rebuilding. Production consumers resolve both
845
853
  from their own tree, so the problem is local to the dev workspace.
846
854
 
855
+ ## Inventory
856
+
857
+ What this mikser is made of.
858
+
859
+ ### `inventory({ workingFolder } = {})`
860
+
861
+ Every installed mikser package with its version, purpose and links, and
862
+ `active: true` on the ones actually running.
863
+
864
+ ```js
865
+ [
866
+ { name: 'mikser-io-drive', version: '0.14.0', active: true,
867
+ summary: 'WebDAV for mikser-io. Exposes working-folder directories as …',
868
+ homepage: '…', repository: 'https://github.com/…', npm: 'https://www.npmjs.com/package/…' },
869
+ ]
870
+ ```
871
+
872
+ **Derived, never declared.** Every plugin already carries a description, a
873
+ homepage and a repository in its own `package.json`, kept current because npm
874
+ publishes from it. A second summary held elsewhere would be one more thing to
875
+ drift — and the first time it drifted it would be describing a plugin that had
876
+ changed underneath it.
877
+
878
+ **Installed and active are different facts.** A package in `node_modules` that
879
+ no config loads explains nothing about a site's behaviour, and an agent told
880
+ otherwise looks for a feature that is not switched on. `active` is reported
881
+ only where it can be established — a plugin that mounts a route names itself
882
+ there, a renderer registers under a known name — and is absent rather than
883
+ `false` where it cannot, because `false` would be a claim.
884
+
847
885
  ## Roles
848
886
 
849
887
  Enforcement needs only the flat capability list. Explaining a refusal needs the
@@ -855,7 +893,37 @@ indistinguishable from inside a session.
855
893
  | `describeAuthority({ capabilities, roles, catalogue, summaries })` | everything a session can say about its own authority |
856
894
  | `reachOf(capabilities)` | `{ writable, readOnly }` as collection names |
857
895
  | `actingRole(held, catalogue)` | which role is in force |
858
- | `otherRoles(held, catalogue, summaries)` | who to ask, and what they add |
896
+ | `rolesIn(catalogue, { acting, summaries })` | every role and its reach, the acting one marked |
897
+ | `registerCapability(capability, meaning)` | declare what a capability protects |
898
+ | `capabilityMeaning(capability)` | what is known about one |
899
+
900
+ ### Capabilities describe themselves
901
+
902
+ Core does not know what `drive:layouts` protects, where that folder is, or what
903
+ it is for — the plugin enforcing it does. So the plugin declares it, the same
904
+ way it declares a route or a schema:
905
+
906
+ ```js
907
+ registerCapability('drive:documents', {
908
+ plugin: 'drive',
909
+ grants: 'read', // 'read' | 'write' | 'operate'
910
+ resource: {
911
+ kind: 'collection',
912
+ name: 'documents',
913
+ folder: 'documents', // where it is on disk
914
+ summary: 'the words on the pages' // what it is FOR
915
+ },
916
+ })
917
+ ```
918
+
919
+ `reachOf` then describes a credential in terms of the SITE rather than of
920
+ verbs — a collection, its folder and its purpose — which is what an agent needs
921
+ to reason about where it is working, not merely whether a call will be refused.
922
+ A capability nothing has declared lands in `also` rather than being dropped: a
923
+ role described only by the part of it that maps to folders is not described.
924
+
925
+ The `drive:<name>[:write]` convention is still recognised for deployments whose
926
+ plugins predate the registry, so they keep their answer.
859
927
  | `explainRefusal({ capability, role, target, catalogue, summaries })` | the sentence an agent repeats |
860
928
 
861
929
  `readOnly` is the field that makes a refusal explainable, and it is more useful
@@ -867,6 +935,11 @@ capabilities cover the others — roles are normally written as widening tiers
867
935
  and `null` when none dominates, because the acting authority genuinely is the
868
936
  union and naming half of it would be a lie.
869
937
 
938
+ `roles` lists every role, not only the ones the session lacks. One field has to
939
+ serve two readers: someone deciding who to ask, and someone holding the widest
940
+ role trying to see what exists at all — and a "roles you do not have" field
941
+ tells the second one nothing, since for an admin it is always empty.
942
+
870
943
  > **Informational, permanently.** Naming the role that could do something is
871
944
  > what makes a handoff possible. There is no way to request one and none should
872
945
  > be added: a role is a decision about a person, taken by whoever configures the
@@ -1013,6 +1086,12 @@ Together they turn "write and guess" into one call that says what the edit
1013
1086
  invalidated. `currentCycle()` and `buildReport()` read the cycle in progress and
1014
1087
  the last completed report.
1015
1088
 
1089
+ `faults()` returns every fault raised since the process started — subsystems
1090
+ that have reported they cannot work — most recently seen first. Each entry
1091
+ carries its `code`, message, any fields from the log call, and `count` /
1092
+ `first` / `last`. Faults are raised by logging at error level with a `code`
1093
+ and nothing else; see [Diagnostics → Faults](./diagnostics.md#faults).
1094
+
1016
1095
  ## Logging
1017
1096
 
1018
1097
  `addLogTransport({ target, options, level })` adds a pino transport from a
@@ -56,9 +56,55 @@ the synchronization point.
56
56
 
57
57
  ## Decision
58
58
 
59
- ### Single database file, schemas registered per subsystem
60
-
61
- One `runtime/mikser.sqlite` file. WAL mode + `synchronous=NORMAL` +
59
+ ### Two database files, schemas registered per subsystem
60
+
61
+ Two files, split by whether the data is DERIVED.
62
+
63
+ `runtime/mikser.sqlite` is the cache — catalog, refs, snapshots, journal.
64
+ Per ADR-0002 the files on disk are the source of truth, so this can be
65
+ deleted at any moment and costs a rebuild.
66
+
67
+ `mikser.data.sqlite`, at the working-folder ROOT, is everything that is not
68
+ derived: OAuth clients and refresh tokens, the change-set log. No file can
69
+ reproduce it, so deleting it is data loss.
70
+
71
+ They were one file, with a `durable: true` flag exempting tables from the
72
+ wipe. That required a LIST of which tables were which, and the list was where
73
+ the bugs lived — a config that did not load the owning plugin saw no durable
74
+ tables and unlinked them; `--clear` had to be routed through the wipe rather
75
+ than removing the file; the table names had to be parsed back out of DDL.
76
+ Two files delete the list: a wipe is `unlink` again, and what must survive it
77
+ is not in the file being unlinked. The durable file lives outside `runtime/`
78
+ because that folder exists to be thrown away.
79
+
80
+ The two are DIFFERENT ENGINES, not two files behind one API.
81
+
82
+ The cache is locked to sqlite and a SYNCHRONOUS driver: render workers open
83
+ their own read-only handle so template helpers like `lookupHref` can resolve a
84
+ reference inline, and a promise cannot be awaited inside a Handlebars helper.
85
+ That constraint is what killed the earlier pluggable-driver attempt.
86
+
87
+ None of it reaches the durable store, which is read and written on the main
88
+ thread only, by HTTP handlers and lifecycle hooks that are already async. So it
89
+ sits behind **knex** and can point at Postgres instead of a local file — which
90
+ is what several mikser processes sharing one sign-in and one change-set log
91
+ would need. Splitting the files is what split that constraint apart.
92
+
93
+ Durable tables are built by MIGRATIONS, not by an idempotent CREATE:
94
+ `registerMigrations(owner, [{ name, up }])`, per owner, recorded in
95
+ `mikser_migrations (owner, name, applied_at)`. Per owner because the tables
96
+ belong to independently versioned npm packages, and installing a plugin next
97
+ month must not reorder migrations that ran last year — which every
98
+ directory-based migration runner assumes it can do. `CREATE TABLE IF NOT
99
+ EXISTS` does nothing to a table that already exists, so a schema replayed every
100
+ boot can never add a column, rename one, or backfill a value; that is invisible
101
+ for a cache table and the permanent condition of a durable one.
102
+
103
+ `registerSchema(name, sql, { durable: true })` now THROWS, pointing at
104
+ `registerMigrations`. Accepting it would put the table in the file that gets
105
+ deleted, and the first sign would be an operator asked to sign in again.
106
+
107
+ For the cache: WAL mode + `synchronous=NORMAL` +
62
108
  `foreign_keys=ON`. Every persistent subsystem registers its schema via
63
109
  `registerSchema(name, sql)` at module-eval time; the database is
64
110
  opened once in `onInitialize` and applies all registered schemas
@@ -25,6 +25,7 @@ engine source, the entry point is missing and belongs on this page.
25
25
  | What would break if I changed this file? | [`runtime.manifest`](#runtimemanifest) `affectedBy` |
26
26
  | I am an agent reading CLI output, not speaking MCP | [`--tools` / `--tool`](#the-two-agent-workflows) |
27
27
  | Did my schema validate anything at all? | [`schemas.names()`](#schemasnames--schemaslookup) |
28
+ | A tool answered emptily — is it broken, or is there nothing to find? | [`faults`](#faults) |
28
29
 
29
30
  ## Command line
30
31
 
@@ -137,7 +138,7 @@ under this flag, so stdout parses whole.
137
138
  npx mikser --json | jq '.summary'
138
139
  ```
139
140
 
140
- Five buckets, and the distinction between them is the point:
141
+ The buckets, and the distinction between them is the point:
141
142
 
142
143
  | Bucket | Meaning |
143
144
  | --- | --- |
@@ -146,6 +147,8 @@ Five buckets, and the distinction between them is the point:
146
147
  | `unchanged` | the render ran and produced bytes identical to what was already on disk |
147
148
  | `errors` | the render ran and **threw** — with `id`, `destination`, `error`, `layout` |
148
149
  | `gated` | a count — the source was unchanged, so no render was ever scheduled |
150
+ | `warnings` | everything that went through `logger.warn` this cycle, with its `code` |
151
+ | `faults` | subsystems that reported they **cannot work** — see [Faults](#faults) |
149
152
 
150
153
  Each report also carries `cycleId`, `startedAt` and `finishedAt`. Under
151
154
  `--watch` two consecutive reports are otherwise indistinguishable, so
@@ -441,16 +444,31 @@ denial-of-service knob rather than a diagnostic, and it stays on the CLI.
441
444
 
442
445
  ## The database
443
446
 
444
- Everything mikser knows lives in one SQLite file at
445
- `<workingFolder>/runtime/mikser.sqlite`. It is readable while a build
446
- runs (WAL mode) and it is often faster to ask it directly than to add
447
- logging.
447
+ **Two files, and the difference between them is what is safe to delete.**
448
+
449
+ | File | Holds | If you delete it |
450
+ | --- | --- | --- |
451
+ | `runtime/mikser.sqlite` | the derived cache — catalog, refs, snapshots, journal | costs a rebuild, nothing else (ADR-0002: your files are the source of truth) |
452
+ | `mikser.data.sqlite` | everything **not** derived — sign-ins, the change-set log | data loss; no file can reproduce it |
453
+
454
+ The durable one sits at the working-folder root rather than in `runtime/`
455
+ on purpose. That folder exists to be thrown away — it is gitignored, and
456
+ `rm -rf runtime` is a line in real deploy scripts — so anything kept
457
+ inside it is one careless command from gone. `--clear` removes the cache
458
+ and does not touch the other file.
459
+
460
+ `mikser.data.sqlite` holds credentials, so mikser adds it to `.gitignore`
461
+ the first time it creates it. A working folder is usually a git repo, and
462
+ `mikser-io-git` runs `git add -A` over it.
463
+
464
+ Both are readable while a build runs (WAL mode), and it is often faster to
465
+ ask them directly than to add logging.
448
466
 
449
467
  ```bash
450
468
  sqlite3 runtime/mikser.sqlite
451
469
  ```
452
470
 
453
- Five tables:
471
+ The cache holds five tables:
454
472
 
455
473
  | Table | Holds |
456
474
  | --- | --- |
@@ -460,6 +478,34 @@ Five tables:
460
478
  | `mikser_snapshots` | what was rendered, where, and from which inputs |
461
479
  | `mikser_meta` | schema version and config checksum, for cache invalidation |
462
480
 
481
+ The durable store is behind **knex**, so it is the one part of mikser that can
482
+ point at another engine — `config.database.durable` takes a path or a
483
+ connection string. The cache cannot: render workers need a synchronous handle
484
+ for template helpers, and nothing about that applies to a store only the main
485
+ thread touches.
486
+
487
+ Durable tables are built by migrations, per owning package:
488
+
489
+ ```js
490
+ registerMigrations('grants', [
491
+ { name: '001-initial', up: (knex) => knex.schema.createTable('my_grants', (t) => {
492
+ t.string('id').primary()
493
+ }) },
494
+ { name: '002-last-used', up: (knex) => knex.schema.alterTable('my_grants', (t) => {
495
+ t.bigInteger('last_used_at')
496
+ }) },
497
+ ])
498
+ ```
499
+
500
+ Applied once each and recorded in `mikser_migrations (owner, name, applied_at)`.
501
+ Per owner because the tables belong to independently versioned packages —
502
+ installing a plugin next month must not reorder migrations that ran last year.
503
+ Names are permanent: renaming one makes it run again against a database that
504
+ already has its effect. Append, never edit. There is no `down` — a durable
505
+ store's rollback is a restore from backup.
506
+
507
+ `registerSchema(..., { durable: true })` throws now and says so.
508
+
463
509
  ### `mikser_journal`
464
510
 
465
511
  The ordered record of what the engine did this cycle — one row per
@@ -657,6 +703,64 @@ content is clean.
657
703
  get, stats, config }` — the on-demand render cache, useful for asking
658
704
  what has been rendered outside a build.
659
705
 
706
+ ## Faults
707
+
708
+ A **fault** is a subsystem saying it cannot do its job — as opposed to an
709
+ error, which is one render that ran and threw.
710
+
711
+ The difference matters because of how a broken subsystem fails. It does not
712
+ raise; it answers. Search returns `[]`. The change-set log lists nothing.
713
+ `affectedBy` reports that nothing is affected. Every one of those is
714
+ byte-identical to the answer a *working* subsystem gives when there is
715
+ genuinely nothing to report, which is why this class of bug is normally found
716
+ by someone noticing the site is wrong days later.
717
+
718
+ A fault is raised by logging at error level with a `code`:
719
+
720
+ ```js
721
+ useLogger().error({ code: 'change-set-log' },
722
+ 'The change-set log could not be written (%s). Writes still land on disk, '
723
+ + 'but they cannot be listed or undone until this is fixed.', err.message)
724
+ ```
725
+
726
+ That log call is the *only* registration — the same contract as warnings,
727
+ where `report.warnings` is a view of what went through `logger.warn`. There is
728
+ no `registerFault()`, because a second way to raise one is a second thing to
729
+ forget.
730
+
731
+ The `code` is required, and is what separates a fault from an ordinary error
732
+ line. It is the condition's identity, so forty occurrences are one fault with
733
+ `count: 40` rather than forty faults. An uncoded `logger.error('Render error:
734
+ %s', id)` is an event about one entity and already travels in `errors` with the
735
+ entity attached. `logger.error(err, msg)` does not become a fault either — pino
736
+ puts an errno under `err`, not at the top level, so a raw throw logged that way
737
+ stays an event.
738
+
739
+ Faults are recorded whether or not anyone asked for a report, and are **not**
740
+ cleared between cycles. Both are deliberate: a subsystem that cannot work goes
741
+ on not working after the cycle that noticed it ends, and the reader who most
742
+ needs to know is usually not the operator watching the terminal — they saw the
743
+ log line — but an agent connecting an hour later, for which the log is a
744
+ channel it never reads.
745
+
746
+ Where they surface:
747
+
748
+ | Surface | Shape |
749
+ | --- | --- |
750
+ | `--json` | the `faults` array and `summary.faults` |
751
+ | `mikser_ping` | a `faults` key, **absent** when there are none |
752
+ | `faults()` | the same array, for a plugin or a REPL |
753
+
754
+ Each entry carries the `code`, the message, any fields from the log call, and
755
+ `count` / `first` / `last`. `last` is the field that says whether it is still
756
+ happening: a fault seen once at boot and one firing every cycle read the same
757
+ by presence alone.
758
+
759
+ Nothing clears a fault. The condition is fixed by an operator, and the restart
760
+ that follows is what clears it — an engine deciding on a subsystem's behalf
761
+ that it has recovered would be inventing the one fact this surface exists to
762
+ report honestly.
763
+
660
764
  ## When mikser is silent
661
765
 
662
766
  Silence is this engine's characteristic failure mode: a declaration that
@@ -677,6 +781,10 @@ surfaces that turn silence into a statement:
677
781
  part in cache invalidation, so a config change forces a rebuild; but a
678
782
  module the config *imports* does not. If you changed a helper the config
679
783
  pulls in, use `--force`.
784
+ - **A tool that answers emptily because it is broken** — check
785
+ [`faults`](#faults) before reading an empty result as a fact about the
786
+ site. This is the one case where the answer and the failure are the same
787
+ bytes.
680
788
  - **A plugin that appears to do nothing** — `No plugins loaded` with a
681
789
  config present is a warning naming the file. A config that fails to
682
790
  load now exits non-zero rather than loading as empty.
package/index.js CHANGED
@@ -3,6 +3,7 @@ export * as constants from './src/constants.js'
3
3
  export * from './src/utils.js'
4
4
  export * from './src/auth.js'
5
5
  export * from './src/roles.js'
6
+ export * from './src/inventory.js'
6
7
  export * from './src/report.js'
7
8
  // The diagnostics behind --explain. Exported so a transport — the MCP tool
8
9
  // surface, the api plugin's routes — can serve the same structured report the
@@ -10,6 +11,11 @@ export * from './src/report.js'
10
11
  export * from './src/explain.js'
11
12
  export * from './src/lifecycle.js'
12
13
  export * from './src/database/index.js'
14
+ // The durable store — registerMigrations / useDurableDatabase. A separate
15
+ // module from the cache because it is a separate database with none of the
16
+ // cache's constraints: main-thread only, so async, so knex, so portable to
17
+ // another engine.
18
+ export * from './src/database/durable.js'
13
19
  export * from './src/journal.js'
14
20
  export * from './src/catalog.js'
15
21
  export * from './src/search.js'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "9.50.2",
4
- "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
3
+ "version": "9.57.0",
4
+ "description": "A mixer for content: entities in, configurable render pipelines, outputs of any kind. Static sites are the canonical recipe, not the definition — the same engine renders PDFs, emails and whatever a renderer plugin produces. Files are the source of truth, every lifecycle phase is observable, and the build graph is queryable by an agent.",
5
5
  "main": "index.js",
6
6
  "exports": {
7
7
  ".": "./index.js",
@@ -44,6 +44,7 @@
44
44
  "handlebars": "^4.7.9",
45
45
  "hasha": "^7.0.0",
46
46
  "is-url": "^1.2.4",
47
+ "knex": "^3.3.0",
47
48
  "line-reader": "^0.4.0",
48
49
  "lodash": "^4.18.1",
49
50
  "mime-types": "^3.0.2",