dhti-cli 0.7.1 → 0.8.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 CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  - 🚀 *Dhanvantari rose out of the water with his four hands, holding a pot full of elixirs!*
15
15
 
16
- ### TL; DR: 🏥 DHTI enables rapid prototyping, sharing, and testing of GenAI healthcare applications within an EHR, facilitating the seamless transition of your experiments to practice!
16
+ ### TL; DR: 🏥 DHTI enables rapid prototyping, sharing, and testing of GenAI healthcare applications within an EHR, facilitating the seamless transition of your experiments to practice! Moreover, DHTI comes with batteries included! It has the [skills](/.github/skills/) to generate GenAI components from simple problem oriented [prompts](/prompts/e2e-sample.md).
17
17
  👉 [Try it out today!](#try-it-out) and give us a star ⭐️ if you like it!
18
18
 
19
19
  ### About
@@ -68,6 +68,8 @@ The essence of DHTI is *modularity* with an emphasis on *configuration!* It is n
68
68
  * **Easy to use**: Can be installed in a few minutes.
69
69
  * **Developer friendly**: Copy working files to running containers for testing.
70
70
  * **Dry-run mode**: Preview changes before execution with the `--dry-run` flag.
71
+ * **Local directory installation**: Install elixirs and conches from local directories using the `-l` flag.
72
+ * **Monorepo support**: Install elixirs and conches from subdirectories in GitHub repositories with the `-s` flag.
71
73
  * **Dependency Injection**: Dependency injection for models and hyperparameters for configuring elixirs.
72
74
  * **Generate synthetic data**: [DHTI supports generating synthetic data for testing, using synthea.](/notes/SYNTHEA.md)
73
75
  * **CQL support**: [CQL for clinical decision support](https://nuchange.ca/2025/06/v-llm-in-the-loop-cql-execution-with-unstructured-data-and-fhir-terminology-support.html).
@@ -80,6 +82,8 @@ The essence of DHTI is *modularity* with an emphasis on *configuration!* It is n
80
82
  * **LLM**: Ollama for self-hosting LLM models.
81
83
 
82
84
  ## ✨ New
85
+ * **Local directory installation**: Install elixirs and conches from local directories using the new `-l` flag, enabling seamless integration with locally generated projects.
86
+ * **start-dhti skill**: New AI agent skill that orchestrates complete DHTI application development - from generating elixirs and conches to starting a fully functional DHTI server.
83
87
  * **MCPX integration**: DHTI now includes an [MCP integrator](https://docs.lunar.dev/mcpx/) that allows other MCP servers to be "installed" and exposed seamlessly to DHTI through the MCPX gateway.
84
88
  * **DOCKTOR module**: A new module, [DOCKTOR](/notes/DOCKTOR.md), support traditional machine learning model packaged as Docker containers, to be used as MCP tools, enabling the deployment of inference pipelines as agent-invokable tools. (in beta)
85
89
  * **MCP aware agent**: [dhti-elixir-template](https://github.com/dermatologist/dhti-elixir-template) used in the examples now includes an [MCP aware agent](https://github.com/dermatologist/dhti-elixir-template/blob/feature/agent-2/src/dhti_elixir_template/chain.py) that can autodiscover and invoke tools from the MCPX gateway. Install it using `npx dhti-cli elixir install -g https://github.com/dermatologist/dhti-elixir-template.git -n dhti-elixir-template -b feature/agent2`.
@@ -148,11 +152,11 @@ Tools to fine-tune language models for the stack are on our roadmap. We encourag
148
152
 
149
153
  * `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`
150
154
 
151
- * `npx dhti-cli elixir install -g https://github.com/dermatologist/dhti-elixir-template.git -n dhti-elixir-template` to install a sample elixir from github. *(Optional)* You may configure the LLM and hyperparameters in `~/dhti/elixir/app/bootstrap.py`. You can install multiple elixirs.
155
+ * `npx dhti-cli elixir install -g https://github.com/dermatologist/dhti-elixir-template.git -n dhti-elixir-template` to install a sample elixir from github. *(Optional)* You may configure the LLM and hyperparameters in `~/dhti/elixir/app/bootstrap.py`. You can install multiple elixirs. Alternatively, use `-l <local-directory>` to install from a local directory.
152
156
 
153
157
  * `npx dhti-cli docker -n yourdockerhandle/genai-test:1.0 -t elixir` to build a docker image for the elixir.
154
158
 
155
- * `npx dhti-cli conch install -g https://github.com/dermatologist/openmrs-esm-dhti-template.git -n openmrs-esm-dhti-template` to install a simple OpenMRS ESM module (conch)from github. You can install multiple conches.
159
+ * `npx dhti-cli conch install -g https://github.com/dermatologist/openmrs-esm-dhti-template.git -n openmrs-esm-dhti-template` to install a simple OpenMRS ESM module (conch)from github. You can install multiple conches. Alternatively, use `-l <local-directory>` to install from a local directory.
156
160
 
157
161
  * `npx dhti-cli docker -n yourdockerhandle/conch-test:1.0 -t conch` to build a docker image for the conches.
158
162
 
@@ -12,8 +12,10 @@ export default class Conch extends Command {
12
12
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
13
  git: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
14
  image: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
+ local: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
16
  name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
17
  repoVersion: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
18
+ subdirectory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
17
19
  workdir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
18
20
  };
19
21
  run(): Promise<void>;
@@ -5,6 +5,10 @@ import fs from 'node:fs';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ // Helper function to escape shell arguments
9
+ function escapeShellArg(arg) {
10
+ return `'${arg.replaceAll("'", "'\\''")}'`;
11
+ }
8
12
  export default class Conch extends Command {
9
13
  static args = {
10
14
  op: Args.string({ description: 'Operation to perform (install, uninstall or dev)' }),
@@ -29,8 +33,14 @@ export default class Conch extends Command {
29
33
  default: 'openmrs/openmrs-reference-application-3-frontend:3.0.0-beta.17',
30
34
  description: 'Base image to use for the conch',
31
35
  }),
36
+ local: Flags.string({ char: 'l', default: 'none', description: 'Local directory to install from' }),
32
37
  name: Flags.string({ char: 'n', description: 'Name of the elixir' }),
33
38
  repoVersion: Flags.string({ char: 'v', default: '1.0.0', description: 'Version of the conch' }),
39
+ subdirectory: Flags.string({
40
+ char: 's',
41
+ default: 'none',
42
+ description: 'Subdirectory in the repository to install from (for monorepos)',
43
+ }),
34
44
  workdir: Flags.string({
35
45
  char: 'w',
36
46
  default: `${os.homedir()}/dhti`,
@@ -160,12 +170,42 @@ export default class Conch extends Command {
160
170
  fs.writeFileSync(`${flags.workdir}/conch/def/routes.registry.json`, JSON.stringify(registry, null, 2));
161
171
  };
162
172
  if (flags.git !== 'none') {
163
- const cloneCommand = `git clone ${flags.git} ${flags.workdir}/conch/${flags.name}`;
164
- const checkoutCommand = `cd ${flags.workdir}/conch/${flags.name} && git checkout ${flags.branch}`;
173
+ let cloneCommand;
174
+ let checkoutCommand;
175
+ if (flags.subdirectory === 'none') {
176
+ cloneCommand = `git clone ${escapeShellArg(flags.git)} ${escapeShellArg(`${flags.workdir}/conch/${flags.name}`)}`;
177
+ checkoutCommand = `cd ${escapeShellArg(`${flags.workdir}/conch/${flags.name}`)} && git checkout ${escapeShellArg(flags.branch)}`;
178
+ }
179
+ else {
180
+ // Use sparse checkout for subdirectory - broken into steps for readability and security
181
+ const targetDir = `${flags.workdir}/conch/${flags.name}`;
182
+ const escapedDir = escapeShellArg(targetDir);
183
+ const escapedGit = escapeShellArg(flags.git);
184
+ const escapedBranch = escapeShellArg(flags.branch);
185
+ const escapedSubdir = escapeShellArg(flags.subdirectory);
186
+ // Build sparse checkout command with proper escaping
187
+ const initCommand = `mkdir -p ${escapedDir} && cd ${escapedDir} && git init`;
188
+ const remoteCommand = `git remote add origin ${escapedGit}`;
189
+ const sparseCommand = `git config core.sparseCheckout true`;
190
+ // Don't escape the glob pattern itself, only the subdirectory name
191
+ const patternCommand = `echo ${escapedSubdir}/'*' >> .git/info/sparse-checkout`;
192
+ const fetchCommand = `git fetch --depth=1 origin ${escapedBranch}`;
193
+ const checkoutCmd = `git checkout ${escapedBranch}`;
194
+ // Use bash's dotglob to include hidden files, and handle the case when no files exist
195
+ const moveCommand = `bash -c "shopt -s dotglob; if [ -d ${escapedSubdir} ]; then mv ${escapedSubdir}/* . 2>/dev/null || true; fi"`;
196
+ const cleanupCommand = `rm -rf ${escapedSubdir}`;
197
+ cloneCommand = `${initCommand} && ${remoteCommand} && ${sparseCommand} && ${patternCommand} && ${fetchCommand} && ${checkoutCmd} && ${moveCommand} && ${cleanupCommand}`;
198
+ checkoutCommand = `cd ${escapedDir} && echo "Sparse checkout complete"`;
199
+ }
165
200
  if (flags['dry-run']) {
166
201
  console.log(chalk.yellow('[DRY RUN] Would execute git commands:'));
167
- console.log(chalk.cyan(` ${cloneCommand}`));
168
- console.log(chalk.cyan(` ${checkoutCommand}`));
202
+ if (flags.subdirectory === 'none') {
203
+ console.log(chalk.cyan(` ${cloneCommand}`));
204
+ console.log(chalk.cyan(` ${checkoutCommand}`));
205
+ }
206
+ else {
207
+ console.log(chalk.cyan(` Sparse checkout: ${flags.subdirectory} from ${flags.git}`));
208
+ }
169
209
  rewrite();
170
210
  return;
171
211
  }
@@ -175,7 +215,7 @@ export default class Conch extends Command {
175
215
  console.error(`exec error: ${error}`);
176
216
  return;
177
217
  }
178
- // Checkout the branch
218
+ // Checkout the branch (or confirm sparse checkout)
179
219
  exec(checkoutCommand, (error, stdout, stderr) => {
180
220
  if (error) {
181
221
  console.error(`exec error: ${error}`);
@@ -200,5 +240,29 @@ export default class Conch extends Command {
200
240
  rewrite();
201
241
  }
202
242
  }
243
+ // If flags.local is not none, copy the local directory to the conch directory
244
+ if (flags.local !== 'none' && args.op !== 'dev') {
245
+ const absolutePath = path.isAbsolute(flags.local) ? flags.local : path.resolve(process.cwd(), flags.local);
246
+ // Validate that the path exists and is a directory (skip validation in dry-run mode)
247
+ if (!flags['dry-run']) {
248
+ if (!fs.existsSync(absolutePath)) {
249
+ console.error(chalk.red(`Error: Local directory does not exist: ${absolutePath}`));
250
+ this.exit(1);
251
+ }
252
+ const stats = fs.statSync(absolutePath);
253
+ if (!stats.isDirectory()) {
254
+ console.error(chalk.red(`Error: Path is not a directory: ${absolutePath}`));
255
+ this.exit(1);
256
+ }
257
+ }
258
+ if (flags['dry-run']) {
259
+ console.log(chalk.yellow(`[DRY RUN] Would copy ${absolutePath} to ${flags.workdir}/conch/${flags.name}`));
260
+ rewrite();
261
+ }
262
+ else {
263
+ fs.cpSync(absolutePath, `${flags.workdir}/conch/${flags.name}`, { recursive: true });
264
+ rewrite();
265
+ }
266
+ }
203
267
  }
204
268
  }
@@ -11,9 +11,11 @@ export default class Elixir extends Command {
11
11
  dev: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
12
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
13
  git: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
+ local: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
15
  name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
16
  pypi: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
16
17
  repoVersion: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
18
+ subdirectory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
17
19
  whl: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
18
20
  workdir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
19
21
  };
@@ -24,6 +24,7 @@ export default class Elixir extends Command {
24
24
  description: 'Show what changes would be made without actually making them',
25
25
  }),
26
26
  git: Flags.string({ char: 'g', default: 'none', description: 'Github repository to install' }),
27
+ local: Flags.string({ char: 'l', default: 'none', description: 'Local directory to install from' }),
27
28
  name: Flags.string({ char: 'n', description: 'Name of the elixir' }),
28
29
  pypi: Flags.string({
29
30
  char: 'p',
@@ -31,6 +32,11 @@ export default class Elixir extends Command {
31
32
  description: 'PyPi package to install. Ex: dhti-elixir-base = ">=0.1.0"',
32
33
  }),
33
34
  repoVersion: Flags.string({ char: 'v', default: '0.1.0', description: 'Version of the elixir' }),
35
+ subdirectory: Flags.string({
36
+ char: 's',
37
+ default: 'none',
38
+ description: 'Subdirectory in the repository to install from (for monorepos)',
39
+ }),
34
40
  whl: Flags.string({ char: 'e', default: 'none', description: 'Whl file to install' }),
35
41
  workdir: Flags.string({
36
42
  char: 'w',
@@ -126,11 +132,31 @@ export default class Elixir extends Command {
126
132
  lineToAdd = `${flags.name} = { file = "whl/${path.basename(flags.whl)}" }`;
127
133
  }
128
134
  if (flags.git !== 'none') {
129
- lineToAdd = `${flags.name} = { git = "${flags.git}", branch = "${flags.branch}" }`;
135
+ lineToAdd =
136
+ flags.subdirectory === 'none'
137
+ ? `${flags.name} = { git = "${flags.git}", branch = "${flags.branch}" }`
138
+ : `${flags.name} = { git = "${flags.git}", branch = "${flags.branch}", subdirectory = "${flags.subdirectory}" }`;
130
139
  }
131
140
  if (flags.pypi !== 'none') {
132
141
  lineToAdd = flags.pypi;
133
142
  }
143
+ if (flags.local !== 'none') {
144
+ // Use path for local directory installation
145
+ const absolutePath = path.isAbsolute(flags.local) ? flags.local : path.resolve(process.cwd(), flags.local);
146
+ // Validate that the path exists and is a directory (skip validation in dry-run mode)
147
+ if (!flags['dry-run']) {
148
+ if (!fs.existsSync(absolutePath)) {
149
+ console.error(chalk.red(`Error: Local directory does not exist: ${absolutePath}`));
150
+ this.exit(1);
151
+ }
152
+ const stats = fs.statSync(absolutePath);
153
+ if (!stats.isDirectory()) {
154
+ console.error(chalk.red(`Error: Path is not a directory: ${absolutePath}`));
155
+ this.exit(1);
156
+ }
157
+ }
158
+ lineToAdd = `${flags.name} = { path = "${absolutePath}" }`;
159
+ }
134
160
  if (!flags['dry-run']) {
135
161
  pyproject = pyproject.replace('dependencies = [', `dependencies = [\n"${flags.name}",`);
136
162
  pyproject = pyproject.replace('[tool.uv.sources]', `[tool.uv.sources]\n${lineToAdd}\n`);
@@ -116,6 +116,15 @@
116
116
  "multiple": false,
117
117
  "type": "option"
118
118
  },
119
+ "local": {
120
+ "char": "l",
121
+ "description": "Local directory to install from",
122
+ "name": "local",
123
+ "default": "none",
124
+ "hasDynamicHelp": false,
125
+ "multiple": false,
126
+ "type": "option"
127
+ },
119
128
  "name": {
120
129
  "char": "n",
121
130
  "description": "Name of the elixir",
@@ -133,6 +142,15 @@
133
142
  "multiple": false,
134
143
  "type": "option"
135
144
  },
145
+ "subdirectory": {
146
+ "char": "s",
147
+ "description": "Subdirectory in the repository to install from (for monorepos)",
148
+ "name": "subdirectory",
149
+ "default": "none",
150
+ "hasDynamicHelp": false,
151
+ "multiple": false,
152
+ "type": "option"
153
+ },
136
154
  "workdir": {
137
155
  "char": "w",
138
156
  "description": "Working directory to install the conch",
@@ -393,6 +411,15 @@
393
411
  "multiple": false,
394
412
  "type": "option"
395
413
  },
414
+ "local": {
415
+ "char": "l",
416
+ "description": "Local directory to install from",
417
+ "name": "local",
418
+ "default": "none",
419
+ "hasDynamicHelp": false,
420
+ "multiple": false,
421
+ "type": "option"
422
+ },
396
423
  "name": {
397
424
  "char": "n",
398
425
  "description": "Name of the elixir",
@@ -419,6 +446,15 @@
419
446
  "multiple": false,
420
447
  "type": "option"
421
448
  },
449
+ "subdirectory": {
450
+ "char": "s",
451
+ "description": "Subdirectory in the repository to install from (for monorepos)",
452
+ "name": "subdirectory",
453
+ "default": "none",
454
+ "hasDynamicHelp": false,
455
+ "multiple": false,
456
+ "type": "option"
457
+ },
422
458
  "whl": {
423
459
  "char": "e",
424
460
  "description": "Whl file to install",
@@ -756,5 +792,5 @@
756
792
  ]
757
793
  }
758
794
  },
759
- "version": "0.7.1"
795
+ "version": "0.8.0"
760
796
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dhti-cli",
3
3
  "description": "DHTI CLI",
4
- "version": "0.7.1",
4
+ "version": "0.8.0",
5
5
  "author": "Bell Eapen",
6
6
  "bin": {
7
7
  "dhti-cli": "bin/run.js"