@enruana/claude-orka 0.3.2 → 0.4.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.
@@ -1,171 +1,1789 @@
1
- import { app, BrowserWindow, ipcMain } from 'electron';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
- import { ClaudeOrka } from '../../src/core/ClaudeOrka';
5
- import chokidar from 'chokidar';
6
- import execa from 'execa';
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
- // Store windows by project path
10
- const windows = new Map();
11
- // Store active sessions
12
- let currentSessionId = null;
13
- let currentProjectPath = null;
14
- function createWindow(sessionId, projectPath) {
15
- // If window already exists for this project, focus it
16
- if (windows.has(projectPath)) {
17
- const existingWindow = windows.get(projectPath);
18
- existingWindow.focus();
19
- return existingWindow;
20
- }
21
- const mainWindow = new BrowserWindow({
22
- width: 600,
23
- height: 800,
24
- minWidth: 500,
25
- minHeight: 600,
26
- frame: false,
27
- transparent: true,
28
- alwaysOnTop: true,
29
- resizable: true,
30
- webPreferences: {
31
- preload: path.join(__dirname, '../preload/preload.js'),
32
- contextIsolation: true,
33
- nodeIntegration: false,
34
- },
35
- });
36
- // Store session info
37
- currentSessionId = sessionId;
38
- currentProjectPath = projectPath;
39
- // Load UI
40
- if (process.env.NODE_ENV === 'development') {
41
- // Dev mode - load from Vite dev server
42
- mainWindow.loadURL('http://localhost:5173');
43
- mainWindow.webContents.openDevTools();
44
- }
45
- else {
46
- // Production - load from built files
47
- const indexPath = path.join(__dirname, '../renderer/index.html');
48
- mainWindow.loadFile(indexPath);
49
- }
50
- // Watch state.json for changes
51
- watchStateFile(projectPath, mainWindow);
52
- // Store window
53
- windows.set(projectPath, mainWindow);
54
- mainWindow.on('closed', () => {
55
- windows.delete(projectPath);
56
- });
57
- return mainWindow;
58
- }
59
- function watchStateFile(projectPath, window) {
60
- const statePath = path.join(projectPath, '.claude-orka/state.json');
61
- const watcher = chokidar.watch(statePath, {
62
- persistent: true,
63
- ignoreInitial: true,
64
- });
65
- watcher.on('change', async () => {
1
+ // electron/main/main.ts
2
+ import { app, BrowserWindow, ipcMain, shell } from "electron";
3
+ import path5 from "path";
4
+ import { fileURLToPath as fileURLToPath2 } from "url";
5
+
6
+ // src/core/StateManager.ts
7
+ import path3 from "path";
8
+ import fs3 from "fs-extra";
9
+
10
+ // src/utils/tmux.ts
11
+ import execa from "execa";
12
+
13
+ // src/utils/logger.ts
14
+ import fs from "fs-extra";
15
+ import path from "path";
16
+ var Logger = class {
17
+ level = 1 /* INFO */;
18
+ logFilePath = null;
19
+ setLevel(level) {
20
+ this.level = level;
21
+ }
22
+ setLogFile(projectPath) {
23
+ const logDir = path.join(projectPath, ".claude-orka");
24
+ fs.ensureDirSync(logDir);
25
+ this.logFilePath = path.join(logDir, "orka.log");
26
+ }
27
+ writeToFile(level, ...args) {
28
+ if (!this.logFilePath) return;
29
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
30
+ const message = args.map(
31
+ (arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
32
+ ).join(" ");
33
+ const logLine = `${timestamp} [${level}] ${message}
34
+ `;
35
+ try {
36
+ fs.appendFileSync(this.logFilePath, logLine);
37
+ } catch (error) {
38
+ }
39
+ }
40
+ debug(...args) {
41
+ if (this.level <= 0 /* DEBUG */) {
42
+ console.log("[DEBUG]", ...args);
43
+ this.writeToFile("DEBUG", ...args);
44
+ }
45
+ }
46
+ info(...args) {
47
+ if (this.level <= 1 /* INFO */) {
48
+ console.log("[INFO]", ...args);
49
+ this.writeToFile("INFO", ...args);
50
+ }
51
+ }
52
+ warn(...args) {
53
+ if (this.level <= 2 /* WARN */) {
54
+ console.warn("[WARN]", ...args);
55
+ this.writeToFile("WARN", ...args);
56
+ }
57
+ }
58
+ error(...args) {
59
+ if (this.level <= 3 /* ERROR */) {
60
+ console.error("[ERROR]", ...args);
61
+ this.writeToFile("ERROR", ...args);
62
+ }
63
+ }
64
+ };
65
+ var logger = new Logger();
66
+
67
+ // src/utils/tmux.ts
68
+ var TmuxError = class extends Error {
69
+ constructor(message, command, originalError) {
70
+ super(message);
71
+ this.command = command;
72
+ this.originalError = originalError;
73
+ this.name = "TmuxError";
74
+ }
75
+ };
76
+ var TmuxCommands = class {
77
+ /**
78
+ * Verificar si tmux está disponible
79
+ */
80
+ static async isAvailable() {
81
+ try {
82
+ await execa("which", ["tmux"]);
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+ /**
89
+ * Crear una nueva sesión tmux en modo detached
90
+ */
91
+ static async createSession(name, projectPath) {
92
+ try {
93
+ logger.debug(`Creating tmux session: ${name} at ${projectPath}`);
94
+ await execa("tmux", ["new-session", "-d", "-s", name, "-c", projectPath]);
95
+ logger.info(`Tmux session created: ${name}`);
96
+ } catch (error) {
97
+ throw new TmuxError(
98
+ `Failed to create tmux session: ${name}`,
99
+ `tmux new-session -d -s ${name} -c ${projectPath}`,
100
+ error
101
+ );
102
+ }
103
+ }
104
+ /**
105
+ * Abrir una terminal que se adjunte a una sesión tmux existente
106
+ * (Solo macOS por ahora)
107
+ */
108
+ static async openTerminalWindow(sessionName) {
109
+ try {
110
+ logger.debug(`Opening terminal window for session: ${sessionName}`);
111
+ const platform = process.platform;
112
+ if (platform === "darwin") {
113
+ const script = `tell application "Terminal"
114
+ do script "tmux attach -t ${sessionName}"
115
+ activate
116
+ end tell`;
117
+ await execa("osascript", ["-e", script]);
118
+ logger.info("Terminal window opened (Terminal.app)");
119
+ } else if (platform === "linux") {
66
120
  try {
67
- const orka = new ClaudeOrka(projectPath);
68
- await orka.initialize();
69
- if (currentSessionId) {
70
- const session = await orka.getSession(currentSessionId);
71
- if (session) {
72
- window.webContents.send('state-updated', session);
73
- }
74
- }
75
- }
76
- catch (error) {
77
- console.error('Error watching state file:', error);
121
+ await execa("gnome-terminal", ["--", "tmux", "attach", "-t", sessionName]);
122
+ logger.info("Terminal window opened (gnome-terminal)");
123
+ } catch {
124
+ try {
125
+ await execa("xterm", ["-e", `tmux attach -t ${sessionName}`]);
126
+ logger.info("Terminal window opened (xterm)");
127
+ } catch {
128
+ logger.warn("Could not open terminal window on Linux");
129
+ throw new Error("No suitable terminal emulator found");
130
+ }
78
131
  }
132
+ } else {
133
+ logger.warn(`Platform ${platform} not supported for opening terminal windows`);
134
+ throw new Error(`Platform ${platform} not supported`);
135
+ }
136
+ } catch (error) {
137
+ throw new TmuxError(
138
+ `Failed to open terminal window for session: ${sessionName}`,
139
+ `osascript/terminal`,
140
+ error
141
+ );
142
+ }
143
+ }
144
+ /**
145
+ * Cerrar una sesión tmux
146
+ */
147
+ static async killSession(sessionName) {
148
+ try {
149
+ logger.debug(`Killing tmux session: ${sessionName}`);
150
+ await execa("tmux", ["kill-session", "-t", sessionName]);
151
+ logger.info(`Tmux session killed: ${sessionName}`);
152
+ } catch (error) {
153
+ throw new TmuxError(
154
+ `Failed to kill tmux session: ${sessionName}`,
155
+ `tmux kill-session -t ${sessionName}`,
156
+ error
157
+ );
158
+ }
159
+ }
160
+ /**
161
+ * Verificar si una sesión existe
162
+ */
163
+ static async sessionExists(sessionName) {
164
+ try {
165
+ await execa("tmux", ["has-session", "-t", sessionName]);
166
+ return true;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+ /**
172
+ * Obtener el ID del pane principal de una sesión
173
+ */
174
+ static async getMainPaneId(sessionName) {
175
+ try {
176
+ logger.debug(`Getting main pane ID for session: ${sessionName}`);
177
+ const { stdout } = await execa("tmux", [
178
+ "list-panes",
179
+ "-t",
180
+ sessionName,
181
+ "-F",
182
+ "#{pane_id}"
183
+ ]);
184
+ const paneId = stdout.split("\n")[0];
185
+ logger.debug(`Main pane ID: ${paneId}`);
186
+ return paneId;
187
+ } catch (error) {
188
+ throw new TmuxError(
189
+ `Failed to get main pane ID for session: ${sessionName}`,
190
+ `tmux list-panes -t ${sessionName} -F '#{pane_id}'`,
191
+ error
192
+ );
193
+ }
194
+ }
195
+ /**
196
+ * Obtener el ID de la ventana principal de una sesión
197
+ */
198
+ static async getMainWindowId(sessionName) {
199
+ try {
200
+ logger.debug(`Getting main window ID for session: ${sessionName}`);
201
+ const { stdout } = await execa("tmux", [
202
+ "list-windows",
203
+ "-t",
204
+ sessionName,
205
+ "-F",
206
+ "#{window_id}"
207
+ ]);
208
+ const windowId = stdout.split("\n")[0];
209
+ logger.debug(`Main window ID: ${windowId}`);
210
+ return windowId;
211
+ } catch (error) {
212
+ throw new TmuxError(
213
+ `Failed to get main window ID for session: ${sessionName}`,
214
+ `tmux list-windows -t ${sessionName} -F '#{window_id}'`,
215
+ error
216
+ );
217
+ }
218
+ }
219
+ /**
220
+ * Dividir un pane (crear fork)
221
+ * @param sessionName Nombre de la sesión
222
+ * @param vertical Si es true, divide verticalmente (-h), si es false horizontalmente (-v)
223
+ * @returns ID del nuevo pane creado
224
+ */
225
+ static async splitPane(sessionName, vertical = false) {
226
+ try {
227
+ const direction = vertical ? "-h" : "-v";
228
+ logger.debug(`Splitting pane in session ${sessionName} (${vertical ? "vertical" : "horizontal"})`);
229
+ await execa("tmux", ["split-window", "-t", sessionName, direction]);
230
+ const { stdout } = await execa("tmux", [
231
+ "list-panes",
232
+ "-t",
233
+ sessionName,
234
+ "-F",
235
+ "#{pane_id}"
236
+ ]);
237
+ const panes = stdout.split("\n");
238
+ const newPaneId = panes[panes.length - 1];
239
+ logger.info(`New pane created: ${newPaneId}`);
240
+ return newPaneId;
241
+ } catch (error) {
242
+ throw new TmuxError(
243
+ `Failed to split pane in session: ${sessionName}`,
244
+ `tmux split-window -t ${sessionName} ${vertical ? "-h" : "-v"}`,
245
+ error
246
+ );
247
+ }
248
+ }
249
+ /**
250
+ * Listar todos los panes de una sesión
251
+ * @param sessionName Nombre de la sesión
252
+ * @returns Array de IDs de panes
253
+ */
254
+ static async listPanes(sessionName) {
255
+ try {
256
+ const { stdout } = await execa("tmux", [
257
+ "list-panes",
258
+ "-t",
259
+ sessionName,
260
+ "-F",
261
+ "#{pane_id}"
262
+ ]);
263
+ return stdout.trim().split("\n").filter(Boolean);
264
+ } catch (error) {
265
+ throw new TmuxError(
266
+ `Failed to list panes for session: ${sessionName}`,
267
+ `tmux list-panes -t ${sessionName}`,
268
+ error
269
+ );
270
+ }
271
+ }
272
+ /**
273
+ * Cerrar un pane específico
274
+ */
275
+ static async killPane(paneId) {
276
+ try {
277
+ logger.debug(`Killing pane: ${paneId}`);
278
+ await execa("tmux", ["kill-pane", "-t", paneId]);
279
+ logger.info(`Pane killed: ${paneId}`);
280
+ } catch (error) {
281
+ throw new TmuxError(
282
+ `Failed to kill pane: ${paneId}`,
283
+ `tmux kill-pane -t ${paneId}`,
284
+ error
285
+ );
286
+ }
287
+ }
288
+ /**
289
+ * Enviar texto a un pane (SIN Enter)
290
+ * IMPORTANTE: No envía Enter, debe llamarse a sendEnter() por separado
291
+ */
292
+ static async sendKeys(paneId, text) {
293
+ try {
294
+ logger.debug(`Sending keys to pane ${paneId}: ${text.substring(0, 50)}...`);
295
+ await execa("tmux", ["send-keys", "-t", paneId, text]);
296
+ } catch (error) {
297
+ throw new TmuxError(
298
+ `Failed to send keys to pane: ${paneId}`,
299
+ `tmux send-keys -t ${paneId} "${text}"`,
300
+ error
301
+ );
302
+ }
303
+ }
304
+ /**
305
+ * Enviar SOLO Enter a un pane
306
+ */
307
+ static async sendEnter(paneId) {
308
+ try {
309
+ logger.debug(`Sending Enter to pane: ${paneId}`);
310
+ await execa("tmux", ["send-keys", "-t", paneId, "Enter"]);
311
+ } catch (error) {
312
+ throw new TmuxError(
313
+ `Failed to send Enter to pane: ${paneId}`,
314
+ `tmux send-keys -t ${paneId} Enter`,
315
+ error
316
+ );
317
+ }
318
+ }
319
+ /**
320
+ * Enviar teclas especiales (flechas, escape, etc.)
321
+ * @param paneId ID del pane
322
+ * @param key Nombre de la tecla: 'Up', 'Down', 'Left', 'Right', 'Escape', 'Space', etc.
323
+ */
324
+ static async sendSpecialKey(paneId, key) {
325
+ try {
326
+ logger.debug(`Sending special key '${key}' to pane: ${paneId}`);
327
+ await execa("tmux", ["send-keys", "-t", paneId, key]);
328
+ } catch (error) {
329
+ throw new TmuxError(
330
+ `Failed to send special key '${key}' to pane: ${paneId}`,
331
+ `tmux send-keys -t ${paneId} ${key}`,
332
+ error
333
+ );
334
+ }
335
+ }
336
+ /**
337
+ * Capturar el contenido de un pane
338
+ * @param paneId ID del pane
339
+ * @param startLine Línea desde donde empezar a capturar (negativo = desde el final)
340
+ * @returns Contenido del pane
341
+ */
342
+ static async capturePane(paneId, startLine = -100) {
343
+ try {
344
+ logger.debug(`Capturing pane ${paneId} from line ${startLine}`);
345
+ const { stdout } = await execa("tmux", [
346
+ "capture-pane",
347
+ "-t",
348
+ paneId,
349
+ "-p",
350
+ "-S",
351
+ startLine.toString()
352
+ ]);
353
+ return stdout;
354
+ } catch (error) {
355
+ throw new TmuxError(
356
+ `Failed to capture pane: ${paneId}`,
357
+ `tmux capture-pane -t ${paneId} -p -S ${startLine}`,
358
+ error
359
+ );
360
+ }
361
+ }
362
+ /**
363
+ * Listar todas las sesiones tmux
364
+ */
365
+ static async listSessions() {
366
+ try {
367
+ const { stdout } = await execa("tmux", ["list-sessions", "-F", "#{session_id}:#{session_name}"]);
368
+ return stdout.split("\n").map((line) => {
369
+ const [id, name] = line.split(":");
370
+ return { id, name };
371
+ });
372
+ } catch (error) {
373
+ if (error.stderr?.includes("no server running")) {
374
+ return [];
375
+ }
376
+ throw new TmuxError(
377
+ "Failed to list tmux sessions",
378
+ "tmux list-sessions",
379
+ error
380
+ );
381
+ }
382
+ }
383
+ };
384
+
385
+ // src/utils/claude-history.ts
386
+ import fs2 from "fs-extra";
387
+ import os from "os";
388
+ import path2 from "path";
389
+ var CLAUDE_HISTORY_PATH = path2.join(os.homedir(), ".claude", "history.jsonl");
390
+ async function readClaudeHistory() {
391
+ try {
392
+ const exists = await fs2.pathExists(CLAUDE_HISTORY_PATH);
393
+ if (!exists) {
394
+ logger.warn(`Claude history file not found: ${CLAUDE_HISTORY_PATH}`);
395
+ return [];
396
+ }
397
+ const content = await fs2.readFile(CLAUDE_HISTORY_PATH, "utf-8");
398
+ const lines = content.trim().split("\n").filter(Boolean);
399
+ const entries = [];
400
+ for (const line of lines) {
401
+ try {
402
+ const entry = JSON.parse(line);
403
+ entries.push(entry);
404
+ } catch (err) {
405
+ logger.warn(`Failed to parse history line: ${line}`);
406
+ }
407
+ }
408
+ return entries;
409
+ } catch (error) {
410
+ logger.error(`Error reading Claude history: ${error}`);
411
+ return [];
412
+ }
413
+ }
414
+ async function getExistingSessionIds() {
415
+ const entries = await readClaudeHistory();
416
+ return new Set(entries.map((e) => e.sessionId));
417
+ }
418
+ async function detectNewSessionId(previousIds, maxWaitMs = 1e4, pollIntervalMs = 500) {
419
+ const startTime = Date.now();
420
+ while (Date.now() - startTime < maxWaitMs) {
421
+ const currentIds = await getExistingSessionIds();
422
+ for (const id of currentIds) {
423
+ if (!previousIds.has(id)) {
424
+ logger.info(`Detected new Claude session: ${id}`);
425
+ return id;
426
+ }
427
+ }
428
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
429
+ }
430
+ logger.warn("Timeout waiting for new Claude session ID");
431
+ return null;
432
+ }
433
+
434
+ // src/core/StateManager.ts
435
+ var StateManager = class {
436
+ projectPath;
437
+ orkaDir;
438
+ statePath;
439
+ constructor(projectPath) {
440
+ this.projectPath = path3.resolve(projectPath);
441
+ this.orkaDir = path3.join(this.projectPath, ".claude-orka");
442
+ this.statePath = path3.join(this.orkaDir, "state.json");
443
+ }
444
+ /**
445
+ * Initialize StateManager
446
+ * Creates necessary folders if they don't exist
447
+ */
448
+ async initialize() {
449
+ logger.debug("Initializing StateManager");
450
+ await this.ensureDirectories();
451
+ if (!await fs3.pathExists(this.statePath)) {
452
+ logger.info("Creating initial state.json");
453
+ const initialState = {
454
+ version: "1.0.0",
455
+ projectPath: this.projectPath,
456
+ sessions: [],
457
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
458
+ };
459
+ await this.save(initialState);
460
+ }
461
+ logger.info("StateManager initialized");
462
+ }
463
+ /**
464
+ * Create directory structure
465
+ */
466
+ async ensureDirectories() {
467
+ await fs3.ensureDir(this.orkaDir);
468
+ logger.debug("Directories ensured");
469
+ }
470
+ /**
471
+ * Read current state
472
+ */
473
+ async read() {
474
+ try {
475
+ const content = await fs3.readFile(this.statePath, "utf-8");
476
+ return JSON.parse(content);
477
+ } catch (error) {
478
+ logger.error("Failed to read state:", error);
479
+ throw new Error(`Failed to read state: ${error.message}`);
480
+ }
481
+ }
482
+ /**
483
+ * Save state
484
+ */
485
+ async save(state) {
486
+ try {
487
+ state.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
488
+ await fs3.writeFile(this.statePath, JSON.stringify(state, null, 2), "utf-8");
489
+ logger.debug("State saved");
490
+ } catch (error) {
491
+ logger.error("Failed to save state:", error);
492
+ throw new Error(`Failed to save state: ${error.message}`);
493
+ }
494
+ }
495
+ // --- OPERACIONES DE SESIONES ---
496
+ /**
497
+ * Obtener el estado completo
498
+ */
499
+ async getState() {
500
+ return await this.read();
501
+ }
502
+ /**
503
+ * Listar todas las sesiones con filtros opcionales
504
+ */
505
+ async listSessions(filters) {
506
+ const state = await this.read();
507
+ let sessions = state.sessions;
508
+ if (filters?.status) {
509
+ sessions = sessions.filter((s) => s.status === filters.status);
510
+ }
511
+ if (filters?.name) {
512
+ sessions = sessions.filter(
513
+ (s) => s.name.toLowerCase().includes(filters.name.toLowerCase())
514
+ );
515
+ }
516
+ return sessions;
517
+ }
518
+ /**
519
+ * Agregar una nueva sesión
520
+ */
521
+ async addSession(session) {
522
+ const state = await this.read();
523
+ state.sessions.push(session);
524
+ await this.save(state);
525
+ logger.info(`Session added: ${session.id}`);
526
+ }
527
+ /**
528
+ * Obtener una sesión por ID
529
+ */
530
+ async getSession(sessionId) {
531
+ const state = await this.read();
532
+ return state.sessions.find((s) => s.id === sessionId) || null;
533
+ }
534
+ /**
535
+ * Obtener todas las sesiones con filtros opcionales
536
+ */
537
+ async getAllSessions(filters) {
538
+ const state = await this.read();
539
+ let sessions = state.sessions;
540
+ if (filters?.status) {
541
+ sessions = sessions.filter((s) => s.status === filters.status);
542
+ }
543
+ if (filters?.name) {
544
+ sessions = sessions.filter((s) => s.name.includes(filters.name));
545
+ }
546
+ return sessions;
547
+ }
548
+ /**
549
+ * Actualizar el estado de una sesión
550
+ */
551
+ async updateSessionStatus(sessionId, status) {
552
+ const state = await this.read();
553
+ const session = state.sessions.find((s) => s.id === sessionId);
554
+ if (!session) {
555
+ throw new Error(`Session not found: ${sessionId}`);
556
+ }
557
+ session.status = status;
558
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
559
+ await this.save(state);
560
+ logger.info(`Session ${sessionId} status updated to: ${status}`);
561
+ }
562
+ /**
563
+ * Actualizar una sesión completa
564
+ */
565
+ async updateSession(sessionId, updates) {
566
+ const state = await this.read();
567
+ const sessionIndex = state.sessions.findIndex((s) => s.id === sessionId);
568
+ if (sessionIndex === -1) {
569
+ throw new Error(`Session not found: ${sessionId}`);
570
+ }
571
+ state.sessions[sessionIndex] = {
572
+ ...state.sessions[sessionIndex],
573
+ ...updates,
574
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString()
575
+ };
576
+ await this.save(state);
577
+ logger.debug(`Session ${sessionId} updated`);
578
+ }
579
+ /**
580
+ * Reemplazar una sesión completa
581
+ */
582
+ async replaceSession(session) {
583
+ const state = await this.read();
584
+ const sessionIndex = state.sessions.findIndex((s) => s.id === session.id);
585
+ if (sessionIndex === -1) {
586
+ throw new Error(`Session not found: ${session.id}`);
587
+ }
588
+ state.sessions[sessionIndex] = session;
589
+ await this.save(state);
590
+ logger.debug(`Session ${session.id} replaced`);
591
+ }
592
+ /**
593
+ * Eliminar una sesión permanentemente
594
+ */
595
+ async deleteSession(sessionId) {
596
+ const state = await this.read();
597
+ state.sessions = state.sessions.filter((s) => s.id !== sessionId);
598
+ await this.save(state);
599
+ logger.info(`Session deleted: ${sessionId}`);
600
+ }
601
+ // --- OPERACIONES DE FORKS ---
602
+ /**
603
+ * Agregar un fork a una sesión
604
+ */
605
+ async addFork(sessionId, fork) {
606
+ const state = await this.read();
607
+ const session = state.sessions.find((s) => s.id === sessionId);
608
+ if (!session) {
609
+ throw new Error(`Session not found: ${sessionId}`);
610
+ }
611
+ session.forks.push(fork);
612
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
613
+ await this.save(state);
614
+ logger.info(`Fork added to session ${sessionId}: ${fork.id}`);
615
+ }
616
+ /**
617
+ * Obtener un fork específico
618
+ */
619
+ async getFork(sessionId, forkId) {
620
+ const session = await this.getSession(sessionId);
621
+ if (!session) return null;
622
+ return session.forks.find((f) => f.id === forkId) || null;
623
+ }
624
+ /**
625
+ * Actualizar el estado de un fork
626
+ */
627
+ async updateForkStatus(sessionId, forkId, status) {
628
+ const state = await this.read();
629
+ const session = state.sessions.find((s) => s.id === sessionId);
630
+ if (!session) {
631
+ throw new Error(`Session not found: ${sessionId}`);
632
+ }
633
+ const fork = session.forks.find((f) => f.id === forkId);
634
+ if (!fork) {
635
+ throw new Error(`Fork not found: ${forkId}`);
636
+ }
637
+ fork.status = status;
638
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
639
+ await this.save(state);
640
+ logger.info(`Fork ${forkId} status updated to: ${status}`);
641
+ }
642
+ /**
643
+ * Actualizar el path del contexto de un fork
644
+ */
645
+ async updateForkContext(sessionId, forkId, contextPath) {
646
+ const state = await this.read();
647
+ const session = state.sessions.find((s) => s.id === sessionId);
648
+ if (!session) {
649
+ throw new Error(`Session not found: ${sessionId}`);
650
+ }
651
+ const fork = session.forks.find((f) => f.id === forkId);
652
+ if (!fork) {
653
+ throw new Error(`Fork not found: ${forkId}`);
654
+ }
655
+ fork.contextPath = contextPath;
656
+ await this.save(state);
657
+ logger.debug(`Fork ${forkId} context updated: ${contextPath}`);
658
+ }
659
+ /**
660
+ * Actualizar un fork completo
661
+ */
662
+ async updateFork(sessionId, forkId, updates) {
663
+ const state = await this.read();
664
+ const session = state.sessions.find((s) => s.id === sessionId);
665
+ if (!session) {
666
+ throw new Error(`Session not found: ${sessionId}`);
667
+ }
668
+ const forkIndex = session.forks.findIndex((f) => f.id === forkId);
669
+ if (forkIndex === -1) {
670
+ throw new Error(`Fork not found: ${forkId}`);
671
+ }
672
+ session.forks[forkIndex] = {
673
+ ...session.forks[forkIndex],
674
+ ...updates
675
+ };
676
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
677
+ await this.save(state);
678
+ logger.debug(`Fork ${forkId} updated`);
679
+ }
680
+ /**
681
+ * Eliminar un fork
682
+ */
683
+ async deleteFork(sessionId, forkId) {
684
+ const state = await this.read();
685
+ const session = state.sessions.find((s) => s.id === sessionId);
686
+ if (!session) {
687
+ throw new Error(`Session not found: ${sessionId}`);
688
+ }
689
+ const forkIndex = session.forks.findIndex((f) => f.id === forkId);
690
+ if (forkIndex === -1) {
691
+ throw new Error(`Fork not found: ${forkId}`);
692
+ }
693
+ session.forks.splice(forkIndex, 1);
694
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
695
+ await this.save(state);
696
+ logger.debug(`Fork ${forkId} deleted`);
697
+ }
698
+ // --- OPERACIONES DE CONTEXTOS ---
699
+ /**
700
+ * Guardar un contexto en archivo
701
+ */
702
+ async saveContext(type, id, content) {
703
+ const contextPath = type === "session" ? this.getSessionContextPath(id) : this.getForkContextPath(id);
704
+ const fullPath = path3.join(this.projectPath, contextPath);
705
+ await fs3.writeFile(fullPath, content, "utf-8");
706
+ logger.info(`Context saved: ${contextPath}`);
707
+ return contextPath;
708
+ }
709
+ /**
710
+ * Leer un contexto desde archivo
711
+ */
712
+ async readContext(contextPath) {
713
+ const fullPath = path3.join(this.projectPath, contextPath);
714
+ if (!await fs3.pathExists(fullPath)) {
715
+ throw new Error(`Context file not found: ${contextPath}`);
716
+ }
717
+ return await fs3.readFile(fullPath, "utf-8");
718
+ }
719
+ // --- HELPERS ---
720
+ /**
721
+ * Obtener el path para el contexto de una sesión
722
+ */
723
+ getSessionContextPath(sessionId) {
724
+ return `.claude-orka/sessions/${sessionId}.md`;
725
+ }
726
+ /**
727
+ * Obtener el path para el contexto de un fork
728
+ */
729
+ getForkContextPath(forkId) {
730
+ return `.claude-orka/forks/${forkId}.md`;
731
+ }
732
+ /**
733
+ * Obtener el path para un export manual
734
+ */
735
+ getExportPath(forkId, name) {
736
+ return `.claude-orka/exports/${forkId}-${name}.md`;
737
+ }
738
+ };
739
+
740
+ // src/core/SessionManager.ts
741
+ import { v4 as uuidv4 } from "uuid";
742
+ import path4 from "path";
743
+ import { fileURLToPath } from "url";
744
+ import { dirname } from "path";
745
+ import fs4 from "fs-extra";
746
+ import { spawn } from "child_process";
747
+ import { createRequire } from "module";
748
+ var require2 = createRequire(import.meta.url);
749
+ var __filename = fileURLToPath(import.meta.url);
750
+ var __dirname = dirname(__filename);
751
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
752
+ var SessionManager = class {
753
+ stateManager;
754
+ projectPath;
755
+ constructor(projectPath) {
756
+ this.projectPath = projectPath;
757
+ this.stateManager = new StateManager(projectPath);
758
+ }
759
+ /**
760
+ * Initialize el manager
761
+ */
762
+ async initialize() {
763
+ await this.stateManager.initialize();
764
+ }
765
+ /**
766
+ * Obtener el state
767
+ */
768
+ async getState() {
769
+ return await this.stateManager.getState();
770
+ }
771
+ // ==========================================
772
+ // SESIONES
773
+ // ==========================================
774
+ /**
775
+ * Crear una nueva sesión de Claude Code
776
+ */
777
+ async createSession(name, openTerminal = true) {
778
+ const sessionId = uuidv4();
779
+ const sessionName = name || `Session-${Date.now()}`;
780
+ const tmuxSessionId = `orka-${sessionId}`;
781
+ logger.info(`Creating session: ${sessionName}`);
782
+ await TmuxCommands.createSession(tmuxSessionId, this.projectPath);
783
+ if (openTerminal) {
784
+ await TmuxCommands.openTerminalWindow(tmuxSessionId);
785
+ }
786
+ await sleep(2e3);
787
+ const paneId = await TmuxCommands.getMainPaneId(tmuxSessionId);
788
+ logger.debug(`Main pane ID: ${paneId}`);
789
+ const claudeSessionId = uuidv4();
790
+ await this.initializeClaude(paneId, {
791
+ type: "new",
792
+ sessionId: claudeSessionId,
793
+ sessionName
79
794
  });
80
- window.on('closed', () => {
81
- watcher.close();
795
+ const session = {
796
+ id: sessionId,
797
+ name: sessionName,
798
+ tmuxSessionId,
799
+ status: "active",
800
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
801
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
802
+ main: {
803
+ claudeSessionId,
804
+ tmuxPaneId: paneId,
805
+ status: "active"
806
+ },
807
+ forks: []
808
+ };
809
+ await this.stateManager.addSession(session);
810
+ logger.info(`Session created: ${sessionName} (${sessionId})`);
811
+ if (openTerminal) {
812
+ await this.launchUI(sessionId);
813
+ }
814
+ return session;
815
+ }
816
+ /**
817
+ * Restaurar una sesión guardada
818
+ */
819
+ async resumeSession(sessionId, openTerminal = true) {
820
+ const session = await this.getSession(sessionId);
821
+ if (!session) {
822
+ throw new Error(`Session ${sessionId} not found`);
823
+ }
824
+ logger.info(`Resuming session: ${session.name}`);
825
+ const tmuxSessionId = `orka-${sessionId}`;
826
+ if (session.status === "active") {
827
+ logger.info(`Session is already active, reconnecting...`);
828
+ if (openTerminal) {
829
+ await TmuxCommands.openTerminalWindow(tmuxSessionId);
830
+ await this.launchUI(sessionId);
831
+ }
832
+ return session;
833
+ }
834
+ await TmuxCommands.createSession(tmuxSessionId, this.projectPath);
835
+ if (openTerminal) {
836
+ await TmuxCommands.openTerminalWindow(tmuxSessionId);
837
+ }
838
+ await sleep(2e3);
839
+ const paneId = await TmuxCommands.getMainPaneId(tmuxSessionId);
840
+ await this.initializeClaude(paneId, {
841
+ type: "resume",
842
+ resumeSessionId: session.main.claudeSessionId,
843
+ sessionName: session.name
82
844
  });
83
- }
84
- // IPC Handlers
85
- ipcMain.handle('get-session', async () => {
86
- if (!currentSessionId || !currentProjectPath) {
87
- throw new Error('No active session');
88
- }
89
- const orka = new ClaudeOrka(currentProjectPath);
90
- await orka.initialize();
91
- const session = await orka.getSession(currentSessionId);
845
+ session.tmuxSessionId = tmuxSessionId;
846
+ session.main.tmuxPaneId = paneId;
847
+ session.main.status = "active";
848
+ session.status = "active";
849
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
850
+ await this.stateManager.replaceSession(session);
851
+ const forksToRestore = session.forks.filter((f) => f.status !== "merged");
852
+ if (forksToRestore.length > 0) {
853
+ logger.info(`Restoring ${forksToRestore.length} fork(s)...`);
854
+ for (const fork of forksToRestore) {
855
+ await this.resumeFork(sessionId, fork.id);
856
+ }
857
+ }
858
+ logger.info(`Session resumed: ${session.name}`);
859
+ if (openTerminal) {
860
+ await this.launchUI(sessionId);
861
+ }
92
862
  return session;
93
- });
94
- ipcMain.handle('select-node', async (_, nodeId) => {
95
- if (!currentSessionId || !currentProjectPath) {
96
- throw new Error('No active session');
97
- }
98
- const orka = new ClaudeOrka(currentProjectPath);
99
- await orka.initialize();
100
- const session = await orka.getSession(currentSessionId);
101
- if (!session)
102
- return;
103
- // Get tmux pane ID for the selected node
104
- let tmuxPaneId;
105
- if (nodeId === 'main') {
106
- tmuxPaneId = session.main?.tmuxPaneId;
863
+ }
864
+ /**
865
+ * Close a session (save and kill tmux)
866
+ */
867
+ async closeSession(sessionId) {
868
+ const session = await this.getSession(sessionId);
869
+ if (!session) {
870
+ throw new Error(`Session ${sessionId} not found`);
107
871
  }
108
- else {
109
- const fork = session.forks.find((f) => f.id === nodeId);
110
- tmuxPaneId = fork?.tmuxPaneId;
872
+ logger.info(`Closing session: ${session.name}`);
873
+ const activeForks = session.forks.filter((f) => f.status === "active");
874
+ for (const fork of activeForks) {
875
+ await this.closeFork(sessionId, fork.id);
111
876
  }
112
- if (tmuxPaneId) {
113
- // Focus the tmux pane
114
- await execa('tmux', ['select-pane', '-t', tmuxPaneId]);
877
+ if (session.tmuxSessionId) {
878
+ await TmuxCommands.killSession(session.tmuxSessionId);
115
879
  }
116
- });
117
- ipcMain.handle('create-fork', async (_, sessionId, name) => {
118
- if (!currentProjectPath) {
119
- throw new Error('No active project');
880
+ session.main.status = "saved";
881
+ session.main.tmuxPaneId = void 0;
882
+ session.status = "saved";
883
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
884
+ await this.stateManager.replaceSession(session);
885
+ logger.info(`Session closed: ${session.name}`);
886
+ }
887
+ /**
888
+ * Eliminar una sesión permanentemente
889
+ */
890
+ async deleteSession(sessionId) {
891
+ const session = await this.getSession(sessionId);
892
+ if (!session) {
893
+ throw new Error(`Session ${sessionId} not found`);
894
+ }
895
+ logger.info(`Deleting session: ${session.name}`);
896
+ if (session.status === "active") {
897
+ await this.closeSession(sessionId);
898
+ }
899
+ await this.stateManager.deleteSession(sessionId);
900
+ logger.info(`Session deleted: ${session.name}`);
901
+ }
902
+ /**
903
+ * Listar sesiones con filtros opcionales
904
+ */
905
+ async listSessions(filters) {
906
+ return await this.stateManager.listSessions(filters);
907
+ }
908
+ /**
909
+ * Obtener una sesión por ID
910
+ */
911
+ async getSession(sessionId) {
912
+ return await this.stateManager.getSession(sessionId);
913
+ }
914
+ // ==========================================
915
+ // FORKS
916
+ // ==========================================
917
+ /**
918
+ * Crear un fork (rama de conversación)
919
+ */
920
+ async createFork(sessionId, name, parentId = "main", vertical = false) {
921
+ const session = await this.getSession(sessionId);
922
+ if (!session) {
923
+ throw new Error(`Session ${sessionId} not found`);
924
+ }
925
+ const forkId = uuidv4();
926
+ const forkName = name || `Fork-${session.forks.length + 1}`;
927
+ logger.info(`Creating fork: ${forkName} from parent ${parentId} in session ${session.name}`);
928
+ let parentClaudeSessionId;
929
+ if (parentId === "main") {
930
+ parentClaudeSessionId = session.main.claudeSessionId;
931
+ } else {
932
+ const parentFork = session.forks.find((f) => f.id === parentId);
933
+ if (!parentFork) {
934
+ throw new Error(`Parent fork ${parentId} not found`);
935
+ }
936
+ parentClaudeSessionId = parentFork.claudeSessionId;
937
+ }
938
+ logger.debug(`Parent Claude session ID: ${parentClaudeSessionId}`);
939
+ await TmuxCommands.splitPane(session.tmuxSessionId, vertical);
940
+ await sleep(1e3);
941
+ const allPanes = await TmuxCommands.listPanes(session.tmuxSessionId);
942
+ const forkPaneId = allPanes[allPanes.length - 1];
943
+ logger.debug(`Fork pane ID: ${forkPaneId}`);
944
+ const existingIds = await getExistingSessionIds();
945
+ logger.debug(`Existing sessions before fork: ${existingIds.size}`);
946
+ await this.initializeClaude(forkPaneId, {
947
+ type: "fork",
948
+ parentSessionId: parentClaudeSessionId,
949
+ forkName
950
+ });
951
+ logger.info("Detecting fork session ID from history...");
952
+ const detectedForkId = await detectNewSessionId(existingIds, 3e4, 500);
953
+ if (!detectedForkId) {
954
+ throw new Error(
955
+ "Failed to detect fork session ID. Fork may not have been created. Check if the parent session is valid."
956
+ );
957
+ }
958
+ logger.info(`Fork session ID detected: ${detectedForkId}`);
959
+ const fork = {
960
+ id: forkId,
961
+ name: forkName,
962
+ parentId,
963
+ claudeSessionId: detectedForkId,
964
+ // ✅ ID real detectado
965
+ tmuxPaneId: forkPaneId,
966
+ status: "active",
967
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
968
+ };
969
+ session.forks.push(fork);
970
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
971
+ await this.stateManager.replaceSession(session);
972
+ logger.info(`Fork created: ${forkName} (${forkId})`);
973
+ return fork;
974
+ }
975
+ /**
976
+ * Restaurar un fork guardado
977
+ */
978
+ async resumeFork(sessionId, forkId) {
979
+ const session = await this.getSession(sessionId);
980
+ if (!session) {
981
+ throw new Error(`Session ${sessionId} not found`);
982
+ }
983
+ const fork = session.forks.find((f) => f.id === forkId);
984
+ if (!fork) {
985
+ throw new Error(`Fork ${forkId} not found`);
120
986
  }
121
- const orka = new ClaudeOrka(currentProjectPath);
122
- await orka.initialize();
123
- const fork = await orka.createFork(sessionId, name);
987
+ logger.info(`Resuming fork: ${fork.name}`);
988
+ await TmuxCommands.splitPane(session.tmuxSessionId, false);
989
+ await sleep(1e3);
990
+ const allPanes = await TmuxCommands.listPanes(session.tmuxSessionId);
991
+ const forkPaneId = allPanes[allPanes.length - 1];
992
+ await this.initializeClaude(forkPaneId, {
993
+ type: "resume",
994
+ resumeSessionId: fork.claudeSessionId,
995
+ sessionName: fork.name
996
+ });
997
+ fork.tmuxPaneId = forkPaneId;
998
+ fork.status = "active";
999
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
1000
+ await this.stateManager.replaceSession(session);
1001
+ logger.info(`Fork resumed: ${fork.name}`);
124
1002
  return fork;
1003
+ }
1004
+ /**
1005
+ * Cerrar un fork
1006
+ */
1007
+ async closeFork(sessionId, forkId) {
1008
+ const session = await this.getSession(sessionId);
1009
+ if (!session) {
1010
+ throw new Error(`Session ${sessionId} not found`);
1011
+ }
1012
+ const fork = session.forks.find((f) => f.id === forkId);
1013
+ if (!fork) {
1014
+ throw new Error(`Fork ${forkId} not found`);
1015
+ }
1016
+ logger.info(`Closing fork: ${fork.name}`);
1017
+ if (fork.tmuxPaneId) {
1018
+ await TmuxCommands.killPane(fork.tmuxPaneId);
1019
+ }
1020
+ fork.status = "closed";
1021
+ fork.tmuxPaneId = void 0;
1022
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
1023
+ await this.stateManager.replaceSession(session);
1024
+ logger.info(`Fork closed: ${fork.name}`);
1025
+ }
1026
+ /**
1027
+ * Eliminar un fork permanentemente
1028
+ */
1029
+ async deleteFork(sessionId, forkId) {
1030
+ const session = await this.getSession(sessionId);
1031
+ if (!session) {
1032
+ throw new Error(`Session ${sessionId} not found`);
1033
+ }
1034
+ const forkIndex = session.forks.findIndex((f) => f.id === forkId);
1035
+ if (forkIndex === -1) {
1036
+ throw new Error(`Fork ${forkId} not found`);
1037
+ }
1038
+ const fork = session.forks[forkIndex];
1039
+ logger.info(`Deleting fork: ${fork.name}`);
1040
+ if (fork.status === "active") {
1041
+ await this.closeFork(sessionId, forkId);
1042
+ }
1043
+ session.forks.splice(forkIndex, 1);
1044
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
1045
+ await this.stateManager.replaceSession(session);
1046
+ logger.info(`Fork deleted: ${fork.name}`);
1047
+ }
1048
+ // ==========================================
1049
+ // COMANDOS
1050
+ // ==========================================
1051
+ /**
1052
+ * Enviar comando a main
1053
+ */
1054
+ async sendToMain(sessionId, command) {
1055
+ const session = await this.getSession(sessionId);
1056
+ if (!session) {
1057
+ throw new Error(`Session ${sessionId} not found`);
1058
+ }
1059
+ if (!session.main.tmuxPaneId) {
1060
+ throw new Error("Main pane is not active");
1061
+ }
1062
+ logger.info(`Sending command to main: ${command}`);
1063
+ await TmuxCommands.sendKeys(session.main.tmuxPaneId, command);
1064
+ await TmuxCommands.sendEnter(session.main.tmuxPaneId);
1065
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
1066
+ await this.stateManager.replaceSession(session);
1067
+ }
1068
+ /**
1069
+ * Enviar comando a un fork
1070
+ */
1071
+ async sendToFork(sessionId, forkId, command) {
1072
+ const session = await this.getSession(sessionId);
1073
+ if (!session) {
1074
+ throw new Error(`Session ${sessionId} not found`);
1075
+ }
1076
+ const fork = session.forks.find((f) => f.id === forkId);
1077
+ if (!fork) {
1078
+ throw new Error(`Fork ${forkId} not found`);
1079
+ }
1080
+ if (!fork.tmuxPaneId) {
1081
+ throw new Error("Fork pane is not active");
1082
+ }
1083
+ logger.info(`Sending command to fork ${fork.name}: ${command}`);
1084
+ await TmuxCommands.sendKeys(fork.tmuxPaneId, command);
1085
+ await TmuxCommands.sendEnter(fork.tmuxPaneId);
1086
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
1087
+ await this.stateManager.replaceSession(session);
1088
+ }
1089
+ // ==========================================
1090
+ // EXPORT & MERGE
1091
+ // ==========================================
1092
+ /**
1093
+ * Generar export de un fork con resumen
1094
+ * Envía un prompt a Claude pidiendo que genere resumen y exporte
1095
+ */
1096
+ async generateForkExport(sessionId, forkId) {
1097
+ const session = await this.getSession(sessionId);
1098
+ if (!session) {
1099
+ throw new Error(`Session ${sessionId} not found`);
1100
+ }
1101
+ const fork = session.forks.find((f) => f.id === forkId);
1102
+ if (!fork) {
1103
+ throw new Error(`Fork ${forkId} not found`);
1104
+ }
1105
+ logger.info(`Generating export for fork: ${fork.name}`);
1106
+ const exportsDir = path4.join(this.projectPath, ".claude-orka", "exports");
1107
+ await fs4.ensureDir(exportsDir);
1108
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1109
+ const exportName = `fork-${fork.name}-${timestamp}.md`;
1110
+ const relativeExportPath = `.claude-orka/exports/${exportName}`;
1111
+ const absoluteExportPath = path4.join(this.projectPath, relativeExportPath);
1112
+ const prompt = `
1113
+ Please generate a complete summary of this fork conversation "${fork.name}" and save it to the file:
1114
+ \`${absoluteExportPath}\`
1115
+
1116
+ The summary should include:
1117
+
1118
+ ## Executive Summary
1119
+ - What was attempted to achieve in this fork
1120
+ - Why this exploration branch was created
1121
+
1122
+ ## Changes Made
1123
+ - Detailed list of changes, modified files, written code
1124
+ - Technical decisions made
1125
+
1126
+ ## Results
1127
+ - What works correctly
1128
+ - What problems were encountered
1129
+ - What remains pending
1130
+
1131
+ ## Recommendations
1132
+ - Suggested next steps
1133
+ - How to integrate this to main
1134
+ - Important considerations
1135
+
1136
+ Write the summary in Markdown format and save it to the specified file.
1137
+ `.trim();
1138
+ if (!fork.tmuxPaneId) {
1139
+ throw new Error("Fork pane is not active. Cannot send export command.");
1140
+ }
1141
+ await TmuxCommands.sendKeys(fork.tmuxPaneId, prompt);
1142
+ await TmuxCommands.sendEnter(fork.tmuxPaneId);
1143
+ fork.contextPath = relativeExportPath;
1144
+ await this.stateManager.replaceSession(session);
1145
+ logger.info(`Export generation requested`);
1146
+ logger.info(` Filename: ${exportName}`);
1147
+ logger.info(` Relative path (saved in state): ${relativeExportPath}`);
1148
+ logger.info(` Absolute path (sent to Claude): ${absoluteExportPath}`);
1149
+ logger.warn("IMPORTANT: Wait for Claude to complete before calling merge()");
1150
+ return relativeExportPath;
1151
+ }
1152
+ /**
1153
+ * Hacer merge de un fork a main
1154
+ * PREREQUISITO: Debes llamar a generateForkExport() primero y esperar
1155
+ */
1156
+ async mergeFork(sessionId, forkId) {
1157
+ const session = await this.getSession(sessionId);
1158
+ if (!session) {
1159
+ throw new Error(`Session ${sessionId} not found`);
1160
+ }
1161
+ const fork = session.forks.find((f) => f.id === forkId);
1162
+ if (!fork) {
1163
+ throw new Error(`Fork ${forkId} not found`);
1164
+ }
1165
+ if (!fork.contextPath) {
1166
+ throw new Error(
1167
+ "Fork does not have an exported context. Call generateForkExport() first."
1168
+ );
1169
+ }
1170
+ const parentId = fork.parentId;
1171
+ const parentName = parentId === "main" ? "MAIN" : session.forks.find((f) => f.id === parentId)?.name || parentId;
1172
+ logger.info(`Merging fork ${fork.name} to parent ${parentName}`);
1173
+ let parentTmuxPaneId;
1174
+ if (parentId === "main") {
1175
+ parentTmuxPaneId = session.main.tmuxPaneId;
1176
+ } else {
1177
+ const parentFork = session.forks.find((f) => f.id === parentId);
1178
+ if (!parentFork) {
1179
+ throw new Error(`Parent fork ${parentId} not found`);
1180
+ }
1181
+ parentTmuxPaneId = parentFork.tmuxPaneId;
1182
+ }
1183
+ if (!parentTmuxPaneId) {
1184
+ throw new Error(`Parent ${parentName} is not active. Cannot send merge command.`);
1185
+ }
1186
+ let contextPath = fork.contextPath;
1187
+ let fullPath = path4.join(this.projectPath, contextPath);
1188
+ let exists = await fs4.pathExists(fullPath);
1189
+ if (!exists) {
1190
+ logger.warn(`Export file not found: ${contextPath}. Looking for most recent export...`);
1191
+ const exportsDir = path4.join(this.projectPath, ".claude-orka", "exports");
1192
+ const files = await fs4.readdir(exportsDir);
1193
+ const forkExports = files.filter((f) => f.startsWith(`fork-${fork.name}-`) && f.endsWith(".md")).sort().reverse();
1194
+ if (forkExports.length > 0) {
1195
+ contextPath = `.claude-orka/exports/${forkExports[0]}`;
1196
+ fullPath = path4.join(this.projectPath, contextPath);
1197
+ exists = await fs4.pathExists(fullPath);
1198
+ logger.info(`Using most recent export: ${contextPath}`);
1199
+ }
1200
+ }
1201
+ if (!exists) {
1202
+ throw new Error(
1203
+ `No export file found for fork "${fork.name}". Please run Export first and wait for Claude to complete.`
1204
+ );
1205
+ }
1206
+ const mergePrompt = `
1207
+ I have completed work on the fork "${fork.name}".
1208
+ Please read the file \`${contextPath}\` which contains:
1209
+ 1. An executive summary of the work completed
1210
+ 2. The complete context of the fork conversation
1211
+
1212
+ Analyze the content and help me integrate the changes and learnings from the fork into this conversation.
1213
+ `.trim();
1214
+ await TmuxCommands.sendKeys(parentTmuxPaneId, mergePrompt);
1215
+ await TmuxCommands.sendEnter(parentTmuxPaneId);
1216
+ fork.status = "merged";
1217
+ fork.mergedToMain = true;
1218
+ fork.mergedAt = (/* @__PURE__ */ new Date()).toISOString();
1219
+ if (fork.tmuxPaneId) {
1220
+ await TmuxCommands.killPane(fork.tmuxPaneId);
1221
+ fork.tmuxPaneId = void 0;
1222
+ }
1223
+ session.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
1224
+ await this.stateManager.replaceSession(session);
1225
+ logger.info(`Fork ${fork.name} merged to main`);
1226
+ }
1227
+ /**
1228
+ * Export manual de un fork (deprecated - usa generateForkExport)
1229
+ */
1230
+ async exportFork(sessionId, forkId) {
1231
+ logger.warn("exportFork() is deprecated. Use generateForkExport() instead.");
1232
+ return await this.generateForkExport(sessionId, forkId);
1233
+ }
1234
+ // ==========================================
1235
+ // HELPERS PRIVADOS
1236
+ // ==========================================
1237
+ /**
1238
+ * Initialize Claude en un pane con prompt inicial
1239
+ */
1240
+ async initializeClaude(paneId, options) {
1241
+ const { type, sessionId, resumeSessionId, parentSessionId, sessionName, forkName } = options;
1242
+ await TmuxCommands.sendKeys(paneId, `cd ${this.projectPath}`);
1243
+ await TmuxCommands.sendEnter(paneId);
1244
+ await sleep(500);
1245
+ let command = "";
1246
+ switch (type) {
1247
+ case "new":
1248
+ const newPrompt = `Hello, this is a new main session called "${sessionName}". We are working on the project.`;
1249
+ command = `claude --session-id ${sessionId} "${newPrompt}"`;
1250
+ break;
1251
+ case "resume":
1252
+ const resumePrompt = `Resuming session "${sessionName}".`;
1253
+ command = `claude --resume ${resumeSessionId} "${resumePrompt}"`;
1254
+ break;
1255
+ case "fork":
1256
+ const forkPrompt = `This is a fork called "${forkName}". Keep in mind we are exploring an alternative to the main conversation.`;
1257
+ command = `claude --resume ${parentSessionId} --fork-session "${forkPrompt}"`;
1258
+ break;
1259
+ }
1260
+ logger.info(`Executing: ${command}`);
1261
+ await TmuxCommands.sendKeys(paneId, command);
1262
+ await TmuxCommands.sendEnter(paneId);
1263
+ await sleep(8e3);
1264
+ }
1265
+ /**
1266
+ * Launch Electron UI for a session
1267
+ */
1268
+ async launchUI(sessionId) {
1269
+ try {
1270
+ let electronPath;
1271
+ try {
1272
+ electronPath = require2("electron");
1273
+ } catch (error) {
1274
+ logger.warn("Electron not available, skipping UI launch");
1275
+ return;
1276
+ }
1277
+ let mainPath = path4.join(__dirname, "../../electron/main/main.js");
1278
+ if (!fs4.existsSync(mainPath)) {
1279
+ mainPath = path4.join(__dirname, "../../dist/electron/main/main.js");
1280
+ }
1281
+ if (!fs4.existsSync(mainPath)) {
1282
+ logger.warn(`Electron main.js not found at ${mainPath}, skipping UI launch`);
1283
+ return;
1284
+ }
1285
+ const electronProcess = spawn(
1286
+ electronPath,
1287
+ [mainPath, "--session-id", sessionId, "--project-path", this.projectPath],
1288
+ {
1289
+ detached: true,
1290
+ stdio: "ignore"
1291
+ }
1292
+ );
1293
+ electronProcess.unref();
1294
+ logger.info(`Launched UI for session ${sessionId}`);
1295
+ } catch (error) {
1296
+ logger.warn(`Failed to launch UI: ${error}`);
1297
+ }
1298
+ }
1299
+ };
1300
+
1301
+ // src/core/ClaudeOrka.ts
1302
+ var ClaudeOrka = class {
1303
+ sessionManager;
1304
+ /**
1305
+ * Create a ClaudeOrka instance
1306
+ * @param projectPath Absolute path to the project
1307
+ */
1308
+ constructor(projectPath) {
1309
+ logger.setLogFile(projectPath);
1310
+ this.sessionManager = new SessionManager(projectPath);
1311
+ }
1312
+ /**
1313
+ * Initialize ClaudeOrka
1314
+ * Creates the .claude-orka/ structure if it doesn't exist
1315
+ */
1316
+ async initialize() {
1317
+ logger.info("Initializing ClaudeOrka");
1318
+ await this.sessionManager.initialize();
1319
+ }
1320
+ // --- SESSIONS ---
1321
+ /**
1322
+ * Create a new Claude Code session
1323
+ * @param name Optional name for the session
1324
+ * @param openTerminal Whether to open a terminal window (default: true)
1325
+ * @returns Created session
1326
+ */
1327
+ async createSession(name, openTerminal) {
1328
+ return await this.sessionManager.createSession(name, openTerminal);
1329
+ }
1330
+ /**
1331
+ * Resume a saved session
1332
+ * @param sessionId Session ID to resume
1333
+ * @param openTerminal Whether to open a terminal window (default: true)
1334
+ * @returns Resumed session
1335
+ */
1336
+ async resumeSession(sessionId, openTerminal) {
1337
+ return await this.sessionManager.resumeSession(sessionId, openTerminal);
1338
+ }
1339
+ /**
1340
+ * Close a session
1341
+ * @param sessionId Session ID
1342
+ */
1343
+ async closeSession(sessionId) {
1344
+ await this.sessionManager.closeSession(sessionId);
1345
+ }
1346
+ /**
1347
+ * Permanently delete a session
1348
+ * @param sessionId Session ID
1349
+ */
1350
+ async deleteSession(sessionId) {
1351
+ await this.sessionManager.deleteSession(sessionId);
1352
+ }
1353
+ /**
1354
+ * List sessions with optional filters
1355
+ * @param filters Optional filters (status, name)
1356
+ * @returns Array of sessions
1357
+ */
1358
+ async listSessions(filters) {
1359
+ return await this.sessionManager.listSessions(filters);
1360
+ }
1361
+ /**
1362
+ * Get a session by ID
1363
+ * @param sessionId Session ID
1364
+ * @returns Session or null if not found
1365
+ */
1366
+ async getSession(sessionId) {
1367
+ return await this.sessionManager.getSession(sessionId);
1368
+ }
1369
+ /**
1370
+ * Get complete project summary
1371
+ * Includes statistics of all sessions and their forks
1372
+ * @returns Project summary with all sessions and statistics
1373
+ */
1374
+ async getProjectSummary() {
1375
+ const sessions = await this.sessionManager.listSessions();
1376
+ const state = await this.sessionManager.getState();
1377
+ const sessionSummaries = sessions.map((session) => {
1378
+ const forkSummaries = session.forks.map((fork) => ({
1379
+ id: fork.id,
1380
+ name: fork.name,
1381
+ claudeSessionId: fork.claudeSessionId,
1382
+ status: fork.status,
1383
+ createdAt: fork.createdAt,
1384
+ hasContext: !!fork.contextPath,
1385
+ contextPath: fork.contextPath,
1386
+ mergedToMain: fork.mergedToMain || false,
1387
+ mergedAt: fork.mergedAt
1388
+ }));
1389
+ const activeForks = session.forks.filter((f) => f.status === "active").length;
1390
+ const savedForks = session.forks.filter((f) => f.status === "saved").length;
1391
+ const mergedForks = session.forks.filter((f) => f.status === "merged").length;
1392
+ return {
1393
+ id: session.id,
1394
+ name: session.name,
1395
+ claudeSessionId: session.main.claudeSessionId,
1396
+ status: session.status,
1397
+ createdAt: session.createdAt,
1398
+ lastActivity: session.lastActivity,
1399
+ totalForks: session.forks.length,
1400
+ activeForks,
1401
+ savedForks,
1402
+ mergedForks,
1403
+ forks: forkSummaries
1404
+ };
1405
+ });
1406
+ const activeSessions = sessions.filter((s) => s.status === "active").length;
1407
+ const savedSessions = sessions.filter((s) => s.status === "saved").length;
1408
+ return {
1409
+ projectPath: state.projectPath,
1410
+ totalSessions: sessions.length,
1411
+ activeSessions,
1412
+ savedSessions,
1413
+ sessions: sessionSummaries,
1414
+ lastUpdated: state.lastUpdated
1415
+ };
1416
+ }
1417
+ // --- FORKS ---
1418
+ /**
1419
+ * Create a fork (conversation branch)
1420
+ * @param sessionId Session ID
1421
+ * @param name Optional fork name
1422
+ * @param parentId Parent fork/session ID (default: 'main')
1423
+ * @param vertical Whether to split vertically (default: false = horizontal)
1424
+ * @returns Created fork
1425
+ */
1426
+ async createFork(sessionId, name, parentId = "main", vertical) {
1427
+ return await this.sessionManager.createFork(sessionId, name, parentId, vertical);
1428
+ }
1429
+ /**
1430
+ * Close a fork
1431
+ * @param sessionId Session ID
1432
+ * @param forkId Fork ID
1433
+ */
1434
+ async closeFork(sessionId, forkId) {
1435
+ await this.sessionManager.closeFork(sessionId, forkId);
1436
+ }
1437
+ /**
1438
+ * Resume a saved fork
1439
+ * @param sessionId Session ID
1440
+ * @param forkId Fork ID
1441
+ * @returns Resumed fork
1442
+ */
1443
+ async resumeFork(sessionId, forkId) {
1444
+ return await this.sessionManager.resumeFork(sessionId, forkId);
1445
+ }
1446
+ /**
1447
+ * Permanently delete a fork
1448
+ * @param sessionId Session ID
1449
+ * @param forkId Fork ID
1450
+ */
1451
+ async deleteFork(sessionId, forkId) {
1452
+ await this.sessionManager.deleteFork(sessionId, forkId);
1453
+ }
1454
+ // --- COMANDOS ---
1455
+ /**
1456
+ * Send command to a session or fork
1457
+ * @param sessionId Session ID
1458
+ * @param command Command to send
1459
+ * @param target Fork ID (optional, if not specified goes to main)
1460
+ */
1461
+ async send(sessionId, command, target) {
1462
+ if (target) {
1463
+ await this.sessionManager.sendToFork(sessionId, target, command);
1464
+ } else {
1465
+ await this.sessionManager.sendToMain(sessionId, command);
1466
+ }
1467
+ }
1468
+ // --- EXPORT & MERGE ---
1469
+ /**
1470
+ * Export fork context (old method - uses manual capture)
1471
+ * @deprecated Use generateForkExport() instead for Claude to generate the summary
1472
+ * @param sessionId Session ID
1473
+ * @param forkId Fork ID
1474
+ * @returns Path to the exported file
1475
+ */
1476
+ async export(sessionId, forkId) {
1477
+ return await this.sessionManager.exportFork(sessionId, forkId);
1478
+ }
1479
+ /**
1480
+ * Generate fork export with summary
1481
+ *
1482
+ * Sends a prompt to Claude requesting:
1483
+ * 1. Generate executive summary of the conversation
1484
+ * 2. Export using /export to the specified path
1485
+ *
1486
+ * IMPORTANT: This method is async but returns immediately.
1487
+ * Claude will execute tasks in the background. Wait a few seconds before calling merge().
1488
+ *
1489
+ * @param sessionId Session ID
1490
+ * @param forkId Fork ID
1491
+ * @returns Path where Claude will save the export
1492
+ */
1493
+ async generateForkExport(sessionId, forkId) {
1494
+ return await this.sessionManager.generateForkExport(sessionId, forkId);
1495
+ }
1496
+ /**
1497
+ * Merge a fork to main
1498
+ *
1499
+ * PREREQUISITE: You must call generateForkExport() first and wait for Claude to complete
1500
+ *
1501
+ * @param sessionId Session ID
1502
+ * @param forkId Fork ID
1503
+ */
1504
+ async merge(sessionId, forkId) {
1505
+ await this.sessionManager.mergeFork(sessionId, forkId);
1506
+ }
1507
+ /**
1508
+ * Generate export and merge a fork to main (recommended method)
1509
+ *
1510
+ * Workflow:
1511
+ * 1. Generates export with summary (Claude does the work)
1512
+ * 2. Wait for the file to be created
1513
+ * 3. Merge to main
1514
+ *
1515
+ * @param sessionId Session ID
1516
+ * @param forkId Fork ID
1517
+ * @param waitTime Wait time in ms for Claude to complete (default: 15000)
1518
+ */
1519
+ async generateExportAndMerge(sessionId, forkId, waitTime = 15e3) {
1520
+ await this.generateForkExport(sessionId, forkId);
1521
+ logger.info(`Waiting ${waitTime}ms for Claude to complete export...`);
1522
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
1523
+ await this.merge(sessionId, forkId);
1524
+ }
1525
+ /**
1526
+ * Generate export, merge and close a fork (complete flow)
1527
+ *
1528
+ * @param sessionId Session ID
1529
+ * @param forkId Fork ID
1530
+ * @param waitTime Wait time in ms for Claude to complete (default: 15000)
1531
+ */
1532
+ async generateExportMergeAndClose(sessionId, forkId, waitTime = 15e3) {
1533
+ await this.generateExportAndMerge(sessionId, forkId, waitTime);
1534
+ }
1535
+ /**
1536
+ * Export and merge a fork to main (old method)
1537
+ * @deprecated Usa generateExportAndMerge() en su lugar
1538
+ */
1539
+ async exportAndMerge(sessionId, forkId) {
1540
+ await this.export(sessionId, forkId);
1541
+ await this.merge(sessionId, forkId);
1542
+ }
1543
+ /**
1544
+ * Export, merge and close a fork (old method)
1545
+ * @deprecated Usa generateExportMergeAndClose() en su lugar
1546
+ */
1547
+ async mergeAndClose(sessionId, forkId) {
1548
+ await this.exportAndMerge(sessionId, forkId);
1549
+ await this.closeFork(sessionId, forkId);
1550
+ }
1551
+ };
1552
+
1553
+ // electron/main/main.ts
1554
+ import chokidar from "chokidar";
1555
+ import execa2 from "execa";
1556
+ var __filename2 = fileURLToPath2(import.meta.url);
1557
+ var __dirname2 = path5.dirname(__filename2);
1558
+ var windows = /* @__PURE__ */ new Map();
1559
+ var currentSessionId = null;
1560
+ var currentProjectPath = null;
1561
+ function createWindow(sessionId, projectPath) {
1562
+ if (windows.has(projectPath)) {
1563
+ const existingWindow = windows.get(projectPath);
1564
+ existingWindow.focus();
1565
+ return existingWindow;
1566
+ }
1567
+ const projectName = path5.basename(projectPath);
1568
+ const mainWindow = new BrowserWindow({
1569
+ width: 600,
1570
+ height: 800,
1571
+ minWidth: 500,
1572
+ minHeight: 600,
1573
+ frame: false,
1574
+ transparent: true,
1575
+ alwaysOnTop: true,
1576
+ resizable: true,
1577
+ title: `Claude Orka - ${projectName}`,
1578
+ webPreferences: {
1579
+ preload: path5.join(__dirname2, "../preload/preload.js"),
1580
+ contextIsolation: true,
1581
+ nodeIntegration: false
1582
+ }
1583
+ });
1584
+ currentSessionId = sessionId;
1585
+ currentProjectPath = projectPath;
1586
+ if (process.env.NODE_ENV === "development") {
1587
+ mainWindow.loadURL("http://localhost:5173");
1588
+ } else {
1589
+ const indexPath = path5.join(__dirname2, "../renderer/index.html");
1590
+ mainWindow.loadFile(indexPath);
1591
+ }
1592
+ watchStateFile(projectPath, mainWindow);
1593
+ windows.set(projectPath, mainWindow);
1594
+ mainWindow.on("closed", () => {
1595
+ windows.delete(projectPath);
1596
+ });
1597
+ return mainWindow;
1598
+ }
1599
+ function watchStateFile(projectPath, window) {
1600
+ const statePath = path5.join(projectPath, ".claude-orka/state.json");
1601
+ const watcher = chokidar.watch(statePath, {
1602
+ persistent: true,
1603
+ ignoreInitial: true
1604
+ });
1605
+ watcher.on("change", async () => {
1606
+ try {
1607
+ const orka = new ClaudeOrka(projectPath);
1608
+ await orka.initialize();
1609
+ if (currentSessionId) {
1610
+ const session = await orka.getSession(currentSessionId);
1611
+ if (session) {
1612
+ window.webContents.send("state-updated", session);
1613
+ }
1614
+ }
1615
+ } catch (error) {
1616
+ console.error("Error watching state file:", error);
1617
+ }
1618
+ });
1619
+ window.on("closed", () => {
1620
+ watcher.close();
1621
+ });
1622
+ }
1623
+ ipcMain.handle("get-session", async () => {
1624
+ if (!currentSessionId || !currentProjectPath) {
1625
+ throw new Error("No active session");
1626
+ }
1627
+ const orka = new ClaudeOrka(currentProjectPath);
1628
+ await orka.initialize();
1629
+ const session = await orka.getSession(currentSessionId);
1630
+ return session;
125
1631
  });
126
- ipcMain.handle('export-fork', async (_, sessionId, forkId) => {
127
- if (!currentProjectPath) {
128
- throw new Error('No active project');
129
- }
130
- const orka = new ClaudeOrka(currentProjectPath);
131
- await orka.initialize();
132
- const summary = await orka.generateForkExport(sessionId, forkId);
133
- return summary;
1632
+ ipcMain.handle("select-node", async (_, nodeId) => {
1633
+ if (!currentSessionId || !currentProjectPath) {
1634
+ throw new Error("No active session");
1635
+ }
1636
+ const orka = new ClaudeOrka(currentProjectPath);
1637
+ await orka.initialize();
1638
+ const session = await orka.getSession(currentSessionId);
1639
+ if (!session) return;
1640
+ let tmuxPaneId;
1641
+ if (nodeId === "main") {
1642
+ tmuxPaneId = session.main?.tmuxPaneId;
1643
+ } else {
1644
+ const fork = session.forks.find((f) => f.id === nodeId);
1645
+ tmuxPaneId = fork?.tmuxPaneId;
1646
+ }
1647
+ if (tmuxPaneId) {
1648
+ await execa2("tmux", ["select-pane", "-t", tmuxPaneId]);
1649
+ }
134
1650
  });
135
- ipcMain.handle('merge-fork', async (_, sessionId, forkId) => {
136
- if (!currentProjectPath) {
137
- throw new Error('No active project');
138
- }
139
- const orka = new ClaudeOrka(currentProjectPath);
140
- await orka.initialize();
141
- await orka.generateExportAndMerge(sessionId, forkId);
1651
+ ipcMain.handle("create-fork", async (_, sessionId, name, parentId) => {
1652
+ if (!currentProjectPath) {
1653
+ throw new Error("No active project");
1654
+ }
1655
+ const orka = new ClaudeOrka(currentProjectPath);
1656
+ await orka.initialize();
1657
+ const fork = await orka.createFork(sessionId, name, parentId);
1658
+ return fork;
142
1659
  });
143
- ipcMain.on('close-window', (event) => {
144
- const window = BrowserWindow.fromWebContents(event.sender);
145
- window?.close();
1660
+ ipcMain.handle("export-fork", async (_, sessionId, forkId) => {
1661
+ if (!currentProjectPath) {
1662
+ throw new Error("No active project");
1663
+ }
1664
+ const orka = new ClaudeOrka(currentProjectPath);
1665
+ await orka.initialize();
1666
+ const summary = await orka.generateForkExport(sessionId, forkId);
1667
+ return summary;
146
1668
  });
147
- // App lifecycle
148
- app.whenReady().then(() => {
149
- // Get session ID and project path from command line args
150
- const args = process.argv.slice(2);
151
- const sessionIdIndex = args.indexOf('--session-id');
152
- const projectPathIndex = args.indexOf('--project-path');
153
- if (sessionIdIndex !== -1 && args[sessionIdIndex + 1]) {
154
- const sessionId = args[sessionIdIndex + 1];
155
- const projectPath = projectPathIndex !== -1 && args[projectPathIndex + 1]
156
- ? args[projectPathIndex + 1]
157
- : process.cwd();
158
- createWindow(sessionId, projectPath);
159
- }
160
- app.on('activate', () => {
161
- if (BrowserWindow.getAllWindows().length === 0) {
162
- // Don't create window on activate if no session
1669
+ ipcMain.handle("merge-fork", async (_, sessionId, forkId) => {
1670
+ if (!currentProjectPath) {
1671
+ throw new Error("No active project");
1672
+ }
1673
+ const orka = new ClaudeOrka(currentProjectPath);
1674
+ await orka.initialize();
1675
+ await orka.merge(sessionId, forkId);
1676
+ });
1677
+ ipcMain.handle("close-fork", async (_, sessionId, forkId) => {
1678
+ if (!currentProjectPath) {
1679
+ throw new Error("No active project");
1680
+ }
1681
+ const orka = new ClaudeOrka(currentProjectPath);
1682
+ await orka.initialize();
1683
+ await orka.closeFork(sessionId, forkId);
1684
+ });
1685
+ ipcMain.handle("open-export-file", async (_, exportPath) => {
1686
+ if (!currentProjectPath) {
1687
+ throw new Error("No active project");
1688
+ }
1689
+ const fullPath = path5.join(currentProjectPath, exportPath);
1690
+ await shell.openPath(fullPath);
1691
+ });
1692
+ ipcMain.handle("open-project-folder", async () => {
1693
+ if (!currentProjectPath) {
1694
+ throw new Error("No active project");
1695
+ }
1696
+ try {
1697
+ await execa2("cursor", [currentProjectPath]);
1698
+ return;
1699
+ } catch (error) {
1700
+ }
1701
+ try {
1702
+ await execa2("code", [currentProjectPath]);
1703
+ return;
1704
+ } catch (error) {
1705
+ }
1706
+ await shell.openPath(currentProjectPath);
1707
+ });
1708
+ ipcMain.handle("focus-terminal", async () => {
1709
+ const terminalApps = ["Terminal", "iTerm"];
1710
+ for (const app2 of terminalApps) {
1711
+ try {
1712
+ await execa2("osascript", ["-e", `tell application "${app2}" to activate`]);
1713
+ return;
1714
+ } catch (error) {
1715
+ }
1716
+ }
1717
+ try {
1718
+ await execa2("open", ["-a", "Terminal"]);
1719
+ } catch (error) {
1720
+ console.error("Failed to open terminal:", error);
1721
+ }
1722
+ });
1723
+ ipcMain.handle("save-and-close", async () => {
1724
+ if (currentSessionId && currentProjectPath) {
1725
+ try {
1726
+ const orka = new ClaudeOrka(currentProjectPath);
1727
+ await orka.initialize();
1728
+ const session = await orka.getSession(currentSessionId);
1729
+ if (session?.tmuxSessionId) {
1730
+ console.log("Save and close: detaching from tmux session:", session.tmuxSessionId);
1731
+ try {
1732
+ await execa2("tmux", ["detach-client", "-s", session.tmuxSessionId]);
1733
+ console.log("Detached from tmux session (session remains alive)");
1734
+ } catch (error) {
1735
+ console.log("Error detaching from tmux:", error);
163
1736
  }
164
- });
1737
+ await new Promise((resolve) => setTimeout(resolve, 300));
1738
+ try {
1739
+ await execa2("osascript", [
1740
+ "-e",
1741
+ `tell application "Terminal" to close (first window whose name contains "${session.tmuxSessionId}")`
1742
+ ]);
1743
+ console.log("Closed specific Terminal window");
1744
+ } catch (error) {
1745
+ console.log("Could not close specific window with AppleScript");
1746
+ try {
1747
+ const { stdout } = await execa2("osascript", [
1748
+ "-e",
1749
+ 'tell application "Terminal" to count windows'
1750
+ ]);
1751
+ const windowCount = parseInt(stdout.trim());
1752
+ if (windowCount === 1) {
1753
+ await execa2("osascript", ["-e", 'tell application "Terminal" to quit']);
1754
+ console.log("Quit Terminal (was last window)");
1755
+ } else {
1756
+ console.log("Multiple Terminal windows open, cannot close safely");
1757
+ }
1758
+ } catch (countError) {
1759
+ console.log("Could not determine Terminal window count:", countError);
1760
+ }
1761
+ }
1762
+ }
1763
+ } catch (error) {
1764
+ console.error("Error in save-and-close:", error);
1765
+ }
1766
+ }
1767
+ app.quit();
1768
+ });
1769
+ ipcMain.on("close-window", async (event) => {
1770
+ const window = BrowserWindow.fromWebContents(event.sender);
1771
+ window?.close();
165
1772
  });
166
- app.on('window-all-closed', () => {
167
- if (process.platform !== 'darwin') {
168
- app.quit();
1773
+ app.whenReady().then(() => {
1774
+ const args = process.argv.slice(2);
1775
+ const sessionIdIndex = args.indexOf("--session-id");
1776
+ const projectPathIndex = args.indexOf("--project-path");
1777
+ if (sessionIdIndex !== -1 && args[sessionIdIndex + 1]) {
1778
+ const sessionId = args[sessionIdIndex + 1];
1779
+ const projectPath = projectPathIndex !== -1 && args[projectPathIndex + 1] ? args[projectPathIndex + 1] : process.cwd();
1780
+ createWindow(sessionId, projectPath);
1781
+ }
1782
+ app.on("activate", () => {
1783
+ if (BrowserWindow.getAllWindows().length === 0) {
169
1784
  }
1785
+ });
1786
+ });
1787
+ app.on("window-all-closed", () => {
1788
+ app.quit();
170
1789
  });
171
- //# sourceMappingURL=main.js.map