dhti-cli 1.3.1 → 1.3.3

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
@@ -14,7 +14,7 @@
14
14
  > 🚀 Dhanvantari rose out of the water with his four hands, holding a pot full of elixirs.
15
15
 
16
16
  # DHTI
17
- *DHTI enables rapid prototyping, sharing, and testing of GenAI healthcare applications inside an EHR, helping experiments move smoothly into practice. DHTI also includes [skills](/.github/skills/) that generate GenAI components from problem‑oriented [prompts](/prompts/e2e-sample.md).*
17
+ *DHTI enables rapid prototyping, sharing, and testing of GenAI healthcare applications inside an EHR, helping experiments move smoothly into practice. DHTI also includes [skills](/.agents/skills/) that generate GenAI components from problem‑oriented [prompts](/.agents/skills/start-dhti/examples/e2e-sample.md).*
18
18
 
19
19
  ### Why?
20
20
 
@@ -32,7 +32,12 @@ DHTI includes ready‑to‑use [skills](/.agents/skills/) that can prompt agenti
32
32
 
33
33
  Other skills from the open agent skills ecosystem may be useful too! For example, use `npx skills find clinical trial` to find clinical trial related skills. From the results, you can use `npx skills add <skill-name>` to use the skill in your agentic platform. (e.g.`npx skills add anthropics/healthcare@clinical-trial-protocol-skill`)
34
34
 
35
- **🤖 [AI-Powered Workflow with GitHub Copilot SDK:](/notes/COPILOT.md) - WIP**
35
+ ## **🤖 [AI-Powered Workflow with GitHub Copilot SDK:](/notes/COPILOT.md) - WIP**
36
+ ### Quick example
37
+ ```bash
38
+ npx dhti-cli copilot --model gpt-5.3-codex --skill elixir-generator --prompt "Generate an elixir glycemic_advisor that summarizes diabetic patients' latest lab results and medications"
39
+ npx dhti-cli copilot --model gpt-5.3-codex --skill start-dhti --prompt "Start the glycemic_advisor elixir and display in CDS-Hooks sandbox"
40
+ ```
36
41
 
37
42
  ## Try it out
38
43
  [[Cheatsheet](/notes/cheatsheet.md) | [Download PDF Cheatsheet](https://nuchange.ca/wp-content/uploads/2026/01/dhti_cheatsheet.pdf)]
@@ -12,13 +12,25 @@ export default class Copilot extends Command {
12
12
  model: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
13
  prompt: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
14
  skill: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
+ timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
15
16
  };
17
+ run(): Promise<void>;
18
+ /**
19
+ * Clears the conversation history
20
+ */
21
+ private clearConversationHistory;
16
22
  /**
17
23
  * Detects the appropriate skill based on the prompt content
18
24
  * @param prompt - The user's prompt text
19
25
  * @returns The detected skill name
20
26
  */
21
27
  private detectSkill;
28
+ /**
29
+ * Fetches skill content from GitHub if not available locally
30
+ * @param skillName - The name of the skill to fetch
31
+ * @returns The skill content or null if not found
32
+ */
33
+ private fetchSkillFromGitHub;
22
34
  /**
23
35
  * Gets the path to the conversation history file
24
36
  * @returns The path to the history file
@@ -29,26 +41,15 @@ export default class Copilot extends Command {
29
41
  * @returns Array of conversation turns or empty array if no history
30
42
  */
31
43
  private loadConversationHistory;
32
- /**
33
- * Saves conversation history to file
34
- * @param history - Array of conversation turns to save
35
- */
36
- private saveConversationHistory;
37
- /**
38
- * Clears the conversation history
39
- */
40
- private clearConversationHistory;
41
- /**
42
- * Fetches skill content from GitHub if not available locally
43
- * @param skillName - The name of the skill to fetch
44
- * @returns The skill content or null if not found
45
- */
46
- private fetchSkillFromGitHub;
47
44
  /**
48
45
  * Loads skill instructions from local or remote source
49
46
  * @param skillName - The name of the skill to load
50
47
  * @returns The skill content or null if not found
51
48
  */
52
49
  private loadSkill;
53
- run(): Promise<void>;
50
+ /**
51
+ * Saves conversation history to file
52
+ * @param history - Array of conversation turns to save
53
+ */
54
+ private saveConversationHistory;
54
55
  }
@@ -13,13 +13,15 @@ export default class Copilot extends Command {
13
13
  static description = 'Interact with DHTI using GitHub Copilot SDK with streaming responses';
14
14
  static examples = [
15
15
  '<%= config.bin %> <%= command.id %> --prompt "Start the DHTI stack with langserve"',
16
- '<%= config.bin %> <%= command.id %> --file ./my-prompt.txt --model gpt-4.1',
16
+ '<%= config.bin %> <%= command.id %> --file ./my-prompt.txt --model gpt-5.2',
17
17
  '<%= config.bin %> <%= command.id %> --prompt "Generate a new elixir for patient risk assessment" --skill elixir-generator',
18
+ '<%= config.bin %> <%= command.id %> --prompt "Complex task" --timeout 300 # Increase timeout for long-running requests',
18
19
  '<%= config.bin %> <%= command.id %> --clear-history --prompt "Start fresh conversation"',
19
20
  '<%= config.bin %> <%= command.id %> --clear-history # Clear history without starting new conversation',
20
21
  ];
21
22
  static flags = {
22
23
  'clear-history': Flags.boolean({
24
+ char: 'c',
23
25
  default: false,
24
26
  description: 'Clear conversation history and start a new session',
25
27
  }),
@@ -31,7 +33,12 @@ export default class Copilot extends Command {
31
33
  model: Flags.string({
32
34
  char: 'm',
33
35
  default: 'gpt-4.1',
34
- description: 'Model to use for copilot-sdk interactions',
36
+ description: 'Model to use for copilot-sdk interactions. Supported models include: ' +
37
+ 'GPT-4.1 (default), GPT-5.1, GPT-5.2, GPT-5.3, o1-mini, o3-mini, o4-mini, ' +
38
+ 'Claude Haiku 4.5, Claude Opus 4.1/4.5/4.6, Claude Sonnet 3.5/3.7/4.5, ' +
39
+ 'Gemini 2.0 Flash, Gemini 2.5 Pro, Gemini 3 Flash/Pro, Grok Code Fast 1, Raptor mini. ' +
40
+ 'Model availability depends on your GitHub Copilot subscription. ' +
41
+ 'See https://docs.github.com/en/copilot/reference/ai-models/supported-models for details.',
35
42
  }),
36
43
  prompt: Flags.string({
37
44
  char: 'p',
@@ -43,142 +50,13 @@ export default class Copilot extends Command {
43
50
  default: 'auto',
44
51
  description: 'Skill to use for copilot-sdk interactions (auto, start-dhti, elixir-generator, conch-generator)',
45
52
  }),
53
+ timeout: Flags.integer({
54
+ char: 't',
55
+ default: 180,
56
+ description: 'Timeout in seconds for model response (default: 180). Increase for complex prompts or slower models.',
57
+ min: 30,
58
+ }),
46
59
  };
47
- /**
48
- * Detects the appropriate skill based on the prompt content
49
- * @param prompt - The user's prompt text
50
- * @returns The detected skill name
51
- */
52
- detectSkill(prompt) {
53
- const lowerPrompt = prompt.toLowerCase();
54
- // Check for elixir-related keywords
55
- if (lowerPrompt.includes('elixir') ||
56
- lowerPrompt.includes('backend') ||
57
- lowerPrompt.includes('langserve') ||
58
- lowerPrompt.includes('genai app')) {
59
- return 'elixir-generator';
60
- }
61
- // Check for conch-related keywords
62
- if (lowerPrompt.includes('conch') ||
63
- lowerPrompt.includes('frontend') ||
64
- lowerPrompt.includes('ui') ||
65
- lowerPrompt.includes('openmrs')) {
66
- return 'conch-generator';
67
- }
68
- // Use start-dhti if prompt includes 'start', 'show', or 'run'
69
- if (lowerPrompt.includes('start') || lowerPrompt.includes('show') || lowerPrompt.includes('run')) {
70
- return 'start-dhti';
71
- }
72
- // If none of the skills match, exit asking for a skill name and show available skills
73
- const availableSkills = ['start-dhti', 'elixir-generator', 'conch-generator'];
74
- this.error(`Could not detect the appropriate skill from the prompt.\n` +
75
- `Please specify a skill name using --skill.\n` +
76
- `Available skills: ${availableSkills.join(', ')}`);
77
- return '';
78
- }
79
- /**
80
- * Gets the path to the conversation history file
81
- * @returns The path to the history file
82
- */
83
- getHistoryFilePath() {
84
- const dhtiDir = path.join(os.homedir(), '.dhti');
85
- if (!fs.existsSync(dhtiDir)) {
86
- fs.mkdirSync(dhtiDir, { recursive: true });
87
- }
88
- return path.join(dhtiDir, 'copilot-history.json');
89
- }
90
- /**
91
- * Loads conversation history from file
92
- * @returns Array of conversation turns or empty array if no history
93
- */
94
- loadConversationHistory() {
95
- try {
96
- const historyPath = this.getHistoryFilePath();
97
- if (fs.existsSync(historyPath)) {
98
- const historyData = fs.readFileSync(historyPath, 'utf8');
99
- return JSON.parse(historyData);
100
- }
101
- }
102
- catch (error) {
103
- this.warn(chalk.yellow(`Failed to load conversation history: ${error}`));
104
- }
105
- return [];
106
- }
107
- /**
108
- * Saves conversation history to file
109
- * @param history - Array of conversation turns to save
110
- */
111
- saveConversationHistory(history) {
112
- try {
113
- const historyPath = this.getHistoryFilePath();
114
- fs.writeFileSync(historyPath, JSON.stringify(history, null, 2), 'utf8');
115
- }
116
- catch (error) {
117
- this.warn(chalk.yellow(`Failed to save conversation history: ${error}`));
118
- }
119
- }
120
- /**
121
- * Clears the conversation history
122
- */
123
- clearConversationHistory() {
124
- try {
125
- const historyPath = this.getHistoryFilePath();
126
- if (fs.existsSync(historyPath)) {
127
- fs.unlinkSync(historyPath);
128
- this.log(chalk.green('✓ Conversation history cleared'));
129
- }
130
- else {
131
- this.log(chalk.yellow('No conversation history to clear'));
132
- }
133
- }
134
- catch (error) {
135
- this.warn(chalk.yellow(`Failed to clear conversation history: ${error}`));
136
- }
137
- }
138
- /**
139
- * Fetches skill content from GitHub if not available locally
140
- * @param skillName - The name of the skill to fetch
141
- * @returns The skill content or null if not found
142
- */
143
- async fetchSkillFromGitHub(skillName) {
144
- try {
145
- const url = `https://raw.githubusercontent.com/dermatologist/dhti/develop/.agents/skills/${skillName}/SKILL.md`;
146
- const response = await fetch(url);
147
- if (!response.ok) {
148
- return null;
149
- }
150
- return response.text();
151
- }
152
- catch (error) {
153
- this.warn(`Failed to fetch skill ${skillName} from GitHub: ${error}`);
154
- return null;
155
- }
156
- }
157
- /**
158
- * Loads skill instructions from local or remote source
159
- * @param skillName - The name of the skill to load
160
- * @returns The skill content or null if not found
161
- */
162
- async loadSkill(skillName) {
163
- // Resolve skills directory
164
- const __filename = fileURLToPath(import.meta.url);
165
- const __dirname = path.dirname(__filename);
166
- const skillsDir = path.resolve(__dirname, '../../.agents/skills');
167
- const skillPath = path.join(skillsDir, skillName, 'SKILL.md');
168
- // Try to load from local directory first
169
- if (fs.existsSync(skillPath)) {
170
- try {
171
- return fs.readFileSync(skillPath, 'utf8');
172
- }
173
- catch (error) {
174
- this.warn(`Failed to read local skill file: ${error}`);
175
- }
176
- }
177
- // If not found locally, try to fetch from GitHub
178
- this.log(chalk.yellow(`Skill ${skillName} not found locally, fetching from GitHub...`));
179
- return this.fetchSkillFromGitHub(skillName);
180
- }
181
- // eslint-disable-next-line perfectionist/sort-classes
182
60
  async run() {
183
61
  const { flags } = await this.parse(Copilot);
184
62
  // Handle clear-history flag
@@ -273,14 +151,33 @@ export default class Copilot extends Command {
273
151
  process.stdout.write(content);
274
152
  assistantResponse += content;
275
153
  });
276
- // Handle session idle (response complete)
277
- session.on('session.idle', () => {
278
- if (responseStarted) {
279
- console.log('\n'); // Add newline after response
280
- }
154
+ // Handle session errors
155
+ session.on('session.error', (error) => {
156
+ this.warn(chalk.yellow(`Session error: ${error}`));
157
+ });
158
+ // Send the prompt with a configurable timeout
159
+ const timeoutMs = flags.timeout * 1000;
160
+ const timeoutPromise = new Promise((_, reject) => {
161
+ setTimeout(() => {
162
+ reject(new Error(`Request timeout after ${timeoutMs}ms - increase with --timeout flag if needed`));
163
+ }, timeoutMs);
281
164
  });
282
- // Send the prompt and wait for completion
283
- await session.sendAndWait({ prompt: promptContent });
165
+ this.log(chalk.dim(`⏱ Timeout set to ${flags.timeout} seconds (use --timeout to adjust)`));
166
+ try {
167
+ await Promise.race([session.sendAndWait({ prompt: promptContent }), timeoutPromise]);
168
+ }
169
+ catch (timeoutError) {
170
+ if (assistantResponse.trim()) {
171
+ this.log(chalk.yellow(`\n⚠ Response incomplete due to timeout, but partial response was captured.`));
172
+ this.log(chalk.yellow(`💡 Tip: Try increasing timeout with --timeout ${flags.timeout * 2} for longer responses`));
173
+ }
174
+ else {
175
+ throw timeoutError;
176
+ }
177
+ }
178
+ if (responseStarted) {
179
+ console.log('\n'); // Add newline after response
180
+ }
284
181
  this.log(chalk.blue('\n--- End of Response ---\n'));
285
182
  // Save conversation history
286
183
  conversationHistory.push({ content: promptContent, role: 'user' });
@@ -289,6 +186,7 @@ export default class Copilot extends Command {
289
186
  }
290
187
  this.saveConversationHistory(conversationHistory);
291
188
  this.log(chalk.dim(`💾 Conversation saved (${conversationHistory.length} messages). Use --clear-history to reset.`));
189
+ this.log(chalk.yellow('🧹 Cleaning up session, please wait...'));
292
190
  }
293
191
  catch (error) {
294
192
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -305,4 +203,138 @@ export default class Copilot extends Command {
305
203
  }
306
204
  }
307
205
  }
206
+ /**
207
+ * Clears the conversation history
208
+ */
209
+ clearConversationHistory() {
210
+ try {
211
+ const historyPath = this.getHistoryFilePath();
212
+ if (fs.existsSync(historyPath)) {
213
+ fs.unlinkSync(historyPath);
214
+ this.log(chalk.green('✓ Conversation history cleared'));
215
+ }
216
+ else {
217
+ this.log(chalk.yellow('No conversation history to clear'));
218
+ }
219
+ }
220
+ catch (error) {
221
+ this.warn(chalk.yellow(`Failed to clear conversation history: ${error}`));
222
+ }
223
+ }
224
+ /**
225
+ * Detects the appropriate skill based on the prompt content
226
+ * @param prompt - The user's prompt text
227
+ * @returns The detected skill name
228
+ */
229
+ detectSkill(prompt) {
230
+ const lowerPrompt = prompt.toLowerCase();
231
+ // Check for elixir-related keywords
232
+ if (lowerPrompt.includes('elixir') ||
233
+ lowerPrompt.includes('backend') ||
234
+ lowerPrompt.includes('langserve') ||
235
+ lowerPrompt.includes('genai app')) {
236
+ return 'elixir-generator';
237
+ }
238
+ // Check for conch-related keywords
239
+ if (lowerPrompt.includes('conch') ||
240
+ lowerPrompt.includes('frontend') ||
241
+ lowerPrompt.includes('ui') ||
242
+ lowerPrompt.includes('openmrs')) {
243
+ return 'conch-generator';
244
+ }
245
+ // Use start-dhti if prompt includes 'start', 'show', or 'run'
246
+ if (lowerPrompt.includes('start') || lowerPrompt.includes('show') || lowerPrompt.includes('run')) {
247
+ return 'start-dhti';
248
+ }
249
+ // If none of the skills match, exit asking for a skill name and show available skills
250
+ const availableSkills = ['start-dhti', 'elixir-generator', 'conch-generator'];
251
+ this.error(`Could not detect the appropriate skill from the prompt.\n` +
252
+ `Please specify a skill name using --skill.\n` +
253
+ `Available skills: ${availableSkills.join(', ')}`);
254
+ return '';
255
+ }
256
+ /**
257
+ * Fetches skill content from GitHub if not available locally
258
+ * @param skillName - The name of the skill to fetch
259
+ * @returns The skill content or null if not found
260
+ */
261
+ async fetchSkillFromGitHub(skillName) {
262
+ try {
263
+ const url = `https://raw.githubusercontent.com/dermatologist/dhti/develop/.agents/skills/${skillName}/SKILL.md`;
264
+ const response = await fetch(url);
265
+ if (!response.ok) {
266
+ return null;
267
+ }
268
+ return response.text();
269
+ }
270
+ catch (error) {
271
+ this.warn(`Failed to fetch skill ${skillName} from GitHub: ${error}`);
272
+ return null;
273
+ }
274
+ }
275
+ /**
276
+ * Gets the path to the conversation history file
277
+ * @returns The path to the history file
278
+ */
279
+ getHistoryFilePath() {
280
+ const dhtiDir = path.join(os.homedir(), '.dhti');
281
+ if (!fs.existsSync(dhtiDir)) {
282
+ fs.mkdirSync(dhtiDir, { recursive: true });
283
+ }
284
+ return path.join(dhtiDir, 'copilot-history.json');
285
+ }
286
+ /**
287
+ * Loads conversation history from file
288
+ * @returns Array of conversation turns or empty array if no history
289
+ */
290
+ loadConversationHistory() {
291
+ try {
292
+ const historyPath = this.getHistoryFilePath();
293
+ if (fs.existsSync(historyPath)) {
294
+ const historyData = fs.readFileSync(historyPath, 'utf8');
295
+ return JSON.parse(historyData);
296
+ }
297
+ }
298
+ catch (error) {
299
+ this.warn(chalk.yellow(`Failed to load conversation history: ${error}`));
300
+ }
301
+ return [];
302
+ }
303
+ /**
304
+ * Loads skill instructions from local or remote source
305
+ * @param skillName - The name of the skill to load
306
+ * @returns The skill content or null if not found
307
+ */
308
+ async loadSkill(skillName) {
309
+ // Resolve skills directory
310
+ const __filename = fileURLToPath(import.meta.url);
311
+ const __dirname = path.dirname(__filename);
312
+ const skillsDir = path.resolve(__dirname, '../../.agents/skills');
313
+ const skillPath = path.join(skillsDir, skillName, 'SKILL.md');
314
+ // Try to load from local directory first
315
+ if (fs.existsSync(skillPath)) {
316
+ try {
317
+ return fs.readFileSync(skillPath, 'utf8');
318
+ }
319
+ catch (error) {
320
+ this.warn(`Failed to read local skill file: ${error}`);
321
+ }
322
+ }
323
+ // If not found locally, try to fetch from GitHub
324
+ this.log(chalk.yellow(`Skill ${skillName} not found locally, fetching from GitHub...`));
325
+ return this.fetchSkillFromGitHub(skillName);
326
+ }
327
+ /**
328
+ * Saves conversation history to file
329
+ * @param history - Array of conversation turns to save
330
+ */
331
+ saveConversationHistory(history) {
332
+ try {
333
+ const historyPath = this.getHistoryFilePath();
334
+ fs.writeFileSync(historyPath, JSON.stringify(history, null, 2), 'utf8');
335
+ }
336
+ catch (error) {
337
+ this.warn(chalk.yellow(`Failed to save conversation history: ${error}`));
338
+ }
339
+ }
308
340
  }
@@ -181,13 +181,15 @@
181
181
  "description": "Interact with DHTI using GitHub Copilot SDK with streaming responses",
182
182
  "examples": [
183
183
  "<%= config.bin %> <%= command.id %> --prompt \"Start the DHTI stack with langserve\"",
184
- "<%= config.bin %> <%= command.id %> --file ./my-prompt.txt --model gpt-4.1",
184
+ "<%= config.bin %> <%= command.id %> --file ./my-prompt.txt --model gpt-5.2",
185
185
  "<%= config.bin %> <%= command.id %> --prompt \"Generate a new elixir for patient risk assessment\" --skill elixir-generator",
186
+ "<%= config.bin %> <%= command.id %> --prompt \"Complex task\" --timeout 300 # Increase timeout for long-running requests",
186
187
  "<%= config.bin %> <%= command.id %> --clear-history --prompt \"Start fresh conversation\"",
187
188
  "<%= config.bin %> <%= command.id %> --clear-history # Clear history without starting new conversation"
188
189
  ],
189
190
  "flags": {
190
191
  "clear-history": {
192
+ "char": "c",
191
193
  "description": "Clear conversation history and start a new session",
192
194
  "name": "clear-history",
193
195
  "allowNo": false,
@@ -206,7 +208,7 @@
206
208
  },
207
209
  "model": {
208
210
  "char": "m",
209
- "description": "Model to use for copilot-sdk interactions",
211
+ "description": "Model to use for copilot-sdk interactions. Supported models include: GPT-4.1 (default), GPT-5.1, GPT-5.2, GPT-5.3, o1-mini, o3-mini, o4-mini, Claude Haiku 4.5, Claude Opus 4.1/4.5/4.6, Claude Sonnet 3.5/3.7/4.5, Gemini 2.0 Flash, Gemini 2.5 Pro, Gemini 3 Flash/Pro, Grok Code Fast 1, Raptor mini. Model availability depends on your GitHub Copilot subscription. See https://docs.github.com/en/copilot/reference/ai-models/supported-models for details.",
210
212
  "name": "model",
211
213
  "default": "gpt-4.1",
212
214
  "hasDynamicHelp": false,
@@ -232,6 +234,15 @@
232
234
  "hasDynamicHelp": false,
233
235
  "multiple": false,
234
236
  "type": "option"
237
+ },
238
+ "timeout": {
239
+ "char": "t",
240
+ "description": "Timeout in seconds for model response (default: 180). Increase for complex prompts or slower models.",
241
+ "name": "timeout",
242
+ "default": 180,
243
+ "hasDynamicHelp": false,
244
+ "multiple": false,
245
+ "type": "option"
235
246
  }
236
247
  },
237
248
  "hasDynamicHelp": false,
@@ -882,5 +893,5 @@
882
893
  ]
883
894
  }
884
895
  },
885
- "version": "1.3.1"
896
+ "version": "1.3.3"
886
897
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dhti-cli",
3
3
  "description": "DHTI CLI",
4
- "version": "1.3.1",
4
+ "version": "1.3.3",
5
5
  "author": "Bell Eapen",
6
6
  "bin": {
7
7
  "dhti-cli": "bin/run.js"