maistro 1.0.390
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/LICENSE +15 -0
- package/README.md +107 -0
- package/dist/app.d.ts +247 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +4971 -0
- package/dist/app.js.map +1 -0
- package/dist/buildInfo.d.ts +5 -0
- package/dist/buildInfo.d.ts.map +1 -0
- package/dist/buildInfo.js +2 -0
- package/dist/buildInfo.js.map +1 -0
- package/dist/caffeinate.d.ts +72 -0
- package/dist/caffeinate.d.ts.map +1 -0
- package/dist/caffeinate.js +258 -0
- package/dist/caffeinate.js.map +1 -0
- package/dist/claudePath.d.ts +10 -0
- package/dist/claudePath.d.ts.map +1 -0
- package/dist/claudePath.js +34 -0
- package/dist/claudePath.js.map +1 -0
- package/dist/clipboard.d.ts +44 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +442 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/config.d.ts +211 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +933 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +50 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +81 -0
- package/dist/constants.js.map +1 -0
- package/dist/contextBuilder.d.ts +38 -0
- package/dist/contextBuilder.d.ts.map +1 -0
- package/dist/contextBuilder.js +113 -0
- package/dist/contextBuilder.js.map +1 -0
- package/dist/dependencyDetector.d.ts +57 -0
- package/dist/dependencyDetector.d.ts.map +1 -0
- package/dist/dependencyDetector.js +505 -0
- package/dist/dependencyDetector.js.map +1 -0
- package/dist/executor.d.ts +83 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +583 -0
- package/dist/executor.js.map +1 -0
- package/dist/git.d.ts +85 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +283 -0
- package/dist/git.js.map +1 -0
- package/dist/imageManager.d.ts +161 -0
- package/dist/imageManager.d.ts.map +1 -0
- package/dist/imageManager.js +674 -0
- package/dist/imageManager.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +437 -0
- package/dist/index.js.map +1 -0
- package/dist/input-visual-test.d.ts +9 -0
- package/dist/input-visual-test.d.ts.map +1 -0
- package/dist/input-visual-test.js +108 -0
- package/dist/input-visual-test.js.map +1 -0
- package/dist/inputBox.d.ts +228 -0
- package/dist/inputBox.d.ts.map +1 -0
- package/dist/inputBox.js +966 -0
- package/dist/inputBox.js.map +1 -0
- package/dist/logger.d.ts +136 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +347 -0
- package/dist/logger.js.map +1 -0
- package/dist/orchestrator.d.ts +149 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +821 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/planner.d.ts +86 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +830 -0
- package/dist/planner.js.map +1 -0
- package/dist/pty-test-runner.d.ts +87 -0
- package/dist/pty-test-runner.d.ts.map +1 -0
- package/dist/pty-test-runner.js +721 -0
- package/dist/pty-test-runner.js.map +1 -0
- package/dist/screen.d.ts +44 -0
- package/dist/screen.d.ts.map +1 -0
- package/dist/screen.js +152 -0
- package/dist/screen.js.map +1 -0
- package/dist/taskQueue.d.ts +70 -0
- package/dist/taskQueue.d.ts.map +1 -0
- package/dist/taskQueue.js +282 -0
- package/dist/taskQueue.js.map +1 -0
- package/dist/tui-test-harness.d.ts +216 -0
- package/dist/tui-test-harness.d.ts.map +1 -0
- package/dist/tui-test-harness.js +527 -0
- package/dist/tui-test-harness.js.map +1 -0
- package/dist/types.d.ts +257 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +46 -0
- package/dist/types.js.map +1 -0
- package/dist/ui-visual-test.d.ts +15 -0
- package/dist/ui-visual-test.d.ts.map +1 -0
- package/dist/ui-visual-test.js +141 -0
- package/dist/ui-visual-test.js.map +1 -0
- package/dist/ui.d.ts +272 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +1531 -0
- package/dist/ui.js.map +1 -0
- package/dist/validator.d.ts +53 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +491 -0
- package/dist/validator.js.map +1 -0
- package/dist/versionCheck.d.ts +63 -0
- package/dist/versionCheck.d.ts.map +1 -0
- package/dist/versionCheck.js +261 -0
- package/dist/versionCheck.js.map +1 -0
- package/package.json +62 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import { getClaudePath } from './claudePath.js';
|
|
6
|
+
/**
|
|
7
|
+
* Default settings for new installations
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_SETTINGS = {
|
|
10
|
+
preventSleep: true,
|
|
11
|
+
};
|
|
12
|
+
const CONFIG_DIR = join(homedir(), '.maistro');
|
|
13
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
14
|
+
/**
|
|
15
|
+
* Ensure config directory exists
|
|
16
|
+
*/
|
|
17
|
+
function ensureConfigDir() {
|
|
18
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
19
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Load global configuration
|
|
24
|
+
*/
|
|
25
|
+
export function loadConfig() {
|
|
26
|
+
try {
|
|
27
|
+
if (existsSync(CONFIG_FILE)) {
|
|
28
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
29
|
+
return JSON.parse(content);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Invalid config, return empty
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Save global configuration
|
|
39
|
+
*/
|
|
40
|
+
export function saveConfig(config) {
|
|
41
|
+
ensureConfigDir();
|
|
42
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if Claude Code CLI is installed
|
|
46
|
+
*/
|
|
47
|
+
export async function isClaudeCodeInstalled() {
|
|
48
|
+
try {
|
|
49
|
+
const claudePath = getClaudePath();
|
|
50
|
+
const result = await execa(claudePath, ['--version'], { reject: false });
|
|
51
|
+
if (result.exitCode === 0) {
|
|
52
|
+
const version = typeof result.stdout === 'string' ? result.stdout.trim() : '';
|
|
53
|
+
return { installed: true, version };
|
|
54
|
+
}
|
|
55
|
+
return { installed: false, error: 'Claude Code CLI returned non-zero exit code' };
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return {
|
|
59
|
+
installed: false,
|
|
60
|
+
error: error instanceof Error ? error.message : 'Claude Code CLI not found',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if Claude Code is authenticated (can make API calls)
|
|
66
|
+
* Uses cached validation to avoid expensive API calls on every startup
|
|
67
|
+
*/
|
|
68
|
+
export async function isClaudeCodeAuthenticated(forceCheck = false) {
|
|
69
|
+
// Check cache first (valid for 1 hour)
|
|
70
|
+
const config = loadConfig();
|
|
71
|
+
const cacheValidMs = 60 * 60 * 1000; // 1 hour
|
|
72
|
+
if (!forceCheck && config.claudeCodeValidated && config.lastValidated) {
|
|
73
|
+
const lastCheck = new Date(config.lastValidated).getTime();
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
if (now - lastCheck < cacheValidMs) {
|
|
76
|
+
return { authenticated: true };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const claudePath = getClaudePath();
|
|
81
|
+
// Try a simple command that requires auth
|
|
82
|
+
const result = await execa(claudePath, ['--print', '-p', 'Say "ok"'], {
|
|
83
|
+
reject: false,
|
|
84
|
+
timeout: 30000,
|
|
85
|
+
stdin: 'ignore',
|
|
86
|
+
});
|
|
87
|
+
if (result.exitCode === 0) {
|
|
88
|
+
// Cache successful validation
|
|
89
|
+
saveConfig({
|
|
90
|
+
...config,
|
|
91
|
+
claudeCodeValidated: true,
|
|
92
|
+
lastValidated: new Date().toISOString(),
|
|
93
|
+
});
|
|
94
|
+
return { authenticated: true };
|
|
95
|
+
}
|
|
96
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : '';
|
|
97
|
+
if (stderr.includes('auth') || stderr.includes('login') || stderr.includes('API')) {
|
|
98
|
+
// Clear cache on auth failure
|
|
99
|
+
saveConfig({
|
|
100
|
+
...config,
|
|
101
|
+
claudeCodeValidated: false,
|
|
102
|
+
lastValidated: undefined,
|
|
103
|
+
});
|
|
104
|
+
return { authenticated: false, error: 'Claude Code is not authenticated. Run: claude login' };
|
|
105
|
+
}
|
|
106
|
+
return { authenticated: true }; // Assume authenticated if no auth error
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
authenticated: false,
|
|
111
|
+
error: error instanceof Error ? error.message : 'Failed to check authentication',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Test Claude Code interaction with tool use
|
|
117
|
+
* This catches API errors like rate limiting or concurrency issues
|
|
118
|
+
*/
|
|
119
|
+
export async function testClaudeInteraction() {
|
|
120
|
+
try {
|
|
121
|
+
const claudePath = getClaudePath();
|
|
122
|
+
// Run a simple task that uses tools - this tests full interaction
|
|
123
|
+
// Note: --output-format stream-json requires --verbose when used with --print
|
|
124
|
+
const result = await execa(claudePath, ['--print', '--verbose', '--output-format', 'stream-json', '-p', 'List one file in the current directory using the Bash tool, then say "test complete"'], {
|
|
125
|
+
reject: false,
|
|
126
|
+
timeout: 60000, // 60 second timeout
|
|
127
|
+
stdin: 'ignore',
|
|
128
|
+
cwd: process.cwd(),
|
|
129
|
+
});
|
|
130
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout : '';
|
|
131
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : '';
|
|
132
|
+
const combined = stdout + stderr;
|
|
133
|
+
// Check for OAuth token expiration (401 + authentication_error)
|
|
134
|
+
if (combined.includes('OAuth token has expired') ||
|
|
135
|
+
combined.includes('authentication_error') ||
|
|
136
|
+
(combined.includes('API Error: 401') || combined.includes('API Error:401'))) {
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
error: 'OAuth token has expired. Please run /login to re-authenticate.',
|
|
140
|
+
output: stdout,
|
|
141
|
+
tokenExpired: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Check for API errors in output
|
|
145
|
+
if (stdout.includes('API Error:') || stderr.includes('API Error:')) {
|
|
146
|
+
const apiErrorMatch = (stdout + stderr).match(/API Error:\s*(\d+)\s*([^\n]+)?/i);
|
|
147
|
+
if (apiErrorMatch) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
error: `API Error ${apiErrorMatch[1]}: ${apiErrorMatch[2] || 'Unknown error'}`,
|
|
151
|
+
output: stdout,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return { success: false, error: 'API Error occurred', output: stdout };
|
|
155
|
+
}
|
|
156
|
+
// Check for rate limiting
|
|
157
|
+
if (stdout.includes('rate limit') || stderr.includes('rate limit') ||
|
|
158
|
+
stdout.includes('concurrency') || stderr.includes('concurrency')) {
|
|
159
|
+
return { success: false, error: 'Rate limited or concurrency issues', output: stdout };
|
|
160
|
+
}
|
|
161
|
+
if (result.exitCode === 0) {
|
|
162
|
+
return { success: true, output: stdout };
|
|
163
|
+
}
|
|
164
|
+
// Non-zero exit - check stderr for specific errors
|
|
165
|
+
if (stderr.includes('auth') || stderr.includes('login')) {
|
|
166
|
+
return { success: false, error: 'Authentication error', output: stdout };
|
|
167
|
+
}
|
|
168
|
+
return { success: false, error: stderr || `Exit code ${result.exitCode}`, output: stdout };
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return {
|
|
172
|
+
success: false,
|
|
173
|
+
error: error instanceof Error ? error.message : 'Failed to test interaction',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Launch Claude Code /login command interactively to re-authenticate
|
|
179
|
+
* Returns true if login was successful
|
|
180
|
+
*/
|
|
181
|
+
export async function launchClaudeLogin() {
|
|
182
|
+
try {
|
|
183
|
+
const claudePath = getClaudePath();
|
|
184
|
+
// Spawn Claude Code interactively with /login - user can interact directly
|
|
185
|
+
const result = await execa(claudePath, ['/login'], {
|
|
186
|
+
stdio: 'inherit', // Connect to user's terminal for interactive login
|
|
187
|
+
reject: false,
|
|
188
|
+
});
|
|
189
|
+
return result.exitCode === 0;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if maistro is configured (Claude Code CLI is available and authenticated)
|
|
197
|
+
*/
|
|
198
|
+
export async function isConfigured() {
|
|
199
|
+
const installed = await isClaudeCodeInstalled();
|
|
200
|
+
if (!installed.installed)
|
|
201
|
+
return false;
|
|
202
|
+
const authenticated = await isClaudeCodeAuthenticated();
|
|
203
|
+
return authenticated.authenticated;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get config file path (for display)
|
|
207
|
+
*/
|
|
208
|
+
export function getConfigPath() {
|
|
209
|
+
return CONFIG_FILE;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Get the preventSleep setting (defaults to true)
|
|
213
|
+
*/
|
|
214
|
+
export function getPreventSleep() {
|
|
215
|
+
const config = loadConfig();
|
|
216
|
+
return config.preventSleep ?? DEFAULT_SETTINGS.preventSleep;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Set the preventSleep setting
|
|
220
|
+
*/
|
|
221
|
+
export function setPreventSleep(value) {
|
|
222
|
+
const config = loadConfig();
|
|
223
|
+
saveConfig({
|
|
224
|
+
...config,
|
|
225
|
+
preventSleep: value,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Detect the current terminal emulator
|
|
230
|
+
*/
|
|
231
|
+
export function detectTerminal() {
|
|
232
|
+
const termProgram = process.env.TERM_PROGRAM || '';
|
|
233
|
+
const termProgramVersion = process.env.TERM_PROGRAM_VERSION || '';
|
|
234
|
+
const term = process.env.TERM || '';
|
|
235
|
+
const lcTerminal = process.env.LC_TERMINAL || '';
|
|
236
|
+
const wtSession = process.env.WT_SESSION || '';
|
|
237
|
+
const alacrittySocket = process.env.ALACRITTY_SOCKET || '';
|
|
238
|
+
const kitty = process.env.KITTY_WINDOW_ID || '';
|
|
239
|
+
const wezterm = process.env.WEZTERM_EXECUTABLE || '';
|
|
240
|
+
const ghostty = process.env.GHOSTTY_RESOURCES_DIR || '';
|
|
241
|
+
// Ghostty - native support
|
|
242
|
+
if (ghostty || termProgram === 'ghostty') {
|
|
243
|
+
return {
|
|
244
|
+
name: 'Ghostty',
|
|
245
|
+
program: 'ghostty',
|
|
246
|
+
supportsShiftEnter: true,
|
|
247
|
+
configurable: false,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// WezTerm - native support
|
|
251
|
+
if (wezterm || termProgram === 'WezTerm') {
|
|
252
|
+
return {
|
|
253
|
+
name: 'WezTerm',
|
|
254
|
+
program: 'wezterm',
|
|
255
|
+
supportsShiftEnter: true,
|
|
256
|
+
configurable: false,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
// Kitty - native support with CSI u protocol
|
|
260
|
+
if (kitty || term === 'xterm-kitty') {
|
|
261
|
+
return {
|
|
262
|
+
name: 'Kitty',
|
|
263
|
+
program: 'kitty',
|
|
264
|
+
supportsShiftEnter: true,
|
|
265
|
+
configurable: false,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// VS Code integrated terminal
|
|
269
|
+
if (termProgram === 'vscode') {
|
|
270
|
+
return {
|
|
271
|
+
name: 'VS Code Terminal',
|
|
272
|
+
program: 'vscode',
|
|
273
|
+
supportsShiftEnter: false,
|
|
274
|
+
configurable: true,
|
|
275
|
+
instructions: [
|
|
276
|
+
'Open VS Code Settings (Cmd+, or Ctrl+,)',
|
|
277
|
+
'Search for "terminal.integrated.sendKeybindingsToShell"',
|
|
278
|
+
'Enable this setting',
|
|
279
|
+
'',
|
|
280
|
+
'Or add to keybindings.json (Cmd+K Cmd+S → Open Keyboard Shortcuts JSON):',
|
|
281
|
+
'{',
|
|
282
|
+
' "key": "shift+enter",',
|
|
283
|
+
' "command": "workbench.action.terminal.sendSequence",',
|
|
284
|
+
' "args": { "text": "\\n" },',
|
|
285
|
+
' "when": "terminalFocus"',
|
|
286
|
+
'}',
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
// Windows Terminal
|
|
291
|
+
if (wtSession) {
|
|
292
|
+
return {
|
|
293
|
+
name: 'Windows Terminal',
|
|
294
|
+
program: 'windows-terminal',
|
|
295
|
+
supportsShiftEnter: false,
|
|
296
|
+
configurable: true,
|
|
297
|
+
instructions: [
|
|
298
|
+
'Open Windows Terminal settings (Ctrl+,)',
|
|
299
|
+
'Go to Actions tab',
|
|
300
|
+
'Add a new action:',
|
|
301
|
+
' - Keys: shift+enter',
|
|
302
|
+
' - Action: Send Input',
|
|
303
|
+
' - Input: \\x0a',
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// Alacritty
|
|
308
|
+
if (alacrittySocket || termProgram === 'Alacritty') {
|
|
309
|
+
return {
|
|
310
|
+
name: 'Alacritty',
|
|
311
|
+
program: 'alacritty',
|
|
312
|
+
supportsShiftEnter: false,
|
|
313
|
+
configurable: true,
|
|
314
|
+
instructions: [
|
|
315
|
+
'Add to ~/.config/alacritty/alacritty.toml:',
|
|
316
|
+
'',
|
|
317
|
+
'[keyboard]',
|
|
318
|
+
'bindings = [',
|
|
319
|
+
' { key = "Return", mods = "Shift", chars = "\\u000a" }',
|
|
320
|
+
']',
|
|
321
|
+
],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
// iTerm2
|
|
325
|
+
if (termProgram === 'iTerm.app' || lcTerminal === 'iTerm2') {
|
|
326
|
+
return {
|
|
327
|
+
name: 'iTerm2',
|
|
328
|
+
program: 'iterm2',
|
|
329
|
+
supportsShiftEnter: false,
|
|
330
|
+
configurable: true,
|
|
331
|
+
instructions: [
|
|
332
|
+
'Open iTerm2 → Preferences → Keys → Key Bindings',
|
|
333
|
+
'Click the + button to add a new binding:',
|
|
334
|
+
' - Keyboard Shortcut: Shift + Enter',
|
|
335
|
+
' - Action: Send Escape Sequence',
|
|
336
|
+
' - Esc+: OM',
|
|
337
|
+
'',
|
|
338
|
+
'Or use: Action → Send Text with "vi Special Chars"',
|
|
339
|
+
' - Enter: \\n',
|
|
340
|
+
],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
// Terminal.app (macOS)
|
|
344
|
+
if (termProgram === 'Apple_Terminal') {
|
|
345
|
+
return {
|
|
346
|
+
name: 'Terminal.app',
|
|
347
|
+
program: 'apple-terminal',
|
|
348
|
+
supportsShiftEnter: false,
|
|
349
|
+
configurable: false, // Terminal.app doesn't support custom key bindings easily
|
|
350
|
+
instructions: [
|
|
351
|
+
'Terminal.app has limited key binding customization.',
|
|
352
|
+
'Use Ctrl+J for newlines instead of Shift+Enter.',
|
|
353
|
+
'',
|
|
354
|
+
'Or consider switching to iTerm2, Kitty, WezTerm, or Ghostty',
|
|
355
|
+
'which have better keyboard customization support.',
|
|
356
|
+
],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
// Zed
|
|
360
|
+
if (termProgram === 'Zed') {
|
|
361
|
+
return {
|
|
362
|
+
name: 'Zed Terminal',
|
|
363
|
+
program: 'zed',
|
|
364
|
+
supportsShiftEnter: false,
|
|
365
|
+
configurable: true,
|
|
366
|
+
instructions: [
|
|
367
|
+
'Add to ~/.config/zed/keymap.json:',
|
|
368
|
+
'',
|
|
369
|
+
'[',
|
|
370
|
+
' {',
|
|
371
|
+
' "context": "Terminal",',
|
|
372
|
+
' "bindings": {',
|
|
373
|
+
' "shift-enter": ["terminal::SendKeystroke", "\\n"]',
|
|
374
|
+
' }',
|
|
375
|
+
' }',
|
|
376
|
+
']',
|
|
377
|
+
],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
// Warp
|
|
381
|
+
if (termProgram === 'WarpTerminal') {
|
|
382
|
+
return {
|
|
383
|
+
name: 'Warp',
|
|
384
|
+
program: 'warp',
|
|
385
|
+
supportsShiftEnter: false,
|
|
386
|
+
configurable: true,
|
|
387
|
+
instructions: [
|
|
388
|
+
'Open Warp → Settings → Keyboard Shortcuts',
|
|
389
|
+
'Add a custom shortcut:',
|
|
390
|
+
' - Shortcut: Shift + Enter',
|
|
391
|
+
' - Action: Send text',
|
|
392
|
+
' - Text: \\n',
|
|
393
|
+
],
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
// Unknown terminal
|
|
397
|
+
return {
|
|
398
|
+
name: termProgram || term || 'Unknown Terminal',
|
|
399
|
+
program: 'unknown',
|
|
400
|
+
supportsShiftEnter: false,
|
|
401
|
+
configurable: false,
|
|
402
|
+
instructions: [
|
|
403
|
+
'Your terminal may not support custom Shift+Enter bindings.',
|
|
404
|
+
'Use Ctrl+J for newlines (this works in all terminals).',
|
|
405
|
+
],
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Check if terminal setup has been shown
|
|
410
|
+
*/
|
|
411
|
+
export function isTerminalSetupShown() {
|
|
412
|
+
const config = loadConfig();
|
|
413
|
+
return config.terminalSetupShown === true;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Mark terminal setup as shown
|
|
417
|
+
*/
|
|
418
|
+
export function setTerminalSetupShown(value) {
|
|
419
|
+
const config = loadConfig();
|
|
420
|
+
saveConfig({
|
|
421
|
+
...config,
|
|
422
|
+
terminalSetupShown: value,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Get the shiftEnterEnabled setting
|
|
427
|
+
*/
|
|
428
|
+
export function getShiftEnterEnabled() {
|
|
429
|
+
const config = loadConfig();
|
|
430
|
+
return config.shiftEnterEnabled === true;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Set the shiftEnterEnabled setting
|
|
434
|
+
*/
|
|
435
|
+
export function setShiftEnterEnabled(value) {
|
|
436
|
+
const config = loadConfig();
|
|
437
|
+
saveConfig({
|
|
438
|
+
...config,
|
|
439
|
+
shiftEnterEnabled: value,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Get the cmdVPasteEnabled setting
|
|
444
|
+
*/
|
|
445
|
+
export function getCmdVPasteEnabled() {
|
|
446
|
+
const config = loadConfig();
|
|
447
|
+
return config.cmdVPasteEnabled === true;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Set the cmdVPasteEnabled setting
|
|
451
|
+
*/
|
|
452
|
+
export function setCmdVPasteEnabled(value) {
|
|
453
|
+
const config = loadConfig();
|
|
454
|
+
saveConfig({
|
|
455
|
+
...config,
|
|
456
|
+
cmdVPasteEnabled: value,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* VS Code keybinding for Shift+Enter
|
|
461
|
+
*/
|
|
462
|
+
// VS Code keybinding sends the Kitty keyboard protocol sequence for Shift+Enter
|
|
463
|
+
// This is explicitly handled by maistro as a newline insertion
|
|
464
|
+
const VSCODE_SHIFT_ENTER_KEYBINDING = {
|
|
465
|
+
key: 'shift+enter',
|
|
466
|
+
command: 'workbench.action.terminal.sendSequence',
|
|
467
|
+
args: { text: '\x1b[13;2u' },
|
|
468
|
+
when: 'terminalFocus',
|
|
469
|
+
};
|
|
470
|
+
/**
|
|
471
|
+
* VS Code keybinding for Cmd+V (clipboard paste)
|
|
472
|
+
*/
|
|
473
|
+
// VS Code keybinding sends the Kitty keyboard protocol sequence for Cmd+V
|
|
474
|
+
// 118 = 'v', 9 = Super/Cmd modifier
|
|
475
|
+
const VSCODE_CMD_V_KEYBINDING = {
|
|
476
|
+
key: 'cmd+v',
|
|
477
|
+
command: 'workbench.action.terminal.sendSequence',
|
|
478
|
+
args: { text: '\x1b[118;9u' },
|
|
479
|
+
when: 'terminalFocus',
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* Get VS Code keybindings.json path
|
|
483
|
+
*/
|
|
484
|
+
function getVSCodeKeybindingsPath() {
|
|
485
|
+
const platform = process.platform;
|
|
486
|
+
if (platform === 'darwin') {
|
|
487
|
+
return join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'keybindings.json');
|
|
488
|
+
}
|
|
489
|
+
else if (platform === 'win32') {
|
|
490
|
+
return join(homedir(), 'AppData', 'Roaming', 'Code', 'User', 'keybindings.json');
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
return join(homedir(), '.config', 'Code', 'User', 'keybindings.json');
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Check if VS Code Shift+Enter keybinding is already configured
|
|
498
|
+
*/
|
|
499
|
+
export function isVSCodeShiftEnterConfigured() {
|
|
500
|
+
const keybindingsPath = getVSCodeKeybindingsPath();
|
|
501
|
+
if (!existsSync(keybindingsPath)) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
let content = readFileSync(keybindingsPath, 'utf-8');
|
|
506
|
+
// Remove comments (VS Code keybindings.json supports comments)
|
|
507
|
+
content = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
508
|
+
const keybindings = JSON.parse(content);
|
|
509
|
+
if (!Array.isArray(keybindings)) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
// Check if a shift+enter binding for terminal exists
|
|
513
|
+
return keybindings.some((kb) => kb.key === 'shift+enter' &&
|
|
514
|
+
kb.command === 'workbench.action.terminal.sendSequence' &&
|
|
515
|
+
kb.when?.includes('terminalFocus'));
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Configure VS Code Shift+Enter keybinding
|
|
523
|
+
* Returns { success, error?, backupPath? }
|
|
524
|
+
*/
|
|
525
|
+
export function configureVSCodeShiftEnter() {
|
|
526
|
+
const keybindingsPath = getVSCodeKeybindingsPath();
|
|
527
|
+
const keybindingsDir = join(keybindingsPath, '..');
|
|
528
|
+
try {
|
|
529
|
+
// Ensure directory exists
|
|
530
|
+
if (!existsSync(keybindingsDir)) {
|
|
531
|
+
mkdirSync(keybindingsDir, { recursive: true });
|
|
532
|
+
}
|
|
533
|
+
let keybindings = [];
|
|
534
|
+
let backupPath;
|
|
535
|
+
// Read existing keybindings if file exists
|
|
536
|
+
if (existsSync(keybindingsPath)) {
|
|
537
|
+
const content = readFileSync(keybindingsPath, 'utf-8');
|
|
538
|
+
// Backup existing file
|
|
539
|
+
backupPath = keybindingsPath + '.maistro-backup';
|
|
540
|
+
writeFileSync(backupPath, content, 'utf-8');
|
|
541
|
+
// Parse (remove comments first)
|
|
542
|
+
const cleanContent = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
543
|
+
try {
|
|
544
|
+
keybindings = JSON.parse(cleanContent);
|
|
545
|
+
if (!Array.isArray(keybindings)) {
|
|
546
|
+
keybindings = [];
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
// If parse fails, start fresh but keep backup
|
|
551
|
+
keybindings = [];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// Check if already configured
|
|
555
|
+
const existingIndex = keybindings.findIndex((kb) => kb.key === 'shift+enter' &&
|
|
556
|
+
kb.command === 'workbench.action.terminal.sendSequence' &&
|
|
557
|
+
kb.when?.includes('terminalFocus'));
|
|
558
|
+
if (existingIndex >= 0) {
|
|
559
|
+
// Already configured
|
|
560
|
+
return { success: true };
|
|
561
|
+
}
|
|
562
|
+
// Add the keybinding
|
|
563
|
+
keybindings.push(VSCODE_SHIFT_ENTER_KEYBINDING);
|
|
564
|
+
// Write back
|
|
565
|
+
writeFileSync(keybindingsPath, JSON.stringify(keybindings, null, 2), 'utf-8');
|
|
566
|
+
return { success: true, backupPath };
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
return {
|
|
570
|
+
success: false,
|
|
571
|
+
error: error instanceof Error ? error.message : 'Failed to configure VS Code',
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Remove VS Code Shift+Enter keybinding
|
|
577
|
+
*/
|
|
578
|
+
export function removeVSCodeShiftEnter() {
|
|
579
|
+
const keybindingsPath = getVSCodeKeybindingsPath();
|
|
580
|
+
if (!existsSync(keybindingsPath)) {
|
|
581
|
+
return { success: true }; // Nothing to remove
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
const content = readFileSync(keybindingsPath, 'utf-8');
|
|
585
|
+
const cleanContent = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
586
|
+
let keybindings;
|
|
587
|
+
try {
|
|
588
|
+
keybindings = JSON.parse(cleanContent);
|
|
589
|
+
if (!Array.isArray(keybindings)) {
|
|
590
|
+
return { success: true };
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
return { success: true };
|
|
595
|
+
}
|
|
596
|
+
// Remove the keybinding
|
|
597
|
+
const filtered = keybindings.filter((kb) => !(kb.key === 'shift+enter' &&
|
|
598
|
+
kb.command === 'workbench.action.terminal.sendSequence' &&
|
|
599
|
+
kb.when?.includes('terminalFocus')));
|
|
600
|
+
if (filtered.length === keybindings.length) {
|
|
601
|
+
// Nothing was removed
|
|
602
|
+
return { success: true };
|
|
603
|
+
}
|
|
604
|
+
// Write back
|
|
605
|
+
writeFileSync(keybindingsPath, JSON.stringify(filtered, null, 2), 'utf-8');
|
|
606
|
+
return { success: true };
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
return {
|
|
610
|
+
success: false,
|
|
611
|
+
error: error instanceof Error ? error.message : 'Failed to remove VS Code keybinding',
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Check if VS Code Cmd+V keybinding is already configured
|
|
617
|
+
*/
|
|
618
|
+
export function isVSCodeCmdVConfigured() {
|
|
619
|
+
const keybindingsPath = getVSCodeKeybindingsPath();
|
|
620
|
+
if (!existsSync(keybindingsPath)) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
let content = readFileSync(keybindingsPath, 'utf-8');
|
|
625
|
+
content = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
626
|
+
const keybindings = JSON.parse(content);
|
|
627
|
+
if (!Array.isArray(keybindings)) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
return keybindings.some((kb) => kb.key === 'cmd+v' &&
|
|
631
|
+
kb.command === 'workbench.action.terminal.sendSequence' &&
|
|
632
|
+
kb.when?.includes('terminalFocus'));
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Configure VS Code Cmd+V keybinding for clipboard paste
|
|
640
|
+
*/
|
|
641
|
+
export function configureVSCodeCmdV() {
|
|
642
|
+
const keybindingsPath = getVSCodeKeybindingsPath();
|
|
643
|
+
const keybindingsDir = join(keybindingsPath, '..');
|
|
644
|
+
try {
|
|
645
|
+
if (!existsSync(keybindingsDir)) {
|
|
646
|
+
mkdirSync(keybindingsDir, { recursive: true });
|
|
647
|
+
}
|
|
648
|
+
let keybindings = [];
|
|
649
|
+
let backupPath;
|
|
650
|
+
if (existsSync(keybindingsPath)) {
|
|
651
|
+
const content = readFileSync(keybindingsPath, 'utf-8');
|
|
652
|
+
backupPath = keybindingsPath + '.maistro-backup';
|
|
653
|
+
writeFileSync(backupPath, content, 'utf-8');
|
|
654
|
+
const cleanContent = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
655
|
+
try {
|
|
656
|
+
keybindings = JSON.parse(cleanContent);
|
|
657
|
+
if (!Array.isArray(keybindings)) {
|
|
658
|
+
keybindings = [];
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
keybindings = [];
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
const existingIndex = keybindings.findIndex((kb) => kb.key === 'cmd+v' &&
|
|
666
|
+
kb.command === 'workbench.action.terminal.sendSequence' &&
|
|
667
|
+
kb.when?.includes('terminalFocus'));
|
|
668
|
+
if (existingIndex >= 0) {
|
|
669
|
+
return { success: true };
|
|
670
|
+
}
|
|
671
|
+
keybindings.push(VSCODE_CMD_V_KEYBINDING);
|
|
672
|
+
writeFileSync(keybindingsPath, JSON.stringify(keybindings, null, 2), 'utf-8');
|
|
673
|
+
return { success: true, backupPath };
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
return {
|
|
677
|
+
success: false,
|
|
678
|
+
error: error instanceof Error ? error.message : 'Failed to configure VS Code',
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Remove VS Code Cmd+V keybinding
|
|
684
|
+
*/
|
|
685
|
+
export function removeVSCodeCmdV() {
|
|
686
|
+
const keybindingsPath = getVSCodeKeybindingsPath();
|
|
687
|
+
if (!existsSync(keybindingsPath)) {
|
|
688
|
+
return { success: true };
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const content = readFileSync(keybindingsPath, 'utf-8');
|
|
692
|
+
const cleanContent = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
693
|
+
let keybindings;
|
|
694
|
+
try {
|
|
695
|
+
keybindings = JSON.parse(cleanContent);
|
|
696
|
+
if (!Array.isArray(keybindings)) {
|
|
697
|
+
return { success: true };
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
return { success: true };
|
|
702
|
+
}
|
|
703
|
+
const filtered = keybindings.filter((kb) => !(kb.key === 'cmd+v' &&
|
|
704
|
+
kb.command === 'workbench.action.terminal.sendSequence' &&
|
|
705
|
+
kb.when?.includes('terminalFocus')));
|
|
706
|
+
if (filtered.length === keybindings.length) {
|
|
707
|
+
return { success: true };
|
|
708
|
+
}
|
|
709
|
+
writeFileSync(keybindingsPath, JSON.stringify(filtered, null, 2), 'utf-8');
|
|
710
|
+
return { success: true };
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
return {
|
|
714
|
+
success: false,
|
|
715
|
+
error: error instanceof Error ? error.message : 'Failed to remove VS Code keybinding',
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Configure terminal for Cmd+V paste support
|
|
721
|
+
* Warning: VS Code keybindings with "terminalFocus" affect ALL terminals,
|
|
722
|
+
* which may interfere with other terminal apps like Claude Code.
|
|
723
|
+
*/
|
|
724
|
+
export function configureTerminalCmdVPaste(terminalInfo) {
|
|
725
|
+
// VS Code: Can be configured but affects all terminals (may break Claude Code)
|
|
726
|
+
if (terminalInfo.program === 'vscode') {
|
|
727
|
+
const result = configureVSCodeCmdV();
|
|
728
|
+
if (result.success) {
|
|
729
|
+
return {
|
|
730
|
+
success: true,
|
|
731
|
+
message: 'VS Code Cmd+V keybinding configured',
|
|
732
|
+
backupPath: result.backupPath,
|
|
733
|
+
warning: 'This affects ALL VS Code terminals, including other apps like Claude Code.',
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
// Other terminals require manual configuration
|
|
739
|
+
return {
|
|
740
|
+
success: false,
|
|
741
|
+
error: `Automatic configuration not supported for ${terminalInfo.name}. See instructions for manual setup.`,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Remove terminal Cmd+V paste configuration
|
|
746
|
+
*/
|
|
747
|
+
export function removeTerminalCmdVPaste(terminalInfo) {
|
|
748
|
+
switch (terminalInfo.program) {
|
|
749
|
+
case 'vscode': {
|
|
750
|
+
const result = removeVSCodeCmdV();
|
|
751
|
+
if (result.success) {
|
|
752
|
+
return { success: true, message: 'VS Code Cmd+V keybinding removed' };
|
|
753
|
+
}
|
|
754
|
+
return result;
|
|
755
|
+
}
|
|
756
|
+
default:
|
|
757
|
+
return { success: true };
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Configure terminal for Shift+Enter support
|
|
762
|
+
* Returns result with success status and any messages
|
|
763
|
+
*/
|
|
764
|
+
export function configureTerminalShiftEnter(terminalInfo) {
|
|
765
|
+
// Terminals with native support don't need configuration
|
|
766
|
+
if (terminalInfo.supportsShiftEnter) {
|
|
767
|
+
return { success: true, message: 'Terminal already supports Shift+Enter natively' };
|
|
768
|
+
}
|
|
769
|
+
// Handle each configurable terminal
|
|
770
|
+
switch (terminalInfo.program) {
|
|
771
|
+
case 'vscode': {
|
|
772
|
+
const result = configureVSCodeShiftEnter();
|
|
773
|
+
if (result.success) {
|
|
774
|
+
return {
|
|
775
|
+
success: true,
|
|
776
|
+
message: 'VS Code keybinding configured. Restart terminal for changes to take effect.',
|
|
777
|
+
backupPath: result.backupPath,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
return result;
|
|
781
|
+
}
|
|
782
|
+
// TODO: Add support for other terminals
|
|
783
|
+
// case 'iterm2':
|
|
784
|
+
// case 'alacritty':
|
|
785
|
+
// case 'zed':
|
|
786
|
+
default:
|
|
787
|
+
return {
|
|
788
|
+
success: false,
|
|
789
|
+
error: `Automatic configuration not supported for ${terminalInfo.name}. Use Ctrl+J instead.`,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Remove terminal Shift+Enter configuration
|
|
795
|
+
*/
|
|
796
|
+
export function removeTerminalShiftEnter(terminalInfo) {
|
|
797
|
+
if (terminalInfo.supportsShiftEnter) {
|
|
798
|
+
return { success: true, message: 'Terminal has native support, nothing to remove' };
|
|
799
|
+
}
|
|
800
|
+
switch (terminalInfo.program) {
|
|
801
|
+
case 'vscode': {
|
|
802
|
+
const result = removeVSCodeShiftEnter();
|
|
803
|
+
if (result.success) {
|
|
804
|
+
return { success: true, message: 'VS Code keybinding removed' };
|
|
805
|
+
}
|
|
806
|
+
return result;
|
|
807
|
+
}
|
|
808
|
+
default:
|
|
809
|
+
return { success: true };
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Get the appropriate newline hint based on terminal configuration
|
|
814
|
+
* Returns "Ctrl+J newline" or "Ctrl+J | Shift+Enter newline"
|
|
815
|
+
*/
|
|
816
|
+
export function getNewlineHint() {
|
|
817
|
+
const terminalInfo = detectTerminal();
|
|
818
|
+
const shiftEnterEnabled = getShiftEnterEnabled();
|
|
819
|
+
// Show Shift+Enter hint if terminal has native support or is configured
|
|
820
|
+
if (terminalInfo.supportsShiftEnter || shiftEnterEnabled) {
|
|
821
|
+
return 'Ctrl+J | Shift+Enter newline';
|
|
822
|
+
}
|
|
823
|
+
return 'Ctrl+J newline';
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Get terminal-specific instructions for configuring Cmd+V paste
|
|
827
|
+
* macOS terminals intercept Cmd+V for their own paste handling
|
|
828
|
+
* We need them to send an escape sequence we can detect
|
|
829
|
+
*/
|
|
830
|
+
export function getCmdVPasteInstructions() {
|
|
831
|
+
const terminalInfo = detectTerminal();
|
|
832
|
+
switch (terminalInfo.program) {
|
|
833
|
+
case 'iterm2':
|
|
834
|
+
return {
|
|
835
|
+
terminal: 'iTerm2',
|
|
836
|
+
instructions: [
|
|
837
|
+
'Open iTerm2 → Preferences → Keys → Key Bindings',
|
|
838
|
+
'Click the + button to add a new binding:',
|
|
839
|
+
' - Keyboard Shortcut: ⌘V (Cmd+V)',
|
|
840
|
+
' - Action: Send Escape Sequence',
|
|
841
|
+
' - Esc+: OV',
|
|
842
|
+
'',
|
|
843
|
+
'This makes Cmd+V send \\x1bOV which maistro detects for image paste.',
|
|
844
|
+
],
|
|
845
|
+
};
|
|
846
|
+
case 'kitty':
|
|
847
|
+
return {
|
|
848
|
+
terminal: 'Kitty',
|
|
849
|
+
instructions: [
|
|
850
|
+
'Kitty uses the CSI u keyboard protocol.',
|
|
851
|
+
'Add to ~/.config/kitty/kitty.conf:',
|
|
852
|
+
'',
|
|
853
|
+
'map cmd+v send_text all \\x1b[118;9u',
|
|
854
|
+
'',
|
|
855
|
+
'Or Kitty may already send this sequence natively.',
|
|
856
|
+
],
|
|
857
|
+
};
|
|
858
|
+
case 'wezterm':
|
|
859
|
+
return {
|
|
860
|
+
terminal: 'WezTerm',
|
|
861
|
+
instructions: [
|
|
862
|
+
'Add to ~/.wezterm.lua:',
|
|
863
|
+
'',
|
|
864
|
+
'return {',
|
|
865
|
+
' keys = {',
|
|
866
|
+
' { key = "v", mods = "CMD", action = wezterm.action.SendString("\\x1b[118;9u") },',
|
|
867
|
+
' },',
|
|
868
|
+
'}',
|
|
869
|
+
],
|
|
870
|
+
};
|
|
871
|
+
case 'ghostty':
|
|
872
|
+
return {
|
|
873
|
+
terminal: 'Ghostty',
|
|
874
|
+
instructions: [
|
|
875
|
+
'Add to ~/.config/ghostty/config:',
|
|
876
|
+
'',
|
|
877
|
+
'keybind = cmd+v=text:\\x1b[118;9u',
|
|
878
|
+
],
|
|
879
|
+
};
|
|
880
|
+
case 'vscode':
|
|
881
|
+
return {
|
|
882
|
+
terminal: 'VS Code Terminal',
|
|
883
|
+
instructions: [
|
|
884
|
+
'\x1b[33m⚠ WARNING:\x1b[0m VS Code keybindings with "terminalFocus" affect ALL terminals.',
|
|
885
|
+
'This will change Cmd+V behavior in every terminal, including Claude Code.',
|
|
886
|
+
'',
|
|
887
|
+
'\x1b[1mOption 1: Enable Cmd+V override\x1b[0m (may affect other terminal apps)',
|
|
888
|
+
' Use the Settings menu to enable "Cmd+V for image paste"',
|
|
889
|
+
' To remove later: toggle OFF in Settings or delete the keybinding from:',
|
|
890
|
+
' ~/Library/Application Support/Code/User/keybindings.json',
|
|
891
|
+
'',
|
|
892
|
+
'\x1b[1mOption 2: Use Ctrl+V instead\x1b[0m (recommended, no conflicts)',
|
|
893
|
+
' Ctrl+V sends the standard paste sequence (\\x16) that maistro detects.',
|
|
894
|
+
' This works without any configuration and won\'t affect other apps.',
|
|
895
|
+
'',
|
|
896
|
+
'\x1b[1mOption 3: Use /paste command\x1b[0m',
|
|
897
|
+
' Type /paste in maistro to paste images from clipboard.',
|
|
898
|
+
],
|
|
899
|
+
};
|
|
900
|
+
case 'alacritty':
|
|
901
|
+
return {
|
|
902
|
+
terminal: 'Alacritty',
|
|
903
|
+
instructions: [
|
|
904
|
+
'Add to ~/.config/alacritty/alacritty.toml:',
|
|
905
|
+
'',
|
|
906
|
+
'[keyboard]',
|
|
907
|
+
'bindings = [',
|
|
908
|
+
' { key = "V", mods = "Command", chars = "\\u001b[118;9u" }',
|
|
909
|
+
']',
|
|
910
|
+
],
|
|
911
|
+
};
|
|
912
|
+
case 'apple-terminal':
|
|
913
|
+
return {
|
|
914
|
+
terminal: 'Terminal.app',
|
|
915
|
+
instructions: [
|
|
916
|
+
'Terminal.app has limited key binding customization.',
|
|
917
|
+
'Use /paste command to paste images from clipboard.',
|
|
918
|
+
'',
|
|
919
|
+
'Or consider switching to iTerm2, Kitty, WezTerm, or Ghostty',
|
|
920
|
+
'which have better keyboard customization support.',
|
|
921
|
+
],
|
|
922
|
+
};
|
|
923
|
+
default:
|
|
924
|
+
return {
|
|
925
|
+
terminal: terminalInfo.name,
|
|
926
|
+
instructions: [
|
|
927
|
+
'Configure your terminal to send \\x1b[118;9u for Cmd+V.',
|
|
928
|
+
'Or use the /paste command to paste images from clipboard.',
|
|
929
|
+
],
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
//# sourceMappingURL=config.js.map
|