Skip to content

Commit 0512c0f

Browse files
committed
1.2.0: Templates for LLM
1 parent 6912f89 commit 0512c0f

File tree

6 files changed

+643
-38
lines changed

6 files changed

+643
-38
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lenne.tech/cli",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "lenne.Tech CLI: lt",
55
"keywords": [
66
"lenne.Tech",

src/commands/templates/llm.ts

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import { spawn } from 'child_process';
2+
import { GluegunCommand } from 'gluegun';
3+
import { join } from 'path';
4+
5+
import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox';
6+
7+
/**
8+
* Copy text to clipboard (cross-platform)
9+
*/
10+
async function copyToClipboard(text: string): Promise<boolean> {
11+
try {
12+
const platform = process.platform;
13+
if (platform === 'darwin') {
14+
return new Promise((resolve) => {
15+
const proc = spawn('pbcopy');
16+
proc.stdin.write(text);
17+
proc.stdin.end();
18+
proc.on('close', () => resolve(true));
19+
proc.on('error', () => resolve(false));
20+
});
21+
} else if (platform === 'linux') {
22+
try {
23+
return new Promise((resolve) => {
24+
const proc = spawn('xclip', ['-selection', 'clipboard']);
25+
proc.stdin.write(text);
26+
proc.stdin.end();
27+
proc.on('close', () => resolve(true));
28+
proc.on('error', () => resolve(false));
29+
});
30+
} catch {
31+
return false;
32+
}
33+
} else if (platform === 'win32') {
34+
return new Promise((resolve) => {
35+
const proc = spawn('clip');
36+
proc.stdin.write(text);
37+
proc.stdin.end();
38+
proc.on('close', () => resolve(true));
39+
proc.on('error', () => resolve(false));
40+
});
41+
}
42+
return false;
43+
} catch {
44+
return false;
45+
}
46+
}
47+
48+
/**
49+
* Extract prompt content (everything after frontmatter)
50+
*/
51+
function getPromptContent(promptPath: string, filesystem: any): string {
52+
const content = filesystem.read(promptPath);
53+
// Remove frontmatter if present
54+
const withoutFrontmatter = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
55+
return withoutFrontmatter.trim();
56+
}
57+
58+
/**
59+
* Get prompt metadata from .md frontmatter
60+
*/
61+
function getPromptMetadata(promptPath: string, filesystem: any): { description: string; name: string } {
62+
if (!filesystem.exists(promptPath)) {
63+
return { description: 'No description available', name: '' };
64+
}
65+
66+
const content = filesystem.read(promptPath);
67+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
68+
if (!frontmatterMatch) {
69+
return { description: 'No description available', name: '' };
70+
}
71+
72+
const frontmatter = frontmatterMatch[1];
73+
const descMatch = frontmatter.match(/description:\s*([^\n]+)/);
74+
const nameMatch = frontmatter.match(/name:\s*([^\n]+)/);
75+
76+
return {
77+
description: descMatch ? descMatch[1].trim() : 'No description available',
78+
name: nameMatch ? nameMatch[1].trim() : '',
79+
};
80+
}
81+
82+
/**
83+
* LLM Prompt Templates - Get prompts for various LLMs
84+
*/
85+
const LlmCommand: GluegunCommand = {
86+
alias: ['llm', 'prompts'],
87+
description: 'Get LLM prompt templates (e.g., for ChatGPT, Gemini, etc.)',
88+
hidden: false,
89+
name: 'llm',
90+
run: async (toolbox: ExtendedGluegunToolbox) => {
91+
const {
92+
filesystem,
93+
parameters,
94+
print: { error, highlight, info, success, warning },
95+
prompt,
96+
} = toolbox;
97+
98+
try {
99+
// Get the CLI installation directory
100+
const cliRoot = join(__dirname, '..', '..');
101+
const promptsTemplateDir = join(cliRoot, 'templates', 'llm-prompts');
102+
103+
// Check if llm-prompts directory exists
104+
if (!filesystem.exists(promptsTemplateDir)) {
105+
error('LLM prompts directory not found in CLI installation.');
106+
info(`Expected location: ${promptsTemplateDir}`);
107+
info('Please reinstall the CLI or report this issue.');
108+
return;
109+
}
110+
111+
// Get all available prompts (*.md files)
112+
const allFiles = filesystem.list(promptsTemplateDir) || [];
113+
const availablePrompts = allFiles.filter(file => file.endsWith('.md'));
114+
115+
if (availablePrompts.length === 0) {
116+
error('No LLM prompts found in CLI installation.');
117+
return;
118+
}
119+
120+
// Show available prompts if no specific prompt requested
121+
let selectedPrompt: string;
122+
123+
if (parameters.first) {
124+
// Check if the requested prompt exists
125+
const requestedPrompt = parameters.first.endsWith('.md')
126+
? parameters.first
127+
: `${parameters.first}.md`;
128+
129+
if (!availablePrompts.includes(requestedPrompt)) {
130+
error(`Prompt "${parameters.first}" not found.`);
131+
info('');
132+
info('Available prompts:');
133+
availablePrompts.forEach(p => {
134+
const promptPath = join(promptsTemplateDir, p);
135+
const metadata = getPromptMetadata(promptPath, filesystem);
136+
const promptName = p.replace('.md', '');
137+
info(` • ${promptName}`);
138+
info(` ${metadata.description}`);
139+
info('');
140+
});
141+
return;
142+
}
143+
selectedPrompt = requestedPrompt;
144+
} else {
145+
// Interactive mode: show menu
146+
info('');
147+
info('Available LLM Prompt Templates:');
148+
info('');
149+
150+
const choices: Array<{ message: string; name: string; value: string }> = [];
151+
152+
availablePrompts.forEach(p => {
153+
const promptPath = join(promptsTemplateDir, p);
154+
const metadata = getPromptMetadata(promptPath, filesystem);
155+
const promptName = p.replace('.md', '');
156+
choices.push({
157+
message: `${promptName} - ${metadata.description}`,
158+
name: promptName,
159+
value: p,
160+
});
161+
});
162+
163+
const { selected } = await prompt.ask({
164+
choices: choices.map(c => c.message),
165+
message: 'Select a prompt template:',
166+
name: 'selected',
167+
type: 'select',
168+
});
169+
170+
// Find the selected prompt file
171+
const selectedChoice = choices.find(c => c.message === selected);
172+
if (!selectedChoice) {
173+
error('Invalid selection.');
174+
return;
175+
}
176+
selectedPrompt = selectedChoice.value;
177+
}
178+
179+
// Get prompt content
180+
const promptPath = join(promptsTemplateDir, selectedPrompt);
181+
const promptContent = getPromptContent(promptPath, filesystem);
182+
const metadata = getPromptMetadata(promptPath, filesystem);
183+
184+
// Determine output method
185+
let outputMethod: string;
186+
187+
if (parameters.options.output || parameters.options.o) {
188+
// Save to file
189+
outputMethod = 'file';
190+
} else if (parameters.options.clipboard || parameters.options.c) {
191+
// Copy to clipboard
192+
outputMethod = 'clipboard';
193+
} else if (parameters.options.display || parameters.options.d) {
194+
// Display only
195+
outputMethod = 'display';
196+
} else {
197+
// Interactive: ask user
198+
const { action } = await prompt.ask({
199+
choices: [
200+
'Display in terminal',
201+
'Copy to clipboard',
202+
'Save as Markdown file',
203+
],
204+
message: 'What would you like to do with this prompt?',
205+
name: 'action',
206+
type: 'select',
207+
});
208+
209+
if (action === 'Display in terminal') {
210+
outputMethod = 'display';
211+
} else if (action === 'Copy to clipboard') {
212+
outputMethod = 'clipboard';
213+
} else {
214+
outputMethod = 'file';
215+
}
216+
}
217+
218+
// Execute output method
219+
if (outputMethod === 'clipboard') {
220+
const copied = await copyToClipboard(promptContent);
221+
if (copied) {
222+
success('✓ Prompt copied to clipboard!');
223+
info('');
224+
info('You can now paste it into ChatGPT, Gemini, Claude, or any other LLM.');
225+
} else {
226+
warning('Could not copy to clipboard automatically.');
227+
info('');
228+
info('The prompt will be displayed below for manual copying:');
229+
info('');
230+
info('─'.repeat(60));
231+
info(promptContent);
232+
info('─'.repeat(60));
233+
}
234+
} else if (outputMethod === 'display') {
235+
info('');
236+
info('─'.repeat(60));
237+
highlight(`📝 ${metadata.name || selectedPrompt.replace('.md', '')}`);
238+
info('─'.repeat(60));
239+
info('');
240+
info(promptContent);
241+
info('');
242+
info('─'.repeat(60));
243+
success('Prompt displayed above. Copy and paste into your preferred LLM.');
244+
} else if (outputMethod === 'file') {
245+
let outputPath: string;
246+
247+
if (typeof parameters.options.output === 'string') {
248+
outputPath = parameters.options.output;
249+
} else if (typeof parameters.options.o === 'string') {
250+
outputPath = parameters.options.o;
251+
} else {
252+
// Ask for file path
253+
const defaultFilename = selectedPrompt;
254+
const { filepath } = await prompt.ask({
255+
initial: defaultFilename,
256+
message: 'Enter the file path to save:',
257+
name: 'filepath',
258+
type: 'input',
259+
});
260+
outputPath = filepath;
261+
}
262+
263+
// Ensure .md extension
264+
if (!outputPath.endsWith('.md')) {
265+
outputPath += '.md';
266+
}
267+
268+
// Check if file exists
269+
if (filesystem.exists(outputPath)) {
270+
const { overwrite } = await prompt.ask({
271+
initial: false,
272+
message: `File "${outputPath}" already exists. Overwrite?`,
273+
name: 'overwrite',
274+
type: 'confirm',
275+
});
276+
277+
if (!overwrite) {
278+
info('Operation cancelled.');
279+
return;
280+
}
281+
}
282+
283+
// Write file with frontmatter
284+
const fileContent = `---
285+
name: ${metadata.name || selectedPrompt.replace('.md', '')}
286+
description: ${metadata.description}
287+
source: lenne.tech CLI (lt templates llm)
288+
---
289+
290+
${promptContent}
291+
`;
292+
293+
filesystem.write(outputPath, fileContent);
294+
success(`✓ Prompt saved to: ${outputPath}`);
295+
}
296+
297+
info('');
298+
299+
} catch (err) {
300+
error(`Failed to process prompt: ${err.message}`);
301+
process.exit(1);
302+
}
303+
304+
process.exit(0);
305+
},
306+
};
307+
308+
export default LlmCommand;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox';
2+
3+
/**
4+
* Templates commands
5+
*/
6+
module.exports = {
7+
alias: ['tpl'],
8+
description: 'Template commands for LLM prompts and more',
9+
hidden: false,
10+
name: 'templates',
11+
run: async (toolbox: ExtendedGluegunToolbox) => {
12+
await toolbox.helper.showMenu('templates');
13+
return 'templates';
14+
},
15+
};

0 commit comments

Comments
 (0)