thelapyae 0.3.1 → 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/CHANGELOG.md CHANGED
@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.0] - 2026-01-20
9
+
10
+ ### Added
11
+ - **Full TUI Overhaul**: Implemented a rich Text User Interface (TUI) with a centralized layout and header.
12
+ - **Integrated CLI-to-TUI Consultation**: Running `thelapyae "your question"` now seamlessly launches the TUI and auto-starts the consultation.
13
+ - **Dynamic UI Labels**: Titles and input box labels now update dynamically based on the current mode (e.g., "Consultation", "API Configuration", "Help").
14
+ - **Inline Commands**: Converted popup dialogs for `/help`, `/config`, and `/sessions` into inline views for a more fluid experience.
15
+ - **New Slash Commands**: Added `/new` to start a fresh session and `/exit` to cleanly quit the application.
16
+ - **Branding & Aesthetics**: Added brand color (#f3c12b) and improved text alignment and layout responsiveness.
17
+
18
+ ### Changed
19
+ - **Navigation**: Standardized Esc key to act as a "Back" button to return to the welcome screen from any command view.
20
+ - **Default Behavior**: Removed redundant `q` and `Ctrl+N` keyboard shortcuts in favor of more explicit slash commands and Ctrl+C.
21
+ - **Layout**: Simplified the main layout to prioritize readability with left-aligned examples and centered headers.
22
+
23
+ ### Fixed
24
+ - **Input Issues**: Resolved double character input and accidental Vim mode triggers in the TUI input box.
25
+ - **Service Integration**: Fixed errors in `SearchService`, `ConversationService`, and `SessionService` integration.
26
+ - **Terminal Behavior**: Improved Ctrl+C handling for reliable immediate exit.
27
+ - **API Config**: Fixed input blocking and Esc key behavior in the API configuration view.
28
+
29
+ ---
30
+
31
+ ## [0.4.0] - 2026-01-20
32
+
33
+ ### Added
34
+ - Initial implementation of the `blessed`-based TUI framework.
35
+ - Basic support for interactive slash commands.
36
+ - Enhanced search service with Fuse.js integration.
37
+
38
+ ---
39
+
8
40
  ## [0.3.1] - 2026-01-18
9
41
 
10
42
  ### Fixed
package/README.md CHANGED
@@ -21,7 +21,16 @@
21
21
 
22
22
  It features a **Think-with-AI** mode where it acts as a consultant, analyzing your specific life or work problems using these mental models to provide actionable advice. Even without AI, it provides powerful offline synthesis combining multiple perspectives.
23
23
 
24
- ## ✨ What's New in v0.2.0
24
+ ## ✨ What's New in v0.5.0
25
+
26
+ - 🎨 **Unified TUI Experience**: Entirely redesigned terminal interface with consistent branding (#f3c12b).
27
+ - ⌨️ **Slash Command Power**: New `/new` and `/exit` commands for better session management.
28
+ - 🔍 **Integrated CLI-to-TUI**: Run `thelapyae "question"` to jump directly into a TUI consultation.
29
+ - 📑 **Inline Views**: Help, Configuration, and Sessions now display inline for a smoother workflow.
30
+ - ⌨️ **Esc to Back**: Standardized **Esc** key navigation to return to the welcome screen from anywhere.
31
+ - 🎯 **Fixed Input**: Resolved double-character and Vim mode issues for a fluid typing experience.
32
+
33
+ ### Previous Updates (v0.2.0-v0.3.0)
25
34
 
26
35
  - 🔐 **Secure Storage**: API keys now stored in OS keychain (not plain text)
27
36
  - 🔍 **Better Search**: Fuzzy matching with typo tolerance
@@ -47,7 +56,44 @@ npm install -g thelapyae
47
56
 
48
57
  ## 💡 Usage
49
58
 
50
- ### Interactive Mode (Recommended)
59
+ ### TUI Mode (Recommended) 🎨
60
+
61
+ The **new rich terminal interface** provides a beautiful, interactive experience:
62
+
63
+ ```bash
64
+ $ thelapyae
65
+
66
+ # You'll see a stunning centralized interface:
67
+ # - Header: Brand logo and version
68
+ # - Content: Consultations, Help, or History
69
+ # - Bottom: Responsive input box
70
+
71
+ # Commands:
72
+ # /help - Show interactive help
73
+ # /new - Start a new session
74
+ # /exit - Cleanly quit
75
+ # /random - Get a random model
76
+ # /list - List all models
77
+ # /sessions - View past sessions
78
+ # /config - Setup API key
79
+
80
+ # Navigation:
81
+ # Enter - Submit question/command
82
+ # Esc - Back to Welcome / Clear input
83
+ # Ctrl+C - Force Quit
84
+ ```
85
+
86
+ **Features:**
87
+ - 🎨 Beautiful interface with brand colors
88
+ - ⌨️ Intuitive slash commands
89
+ - 🔍 Live autocomplete as you type `/`
90
+ - 💬 Interactive AI consultations
91
+ - 📚 Browse session history inline
92
+ - 🎯 Zero mouse required
93
+
94
+ ---
95
+
96
+ ### Interactive Mode (Classic)
51
97
 
52
98
  Just run `thelapyae` to enter an immersive interactive session:
53
99
 
package/bin/thelapyae.js CHANGED
@@ -116,19 +116,20 @@ program
116
116
 
117
117
  // Default action (when no command is specified)
118
118
  program.action(async () => {
119
- const InteractiveMode = require('../src/services/interactive-mode');
120
- const interactive = new InteractiveMode(models, packageJson);
121
- await interactive.start();
119
+ const TUI = require('../src/tui');
120
+ const tui = new TUI(models, packageJson);
121
+ tui.start();
122
122
  });
123
123
 
124
- // Handle unknown commands - treat as consult query
124
+ // Handle unknown commands - treat as consult query in TUI
125
125
  program.on('command:*', async function (operands) {
126
126
  const query = operands.join(' ');
127
127
 
128
- // If it looks like a question or statement, treat as consult
128
+ // If it looks like a question or statement, launch TUI with query
129
129
  if (query && query.length > 0) {
130
- await consultCommand(query, models, program.opts());
131
- process.exit(0);
130
+ const TUI = require('../src/tui');
131
+ const tui = new TUI(models, packageJson);
132
+ tui.start(query);
132
133
  } else {
133
134
  console.error(chalk.red(`\n❌ Unknown command: ${operands[0]}\n`));
134
135
  console.log(chalk.gray('Run'), chalk.cyan('thelapyae --help'), chalk.gray('for available commands\n'));
@@ -138,18 +139,19 @@ program.on('command:*', async function (operands) {
138
139
 
139
140
  // Parse arguments
140
141
  if (process.argv.length === 2) {
141
- // No arguments - start interactive mode directly
142
+ // No arguments - start TUI mode directly
142
143
  (async () => {
143
- const InteractiveMode = require('../src/services/interactive-mode');
144
- const interactive = new InteractiveMode(models, packageJson);
145
- await interactive.start();
144
+ const TUI = require('../src/tui');
145
+ const tui = new TUI(models, packageJson);
146
+ tui.start();
146
147
  })();
147
148
  } else if (process.argv.length > 2 && !process.argv[2].startsWith('-')) {
148
- // Direct question provided (not a command or option)
149
+ // Direct question provided (not a command or option) - launch TUI with question
149
150
  const query = process.argv.slice(2).join(' ');
150
151
  (async () => {
151
- await consultCommand(query, models, {});
152
- process.exit(0);
152
+ const TUI = require('../src/tui');
153
+ const tui = new TUI(models, packageJson);
154
+ tui.start(query);
153
155
  })();
154
156
  } else {
155
157
  // Parse commands normally
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "thelapyae",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Personal CLI of La Pyae – mental models, stoicism, clear thinking",
5
5
  "bin": {
6
- "thelapyae": "./bin/thelapyae.js"
6
+ "thelapyae": "bin/thelapyae.js"
7
7
  },
8
8
  "author": "La Pyae",
9
9
  "license": "MIT",
@@ -14,6 +14,8 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@google/generative-ai": "^0.24.1",
17
+ "blessed": "^0.1.81",
18
+ "blessed-contrib": "^4.11.0",
17
19
  "boxen": "^8.0.1",
18
20
  "chalk": "^4.1.2",
19
21
  "cli-table3": "^0.6.3",
@@ -24,6 +26,7 @@
24
26
  "gradient-string": "^3.0.0",
25
27
  "keytar": "^7.9.0",
26
28
  "natural": "^6.10.0",
29
+ "neo-blessed": "^0.2.0",
27
30
  "ora": "^5.4.1",
28
31
  "terminal-link": "^5.0.0"
29
32
  },
@@ -13,8 +13,9 @@ class ConversationService {
13
13
  /**
14
14
  * Generate reflection questions using AI
15
15
  */
16
- async generateReflectionQuestions(query, models, count = 5) {
17
- const spinner = ora('Generating reflection questions...').start();
16
+ async generateReflectionQuestions(query, models, count = 5, options = {}) {
17
+ const { showSpinner = true } = options;
18
+ const spinner = showSpinner ? ora('Generating reflection questions...').start() : null;
18
19
 
19
20
  try {
20
21
  const prompt = `You are a thoughtful consultant helping someone think through: "${query}"
@@ -55,11 +56,11 @@ Make them simple, clear, and thought-provoking.`;
55
56
  // Parse JSON
56
57
  const questions = JSON.parse(text);
57
58
 
58
- spinner.succeed(`Generated ${questions.length} reflection questions`);
59
+ if (spinner) spinner.succeed(`Generated ${questions.length} reflection questions`);
59
60
  return questions;
60
61
 
61
62
  } catch (error) {
62
- spinner.fail('Failed to generate reflection questions');
63
+ if (spinner) spinner.fail('Failed to generate reflection questions');
63
64
  throw new Error(`Could not generate questions: ${error.message}`);
64
65
  }
65
66
  }
@@ -136,8 +137,9 @@ Make them simple, clear, and thought-provoking.`;
136
137
  /**
137
138
  * Generate final analysis with mental models
138
139
  */
139
- async generateFinalAnalysis(query, questionsAndAnswers, models) {
140
- const spinner = ora('Analyzing your responses with mental models...').start();
140
+ async generateFinalAnalysis(query, questionsAndAnswers, models, options = {}) {
141
+ const { showSpinner = true } = options;
142
+ const spinner = showSpinner ? ora('Analyzing your responses with mental models...').start() : null;
141
143
 
142
144
  try {
143
145
  const conversation = this.formatConversation(query, questionsAndAnswers);
@@ -180,11 +182,11 @@ CRITICAL FORMATTING RULES FOR TERMINAL DISPLAY:
180
182
  const response = await result.response;
181
183
  const analysis = response.text();
182
184
 
183
- spinner.succeed('Analysis complete');
185
+ if (spinner) spinner.succeed('Analysis complete');
184
186
  return analysis;
185
187
 
186
188
  } catch (error) {
187
- spinner.fail('Analysis failed');
189
+ if (spinner) spinner.fail('Analysis failed');
188
190
  throw error;
189
191
  }
190
192
  }
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Consultation Panel Component
3
+ * Interactive consultation interface with AI
4
+ */
5
+
6
+ const blessed = require('blessed');
7
+ const theme = require('../theme');
8
+ const SearchService = require('../../services/search-service');
9
+ const ConversationService = require('../../services/conversation-service');
10
+ const AIService = require('../../services/ai-service');
11
+ const SessionService = require('../../services/session-service');
12
+ const configService = require('../../services/config-service');
13
+
14
+ class ConsultationPanel {
15
+ constructor(parent, models, layout) {
16
+ this.parent = parent;
17
+ this.models = models;
18
+ this.layout = layout;
19
+
20
+ // Create search service instance
21
+ this.searchService = new SearchService(models);
22
+
23
+ // AI and conversation services will be initialized when needed
24
+ this.aiService = null;
25
+ this.conversationService = null;
26
+
27
+ this.currentQuery = '';
28
+ this.currentQuestions = [];
29
+ this.currentAnswers = [];
30
+ this.relevantModels = [];
31
+
32
+ this.welcomeBox = null;
33
+ this.inputBox = null;
34
+ this.outputBox = null;
35
+ }
36
+
37
+ /**
38
+ * Show the consultation panel
39
+ */
40
+ show() {
41
+ this.parent.setContent('');
42
+ this.showWelcome();
43
+ }
44
+
45
+ /**
46
+ * Show welcome message
47
+ */
48
+ showWelcome() {
49
+ const welcomeText = `{center}{bold}{#f3c12b-fg}Welcome to thelapyae{/#f3c12b-fg}{/bold}{/center}
50
+
51
+ Ask me anything about thinking, decisions, or life.
52
+ I'll help you explore it using mental models.
53
+
54
+ {bold}Examples:{/bold}
55
+ • "Should I quit my job?"
56
+ • "How do I prioritize my tasks?"
57
+ • "I feel overwhelmed with too many projects"
58
+
59
+ {bold}Slash Commands:{/bold}
60
+ {cyan-fg}/random{/cyan-fg} - Get a random mental model
61
+ {cyan-fg}/list{/cyan-fg} - List all mental models
62
+ {cyan-fg}/sessions{/cyan-fg} - View past consultations
63
+ {cyan-fg}/config{/cyan-fg} - Configure API key
64
+ {cyan-fg}/help{/cyan-fg} - Show help
65
+ {cyan-fg}/new{/cyan-fg} - Start new session
66
+ {cyan-fg}/exit{/cyan-fg} - Exit application
67
+
68
+ Just type your question or a slash command and press Enter!`;
69
+
70
+ this.parent.setContent(welcomeText);
71
+ this.parent.screen.render();
72
+ }
73
+
74
+ /**
75
+ * Start a new consultation
76
+ */
77
+ async startConsultation() {
78
+ // Create input box
79
+ const inputBox = blessed.textarea({
80
+ parent: this.parent.screen,
81
+ top: 'center',
82
+ left: 'center',
83
+ width: '80%',
84
+ height: 7,
85
+ label: ' What\'s on your mind? ',
86
+ border: theme.boxes.input.border,
87
+ style: theme.boxes.input.style,
88
+ inputOnFocus: true,
89
+ keys: true,
90
+ vi: true
91
+ });
92
+
93
+ inputBox.focus();
94
+ this.parent.screen.render();
95
+
96
+ // Get input
97
+ inputBox.readInput(async (err, value) => {
98
+ if (err || !value || !value.trim()) {
99
+ inputBox.destroy();
100
+ this.parent.screen.render();
101
+ return;
102
+ }
103
+
104
+ this.currentQuery = value.trim();
105
+ inputBox.destroy();
106
+
107
+ // Start consultation flow
108
+ await this.runConsultation();
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Handle a query from the input box
114
+ */
115
+ async handleQuery(query) {
116
+ this.currentQuery = query;
117
+ await this.runConsultation();
118
+ }
119
+
120
+ /**
121
+ * Run the consultation flow
122
+ */
123
+ async runConsultation() {
124
+ try {
125
+ // Show loading
126
+ this.showLoading('Analyzing your question...');
127
+
128
+ // Find relevant models using search service
129
+ const searchResults = this.searchService.search(this.currentQuery, 5);
130
+ this.relevantModels = searchResults.map(r => r.model);
131
+
132
+ // Check if API key is configured
133
+ const apiKey = await configService.getApiKey();
134
+
135
+ if (!apiKey) {
136
+ this.showOfflineConsultation();
137
+ return;
138
+ }
139
+
140
+ // Initialize AI services if not already done
141
+ if (!this.aiService) {
142
+ this.aiService = new AIService(apiKey);
143
+ }
144
+ if (!this.conversationService) {
145
+ this.conversationService = new ConversationService(this.aiService);
146
+ }
147
+
148
+ // Generate reflection questions
149
+ this.showLoading('Generating reflection questions...');
150
+ this.currentQuestions = await this.conversationService.generateReflectionQuestions(
151
+ this.currentQuery,
152
+ this.relevantModels,
153
+ 5,
154
+ { showSpinner: false }
155
+ );
156
+
157
+ // Ask questions interactively
158
+ await this.askQuestions();
159
+
160
+ // Generate final analysis
161
+ this.showLoading('Analyzing your responses...');
162
+ const questionsAndAnswers = this.currentQuestions.map((q, i) => ({
163
+ question: q,
164
+ answer: this.currentAnswers[i]
165
+ }));
166
+
167
+ const analysis = await this.conversationService.generateFinalAnalysis(
168
+ this.currentQuery,
169
+ questionsAndAnswers,
170
+ this.relevantModels,
171
+ { showSpinner: false }
172
+ );
173
+
174
+ // Show results
175
+ this.showResults(analysis);
176
+
177
+ // Save session
178
+ const sessionService = new SessionService();
179
+ const modelsUsed = this.relevantModels.map(m => m.title).join(', ');
180
+ sessionService.saveSession(
181
+ this.currentQuery,
182
+ questionsAndAnswers,
183
+ analysis,
184
+ { modelsUsed }
185
+ );
186
+
187
+ } catch (error) {
188
+ this.showError(error.message);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Ask reflection questions one by one
194
+ */
195
+ async askQuestions() {
196
+ this.currentAnswers = [];
197
+
198
+ for (let i = 0; i < this.currentQuestions.length; i++) {
199
+ const question = this.currentQuestions[i];
200
+ const answer = await this.askSingleQuestion(question, i + 1, this.currentQuestions.length);
201
+
202
+ if (answer === null) {
203
+ // User cancelled
204
+ throw new Error('Consultation cancelled');
205
+ }
206
+
207
+ this.currentAnswers.push(answer);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Ask a single question
213
+ */
214
+ askSingleQuestion(question, index, total) {
215
+ return new Promise((resolve) => {
216
+ // Show question in content area
217
+ const questionContent = `{bold}{cyan-fg}Question ${index}/${total}{/cyan-fg}{/bold}
218
+
219
+ {bold}${question}{/bold}
220
+
221
+ {gray-fg}Type your answer in the input box below and press Enter.
222
+ Press Esc to skip this question.{/gray-fg}`;
223
+
224
+ this.parent.setContent(questionContent);
225
+
226
+ // Update input box label
227
+ const mainInputBox = this.layout.inputBox;
228
+ mainInputBox.setLabel(` Type your answer here (Question ${index}/${total}) `);
229
+ mainInputBox.style.focus.border.fg = 'cyan';
230
+
231
+ this.parent.screen.render();
232
+
233
+ // Temporarily override Enter handler
234
+ const tempEnterHandler = (ch, key) => {
235
+ if (key.name === 'enter') {
236
+ const answer = mainInputBox.getValue().trim();
237
+ mainInputBox.clearValue();
238
+
239
+ // Restore original state
240
+ mainInputBox.removeListener('keypress', tempEnterHandler);
241
+ mainInputBox.removeListener('keypress', tempEscHandler);
242
+
243
+ // Reset label
244
+ mainInputBox.setLabel(' Ask me anything... (Press Enter to submit, Esc to clear) ');
245
+ mainInputBox.style.focus.border.fg = 'yellow';
246
+
247
+ resolve(answer || '');
248
+ }
249
+ };
250
+
251
+ // Temporarily override Esc handler to skip
252
+ const tempEscHandler = (ch, key) => {
253
+ if (key.name === 'escape') {
254
+ mainInputBox.clearValue();
255
+
256
+ // Restore original state
257
+ mainInputBox.removeListener('keypress', tempEnterHandler);
258
+ mainInputBox.removeListener('keypress', tempEscHandler);
259
+
260
+ // Reset label
261
+ mainInputBox.setLabel(' Ask me anything... (Press Enter to submit, Esc to clear) ');
262
+ mainInputBox.style.focus.border.fg = 'yellow';
263
+
264
+ resolve(''); // Empty answer means skipped
265
+ }
266
+ };
267
+
268
+ mainInputBox.on('keypress', tempEnterHandler);
269
+ mainInputBox.on('keypress', tempEscHandler);
270
+
271
+ mainInputBox.focus();
272
+ this.parent.screen.render();
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Show loading message
278
+ */
279
+ showLoading(message) {
280
+ this.parent.setContent(`{center}\n\n{bold}{cyan-fg}${message}{/cyan-fg}{/bold}\n\n{gray-fg}Please wait...{/gray-fg}{/center}`);
281
+ this.parent.screen.render();
282
+ }
283
+
284
+ /**
285
+ * Show offline consultation
286
+ */
287
+ showOfflineConsultation() {
288
+ const modelsText = this.relevantModels.map((m, i) =>
289
+ `${i + 1}. {bold}${m.title}{/bold}\n ${m.explanation.substring(0, 200)}...`
290
+ ).join('\n\n');
291
+
292
+ const content = `{center}{bold}{yellow-fg}Offline Mode{/yellow-fg}{/bold}{/center}
293
+
294
+ {bold}Your Question:{/bold}
295
+ ${this.currentQuery}
296
+
297
+ {bold}Relevant Mental Models:{/bold}
298
+
299
+ ${modelsText}
300
+
301
+ {center}{gray-fg}Configure your API key with 'thelapyae config' to enable AI consultation.{/gray-fg}{/center}`;
302
+
303
+ this.parent.setContent(content);
304
+ this.parent.screen.render();
305
+ }
306
+
307
+ /**
308
+ * Show results
309
+ */
310
+ showResults(analysis) {
311
+ const content = `{bold}{green-fg}✓ Analysis Complete{/green-fg}{/bold}
312
+
313
+ {bold}Your Question:{/bold}
314
+ ${this.currentQuery}
315
+
316
+ {bold}Analysis:{/bold}
317
+
318
+ ${analysis}
319
+
320
+ {center}{gray-fg}Session saved. Press Ctrl+N for a new consultation.{/gray-fg}{/center}`;
321
+
322
+ this.parent.setContent(content);
323
+ this.parent.setScrollPerc(0);
324
+ this.parent.screen.render();
325
+ }
326
+
327
+ /**
328
+ * Show error
329
+ */
330
+ showError(message) {
331
+ this.parent.setContent(`{center}\n\n{bold}{red-fg}Error{/red-fg}{/bold}\n\n${message}\n\n{gray-fg}Press Ctrl+N to try again.{/gray-fg}{/center}`);
332
+ this.parent.screen.render();
333
+ }
334
+ }
335
+
336
+ module.exports = ConsultationPanel;