@kikkimo/claude-launcher 1.0.0 → 2.1.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.
@@ -0,0 +1,180 @@
1
+ /**
2
+ * String Width Utilities - Handle multi-language character width calculations
3
+ * Properly calculates display width for CJK (Chinese, Japanese, Korean) characters
4
+ */
5
+
6
+ /**
7
+ * Get the display width of a string in terminal
8
+ * @param {string} str - The string to measure
9
+ * @returns {number} - Display width in terminal columns
10
+ */
11
+ function getStringWidth(str) {
12
+ if (!str || typeof str !== 'string') {
13
+ return 0;
14
+ }
15
+
16
+ let width = 0;
17
+
18
+ // Remove ANSI color codes for accurate width calculation
19
+ const cleanStr = str.replace(/\x1b\[[0-9;]*m/g, '');
20
+
21
+ for (const char of cleanStr) {
22
+ const code = char.codePointAt(0);
23
+
24
+ if (code == null) {
25
+ continue;
26
+ }
27
+
28
+ // East Asian Full Width characters (including Chinese)
29
+ if (isFullWidth(code)) {
30
+ width += 2;
31
+ }
32
+ // Control characters (width 0)
33
+ else if (isControlCharacter(code)) {
34
+ width += 0;
35
+ }
36
+ // Normal characters (width 1)
37
+ else {
38
+ width += 1;
39
+ }
40
+ }
41
+
42
+ return width;
43
+ }
44
+
45
+ /**
46
+ * Pad string to specified display width (considering CJK characters)
47
+ * @param {string} str - String to pad
48
+ * @param {number} targetWidth - Target display width
49
+ * @param {string} padChar - Character to pad with (default: space)
50
+ * @param {string} padDirection - 'end' or 'start' (default: 'end')
51
+ * @returns {string} - Padded string
52
+ */
53
+ function padStringToWidth(str, targetWidth, padChar = ' ', padDirection = 'end') {
54
+ const currentWidth = getStringWidth(str);
55
+ const paddingNeeded = Math.max(0, targetWidth - currentWidth);
56
+ const padding = padChar.repeat(paddingNeeded);
57
+
58
+ return padDirection === 'start' ? padding + str : str + padding;
59
+ }
60
+
61
+ /**
62
+ * Truncate string to specified display width (considering CJK characters)
63
+ * @param {string} str - String to truncate
64
+ * @param {number} maxWidth - Maximum display width
65
+ * @param {string} ellipsis - Ellipsis string (default: '...')
66
+ * @returns {string} - Truncated string
67
+ */
68
+ function truncateStringToWidth(str, maxWidth, ellipsis = '...') {
69
+ const ellipsisWidth = getStringWidth(ellipsis);
70
+
71
+ if (getStringWidth(str) <= maxWidth) {
72
+ return str;
73
+ }
74
+
75
+ if (maxWidth <= ellipsisWidth) {
76
+ return ellipsis.substring(0, maxWidth);
77
+ }
78
+
79
+ let width = 0;
80
+ let result = '';
81
+
82
+ for (const char of str) {
83
+ const charWidth = getStringWidth(char);
84
+
85
+ if (width + charWidth + ellipsisWidth > maxWidth) {
86
+ break;
87
+ }
88
+
89
+ result += char;
90
+ width += charWidth;
91
+ }
92
+
93
+ return result + ellipsis;
94
+ }
95
+
96
+ /**
97
+ * Check if a character code represents a full-width character
98
+ * @param {number} code - Unicode code point
99
+ * @returns {boolean} - True if full-width
100
+ */
101
+ function isFullWidth(code) {
102
+ // Based on Unicode East Asian Width property
103
+ // Full Width (F) and Wide (W) characters
104
+ return (
105
+ // CJK Unified Ideographs
106
+ (code >= 0x4E00 && code <= 0x9FFF) ||
107
+ // CJK Compatibility Ideographs
108
+ (code >= 0xF900 && code <= 0xFAFF) ||
109
+ // CJK Unified Ideographs Extension A
110
+ (code >= 0x3400 && code <= 0x4DBF) ||
111
+ // CJK Unified Ideographs Extension B
112
+ (code >= 0x20000 && code <= 0x2A6DF) ||
113
+ // CJK Unified Ideographs Extension C
114
+ (code >= 0x2A700 && code <= 0x2B73F) ||
115
+ // CJK Unified Ideographs Extension D
116
+ (code >= 0x2B740 && code <= 0x2B81F) ||
117
+ // CJK Unified Ideographs Extension E
118
+ (code >= 0x2B820 && code <= 0x2CEAF) ||
119
+ // CJK Symbols and Punctuation
120
+ (code >= 0x3000 && code <= 0x303F) ||
121
+ // Hiragana
122
+ (code >= 0x3040 && code <= 0x309F) ||
123
+ // Katakana
124
+ (code >= 0x30A0 && code <= 0x30FF) ||
125
+ // Halfwidth and Fullwidth Forms (Fullwidth part)
126
+ (code >= 0xFF01 && code <= 0xFF60) ||
127
+ // CJK Compatibility Forms
128
+ (code >= 0xFE30 && code <= 0xFE4F) ||
129
+ // Other common full-width characters
130
+ code === 0x3000 || // Ideographic space
131
+ code === 0xFF0C || // Fullwidth comma
132
+ code === 0xFF0E || // Fullwidth full stop
133
+ code === 0xFF1A || // Fullwidth colon
134
+ code === 0xFF1B || // Fullwidth semicolon
135
+ code === 0xFF1F || // Fullwidth question mark
136
+ code === 0xFF01 // Fullwidth exclamation mark
137
+ );
138
+ }
139
+
140
+ /**
141
+ * Check if a character code represents a control character
142
+ * @param {number} code - Unicode code point
143
+ * @returns {boolean} - True if control character
144
+ */
145
+ function isControlCharacter(code) {
146
+ return (
147
+ // C0 Controls
148
+ (code >= 0x0000 && code <= 0x001F) ||
149
+ // DEL
150
+ code === 0x007F ||
151
+ // C1 Controls
152
+ (code >= 0x0080 && code <= 0x009F)
153
+ );
154
+ }
155
+
156
+ /**
157
+ * Create a line with proper alignment for mixed-width content
158
+ * @param {string} left - Left content
159
+ * @param {string} right - Right content
160
+ * @param {number} totalWidth - Total line width
161
+ * @param {string} fillChar - Fill character (default: space)
162
+ * @returns {string} - Aligned line
163
+ */
164
+ function createAlignedLine(left, right, totalWidth, fillChar = ' ') {
165
+ const leftWidth = getStringWidth(left);
166
+ const rightWidth = getStringWidth(right);
167
+ const fillWidth = Math.max(0, totalWidth - leftWidth - rightWidth);
168
+ const fill = fillChar.repeat(fillWidth);
169
+
170
+ return left + fill + right;
171
+ }
172
+
173
+ module.exports = {
174
+ getStringWidth,
175
+ padStringToWidth,
176
+ truncateStringToWidth,
177
+ createAlignedLine,
178
+ isFullWidth,
179
+ isControlCharacter
180
+ };
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Version Check Module
3
+ * Checks for updates from npm registry with persistent config file caching
4
+ */
5
+
6
+ const https = require('https');
7
+ const fs = require('fs').promises;
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ /**
12
+ * Get configuration file path
13
+ */
14
+ function getConfigPath() {
15
+ return path.join(os.homedir(), '.claude-launcher-config.json');
16
+ }
17
+
18
+ /**
19
+ * Load configuration from file
20
+ */
21
+ async function loadConfig() {
22
+ try {
23
+ const configPath = getConfigPath();
24
+ const data = await fs.readFile(configPath, 'utf8');
25
+ return JSON.parse(data);
26
+ } catch (error) {
27
+ // Return default config if file doesn't exist
28
+ return {
29
+ language: 'zh',
30
+ lastVersionCheck: 0,
31
+ cachedLatestVersion: null
32
+ };
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Save configuration to file
38
+ */
39
+ async function saveConfig(config) {
40
+ try {
41
+ const configPath = getConfigPath();
42
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
43
+ } catch (error) {
44
+ // Silently fail - not critical
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Fetch latest version from npm registry with timeout
50
+ * @param {string} packageName - Package name to check
51
+ * @param {number} timeoutMs - Timeout in milliseconds (default: 15000)
52
+ */
53
+ function fetchLatestVersion(packageName, timeoutMs = 15000) {
54
+ return new Promise((resolve, reject) => {
55
+ const url = `https://registry.npmjs.org/${packageName}/latest`;
56
+
57
+ const req = https.get(url, (res) => {
58
+ let data = '';
59
+
60
+ res.on('data', (chunk) => {
61
+ data += chunk;
62
+ });
63
+
64
+ res.on('end', () => {
65
+ try {
66
+ const json = JSON.parse(data);
67
+ resolve(json.version);
68
+ } catch (error) {
69
+ reject(new Error(`Failed to parse response: ${error.message}`));
70
+ }
71
+ });
72
+ }).on('error', (err) => {
73
+ reject(new Error(`Network error: ${err.message}`));
74
+ });
75
+
76
+ // Set timeout
77
+ req.setTimeout(timeoutMs, () => {
78
+ req.destroy();
79
+ reject(new Error(`Request timeout (${timeoutMs}ms)`));
80
+ });
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Compare version strings (semantic versioning)
86
+ * Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
87
+ */
88
+ function compareVersions(v1, v2) {
89
+ const parts1 = v1.split('.').map(Number);
90
+ const parts2 = v2.split('.').map(Number);
91
+
92
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
93
+ const part1 = parts1[i] || 0;
94
+ const part2 = parts2[i] || 0;
95
+
96
+ if (part1 > part2) return 1;
97
+ if (part1 < part2) return -1;
98
+ }
99
+
100
+ return 0;
101
+ }
102
+
103
+ /**
104
+ * Check for updates with persistent config file caching
105
+ * @param {boolean} force - Force check regardless of cache
106
+ * @param {number} intervalHours - Cache duration in hours (default: 12)
107
+ * @returns {Promise<{available: boolean, currentVersion: string, latestVersion: string, packageUrl: string}>}
108
+ */
109
+ async function checkForUpdates(force = false, intervalHours = 12) {
110
+ const CACHE_DURATION = intervalHours * 60 * 60 * 1000; // Convert hours to milliseconds
111
+ try {
112
+ const packageInfo = require('../../package.json');
113
+ const currentVersion = packageInfo.version;
114
+ const packageName = packageInfo.name;
115
+ const packageUrl = `https://www.npmjs.com/package/${packageName}`;
116
+
117
+ // Load config to check cache
118
+ const config = await loadConfig();
119
+ const now = Date.now();
120
+
121
+ // Check if we need to fetch the online version
122
+ const needsCheck = force ||
123
+ !config.cachedLatestVersion ||
124
+ !config.lastVersionCheck ||
125
+ (now - config.lastVersionCheck > CACHE_DURATION);
126
+
127
+ let latestVersion;
128
+
129
+ if (needsCheck) {
130
+ // Need to check online
131
+ try {
132
+ latestVersion = await fetchLatestVersion(packageName);
133
+
134
+ // Update config file cache
135
+ config.cachedLatestVersion = latestVersion;
136
+ config.lastVersionCheck = now;
137
+ await saveConfig(config);
138
+
139
+ } catch (networkError) {
140
+ // Use cached version on network error, fallback to current version if no cache
141
+ latestVersion = config.cachedLatestVersion || currentVersion;
142
+ }
143
+ } else {
144
+ // Use cached version from config file
145
+ latestVersion = config.cachedLatestVersion;
146
+ }
147
+
148
+ // Compare versions
149
+ const updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
150
+
151
+ return {
152
+ available: updateAvailable,
153
+ currentVersion,
154
+ latestVersion,
155
+ packageUrl,
156
+ fromCache: !needsCheck
157
+ };
158
+
159
+ } catch (error) {
160
+ // Return current version info on error
161
+ const packageInfo = require('../../package.json');
162
+ return {
163
+ available: false,
164
+ currentVersion: packageInfo.version,
165
+ latestVersion: packageInfo.version,
166
+ packageUrl: `https://www.npmjs.com/package/${packageInfo.name}`,
167
+ error: true
168
+ };
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Clear the config file cache (force next check to be from network)
174
+ */
175
+ async function clearCache() {
176
+ try {
177
+ const config = await loadConfig();
178
+ config.lastVersionCheck = 0;
179
+ config.cachedLatestVersion = null;
180
+ await saveConfig(config);
181
+ } catch (error) {
182
+ // Silently fail
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Force check for updates (always check online, ignore cache, with custom timeout)
188
+ * @param {number} timeoutMs - Timeout in milliseconds (default: 15000)
189
+ * @returns {Promise<{available: boolean, currentVersion: string, latestVersion: string, packageUrl: string, error?: string}>}
190
+ */
191
+ async function forceCheckForUpdates(timeoutMs = 15000) {
192
+ try {
193
+ const packageInfo = require('../../package.json');
194
+ const currentVersion = packageInfo.version;
195
+ const packageName = packageInfo.name;
196
+ const packageUrl = `https://www.npmjs.com/package/${packageName}`;
197
+
198
+ // Always fetch from network
199
+ const latestVersion = await fetchLatestVersion(packageName, timeoutMs);
200
+
201
+ // Update config cache with new result
202
+ try {
203
+ const config = await loadConfig();
204
+ config.cachedLatestVersion = latestVersion;
205
+ config.lastVersionCheck = Date.now();
206
+ await saveConfig(config);
207
+ } catch (configError) {
208
+ // Ignore config save errors
209
+ }
210
+
211
+ // Compare versions
212
+ const updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
213
+
214
+ return {
215
+ available: updateAvailable,
216
+ currentVersion,
217
+ latestVersion,
218
+ packageUrl
219
+ };
220
+
221
+ } catch (error) {
222
+ // Return error information
223
+ const packageInfo = require('../../package.json');
224
+ return {
225
+ available: false,
226
+ currentVersion: packageInfo.version,
227
+ latestVersion: packageInfo.version,
228
+ packageUrl: `https://www.npmjs.com/package/${packageInfo.name}`,
229
+ error: error.message
230
+ };
231
+ }
232
+ }
233
+
234
+ module.exports = {
235
+ checkForUpdates,
236
+ forceCheckForUpdates,
237
+ clearCache,
238
+ loadConfig,
239
+ saveConfig
240
+ };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Validators Module - Input validation functions
3
+ */
4
+
5
+ const i18n = require('./i18n');
6
+
7
+ /**
8
+ * Validate base URL format
9
+ */
10
+ function validateBaseUrl(url) {
11
+ if (!url || url.trim() === '') {
12
+ return { valid: false, error: i18n.tSync('errors.validation.base_url_empty') };
13
+ }
14
+
15
+ try {
16
+ const urlObj = new URL(url);
17
+
18
+ // Ensure it's HTTP or HTTPS
19
+ if (!['http:', 'https:'].includes(urlObj.protocol)) {
20
+ return { valid: false, error: 'URL must use HTTP or HTTPS protocol' };
21
+ }
22
+
23
+ return { valid: true, value: url.trim() };
24
+ } catch (error) {
25
+ return { valid: false, error: i18n.tSync('errors.validation.invalid_url_format') };
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Validate authentication token
31
+ */
32
+ function validateAuthToken(token) {
33
+ if (!token || token.trim() === '') {
34
+ return { valid: false, error: i18n.tSync('errors.validation.auth_token_empty') };
35
+ }
36
+
37
+ if (token.length < 10) {
38
+ return { valid: false, error: i18n.tSync('errors.validation.auth_token_too_short') };
39
+ }
40
+
41
+ return { valid: true, value: token.trim() };
42
+ }
43
+
44
+ /**
45
+ * Validate model name
46
+ */
47
+ function validateModel(model) {
48
+ if (!model || model.trim() === '') {
49
+ return { valid: false, error: i18n.tSync('errors.validation.model_name_empty') };
50
+ }
51
+
52
+ // Check for common model name patterns
53
+ const validPatterns = [
54
+ /^claude-/i, // Claude models
55
+ /^gpt-/i, // OpenAI models
56
+ /^gemini-/i, // Google models
57
+ /^llama-/i, // Meta models
58
+ /^mistral-/i, // Mistral models
59
+ /^deepseek-/i, // DeepSeek models
60
+ /^qwen-/i, // Qwen models
61
+ /^moonshot-/i, // Moonshot models
62
+ ];
63
+
64
+ const hasValidPattern = validPatterns.some(pattern => pattern.test(model));
65
+
66
+ if (!hasValidPattern && model.length < 3) {
67
+ return { valid: false, error: i18n.tSync('errors.validation.model_name_invalid') };
68
+ }
69
+
70
+ return { valid: true, value: model.trim() };
71
+ }
72
+
73
+ /**
74
+ * Validate API name
75
+ */
76
+ function validateApiName(name) {
77
+ if (!name || name.trim() === '') {
78
+ return { valid: true, value: '' }; // Name is optional
79
+ }
80
+
81
+ if (name.length > 20) {
82
+ return { valid: false, error: 'Name is too long (maximum 20 characters)' };
83
+ }
84
+
85
+ // Check for invalid characters
86
+ if (!/^[a-zA-Z0-9\s\-_\.]+$/.test(name)) {
87
+ return { valid: false, error: 'Name contains invalid characters' };
88
+ }
89
+
90
+ return { valid: true, value: name.trim() };
91
+ }
92
+
93
+ /**
94
+ * Mask sensitive data for display
95
+ */
96
+ function maskSensitiveData(data, visibleChars = 4) {
97
+ if (!data || data.length <= visibleChars * 2) {
98
+ return '***';
99
+ }
100
+ return data.substring(0, visibleChars) + '***' + data.substring(data.length - visibleChars);
101
+ }
102
+
103
+ /**
104
+ * Mask API token for display with optimized formatting
105
+ */
106
+ function maskApiToken(token) {
107
+ if (!token || typeof token !== 'string') {
108
+ return '***INVALID***';
109
+ }
110
+
111
+ // Handle different token lengths according to requirements
112
+ if (token.length < 10) {
113
+ return '***INVALID_API***';
114
+ } else if (token.length >= 16) {
115
+ // Show first 10, last 6, middle 16 stars: sk-a53*************e2bc
116
+ return token.substring(0, 10) + '*'.repeat(16) + token.substring(token.length - 6);
117
+ } else {
118
+ // Length 10-15: Show first 5, last 5, middle 16 stars
119
+ return token.substring(0, 5) + '*'.repeat(16) + token.substring(token.length - 5);
120
+ }
121
+ }
122
+
123
+ module.exports = {
124
+ validateBaseUrl,
125
+ validateAuthToken,
126
+ validateModel,
127
+ validateApiName,
128
+ maskSensitiveData,
129
+ maskApiToken
130
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kikkimo/claude-launcher",
3
- "version": "1.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Interactive launcher for Claude Code with beautiful Claude-style interface",
5
5
  "main": "claude-launcher",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "files": [
20
20
  "claude-launcher",
21
- "claude-launcher-template.env",
21
+ "lib/",
22
22
  "README.md",
23
23
  "LICENSE",
24
24
  "CHANGELOG.md",