@automagik/genie 0.260201.2240

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.
Files changed (59) hide show
  1. package/.github/workflows/publish.yml +26 -0
  2. package/.worktrees/.metadata.json +3 -0
  3. package/README.md +532 -0
  4. package/bun.lock +101 -0
  5. package/dist/claudio.js +76 -0
  6. package/dist/genie.js +201 -0
  7. package/dist/term.js +136 -0
  8. package/install.sh +351 -0
  9. package/package.json +37 -0
  10. package/scripts/version.ts +48 -0
  11. package/src/claudio.ts +128 -0
  12. package/src/commands/launch.ts +245 -0
  13. package/src/commands/models.ts +43 -0
  14. package/src/commands/profiles.ts +95 -0
  15. package/src/commands/setup.ts +5 -0
  16. package/src/genie-commands/hooks.ts +317 -0
  17. package/src/genie-commands/install.ts +351 -0
  18. package/src/genie-commands/setup.ts +282 -0
  19. package/src/genie-commands/shortcuts.ts +62 -0
  20. package/src/genie-commands/update.ts +228 -0
  21. package/src/genie.ts +106 -0
  22. package/src/lib/api-client.ts +109 -0
  23. package/src/lib/claude-settings.ts +252 -0
  24. package/src/lib/config.ts +109 -0
  25. package/src/lib/genie-config.ts +164 -0
  26. package/src/lib/hook-manager.ts +130 -0
  27. package/src/lib/hook-script.ts +256 -0
  28. package/src/lib/hooks/compose.ts +72 -0
  29. package/src/lib/hooks/index.ts +163 -0
  30. package/src/lib/hooks/presets/audited.ts +191 -0
  31. package/src/lib/hooks/presets/collaborative.ts +143 -0
  32. package/src/lib/hooks/presets/sandboxed.ts +153 -0
  33. package/src/lib/hooks/presets/supervised.ts +66 -0
  34. package/src/lib/hooks/utils/escape.ts +46 -0
  35. package/src/lib/log-reader.ts +213 -0
  36. package/src/lib/picker.ts +62 -0
  37. package/src/lib/session-metadata.ts +58 -0
  38. package/src/lib/system-detect.ts +185 -0
  39. package/src/lib/tmux.ts +410 -0
  40. package/src/lib/version.ts +15 -0
  41. package/src/lib/wizard.ts +104 -0
  42. package/src/lib/worktree.ts +362 -0
  43. package/src/term-commands/attach.ts +23 -0
  44. package/src/term-commands/exec.ts +34 -0
  45. package/src/term-commands/hook.ts +42 -0
  46. package/src/term-commands/ls.ts +33 -0
  47. package/src/term-commands/new.ts +73 -0
  48. package/src/term-commands/pane.ts +81 -0
  49. package/src/term-commands/read.ts +70 -0
  50. package/src/term-commands/rm.ts +47 -0
  51. package/src/term-commands/send.ts +34 -0
  52. package/src/term-commands/shortcuts.ts +355 -0
  53. package/src/term-commands/split.ts +87 -0
  54. package/src/term-commands/status.ts +116 -0
  55. package/src/term-commands/window.ts +72 -0
  56. package/src/term.ts +192 -0
  57. package/src/types/config.ts +17 -0
  58. package/src/types/genie-config.ts +104 -0
  59. package/tsconfig.json +17 -0
@@ -0,0 +1,70 @@
1
+ import * as logReader from '../lib/log-reader.js';
2
+
3
+ export interface ReadOptions {
4
+ lines?: string;
5
+ from?: string;
6
+ to?: string;
7
+ range?: string;
8
+ search?: string;
9
+ grep?: string;
10
+ follow?: boolean;
11
+ all?: boolean;
12
+ reverse?: boolean;
13
+ json?: boolean;
14
+ }
15
+
16
+ export async function readSessionLogs(sessionName: string, options: ReadOptions): Promise<void> {
17
+ try {
18
+ // Parse options
19
+ const readOptions: logReader.ReadOptions = {
20
+ lines: options.lines ? parseInt(options.lines, 10) : 100,
21
+ from: options.from ? parseInt(options.from, 10) : undefined,
22
+ to: options.to ? parseInt(options.to, 10) : undefined,
23
+ range: options.range,
24
+ search: options.search,
25
+ grep: options.grep,
26
+ follow: options.follow,
27
+ all: options.all,
28
+ reverse: options.reverse,
29
+ };
30
+
31
+ // Handle follow mode
32
+ if (options.follow) {
33
+ console.log(`Following session "${sessionName}" (Ctrl+C to stop)...`);
34
+ console.log('');
35
+
36
+ const stopFollowing = await logReader.followSessionLogs(sessionName, (line) => {
37
+ console.log(line);
38
+ });
39
+
40
+ // Handle Ctrl+C
41
+ process.on('SIGINT', () => {
42
+ stopFollowing();
43
+ console.log('\nStopped following');
44
+ process.exit(0);
45
+ });
46
+
47
+ // Keep process running
48
+ await new Promise(() => {});
49
+ return;
50
+ }
51
+
52
+ // Regular read mode
53
+ const content = await logReader.readSessionLogs(sessionName, readOptions);
54
+
55
+ if (options.json) {
56
+ const lines = content.split('\n');
57
+ console.log(JSON.stringify({
58
+ session: sessionName,
59
+ lineCount: lines.length,
60
+ content: lines,
61
+ }, null, 2));
62
+ return;
63
+ }
64
+
65
+ console.log(content);
66
+ } catch (error: any) {
67
+ console.error(`Error reading session logs: ${error.message}`);
68
+ process.exit(1);
69
+ }
70
+ }
@@ -0,0 +1,47 @@
1
+ import { join } from 'path';
2
+ import * as tmux from '../lib/tmux.js';
3
+ import { createWorktreeManager } from '../lib/worktree.js';
4
+ import { loadSessionMetadata, deleteSessionMetadata } from '../lib/session-metadata.js';
5
+
6
+ export interface RemoveSessionOptions {
7
+ keepWorktree?: boolean;
8
+ }
9
+
10
+ export async function removeSession(name: string, options: RemoveSessionOptions = {}): Promise<void> {
11
+ try {
12
+ // Check if session exists
13
+ const session = await tmux.findSessionByName(name);
14
+ if (!session) {
15
+ console.error(`❌ Session "${name}" not found`);
16
+ process.exit(1);
17
+ }
18
+
19
+ // Check if session has associated worktree
20
+ const metadata = await loadSessionMetadata(name);
21
+
22
+ // Kill session
23
+ await tmux.killSession(session.id);
24
+
25
+ // Remove worktree unless --keep-worktree
26
+ if (metadata?.worktreePath && metadata?.workspace && !options.keepWorktree) {
27
+ const manager = createWorktreeManager({
28
+ baseDir: join(metadata.workspace, '.worktrees'),
29
+ repoPath: metadata.workspace
30
+ });
31
+ await manager.removeWorktree(name);
32
+ console.log(`✅ Session "${name}" and worktree removed`);
33
+ } else if (metadata?.worktreePath && options.keepWorktree) {
34
+ console.log(`✅ Session "${name}" removed (worktree kept at ${metadata.worktreePath})`);
35
+ } else {
36
+ console.log(`✅ Session "${name}" removed`);
37
+ }
38
+
39
+ // Clean up metadata
40
+ if (metadata) {
41
+ await deleteSessionMetadata(name);
42
+ }
43
+ } catch (error: any) {
44
+ console.error(`❌ Error removing session: ${error.message}`);
45
+ process.exit(1);
46
+ }
47
+ }
@@ -0,0 +1,34 @@
1
+ import * as tmux from '../lib/tmux.js';
2
+
3
+ export async function sendKeysToSession(sessionName: string, keys: string): Promise<void> {
4
+ try {
5
+ // Find session
6
+ const session = await tmux.findSessionByName(sessionName);
7
+ if (!session) {
8
+ console.error(`❌ Session "${sessionName}" not found`);
9
+ process.exit(1);
10
+ }
11
+
12
+ // Get first window and pane
13
+ const windows = await tmux.listWindows(session.id);
14
+ if (!windows || windows.length === 0) {
15
+ console.error(`❌ No windows found in session "${sessionName}"`);
16
+ process.exit(1);
17
+ }
18
+
19
+ const panes = await tmux.listPanes(windows[0].id);
20
+ if (!panes || panes.length === 0) {
21
+ console.error(`❌ No panes found in session "${sessionName}"`);
22
+ process.exit(1);
23
+ }
24
+
25
+ const paneId = panes[0].id;
26
+
27
+ // Send keys without Enter
28
+ await tmux.executeCommand(paneId, keys, false, true);
29
+ console.log(`✅ Keys sent to session "${sessionName}"`);
30
+ } catch (error: any) {
31
+ console.error(`❌ Error sending keys: ${error.message}`);
32
+ process.exit(1);
33
+ }
34
+ }
@@ -0,0 +1,355 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
4
+ import * as readline from 'readline';
5
+
6
+ export interface ShortcutsOptions {
7
+ tmux?: boolean;
8
+ termux?: boolean;
9
+ install?: boolean;
10
+ }
11
+
12
+ // Generate tmux.conf snippet for Warp-like shortcuts
13
+ export function generateTmuxConfig(): string {
14
+ return `# Warp-like keyboard shortcuts (generated by genie-cli)
15
+ # To use: add to ~/.tmux.conf or source this file
16
+
17
+ # Ctrl+T: New window (tab) in current session
18
+ bind-key -n C-t new-window
19
+
20
+ # Ctrl+S: Vertical split (requires stty -ixon in shell rc)
21
+ bind-key -n C-s split-window -v
22
+
23
+ # Alt+S: Horizontal split
24
+ bind-key -n M-s split-window -h
25
+ `;
26
+ }
27
+
28
+ // Generate termux.properties snippet for extra keys
29
+ export function generateTermuxConfig(): string {
30
+ return `# Warp-like extra keys (generated by genie-cli)
31
+ # To use: add to ~/.termux/termux.properties
32
+
33
+ # Extra keys row with F-keys for shortcuts
34
+ # F1=New Tab, F2=VSplit, F3=HSplit
35
+ extra-keys = [['ESC','TAB','CTRL','ALT','F1','F2','F3']]
36
+ `;
37
+ }
38
+
39
+ // Generate shell functions for bashrc/zshrc
40
+ export function generateShellFunctions(): string {
41
+ return `# Warp-like shortcuts (generated by genie-cli)
42
+ # To use: add to ~/.bashrc or ~/.zshrc
43
+
44
+ # Disable Ctrl+S flow control (required for Ctrl+S to work in tmux)
45
+ stty -ixon 2>/dev/null
46
+
47
+ # Genie shortcuts (for Termux extra keys F1-F3)
48
+ genie-new-tab() {
49
+ local session
50
+ session=$(tmux display-message -p '#S' 2>/dev/null)
51
+ if [ -n "$session" ]; then
52
+ tmux new-window
53
+ else
54
+ echo "Not in a tmux session"
55
+ fi
56
+ }
57
+
58
+ genie-vsplit() {
59
+ if tmux display-message -p '#S' >/dev/null 2>&1; then
60
+ tmux split-window -v
61
+ else
62
+ echo "Not in a tmux session"
63
+ fi
64
+ }
65
+
66
+ genie-hsplit() {
67
+ if tmux display-message -p '#S' >/dev/null 2>&1; then
68
+ tmux split-window -h
69
+ else
70
+ echo "Not in a tmux session"
71
+ fi
72
+ }
73
+
74
+ # Bind F1-F3 to genie shortcuts (works in Termux)
75
+ bind -x '"\eOP":"genie-new-tab"' 2>/dev/null # F1
76
+ bind -x '"\eOQ":"genie-vsplit"' 2>/dev/null # F2
77
+ bind -x '"\eOR":"genie-hsplit"' 2>/dev/null # F3
78
+ `;
79
+ }
80
+
81
+ // Display all shortcuts in a readable format
82
+ export function displayShortcuts(): void {
83
+ console.log(`
84
+ Warp-like Terminal Shortcuts for tmux + Termux
85
+
86
+ ┌────────────┬────────────────────────────────────────┐
87
+ │ Shortcut │ Action │
88
+ ├────────────┼────────────────────────────────────────┤
89
+ │ Ctrl+T │ New tab (window) in current session │
90
+ │ Ctrl+S │ Vertical split in current session │
91
+ │ Alt+S │ Horizontal split in current session │
92
+ └────────────┴────────────────────────────────────────┘
93
+
94
+ Termux Extra Keys (F1-F3):
95
+ F1 → New tab F2 → Vertical split
96
+ F3 → Horizontal split
97
+
98
+ Commands:
99
+ term shortcuts --tmux Output tmux.conf snippet
100
+ term shortcuts --termux Output termux.properties snippet
101
+ term shortcuts --install Install to config files
102
+ `);
103
+ }
104
+
105
+ // Helper to prompt user
106
+ async function prompt(question: string): Promise<string> {
107
+ const rl = readline.createInterface({
108
+ input: process.stdin,
109
+ output: process.stdout
110
+ });
111
+
112
+ return new Promise((resolve) => {
113
+ rl.question(question, (answer) => {
114
+ rl.close();
115
+ resolve(answer.trim().toLowerCase());
116
+ });
117
+ });
118
+ }
119
+
120
+ // Check if content already exists in file
121
+ function contentExists(filePath: string, marker: string): boolean {
122
+ if (!existsSync(filePath)) return false;
123
+ const content = readFileSync(filePath, 'utf-8');
124
+ return content.includes(marker);
125
+ }
126
+
127
+ // Install shortcuts to config files
128
+ export async function installShortcuts(): Promise<void> {
129
+ const home = homedir();
130
+ const marker = 'generated by genie-cli';
131
+
132
+ console.log('Installing Warp-like shortcuts...\n');
133
+
134
+ // 1. Install to tmux.conf
135
+ const tmuxConf = join(home, '.tmux.conf');
136
+ if (contentExists(tmuxConf, marker)) {
137
+ console.log('✓ tmux.conf already has genie shortcuts');
138
+ } else {
139
+ const answer = await prompt(`Add shortcuts to ${tmuxConf}? [Y/n] `);
140
+ if (answer !== 'n') {
141
+ const content = '\n' + generateTmuxConfig();
142
+ appendFileSync(tmuxConf, content);
143
+ console.log(`✅ Added to ${tmuxConf}`);
144
+ } else {
145
+ console.log('⏭️ Skipped tmux.conf');
146
+ }
147
+ }
148
+
149
+ // 2. Install to bashrc/zshrc
150
+ const shellRc = existsSync(join(home, '.zshrc'))
151
+ ? join(home, '.zshrc')
152
+ : join(home, '.bashrc');
153
+
154
+ if (contentExists(shellRc, marker)) {
155
+ console.log(`✓ ${shellRc} already has genie shortcuts`);
156
+ } else {
157
+ const answer = await prompt(`Add shell functions to ${shellRc}? [Y/n] `);
158
+ if (answer !== 'n') {
159
+ const content = '\n' + generateShellFunctions();
160
+ appendFileSync(shellRc, content);
161
+ console.log(`✅ Added to ${shellRc}`);
162
+ } else {
163
+ console.log(`⏭️ Skipped ${shellRc}`);
164
+ }
165
+ }
166
+
167
+ // 3. Install to termux.properties (if Termux detected)
168
+ const termuxDir = join(home, '.termux');
169
+ const termuxProps = join(termuxDir, 'termux.properties');
170
+ const isTermux = existsSync(termuxDir) || process.env.TERMUX_VERSION;
171
+
172
+ if (isTermux) {
173
+ if (contentExists(termuxProps, marker)) {
174
+ console.log('✓ termux.properties already has genie shortcuts');
175
+ } else {
176
+ const answer = await prompt(`Add extra keys to ${termuxProps}? [Y/n] `);
177
+ if (answer !== 'n') {
178
+ if (!existsSync(termuxDir)) {
179
+ mkdirSync(termuxDir, { recursive: true });
180
+ }
181
+ const content = '\n' + generateTermuxConfig();
182
+ appendFileSync(termuxProps, content);
183
+ console.log(`✅ Added to ${termuxProps}`);
184
+ console.log(' Run: termux-reload-settings');
185
+ } else {
186
+ console.log('⏭️ Skipped termux.properties');
187
+ }
188
+ }
189
+ }
190
+
191
+ console.log('\n✅ Installation complete!');
192
+ console.log('\nNext steps:');
193
+ console.log(' 1. Reload tmux: tmux source ~/.tmux.conf');
194
+ console.log(' 2. Restart your shell or run: source ~/.bashrc');
195
+ if (isTermux) {
196
+ console.log(' 3. Reload Termux: termux-reload-settings');
197
+ }
198
+ }
199
+
200
+ // Check if shortcuts are installed in a file
201
+ export function isShortcutsInstalled(filePath: string): boolean {
202
+ const marker = 'generated by genie-cli';
203
+ return contentExists(filePath, marker);
204
+ }
205
+
206
+ // Remove content between markers from a file
207
+ function removeMarkedContent(filePath: string, marker: string): boolean {
208
+ if (!existsSync(filePath)) return false;
209
+
210
+ const content = readFileSync(filePath, 'utf-8');
211
+ if (!content.includes(marker)) return false;
212
+
213
+ // Split into lines and find the block to remove
214
+ const lines = content.split('\n');
215
+ const filteredLines: string[] = [];
216
+ let inBlock = false;
217
+ let blockStartIndex = -1;
218
+
219
+ for (let i = 0; i < lines.length; i++) {
220
+ const line = lines[i];
221
+ if (line.includes(marker) && !inBlock) {
222
+ // Start of a marked block - find the comment start (could be previous line)
223
+ inBlock = true;
224
+ // Check if previous line is a blank line we should also remove
225
+ if (blockStartIndex === -1 && filteredLines.length > 0 && filteredLines[filteredLines.length - 1].trim() === '') {
226
+ filteredLines.pop();
227
+ }
228
+ continue;
229
+ }
230
+ if (inBlock) {
231
+ // Check if this is the end of the block (empty line followed by non-genie content)
232
+ if (line.trim() === '' && i + 1 < lines.length && !lines[i + 1].includes('genie')) {
233
+ inBlock = false;
234
+ continue;
235
+ }
236
+ // Check if this line is part of our genie config (has specific markers)
237
+ const isGenieContent =
238
+ line.includes('genie') ||
239
+ line.includes('Ctrl+') ||
240
+ line.includes('split-window') ||
241
+ line.includes('new-window') ||
242
+ line.includes('stty -ixon') ||
243
+ line.includes('Warp-like') ||
244
+ line.includes('bind-key -n') ||
245
+ line.includes('extra-keys') ||
246
+ line.includes('F1=') ||
247
+ line.includes('bind -x') ||
248
+ line.startsWith('#') && lines[i - 1]?.includes('genie');
249
+
250
+ if (!isGenieContent && line.trim() !== '') {
251
+ inBlock = false;
252
+ filteredLines.push(line);
253
+ }
254
+ // Skip genie content lines
255
+ continue;
256
+ }
257
+ filteredLines.push(line);
258
+ }
259
+
260
+ // Clean up trailing newlines
261
+ while (filteredLines.length > 0 && filteredLines[filteredLines.length - 1].trim() === '') {
262
+ filteredLines.pop();
263
+ }
264
+
265
+ // Add back one trailing newline if file had content
266
+ const newContent = filteredLines.length > 0 ? filteredLines.join('\n') + '\n' : '';
267
+ writeFileSync(filePath, newContent);
268
+ return true;
269
+ }
270
+
271
+ // Uninstall shortcuts from config files
272
+ export async function uninstallShortcuts(): Promise<void> {
273
+ const home = homedir();
274
+ const marker = 'generated by genie-cli';
275
+
276
+ console.log('Uninstalling Warp-like shortcuts...\n');
277
+
278
+ // 1. Remove from tmux.conf
279
+ const tmuxConf = join(home, '.tmux.conf');
280
+ if (!contentExists(tmuxConf, marker)) {
281
+ console.log('✓ tmux.conf has no genie shortcuts');
282
+ } else {
283
+ const answer = await prompt(`Remove shortcuts from ${tmuxConf}? [Y/n] `);
284
+ if (answer !== 'n') {
285
+ if (removeMarkedContent(tmuxConf, marker)) {
286
+ console.log(`✅ Removed from ${tmuxConf}`);
287
+ }
288
+ } else {
289
+ console.log('⏭️ Skipped tmux.conf');
290
+ }
291
+ }
292
+
293
+ // 2. Remove from bashrc/zshrc
294
+ const zshrc = join(home, '.zshrc');
295
+ const bashrc = join(home, '.bashrc');
296
+
297
+ // Check both files
298
+ for (const shellRc of [zshrc, bashrc]) {
299
+ if (!existsSync(shellRc)) continue;
300
+ if (!contentExists(shellRc, marker)) {
301
+ console.log(`✓ ${shellRc} has no genie shortcuts`);
302
+ } else {
303
+ const answer = await prompt(`Remove shell functions from ${shellRc}? [Y/n] `);
304
+ if (answer !== 'n') {
305
+ if (removeMarkedContent(shellRc, marker)) {
306
+ console.log(`✅ Removed from ${shellRc}`);
307
+ }
308
+ } else {
309
+ console.log(`⏭️ Skipped ${shellRc}`);
310
+ }
311
+ }
312
+ }
313
+
314
+ // 3. Remove from termux.properties (if Termux detected)
315
+ const termuxDir = join(home, '.termux');
316
+ const termuxProps = join(termuxDir, 'termux.properties');
317
+ const isTermux = existsSync(termuxDir) || process.env.TERMUX_VERSION;
318
+
319
+ if (isTermux) {
320
+ if (!contentExists(termuxProps, marker)) {
321
+ console.log('✓ termux.properties has no genie shortcuts');
322
+ } else {
323
+ const answer = await prompt(`Remove extra keys from ${termuxProps}? [Y/n] `);
324
+ if (answer !== 'n') {
325
+ if (removeMarkedContent(termuxProps, marker)) {
326
+ console.log(`✅ Removed from ${termuxProps}`);
327
+ console.log(' Run: termux-reload-settings');
328
+ }
329
+ } else {
330
+ console.log('⏭️ Skipped termux.properties');
331
+ }
332
+ }
333
+ }
334
+
335
+ console.log('\n✅ Uninstallation complete!');
336
+ console.log('\nNext steps:');
337
+ console.log(' 1. Reload tmux: tmux source ~/.tmux.conf');
338
+ console.log(' 2. Restart your shell or run: source ~/.bashrc');
339
+ if (isTermux) {
340
+ console.log(' 3. Reload Termux: termux-reload-settings');
341
+ }
342
+ }
343
+
344
+ // Main handler
345
+ export async function handleShortcuts(options: ShortcutsOptions): Promise<void> {
346
+ if (options.tmux) {
347
+ console.log(generateTmuxConfig());
348
+ } else if (options.termux) {
349
+ console.log(generateTermuxConfig());
350
+ } else if (options.install) {
351
+ await installShortcuts();
352
+ } else {
353
+ displayShortcuts();
354
+ }
355
+ }
@@ -0,0 +1,87 @@
1
+ import { join } from 'path';
2
+ import * as tmux from '../lib/tmux.js';
3
+ import { createWorktreeManager } from '../lib/worktree.js';
4
+
5
+ export interface SplitOptions {
6
+ workspace?: string;
7
+ worktree?: string;
8
+ }
9
+
10
+ export async function splitSessionPane(
11
+ sessionName: string,
12
+ direction?: string,
13
+ options: SplitOptions = {}
14
+ ): Promise<void> {
15
+ try {
16
+ // Find session
17
+ const session = await tmux.findSessionByName(sessionName);
18
+ if (!session) {
19
+ console.error(`❌ Session "${sessionName}" not found`);
20
+ process.exit(1);
21
+ }
22
+
23
+ // Get first window and pane
24
+ const windows = await tmux.listWindows(session.id);
25
+ if (!windows || windows.length === 0) {
26
+ console.error(`❌ No windows found in session "${sessionName}"`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const panes = await tmux.listPanes(windows[0].id);
31
+ if (!panes || panes.length === 0) {
32
+ console.error(`❌ No panes found in session "${sessionName}"`);
33
+ process.exit(1);
34
+ }
35
+
36
+ const paneId = panes[0].id;
37
+
38
+ // Determine direction
39
+ const splitDirection = direction === 'h' ? 'horizontal' : 'vertical';
40
+
41
+ // Handle workspace and worktree options
42
+ let workingDir: string | undefined;
43
+
44
+ if (options.worktree) {
45
+ if (!options.workspace) {
46
+ console.error('❌ --worktree requires --workspace');
47
+ process.exit(1);
48
+ }
49
+
50
+ const manager = createWorktreeManager({
51
+ baseDir: join(options.workspace, '.worktrees'),
52
+ repoPath: options.workspace
53
+ });
54
+
55
+ // Check if worktree exists, create if not
56
+ const exists = await manager.worktreeExists(options.worktree);
57
+ if (!exists) {
58
+ const wt = await manager.createWorktree(options.worktree, true);
59
+ console.log(`✅ Worktree created at ${wt.path}`);
60
+ }
61
+
62
+ workingDir = manager.getWorktreePath(options.worktree);
63
+ } else if (options.workspace) {
64
+ workingDir = options.workspace;
65
+ }
66
+
67
+ // Split pane
68
+ const newPane = await tmux.splitPane(paneId, splitDirection);
69
+ if (!newPane) {
70
+ console.error('❌ Failed to split pane');
71
+ process.exit(1);
72
+ }
73
+
74
+ // Change to working directory if specified
75
+ if (workingDir && newPane) {
76
+ await tmux.executeTmux(`send-keys -t '${newPane.id}' 'cd ${workingDir}' Enter`);
77
+ }
78
+
79
+ console.log(`✅ Pane split ${splitDirection}ly in session "${sessionName}"`);
80
+ if (workingDir) {
81
+ console.log(` Working directory: ${workingDir}`);
82
+ }
83
+ } catch (error: any) {
84
+ console.error(`❌ Error splitting pane: ${error.message}`);
85
+ process.exit(1);
86
+ }
87
+ }