dhti-cli 0.4.0 → 0.5.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/README.md +6 -1
- package/dist/commands/compose.js +5 -1
- package/dist/commands/docktor.d.ts +18 -0
- package/dist/commands/docktor.js +143 -0
- package/dist/resources/docker-compose-master.yml +21 -3
- package/oclif.manifest.json +82 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,9 @@ Generative AI features are built as [LangServe Apps](https://python.langchain.co
|
|
|
22
22
|
|
|
23
23
|
### Want to know more?
|
|
24
24
|
|
|
25
|
+
*Watch this demo video:*
|
|
26
|
+
[](https://www.youtube.com/watch?v=5jFFe3wqKM0)
|
|
27
|
+
|
|
25
28
|
Gen AI can transform medicine. But it needs a framework for collaborative research and practice. DHTI is a reference architecture and an implementation for such a framework that integrates an EMR ([OpenMRS](https://openmrs.org/)), :link: Gen AI application server ([LangServe](https://python.langchain.com/v0.2/docs/langserve/)), self-hosted LLMs for privacy ([Ollama](https://ollama.com/)), tools on [MCP server](https://github.com/dermatologist/fhir-mcp-server), vector store for RAG ([redis](https://redis.io/)), monitoring ([LangFuse](https://langfuse.com/)), 🔥 FHIR repository with [CQL](https://nuchange.ca/2025/06/v-llm-in-the-loop-cql-execution-with-unstructured-data-and-fhir-terminology-support.html) support ([HAPI](https://cloud.alphora.com/sandbox/r4/cqm/)) and graph utilities ([Neo4j](https://neo4j.com/)) in one docker-compose! DHTI is inspired by [Bahmni](https://www.bahmni.org/) and **aims to facilitate GenAI adoption and research in areas with low resources.** The MCP server hosts pluggable, agent-invokable tools (FHIR query, summarization, terminology lookup, custom analytics, etc.) that you can extend without modifying core services.
|
|
26
29
|
|
|
27
30
|
The essence of DHTI is *modularity* with an emphasis on *configuration!* It is non-opinionated on LLMs, hyperparameters and pretty much everything. DHTI supports installable Gen AI routines through [LangServe Apps](https://python.langchain.com/docs/langserve/) (which we call :curry: **elixir**) and installable UI elements through [OpenMRS O3](https://o3-docs.openmrs.org/) React container (which we call :shell: **conch**). 🔥 FHIR is used for backend and [CDS-Hooks](https://cds-hooks.org/) for frontend communication, decoupling conches from OpenMRS, making them potentially usable with any health information system. We have a [fork of the cds-hook sandbox](https://github.com/dermatologist/cds-hooks-sandbox/tree/dhti-1) for testing that uses the [order-select](https://cds-hooks.org/hooks/order-select/) hook, utilizing the contentString from the [FHIR CommunicationRequest](https://build.fhir.org/communicationrequest.html) within the [cds-hook context](https://cds-hooks.org/examples/) for user inputs (recommended).
|
|
@@ -106,7 +109,7 @@ Tools to fine-tune language models for the stack are on our roadmap. We encourag
|
|
|
106
109
|
* **EMR**: Built-in EMR, OpenMRS, for patient records.
|
|
107
110
|
* 👉 [Try it out today!](#try-it-out)
|
|
108
111
|
|
|
109
|
-
|
|
112
|
+
*Join us to make the Gen AI equitable and help doctors save lives!*
|
|
110
113
|
|
|
111
114
|
## :sparkles: Resources
|
|
112
115
|
* [fhiry](https://github.com/dermatologist/fhiry): FHIR to pandas dataframe for data analytics, AI and ML!
|
|
@@ -131,6 +134,8 @@ Tools to fine-tune language models for the stack are on our roadmap. We encourag
|
|
|
131
134
|
|
|
132
135
|
* You only need [Node.js](https://nodejs.org/) and [Docker](https://www.docker.com/) installed to run this project. Optionally, you can install [Python](https://www.python.org/) if you want to develop new elixirs. We use a fake LLM script for testing purposes, so you don't need an OpenAI key to run this project. It just says "Paris" or "I don't know" to any prompt. You can replace it with any internal or external LLM service later.
|
|
133
136
|
|
|
137
|
+
👉 **If you are in a hurry, just run `./demo.sh` from a terminal (Linux or MacOS) in the root folder to try out the demo.** Windows users can use WSL. You only need [Node.js](https://nodejs.org/) and [Docker](https://www.docker.com/). This script runs all the commands below. Once done, use `npx dhti-cli docker -d` to stop and delete all the docker containers.
|
|
138
|
+
|
|
134
139
|
* `npx dhti-cli help` to see all available commands.
|
|
135
140
|
|
|
136
141
|
* `npx dhti-cli compose add -m openmrs -m langserve` to add OpenMRS and Langserve elixirs to your docker-compose.yml at ~/dhti. Other available modules: `ollama, langfuse, cqlFhir, redis, neo4j and mcpFhir`. You can read the newly created docker-compose by: `npx dhti-cli compose read`
|
package/dist/commands/compose.js
CHANGED
|
@@ -24,7 +24,7 @@ export default class Compose extends Command {
|
|
|
24
24
|
// flag with a value (-n, --name=VALUE)
|
|
25
25
|
module: Flags.string({
|
|
26
26
|
char: 'm',
|
|
27
|
-
description: 'Modules to add from ( langserve, openmrs, ollama, langfuse, cqlFhir, redis, neo4j and
|
|
27
|
+
description: 'Modules to add from ( langserve, openmrs, ollama, langfuse, cqlFhir, redis, neo4j, mcpFhir, mcpx and docktor)',
|
|
28
28
|
multiple: true,
|
|
29
29
|
}),
|
|
30
30
|
};
|
|
@@ -57,6 +57,8 @@ export default class Compose extends Command {
|
|
|
57
57
|
const webui = ['ollama-webui'];
|
|
58
58
|
const fhir = ['fhir', 'postgres-db'];
|
|
59
59
|
const mcpFhir = ['mcp-fhir', 'fhir', 'postgres-db'];
|
|
60
|
+
const mcpx = ['mcpx'];
|
|
61
|
+
const docktor = ['mcpx'];
|
|
60
62
|
const _modules = {
|
|
61
63
|
cqlFhir,
|
|
62
64
|
fhir,
|
|
@@ -64,6 +66,8 @@ export default class Compose extends Command {
|
|
|
64
66
|
langfuse,
|
|
65
67
|
langserve,
|
|
66
68
|
mcpFhir,
|
|
69
|
+
mcpx,
|
|
70
|
+
docktor,
|
|
67
71
|
neo4j,
|
|
68
72
|
ollama,
|
|
69
73
|
openmrs,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Docktor extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
op: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
name: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static flags: {
|
|
10
|
+
container: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
environment: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
image: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
'model-path': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
workdir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
};
|
|
16
|
+
private restartMcpxContainer;
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
export default class Docktor extends Command {
|
|
7
|
+
static args = {
|
|
8
|
+
op: Args.string({ description: 'Operation to perform (install, remove, restart, list)', required: true }),
|
|
9
|
+
name: Args.string({ description: 'Name of the inference pipeline (e.g., skin-cancer-classifier)', required: false }),
|
|
10
|
+
};
|
|
11
|
+
static description = 'Manage inference pipelines for MCPX';
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> <%= command.id %> install my-pipeline --image my-image:latest --model-path ./models',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> remove my-pipeline',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> list',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
container: Flags.string({
|
|
19
|
+
char: 'c',
|
|
20
|
+
default: 'dhti-mcpx-1',
|
|
21
|
+
description: 'Docker container name for MCPX (use docker ps to find the correct name)',
|
|
22
|
+
}),
|
|
23
|
+
environment: Flags.string({
|
|
24
|
+
char: 'e',
|
|
25
|
+
multiple: true,
|
|
26
|
+
description: 'Environment variables to pass to docker (format: VAR=value)',
|
|
27
|
+
}),
|
|
28
|
+
image: Flags.string({ char: 'i', description: 'Docker image for the inference pipeline (required for install)' }),
|
|
29
|
+
'model-path': Flags.string({
|
|
30
|
+
char: 'm',
|
|
31
|
+
default: '/lunar/packages/mcpx-server/config',
|
|
32
|
+
description: 'Local path to the model directory (optional for install)',
|
|
33
|
+
}),
|
|
34
|
+
workdir: Flags.string({
|
|
35
|
+
char: 'w',
|
|
36
|
+
default: `${os.homedir()}/dhti`,
|
|
37
|
+
description: 'Working directory for MCPX config',
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
async restartMcpxContainer(mcpxConfigPath, containerName) {
|
|
41
|
+
try {
|
|
42
|
+
const { execSync } = await import('node:child_process');
|
|
43
|
+
execSync(`docker cp ${mcpxConfigPath} ${containerName}:/lunar/packages/mcpx-server/`);
|
|
44
|
+
this.log(chalk.green('Copied mcp.json to container: /lunar/packages/mcpx-server/config/mcp.json'));
|
|
45
|
+
execSync(`docker restart ${containerName}`);
|
|
46
|
+
this.log(chalk.green(`Restarted ${containerName} container.`));
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
this.log(chalk.red(`Failed to copy config or restart container '${containerName}'. Please check Docker status and container name.`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async run() {
|
|
53
|
+
const { args, flags } = await this.parse(Docktor);
|
|
54
|
+
const mcpxConfigPath = path.join(flags.workdir, 'config');
|
|
55
|
+
const mcpJsonPath = path.join(mcpxConfigPath, 'mcp.json');
|
|
56
|
+
// Ensure config directory exists
|
|
57
|
+
if (!fs.existsSync(mcpxConfigPath)) {
|
|
58
|
+
fs.mkdirSync(mcpxConfigPath, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
// Ensure mcp.json exists
|
|
61
|
+
if (!fs.existsSync(mcpJsonPath)) {
|
|
62
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: {} }, null, 2));
|
|
63
|
+
}
|
|
64
|
+
let mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
|
|
65
|
+
// Ensure mcpServers exists
|
|
66
|
+
if (!mcpConfig.mcpServers) {
|
|
67
|
+
mcpConfig.mcpServers = {};
|
|
68
|
+
}
|
|
69
|
+
if (args.op === 'install') {
|
|
70
|
+
if (!args.name) {
|
|
71
|
+
this.error('Name is required for install operation');
|
|
72
|
+
}
|
|
73
|
+
if (!flags.image) {
|
|
74
|
+
this.error('Image is required for install operation');
|
|
75
|
+
}
|
|
76
|
+
const binds = [];
|
|
77
|
+
const envVars = [];
|
|
78
|
+
if (flags['model-path']) {
|
|
79
|
+
const absModelPath = path.resolve(flags['model-path']);
|
|
80
|
+
binds.push(`${absModelPath}:/model`);
|
|
81
|
+
}
|
|
82
|
+
if (flags.environment && flags.environment.length > 0) {
|
|
83
|
+
const invalidEnvVars = flags.environment.filter((e) => {
|
|
84
|
+
const idx = e.indexOf('=');
|
|
85
|
+
return idx <= 0 || idx === e.length - 1;
|
|
86
|
+
});
|
|
87
|
+
if (invalidEnvVars.length > 0) {
|
|
88
|
+
this.error(`Invalid environment variable format. Expected 'NAME=value'. Invalid entries: ${invalidEnvVars.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
envVars.push(...flags.environment);
|
|
91
|
+
}
|
|
92
|
+
// Add socket mounting for docker tools if needed, but primarily we want the container to run as a server
|
|
93
|
+
// MCPX handles the running of the docker container.
|
|
94
|
+
// We need to configure it in mcp.json so MCPX picks it up.
|
|
95
|
+
// Based on MCP std, docker servers are defined with `docker` command.
|
|
96
|
+
// Add (merge) new server into existing mcpServers
|
|
97
|
+
mcpConfig.mcpServers[args.name] = {
|
|
98
|
+
command: 'docker',
|
|
99
|
+
args: [
|
|
100
|
+
'run',
|
|
101
|
+
'-i',
|
|
102
|
+
'--rm',
|
|
103
|
+
...binds.flatMap((b) => ['-v', b]),
|
|
104
|
+
...envVars.flatMap((e) => ['-e', e]),
|
|
105
|
+
flags.image,
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
// Write back the updated config (preserving all other properties and existing servers)
|
|
109
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
|
|
110
|
+
this.log(chalk.green(`Inference pipeline '${args.name}' added to MCPX config.`));
|
|
111
|
+
// Copy only mcp.json to container and restart
|
|
112
|
+
await this.restartMcpxContainer(mcpxConfigPath, flags.container);
|
|
113
|
+
}
|
|
114
|
+
else if (args.op === 'remove') {
|
|
115
|
+
if (!args.name) {
|
|
116
|
+
this.error('Name is required for remove operation');
|
|
117
|
+
}
|
|
118
|
+
if (mcpConfig.mcpServers && mcpConfig.mcpServers[args.name]) {
|
|
119
|
+
delete mcpConfig.mcpServers[args.name];
|
|
120
|
+
// Write back the updated config (preserving all other properties and remaining servers)
|
|
121
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
|
|
122
|
+
this.log(chalk.green(`Inference pipeline '${args.name}' removed from MCPX config.`));
|
|
123
|
+
this.log(chalk.yellow('Please restart the MCPX container to apply changes: dhti-cli docktor restart'));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.log(chalk.yellow(`Inference pipeline '${args.name}' not found.`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (args.op === 'restart') {
|
|
130
|
+
await this.restartMcpxContainer(mcpxConfigPath, flags.container);
|
|
131
|
+
}
|
|
132
|
+
else if (args.op === 'list') {
|
|
133
|
+
this.log(chalk.blue('Installed Inference Pipelines:'));
|
|
134
|
+
for (const [name, config] of Object.entries(mcpConfig.mcpServers)) {
|
|
135
|
+
const argsList = Array.isArray(config.args) ? config.args.join(' ') : '';
|
|
136
|
+
this.log(`- ${name}: ${argsList}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
this.error(`Unknown operation: ${args.op}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -10,7 +10,7 @@ services:
|
|
|
10
10
|
- backend
|
|
11
11
|
ports:
|
|
12
12
|
- "80:80"
|
|
13
|
-
- "
|
|
13
|
+
- "9001:80"
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
frontend:
|
|
@@ -133,7 +133,7 @@ services:
|
|
|
133
133
|
- "hapi.fhir.enforce_referential_integrity_on_delete=false"
|
|
134
134
|
|
|
135
135
|
mcp-fhir:
|
|
136
|
-
image: beapen/fhir-mcp-server:
|
|
136
|
+
image: beapen/fhir-mcp-server:4.0
|
|
137
137
|
ports:
|
|
138
138
|
- 8006:8000
|
|
139
139
|
restart: "unless-stopped"
|
|
@@ -225,6 +225,23 @@ services:
|
|
|
225
225
|
- spring.neo4j.authentication.username=neo4j
|
|
226
226
|
- spring.neo4j.authentication.password=password
|
|
227
227
|
|
|
228
|
+
mcpx:
|
|
229
|
+
image: us-central1-docker.pkg.dev/prj-common-442813/mcpx/mcpx:latest
|
|
230
|
+
ports:
|
|
231
|
+
- "8000:8000"
|
|
232
|
+
- "9000:9000"
|
|
233
|
+
- "5173:5173"
|
|
234
|
+
- "3000:3000"
|
|
235
|
+
# environment:
|
|
236
|
+
# - MCPX_PORT=9000
|
|
237
|
+
# - MCPX_SERVER_URL="http://10.0.0.211:9000"
|
|
238
|
+
# - VITE_MCPX_SERVER_PORT=9000
|
|
239
|
+
# - VITE_MCPX_SERVER_URL="http://10.0.0.211:9000"
|
|
240
|
+
restart: unless-stopped
|
|
241
|
+
volumes:
|
|
242
|
+
- mcpx-config:/lunar/packages/mcpx-server/config
|
|
243
|
+
privileged: true
|
|
244
|
+
|
|
228
245
|
|
|
229
246
|
volumes:
|
|
230
247
|
openmrs-data: ~
|
|
@@ -235,4 +252,5 @@ volumes:
|
|
|
235
252
|
neo4j-db: ~
|
|
236
253
|
ollama-code: ~
|
|
237
254
|
ollama-root: ~
|
|
238
|
-
ollama-webui: ~
|
|
255
|
+
ollama-webui: ~
|
|
256
|
+
mcpx-config: ~
|
package/oclif.manifest.json
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"module": {
|
|
32
32
|
"char": "m",
|
|
33
|
-
"description": "Modules to add from ( langserve, openmrs, ollama, langfuse, cqlFhir, redis, neo4j and
|
|
33
|
+
"description": "Modules to add from ( langserve, openmrs, ollama, langfuse, cqlFhir, redis, neo4j, mcpFhir, mcpx and docktor)",
|
|
34
34
|
"name": "module",
|
|
35
35
|
"hasDynamicHelp": false,
|
|
36
36
|
"multiple": true,
|
|
@@ -243,6 +243,86 @@
|
|
|
243
243
|
"docker.js"
|
|
244
244
|
]
|
|
245
245
|
},
|
|
246
|
+
"docktor": {
|
|
247
|
+
"aliases": [],
|
|
248
|
+
"args": {
|
|
249
|
+
"op": {
|
|
250
|
+
"description": "Operation to perform (install, remove, restart, list)",
|
|
251
|
+
"name": "op",
|
|
252
|
+
"required": true
|
|
253
|
+
},
|
|
254
|
+
"name": {
|
|
255
|
+
"description": "Name of the inference pipeline (e.g., skin-cancer-classifier)",
|
|
256
|
+
"name": "name",
|
|
257
|
+
"required": false
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
"description": "Manage inference pipelines for MCPX",
|
|
261
|
+
"examples": [
|
|
262
|
+
"<%= config.bin %> <%= command.id %> install my-pipeline --image my-image:latest --model-path ./models",
|
|
263
|
+
"<%= config.bin %> <%= command.id %> remove my-pipeline",
|
|
264
|
+
"<%= config.bin %> <%= command.id %> list"
|
|
265
|
+
],
|
|
266
|
+
"flags": {
|
|
267
|
+
"container": {
|
|
268
|
+
"char": "c",
|
|
269
|
+
"description": "Docker container name for MCPX (use docker ps to find the correct name)",
|
|
270
|
+
"name": "container",
|
|
271
|
+
"default": "dhti-mcpx-1",
|
|
272
|
+
"hasDynamicHelp": false,
|
|
273
|
+
"multiple": false,
|
|
274
|
+
"type": "option"
|
|
275
|
+
},
|
|
276
|
+
"environment": {
|
|
277
|
+
"char": "e",
|
|
278
|
+
"description": "Environment variables to pass to docker (format: VAR=value)",
|
|
279
|
+
"name": "environment",
|
|
280
|
+
"hasDynamicHelp": false,
|
|
281
|
+
"multiple": true,
|
|
282
|
+
"type": "option"
|
|
283
|
+
},
|
|
284
|
+
"image": {
|
|
285
|
+
"char": "i",
|
|
286
|
+
"description": "Docker image for the inference pipeline (required for install)",
|
|
287
|
+
"name": "image",
|
|
288
|
+
"hasDynamicHelp": false,
|
|
289
|
+
"multiple": false,
|
|
290
|
+
"type": "option"
|
|
291
|
+
},
|
|
292
|
+
"model-path": {
|
|
293
|
+
"char": "m",
|
|
294
|
+
"description": "Local path to the model directory (optional for install)",
|
|
295
|
+
"name": "model-path",
|
|
296
|
+
"default": "/lunar/packages/mcpx-server/config",
|
|
297
|
+
"hasDynamicHelp": false,
|
|
298
|
+
"multiple": false,
|
|
299
|
+
"type": "option"
|
|
300
|
+
},
|
|
301
|
+
"workdir": {
|
|
302
|
+
"char": "w",
|
|
303
|
+
"description": "Working directory for MCPX config",
|
|
304
|
+
"name": "workdir",
|
|
305
|
+
"default": "/home/runner/dhti",
|
|
306
|
+
"hasDynamicHelp": false,
|
|
307
|
+
"multiple": false,
|
|
308
|
+
"type": "option"
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
"hasDynamicHelp": false,
|
|
312
|
+
"hiddenAliases": [],
|
|
313
|
+
"id": "docktor",
|
|
314
|
+
"pluginAlias": "dhti-cli",
|
|
315
|
+
"pluginName": "dhti-cli",
|
|
316
|
+
"pluginType": "core",
|
|
317
|
+
"strict": true,
|
|
318
|
+
"enableJsonFlag": false,
|
|
319
|
+
"isESM": true,
|
|
320
|
+
"relativePath": [
|
|
321
|
+
"dist",
|
|
322
|
+
"commands",
|
|
323
|
+
"docktor.js"
|
|
324
|
+
]
|
|
325
|
+
},
|
|
246
326
|
"elixir": {
|
|
247
327
|
"aliases": [],
|
|
248
328
|
"args": {
|
|
@@ -486,5 +566,5 @@
|
|
|
486
566
|
]
|
|
487
567
|
}
|
|
488
568
|
},
|
|
489
|
-
"version": "0.
|
|
569
|
+
"version": "0.5.0"
|
|
490
570
|
}
|